20190724のPythonに関する記事は30件です。

pytorchで勾配法

はじめに

この記事は古川研究室 Workout calendar 9日目の記事です. 本記事は古川研究室の学生が学習の一環として書いたものです.

Workout_calendar盛り上がって嬉しいです。今週の金曜にいよいよ新入生の投稿が始まります!

本記事の目的

Pytorchの自動微分機能を使って, 勾配法を実現します!

[これを実装します]
anim_roll.gif

最急降下法

勾配法として最も単純な方法である。

勾配法は $n$次ベクトル $\mathbf{x} = (x_1,x_2,...,x_n)$を引数とする写像 $f(\mathbf{x})$ の極値を求める手法の一つである.

反復法, 学習を繰り返して $\mathbf{x}$を更新する.
$t$時刻目の学習の解が$\mathbf{x}^{(t)}$であるとき, 最急降下法では次のように値を更新する.

\begin{align}
\mathbf{x}^{(t+1)} &= \mathbf{x}^{(t)}-\eta \operatorname{grad} f\left(\mathbf{x}^{(t)}\right) \\ 
\\

 &=  \mathbf{x}^{(t)}-\eta \left[\begin{array}{c}{\frac{\partial f\left(\mathbf{x}^{(t)}\right)}{\partial x_{1}^{(t)}}} \\ {\frac{\partial f\left(\mathbf{x}^{(t)}\right)}{\partial x_{2}^{(t)}}} \\ {\frac{\partial f\left(\mathbf{x}^{(t)}\right)}{\partial x_{2}^{(k)}}} \\ ・\\ {\frac{\partial f\left(\mathbf{x}^{(t)}\right)}{\partial x_{n}^{(k)}}}\end{array}\right]

\end{align}

ここで$\eta$はステップ幅である. ステップ幅が大きくなると更新量は大きくなるが発散の恐れがある. 逆にステップ幅が小さすぎると学習は遅れてしまう.

実装

今回使う写像(関数): $$ E(\mathbf{x}) = x_1^2 + x_2^2$$

gradient_descent.py
n_epoch = 200 # 学習回数
eta = 0.01 # ステップ幅

x = torch.tensor([2.0,2.0],requires_grad=True) # (x_1, x_2)
A = torch.tensor([[1.0,0.0],[0.0,1.0]])

for epoch in range(n_epoch):
    quad = torch.einsum("i,ij,j",x,A,x) # f
    quad.backward() 
    with torch.no_grad(): 
        x = x - eta * x.grad
    x.requires_grad = True 

実装メモ

$\mathbf{x}$: optionにrequires_grad = Trueが必要.
$quad$ : 偏導関数はスカラー値である必要がある.

結果

青のマットは今回の関数の形で, 黒の $x$ は $t$ 時刻目の更新する点です. 黄色の点線は学習の軌跡になっています. 横軸(x,y)が$\mathbf{x}$の成分で, 縦軸(z)が関数の値になっています.
image.png

$(x_1, x_2) = (2.0, 2.0)$ を初期値として, 関数の最小値に向かっていることが確認できました.
anim_roll.gif

まとめ

次はこれと前回あげた Nadaraya-Watson推定 の実装を組み合わせて, 自分の研究の基盤技術を実現しようと思います.

参考ページ

Wiki

ソースコード

今回もGithubColabにソースコードあげております.
ビューワーありで動かしたい方はよかったら参考にしてください.

Special Thanks

コード提供: Watanabe さん

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AtCoder Beginners Selection 体験記

AtCoder Beginners Selection 体験記

AtCoder Beginner Contest に一度挑戦してみたいなーと思って、練習のために AtCoder Beginners Selection を解いてみた. 言語は、性能が辛ければ Go で、そうでなければ Python 3 にしようと思って提出フォームを見たら、選択肢に Python 2 があって、print にカッコつけるのダルいなと思って Python 2 になった(ダメ人間).

PracticeA - Welcome to AtCoder

8分で完了. どっかの記事で Python で AtCoder するときは入力を input() で受け取るというのを見ていて、使うの初めてながら使ってみたら SyntaxError: unexpected EOF while parsing が出て ??? ってなりながら raw_input() に変えて突破. 問題自体は簡単すぎて特に何も言うことはない.

a = int(raw_input())
b, c = [int(e) for e in raw_input().split()]
s = raw_input()
print "%d %s" % (a + b + c, s)

ABC086A - Product

2分で完了. 簡単すぎて特に何も言うことはない.

a, b = [int(e) for e in raw_input().split()]
if a * b % 2 == 0:
  print 'Even'
else:
  print 'Odd'

ABC081A - Placing Marbles

4分で完了. 簡単すぎて特に何も言うことはない.

print len([c for c in raw_input() if c == '1'])

ABC081B - Shift only

9分で完了. any の使い方を思い出せば特に難しいことはない.

n = int(raw_input())
a = [int(e) for e in raw_input().split()]
i = 0
while True:
  if any(e % 2 == 1 for e in a):
    break
  i += 1
  a = [e / 2 for e in a]
print i

ABC087B - Coins

4分で完了. まあ、総当たりでいいだろうで、終わりではある. 0..n なので range+ 1 するのを忘れなければよいだけ.

a = int(raw_input())
b = int(raw_input())
c = int(raw_input())
x = int(raw_input())
result = 0
for i in range(a + 1):
  for j in range(b + 1):
    for k in range(c + 1):
      if i * 500 + j * 100 + k * 50 == x:
        result += 1
print result

ABC083B - Some Sums

6分で完了. 各桁の和は一回文字列に変換して、各桁毎に数値に戻せばいいやで、後は総当りすれば OK. a <= x <= y みたいに書けるのは知ってたけど、初めて書いたかも. 普通の言語では a <= x and x <= y になるのにねー.

n, a, b = [int(e) for e in raw_input().split()]
result = 0
for i in range(1, n + 1):
  if a <= sum(int(e) for e in str(i)) <= b:
    result += i
print result

ABC088B - Card Game for Two

7分で完了. 最後の行に print を書き忘れて初の WA を食らう(しょぼーん). 要するに大きい方から順に並べて、Alice が偶数個目、Bob が奇数個目を取ると思えば、Python の強みであるスライス処理一発で終わるので、ちょー楽ちん.

n = int(raw_input())
a = [int(e) for e in raw_input().split()]
a.sort()
a.reverse()
print sum(a[::2]) - sum(a[1::2])

ABC085B - Kagami Mochi

3分で完了. 同じ直径のものは積めないってことは、要するに直径の unique を取ればよいわけで、Python は set に突っ込めば一発でそれを取れるので、後は set の要素数を取ってお終い.

n = int(raw_input())
d = [int(raw_input()) for i in range(n)]
print len(set(d))

ABC085C - Otoshidama

7分で完了. 最初 k = n + 1 - i - j になっていて WA. 総当りするだけなので特に難しいことはない.

import sys
n, y = [int(e) for e in raw_input().split()]
for i in range(n + 1):
  for j in range(n + 1 - i):
    k = n - i - j
    if 10000 * i + 5000 * j + 1000 * k == y:
      print "%d %d %d" % (i, j, k)
      sys.exit()
print "-1 -1 -1"

ABC049C - 白昼夢 / Daydream

1時間くらいで完了. 最初は再帰関数で書いて RE を食らい、あー maximum recursion depth exceeded かーと言いながらループ版に書き直し. これが TLE を食らいマジどーしよとなったけど、s.find(t + w) == 0 って O(n^2) じゃねと思い、O(n)s.startswith(t + w) ならどうだろうと試してみたら通った.

import sys
s = raw_input()
ts = ['']
while True:
  nts= []
  for t in ts:
    for w in ['dreamer', 'eraser', 'dream', 'erase']:
      if s == t + w:
        print 'YES'
        sys.exit()
      if s.startswith(t + w):
        nts.append(t + w)
  if len(nts) == 0:
    print 'NO'
    sys.exit()
  ts = nts

ABC086C - Traveling

40分くらいで完了. 到着した後の残り時間が偶数なら良いんでしょとさらさらっと書き上げて出したら WA. 延々考えてどう考えてもあってるやんけーってなった後に、前の問題と違って Yes / No が大文字小文字混じりであることに気づいた orz. しょうもねえミスすぎた.

import sys
n = int(raw_input())
data = [map(int, raw_input().split()) for i in range(n)]
t = 0
x = 0
y = 0
for d in data:
  duration = d[0] - t
  distance = abs(x - d[1]) + abs(y - d[2])
  if (distance > duration) or ((duration - distance) % 2 == 1):
    print 'No'
    sys.exit()
  t = d[0]
  x = d[1]
  y = d[2]
print 'Yes'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Apps Scriptで作った無料翻訳APIを使う

はじめに

初心者の備忘録です。
Pythonで翻訳できないかな~、Google Cloud Translationは有料か~、おっgoogletransとかいうモジュールがあるんか~→エラーが出て使えない...
こんな状況だったときに以下の記事を見つけました。
3 分で作る無料の翻訳 API with Google Apps Script

使い方

まず、記事の通りにウェブアプリケーションとして導入してURLが表示されるまで進めます。

アクセス方法は、 exec の後に ?text=翻訳したい文字列&source=翻訳前言語&target=翻訳後言語 を指定して GET リクエストする。

とあります。
試しに"cat"を翻訳してみました。
取得したURLはhttps://script.google.com/macros/s/AKfycbzPH7k0wm5QqP78MtGXt2cOZ_dR0X0G-jMxJODwZvQM3hG89Cct/exec
なので ?text=cat&source=en&target=ja を末尾に追加します。

アクセスしてソースを確認すると以下のようになっています。
cat_trans_html.jpg
"cat"が翻訳されて"ネコ"と表示されました。それ以外はなにもありません。

翻訳結果を取得する

GETリクエストを送ってページからデータを取得できます。
PythonではRequestsモジュールのrequests.get()でGETリクエストできます。
params=で指定することで取得するデータを決められますが、先ほど開いたページでは翻訳されたテキスト以外何もデータがないのでrequests.get()にURLだけを放り込んで全部取得します。

以下はhogeを受け取って、翻訳結果を返す関数です。

from Requests import get
def trans(hoge):
    trans_from = 'en'#翻訳元の言語(英語)
    trans_to = 'ja'  #翻訳先の言語(日本語)
    trans_url = (
        'https://script.google.com/macros/s/AKfycbzPH7k0wm5QqP78MtGXt2cOZ_dR0X0G-jMxJODwZvQM3hG89Cct/exec'
         + '?text=' + hoge + '&source=' + trans_from + '&target=' + trans_to
    )
    res = get(trans_url)
    translated_hoge = res.text
    return translated_hoge

returnの上2行でGETリクエストを送って、翻訳結果を取得しています。

    res = get(trans_url)
    print("res: {}" .format(res))                    #res: <Response [200]>
    translated_hoge = res.text
    print("res.text: {}" .foramt(translated_hoge))   #res.text: 翻訳された結果

getの戻り値はレスポンス内容が格納されたオブジェクトです。resを出力するとResponse [200]となります。
textという属性にページのhtmlが格納されているので、翻訳結果が欲しい場合はres.textとしなければなりません。

参考

  1. 3 分で作る無料の翻訳 API with Google Apps Script
  2. requestsの使い方 webサイトのデータを取得する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SESとPythonでメールの送信者が文字化けしないようにする

Pythonを使ってSESでメールを送る際、送信者(From)に日本語を使うと文字化けします。

https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/send-using-sdk-python.html

↑の公式サンプルコードをもとにメールを送信してみます。

文字化けするパターン

まずは何も考えず、そのまま宛先に日本語をセットしたパターンです。

ses_send.py
import boto3
from botocore.exceptions import ClientError

# Replace sender@example.com with your "From" address.
# This address must be verified with Amazon SES.
SENDER = "テスト <hoge+sender@gmail.com>"

# Replace recipient@example.com with a "To" address. If your account 
# is still in the sandbox, this address must be verified.
RECIPIENT = "fuga@gmail.com"

# If necessary, replace us-west-2 with the AWS Region you're using for Amazon SES.
AWS_REGION = "us-east-1"

# The subject line for the email.
SUBJECT = "Amazon SES テスト (SDK for Python)"

# The email body for recipients with non-HTML email clients.
BODY_TEXT = ("Amazon SES テスト (Python)\r\n"
             "This email was sent with Amazon SES using the "
             "AWS SDK for Python (Boto)."
            )

# The HTML body of the email.
BODY_HTML = """<html>
<head></head>
<body>
  <h1>Amazon SES テスト (SDK for Python)</h1>
  <p>This email was sent with
    <a href='https://aws.amazon.com/ses/'>Amazon SES</a> using the
    <a href='https://aws.amazon.com/sdk-for-python/'>
      AWS SDK for Python (Boto)</a>.</p>
</body>
</html>
            """            

# The character encoding for the email.
CHARSET = "UTF-8"

# Create a new SES resource and specify a region.
client = boto3.client('ses',region_name=AWS_REGION)

# Try to send the email.
try:
    #Provide the contents of the email.
    response = client.send_email(
        Destination={
            'ToAddresses': [
                RECIPIENT,
            ],
        },
        Message={
            'Body': {
                'Text': {
                    'Charset': CHARSET,
                    'Data': BODY_TEXT,
                },
            },
            'Subject': {
                'Charset': CHARSET,
                'Data': SUBJECT,
            },
        },
        Source=SENDER
    )
# Display an error if something goes wrong. 
except ClientError as e:
    print(e.response['Error']['Message'])
else:
    print("Email sent! Message ID:"),
    print(response['MessageId'])

実行します。

>python ses_send.py
Email sent! Message ID:
0100016c23fd79b6-efee9a81-212b-4c27-a5b7-81249789bdb0-000000

受信したメール。

MIME-Version: 1.0
Date: Wed, 24 Jul 2019 21:38:32 +0900
From: =?utf-8?Q?=C6=B9=EF=BF=BD?= <hoge+sender@gmail.com>
Subject: =?utf-8?Q?Amazon_SES_=E3=83=86=E3=82=B9=E3=83=88_(SDK_for_Python)?=
Thread-Topic:
 =?utf-8?Q?Amazon_SES_=E3=83=86=E3=82=B9=E3=83=88_(SDK_for_Python)?=
To: "fuga@gmail.com" <fuga@gmail.com>
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset="utf-8"

Amazon SES =E3=83=86=E3=82=B9=E3=83=88 (Python)
This email was sent with Amazon SES using the AWS SDK for Python (Boto).

同じテストという文言がFromの部分だけエンコードが違っています。
これは、テスト <hoge+sender@gmail.com>という文字列全体にマルチバイトを考慮しないエンコードがかかることで引き起こされる現象です。

このためメーラーで開いたときに送信者だけが化けてしまいます。

文字化けしないパターン

Fromにセットするときに、送信者とメールアドレスを分けてエンコードがすると文字化けしません。

先ほどのスクリプトに手を加えます。

ses_send.py
import boto3
from botocore.exceptions import ClientError
from email.header import Header # eemail.headerをインポート

# Replace sender@example.com with your "From" address.
# This address must be verified with Amazon SES.

# 送信者の名前とアドレスに変数を分ける
SENDER_NAME = "テスト"
SENDER_ADDR = "hoge+sender@gmail.com"

# Replace recipient@example.com with a "To" address. If your account 
# is still in the sandbox, this address must be verified.
RECIPIENT = "fuga@gmail.com"

# If necessary, replace us-west-2 with the AWS Region you're using for Amazon SES.
AWS_REGION = "us-east-1"

# The subject line for the email.
SUBJECT = "Amazon SES テスト (SDK for Python)"

# The email body for recipients with non-HTML email clients.
BODY_TEXT = ("Amazon SES テスト (Python)\r\n"
             "This email was sent with Amazon SES using the "
             "AWS SDK for Python (Boto)."
            )

# The HTML body of the email.
BODY_HTML = """<html>
<head></head>
<body>
  <h1>Amazon SES テスト (SDK for Python)</h1>
  <p>This email was sent with
    <a href='https://aws.amazon.com/ses/'>Amazon SES</a> using the
    <a href='https://aws.amazon.com/sdk-for-python/'>
      AWS SDK for Python (Boto)</a>.</p>
</body>
</html>
            """            

# The character encoding for the email.
CHARSET = "UTF-8"

# Create a new SES resource and specify a region.
client = boto3.client('ses',region_name=AWS_REGION)

# Try to send the email.
try:
    #Provide the contents of the email.
    response = client.send_email(
        Destination={
            'ToAddresses': [
                RECIPIENT,
            ],
        },
        Message={
            'Body': {
                'Text': {
                    'Charset': CHARSET,
                    'Data': BODY_TEXT,
                },
            },
            'Subject': {
                'Charset': CHARSET,
                'Data': SUBJECT,
            },
        },
        # Header関数を使いISO-2022-JPにエンコード
        Source='%s <%s>'%(Header(SENDER_NAME.encode('iso-2022-jp'),'iso-2022-jp').encode(),SENDER_ADDR)
    )
# Display an error if something goes wrong. 
except ClientError as e:
    print(e.response['Error']['Message'])
else:
    print("Email sent! Message ID:"),
    print(response['MessageId'])

これで実行します。

>python ses_send.py
Email sent! Message ID:
0100016c241237ba-ca70ada5-4add-4641-8f97-dfaf904f8de7-000000

受信したメール。

MIME-Version: 1.0
Date: Wed, 24 Jul 2019 22:01:11 +0900
From: =?utf-8?Q?=E3=83=86=E3=82=B9=E3=83=88?= <hoge+sender@gmail.com>
Subject: =?utf-8?Q?Amazon_SES_=E3=83=86=E3=82=B9=E3=83=88_(SDK_for_Python)?=
Thread-Topic:
 =?utf-8?Q?Amazon_SES_=E3=83=86=E3=82=B9=E3=83=88_(SDK_for_Python)?=
To: "fuga@gmail.com" <fuga@gmail.com>
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset="utf-8"

Amazon SES =E3=83=86=E3=82=B9=E3=83=88 (Python)
This email was sent with Amazon SES using the AWS SDK for Python (Boto).

先ほどとFromのエンコードが変わり、メーラでも日本語が化けずに表示されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonでソートアルゴリズムの再発明をしてみる: 選択ソート

概要

  • Pythonでソートアルゴリズムを再発明してみるだけ
  • 今回は選択ソートについて
  • 参考サイトのサンプルコードをできるだけ見ずに実装する
  • とりあえず昇順ソートのみの対応とする

選択ソートの概要

  • 要素を一通りなめて、最小だった値を左端の要素と交換するということを繰り返すアルゴリズム1

  • シンプルで実装が容易である一方、基本的に遅い

  • 不安定ソート

基本的な手順

  1. 左端の要素を「最小値」と見立てる
  2. 配列を左から走査して「最小値」と比較し、より小さい値があれば「最小値」を更新する。これを右端に到達するまで行う
  3. 左端の要素と最小値の位置を入れ替える(この時点では、「最小値」は本当の最小値になっているはず)
  4. 左端の要素は処理の対象外として、また手順1から繰り返す
  5. 全ての要素が処理対象外になったらソート完了

ソース

import random

def selection_sort(lst):
    # 処理全体のループ
    # iの値が更新されることで処理対象範囲が狭まる
    for i in range(len(lst)):
        # 処理対象範囲の中での左端を仮の「最小値」とする
        min_value = lst[i]
        min_index = i
        # 処理対象範囲の中での、本当の最小値を探すループ
        for j in range(i+1, len(lst)):
            if min_value > lst[j]:
                min_value = lst[j]
                min_index = j

        # 処理対象範囲の中での、左端と最小値を入れ替える
        lst[i], lst[min_index] = lst[min_index], lst[i]

lst = list(range(10))
random.shuffle(lst)

print("ソート前: " + str(lst))

selection_sort(lst)

print("ソート後: " + str(lst))

参考サイト


  1. 最大値を探して右端の要素と交換するという手順でも可能ですが、ここでは最小値でやることとします。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Connpass API でIT勉強会の情報を手軽にランダム1件検索

前書き

多種多様に開催されているIT勉強会。
自分は開催情報を知るきっかけの一つに「Connpass」をよく利用しています。

connpass - エンジニアをつなぐIT勉強会支援プラットフォーム
https://connpass.com/

IT勉強会を「Python」で検索してみました。
結果、251件ヒット。かなりの数のIT勉強会が開催されていますね。
スクリーンショット 2019-07-24 19.42.22.png

都道府県で絞ればもう少し数も絞れるとはいえ、
開催数が多くて、選び出すところでひと苦労してしまう。。。

自分はまさにそんな感じの人です orz

何がしたいのか

もっと積極的にIT勉強会へ参加する。
そのきっかけに「手軽なIT勉強会の検索処理」を作りたい!

やったこと

Connpass API からIT勉強会の情報を、
お手軽にランダム1件検索する処理を書いてみました。

なぜランダム1件検索?

何が検索されるか分からないガチャ的なwワクワク感と、
検索を敢えて1件に絞って情報量を減らすことで、
検索結果をしっかり読むきっかけにできないかと考えました。

Connpass API

Connpassはイベントサーチ用APIを提供しており、
誰でも利用することができるようになっています。
こちらを利用させていただきました。

Connpass API
https://connpass.com/about/api/

実行結果

先に処理の実行結果から。
pythonコマンドで該当処理を「検索キーワード」をつけて実行します。
検索キーワードを「Python」にしてみます。

python connpass_search.py python

実行結果はこんな感じになりました。
(一応伏せ字にしましたが、実際の実行結果は全ての情報が閲覧可能です。)

---------------------------------------------
# (・∀・) connpassイベント 何がでるかな?
# 機能:ランダムで1件 イベントを検索します
---------------------------------------------

検索中・・・

■ 検索結果
イベント名:Fintech,Pyt…気。。。略
開 催 日:2019-07-24T19:3。。。略
場   所:東京都港区。。。略
会   場:Cre。。。略
詳細ページ:https://cuc。。。略
参加者定員:17
参 加 者:7
補 欠 者:0
主催グループ名 :Cuc。。。略
主催グループURL:https://cuc。。。略

↑ピン!ときたら、ぜひ参加してみよう!
---------------------------------------------

また「検索キーワード」は「複数指定(AND検索)」もできます。
検索キーワードを「Python」「大阪」にしてみます。

python connpass_search.py python,大阪

実行結果はこんな感じになりました。
(一応伏せ字にしましたが、実際の実行結果は全ての情報が閲覧可能です。)

---------------------------------------------
# (・∀・) connpassイベント 何がでるかな?
# 機能:ランダムで1件 イベントを検索します
---------------------------------------------

検索中・・・

■ 検索結果
イベント名:阪医pyth。。。略
開 催 日:2019-07-26T1:。。。略
場   所:大阪府大阪市。。。略
会   場:PLU。。。略
詳細ページ:https://ou。。。略
参加者定員:30
参 加 者:17
補 欠 者:0
主催グループ名 :阪医pyth。。。略
主催グループURL:https://ou。。。略

↑ピン!ときたら、ぜひ参加してみよう!
---------------------------------------------

検索結果は原則、ランダムになります。
コマンドを叩くたびに違う勉強会の情報を読めるのは、ちょっと楽しい!

処理の実装

実装はこちらになります。
Python 3 で書いています。

import sys
import random
import requests
import time
from datetime import datetime


def main():
    args = sys.argv
    keywords = []
    ym = ""

    print('---------------------------------------------')
    print('# (・∀・) connpassイベント 何がでるかな?')
    print('# 機能:ランダムで1件 イベントを検索します')
    print('---------------------------------------------')

    keywords, ym = _check(args)
    print('')
    print('検索中・・・')
    print('')
    event_data = _req(keywords, ym, 10)
    if len(event_data['events']) == 1:
        print('■ 検索結果')
    else:
        time.sleep(1)
        event_data = _req(keywords, ym, 1)
        if len(event_data['events']) == 1:
            print('■ 検索結果')
        else:
            print('(*_*;) 検索失敗 該当するイベントが見つかりませんでした')
            exit()

    for event in event_data['events']:
        if event['catch'] is not None:
            print('イベント名:' + event['title'] + ' ' + event['catch'])
        else:
            print('イベント名:' + event['title'])
        if event['started_at'] is not None:
            print('開 催 日:' + event['started_at'])
        if event['address'] is not None:
            print('場   所:' + event['address'])
        if event['place'] is not None:
            print('会   場:' + event['place'])
        if event['event_url'] is not None:
            print('詳細ページ:' + event['event_url'])
        if event['limit'] is not None:
            print('参加者定員:' + str(event['limit']))
        if event['accepted'] is not None:
            print('参 加 者:' + str(event['accepted']))
        if event['waiting'] is not None:
            print('補 欠 者:' + str(event['waiting']))
        # if event['description'] is not None:
        #     print('概   要:' + event['description'])
        if event['series'] is not None:
            if event['series']['title'] is not None:
                print('主催グループ名 :' + event['series']['title'])
            if event['series']['url'] is not None:
                print('主催グループURL:' + event['series']['url'])
        print('')
        print('↑ピン!ときたら、ぜひ参加してみよう!')
        print('---------------------------------------------')


def _check(args):
    if len(args) == 1:
        print('(*_*;) INPUT ERROR:第1引数(検索キーワード)は必須です')
        exit()
    keywords = args[1]
    ym = datetime.now().strftime("%Y%m")
    if len(args) == 3:
        if (len(args[2]) == 6 or len(args[2]) == 8) and args[2].isdigit():
            ym = args[2]
        else:
            print('(*_*;) INPUT ERROR:第2引数(開催年月)の入力時は「yyyymm」または「yyyymmdd」の形式で入力してください')
            exit()
    return keywords, ym


def _req(keywords, ym, rand):
    params = {
        'keyword': keywords,
        'order': random.randint(1, 3),
        'start': random.randint(1, rand),
        'count': 1
    }
    if len(ym) == 6:
        params['ym'] = ym
    else:
        params['ymd'] = ym
    url = 'https://connpass.com/api/v1/event/'
    r = requests.get(url, params=params)
    return r.json()


if __name__ == '__main__':
    main()

実装詳細

まずはコマンドライン引数の仕様について。

上記例では「検索キーワード」だけを指定しましたが、
実際にはもう1つ「開催年月日」を指定できるようにしています。

「開催年月日」は毎回打つのが手間ですので、
省略時は「システム年月」を自動で設定するようにしました。
「開催年月日」を指定する時は「yyyymm」か「yyyymmdd」を入力します。

コマンドライン引数関連の処理

def _check(args):
    if len(args) == 1:
        print('(*_*;) INPUT ERROR:第1引数(検索キーワード)は必須です')
        exit()
    keywords = args[1]
    ym = datetime.now().strftime("%Y%m")
    if len(args) == 3:
        if (len(args[2]) == 6 or len(args[2]) == 8) and args[2].isdigit():
            ym = args[2]
        else:
            print('(*_*;) INPUT ERROR:第2引数(開催年月)の入力時は「yyyymm」または「yyyymmdd」の形式で入力してください')
            exit()
    return keywords, ym

続いて Connpass API へのリクエストについて。
ランダム1件検索と謳っておりますが、正しくは以下の仕様としています。

1. 「更新日時順」「開催日時順」「新着順」のいずれかランダムの検索で
   「検索上位10件から1件」をランダム出力する

2. 上記1で該当がない(検索結果が10件以下の場合にランダム指定した数が検索数以上だった時)は
   1秒待ってから「更新日時順」「開催日時順」「新着順」のいずれかランダムの検索で
   「検索上位1件」に絞って出力する

3. 上記2で該当がない場合は「検索失敗」とする

ですので、ピンポイントな「検索キーワード」を指定した場合は、
Connpass API へ2回リクエストを飛ばすこともある。という仕様になっています。

ConnpassAPI へのリクエストパラメータは params で定義しました。
概要はリファレンスを見ていただければ幸いです。
https://connpass.com/about/api/

(ちなみにリファレンスを見るとわかるのですが、
 先述の例で「Python」「大阪」で「大阪の勉強会」が検索できたケース。
 実際には「検索キーワード」は「イベント概要内の文字列」までを見にいきますので、
 「大阪」と入れたのに「東京の勉強会」がヒットした、といったケースもあり。
 (例:「東京の勉強会」だけど講師の方のプロフィールに「大阪出身」と書いていた…etc)
 何か良い解決方法はないものか・・・。)

ConnpassAPI へのリクエスト関連処理

    print('')
    print('検索中・・・')
    print('')
    event_data = _req(keywords, ym, 10)
    if len(event_data['events']) == 1:
        print('■ 検索結果')
    else:
        time.sleep(1)
        event_data = _req(keywords, ym, 1)
        if len(event_data['events']) == 1:
            print('■ 検索結果')
        else:
            print('(*_*;) 検索失敗 該当するイベントが見つかりませんでした')
            exit()

:
中略
:

def _req(keywords, ym, rand):
    params = {
        'keyword': keywords,
        'order': random.randint(1, 3),
        'start': random.randint(1, rand),
        'count': 1
    }
    if len(ym) == 6:
        params['ym'] = ym
    else:
        params['ymd'] = ym
    url = 'https://connpass.com/api/v1/event/'
    r = requests.get(url, params=params)
    return r.json()

そして最後に、検索結果の表示。
ここでは、やたら if で要素の存在確認を行っています。

その理由は、Connpass イベントの必須入力項目は、
イベントの「タイトル」のみだからです。
スクリーンショット 2019-07-24 20.32.37.png

だから「タイトル」以外はNULLがあり得る。チェックが必要と考えました。

それにしても、イベント名だけでイベント登録が完了する潔さ。
思わず自分もIT勉強会を企画してみようかと思ってしまう!
(って、そんな立派なネタないでしょorz)

検索結果の表示関連処理

    for event in event_data['events']:
        if event['catch'] is not None:
            print('イベント名:' + event['title'] + ' ' + event['catch'])
        else:
            print('イベント名:' + event['title'])
        if event['started_at'] is not None:
            print('開 催 日:' + event['started_at'])
        if event['address'] is not None:
            print('場   所:' + event['address'])
        if event['place'] is not None:
            print('会   場:' + event['place'])
        if event['event_url'] is not None:
            print('詳細ページ:' + event['event_url'])
        if event['limit'] is not None:
            print('参加者定員:' + str(event['limit']))
        if event['accepted'] is not None:
            print('参 加 者:' + str(event['accepted']))
        if event['waiting'] is not None:
            print('補 欠 者:' + str(event['waiting']))
        # if event['description'] is not None:
        #     print('概   要:' + event['description'])
        if event['series'] is not None:
            if event['series']['title'] is not None:
                print('主催グループ名 :' + event['series']['title'])
            if event['series']['url'] is not None:
                print('主催グループURL:' + event['series']['url'])
        print('')
        print('↑ピン!ときたら、ぜひ参加してみよう!')
        print('---------------------------------------------')

まとめ

ConnpassAPIを使って勉強会情報を検索するようにしてみました。
情報量を敢えて減らすことで、公式サイトでの検索とは違った楽しさを味わえればと感じた次第。
何より黒画面だから仕事中にこっそり検索できるwww

こちらの実装は、git hub でも公開中です。
git hub には本件を対話型のインプットで組んだ処理もあげております。
荒削りにも程がありますけど、ご興味ある方はぜひどうそ。
https://github.com/masashi-sawai/python-learning/tree/master/tools/conpass_search

そんなわけで、
みなさまご自身の強みにつながる「良い勉強会との出会い」に、
本投稿が少しでも役立てば幸いです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

connpass API でIT勉強会の情報を手軽にランダム1件検索

前書き

多種多様に開催されているIT勉強会。
自分は開催情報を知るきっかけの一つに「connpass」をよく利用しています。

connpass - エンジニアをつなぐIT勉強会支援プラットフォーム
https://connpass.com/

IT勉強会を「Python」で検索してみました。
結果、251件ヒット。かなりの数のIT勉強会が開催されていますね。
スクリーンショット 2019-07-24 19.42.22.png

都道府県で絞ればもう少し数も絞れるとはいえ、
開催数が多くて、選び出すところでひと苦労してしまう。。。

自分はまさにそんな感じの人です orz

何がしたいのか

もっと積極的にIT勉強会へ参加する。
そのきっかけに「手軽なIT勉強会の検索処理」を作りたい!

やったこと

connpass API からIT勉強会の情報を、
お手軽にランダム1件検索する処理を書いてみました。

なぜランダム1件検索?

何が検索されるか分からないガチャ的なwワクワク感と、
検索を敢えて1件に絞って情報量を減らすことで、
検索結果をしっかり読むきっかけにできないかと考えました。

connpass API

connpassはイベントサーチ用APIを提供しており、
誰でも利用することができるようになっています。
こちらを利用させていただきました。

connpass API
https://connpass.com/about/api/

実行結果

先に処理の実行結果から。
pythonコマンドで該当処理を「検索キーワード」をつけて実行します。
検索キーワードを「Python」にしてみます。

python connpass_search.py python

実行結果はこんな感じになりました。
(一応伏せ字にしましたが、実際の実行結果は全ての情報が閲覧可能です。)

---------------------------------------------
# (・∀・) connpassイベント 何がでるかな?
# 機能:ランダムで1件 イベントを検索します
---------------------------------------------

検索中・・・

■ 検索結果
イベント名:Fintech,Pyt…気。。。略
開 催 日:2019-07-24T19:3。。。略
場   所:東京都港区。。。略
会   場:Cre。。。略
詳細ページ:https://cuc。。。略
参加者定員:17
参 加 者:7
補 欠 者:0
主催グループ名 :Cuc。。。略
主催グループURL:https://cuc。。。略

↑ピン!ときたら、ぜひ参加してみよう!
---------------------------------------------

また「検索キーワード」は「複数指定(AND検索)」もできます。
検索キーワードを「Python」「大阪」にしてみます。

python connpass_search.py python,大阪

実行結果はこんな感じになりました。
(一応伏せ字にしましたが、実際の実行結果は全ての情報が閲覧可能です。)

---------------------------------------------
# (・∀・) connpassイベント 何がでるかな?
# 機能:ランダムで1件 イベントを検索します
---------------------------------------------

検索中・・・

■ 検索結果
イベント名:阪医pyth。。。略
開 催 日:2019-07-26T1:。。。略
場   所:大阪府大阪市。。。略
会   場:PLU。。。略
詳細ページ:https://ou。。。略
参加者定員:30
参 加 者:17
補 欠 者:0
主催グループ名 :阪医pyth。。。略
主催グループURL:https://ou。。。略

↑ピン!ときたら、ぜひ参加してみよう!
---------------------------------------------

検索結果は原則、ランダムになります。
コマンドを叩くたびに違う勉強会の情報を読めるのは、ちょっと楽しい!

実装

実装はこちらになります。
Python 3 で書いています。

import sys
import random
import requests
import time
from datetime import datetime


def main():
    args = sys.argv
    keywords = []
    ym = ""

    print('---------------------------------------------')
    print('# (・∀・) connpassイベント 何がでるかな?')
    print('# 機能:ランダムで1件 イベントを検索します')
    print('---------------------------------------------')

    keywords, ym = _check(args)
    print('')
    print('検索中・・・')
    print('')
    event_data = _req(keywords, ym, 10)
    if len(event_data['events']) == 1:
        print('■ 検索結果')
    else:
        time.sleep(1)
        event_data = _req(keywords, ym, 1)
        if len(event_data['events']) == 1:
            print('■ 検索結果')
        else:
            print('(*_*;) 検索失敗 該当するイベントが見つかりませんでした')
            exit()

    for event in event_data['events']:
        if event['catch'] is not None:
            print('イベント名:' + event['title'] + ' ' + event['catch'])
        else:
            print('イベント名:' + event['title'])
        if event['started_at'] is not None:
            print('開 催 日:' + event['started_at'])
        if event['address'] is not None:
            print('場   所:' + event['address'])
        if event['place'] is not None:
            print('会   場:' + event['place'])
        if event['event_url'] is not None:
            print('詳細ページ:' + event['event_url'])
        if event['limit'] is not None:
            print('参加者定員:' + str(event['limit']))
        if event['accepted'] is not None:
            print('参 加 者:' + str(event['accepted']))
        if event['waiting'] is not None:
            print('補 欠 者:' + str(event['waiting']))
        # if event['description'] is not None:
        #     print('概   要:' + event['description'])
        if event['series'] is not None:
            if event['series']['title'] is not None:
                print('主催グループ名 :' + event['series']['title'])
            if event['series']['url'] is not None:
                print('主催グループURL:' + event['series']['url'])
        print('')
        print('↑ピン!ときたら、ぜひ参加してみよう!')
        print('---------------------------------------------')


def _check(args):
    if len(args) == 1:
        print('(*_*;) INPUT ERROR:第1引数(検索キーワード)は必須です')
        exit()
    keywords = args[1]
    ym = datetime.now().strftime("%Y%m")
    if len(args) == 3:
        if (len(args[2]) == 6 or len(args[2]) == 8) and args[2].isdigit():
            ym = args[2]
        else:
            print('(*_*;) INPUT ERROR:第2引数(開催年月)の入力時は「yyyymm」または「yyyymmdd」の形式で入力してください')
            exit()
    return keywords, ym


def _req(keywords, ym, rand):
    params = {
        'keyword': keywords,
        'order': random.randint(1, 3),
        'start': random.randint(1, rand),
        'count': 1
    }
    if len(ym) == 6:
        params['ym'] = ym
    else:
        params['ymd'] = ym
    url = 'https://connpass.com/api/v1/event/'
    r = requests.get(url, params=params)
    return r.json()


if __name__ == '__main__':
    main()

実装詳細

まずはコマンドライン引数の仕様について。

上記例では「検索キーワード」だけを指定しましたが、
実際にはもう1つ「開催年月日」を指定できるようにしています。

「開催年月日」は毎回打つのが手間ですので、
省略時は「システム年月」を自動で設定するようにしました。
「開催年月日」を指定する時は「yyyymm」か「yyyymmdd」を入力します。

コマンドライン引数関連の処理

def _check(args):
    if len(args) == 1:
        print('(*_*;) INPUT ERROR:第1引数(検索キーワード)は必須です')
        exit()
    keywords = args[1]
    ym = datetime.now().strftime("%Y%m")
    if len(args) == 3:
        if (len(args[2]) == 6 or len(args[2]) == 8) and args[2].isdigit():
            ym = args[2]
        else:
            print('(*_*;) INPUT ERROR:第2引数(開催年月)の入力時は「yyyymm」または「yyyymmdd」の形式で入力してください')
            exit()
    return keywords, ym

続いて connpass API へのリクエストについて。
ランダム1件検索と謳っておりますが、正しくは以下の仕様としています。

1. 「更新日時順」「開催日時順」「新着順」のいずれかランダムの検索で
   「検索上位10件から1件」をランダム出力する

2. 上記1で該当がない(検索結果が10件以下の場合にランダム指定した数が検索数以上だった時)は
   1秒待ってから「更新日時順」「開催日時順」「新着順」のいずれかランダムの検索で
   「検索上位1件」に絞って出力する

3. 上記2で該当がない場合は「検索失敗」とする

ですので、ピンポイントな「検索キーワード」を指定した場合は、
connpass API へ2回リクエストを飛ばすこともある。という仕様になっています。

connpassAPI へのリクエストパラメータは params で定義しました。
概要はリファレンスを見ていただければ幸いです。
https://connpass.com/about/api/

(ちなみにリファレンスを見るとわかるのですが、
 先述の例で「Python」「大阪」で「大阪の勉強会」が検索できたケース。
 実際には「検索キーワード」は「イベント概要内の文字列」までを見にいきますので、
 「大阪」と入れたのに「東京の勉強会」がヒットした、といったケースもあり。
 (例:「東京の勉強会」だけど講師の方のプロフィールに「大阪出身」と書いていた…etc)
 何か良い解決方法はないものか・・・。)

connpassAPI へのリクエスト関連処理

    print('')
    print('検索中・・・')
    print('')
    event_data = _req(keywords, ym, 10)
    if len(event_data['events']) == 1:
        print('■ 検索結果')
    else:
        time.sleep(1)
        event_data = _req(keywords, ym, 1)
        if len(event_data['events']) == 1:
            print('■ 検索結果')
        else:
            print('(*_*;) 検索失敗 該当するイベントが見つかりませんでした')
            exit()

:
中略
:

def _req(keywords, ym, rand):
    params = {
        'keyword': keywords,
        'order': random.randint(1, 3),
        'start': random.randint(1, rand),
        'count': 1
    }
    if len(ym) == 6:
        params['ym'] = ym
    else:
        params['ymd'] = ym
    url = 'https://connpass.com/api/v1/event/'
    r = requests.get(url, params=params)
    return r.json()

そして最後に、検索結果の表示。
ここでは、やたら if で要素の存在確認を行っています。

その理由は、connpass イベントの必須入力項目は、
イベントの「タイトル」のみだからです。
スクリーンショット 2019-07-24 20.32.37.png

だから「タイトル」以外はNULLがあり得る。チェックが必要と考えました。

それにしても、イベント名だけでイベント登録が完了する潔さ。
思わず自分もIT勉強会を企画してみようかと思ってしまう!
(って、そんな立派なネタないでしょorz)

検索結果の表示関連処理

    for event in event_data['events']:
        if event['catch'] is not None:
            print('イベント名:' + event['title'] + ' ' + event['catch'])
        else:
            print('イベント名:' + event['title'])
        if event['started_at'] is not None:
            print('開 催 日:' + event['started_at'])
        if event['address'] is not None:
            print('場   所:' + event['address'])
        if event['place'] is not None:
            print('会   場:' + event['place'])
        if event['event_url'] is not None:
            print('詳細ページ:' + event['event_url'])
        if event['limit'] is not None:
            print('参加者定員:' + str(event['limit']))
        if event['accepted'] is not None:
            print('参 加 者:' + str(event['accepted']))
        if event['waiting'] is not None:
            print('補 欠 者:' + str(event['waiting']))
        # if event['description'] is not None:
        #     print('概   要:' + event['description'])
        if event['series'] is not None:
            if event['series']['title'] is not None:
                print('主催グループ名 :' + event['series']['title'])
            if event['series']['url'] is not None:
                print('主催グループURL:' + event['series']['url'])
        print('')
        print('↑ピン!ときたら、ぜひ参加してみよう!')
        print('---------------------------------------------')

まとめ

connpassAPIを使って勉強会情報を検索するようにしてみました。
情報量を敢えて減らすことで、公式サイトでの検索とは違った楽しさを味わえればと感じた次第。
何より黒画面だから仕事中にこっそり検索できるwww

こちらの実装は、git hub でも公開中です。
git hub には本件を対話型のインプットで組んだ処理もあげております。
荒削りにも程がありますけど、ご興味ある方はぜひどうそ。
https://github.com/masashi-sawai/python-learning/tree/master/tools/conpass_search

そんなわけで、
みなさまご自身の強みにつながる「良い勉強会との出会い」に、
本投稿が少しでも役立てば幸いです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初心者向け]稀有だと思うけど、外部ライブラリ使わずに表形式加工

タイトルの通りになりますが、お仕事の内容によってはこの様なレアなケースもあるかと思います。
使った構文等をメモがてらに残していこうと思います。
Pythonのバージョンは3.6です。
Python初心者のため、間違い等ありましたら、コメント頂けますと幸いです。

目標

2つの表形式の.tsvファイルから必要なデータを取り出し1つの表形式データにしたい。

importしたもの

import.py
import gzip #圧縮ファイルを処理する場合必要
import csv #csvやtsvで書き込む場合必要
import sys #コマンド実行時 引数をとる場合必要

ファイル読み込み

2種類の方法を検討しました

open関数

open.py
#読み込みモードでファイルを開く
file = open("data.tsv","rt")

###処理等###

#ファイルを閉じる
file.close()

詳細のモードの設定など
この場合、テキストでの読み込みをしています。
モードを指定することで書き込みなどもできる様になります。
close()を忘れずに。

with open

withopen.py
with open("data.tsv", "rt") as file

大きな違いはclose()が必要なく、勝手にファイルをクローズしてくれます。
with構文は通信やデータベースへのアクセスにも使えて、これらも勝手にクローズや終了をしてくれます。
こちらもモードを指定することで書き込みもできます。
今回はこちらを採用いたしました。

実行時の引数を取得

pythonの実行時に引数を入れることでそれに応じた処理をさせたい時に利用できます。

例えばこんな感じで実行

$ python sys.py 12 24 36
sys.py
args = sys.argv #宣言 引数2個以上の場合、配列になる 
#args[0]にはスクリプトファイル名が入るため、args[1]から取得していく
num1 = args[1] #num1 = 12
num2 = args[2] #num2 = 24
num3 = args[3] #num3 = 36

必要な列のデータを取得

表形式なので、表を縦に分割して、必要な列を取り出します。
file1:

ア行 カ行 サ行

file2:

サ行 タ行 ナ行

これはテーブル型なので、少しややこしいですが、データはtsvなので、表形式でタブ区切りのものです。
そのタブを区切り文字として指定することで列を取得できます。
列はカラムを含めると4つデータを取得する必要がありますので、for文で繰り返し処理をします。
今回はfile1からア行とサ行を、file2からサ行とナ行を取得します。

split.py
data1 = []
data2 = []
data3 = []
data4 = []

for line1 in file1:
    list1 = file1.split('\t') #タブを区切り文字にして分割
    data1.append(list1[1]) #[あ、い、う]
    data2.append(list1[3]) #[さ、し、す]

for line2 in file2:
    list2 = file2.split('\t')
    data3.append(list2[1]) #[さ、す、せ]
    data4.append(list2[3]) #[な、ぬ、ね]

for文の条件にfileを指定するとファイルの中身が終わるまで処理をしてくれます。
dataは配列で入るため、事前に宣言をしています。

表形式結合

SQLでテーブルを結合させる際は、共通のデータを照らし合わせます。
その様な動きを条件文を使ってやってみます。
file1から取得したデータとfile2から取得したデータをそれぞれfor文で回し、データの内容が一致するものを格納する流れにします。
今回はサ行のデータを条件にします。

join.py
table = []

for i in range(len(data1)): #4 range(len('変数名'))でデータの数を返します。
    for j in range(len(data3)):
        if data2[i] == data3[j]:
            table.append([data1[i], data2[i], data4[j])
ア行 サ行 ナ行

この様に表形式のデータを結合することができました。

ファイル書き込み

このデータを新たなファイルに書き込んでいきます。

write.py
with open.gzip("data.tsv.gz", "rt") as file:
    write = csv.writer(file, lineterminator = '\n', delimiter = '\t')
    for output_data in table:
        writer.writerow(output_data)

最初にcsvライブラリをimportしましたが、ここで登場します。
Pythonの標準ライブラリにcsvモジュールはcsvやtsvで読み込みや書き込みを行うことが可能です。
csvライブラリ
区切り文字(delimiter)をタブにすることでtsvファイルでの保存が可能になります。
lineterninatorは改行文字です。

また、with open.gzipとすることでgzファイルに圧縮することも可能です。

まとめ

標準ライブラリの使用のみで表形式データを加工することができました。
結合以外にも変換や、datatime処理をしましたが、それはまた書く気になったら別で書きます。

僕自身、初めてのPythonだったのですが、配列関係、インデント関係のエラーで結構詰まりました。
その辺りは他の丁寧に書かれている記事を参考にしてください(逃げます)

最後に.....
表形式を加工する場合、よっぽどの理由がない限りは、外部ライブラリのpandasやNumpyの使用をオススメします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[備忘録]MoveIt!の使い方について

テスト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[備忘録]MoveIt!の使い方

はじめに

 この記事は,ここ数年でかなり使われるようになってきた,ロボットの動作計画ライブラリMoveIt!について紹介していきます.基本的には,マニピュレータを使ったものについて解説していきたいと思います.ここでは,ROSの基本構造は理解していること,ROSのチュートリアルは終了していることにします.まだインストールしてない方は,http://wiki.ros.org/ja から始めてみましょう.

MoveIt!を始めよう!

 ここでは,OSをUbuntu 16.04 LTS,ROSのバージョンはKineticとなっています.バージョンが違うとコードも若干変わってくるのでKineticを前提として話を進めていきます.Ubuntu18.04のROS Melodic は環境が整えば書きたいと思います.なお参考文献はROS Melodicを前提に解説してあります.

既存のROSパッケージを更新する

rosdep update
sudo apt-get update

MoveIt!のインストール

sudo apt install ros-kinetic-moveit

ワークスペースを作る

sudo apt install ros-kinetic-moveit

GitHubからパッケージをダウンロード

 GitHubにチュートリアル用のパッケージをダウンロードします.オプション-bの後にバージョンを指定します.

cd ~/ws_moveit/src
git clone https://github.com/ros-planning/moveit_tutorials.git -b kinetic-devel
git clone https://github.com/ros-planning/panda_moveit_config.git -b kinetic-devel

ワークスペースのビルド

 依存関係にあるパッケージをまとめてインストールします.オプション--rosdistroの後にバージョンを指定します.

cd ~/ws_moveit/src
rosdep install -y --from-paths . --ignore-src --rosdistro kinetic

 パッケージをビルドします.

cd ~/ws_moveit/
catkin_make

 環境設定ファイルを適用します.

source ~/ws_moveit/devel/setup.bash

 さあ,これでMoveIt!が使えるようになります.ビルドに失敗している場合は,足りないパッケージをインストールするなどのコードを実行する必要があります.

参考文献

https://moveit.ros.org/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MoveIt!の使い方

はじめに

 この記事は,ここ数年でかなり使われるようになってきた,ロボットの動作計画ライブラリMoveIt!について紹介していきます.基本的には,マニピュレータを使ったものについて解説していきたいと思います.ここでは,ROSの基本構造は理解していること,ROSのチュートリアルは終了していることにします.まだインストールしてない方は,http://wiki.ros.org/ja から始めてみましょう.

MoveIt!を始めよう!

 ここでは,OSをUbuntu 16.04 LTS,ROSのバージョンはKineticとなっています.バージョンが違うとコードも若干変わってくるのでKineticを前提として話を進めていきます.Ubuntu18.04のROS Melodic は環境が整えば書きたいと思います.なお参考文献はROS Melodicを前提に解説してあります.

既存のROSパッケージを更新する

rosdep update
sudo apt-get update

MoveIt!のインストール

sudo apt install ros-kinetic-moveit

ワークスペースを作る

mkdir -p ~/ws_moveit/src

GitHubからパッケージをダウンロード

 GitHubにチュートリアル用のパッケージをダウンロードします.オプション-bの後にバージョンを指定します.

cd ~/ws_moveit/src
git clone https://github.com/ros-planning/moveit_tutorials.git -b kinetic-devel
git clone https://github.com/ros-planning/panda_moveit_config.git -b kinetic-devel

ワークスペースのビルド

 依存関係にあるパッケージをまとめてインストールします.オプション--rosdistroの後にバージョンを指定します.

cd ~/ws_moveit/src
rosdep install -y --from-paths . --ignore-src --rosdistro kinetic

 パッケージをビルドします.

cd ~/ws_moveit/
catkin_make

 環境設定ファイルを適用します.

source ~/ws_moveit/devel/setup.bash

 さあ,これでMoveIt!が使えるようになります.ビルドに失敗している場合は,足りないパッケージをインストールするなどのコードを実行する必要があります.

参考文献

https://moveit.ros.org/
https://ros-planning.github.io/moveit_tutorials/doc/getting_started/getting_started.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2を特定の時間に自動で起動/停止する

概要

いったい何番煎じだよという感じですが、Lambda上にデプロイしたコードから、EC2のインスタンスを定期的に起動/停止させます。

今の時代、こちらの記事にある通り、プログラムを書かずともLambdaの定期起動/停止ができてしまいます。
それでも、本記事のコードをデプロイした後であれば、EC2インスタンスを立てるたびにCloudWatchの設定をいじる必要もなく、立てたEC2インスタンスにタグを追加するだけで自動起動/停止の設定ができるようになり、便利です。

Lambdaの設定

本記事のコードは、Lambdaに設定されているタグを見て、インスタンスの起動/停止を行います。そのため、自動設定対象にしたいEC2インスタンスには、以下のようにstart-time,stop-timeのタグをつけます。それぞれ、起動時間と停止時間をHHMM形式で表します。
無題.png

コード

以下のコードを、Lambdaにデプロイします。もちろん、EC2を起動や停止するための、適当なIAMロールをつけます。

manage_ec2.py
import boto3;
import datetime;

# 最初に呼ばれる関数
def lambda_handler(event, context):
    client = boto3.client('ec2', 'ap-northeast-1')
    # boto3でec2の全インスタンスの情報を取得
    response = client.describe_instances()
    # 必要な情報のみを抜き出したリストを作成
    instance_list = extract_instance_info(response)
    # 上のリストを基に、必要であればインスタンスを起動/停止する
    manage_instances(instance_list)

# ec2インスタンスのリストを作成
def extract_instance_info(response):
    return_val = []
    for instance_data in response['Reservations'][0]['Instances']:
        try:
            tmp = {}
            # インスタンスのid
            tmp['id'] = instance_data['InstanceId']
            # タグで指定されている起動時間
            tmp['start_time'] = convert_time([tag['Value'] for tag in instance_data['Tags'] if tag['Key'] == 'start-time'][0])
            # タグで指定されている終了時間
            tmp['stop_time'] = convert_time([tag['Value'] for tag in instance_data['Tags'] if tag['Key'] == 'stop-time'][0])
            # 現在のインスタンスの起動状況
            tmp['status'] = instance_data['State']['Name']
            return_val.append(tmp)
        except:
            # かなり乱暴なのは承知ですが、自動起動/停止対象外のインスタンス(タグが設定されていないもの)があれば、例外を発生させて無視します
            continue
    print(return_val)
    return return_val

# タグで指定されている起動/停止時間を、datetime.time型に変換
def convert_time(str_time):
    hour = int(str_time[:2])
    min = int(str_time[2:4])
    return datetime.time(hour,min)

# インスタンスの起動/停止を行う
def manage_instances(instance_list):
    # 現在時刻をdatetime.time型で取得
    current_time = datetime.datetime.now().time()
    for instance in instance_list:
        # インスタンス停止中で、起動時間を過ぎていれば、インスタンスを立ち上げる
        if instance['status'] != 'running' and current_time >= instance['start_time']:
            response = ec2.start_instances(
                InstanceIds=[
                    instance['id']
                ]
            )
            print(response)
        # インスタンス起動中で、停止時間を過ぎていれば、インスタンスを停止する
        elif instance['status'] == 'running' and current_time <= instance['stop_time']:
            response = ec2.stop_instances(
                InstanceIds=[
                    instance['id']
                ]
            )
            print(response)
        else:
            # 何もすることないけど、ログだけ出しとく
            print('no action was taken')

このコードをデプロイしたLambda関数を、CloudWatchで30分おきなどで実行させれば、完成です。

上のコードでは現在時刻を取得するコードが含まれているので、Lambdaの環境変数に、必ずTZ:Asia/Tokyoを付け加えてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2をタグに基づいて、特定の時間に自動で起動/停止する

概要

いったい何番煎じだよという感じですが、Lambda上にデプロイしたコードから、EC2のインスタンスを定期的に起動/停止させます。

今の時代、こちらの記事にある通り、プログラムを書かずともLambdaの定期起動/停止ができてしまいます。
それでも、本記事のコードをデプロイした後であれば、EC2インスタンスを立てるたびにCloudWatchの設定をいじる必要もなく、インスタンスにタグを追加するだけで自動起動/停止の設定ができるようになり、便利です。

Lambdaの設定

本記事のコードは、EC2に設定されているタグを見て、インスタンスの起動/停止を行います。そのため、自動設定対象にしたいEC2インスタンスには、以下のようにstart-time,stop-timeのタグをつけます。それぞれ、起動時間と停止時間をHHMM形式で表します。
無題.png

コード

以下のコードを、Lambdaにデプロイします。もちろん、LambdaにはEC2を起動や停止するための、適当なIAMロールをつけます。

manage_ec2.py
import boto3;
import datetime;

# 最初に呼ばれる関数
def lambda_handler(event, context):
    client = boto3.client('ec2', 'ap-northeast-1')
    # boto3でec2の全インスタンスの情報を取得
    response = client.describe_instances()
    # 必要な情報のみを抜き出したリストを作成
    instance_list = extract_instance_info(response)
    # 上のリストを基に、必要であればインスタンスを起動/停止する
    manage_instances(instance_list)

# ec2インスタンスのリストを作成
def extract_instance_info(response):
    return_val = []
    for instance_data in response['Reservations'][0]['Instances']:
        try:
            tmp = {}
            # インスタンスのid
            tmp['id'] = instance_data['InstanceId']
            # タグで指定されている起動時間
            tmp['start_time'] = convert_time([tag['Value'] for tag in instance_data['Tags'] if tag['Key'] == 'start-time'][0])
            # タグで指定されている終了時間
            tmp['stop_time'] = convert_time([tag['Value'] for tag in instance_data['Tags'] if tag['Key'] == 'stop-time'][0])
            # 現在のインスタンスの起動状況
            tmp['status'] = instance_data['State']['Name']
            return_val.append(tmp)
        except:
            # かなり乱暴なのは承知ですが、自動起動/停止対象外のインスタンス(タグが設定されていないもの)があれば、例外を発生させて無視します
            continue
    print(return_val)
    return return_val

# タグで指定されている起動/停止時間を、datetime.time型に変換
def convert_time(str_time):
    hour = int(str_time[:2])
    min = int(str_time[2:4])
    return datetime.time(hour,min)

# インスタンスの起動/停止を行う
def manage_instances(instance_list):
    # 現在時刻をdatetime.time型で取得
    current_time = datetime.datetime.now().time()
    for instance in instance_list:
        # インスタンス停止中で、起動時間を過ぎていれば、インスタンスを立ち上げる
        if instance['status'] != 'running' and current_time >= instance['start_time']:
            response = ec2.start_instances(
                InstanceIds=[
                    instance['id']
                ]
            )
            print(response)
        # インスタンス起動中で、停止時間を過ぎていれば、インスタンスを停止する
        elif instance['status'] == 'running' and current_time <= instance['stop_time']:
            response = ec2.stop_instances(
                InstanceIds=[
                    instance['id']
                ]
            )
            print(response)
        else:
            # 何もすることないけど、ログだけ出しとく
            print('no action was taken')

このコードをデプロイしたLambda関数を、CloudWatchで30分おきなどで実行させれば、完成です。

上のコードでは現在時刻を取得するコードが含まれているので、Lambdaの環境変数に、必ずTZ:Asia/Tokyoを付け加えてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

COTOHAで文章の類似度を取得してみる

COTOHA

公式サイト
image.png

実装

  • COTOHAというNTTが提供している自然言語系のAPIを触ってみます。
  • 文章と文章の類似度を算出するAPIを使います。
  • Ipython形式で実装します。

ライブラリのインポート

import json
import numpy as np
import pandas as pd
import urllib.request
from os import path

情報の定義

  • アカウントホームのデータを定義する。
  • Developer API Base URLAccess Token Publish URLは全ユーザー共通だと思いますが、念のため各自登録してお調べください。 スクリーンショット_2019-07-24_8_33_45.png
base_url = [Developer API Base URL]
developer_id = [Developer Client id]
secret = [Developer Client secret]
token_url = [Access Token Publish URL]
    'Content-Type': 'application/json',
}

アクセストークンの取得

  • アクセストークンを取得するため、POSTでアクセストークン取得用のAPIにアクセスします。
data = {
  'grantType': 'client_credentials',
  'clientId': developer_id,
  'clientSecret': secret
}

req = urllib.request.Request(token_url, json.dumps(data).encode(), headers=headers, method='POST')
with urllib.request.urlopen(req) as res:
    access_info = json.load(res)
print(access_info['access_token'])

単語文章同士の類似度取得

*取得したアクセストークンをヘッダーに詰めて、類似度取得APIにアクセスします。

def similarity_api(s1: str, s2: str):
    data = {
        's1': s1,
        's2': s2,
        'type': 'default'
    }
    headers = {
        'Content-Type': 'application/json;charset=UTF-8',
        'Authorization': 'Bearer {}'.format(access_info['access_token'])
    }
    url = base_url + 'nlp/v1/similarity'
    req = urllib.request.Request(url, json.dumps(data).encode(), headers=headers, method='POST')
    with urllib.request.urlopen(req) as res:
        body = json.load(res)
    return body
similarity_api('近くのレストランはどこですか?', 'このあたりの定食屋はどこにありますか?')

所感

  • 単語や文章の類似度の算出は、tf-idf、word2vecやdoc2vecで割と世の中に出回っているため、目新しさはないです。
  • ただ、機械学習に慣れていない方には比較的に簡単に扱えるため、便利だと思います。
  • また、wikipediaのデータで自分で学習したword2vecのモデルより、対応している単語も多くかつ、類似度の精度も高い印象でした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonのurllib.requestで配列を含んだクエリを使ってGETリクエストする方法

はじめに

PythonであるURLにGETリクエストを行おうとした際に、クエリに数字配列と文字列配列が含まれており、ハンドリングに困ったので備忘録として解決策を残します。

解決策

以下のように渡すことで解決しました。

  • 数値(param1)はクォーテーションで囲わずに渡す
  • 文字列(param2)はクォーテーションで囲って渡す
  • 数値配列(param3)は[]で囲って渡す
    • 各要素はクォーテーションで囲わない
  • 文字列配列(param4)はparam4[0]param4[1]のように添え字を含めて、要素数分だけ記載して渡す
    • 各要素はクォーテーションで囲う
requesttest.py
# 読み込み
import urllib.request

# URLとクエリを定義
url = "http://hogehoge.com"
params = { 
    'param1':123,
    'param2':'abc',
    'param3':[0,2,3,4,5],    
    'param4[0]':'aaaaaa',
    'param4[1]':'bbbbbb',     
}

# リクエスト実行
req = urllib.request.Request('{}?{}'.format(url, urllib.parse.urlencode(params)))
with urllib.request.urlopen(req) as res:
    body = res.read()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rust] PyO3 でモジュール構造を持つ Python パッケージ

前記事 の続きです.

中規模以上のパッケージの場合, 関数を複数のモジュールに分けたくなると思います (SciPy の scipy.optimize, scipy.integrate のように). ほぼ前回の記事通りですが, 若干ハマった部分があるので詳しく書いておきます.

モジュールの作成

具体的に英語と日本語で hello world を出力する language パッケージをつくってみます. まずはモジュールを普通につくります.

src/en.rs
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyfunction]
fn hello() -> PyResult<()> {
    println!("Hello, world!");
    Ok(())
}

#[pymodule]
fn english(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_wrapped(wrap_pyfunction!(hello))?;
    Ok(())
}
src/ja.rs
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;


#[pyfunction]
fn hello() -> PyResult<()> {
    println!("こんにちは, 世界!");
    Ok(())
}

#[pymodule]
fn japanese(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_wrapped(wrap_pyfunction!(hello))?;
    Ok(())
}

ここで, 本体 (src/lib.rs) から参照することになる english, japanese 関数に pub を付ける必要はありません. というのも, #[pymodule] アトリビュートによりラッパー関数が自動的に生成され, そちらを読み込みに行くからです. その結果, 本体は次のようなコードになります.

src/lib.rs
use pyo3::prelude::*;
use pyo3::wrap_pymodule;

mod en;
use en::*;

mod ja;
use ja::*;

#[pymodule]
fn language(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_wrapped(wrap_pymodule!(english))?;
    m.add_wrapped(wrap_pymodule!(japanese))?;

    Ok(())
}

ここで例えば use en::english; とするとコンパイルエラーになります. これは, 上で指摘したように, language 関数内で en::english 関数を呼び出しているように見えますが, wrap_pymodule マクロのために実際に呼び出している関数は en::PyInit_english 関数だからです (なのでこの関数を直接 use しても動きます).

Python から呼び出し

Cargo.toml やコンパイル, .so ファイルの配置などは前記事通りです.

$ python3
Python 3.7.2 (default, Jan 24 2019, 15:36:28)
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import language
>>> 
>>> language.english.hello()
Hello, world!
>>> language.japanese.hello()
こんにちは, 世界!
>>> 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonのスライスのテクニック

スライスにはstep機能がある

Pythonのlistのスライスには指定した数ごとに値を取り出してくれるstep機能があります。
具体的には
list[start:stop:step]
と普通のスライスの後にコロン(:)とstepの値を追加してあげます。

以下に具体例をあげときます。

num_list = [1, 2, 3, 4, 5, 6, 7]
print(num_list[1:6])
print(num_list[::2])
print(num_list[1::2])
print(num_list[:6:2])
print(num_list[1:6:2])

実行結果

[2, 3, 4, 5, 6]
[1, 3, 5, 7]
[2, 4, 6]
[1, 3, 5]
[2, 4, 6]

こんな感じになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonのスライスのstep機能

スライスにはstep機能がある

Pythonのlistのスライスには指定した数ごとに値を取り出してくれるstep機能があります。
具体的には
list[start:stop:step]
と普通のスライスの後にコロン(:)とstepの値を追加してあげます。

以下に具体例をあげときます。

num_list = [1, 2, 3, 4, 5, 6, 7]
print(num_list[1:6])
print(num_list[::2])
print(num_list[1::2])
print(num_list[:6:2])
print(num_list[1:6:2])

#実行結果
[2, 3, 4, 5, 6]
[1, 3, 5, 7]
[2, 4, 6]
[1, 3, 5]
[2, 4, 6]

こんな感じになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonでtraitみたいなこと

Overview

ruby の extend みたいな感じにやりたい話。

$ python -V
Python 3.7.2

振る舞いだけ別で定義して後から mixin みたいなことは、言語の機能としてはできないっぽいけど、
type でその場で class 生成して、対象の __class__ に設定すると動的多重継承みたいにできるらしい。

委譲で済むならその方が良いと思うけど。

a.py
class TraitA:
    def m1(self):
        print(self, "m1")

    def m2(self):
        print(self, "m2")

class TraitB:
    def m1(self):
        print(self, "m1")

    def m2(self):
        print(self, "m2")


class Base:

    @classmethod
    def factory(cls, val):
        x = cls(val)

        if val == "A":
            trait = TraitA
        else:
            trait = TraitB

        x.__class__ = type("{}+{}".format(cls.__name__, trait.__name__), (cls, trait), {})

        return x

    def __init__(self, val):
        self.val = val

    def __repr__(self):
        return "<{}>\t{}".format(self.__class__.__name__, str(self.__dict__))

    def is_A(self):
        return isinstance(self, TraitA)

    def is_B(self):
        return isinstance(self, TraitB)

if __name__ == "__main__":
    a = Base.factory("A")
    a.m1()
    a.m2()
    print(a.is_A())
    print(a.is_B())

    print("-"*30)

    b = Base.factory("B")
    b.m1()
    b.m2()
    print(b.is_A())
    print(b.is_B())

実行

$ python a.py
<Base+TraitA>   {'val': 'A'} m1
<Base+TraitA>   {'val': 'A'} m2
True
False
------------------------------
<Base+TraitB>   {'val': 'B'} m1
<Base+TraitB>   {'val': 'B'} m2
False
True
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AtCoder Beginner Contest 004

A - 流行

奇をてらうことなく。

print(int(input())*2)

B - 回転

L1、L2はnumpyで何かしら代用出来るのだろう。
(知らないけど)

printはスマートさに欠けて悲C

import numpy as np

I = [input() for i in range(4)]
L1 = [list(i.replace(" ", "")) for i in I]
L2 = L1[::-1]
L3 = np.fliplr(L2)
[print(' '.join(i)) for i in L3]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

R ユーザーへの pandas 実践ガイド

概要

R で tidyverse (dplyr+tidyr) に使い慣れているが, Python に乗り換えると pandas がどうも使いにくい, と感じている人の視点で, Rの dplyr などとの比較を通して, pandas の効率的な使い方について書いています. そのため, 「R ユーザーへの」と書きましたが, R経験のない pandas ユーザーであってもなんらかの役に立つと思います. また, 自社インターン学生に対する教材も兼ねています. どちらかというと, 初歩を覚えたての初心者向けの記事となっています.

データ分析は一発で終わることはまずなく, 集計・前処理を探索的に行う必要があります. よって, プログラムを頻繁に書き直す必要があり, 普段以上に保守性のある書き方, 例えば参照透過性を考慮した書き方をしたほうが便利です. R の tidyverse の強みとして, 再帰代入をする必要がほとんどなく, 複雑な処理であってもかなりをワンライナーで書ける, というものがあります. 関数が, 集計, グループ化, 列の追加, というように, 機能の分け方が SQL 風になっているのもとっつきやすさにつながっていると思います. 一方で, Python のpandas は, メソッドチェーンで dplyr のパイプに近い機能を持たせるなど, tidyverse に影響された機能も多いですが, 普通に書こうとすると再帰代入を頻繁に要求されます.

結果として, pandas を効率的に書くテクニックは存在しますが, 公式ドキュメントではあまり強調されていません. そこで, ここでは, pandas で実現可能な, 効率的なデータ分析作業をする方法について紹介します.

導入

この記事の記述の多くは, 以下を参考にしています.

  1. Pandas 公式チートシートを翻訳しました
  2. Python/pandasのデータ処理で再帰代入撲滅委員会
  3. dplyr のアレを Pandas でやる
  4. dplyr使いのためのpandas dfplyすごい編

最低限, [1] を読んでいて, pandas の基本的な操作を知っていることを前提としています. そして今回は, 公式チートシートを踏まえて, より効率的なやり方をいろいろと紹介します. よって, ilocloc の違いなど, 公式チートシートに書いてある話はあえて繰り返すことはありません. また, 教材という目的のため, 上記 [2]-[4] で紹介されている内容の繰り返しになっている箇所も多いですが, ご容赦ください.

なお, 動作確認は python 3.6, pandas 0.25, dfply 0.3.3でやっています.

今回紹介するものは, 以下のようにして作成したデータフレームを使っています.

import pandas as pd
import numpy as np
from dfply import *

np.random.seed(42)

df = pd.DataFrame(
    {
        'x': np.random.normal(size=50*3),
        'y': np.random.normal(size=50*3),
        'col': list('ABCDE') * 10 * 3,
    })
pd.options.display.max_rows = 10  # これはなくてもいいです
pd.options.display.precision = 2  # これはなくてもいいです

参考記事のおさらい

データフレームの再帰代入をやめる

このセクションは, 参考記事 [2] の内容と被る箇所がほとんどですが, いくつか異なる点もあります. 冗長ですが今一度確認します.

行の選択・列の作成

上記に挙げた記事では lambda 演算子を使う方法が紹介されていました.
これがなぜ必要なのか. 例えば, 以下の2つはどちらも同じです.

df.assign(z=lambda d: d['x']**2)
df.assign(z=df['x']**2)
x y col z
0 0.50 0.25 A 0.25
1 -0.14 0.35 B 0.02
2 0.65 -0.68 C 0.42
3 1.52 0.23 D 2.32
4 -0.23 0.29 E 0.05
... ... ... ... ...
145 0.78 -0.69 A 0.61
146 -1.24 0.90 B 1.53
147 -1.32 0.31 C 1.74
148 0.52 0.81 D 0.27
149 0.30 0.63 E 0.09

これだけだと何がいいのかわかりませんが, メソッドチェーンを連結していくと lambda が便利であることがわかります. 例えば,

  1. col の値ごとにグループ別合計をとり
  2. x の値が正の行だけを取り出し
  3. y の数値を2乗した z を新たに作成する

という処理を, lambda を使う場合と使わない場合とで考えてみます. 以下の1行目が lambda を使った場合, 2行目が使わない場合です. いずれも同じ結果を返します.

df.groupby('col').sum().loc[lambda d: d['x'] > 0].assign(z=lambda d: d['y']**2)
x y z
col
A 1.6 -1.64 2.68
B 1.9 3.28 10.77
df.groupby('col').sum().loc[df.groupby('col').sum()['x'] > 0].assign(
    z=df.groupby('col').sum().loc[df.groupby('col').sum()['x'] > 0]['y'] ** 2)
x y z
col
A 1.6 -1.64 2.68
B 1.9 3.28 10.77

メソッドチェーンはデータフレームを書き換えるわけではないので, 集計後の値を参照する場合は, このように同一の集計処理を何度も書く必要があり, 非常に冗長となります. 何度もやり直すことが普通なデータ分析の作業で, 同じ処理を何度も書き直すのは効率が悪く, 変更時に書き間違える可能性が増します.
このように, 処理が複雑になり, メソッドチェーンが深くなるほど lambda を使った参照透過性は重要になっていきます.

参考記事 [2] では, df.pipe を使って SQL の select-where 構文の処理を実行していますが, 参考記事 [3] のここ にあるように, このような処理は実際には df.loc で済むことが多いです. pipe メソッドは, 以下にあるようにデータフレーム全体に関数を適用したい際に使う (lambda も関数ですが) ものです.

Python pandas データのイテレーションと関数適用、pipe - StatsFragrments

すべての列 or 行に適用する mutate_all 相当の処理は, apply を使います. こちらでも lambda を使えます.

# mutate_all()
# s は DataFrame ではなく Series であることに注意
df.select_dtypes(exclude='object').apply(lambda s: s - [10] * s.shape[0], axis=0)
# row-wise に適用したい場合
df.select_dtypes(exclude='object').apply(lambda s: s - [10] * s.shape[0], axis=1)
x y
0 -9.50 -9.75
1 -10.14 -9.65
2 -9.35 -10.68
3 -8.48 -9.77
4 -10.23 -9.71
... ... ...
145 -9.22 -10.69
146 -11.24 -9.10
147 -11.32 -9.69
148 -9.48 -9.19
149 -9.70 -9.37

補足: assign と eval

dplyr使いのためのpandas 基本編 にあるように, eval メソッドは文字列を評価して, assign と同様のことができます. 参照透過性も担保されているため, 再帰代入も必要ありません. 最初の例で eval を使うなら, 以下のようになります.

df.groupby('col').sum().loc[lambda d: d['x'] > 0].eval('z = y**2')
x y z
col
A 1.6 -1.64 2.68
B 1.9 3.28 10.77

処理が多い場合は, dict で与える手もあります. リスト内包表記と組み合わせれば複雑な処理も比較的シンプルに書けるかもしれません.

df.assign(**{'a': lambda d: d['x']**2, 'col2': lambda d: d['col'] + '_2'})
x y col a col2
0 0.50 0.25 A 0.25 A_2
1 -0.14 0.35 B 0.02 B_2
2 0.65 -0.68 C 0.42 C_2
3 1.52 0.23 D 2.32 D_2
4 -0.23 0.29 E 0.05 E_2
... ... ... ... ... ...
145 0.78 -0.69 A 0.61 A_2
146 -1.24 0.90 B 1.53 B_2
147 -1.32 0.31 C 1.74 C_2
148 0.52 0.81 D 0.27 D_2
149 0.30 0.63 E 0.09 E_2

補足: ドットか括弧か

参考記事ではいずれも, 列を参照するとき, d.x という書き方をしていますが, この記事では d['x'] という書き方をしています. . で列にアクセスする場合は, 列名とデータフレームのメソッドと名前が重複していると, 列ではなくメソッドが呼び出されてしまいます.
そこで, 名前の衝突回避のため lambda d: d['col'] のような書き方をすると安全です. ただし, 括弧の多用により, 少し見づらくなります. どちらを使うかは好みの問題でしょう.

補足: inplace を使うべきか

これも必ずこうしたほうがいい, というより好みの問題です. pandas のメソッドのほとんどはデータフレームを返すので, 結果を自身に代入するオプションとして, inplace 引数が多くのメソッドに用意されています. しかし, メソッドチェーンの中で inplace=True するとエラーになりますが, 末尾に inplace=True しても何もエラーが発生しません. また, assign など inplace がないメソッドもあります. そのため, 何度も処理の組み合わせを変更しているうちにおかしなコードになってしまう恐れがあるので, 私は inplace=True を使わず, なるべく = 代入の形で統一するようにしています.

列選択

上記の例では, assign.loc での行選択で lambda を使っていますが, 列選択にも使うことができます.

dplyr::select() に対応する操作は, df[['A', 'B']]df.loc[] です. 前者は列名のリストによる指定ですが, 後者は .loc を使うことでインデックスまたはブーリアンによる指定になっています.

しかし, もう少し複雑な条件で取り出したいときはもあります. そのような場合, 以下のようなメソッドが使えます.
1. 正規表現で列名にマッチする df.filter
2. 列の型で条件付けて取り出す df.select_dtypes
3. dplyrstarts_with, ends_with, contains に対応する df.columns.str.startswith/endswith/contains

などがあります.

df.loc[lambda d: d['x'] > 0, lambda d: d.filter(regex='x').columns]
x
0 0.50
2 0.65
3 1.52
6 1.58
7 0.77
... ...
143 0.18
144 0.26
145 0.78
148 0.52
149 0.30
df.filter(regex='x').loc[lambda d: d['x'] > 0]
x
0 0.50
2 0.65
3 1.52
6 1.58
7 0.77
... ...
143 0.18
144 0.26
145 0.78
148 0.52
149 0.30

この例では, メソッドチェーンが浅いため, filter を先に持ってきて, lambda を使わないほうがむしろシンプルになります. しかし, もっと複雑な処理でも, このように lambda 内で .filter() など, pandas.DataFrame の持つ便利なメソッドを呼び出せることを覚えておいてください.

long to wide

wide (いわゆる横持ち) から long (いわゆる縦持ち) への変換は, pd.DataFrame.melt1tidyr::gather とほぼ同じなので省略します. 問題は long から wide への変換の場合です. 公式チートシートでは pivot になっていますが...

df.pivot(columns='col', values=['x', 'y'])
x y
col A B C D E A B C D E
0 0.50 NaN NaN NaN NaN 0.25 NaN NaN NaN NaN
1 NaN -0.14 NaN NaN NaN NaN 0.35 NaN NaN NaN
2 NaN NaN 0.65 NaN NaN NaN NaN -0.68 NaN NaN
3 NaN NaN NaN 1.52 NaN NaN NaN NaN 0.23 NaN
4 NaN NaN NaN NaN -0.23 NaN NaN NaN NaN 0.29
... ... ... ... ... ... ... ... ... ... ...
145 0.78 NaN NaN NaN NaN -0.69 NaN NaN NaN NaN
146 NaN -1.24 NaN NaN NaN NaN 0.90 NaN NaN NaN
147 NaN NaN -1.32 NaN NaN NaN NaN 0.31 NaN NaN
148 NaN NaN NaN 0.52 NaN NaN NaN NaN 0.81 NaN
149 NaN NaN NaN NaN 0.30 NaN NaN NaN NaN 0.63

この通り大失敗です.

そもそもこれは, pivot の名前の通り, 本来ピボットテーブルを作るのが目的のメソッドです. 行に対応する軸を指定しなければ, 意図したとおりに変形してくれません. これは unstack でも同様です.

今回は, A-E の5要素で1グループとしたいので, 変換後の行に対応する行インデックスを新たに作る必要があります. 以下のように

df.assign(
    row=lambda x: sorted(list(range(x.shape[0] // x.col.nunique())) * x.col.nunique())
).head(10)
x y col row
0 0.50 0.25 A 0
1 -0.14 0.35 B 0
2 0.65 -0.68 C 0
3 1.52 0.23 D 0
4 -0.23 0.29 E 0
5 -0.23 -0.71 A 1
6 1.58 1.87 B 1
7 0.77 0.47 C 1
8 -0.47 -1.19 D 1
9 0.54 0.66 E 1

列を作ってから pivot を呼び出します.

df.assign(
    row=lambda x: sorted(list(range(x.shape[0] // x.col.nunique())) * x.col.nunique())
).pivot(index='row', columns='col', values=['x', 'y'])
x y
col A B C D E A B C D E
row
0 0.50 -0.14 0.65 1.52 -0.23 0.25 0.35 -0.68 0.23 0.29
1 -0.23 1.58 0.77 -0.47 0.54 -0.71 1.87 0.47 -1.19 0.66
2 -0.46 -0.47 0.24 -1.91 -1.72 -0.97 0.79 1.16 -0.82 0.96
3 -0.56 -1.01 0.31 -0.91 -1.41 0.41 0.82 1.90 -0.25 -0.75
4 1.47 -0.23 0.07 -1.42 -0.54 -0.89 -0.82 -0.08 0.34 0.28
... ... ... ... ... ... ... ... ... ... ...
25 2.19 -0.99 -0.57 0.10 -0.50 0.46 0.20 -0.60 0.07 -0.39
26 -1.55 0.07 -1.06 0.47 -0.92 0.11 0.66 1.59 -1.24 2.13
27 1.55 -0.78 -0.32 0.81 -1.23 -1.95 -0.15 0.59 0.28 -0.62
28 0.23 1.31 -1.61 0.18 0.26 -0.21 -0.49 -0.59 0.85 0.36
29 0.78 -1.24 -1.32 0.52 0.30 -0.69 0.90 0.31 0.81 0.63

unstack でもほぼ同じです

df.assign(
    row=lambda x: sorted(list(range(x.shape[0] // x.col.nunique())) * x.col.nunique())
).set_index(['row', 'col']).unstack(1)
x y
col A B C D E A B C D E
row
0 0.50 -0.14 0.65 1.52 -0.23 0.25 0.35 -0.68 0.23 0.29
1 -0.23 1.58 0.77 -0.47 0.54 -0.71 1.87 0.47 -1.19 0.66
2 -0.46 -0.47 0.24 -1.91 -1.72 -0.97 0.79 1.16 -0.82 0.96
3 -0.56 -1.01 0.31 -0.91 -1.41 0.41 0.82 1.90 -0.25 -0.75
4 1.47 -0.23 0.07 -1.42 -0.54 -0.89 -0.82 -0.08 0.34 0.28
... ... ... ... ... ... ... ... ... ... ...
25 2.19 -0.99 -0.57 0.10 -0.50 0.46 0.20 -0.60 0.07 -0.39
26 -1.55 0.07 -1.06 0.47 -0.92 0.11 0.66 1.59 -1.24 2.13
27 1.55 -0.78 -0.32 0.81 -1.23 -1.95 -0.15 0.59 0.28 -0.62
28 0.23 1.31 -1.61 0.18 0.26 -0.21 -0.49 -0.59 0.85 0.36
29 0.78 -1.24 -1.32 0.52 0.30 -0.69 0.90 0.31 0.81 0.63

pandas.DataFrame.pivottidyr::spread と同じという説明はかなりミスリードです. pivot は複数列を同時に処理できますが, 行インデックスも指定しなければうまくいきません. 一方で spread は1列づつしか処理できませんが, 当然行インデックスは存在しないので不要です.

なお, tidyr 1.0 ではまた新たな構文が追加される予定らしい2 3 ですが, これに対応する機能は当然まだありません. 個人的には1関数1機能のほうが直感的でわかりやすいです.

unstackpivot の詳しい挙動は以下も参考にしてください.

データの要約

[3] の 『グループ化 dplyr::group_by と集約 dplyr::summarise』セクションにあるように, pandas で各列を要約する agg メソッドは, 任意の列に任意の要約関数 (sumcount など, 列の全要素から計算する関数群のこと) を適用する dplyr::summarise とは機能が異なります. すべての列に要約関数を適用する summarise_atsummarise_all に近いです.

基本的な要約関数ならば, agg を使わなくとも呼び出せます. 使える関数は, [1] などを参考にしてください.

df.groupby('col').mean()
x y
col
A 0.05 -0.05
B 0.06 0.11
C -0.11 0.16
D -0.07 -0.07
E -0.35 0.21
df.groupby('col').agg({'x': [np.min, np.max]})
x
amin amax
col
A -1.92 2.19
B -1.24 1.89
C -1.96 1.48
D -1.91 2.46
E -2.62 1.03

0.25 からの新機能

以下では, 最近追加された機能で特に便利なものをピックアップします

要約処理の改善

pandas 0.25 からは, pd.NamedAgg という新しい構文が追加され, NEW_COLUMN=pd.NamedAgg(column='COL', aggfunc=FUNCTION) という構文で, dplyr::summarise に近いことができるようになりました. 詳細は以下の公式ドキュメントを確認してください.

例えば, x 列に対して最大, 最小, 中央値をグループ別に集計し, それぞれ x_min, x_max, x_med という列名にしたい場合は, 以下のように,

df.groupby('col').agg(
    x_min=pd.NamedAgg('x', 'min'),
    x_max=pd.NamedAgg('x', 'max'),
    x_med=pd.NamedAgg('x', 'median')
)
x_min x_max x_med
col
A -1.92 2.19 0.17
B -1.24 1.89 -0.05
C -1.96 1.48 -0.02
D -1.91 2.46 0.05
E -2.62 1.03 -0.23

と書けます. さらに省略して, (列名, 関数) のタプルだけで表現できる構文も用意されています. これはかなり便利になりました.

df.groupby('col').agg(
    x_min=('x', 'min'),
    x_max=('x', 'max'),
    x_med=('x', 'median')
)
x_min x_max x_med
col
A -1.92 2.19 0.17
B -1.24 1.89 -0.05
C -1.96 1.48 -0.02
D -1.91 2.46 0.05
E -2.62 1.03 -0.23

ネスト/アンネスト

python で使う機会はあまりなさそうな気もしますが, R の tibble のように, データフレームの要素にさらにテーブル構造のデータが入れ子になっている場合を考えます.
pandastibble のようにデータフレームの要素にデータフレームを入れることもできますが, nest/unnest はサポートされていません.
ただし, 要素がリストであるなら, 0.25 以降は行に展開する explode メソッドが使えます.

以下では, それ以外の方法も紹介されています.
* How to unnest (explode) a column in a pandas DataFrame? - Stack Overflow

df_nested = pd.DataFrame({'x': [1, 2], 'y': [df, df]})

iloc で要素 (=データフレーム) を取り出せます.

df_nested.iloc[0, 1]
x y col
0 0.50 0.25 A
1 -0.14 0.35 B
2 0.65 -0.68 C
3 1.52 0.23 D
4 -0.23 0.29 E
... ... ... ...
145 0.78 -0.69 A
146 -1.24 0.90 B
147 -1.32 0.31 C
148 0.52 0.81 D
149 0.30 0.63 E

explode は正確には unnest と対応する処理ではありませんが, 場合によっては代用になります.

df_nested = pd.DataFrame({'a': [1, 2], 'y': [['A', 1], ['B', 2]]})
df_nested
a y
0 1 [A, 1]
1 2 [B, 2]
df_nested.explode('y')
a y
0 1 A
0 1 1
1 2 B
1 2 2

メソッドチェーンでできないことをできるように

しかし, 頻繁に必要になるもののメソッドチェーンで完結できない処理というものが pandas ではまだいくつかあります. ここでは, いくつか解消方法を紹介します.

列に MultiIndex がある場合

インデックスは tidyverse にはない機能でした. pandas に乗り換えたユーザーにとって一番戸惑うのが index の扱いだと思います. grouoby で集計したら index, データフレーム同士を結合したら index, pivot したら index, というように何かにつけて index が出てきます. とはいえ, 基本的には reset_index メソッドを使えば列に戻すことができるので, さほど問題にはなりません.

厄介なのは, 列名が MultiIndex になっている場合です. これは reset_insex で解消できません. これはまさに, 先ほど紹介した pivotunstack をしたとき, あるいは agg で複数の要約関数を適用した場合にも発生します.

df_test = df.assign(
    row=lambda x: sorted(list(range(x.shape[0] // x.col.nunique())) * x.col.nunique())
).set_index(['row', 'col']).unstack(1).reset_index()

列名が MultiIndex で入れ子になっている場合, タプルを使って列にアクセスします.

df_test.columns
MultiIndex([('row',  ''),
            (  'x', 'A'),
            (  'x', 'B'),
            (  'x', 'C'),
            (  'x', 'D'),
            (  'x', 'E'),
            (  'y', 'A'),
            (  'y', 'B'),
            (  'y', 'C'),
            (  'y', 'D'),
            (  'y', 'E')],
           names=[None, 'col'])
df_test[[('row', ''), ('x', 'A')]]
row x
col A
0 0 0.50
1 1 -0.23
2 2 -0.46
3 3 -0.56
4 4 1.47
... ... ...
25 25 2.19
26 26 -1.55
27 27 1.55
28 28 0.23
29 29 0.78

すると, 3点の問題が発生します:
1. df.x のようにドットで列にアクセスできなくなる
2. df.filter での列名マッチがしづらくなる
3. 列を str で解く的できなくなる

(1) はともかくとして, (2, 3) が問題です. seabornplotnine は便利ですが, タプルでは列を指定できません. これ以外にも文字列だけで列名を指定できたほうが便利な場面は多いです. そこで, タプルを文字列に置き換える必要がでてきますが, この機能をもつビルトインメソッドは存在しません.
この問題に対して, 以下では新しくメソッドを定義する方法が提案されています.

"Method chaining solution to drop column level in pandas DataFrame"

ここで提案されている方法を少し修正して, メソッドチェーン内で列インデックスの変換をする方法を紹介します.

def reset_column_index(self, inplace=False,  sep='_'):
    if inplace:
        self.columns = [sep.join(filter(None, tup)) for tup in self.columns]
    else:
        c = self.copy()
        c.columns = [sep.join(filter(None, tup)) for tup in c.columns]
        return c

# 横持ちに変換して multiindex 列を作成
df_wide = df.assign(
    row=lambda x: sorted(list(range(x.shape[0] // x.col.nunique())) * x.col.nunique())
).pivot(index='row', columns='col', values=['x', 'y'])

df_wide.pipe(reset_column_index)
x_A x_B x_C x_D x_E y_A y_B y_C y_D y_E
row
0 0.50 -0.14 0.65 1.52 -0.23 0.25 0.35 -0.68 0.23 0.29
1 -0.23 1.58 0.77 -0.47 0.54 -0.71 1.87 0.47 -1.19 0.66
2 -0.46 -0.47 0.24 -1.91 -1.72 -0.97 0.79 1.16 -0.82 0.96
3 -0.56 -1.01 0.31 -0.91 -1.41 0.41 0.82 1.90 -0.25 -0.75
4 1.47 -0.23 0.07 -1.42 -0.54 -0.89 -0.82 -0.08 0.34 0.28
... ... ... ... ... ... ... ... ... ... ...
25 2.19 -0.99 -0.57 0.10 -0.50 0.46 0.20 -0.60 0.07 -0.39
26 -1.55 0.07 -1.06 0.47 -0.92 0.11 0.66 1.59 -1.24 2.13
27 1.55 -0.78 -0.32 0.81 -1.23 -1.95 -0.15 0.59 0.28 -0.62
28 0.23 1.31 -1.61 0.18 0.26 -0.21 -0.49 -0.59 0.85 0.36
29 0.78 -1.24 -1.32 0.52 0.30 -0.69 0.90 0.31 0.81 0.63

修正点は2つです. 1つは, 今回では row 列のように, 列インデックスが1層しかないものはセパレータ "_" がつかないようになっています. もう1つは, メソッドとして与えるのではなく, .pipe で与えているという点です. この修正もほとんど個人的な好みの問題なので, 上記のリンクそのままの使い方でもかまいません.

再帰的な計算処理

assign, apply でできるのは, element/row/column-wise な処理です. また, groupby メソッドならばグループ別集計ができます. 1つ前の要素も参照して処理するようなことはできません. つまり, ラグ演算や累積的な集計処理です. いちおう, pandas にはラグ計算専用のメソッドがいくつか用意されています

  • cumsum (累積和)
  • cumprod (累積積)
  • cummax (累積最大値)
  • cummin (累積最小値)

なぜか累積平均がありませんが, チートシート[1] の expanding メソッドを使えば実現できます.

df.expanding().mean()
x y
0 0.50 0.25
1 0.18 0.30
2 0.34 -0.03
3 0.63 0.04
4 0.46 0.09
... ... ...
145 -0.07 0.06
146 -0.08 0.06
147 -0.09 0.06
148 -0.08 0.07
149 -0.08 0.07

グループ化にも適用できます.

df.groupby('col').expanding().mean()
x y
col
A 0 0.50 0.25
5 0.13 -0.23
10 -0.07 -0.48
15 -0.19 -0.26
20 0.14 -0.38
... ... ... ...
E 129 -0.34 0.15
134 -0.36 0.22
139 -0.39 0.19
144 -0.37 0.20
149 -0.35 0.21

移動平均を取りたいならば, rolling や, 指数加重移動平均を取る ewma メソッドがあります.

"Exponentially-weighted moving window functions"

しかし, より一般的な再帰式で表現されるような処理はこれらでは処理できません. つまり,
$$
x_{t+1} = f(x_t)\
t=0, 1, 2, ...
$$

のような, 行ごとの再帰的な計算をしたい場合です. これは R の dplyr でもできない処理ですが, tidyverse に属する purrr::accumulate を組み合わせるとできます.

pandas の場合, $f$ に対応する関数を与えるだけで計算してくれるメソッドは存在しません4. そこで, 簡易的な代替方法として, 以下のような関数を用意します. 以下ならば, 1階差分方程式で表現できる計算ができるはずです.

def recurrence(x, func):
    x = x.copy()
    for i in range(1, x.shape[0]):
        x[i] = func(x[i-1])
    return x

例として, $f(x) = 2(x+1)$ を適用します.

df.assign(x2=lambda d: recurrence(d['x'], lambda x: (x+1)*2))
x y col x2
0 0.50 0.25 A 4.97e-01
1 -0.14 0.35 B 2.99e+00
2 0.65 -0.68 C 7.99e+00
3 1.52 0.23 D 1.80e+01
4 -0.23 0.29 E 3.79e+01
... ... ... ... ...
145 0.78 -0.69 A 1.11e+44
146 -1.24 0.90 B 2.23e+44
147 -1.32 0.31 C 4.45e+44
148 0.52 0.81 D 8.91e+44
149 0.30 0.63 E 1.78e+45

ラムダが多すぎる

ここまで, 参照透過性のため lambda を多用してきましたが, そもそも dplyr では lambda すら必要ないため, pandas ではコードが冗長になりがちです. そこで, dplyr 風の構文を python 上で再現することを目指した dfply を紹介します.

https://github.com/kieferk/dfply

日本語での使用法の解説は, 最初に挙げた参考記事 [4] があります.

見てわかるように, パイプ演算子なども再現しています. 一方で, 現時点での dfply の問題点として,

  1. pandas.DataFrame ではない独自のクラスを返すため, dfply のメソッドで処理を完結しなければならない
  2. すべて python で書かれているため, (pandas に比べ) さほど早くない5.
  3. Python の構文上, >> の直後に改行することができない (コードが見づらくなる)

という点が挙げられます.

具体的な操作方法は, dplyr をすでに使っているのなら [4] を読めばあとは直感でできるので, ここでは詳しい説明を省略します.
以前 dfply を見つけたときは使いやすそうだと思いましたが, lambda の利用や pandas 0.25 の新機能によりだいぶ使いやすくなったので, 個人的にはそこまでして使う必要はないかな, という考えに移りつつあります. 特に大きなデータを扱う場合は.


  1. インターネット上のスニペットでは pd.melt を使っている例も多いですが, 0.20 以降はデータフレームのメソッドとしても呼び出せます.  

  2. https://speakerdeck.com/yutannihilation/tidyr-pivot 

  3. https://blog.atusy.net/2019/06/29/pivoting-tidyr-1-0-0/ 

  4. Issues では「そのうち追加したい」機能という程度の優先順位に位置づけられています. https://github.com/pandas-dev/pandas/issues/4567 

  5. すべて確認したわけではないですが, たとえば pandas の sort_values と dfply の arrange とでは, 後者のほうが顕著に時間がかかります. 余計なコピーが発生しているため? 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonで独立性の検定(カイ2乗検定・フィッシャーの直接確率検定)

概要

「独立性の検定」に関するメモ、実施のためのPythonコードです。
「カイ2乗検定」と「フィッシャーの直接確率検定」の内容を含みます。

関連エントリ

設定

 問題例として、次のような設定(状況)を考えていきたいと思います。

ある学校では、学期末に自由記述欄を含んだ授業評価アンケートを実施している。
このアンケートは、担当教員が「紙形式」か「ウェブ形式」を選んで実施している。今後、アンケートを有効活用していくために、実施形式の違いが、自由記述欄(任意)への記入の有無に対して影響してくるのか?を知りたい。

 これを検討するための資料として、現在、手元には次のようなデータ(標本)が得られているとします。表内の数値は観測度数になります。

自由記述欄に記入あり 自由記述欄に記入なし
紙形式 140 150
ウェブ形式 160 180

カイ2乗検定(独立性の検定)

「実施形式が(自由記述欄に対するコメントの)記入の有無に影響するのか?」、つまり「『実施形式』と『記入の有無』には関連性があるのか、それともそれらは独立していて関連性がないのか?」を判断していきます。この独立性を評価するために、カイ2乗検定($\chi^2$ 検定)を利用することができます(フィッシャーの直接確率検定は後ほど)。

 カイ2乗検定では「変数間に関連性がない」を帰無仮説とします(ここでは「実施形式」と「記入有無」が変数となります)。そして、その「変数間に関連性がない」という仮定が正しいとして、仮に何度も標本を無作為抽出したときに「今回のような標本や、さらに極端な標本が得られるケース」がどの程度の確率で発生するのか?という値($p$ 値)を求め、それと事前設定した有意水準 $\alpha$ を比較して判断します。

 なお、「さらに極端な標本」とは「(手元にある標本よりも)変数間に関連性がありそうなことを匂わせる標本」のことです、例えば「紙形式では記入割合が $5\%$、ウェブ形式では記入割合が $85\%$」のような標本のことです。

Pythonで実行

 カイ2乗検定を行なうPythonコードを以下に示します。データはpandasのDataFrameに格納されていることを想定しています。

カイ2乗検定(Python)
import pandas as pd
import scipy.stats as st
df = pd.DataFrame([[140,150],   # 紙形式   で 記入ありの度数, 記入なしの度数
                   [160,180]])  # ウェブ形式 で 記入ありの度数, 記入なしの度数
x2, p, dof, e = st.chi2_contingency(df,correction=False)
print(f'p値    = {p :.3f}')
print(f'カイ2乗値 = {x2:.2f}')
print(f'自由度   = {dof}')
実行結果
p値    = 0.760
カイ2乗値 = 0.09
自由度   = 1

 あらかじめ決めておいた有意水準 $\alpha$ が $5\%$($=0.05$)とすれば、実行結果は $p\ge0.05$ となるので帰無仮説を採択します。つまり、統計学的には「実施形式」と「記入の有無」に関連性はない(それらは独立している)と結論付けます。つまり「アンケートの自由記述欄にコメントが書かれるかどうかは、アンケートの実施形式には無関係」といえそうです。

 なお、以下のように行と列を入れ替えても、同じ実行結果となります。

df = pd.DataFrame([[140,160], 
                   [150,180]])

 ところで、上記では、chi2_contingency に引数 correction=False を指定しています。これは「イェーツの補正(イェーツの連続修正)」を適用しない、というオプションになります。カイ2乗検定の原理について解説しているページなどを見ながら試すような場合には、「イェーツの補正をしない」に設定しておくとよいです。

 なお、デフォルトでは correction=True になります。Wikipediaによれば「イェイツの修正の効果はデータのサンプル数が少ない時に統計学的な重要性を過大に見積もりすぎることを防ぐことである」とのことです。 correction=True として実行すると、次のような結果となります。

実行結果(イェーツ補正あり)
p値    = 0.822
カイ2乗値 = 0.05
自由度   = 1

どのようにp値は計算されるのか

 帰無仮説が正しく「実施形式」と「記入の有無」に変数間に関連性がないとき、つまり変数は独立であるときに期待される度数(これを期待度数という)と、実際の観測度数の差の2乗を、期待度数で割ったものの総和(これをカイ2乗値という)を求め、これとカイ2乗分布から $p$ 値を計算します。

 期待度数は、変数が独立であるとすれば、次式が成立するはずということに基づき計算できます。

  • 形式で記入ありの割合=(形式の割合)$\times$(記入ありの割合)
  • 形式で記入なしの割合=(形式の割合)$\times$(記入なしの割合)
  • ウェブ形式で記入ありの割合=(ウェブ形式の割合)$\times$(記入ありの割合)
  • ウェブ形式で記入なしの割合=(ウェブ形式の割合)$\times$(記入なしの割合)

期待度数を実際に求めてみます。まず、観測度数は次のようになっていました(合計欄を追加しました)。

記入あり 記入なし
紙形式 140 150 290
ウェブ形式 160 180 340
300 330 630

 ここで、「形式の割合」は表の4列目から $290/630\risingdotseq 0.46$ 、「ウェブ形式の割合」は $340/630\risingdotseq 0.54$ のように求めることができます。また、表の4行目から「記入ありの割合」は $300/630\risingdotseq 0.48$、「記入なしの割合」は $330/630\risingdotseq0.52$ となります。

 あとは、これらを使って「形式で記入ありの割合=(形式の割合)$\times$(記入ありの割合)=$(290/630)\times(300/630)\risingdotseq0.219$」を求めます。そして、それに $630$ をかけると、次のように期待度数が求まります。

記入あり 記入なし
紙形式 138.1 151.9
ウェブ形式 161.9 178.1

 この期待度数と観測度数の差の2乗を、期待度数で割ったものの総和(=カイ2乗値)を求め、これとカイ2乗分布から $p$ 値を計算します(詳細は、別資料参照ください)。なお、期待度数は次のように st.chi2_contingency の戻値の4番目に格納されています。

import pandas as pd
import scipy.stats as st
df = pd.DataFrame([[140,150], [160,180]]) 
x2, p, dof, e= st.chi2_contingency(df,correction=False)
print(f'期待度数 : \n {e}')
実行結果
期待度数 : 
 [[138.0952381 151.9047619]
 [161.9047619 178.0952381]]

フィッシャーの直接確率検定

 カイ2乗検定は、一部に極端に小さい観測度数(5未満?)を含んでいる場合に適切な検定ができないことが知られています。特に問題例のように $2\times2$(2行2列)のケースでは、すべての観測度数が10以上であることが望ましいようです(参考資料[5])。そのような場合では、カイ2乗検定ではなくフィッシャーの直接確率検定(フィッシャーの正確確率検定)を使用します。

 なお、十分な観測度数が得られている標本に対してもフィッシャーの直接確率検定は有効です。ただし、フィッシャーの直接確率検定は、観測度数の階乗計算を含むため、計算が大変になることがデメリットになります(問題例では観測度数に140がありますが、140の階乗はとても大きな数になるため計算に工夫が必要です)。Pythonのライブラリを利用するうえでは、あまり気になりませんが。

フィッシャーの正確確率検定(Python)
import pandas as pd
import scipy.stats as st
df = pd.DataFrame([[140,150], [160,180]]) 
_, p = st.fisher_exact(df)
print(f'p値 = {p :.3f}')
実行結果
p値 = 0.810

 有意水準 $\alpha$ が $5\%$($=0.05$)とすれば、実行結果は $p\ge0.05$ となるので帰無仮説を採択します。

R版

フィッシャーの正確確率検定(R)
m=matrix(c(140, 150, 160, 180), nrow=2, byrow=T)
fisher.test(m)
実行結果
    Fisher's Exact Test for Count Data

data:  m
p-value = 0.8103
alternative hypothesis: true odds ratio is not equal to 1
95 percent confidence interval:
 0.757838 1.454709
sample estimates:
odds ratio 
  1.049888 

結論

 Pythonで独立性の検定を行なう場合は、フィッシャーの直接確率検定を利用するのが良いようです。

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonとRで独立性の検定(カイ2乗検定・フィッシャーの直接確率検定)

概要

「独立性の検定」に関するメモ、実施のためのPythonコード、Rコードです。
「カイ2乗検定」と「フィッシャーの直接確率検定」の内容を含みます。

関連エントリ

設定

 問題例として、次のような設定(状況)を考えていきたいと思います。

ある学校では、学期末に自由記述欄を含んだ授業評価アンケートを実施している。
このアンケートは、担当教員が「紙形式」か「ウェブ形式」を選んで実施している。今後、アンケートを有効活用していくために、実施形式の違いが、自由記述欄(任意)への記入の有無に対して影響してくるのか?を知りたい。

 これを検討するための資料として、現在、手元には次のようなデータ(標本)が得られているとします。表内の数値は観測度数になります。

自由記述欄に記入あり 自由記述欄に記入なし
紙形式 140 150
ウェブ形式 160 180

カイ2乗検定(独立性の検定)

「実施形式が(自由記述欄に対するコメントの)記入の有無に影響するのか?」、つまり「『実施形式』と『記入の有無』には関連性があるのか、それともそれらは独立していて関連性がないのか?」を判断していきます。この独立性を評価するために、カイ2乗検定($\chi^2$ 検定)を利用することができます(フィッシャーの直接確率検定は後ほど)。

 カイ2乗検定では「変数間に関連性がない」を帰無仮説とします(ここでは「実施形式」と「記入有無」が変数となります)。そして、その「変数間に関連性がない」という仮定が正しいとして、仮に何度も標本を無作為抽出したときに「今回のような標本や、さらに極端な標本が得られるケース」がどの程度の確率で発生するのか?という値($p$ 値)を求め、それと事前設定した有意水準 $\alpha$ を比較して判断します。

 なお、「さらに極端な標本」とは「(手元にある標本よりも)変数間に関連性がありそうなことを匂わせる標本」のことです、例えば「紙形式では記入割合が $5\%$、ウェブ形式では記入割合が $85\%$」のような標本のことです。

Pythonで実行

 カイ2乗検定を行なうPythonコードを以下に示します。データはpandasのDataFrameに格納されていることを想定しています。

カイ2乗検定(Python版)
import pandas as pd
import scipy.stats as st
df = pd.DataFrame([[140,150],   # 紙形式   で 記入ありの度数, 記入なしの度数
                   [160,180]])  # ウェブ形式 で 記入ありの度数, 記入なしの度数
x2, p, dof, e = st.chi2_contingency(df,correction=False)
print(f'p値    = {p :.3f}')
print(f'カイ2乗値 = {x2:.2f}')
print(f'自由度   = {dof}')
実行結果
p値    = 0.760
カイ2乗値 = 0.09
自由度   = 1

 あらかじめ決めておいた有意水準 $\alpha$ が $5\%$($=0.05$)とすれば、実行結果は $p\ge0.05$ となるので帰無仮説を採択します。つまり、統計学的には「実施形式」と「記入の有無」に関連性はない(それらは独立している)と結論付けます。つまり「アンケートの自由記述欄にコメントが書かれるかどうかは、アンケートの実施形式には無関係」といえそうです。

 なお、以下のように行と列を入れ替えても、同じ実行結果となります。

df = pd.DataFrame([[140,160], 
                   [150,180]])

 ところで、上記では、chi2_contingency に引数 correction=False を指定しています。これは「イェーツの補正(イェーツの連続修正)」を適用しない、というオプションになります。カイ2乗検定の原理について解説しているページなどを見ながら試すような場合には、「イェーツの補正をしない」に設定しておくとよいです。

 なお、デフォルトでは correction=True になります。Wikipediaによれば「イェイツの修正の効果はデータのサンプル数が少ない時に統計学的な重要性を過大に見積もりすぎることを防ぐことである」とのことです。 correction=True として実行すると、次のような結果となります。

実行結果(イェーツ補正あり)
p値    = 0.822
カイ2乗値 = 0.05
自由度   = 1

R版

 同様のことを R を使って行ないました。

カイ2乗検定_イェーツの補正なし(R版)
m=matrix(c(140, 150, 160, 180), nrow=2, byrow=T)
chisq.test(m,correct=F)
実行結果
    Pearson's Chi-squared test

data:  m
X-squared = 0.092937, df = 1, p-value = 0.7605

 つづいて、イェーツの補正ありのバージョンです。

カイ2乗検定_イェーツの補正あり(R)
m=matrix(c(140, 150, 160, 180), nrow=2, byrow=T)
chisq.test(m)
実行結果
    Pearson's Chi-squared test with Yates' continuity correction

data:  m
X-squared = 0.050549, df = 1, p-value = 0.8221

どのようにp値は計算されるのか

 帰無仮説が正しく「実施形式」と「記入の有無」に変数間に関連性がないとき、つまり変数は独立であるときに期待される度数(これを期待度数という)と、実際の観測度数の差の2乗を、期待度数で割ったものの総和(これをカイ2乗値という)を求め、これとカイ2乗分布から $p$ 値を計算します。

 期待度数は、変数が独立であるとすれば、次式が成立するはずということに基づき計算できます。

  • 形式で記入ありの割合=(形式の割合)$\times$(記入ありの割合)
  • 形式で記入なしの割合=(形式の割合)$\times$(記入なしの割合)
  • ウェブ形式で記入ありの割合=(ウェブ形式の割合)$\times$(記入ありの割合)
  • ウェブ形式で記入なしの割合=(ウェブ形式の割合)$\times$(記入なしの割合)

期待度数を実際に求めてみます。まず、観測度数は次のようになっていました(合計欄を追加しました)。

記入あり 記入なし
紙形式 140 150 290
ウェブ形式 160 180 340
300 330 630

 ここで、「形式の割合」は表の4列目から $290/630\risingdotseq 0.46$ 、「ウェブ形式の割合」は $340/630\risingdotseq 0.54$ のように求めることができます。また、表の4行目から「記入ありの割合」は $300/630\risingdotseq 0.48$、「記入なしの割合」は $330/630\risingdotseq0.52$ となります。

 あとは、これらを使って「形式で記入ありの割合=(形式の割合)$\times$(記入ありの割合)=$(290/630)\times(300/630)\risingdotseq0.219$」を求めます。そして、それに $630$ をかけると、次のように期待度数が求まります。

記入あり 記入なし
紙形式 138.1 151.9
ウェブ形式 161.9 178.1

 この期待度数と観測度数の差の2乗を、期待度数で割ったものの総和(=カイ2乗値)を求め、これとカイ2乗分布から $p$ 値を計算します(詳細は、別資料参照ください)。なお、期待度数は次のように st.chi2_contingency の戻値の4番目に格納されています。

import pandas as pd
import scipy.stats as st
df = pd.DataFrame([[140,150], [160,180]]) 
x2, p, dof, e= st.chi2_contingency(df,correction=False)
print(f'期待度数 : \n {e}')
実行結果
期待度数 : 
 [[138.0952381 151.9047619]
 [161.9047619 178.0952381]]

フィッシャーの直接確率検定

 カイ2乗検定は、一部に極端に小さい観測度数(5未満?)を含んでいる場合に適切な検定ができないことが知られています。特に問題例のように $2\times2$(2行2列)のケースでは、すべての観測度数が10以上であることが望ましいようです(参考資料[5])。そのような場合では、カイ2乗検定ではなくフィッシャーの直接確率検定(フィッシャーの正確確率検定)を使用します。

 なお、十分な観測度数が得られている標本に対してもフィッシャーの直接確率検定は有効です。ただし、フィッシャーの直接確率検定は、観測度数の階乗計算を含むため、計算が大変になることがデメリットになります(問題例では観測度数に140がありますが、140の階乗はとても大きな数になるため計算に工夫が必要です)。Pythonのライブラリを利用するうえでは、あまり気になりませんが。

フィッシャーの正確確率検定(Python)
import pandas as pd
import scipy.stats as st
df = pd.DataFrame([[140,150], [160,180]]) 
_, p = st.fisher_exact(df)
print(f'p値 = {p :.3f}')
実行結果
p値 = 0.810

 有意水準 $\alpha$ が $5\%$($=0.05$)とすれば、実行結果は $p\ge0.05$ となるので帰無仮説を採択します。

R版

フィッシャーの正確確率検定(R)
m=matrix(c(140, 150, 160, 180), nrow=2, byrow=T)
fisher.test(m)
実行結果
    Fisher's Exact Test for Count Data

data:  m
p-value = 0.8103
alternative hypothesis: true odds ratio is not equal to 1
95 percent confidence interval:
 0.757838 1.454709
sample estimates:
odds ratio 
  1.049888 

結論

 PythonやRで独立性の検定を行なう場合は、フィッシャーの直接確率検定を利用するのが良いようです。

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VSCode で anaconda の python を使う(設定編)

構文チェックで 「import できねぇぜw」って出るまで気が付かなかった。
以下の設定が必要

python.condaPath
example)E:\python\anaconda\condabin\conda.bat

python.pythonPath
example)E:\python\anaconda\envs\hoge\python.exe

どうやら、パスの指定にエスケープは必要ないらしい。って書きたかった記事。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LeetCode / Remove Duplicates from Sorted List

ブログ記事からの転載)

[https://leetcode.com/problems/remove-duplicates-from-sorted-list/]

Given a sorted linked list, delete all duplicates such that each element appear only once.

Example 1:
Input: 1->1->2
Output: 1->2

Example 2:
Input: 1->1->2->3->3
Output: 1->2->3

LeetCode頻出のLinkedListの問題。以前の記事にも登場してます。

今回の記事も長くはないですが、裏には数多の苦悩の道のりがありました。
LinkedListの性質(より正確には、Pythonの変数間の参照)がどうにも理解できず、youtubeでインドの方の解説動画とかを読み漁りました。

解答・解説

解法1

公式のJavaの解答をPythonに翻訳したのが以下です(余談ですが、LeetCodeをやってると高級言語から入った人でもC++とかJavaが段々読めるようになってくるのがおトクですね)。

class Solution:
    def deleteDuplicates(self, head: ListNode) -> ListNode:
        # currentとheadの間で参照を渡して、headの各nodeをcurrentを動かしながら移動し、重複nodeを発見・削除する
        current = head
        while current and current.next:
            # 重複nodeを発見したら、重複nodeの次のnodeにcurrent.nextをポイントさせて、重複nodeを削除する
            if current.next.val == current.val:
                current.next = current.next.next
            # 重複nodeでなかったら、通常のiterationを進める(currentを次のnodeにポイントさせる)
            else:
                current = current.next
        return head

、、、ということなんですが、皆さん分かりますか?
私はさっぱり分かりませんでした。current.next = current.next.nextだとnodeは削除されるのに、current = current.nextだとnodeは削除されないのはなぜ?

どうにも困ったので、慣れ親しんだデータ型であるlistで同様の状況を再現してみて、改めて考えてみました。

head = [1,1,2]
current = head
while current and current[1:]:
    if current[1] == current[0]:
        current[1:] = current[2:]
    else:
        current = current[1:]
    print('current:{}'.format(current), 'head:{}'.format(head))
head
# ---> current:[1, 2] head:[1, 2]
# ---> current:[2] head:[1, 2]
# ---> [1, 2]

分かりやすいように、print文でIterationの途中のcurrent, headの値を吐き出すようにしました。
この結果から、朧げながら私の理解は、
current[1:] = current[2:]ではcurrentとheadの間の参照は保持したまま値が代入されている(のでheadの値も変わる)が、
current = current[1:]では参照自体が1つ先に移動する(のでheadの値が変わるわけではない)、
ということかと思いました。

間違っていたら是非是非ご指摘ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

"pip install nagisa"でいろいろやった話

"pip install nagisa" に失敗する

タイトル通り。早速入れようと意気込んでいたらコケた。

エラーその1

Microsoft Visual C++ 14.0 is required.

ググったら同じ現象の方が大勢いらっしゃるようで。
https://visualstudio.microsoft.com/ja/downloads/
Visual Studio 2019 のツールからBuild Tools for Visual Studio 2019をダウンロード。

起動して、一番左上のC++ Build Toolsにチェックを入れる。
右側に詳細が出るので、デフォルトで入っているものの他に、MSCV v140にもチェックを入れる。
vc14_0.png

エラーその2

LINK : fatal error LNK1158: 'rc.exe' を実行できません。
など。
こちらについてはQiitaの記事がありました。
tslearnのpipインストールで"LINK : fatal error LNK1158: 'rc.exe' を実行できません。"が出た時の対処 - Qiita
の エラーと対処 の項目を実行します。


無事インストールできた。ヨカッタ。
......今後新たなエラーが起きないことを祈りつつ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】リスト内の特定要素存在チェック方法を比較

はじめに

 Python でリストに特定条件を満たす要素が存在するか判定したい時って結局どう書くのが良いんだろう? と疑問だったので、いくつかの方法の速度を計測、比較してみました。
 単純に '1' in ['1', '2', '3'] のようにリスト内要素の値をそのまま指定できる場合は悩む必要はないので、要素に対して何らかの操作(ここではメンバ変数参照)が必要な場合を想定しています。

存在チェック方法

 class Person のリストからメンバー変数 last_name'紀伊田' と一致する要素が存在するかチェックする、という前提で以下 4つの方法を比較しました。

  1. in 演算子 : '紀伊田' in (p.last_name for p in personlist)
    → 内包表記によりメンバー変数 last_name のみを抽出して in 演算子を使えるようにしたパターン。
  2. any 関数 : any(p.last_name == '紀伊田' for p in personlist)
    → 内包表記により要素ごとに条件を満たすかどうかの bool 値を抽出して any 関数を使ったパターン。
  3. any 関数2 : any(True for p in personlist if p.last_name == '紀伊田')
    → 2.の亜種で if 文により条件を満たす要素のみ True を抽出して any 関数を使うパターン。
  4. next 関数 : next((True for p in personlist if p.last_name == '紀伊田'), False)
    → 3.と同じことを any 関数ではなく next 関数で判定するパターン。

 personlist の要素数は 10000 個、last_nameFaker で生成し、5001 番目のみ紀伊田で上書きして 1 つだけ存在する状態にしています。
 上記はジェネレーター式で記載していますがリスト内包表記とセット内包表記の場合についても計測しました。

計測結果

 1. ~ 4. の方法を for _ in range(100000): で 100000 回実行するのに掛かった時間です(at Google Colaboratory)。
 3. の if文で条件指定したジェネレーター式を any 関数で評価するのが一番速い という結果になりました。

1.in 式 2.any 式 3.any if式 4.next if式 in any any if next forのみ
リスト内包表記 84.067628 99.620646 78.945970 NaN 5.785986 3.312556 0.017510 NaN 0.004489
セット内包表記 71.618741 84.979072 76.397498 NaN 0.008912 0.016706 0.015983 NaN 0.004158
ジェネレーター式 52.239412 58.766906 38.168067 39.631893 NaN NaN NaN NaN 0.004978

※1. 5 ~ 8 列目は 1. ~ 4. の内包表記の式をforの外側に逃がして計測した参考値
※2. 同じジェネレーターを複数回評価できないためジェネレーター式の 5 ~ 8 は未計測

内包表記による違い

 ジェネレーター式 $>$ セット内包表記 $>$ リスト内包表記 の順に速い結果となりました。
ジェネレーター式が速いのは予想どおりですが、リスト内包表記よりセット内包表記の方が速いのは意外でした。Faker の生成した last_name のユニーク値が 52 個しかなかったので、セットの重複チェック処理よりリストのメモリ拡張処理の方が結果に影響したのだと考えられます。

判定方法による違い

 3. any 関数2 $\geqq$ 4. next 関数 $>$ 1. in 演算子 $>$ 2. any 関数 の順に速い結果となりました。
if文のありなしが異なる 2. と 3. で 3. any 関数2 $>$ 2. any 関数 となったのは、2. では p.last_name == '紀伊田' の判定が False のものも列挙されるので、True が出現するまで(今回は 5000 個)の Falseも any で再判定するのに対し、3. では p.last_name == '紀伊田' が True になった要素のみ列挙されるため any の再判定は 1 回のみである点が速度に寄与しているのではないかと思います。この理屈が正しければ 3. any 関数2 $>$ 1. in 演算子 となるのも納得できます。

 1. in 演算子 $>$ 2. any 関数 はどちらも True になるまでの False(5000 個)の要素を判定する点は変わりませんが、1. の方は文字列一致判定が 1 回に対し、2. は == 演算子と any 関数 で 2 回判定される違いが速度差に現れたのだと思われます。

結論

 if文で条件指定したジェネレーター式を any 関数で評価するのが一番速い

おわりに

 以前からの疑問に対して答えが出たので満足です。python の中身を把握しているわけではないので本当のところは違うかも知れませんが、測定結果に対してそれっぽい理由はつけられたと思いますので、結論としては合ってるのではないかと思います。
 ユニーク値の割合や、一致する要素が何番目にあるのかなどで結果も変わってくるでしょうが、基本は if 文つきジェネレーター式と any 関数の組合せが安定すると思います。
 当たり前ですが数回の判定なら体感差は出ないので、速度を気にしない、タイプ量が少ない、分かりやすいとかで in 演算子やその他方法も適宜使えば良いと思います。
 もっと良い方法を知ってるという方は教えていただけると嬉しいです。

おまけ:計測コード(Colaboratory)

(折りたたみ)
!pip install Faker > /dev/null
import time

from faker import Factory
import pandas as pd
class Person:
    def __init__(self, first_name: str, last_name: str):
        self.first_name = first_name
        self.last_name = last_name

    def __str__(self):
        return f'{self.last_name} {self.first_name}'
fake = Factory.create('ja_JP')
personlist = [
    Person(fake.first_name(), fake.last_name()) for _ in range(10000)
]
print(f'紀伊田 in persons : {"紀伊田" in (p.last_name for p in personlist)}')
personlist[5000].first_name = 'きい太'
personlist[5000].last_name = '紀伊田'
print(f'紀伊田 in persons : {"紀伊田" in (p.last_name for p in personlist)}')
print('')
print(' / '.join(str(p) for p in personlist[4996:5005]))
紀伊田 in persons : False
紀伊田 in persons : True

工藤 太郎 / 中津川 淳 / 工藤 加奈 / 原田 幹 / 紀伊田 きい太 / 若松 涼平 / 廣川 さゆり / 大垣 零 / 斉藤 英樹
trials = 1
index = ['in 式', 'any 式', 'any if式', 'next if式', 'in', 'any', 'any if', 'next if', 'forのみ']
# リスト内包表記
series1 = pd.Series(0., index=index)
series1['next if式'] = None
series1['next if'] = None

for _ in range(trials):
    tm = time.time()
    for _ in range(100000):
        '紀伊田' in [p.last_name for p in personlist]
    series1['in 式'] += time.time() - tm

    tm = time.time()
    for _ in range(100000):
        any([p.last_name == '紀伊田' for p in personlist])
    series1['any 式'] += time.time() - tm

    tm = time.time()
    for _ in range(100000):
        any([True for p in personlist if p.last_name == '紀伊田'])
    series1['any if式'] += time.time() - tm

    persons = [p.last_name for p in personlist]
    tm = time.time()
    for _ in range(100000):
        '紀伊田' in persons
    series1['in'] += time.time() - tm

    persons = [p.last_name == '紀伊田' for p in personlist]
    tm = time.time()
    for _ in range(100000):
        any(persons)
    series1['any'] += time.time() - tm

    persons = [True for p in personlist if p.last_name == '紀伊田']
    tm = time.time()
    for _ in range(100000):
        any(persons)
    series1['any if'] += time.time() - tm

    tm = time.time()
    for _ in range(100000):
        pass
    series1['forのみ'] += time.time() - tm

series1 /= trials
# セット内包表記
series2 = pd.Series(0., index=index)
series2['next if式'] = None
series2['next if'] = None

for _ in range(trials):
    tm = time.time()
    for _ in range(100000):
        '紀伊田' in {p.last_name for p in personlist}
    series2['in 式'] += time.time() - tm

    tm = time.time()
    for _ in range(100000):
        any({p.last_name == '紀伊田' for p in personlist})
    series2['any 式'] += time.time() - tm

    tm = time.time()
    for _ in range(100000):
        any({True for p in personlist if p.last_name == '紀伊田'})
    series2['any if式'] += time.time() - tm

    persons = {p.last_name for p in personlist}
    tm = time.time()
    for _ in range(100000):
        '紀伊田' in persons
    series2['in'] += time.time() - tm

    persons = {p.last_name == '紀伊田' for p in personlist}
    tm = time.time()
    for _ in range(100000):
        any(persons)
    series2['any'] += time.time() - tm

    persons = {True for p in personlist if p.last_name == '紀伊田'}
    tm = time.time()
    for _ in range(100000):
        any(persons)
    series2['any if'] += time.time() - tm

    tm = time.time()
    for _ in range(100000):
        pass
    series2['forのみ'] += time.time() - tm

series2 /= trials
# ジェネレーター式
series3 = pd.Series(0., index=index)
series3['in'] = None
series3['any'] = None
series3['any if'] = None
series3['next if'] = None

for _ in range(trials):
    tm = time.time()
    for _ in range(100000):
        '紀伊田' in (p.last_name for p in personlist)
    series3['in 式'] += time.time() - tm

    tm = time.time()
    for _ in range(100000):
        any(p.last_name == '紀伊田' for p in personlist)
    series3['any 式'] += time.time() - tm

    tm = time.time()
    for _ in range(100000):
        any(True for p in personlist if p.last_name == '紀伊田')
    series3['any if式'] += time.time() - tm

    tm = time.time()
    for _ in range(100000):
        next((True for p in personlist if p.last_name == '紀伊田'), False)
    series3['next if式'] += time.time() - tm

    tm = time.time()
    for _ in range(100000):
        pass
    series3['forのみ'] += time.time() - tm

series3 /= trials
df = pd.DataFrame({
    'リスト内包表記': series1,
    'セット内包表記': series2,
    'ジェネレータ式': series3,
})
df.T

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【精度対決】MobileNet V3 vs V2

皆さん、エッジAIを使っていますか?

エッジAIといえば、MobileNet V2ですよね。
先日、後継機となるMobileNet V3論文発表されました。

世界中のエンジニアが、MobileNet V3のベンチマークを既に行っていますが、
自分でもベンチマークをしたくなりました。

本稿では、MobileNet V3のベンチマークをKerasで行いたいと思います。
image.png
(図は論文より引用)
コード全体はGithubで公開しています。

設定

MobileNet V3

  • コードはこちらから拝借しています。
  • 重みは初期化された状態で学習します。
  • V3にはsmalllargeがありますが、ラズパイのCPUで動かすことを想定してsmallを採用しました。
  • $\alpha$の値によって、モデルサイズが変動します。$\alpha$は大きいほど精度が出ますが、速度は遅くなります。

MobileNet V2

  • KerasにはV2が標準装備されており、これを使います。
  • 重みは学習済が用意されていますが、V3と同じく初期化された状態で学習します。
  • $\alpha$はV3と同じ仕様です。

その他

  • データセットはcifar-10を使用します。
  • 実験はColaboratory(GPU)で実施しました。
  • バッチサイズは1024、エポックは100、最適化手法はAdam(lr=0.001)、DataAugmentationを使用。

結果

精度の比較

早速、気になる精度を見てみましょう。
下の図は各モデルを10回学習させたときの精度の結果です。

image.png

V2に対し、V3は精度も良く、ばらつきも少ないです。
V3の推論時間が速ければ、V3を使った方が良さそうです。

推論時間の比較

Colaboratoryで1000回実行して中央値などを取り出そうとしましたが、Colaboratoryに
クセがあるのか、実行する度に計測時間が変動しました。(メモリや使用時間の影響?)

ただし、相対的な推論時間の関係は崩れなかったので、相対比較はある程度信用できそうです。
下の図は、各モデルを1000回実行させたときの推論時間の平均値です。

image.png

推論時間の数値は信用しないでください。
※()の値は$\alpha$の数値です。

きれいな右肩上がりになっており、V2(0.5)とV2(1)の差が大きい印象です。
また、$V2(1)\fallingdotseq V3(0.2)$となっていて、精度を考慮するとV3(0.2)を
採用する方が良いようです。

表にまとめます。

速度 精度
V2(0.5) 速い 悪い
V3 small(0.2)
V3 small(0.5) 遅い 良い

今のところ、V2(0.5)の代替品はV3で存在しないようです。

学習時間の比較

前述のとおり、実行する度に計測時間が変わったため、ここでは計測できませんでした。
ただ、V2とV3でそこまで学習時間の差は生まれない印象です。

重みのサイズの比較

$\alpha$ 重みのサイズ(MB)
MobileNet V2 0.5 3.3
MobileNet V2 1 9.5
MobileNet V3 small 0.2 11.3
MobileNet V3 small 0.5 11.7

V2の方が圧倒的に軽い結果となりました。
また、V3の重みのサイズは、$\alpha$にあまり依存しないようです。

転移学習について

実際に使うことを考えると、モデルをゼロから学習させることは稀だと思います。
通常は転移学習を使いますが、今のところ、Kerasで学習済の重み(V3)は公開され
ていません。しかし、Pytorchでは公開されています。

https://github.com/d-li14/mobilenetv3.pytorch

PytorchモデルをKerasやTensorFlow liteモデルへ変換する方法は、
以下の記事が参考になると思います。

https://qiita.com/lain21/items/9f9f9707ebad4bbc627d

皆さんも、どんどん転移学習しましょう。

ラズパイで実行してみた

ラズパイでリアルタイム認識をしてみました。
コードはこちら

  • 使用機器:Raspberry Pi3 model B
  • 外付けGPU:未使用
  • V3 smallの$\alpha=0.2$
  • 入力画像サイズ:32 x 32 x 3

33tby-1m4wl.gif

だいたい15~20FPSの速度が出ました。
この速度なら、リアルタイムの処理もできそうです。

ちなみに、入力サイズ:96 x 96 x 3だと、10FPSになりました。

まとめ

  • MobileNet V3はV2の代替品になるわけではなく、延長線上という印象。すなわち、「V2よりも精度を出したいけど、多少処理速度が遅くなってもOK」というときに使うと良い。
  • ただ、V3であっても速度はそこまで遅くはならない。
  • 最終的には、入力画像サイズを小さくすればV3の速度は上がるため、チューニングすれば、「V2と速度は同じだけど精度は上回る」という可能性もあり得る。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ラズパイに初めて触る人が、Slackから自宅のエアコンを制御する

はじまり

暑い夏が来る前に、家に着く前にエアコンをつけれる環境が欲しい。
あと、ラズパイ触ってみたい。

そんな時に、格安スマートリモコンの作り方を発見
部品と金額まで書いてある…

ラズパイを用いて、Slackから自宅のエアコンを制御してみよう!!!

ラズパイでリモコンを作る記事は溢れているのになぜ書いたのか

やってみたら、意外と詰まった。

その際、記事を探し回ったので、助けられた内容をまとめておく。

初めてなりの悩み

触り慣れている人からしたら当たり前が詰まっているかもしれないけど、初めて触る人の参考になればと。

ShellとPythonがわかれば良いくらいにしたい(今わかるのそれくらい)

今回の目的はお家の外からエアコンを制御するです。
ラズパイでSlackbotを飼って(Raspberry PiでSlackbotを飼う)、赤外線を飛ばすくらいなら、Pythonのみでできました(まだ理解していないがゆえの不都合が考えられるのですが…)。

必要なもの

格安スマートリモコンの作り方に従う

作成手順

ラズパイのインストール・設定

あまり余計なものは買いたくないと思い、変換プラグ不要で設定できる方法を探した。
Raspberry Pi Zero Wをディスプレイやキーボードなしで初期設定、Wi-Fi接続がわかりやすく、大変参考になった。

ラズパイの起動

電源をさしてから少し待たないと、sshできない。
当たり前のことではあるが、最初戸惑った。

ラズパイにPython及びPipをインストール

普段使っているのがpyenvだったので、同様の環境の方が楽だと思い、pyenvを導入しました。
RaspberryPiにpyenvを導入を参考にすれば、インストールできます(僕は3.6.6をインストールしました)。
pipのインストールはしなくても入ってました。

つまり、

% sudo apt-get install -y git openssl libssl-dev libbz2-dev libreadline-dev libsqlite3-dev 
% git clone git://github.com/yyuu/pyenv.git ~/.pyenv

Shellごとに合わせた設定をして、sourceする。

% pyenv install 3.6.6
% pyenv global 3.6.6

と、pythonもpipも入ります。
ちなみに、今回pipでインストールしたのは

% pip install Slackbot pigpio

pyenv install x.x.x

すぐは終わらないです。
長そうだったので、朝出かける前にインストールをはじめて半日後帰宅して確認したら、終わってました。

赤外線送受信回路作成

格安スマートリモコンの作り方及び赤外線LEDドライブ回路の決定版 | 電脳伝説の通りです。

回路におけるLEDの向きについて

これも当たり前かもしれないですが、長い方がアノード、短い方がカソードです。
今回の場合、短い方がGNDと接続するようにしてください。

ラズパイで回路を制御

格安スマートリモコンの作り方を参考に、pigpioを用いた制御を行います。

インストール及び起動設定

% sudo apt-get update
% sudo apt-get upgrade
% sudo apt-get install pigpio
% sudo systemctl enable pigpiod.service
% sudo systemctl start pigpiod
% pip install pigpio

GPIOの設定

% echo 'm 17 w   w 17 0   m 18 r   pud 18 u' > /dev/pigpio
% crontab -e
@reboot until echo 'm 17 w   w 17 0   m 18 r   pud 18 u' > /dev/pigpio; do sleep 1s; done

pigpioによる送受信プログラム

pigpioの作者自らが作ったIR Record and Playbackをベースにしました。

格安スマートリモコンの作り方の手法をそのまま踏襲すると、pythonコード内でsubprocessとしてirrp.pyを呼び出すことになります。
そのため、irrp.pyをclass化して、pythonのコード内で呼び出せるようにしました(多分、subprocessとして呼び出してもうまく行くと思います)。
この際、引数として、オプションで取っていた値を与えるようにしています。

学習と送信に関しては、格安スマートリモコンの作り方に記載がある通りです。

pigpio.error: 'chain is too long'

我が家のエアコン(ダイキン)の信号は長すぎたようです。
ラズパイでエアコン対応の赤外線学習リモコンを pigpioライブラリを使って作る方法、Goodbye LIRC (Raspbian Stretchでエアコン対応の赤外線リモコンを動かす、ややこしい LIRCを使わないで IR機器を制御)を参考に、対応しました。
端的に述べると、学習した信号を三つに分割して送信するようにしています。
学習した信号をどこで切るか等はリンク先を参考にしてください。

Slackbotの導入

Raspberry PiでSlackbotを飼うを参考にしました。
とりあえず以下のような感じで、RecordとPlayを行うようにしています。

SlackBotPlugin.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import re
import pigpio
from slackbot.bot import listen_to, respond_to
from irrp_class import irrp


@listen_to('Record .*')
@respond_to('Record .*')
def record(message, *something):
    pi = pigpio.pi()
    if not pi.connected:
        exit(0)

    name = message.body['text'].replace('Record ', '')
    args = {
        'gpio': 18,
        'file': 'pigpio.json',
        'id': name,
        'freq': 38.0,
        'gap': 100,
        'glitch': 100,
        'post': 130,
        'pre': 200,
        'short': 10,
        'tolerance': 15,
        'verbose': False,
        'no_confirm': True
    }
    ir = irrp(pi, args)
    ir.record()

    message.reply(f'Recorded {name}')

    pi.stop()


@listen_to('Play .*')
@respond_to('Play .*')
def play(message, *something):
    

実際、Slackでやってみると、こんな感じです。
ss 2019-07-24 11.16.23.png

完成!!!

じゃあ、早速試そうと思い使ってみたものの、
学習はうまくいったけど、送信できない…(pigpio.error: 'chain is too long'とエラーが出ました)

そのエラーを上記のように解決した結果、
送信は行われているようだが、エアコンがピクリともしない…

一旦落ち着いて、テレビのOnOffをやらせてみると、あっさりできる…

送信は正常に行われていることがわかったので、エアコンにラズパイを近づけてみると…(30 cmくらいの距離)
クーラーがついた!

とは言え、そんな距離に電源を確保するのも大変…と思い、色々試した結果。
2 mくらいの距離でもうまくいきました。
重要なのはLEDの向きでした(当たり前だろって感じだと思いますけど)。

まとめると

LEDがエアコンにきちんと向いていれば、2 mくらいの距離でもエアコンは信号を受信してくれました(電源は5V/2.4A)。

最後に

これで無事夏を乗り切れそうな気がする!(ちょうど今日から地獄のような暑さが到来してますね)

デバイスに触れて作業するのは久々でしたけど、なかなか楽しかったですね。
今度はラズパイ + カメラにチャレンジしようと思います。

以下の問題を抱えている気がするので、今後考えます。
* 連続稼働による影響
* ブレッドボードのままなのでユニバーサル基板に移行してハンダ付け
* ラズパイへの理解が未熟であることに起因する何か

実際使っているコードは載せれるといいなぁと思っています。
参照した記事を示して、説明は参照先を見てください状態なので、書き直した方が良いのかなぁとも思っています。

Ref.

  1. 格安スマートリモコンの作り方
  2. Raspberry PiでSlackbotを飼う
  3. Raspberry Pi Zero Wをディスプレイやキーボードなしで初期設定、Wi-Fi接続
  4. RaspberryPiにpyenvを導入
  5. 赤外線LEDドライブ回路の決定版 | 電脳伝説
  6. ラズパイでエアコン対応の赤外線学習リモコンを pigpioライブラリを使って作る方法、Goodbye LIRC (Raspbian Stretchでエアコン対応の赤外線リモコンを動かす、ややこしい LIRCを使わないで IR機器を制御)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python

ただの実験投稿です。

よろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む