静かなる名辞

pythonとプログラミングのこと


sklearnのLabelEncoderとOneHotEncoderの使い方

 注意

 この記事で取り扱ったモデルの仕様がsklearn 0.20から変更された結果、この記事の内容はもはやあまり役に立たなくなっています。

 この記事は記録として残しますが、最新の仕様については下リンクの記事を御覧ください。

www.haya-programming.com

 以下のこの記事の記述は古い仕様に基づいており、また内容的にも若干不完全な部分があります。お読みになる方は、その点についてご承知くださいますようお願いします。

 2018年12月2日


スポンサーリンク



はじめに

 sklearnのLabelEncoderとOneHotEncoderは、カテゴリデータを取り扱うときに大活躍します。シチュエーションとしては、

  • なんかぐちゃぐちゃとカテゴリデータがある特徴量をとにかくなんとかしてしまいたい
  • 教師ラベルがカテゴリデータなので数値ラベルにしたい

 こんなとき使えます。

 使い方は簡単なのですが、備忘録としてまとめておきます。

LabelEncoderの使い方

 厳密にはsklearn.preprocessing.LabelEncoderですね。

sklearn.preprocessing.LabelEncoder — scikit-learn 0.20.2 documentation

 必要なことは公式サンプルにぜんぶ書いてあるのですが、自分でも使ってみましょう。

>>> from sklearn.preprocessing import LabelEncoder
>>> week_breakfast = ["パン","ご飯","なし","パン","シリアル","なし","なし"]
>>> le = LabelEncoder()
>>> labels = le.fit_transform(week_breakfast)
>>> labels
array([3, 0, 1, 3, 2, 1, 1])

 このように変換できます。

 ラベルから元のカテゴリに変換するには?

>>> le.classes_  # indexとカテゴリが対応したnumpy配列になっていることを確認
array(['ご飯', 'なし', 'シリアル', 'パン'], dtype='<U4')
>>> [le.classes_[x] for x in labels]    # リスト内包表記を活用する(最速かどうかはよくわからない)
['パン', 'ご飯', 'なし', 'パン', 'シリアル', 'なし', 'なし']
>>> week_breakfast  # 確認のため再掲(私の食生活とかではありません)
['パン', 'ご飯', 'なし', 'パン', 'シリアル', 'なし', 'なし']

 普通にできますね。

 実際にはこういうことはしないと思います。その代わり、le.classes_をいろいろなものに渡して使うことができます。

 たとえば、分類をしてsklearn.metrics.classification_reportで見てみたいと思ったときに、

>>> from sklearn.metrics import classification_report
>>> print(classification_report(labels, labels))  # とりあえず両方同じlabelsを渡している
             precision    recall  f1-score   support

          0       1.00      1.00      1.00         1
          1       1.00      1.00      1.00         3
          2       1.00      1.00      1.00         1
          3       1.00      1.00      1.00         2

avg / total       1.00      1.00      1.00         7

 0,1,2,3だとわかりづらいですね。でもclassification_reportにはtarget_namesという引数があり、

>>> print(classification_report(labels, labels, target_names=le.classes_))
             precision    recall  f1-score   support

         ご飯       1.00      1.00      1.00         1
         なし       1.00      1.00      1.00         3
       シリアル       1.00      1.00      1.00         1
         パン       1.00      1.00      1.00         2

avg / total       1.00      1.00      1.00         7

 こうしてやることができる訳です。なので、LabelEncoderのインスタンスは、大切に(プログラムのスコープ上とかに)取っておきましょう。

 参考:
sklearn.metrics.classification_report — scikit-learn 0.20.2 documentation
sklearnのclassification_reportで多クラス分類の結果を簡単に見る - 静かなる名辞
 (二番目は自分の記事)

OneHotEncoderの使い方

 これはいわゆるOne-hot表現を得るものです。いろいろな機械学習フレームワークに類似の機能があると思いますが、sklearnではsklearn.preprocessing.OneHotEncoderが対応します。

 使い方は以下の通りです。

>>> import numpy as np
>>> data = np.arange(9).reshape(9,1)
>>> data
array([[0],
       [1],
       [2],
       [3],
       [4],
       [5],
       [6],
       [7],
       [8]])
>>> from sklearn.preprocessing import OneHotEncoder
>>> ohe = OneHotEncoder()
>>> ohe.fit_transform(data).A  # sparse matrixを返しやがるのでdenseにして見ている
array([[1., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 1., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 1., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 1., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 1., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 1., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 1.]])

 注意点として、ndim=1の配列を渡すとエラーになるので、ベクトル風の表現に直して渡してやる必要があります。

>>> ohe.fit_transform(np.arange(10))
# 中略
ValueError: Expected 2D array, got 1D array instead:
array=[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.].
Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.
>>> ohe.fit_transform(np.arange(10).reshape(-1, 1)).A
array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 1., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]])

 ところで、ベクトル風にして渡すということは、こういうことになります。

>>> np.arange(10).reshape(5,2)
array([[0, 1],
       [2, 3],
       [4, 5],
       [6, 7],
       [8, 9]])
>>> ohe.fit_transform(np.arange(10).reshape(5,2)).A
array([[1., 0., 0., 0., 0., 1., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 1., 0., 0., 0.],
       [0., 0., 1., 0., 0., 0., 0., 1., 0., 0.],
       [0., 0., 0., 1., 0., 0., 0., 0., 1., 0.],
       [0., 0., 0., 0., 1., 0., 0., 0., 0., 1.]])

 なるほど、こうなるのか。

 文字列でもいけるかな?

>>> week_breakfast = ["パン","ご飯","なし","パン","シリアル","なし","なし"]
>>> ohe.fit_transform(np.array(week_breakfast).reshape(-1, 1))
# 中略
ValueError: could not convert string to float: 'パン'

 ダメでした。LabelEncoderと併用して数値ラベルにしておく必要があるということだと思います(ドキュメントにもそんな感じのことが書いてある)。

おまけ:CategoricalEncoder

 これは開発中のscikit-learn 0.20の機能です。なので、まだ使えません。リリース待ちです(2018年6月現在)。

http://scikit-learn.org/dev/modules/generated/sklearn.preprocessing.CategoricalEncoder.html

 いろいろと柔軟に使えるような機能が追加されているようです。リリースされたら、こっちも使ってみましょう(つっても、まだまだ時間かかりそうよねぇ)。

 2018年11月15日追記:sklearn 0.20はリリースされましたが、CategoricalEncoderはなくなっちゃったみたいです・・・。残念。

【python】反転させて先頭n個取るスライス

 タイトルの通りのものが必要になりました。一体どう書くのでしょう?

とりあえず反転させる

>>> lst = list(range(20))
>>> lst[::-1]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

 ま、これは常識(python廃人の皆さんには)。

n個取ってみる

>>> lst[::-1][:10]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10]

 こうするとリストオブジェクトの生成が二回繰り返されるので遅いはずです。

 できればスライス一発で済ませたい。

やってみる

>>> lst[:9:-1]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10]

 なんとなくできたような気になりますが、

>>> lst[:3:-1]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4]

 逆向きなのでした。スライスも左から順に評価される(切り出してから反転)のでしょうか。

正しい(?)やりかた

>>> lst[:-4:-1]
[19, 18, 17]

 微妙な違和感が・・・

先頭n個取って反転

 これもやってみよう。考え方は同じです。

>>> lst[2::-1]
[2, 1, 0]

測ってみた

>>> import timeit
>>> timeit.timeit(lambda : lst[::-1][:3])
0.35431641300010597
>>> timeit.timeit(lambda : lst[:-4:-1])
0.20682314900022902

 計測誤差ではないようです。かくして約43%の高速化が実現されました。

追記:書き上げて投稿してから思いついた別解

>>> lst[:-4:-1]
[19, 18, 17]
>>> lst[-3:][::-1]
[19, 18, 17]

 結果は同じ。これだと反転するリストが小さいので速いのでは? と思いました。

 効果を見やすくするために、長さ1000のリストで試してみます。

>>> timeit.timeit(lambda : lst[:-4:-1])
0.21827364299997498
>>> timeit.timeit(lambda : lst[::-1][:3])
2.8037329549997594
>>> timeit.timeit(lambda : lst[-3:][::-1])
0.33725221500026237

 早い順に、

  1. 1つのスライスで反転と切り出しをやる
  2. 後ろ3つを取ってから反転
  3. ぜんぶ反転させてから先頭3つを取る

 となりました。

 「ぜんぶ反転させてから~」が遅いのはまあ、予想通りですが、「後ろ3つを取ってから~」は1つのスライスでやる方法に勝てておらず、リストが小さいときの「ぜんぶ反転させてから~」と同程度。つまりオーバーヘッド分が同程度あるということです。また、built-inのスライスはそうとう気が効いてるみたいです(恐らく、lenを見てどの順番で切っていくか決めているのでは? 確認していませんが・・・)。

 結論としては、とにかくできるだけ1つのスライスで書いちゃおう、ということになると思います。ただし、実際には大した時間じゃないので、可読性重視で分けるのも不可ではない、という程度です。

【python】べき乗とべき根の計算

 べき乗はx^n、べき根は\sqrt[n]{x}です。では、pythonではどう書くのでしょうか。

 2乗とかsqrtくらいはわかっても、n乗根あたりになるとすぐ出てこないという人も多いのでは? そこで、説明を書きます。

 目次

スポンサーリンク



組み込み関数powを使う方法

 powという組み込み関数があります。まあ、要は達します。

>>> pow(2, 10)
1024
>>> pow(0.5, 10)
0.0009765625
>>> pow(1.5, 3.5)
4.133513940946613

 見たところ浮動小数点でも大丈夫。

 ドキュメントによると、第三引数を指定することで「x の y 乗に対する z の剰余」を計算できるそうですが、何に使えるのでしょうかね・・・。

組み込み関数 — Python 3.7.3 ドキュメント

 2018年11月26日追記:
 第三引数の件についてコメントでご指摘をいただきました。これを効率的に計算できることで、公開鍵暗号へ応用できるということのようです。

記事の中で「第三引数を指定することで「x の y 乗に対する z の剰余」を計算できる」のが何のためだろうと書かれていたので、回答になるかもと思いコメントします。
共通鍵暗号の鍵を通信相手と交換する方法に、デフィー・ヘルマン鍵交換という方式があります。その方式では、
(1) Ya = r の Xa乗 に対する q の余剰(Ya: ユーザaの公開鍵, r: qの原始根, Xa: ユーザaの秘密鍵, q: 素数)
(2) K = Ya の Xb乗 に対する q の余剰(K: 共通鍵, Ya: ユーザaの公開鍵, Xb: ユーザbの秘密鍵, q: 素数)
という計算が出てきます。

powを使えば例えば(1)は Ya = pow (r, Xa, q) だけで計算できるということです。

(tomoさんのコメントより引用)

 関連しそうなwikipediaのページを貼っておきます。

冪剰余 - Wikipedia
ディフィー・ヘルマン鍵共有 - Wikipedia
離散対数 - Wikipedia

べき乗演算子を使う方法

 べき乗の計算では、pow関数と同じことが演算子でもできます。

>>> 2**10
1024
>>> 0.5**10
0.0009765625
>>> 1.5**3.5
4.133513940946613

 powを多用すると式がごちゃごちゃするので、こちらを使いたいところです。

 ただ、罠があるようです。

Python のべき乗演算子に潜む罠 | CUBE SUGAR STORAGE

 単項演算子の-などと組み合わせる場合、べき乗演算子が先に評価されます。どういうこと? と思うかもしれませんが、要するに

>>> -1**2  # WTF!?
-1
>>> -(1**2)  # つまりはこういうこと
-1
>>> (-1)**2  # 本当に欲しかったもの
1

 思い通りの結果を得たければカッコを多用すべきであり、そうするとなんかpowでも良いような気もしてきます。

numpyに頼る方法

 np.powerがあります。

>>> np.power(2, 10)
1024
>>> np.power(0.5, 10)
0.0009765625
>>> np.power(1.5, 3.5)
4.133513940946613

 見たところ、型の取扱も含めてほぼ同じ。まあ、これを単体で使うメリットは特に感じません。numpy配列を相手にするなら、ありかも(ただしnumpy配列にべき乗演算子を使うという選択肢もある)。なお、片方が配列で片方がスカラー、とか配列同士、というケースでは、

>>> import numpy as np
>>> np.power([1,2,3], 2)
array([1, 4, 9])
>>> np.power(2, [1,2,3])
array([2, 4, 8])
>>> np.power([1,2,3], [4,5,6])
array([  1,  32, 729])

 こんな扱いになります。特に難しいことはないです。

n乗根について

 n乗根(べき根)がわからなかった人は、\sqrt[2]{x} = x^\frac{1}{2}を思い出しましょう。

>>> 2**10  # べき乗(10乗)
1024
>>> 1024**(1/10)  # べき根(10乗根)
2.0

 小数のべき乗が計算できれば、べき根も計算できるということです。

どれが速いの?

 timeitで簡単に比較してみます。私はIPython使いではないので、モジュールimportで使います。

>>> import numpy as np
>>> import timeit
>>> timeit.timeit(lambda : [[x**y for y in range(30)] for x in range(30)], number=1000)
0.37399847699998645
>>> timeit.timeit(lambda : [[pow(x,y) for y in range(30)] for x in range(30)], number=1000)
0.4485901359998934
>>> timeit.timeit(lambda : [[np.power(x,y) for y in range(30)] for x in range(30)], number=1000)
1.3186961119999978

 スカラーに対してはべき乗演算子一択。

 numpy配列だと、べき乗演算子とnp.powerはどちらが上でしょうか。

>>> timeit.timeit(lambda : np.arange(1000)**np.arange(1000), number=100000)
1.4120757860000595
>>> timeit.timeit(lambda : np.power(np.arange(1000), np.arange(1000)), number=100000)
1.402805570000055

 そもそも冗談みたいに速いんですが(numberに注目)、速度差自体は計測誤差レベル。一番コアな部分は同じ処理なのでしょう。

まとめ

 pythonではべき乗・べき根を計算する方法が何通りかあります。とはいえ、べき乗演算子で大抵の用途では用が済むようなので、罠に気をつけて(負の数が絡むときだけ気をつける)使えば良いと思いました。

 あと、numpyはやっぱりすごいなぁ・・・

python環境構築まとめ

はじめに

 pythonは最近よく流行っているスクリプト言語ですが、残念ながら環境構築のとっつきづらさは他の言語の比ではないと思います*1。初心者が変な環境を作ってトラブルの元になる・・・というのもよくあることなので、この際まとめておこうと思いました。

 さて、まず大前提として、メインで使うpythonはpython3系にします。今からpython2系を学習する意味はほとんどないからです。

 また、この記事では環境構築に際して使う各ツールの操作手順については述べません。それぞれの構築方法についてご自身で検索していただければ良いと思います。日本語Web圏におおむね十分な情報があります*2

覚えておいてほしい大切なこと

 「自分でよくわからない/把握できない環境は作らない」

 これが基本です。

 ネットには無責任に「○○が良いですよ~」と書いてしまう人はたくさんいます。それを真に受けて、使い方もよくわからないツールを落としてきて入れ、記事に書いてある操作手順通りにインストールし、インストールはできたけど使用開始とほぼ同時に早速ハマる……というどうしようもないことをする初心者の方も一定数います。

 むやみにツールを入れないでください。入れるなら、入れる前に目的と使い方くらいは理解しておきましょう。そうしないと扱いきれません。

 実際、何も知らない初心者が、ネットの記事に書いてあるコマンドをコピペしてpyenvやanacondaを導入し、扱いきれなくて挫折する……という悲劇がたくさん起きています。

pyenvが必要かどうかフローチャート - Qiita
Pythonの環境管理ツール良し悪し - Zopfcode

python import errorに関連する質問・回答の検索結果(1ページ目)|teratail

 できるだけそういう事態になりそうな展開は回避し、単純な環境で構築してから他のツールに手を出した方が、失敗しづらいはずです。

pythonをやるのに向いたOS

 ご存知の通り、pythonはマルチプラットフォームなスクリプト言語です。しかしどうしても、OSによって向き不向きがあるようです。

windows

 おすすめ度:☆3
 率直に言って、おすすめできません。windowsだと余計な苦労が増えます。上級者は普通に使いこなせると思いますが、初心者向けではありません。

 それでも一時期に比べれば、wheelで入るライブラリも増えたし、UNIXに依存するライブラリというのもないので、簡単になった方です。エラーメッセージが出たときに、根気よく検索して対処できる人向けです。

 Bash on Ubuntu on Windowsは使ったことがないのでわかりません。ごめんなさい。

Mac OS

 おすすめ度:☆2
 これも、あまりおすすめしません。使い勝手自体はwindowsよりはマシですが、Web情報はwindowsより少なかったりします。Mac特有のハマり方があるので*3、少ない英語情報を必死に探って自己解決できる人でないとしんどいと思います。

 初心者は避けた方が無難です。「pythonでプログラミングを勉強したい!」と思ったとき、いきなりMacを買う理由はないということです。

linux系

 おすすめ度:☆4
 windows, macよりはおすすめです。初心者はlinux系を使えば良いと思います(pythonをやるついでにlinuxの勉強も出来て一石二鳥です)。ただ、まったく経験がないと敷居が高いのも事実です。

 windowsマシンの上に仮想マシン(後で述べるpython仮想環境とは別物です)で入れれば、環境ぶっ壊しちゃっても作り直せば良いだけで、とても気楽です。

 linux系にはディストリビューションがいろいろありますが、デファクトスタンダードはubuntuです。このことは、「ubuntu python 環境構築」とか検索すれば幾らでも記事が出てくる、ということを意味します。他のディストリビューションはそこまで強くないので、手を出すのは中級者以上になってからで良いと思います。

その他のOS

 そんなの使いたがる人はこの記事を読んでいないと思いますが・・・。

 FreeBSD使いの人はいるかもしれませんね。頑張ってください。応援しています。

 Android、iPhoneなどのスマホ・タブレットに入れたがる人も時々います。対応した実装もあるといえばあるのですが、あまり使いやすいものではありませんから、素直にノートパソコンを買った方が有利です。

Web上実行環境

 OSではありませんが、実はWeb上でpython(に限らずいろいろな言語)を実行できるサービスがあります。たとえばWandboxなんてどうでしょう。

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

 こういった環境は、勉強とか、ちょっとした動作チェックには重宝します。それ以上でもそれ以下でもないのですが、とりあえず取っ掛かりとしてはありです。簡単に試してみたい、というときはここに入門書の内容などを打ち込んでいくと良いと思います。

結論

 今使ってるwindowsマシンにVirtualBoxかVMWare Playerを落としてきて、ubuntuを入れましょう。

生のシステムのpythonを使う(システムに直接インストールしてそのまま使う)

 さて、ubuntuを入れた方はターミナルで「python」と打つといきなりpythonが立ち上がると思います。今どきのUNIX系OS(事実上linux+Mac)であれば最初からpythonが入っているので、それが使える訳です。

 これをそのまま使う、というのは選択肢の一つです。ただ、2018年6月現在では、まだシステムデフォルトのpythonがpython2系のシステムがほとんどだと思います。できればpython3系を使いたいので、これはボツです。もしデフォルトがpython3系のシステムにあたったら、しばらくはそれを使って基本的な構文や機能の勉強をやれば良いと思います。

 システムのpythonがpython2系だけどpython3系を使いたい、という場合、システムにpython3をインストールすることができます。この場合、python2を消したり、置き換えたりはせず、python2とpython3を共存させるようにするのが無難です。python2はシステムの中で使っているプログラムがいろいろあるので、消してしまうとOSがまともに動かなくなります。

 また、python2系がデフォルトだけど、python3系も一緒に入っている、というシステムもあります(最近のubuntuはそうだったはずです)。これはしめたもので、そのまま使えばインストールの手間が省けますし、インストールで事故る確率も減らせます。ただし、pipなどは自分で入れてやる必要があったりするようです*4

 おそらく、python2とpython3が共存する環境では、「python」とターミナルに打ち込むと平然とpython2が立ち上がってくるはずです。そういうときは、慌てずに「python3」と打ち込みます。そうすると(ちゃんとインストールされていれば)python3が立ち上がるので、使うことができます。

 また、パッケージマネージャのpipも、(インストールされていれば)「pip」と打つとpython2に対応するpipが立ち上がってくるはずです。同様にpip3と打てばpip3が立ち上がるので、こちらを使います。パッケージマネージャについては次章で詳しく述べます。

パッケージマネージャについて

 パッケージマネージャは、主に外部ライブラリなどを管理するためのツールです。

 2018年6月現在では、間違いなくpipがスタンダードです。大抵のものがpipで入ります。というか、pipで入らないものは他のパッケージマネージャでも扱えないので、pip以外を使う意味がまったくありません(というか、そもそも存在しているの? レベル)。

 例外として、anaconda(あとで述べます)を使う人はcondaというanaconda独自のツールを使う必要があります。

 また、pipで扱えない、ソースコードで配布されているだけのパッケージも存在します。そういうものはsetup.pyなど*5を使って環境に入れます。大抵はパッケージのreadmeとかで親切に説明してくれているので、その通りにやれば良いです。

 さて、pipと一口に言っても、実はコマンドの打ち方はいろいろあります。

 もしインストール手順などで「pip install ○○」と指示されていたら、それは「自分の環境に合った方法でpipを使って入れろ」ってことです。この通りコマンドを打っても、ほぼ入りません。

 以下に大まかな傾向を書いておきますが、あくまでも参考情報です。極論すれば、pip自体のありかがわかって、権限の問題が解決できれば、コマンドの細かい違いはどうでも良いことです。でも、使い分ける上ではこれらは重要です。

  • sudo pip

 これは普通はシステムのpythonにインストールする場合です(そういう認識で構いません。普通にaptとかでインストールすると権限が必要な場所に入ると思います)。

 sudoはunix系のコマンドであり、windowsだとsudoはないので、なにか違う方法で同様のことを実現する必要があります*6。また、python2とpython3を使い分けていれば「sudo pip3」というのもありえます。

  • pip

 ただのpip。仮想環境を使うと、これで済むと思います。逆に、仮想環境を使っていないとただのpipで済む場面はあまりないとも言えます。

  • pip2、pip3

 システムに異なるバリエーションのpythonを複数インストールして、仮想環境等を使わずに使い分ける場合、区別のために数字付きのpipコマンドを使うことになります。インストール先にもよりますが、sudoを付ける必要があるかもしれません。また、python3.5とpython3.6を使い分けたい、という場合、pip3.5とpip3.6を使い分けます。面倒くさいですね。

 自分が使っているpythonに入れるにはどれを使えば良いのか、把握しておきましょう。

「pipでインストールしたのに使えません!」
「違うpythonに入れてるみたいですね!」

 というやり取りは、割と頻繁に目にします。

仮想環境編

 この章はあんまり書きたくはないのですが・・・。

 システムのpython(インストールしたままのpython)を使うと、ミスって環境をぶっ壊してしまうリスクがあります。管理もいろいろ面倒です。そこで、仮想環境を使うと良いですよ、ということがいろいろなところで言われています*7

 ぶっちゃけた話、仮想環境を作るのは初心者にはコストが高いです。ネットで調べてその通り打ち込むだけといえば、その通りなのですが、それでもけっこうハマりどころがあります。なので、入門書程度の内容をやっている間は、仮想環境は要らないと思います。pipでライブラリをがつがつ入れたくなったときに、検討してみてください。

 また、げんなりすることに、仮想環境は何種類もあります。互換性もないし、使い方も違うしで、どれを選べば良いのかよくわかりませんよね。安心してください。ある程度定石があります。

定石

 venvかvirtualenvのどちらかを使う。

 これで良いと思います。それぞれについては追って説明します。

venv

 おすすめ度:☆5 
 最近のスタンダードです。が、実は次節のvirtualenvがpython3.3で標準モジュールとして取り入れられたものと解釈して、ほとんど間違いありません。

 venvはシステムにインストールされているpython3.3以上に依存します。というか、python3.3以上のpythonが環境に入っていれば、自動的に使えます(よほど変な環境構築をしていない限り)。なので、最近のubuntuならこれを使えば良いですし、自分でpython3を入れてvenv、というのもありです。

 特に使いづらいとかもなく、普通に使えます。

virtualenv

 おすすめ度:☆4 
 一昔前のスタンダードでした。venvがpython3.3に入って以降は存在意義が薄くなっていますが、システムにpython2系しかなく、というかそもそも使うのがpython2系だけです、みたいな状況で使うのは今でもありです。

 これは外部ライブラリなので、pipでインストールする必要があります。

 また、venvでできなくてvirtualenvでできることとして、pythonインタプリタ本体のバージョンを切り替える操作があります。複数のバージョンを共存させたい場合は、システムのpythonにvirtualenvを入れ、あとは好きなところに複数バージョンのpythonを入れ、仮想環境を作るといった使い方ができます。

pyenv

 おすすめ度:☆3 
 一昔前にちょっと流行っていました。未だに記事をたくさん見かけますが、今から使いたいかと言われると、正直イヤです。venvでいいと思います。

 長所はpythonに依存しないことです。そのかわり、シェルスクリプトに依存します。当然windowsでは動きません。これを欠点と呼ぶかは悩みますが(記事の最初でwindowsは斬って捨ててる訳だし)、とにかくそういう特徴があります。

anaconda

 おすすめ度:☆3
 anacondaは有名ですね。環境構築の手間が省けて、統計や機械学習用のライブラリまで丸ごと入れられる、という奴です。しかし、個人的には☆3を付けます。

 anacondaの最大の利点は、condaという独自のパッケージ管理システムを持つことで、オリジナルのpipと比べてもパッケージのインストールや仮想環境の操作が容易であることです。

 一方、欠点は、パッケージ管理周りがpipとはまったく別物なので、anaconda独自のハマりどころがたくさんある、ということです。ハマると面倒ですし、ちょっとマイナーな話題だとWebに情報があるかも怪しい、githubにissue上がっててもcloseしないまま何ヶ月も放置されてる、といった具合です。

onoz000.hatenablog.com


 端的にいうと、確かにanacondaの開発は頑張ってはいるけど、パーフェクトというほどの完成度ではないので、anacondaを使うと問題がややこしくなるケースというのが一定数存在します。どう判断するかは人によりますが、私は最初からpipを使った方がマシだと思います。

 それでも、環境構築のことは何も知らない人が、手軽に統計とか機械学習を試すためにあると思えは、anacondaは弁護できます。実際、numpyとかscipyとかcythonとかmatplotlibとかsklearnとかを自分でインストールしようとすると、それはそれで一筋縄ではいかないので、「その辺のライブラリをさっさと使いたいんです、授業が/研究が/研修が済んだらもうpythonなんか触らないんです」という状況であれば、anacondaはありです。

 逆に、長くpythonを使い続ける、いろいろなライブラリを入れたりして愛用していく、のであれば、やめた方が良いと個人的には思います。多少手間がかかっても、自分で普通に環境構築して、pipを使ったインストールに慣れた方が良いです。

 申し訳ありませんが、そういう認識です。

その他の仮想環境

 採点はしません。

virtualenvwrapper

 名前の通り、virtualenvのラッパーです。使い勝手が改善されているらしいですが、使ったことがないのでわかりません。ごめんなさい。

 生virtualenvを使いづらいと感じたことは特にないので、これを使うならvirtualenvで良いと思います。まあ、最終的には好みの問題ですが、多少コマンドが打ちやすくなるとかのために不安要素を増やすかというと、Noです。

pyenv-virtualenv

 pyenvでvirtualenvwrapperに相当するものです。これも恐らく要らないと思います。

pyvenv

 venvの別名。あまりにpyenvと紛らわしいため(だと思います)、非推奨にされたそうです。

venv --- 仮想環境の作成 — Python 3.7.4 ドキュメント

conda

 これはanacondaの管理ツールです。「conad install ○○」とかやって使うそうです。見た目はpipっぽいですが、中身はまったくの別物。

pipenv

 これは比較的新し目のツールです。pipとvirtualenvを統合したという、ちょっと触れ込みだけでは想像がつかないもの。まだ試していませんが、そのうち試してみます。

結論

 venvかvirtualenvのどちらかを使う。

 最初に言いたいことはまとめたので、特に追加で言うことはないです。

まとめ

 この記事に書いてあることをどこまで信頼するかは、あなた自身で決めてください。私は何も保証しません。それはあなたの責任です。

 この記事の内容自体はそんなに外していないとは思います。しかし、人によって考え方の違いもありますし、環境構築に正解なんてありません。究極的には動けば良いのです。せいぜい一つの指針にしてください。

 あと、間違いにお気づきになられた識者の方は、ご遠慮無くご指摘いただけると大変助かります。

*1:もちろん、探せばもっとひどい言語はあるだろうけど。スクリプト言語の平均よりは明らかに悪いという程度の意味

*2:信用に足る記事があるかはまた別ですが・・・新し目でまともそうなものを選ぼう、としか言えません

*3:というかライブラリとかがMacまでしっかり考えて作られていない・・・Macのパッケージマネージャもいまいちpythonに優しくない・・・

*4:参考:Ubuntu環境のPython - python.jp

*5:distutilsなどという。参考:distutils --- Python モジュールの構築とインストール — Python 3.7.4 ドキュメント

*6:もしかしたらただのpipかもしれないし、管理者権限でやる必要があるかもしれない。インストール先等で変わってきます

*7:仮想環境とは何ぞや、という基本的なことについてはこちらを参照:仮想環境 - python.jp

【python】listをforループで回してremoveしたら思い通りにならない


 forループでループ対象のリストから要素を削除してしまったりすると、まったく想定と違った結果になってしまうことがあります。

 pythonプログラミングを始めたばかりの人がよくハマるトラブルです。日本語Web圏にはイマイチよくまとまった記事がないようなので、まとめておきます。

スポンサーリンク



問題の概要

 たとえば、0から9のリストから偶数だけ取り出そうとして、こんなコードを書いてみます。

>>> lst = [0,1,2,3,4,5,6,7,8,9]
>>> for x in lst:
...     if x%2 != 0:
...         lst.remove(x)
... 
>>> lst
[0, 2, 4, 6, 8]

 一見すると上手く動いているようです。調子に乗って、今度は3の倍数を取り出そうとしてみます。

>>> lst = [0,1,2,3,4,5,6,7,8,9]
>>> for x in lst:
...     if x%3 != 0:
...         lst.remove(x)
... 
>>> lst
[0, 2, 3, 5, 6, 8, 9]

 おかしくなった。なぜでしょう? forがちゃんと動いていない? という感じで、ハマります。

原因

 こういうときはforのループごとにxに代入されている値をprintしてみると、どんなことになっているのかよくわかります。

>>> lst = [0,1,2,3,4,5,6,7,8,9]
>>> for x in lst:
...     print(x)
...     if x%2 != 0:
...         lst.remove(x)
... 
0
1
3
5
7
9
>>> lst
[0, 2, 4, 6, 8]
>>> lst = [0,1,2,3,4,5,6,7,8,9]
>>> for x in lst:
...     print(x)
...     if x%3 != 0:
...         lst.remove(x)
... 
0
1
3
4
6
7
9
>>> lst
[0, 2, 3, 5, 6, 8, 9]

 なんてことでしょう、ちゃんと動いていない!

 ・・・これはドキュメントにも書いてある、pythonのれっきとした仕様です。

注釈 ループ中でのシーケンスの変更には微妙な問題があります (これはミュータブルなシーケンス、すなわちリストなどでのみ起こります)。どの要素が次に使われるかを追跡するために、内部的なカウンタが使われており、このカウンタは反復のたびに加算されます。このカウンタがシーケンスの長さに達すると、ループは終了します。このことから、スイート中でシーケンスから現在の (または以前の) 要素を除去すると、(次の要素のインデクスは、すでに取り扱った現在の要素のインデクスになるために) 次の要素が飛ばされることになります。(※筆者強調) 同様に、スイート中でシーケンス中の現在の要素以前に要素を挿入すると、現在の要素がループの次の週で再度扱われることになります。こうした仕様は、厄介なバグにつながります。

8. 複合文 (compound statement) — Python 3.7.3 ドキュメント

 インタプリタの内部では、カウンタで管理しているんですね。削除しようとして失敗するのは、それが原因です。

回避策

 とりあえず、公式ドキュメントにはこのような方法が記載されています。

for x in a[:]:
    if x < 0: a.remove(x)

 ここで[:]というのは範囲指定なしのスライスです。これはこのように機能します。

>>> lst = [0,1,2,3,4,5,6,7,8,9]
>>> lst[:]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

 まったく無意味な気がしますが、実は両者は別のオブジェクトになっています。

 id()関数で確認してみます。

>>> id(lst)
140603063055816
>>> id(lst[:])
140603063055048

 つまり、[:]は中身の同じコピーを作ることができます。こうすればループの対象のリストは変更されないので、問題なくループを回せるという訳ですね。

 でもこういうコードはちょっとかっこ悪いので、内包表記を使った方がベターです。

>>> lst = [0,1,2,3,4,5,6,7,8,9]
>>> [x for x in lst if x%3 == 0]  # 条件の反転に注意(残すものの条件を指定する)
[0, 3, 6, 9]
>>> lst  # 上のコードは新しいリストを作る。元のリストは変わらない
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> lst = [x for x in lst if x%3 == 0]  # 再代入するとlstの値が変わる(ただし別のオブジェクトになる)
>>> lst
[0, 3, 6, 9]

 どうしても同じオブジェクトをいじらないといけない、というシチュエーションも稀にありますが、そうでなければ内包表記などを使って新しくリストを作る、という発想で書いた方が簡単ですし、へんなバグも生みません。

 余談ですが、pythonではlist.remove()はあまり使わないメソッドです。他にもlist.pop()やlist.insert()などリストを操作するメソッドはたくさんありますが、これらをforループと組み合わせて書くような操作は、大抵の場合は内包表記などで代用できます。そして、その方が元のリストを壊さないので、バグが発生する余地が減ります*1*2

 なので、初心者の方はあまりこういったものに頼らず、まずは内包表記から覚えるか、内包表記がとっつきづらければ空listにappendしていく方法を使うのが良いと思います。

>>> lst = [0,1,2,3,4,5,6,7,8,9]
>>> result = []
>>> for x in lst:
...     if x%3 == 0:
...         result.append(x)
... 
>>> result
[0, 3, 6, 9]

 これはappendで書く場合の例です。実はリスト内包表記とほとんど同じようなことをやっているのですが、最初はこちらの方が読みやすいかもしれません。

まとめ

 pythonってけっこう直感的じゃない仕様があるので、「なんで!?」と思うこともままありますね。でも、どうせ慣れれば、そういう仕様は使わないで済ませられるようになってくるので、大丈夫です。

 基本的には「listをforループで回すときは、回しているlist自体はいじらないで処理する」ことを心がけるようにしましょう。この考え方が大切です。

*1:この考え方はけっこう重要です。こういうオブジェクトの状態を変更する操作を破壊的操作といいますが、これはよく把握していないとわかりづらいバグを生みやすいです

*2:他にも、特にlist.remove()はけっこうコストが高い(該当する要素が見つかるまで線形探索する)という理由があり、嫌われがちなメソッドです

【python】pandasのDataFrameをLaTeX出力

 そんな機能があるらしい。DataFrame.to_latex()という名前のメソッドである。

pandas.DataFrame.to_latex — pandas 0.21.1 documentation

 これが使えると何かの役に立つかもしれないので、使い物になるかどうか確認してみる。

お試し

 とりあえず、てきとーにdfを作ってみる。中身に意味はないけど、意味のないdfをできるだけ手っ取り早く作りたかったのでnumpy配列から作っている。*1

>>> import numpy as np
>>> import pandas as pd
>>> df = pd.DataFrame(np.arange(32).reshape(8,4), columns=list("abcd"))
>>> df
    a   b   c   d
0   0   1   2   3
1   4   5   6   7
2   8   9  10  11
3  12  13  14  15
4  16  17  18  19
5  20  21  22  23
6  24  25  26  27
7  28  29  30  31

 そのまま何も考えず、to_latex()を呼ぶ。strで返っても都合が悪いのでprintしてみる。

>>> print(df.to_latex())
\begin{tabular}{lrrrr}
\toprule
{} &   a &   b &   c &   d \\
\midrule
0 &   0 &   1 &   2 &   3 \\
1 &   4 &   5 &   6 &   7 \\
2 &   8 &   9 &  10 &  11 \\
3 &  12 &  13 &  14 &  15 \\
4 &  16 &  17 &  18 &  19 \\
5 &  20 &  21 &  22 &  23 \\
6 &  24 &  25 &  26 &  27 \\
7 &  28 &  29 &  30 &  31 \\
\bottomrule
\end{tabular}

 そしたらこれを別途作ったTeXのソースに貼る。ドキュメント曰く、

Render an object to a tabular environment table. You can splice this into a LaTeX document. Requires \usepackage{booktabs}.

 (強調は僕が勝手に付けたもの)

 ということらしい。とにかく次のようなTeXファイルを作ってみた。

\documentclass{jsarticle}
\usepackage{booktabs}

\begin{document}

\begin{table}[h]
\begin{tabular}{lrrrr}
\toprule
{} &   a &   b &   c &   d \\
\midrule
0 &   0 &   1 &   2 &   3 \\
1 &   4 &   5 &   6 &   7 \\
2 &   8 &   9 &  10 &  11 \\
3 &  12 &  13 &  14 &  15 \\
4 &  16 &  17 &  18 &  19 \\
5 &  20 &  21 &  22 &  23 \\
6 &  24 &  25 &  26 &  27 \\
7 &  28 &  29 &  30 &  31 \\
\bottomrule
\end{tabular}
\end{table}

\end{document}

 TeXとかよくわからないけど、これでコンパイルできてこんな結果が得られた。

できた表

 なるほど、できてますね。

 フォーマットは、論文でよく見かける罫線の少ない表です。カッコいい気もするし、罫線多めのちょいダサな表の方が安心感があって良いような気もするという、人によって好みの分かれる奴です。

カスタマイズしてみよう

 たかがto_latex()なのに、なんかいろいろ引数があります。公式をまとめておきます。

  • bold_rows : boolean, default False

 インデックス列の文字がboldになる

  • column_format : str, default None

 \begin{tabular}{}の{}の中に入る列の書式を文字列で渡す

  • longtable : boolean, default will be read from the pandas config module Default: False

 TeXのlongtableだって。参考(外部サイト):[LaTeX]長い表を表示する - Qiita

  • escape : boolean, default will be read from the pandas config module Default: True.

 エスケープがうまく効くかどうかにかかってくるんだと思う

  • encoding : str, default None

 何も指定しないとpython2はascii, python3はutf-8になるらしい。

  • decimal : string, default ‘.’

 Character recognized as decimal separator, e.g. ‘,’ in Europe.
 (説明を読んでもよくわからん)

  • multicolumn : boolean, default True

Use multicolumn to enhance MultiIndex columns. The default will be read from the config module.

  • multicolumn_format : str, default ‘l’

The alignment for multicolumns, similar to column_format The default will be read from the config module.

  • multirow : boolean, default False

Use multirow to enhance MultiIndex rows. Requires adding a \usepackage{multirow} to your LaTeX preamble. Will print centered labels (instead of top-aligned) across the contained rows, separating groups via clines. The default will be read from the pandas config module.

 上の3つは使い方がよくわからない。まあ、たぶん使えば使えるんだろう。

 せっかくなので、インデックス列bold、罫線多めな表を作ってみようと思う。見た目がダサくなるはずだ。

>>> print(df.to_latex(bold_rows=True, column_format="|l|l|l|l|"))
\begin{tabular}{|l|l|l|l|}
\toprule
{} &   a &   b &   c &   d \\
\midrule
\textbf{0} &   0 &   1 &   2 &   3 \\
\textbf{1} &   4 &   5 &   6 &   7 \\
\textbf{2} &   8 &   9 &  10 &  11 \\
\textbf{3} &  12 &  13 &  14 &  15 \\
\textbf{4} &  16 &  17 &  18 &  19 \\
\textbf{5} &  20 &  21 &  22 &  23 \\
\textbf{6} &  24 &  25 &  26 &  27 \\
\textbf{7} &  28 &  29 &  30 &  31 \\
\bottomrule
\end{tabular}
\documentclass{jsarticle}
\usepackage{booktabs}

\begin{document}

\begin{table}[h]
\begin{tabular}{|l|l|l|l|l|}
\toprule
{} &   a &   b &   c &   d \\
\midrule
\textbf{0} &   0 &   1 &   2 &   3 \\
\textbf{1} &   4 &   5 &   6 &   7 \\
\textbf{2} &   8 &   9 &  10 &  11 \\
\textbf{3} &  12 &  13 &  14 &  15 \\
\textbf{4} &  16 &  17 &  18 &  19 \\
\textbf{5} &  20 &  21 &  22 &  23 \\
\textbf{6} &  24 &  25 &  26 &  27 \\
\textbf{7} &  28 &  29 &  30 &  31 \\
\bottomrule
\end{tabular}
\end{table}

\end{document}

 結果は、

f:id:hayataka2049:20180531015049p:plain

 なんか思ってたのと違う・・・\*rule系と縦罫線の相性が悪いので、\hlineに変えてみる(TeXソースを直接いじって)。

\documentclass{jsarticle}
\usepackage{booktabs}

\begin{document}

\begin{table}[h]
\begin{tabular}{|l|l|l|l|l|}
\hline
{} &   a &   b &   c &   d \\
\hline
\textbf{0} &   0 &   1 &   2 &   3 \\
\textbf{1} &   4 &   5 &   6 &   7 \\
\textbf{2} &   8 &   9 &  10 &  11 \\
\textbf{3} &  12 &  13 &  14 &  15 \\
\textbf{4} &  16 &  17 &  18 &  19 \\
\textbf{5} &  20 &  21 &  22 &  23 \\
\textbf{6} &  24 &  25 &  26 &  27 \\
\textbf{7} &  28 &  29 &  30 &  31 \\
\hline
\end{tabular}
\end{table}

\end{document}

f:id:hayataka2049:20180531015313p:plain

 これは期待通りの結果だが、わざわざpandasが出力されるものをいじってこうしたいか? と考えると、デフォルトで吐き出されたものをそのまま使った方が潔いかもしれない。

まとめ

 使えるか? というと、とても微妙な機能ですが、考えようによっては、データをDataFrameに入れさえすれば、TeXの表組みと格闘する必要が一切なくなります。
(デフォルトで出てきた表の見た目に満足できれば)

 なので、それなりにおすすめです。

*1:pandasの機能を試すときって、試すためのdf作るのがそもそも面倒くさいということが往々にしてある。みんなはどうやってるんだろうか

VMware Playerでキャッシュを削除して仮想ディスクの容量を空ける(linux)

 VMware Playerはホストとゲスト間で、ドラッグ・アンド・ドロップやコピ・アンド・ペーストによってファイルを移動できる。

 便利な機能なのでつい頻繁に使ってしまうが、これは腹立たしいことにゲストの仮想ディスク上にキャッシュを生成する。

 そしてこのキャッシュはなぜか勝手に消えてくれないので、気がつくとかなりディスク容量を圧迫してたりする。

 その消し方を備忘録としてメモ。

  1. ~/.cache/vmware/drag_and_drop/をまるごと消す

 以上。他にやることは特にない。

 僕が使っているのはubuntuだけど、たぶんlinux系なら何でも同じ場所にあるんだと思う(未確認)。もしかしたらVMware Playerのバージョンによっては場所が違うとかあるかもしれないけど、そのときはvmwareって名前の付いたディレクトリを検索すれば出てくると思う。

 放って置くとどんどん大きくなるので、たまに消してあげよう。これは仮想ディスクがいっぱいになっちゃった! というとき、とりあえず容量を空ける方法としても役に立つ。

【python】その矛盾した__eq__は・・・

私は疑問を持った

 pythonでは比較演算子==を使うと、内部的には__eq__メソッドが呼ばれる。

 ここから、素朴な疑問が生じる。比較演算子は二項演算子なので、2つのオブジェクトに対して適用される。

 どちらのオブジェクトの__eq__が呼ばれるのだろう? また、もし2つのオブジェクトの__eq__が違う値を返すとしたら、一体どんな事態が生じるのだろう? まずいことにならないのだろうか?

 ドキュメントにはちゃんとこのことが書いてある。

x==y は x.__eq__(y) を呼び出します

 3. データモデル — Python 3.6.5 ドキュメント

 これだけ。いまいち納得がいかない。

検証した

 こんなコードを書いてみた。

class Hoge:
    def __init__(self, name):
        self.name = name
    def __eq__(self, other):
        print("__eq__ of Hoge!  by", self.name)
        return True

class Fuga:
    def __init__(self, name):
        self.name = name
    def __eq__(self, other):
        print("__eq__ of Fuga!  by", self.name)
        return False

h1 = Hoge("h1")
h2 = Hoge("h2")
f1 = Fuga("f1")
print("h1 == h1")
print(h1 == h1)
print("h1 == h2")
print(h1 == h2)
print("h2 == h1")
print(h2 == h1)
print("h1 == f1")
print(h1 == f1)
print("f1 == h1")
print(f1 == h1)

 常にTrueを返す__eq__を持つHogeクラス、常にFalseを返す__eq__を持つFugaクラスを定義し、片っ端から比較している。どの__eq__がprintされたかもこれでわかるはずだ。

 結果は、こんなものだった。

h1 == h1
__eq__ of Hoge!  by h1
True
h1 == h2
__eq__ of Hoge!  by h1
True
h2 == h1
__eq__ of Hoge!  by h2
True
h1 == f1
__eq__ of Hoge!  by h1
True
f1 == h1
__eq__ of Fuga!  by f1
False

 とりあえず、興味深いのは二回printされたりはまったくしなかったこと。そして、常に左側のオブジェクトの__eq__メソッドが呼ばれているように見えること。

 要するにドキュメントの「x==y は x.__eq__(y) を呼び出します」は極めて正しく、それ以外の動作はまったくないということだ。

 意外な感じがする・・・。

まずい事態

 ということは、__eq__を好き勝手に拡張しているpython外部ライブラリとかだと、まずい事態が生じるのではないだろうか。

 たとえばnumpyのarrayをintと比較すると、次のように動作する。

>>> import numpy as np
>>> a = np.array([0,0,0,1,1,1,2,2,2])
>>> a == 0
array([ True,  True,  True, False, False, False, False, False, False])

 慣れない頃は不思議な感じがしていたが、これはnumpy.ndarrayクラスの__eq__がbooleanのndarrayを返すように書かれているからだ、と考えるとそれほどおかしくはない。

 ここまでは良い。だけど、このコードで0==aとするとintクラスの__eq__が呼ばれて違う結果が出てしまうのでは?

>>> 0 == a
array([ True,  True,  True, False, False, False, False, False, False])

 理解不能すぎる・・・。

 いやまてよ、pythonのintがndarrayに忖度しているだけなのでは? と思い、次のコードを実行してみた。

>>> class Hoge:
...     def __init__(self, name):
...         self.name = name
...     def __eq__(self, other):
...         print("__eq__ of Hoge!  by", self.name)
...         return True
... 
>>> h = Hoge("h")
>>> a == h
__eq__ of Hoge!  by h
__eq__ of Hoge!  by h
__eq__ of Hoge!  by h
__eq__ of Hoge!  by h
__eq__ of Hoge!  by h
__eq__ of Hoge!  by h
__eq__ of Hoge!  by h
__eq__ of Hoge!  by h
__eq__ of Hoge!  by h
array([ True,  True,  True,  True,  True,  True,  True,  True,  True])
>>> h == a
__eq__ of Hoge!  by h
True

 とりあえず、a == hで__eq__ of Hoge! by hがいっぱい出てきたことは理解可能。numpy.ndarray.__eq__は各要素に対してotherの__eq__を呼ぶ感じに動作しているのだろう。

 h == aは普通にTrueになった。ということは、intとは違う動作になっている。intの方は、一体どんな挙動なのだろう?

 ググったら出てきた。

It works because int.__eq__() returns NotImplemented and when that happens it results in a call to other.__eq__(self) and that's what is returning True and False here.

python - Why is `int.__eq__(other)` a working comparison? - Stack Overflow

 NotImplementedはpythonの組み込み定数らしい。

3. 組み込み定数 — Python 3.6.7 ドキュメント

 intの__eq__は(int型、およびその他のintと直接比較できることになっている型以外との比較時には)NotImplementedを返す。これが返ると、otherの__eq__が呼ばれ、判定が行われる、とか。

 なるほど、実験してみよう!

>>> obj = 0
>>> obj.__eq__(h)
NotImplemented
>>> obj == h
__eq__ of Hoge!  by h
True

 すげえ、よくできてる・・・。つまり、intの忖度でもなんでもなく、この機能(NotImplementedが返ったときの挙動)をうまく使ってnumpy.ndarrayは実装されていたのだった*1

 逆に、自作オブジェクトでやるときは、想定していない型が来たときはNotImplementedを返すみたいな注意が必要という話でもあるが。

まとめ

 ま、私が心配する程度のことは、聡明なpython開発陣の皆さんはしっかり対処しておられるのでした。つーか、これを考えた人たちは本当に頭が良い。とてもよく出来たシステムだと思う。

 逆に、一瞬で終わりそうな比較なのに内部ではメソッドがバンバン呼ばれていて、そのコストは実はけっこう高いという現実も見てしまった感も、あるといえばある。動的型付け言語だから、仕方ないのだけど。

 とにかく、これで安心して__eq__を使えるようになったね!*2

*1:とすると、上の「__eq__ of Hoge! by h 」がいっぱい出てきたのも、おそらく一回np.int64の__eq__が呼ばれて、NotImplementedが返ってからHoge.__eq__が呼ばれる、という流れのような気がする。確証はない

*2:現実に実装する際どんな風にすれば良いのかについては、日本語圏だとこちらの記事が参考になりました:Pythonにおける同値性比較の実装

【python】MeanShiftのbandwidthを変えるとどうなるか実験してみた

 前回の記事ではMeanShiftクラスタリングを試してみました。

www.haya-programming.com

 このMeanShiftにはbandwidthというパラメータがあり、クラスタ数を決定する上で重要な役割を果たしているはずです。

 いまいち結果に納得がいかないというとき、bandwidthをいじって改善が見込めるのかどうか確認してみます。

プログラム

 例によってirisとwineで比較。簡単に書きました。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from sklearn.datasets import load_iris, load_wine
from sklearn.cluster import MeanShift, estimate_bandwidth
from sklearn.decomposition import PCA

def process(dataset, name):
    origin_bandwidth = estimate_bandwidth(dataset.data)
    rates = np.logspace(np.log10(0.2), np.log10(5), 11)
    fig, axes = plt.subplots(nrows=3, ncols=4, figsize=(24,18))

    PCA_X = PCA().fit_transform(dataset.data)
    for target in range(3):
        axes[0,0].scatter(PCA_X[dataset.target==target, 0],
                        PCA_X[dataset.target==target, 1],
                        c=cm.Paired(target/3))
    axes[0,0].set_title("original label", fontsize=28)

    for r, ax in zip(rates, axes.ravel()[1:]):
        ms = MeanShift(bandwidth=r*origin_bandwidth, n_jobs=-1)
        y = ms.fit_predict(dataset.data)
        n_cluster = ms.cluster_centers_.shape[0]
        for target in range(n_cluster):
            ax.scatter(PCA_X[y==target, 0],
                       PCA_X[y==target, 1],
                       c=cm.Paired(target/n_cluster))
        ax.set_title("r:{0:.3f} b:{1:.3f}".format(
            r, origin_bandwidth), fontsize=28)
    fig.savefig(name+".png")

def main():
    iris = load_iris()
    wine = load_wine()

    process(iris, "iris")
    process(wine, "wine")

if __name__ == "__main__":
    main()

 bandwidthをsklearn.cluster.estimate_bandwidthの推定値(デフォルトで用いられる値)の1/5倍から5倍まで変化させ、結果をプロットします。

結果

 プロットされた結果を示します。

 結果の図の見方は、まずタイトルが

  • b

 sklearn.cluster.estimate_bandwidthによる推定値

  • r

 かけた比率

 という風に対応しており、あとは便宜的に2次元上に主成分分析で写像した散布図が、クラスタごとに色分けされて出ています。一枚目が本来のクラスに基づく色分け、r=1の図が推定値による色分けです。

 まずiris。

iris.png
iris.png
 きれいに元通りになるrは今回見た中にはありませんでした。クラスタ数的にはr=0.525とr=0.725の間くらいで3クラスタになりそうですが、この図を見るとそれでうまく元通りまとまるかは疑問です。

 次にwine。

wine.png
wine.png
 こちらもうまく元通りにはならないようです。そもそもデータが悪いという話はあると思います。

結論

 確かにクラスタ数は変わるが、クラスタリングの良し悪しが改善するかはなんともいえないですね。

 データをスケーリングしたり、もっと色々頑張ると改善は見込めるかもしれません。

【python】numpyでの等比数列の作り方

 ※2018年9月2日追記

 以前にこんな記事を書きましたが、改めて確認したところnumpyの機能で普通に書けました。

 その方法は追記に記したので、本文は読み飛ばしてください。

 内容そのものは記録として残しておきます。


 等比数列がほしくなった。作り方をメモしておく。

 目次

スポンサーリンク



比が決まっている場合

 たとえば、3倍ずつ増えていく等比数列がほしいとしよう。

 これは次のように簡単に作れる。

>>> import numpy as np
>>> rates = np.ones(10) * 3
>>> rates
array([3., 3., 3., 3., 3., 3., 3., 3., 3., 3.])
>>> rates ** np.arange(10)
array([1.0000e+00, 3.0000e+00, 9.0000e+00, 2.7000e+01, 8.1000e+01,
       2.4300e+02, 7.2900e+02, 2.1870e+03, 6.5610e+03, 1.9683e+04])

 できたけど、これは面白くもなんともないのだった。

範囲とnが決まっている場合

 たとえば「1から10までの範囲で10個ほしい!」というパターン。これは一筋縄ではいかなさそうである。

 幸い、np.logspaceという便利そうなものがある。

 numpy.logspace — NumPy v1.16 Manual

 これを使えばたぶんできるだろう。

>>> import numpy as np
>>> np.logspace(1,10,10)
array([1.e+01, 1.e+02, 1.e+03, 1.e+04, 1.e+05, 1.e+06, 1.e+07, 1.e+08,
       1.e+09, 1.e+10])

 期待通り動かないのだった。baseというオプションがあり、default=10で、その1,2,3,...,9,10乗が得られている訳である。

 ちょっと意図と違うので、こうしてみた。

>>> np.logspace(0, np.log10(10), 10)
array([ 1.        ,  1.29154967,  1.66810054,  2.15443469,  2.7825594 ,
        3.59381366,  4.64158883,  5.9948425 ,  7.74263683, 10.        ])

 成功した。冷静に考えると、この場合はこれで良い(log10(10)は1だ・・・)。

>>> np.logspace(0, 1, 10)
array([ 1.        ,  1.29154967,  1.66810054,  2.15443469,  2.7825594 ,
        3.59381366,  4.64158883,  5.9948425 ,  7.74263683, 10.        ])

 でもまあ、log10を使う方法だと他の範囲にも対応できる。

>>> np.logspace(0, np.log10(20), 10)
array([ 1.        ,  1.39495079,  1.94588772,  2.71441762,  3.78647901,
        5.2819519 ,  7.368063  , 10.27808533, 14.33742329, 20.        ])

 たとえば23から54まで10個取りたい場合は、どうしたら良いのだろう? こうする。

>>> np.logspace(np.log10(23), np.log10(54), 10)
array([23.        , 25.28791009, 27.80340855, 30.56913458, 33.60997942,
       36.95331033, 40.62921692, 44.67078193, 49.114379  , 54.        ])

 これで一般化できたので、いつでも使えるようになったと思う。

追記

 何もしなくても等比数列を生成するnumpy.geomspaceが用意されていて、普通にこちらでできました。

numpy.geomspace — NumPy v1.16 Manual

>>> import numpy as np
>>> np.geomspace(1, 10, 10)
array([ 1.        ,  1.29154967,  1.66810054,  2.15443469,  2.7825594 ,
        3.59381366,  4.64158883,  5.9948425 ,  7.74263683, 10.        ])
>>> np.geomspace(23, 54, 10)
array([23.        , 25.28791009, 27.80340855, 30.56913458, 33.60997942,
       36.95331033, 40.62921692, 44.67078193, 49.114379  , 54.        ])

 geometric progressionという英単語に馴染みがなくて、記事を書いたときには見つけられなかったのですが……。

 精進しようと思います。

【python】sklearnのMeanShiftクラスタリングを試してみる

はじめに

 MeanShiftはクラスタリングアルゴリズム。クラスタ数を自動で決定してくれるという長所がある。

 理論的には最急降下法で各クラスタの極大点を探していく感じらしいです。わかりやすい解説があったので、リンクを張っておきます(ただし私自身はすべては読み込めていない)。

Mean Shift Clustering

 このMeanShiftはsklearnに実装されているので、簡単に試してみることができます。

 sklearn.cluster.MeanShift — scikit-learn 0.20.1 documentation

 sklearnのトイデータで遊んでみましょう。

 目次

使い方

 いつものsklearnのモデルです。fitしてpredictするだけ。

 いつだったかFuzzy C-Meansをやったときは苦労しましたが、とりあえずそんな心配は要りません。

 となると気になるのはパラメータですが、指定しなくても

  • bandwidth

 勝手に推定される

  • seeds

 乱数のシードなので勝手に決まる。指定するときは[n_samples, n_features]が必要。

  • bin_seeding

 よくわからないけど、初期値の選び方みたいな。上のと関係がありそう。Trueにすると速くなるらしい。デフォルトのFalseの方がアルゴリズムとしては厳密なはず(よくわからんけど)。

  • min_bin_freq

 これも上のと関係がありそう。わかるようなわからないような感じ。

  • cluster_all

 すべての点をクラスタリングするかどうか。default=Trueなので敢えてFalseにする理由は・・・(高速化なんだろうな)。

  • n_jobs : int

 いつもの並列化数

 本質的な挙動に関わるのはbandwidthで、あとは高速化のために計算を端折るための引数がいっぱいあるだけっぽいです。

 そしてbandwidthも勝手に推定してくれるので、敢えて指定する必要性を感じません(推定の良し悪しがどうかという議論はありますが)。

 今回はn_jobs以外すべてデフォルトでやってみます。

実験

 iris, wineで見てみる。せっかくなのでK-Meansと比較してみます。ついでに、入力をスケーリングすると結果が変わるかも見ます。

プログラム

 いろいろ手抜きをしています。が、とにかく結果は出ます。

import matplotlib.pyplot as plt
from sklearn.datasets import load_iris, load_wine
from sklearn.cluster import MeanShift, KMeans
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

def main():
    iris = load_iris()
    wine = load_wine()

    kmeans = KMeans(n_clusters=3, n_jobs=-1)
    mean_shift = MeanShift(n_jobs=-1)
    s_kmeans = Pipeline([("scaler", StandardScaler()), 
                         ("kmeans", KMeans(n_clusters=3, n_jobs=-1))])
    s_mean_shift = Pipeline([("scaler", StandardScaler()), 
                             ("meanshift", MeanShift(n_jobs=-1))])


    # iris and wine
    pca = PCA(n_components=2)
    for dataset, dataset_name in zip([iris, wine], ["iris", "wine"]):
        fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(20,8))
        axes = axes.ravel()

        PCA_X = pca.fit_transform(dataset.data)
        origin_y = dataset.target
        km_y = kmeans.fit_predict(dataset.data)
        ms_y = mean_shift.fit_predict(dataset.data)
        s_km_y = s_kmeans.fit_predict(dataset.data)
        s_ms_y = s_mean_shift.fit_predict(dataset.data)
        n_clusters = [3, 3, mean_shift.cluster_centers_.shape[0],
                      3, s_mean_shift.named_steps.meanshift.cluster_centers_.shape[0]]

        for i, (y, name, n_cluster) in enumerate(
                zip([origin_y, km_y, ms_y, s_km_y, s_ms_y], 
                    ["original", "k-means", "mean-shift",
                     "scaling+k-means", "scaling+mean-shift"],
                    n_clusters)):

            for target in range(n_cluster):
                axes[i].scatter(PCA_X[y==target, 0],
                                PCA_X[y==target, 1],
                                c="rgb"[target])
            axes[i].set_title("{0} n_cluster:{1}".format(name, n_cluster))
        plt.savefig("{0}.png".format(dataset_name))

if __name__ == "__main__":
    main()

結果

 まずirisの結果から。

iris.png
iris.png

 MeanShiftは2クラスタと解釈しているようです。本来のデータとは異なりますが、敢えて人の目で見ると妥当な結果な気もします。この場合、スケーリングによる変化は微々たるものです。

 次に、wineの方。

wine.png
wine.png

 一見するとoriginalが悪すぎるように見えますが、PCAでむりやり二次元に写像しているためです。クラスタリング自体は写像前のオリジナルの空間で行っているため、影響はありません。

 この場合、合格と言って良いのは入力をスケーリングしたKMeansだけで、あとはダメダメです。特徴量の次元数が大きいから、うまく動いていないのでしょうか。ちょっと謎。

結論

 良いか悪いかの判断はつきかねますが、できることはわかりました。

 たぶんbandwidthを変えるとコロコロ結果が変わるのでしょう。どんな感じで変わるのかは、今後気が向いたときに検証しようと思います。

 →やりました。
www.haya-programming.com

【python】matplotlib.cmの使い方を説明しようと思う

テーマ

 matplotlib.cmが直接使うことはない謎の技術と思われがちなので、「普通に可愛い子なんですよ」って説明する。

cm (colormap) — Matplotlib 3.0.2 documentation

スポンサーリンク


cmとは何か

 とりあえずmatplotlib.cmの属性に何があるか見てみましょう。ちなみに、属性は組み込み関数dir()で見れます。

>>> import matplotlib.cm as cm
>>> dir(cm)
['Accent', 'Accent_r', 'Blues', 'Blues_r', 'BrBG', 'BrBG_r', 'BuGn', 'BuGn_r', 'BuPu', 'BuPu_r', 'CMRmap', 'CMRmap_r', 'Dark2', 'Dark2_r', 'GnBu', 'GnBu_r', 'Greens', 'Greens_r', 'Greys', 'Greys_r', 'LUTSIZE', 'OrRd', 'OrRd_r', 'Oranges', 'Oranges_r', 'PRGn', 'PRGn_r', 'Paired', 'Paired_r', 'Pastel1', 'Pastel1_r', 'Pastel2', 'Pastel2_r', 'PiYG', 'PiYG_r', 'PuBu', 'PuBuGn', 'PuBuGn_r', 'PuBu_r', 'PuOr', 'PuOr_r', 'PuRd', 'PuRd_r', 'Purples', 'Purples_r', 'RdBu', 'RdBu_r', 'RdGy', 'RdGy_r', 'RdPu', 'RdPu_r', 'RdYlBu', 'RdYlBu_r', 'RdYlGn', 'RdYlGn_r', 'Reds', 'Reds_r', 'ScalarMappable', 'Set1', 'Set1_r', 'Set2', 'Set2_r', 'Set3', 'Set3_r', 'Spectral', 'Spectral_r', 'Vega10', 'Vega10_r', 'Vega20', 'Vega20_r', 'Vega20b', 'Vega20b_r', 'Vega20c', 'Vega20c_r', 'Wistia', 'Wistia_r', 'YlGn', 'YlGnBu', 'YlGnBu_r', 'YlGn_r', 'YlOrBr', 'YlOrBr_r', 'YlOrRd', 'YlOrRd_r', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_deprecation_datad', '_generate_cmap', '_reverse_cmap_spec', '_reverser', 'absolute_import', 'afmhot', 'afmhot_r', 'autumn', 'autumn_r', 'binary', 'binary_r', 'bone', 'bone_r', 'brg', 'brg_r', 'bwr', 'bwr_r', 'cbook', 'cmap_d', 'cmapname', 'cmaps_listed', 'colors', 'cool', 'cool_r', 'coolwarm', 'coolwarm_r', 'copper', 'copper_r', 'cubehelix', 'cubehelix_r', 'datad', 'division', 'flag', 'flag_r', 'get_cmap', 'gist_earth', 'gist_earth_r', 'gist_gray', 'gist_gray_r', 'gist_heat', 'gist_heat_r', 'gist_ncar', 'gist_ncar_r', 'gist_rainbow', 'gist_rainbow_r', 'gist_stern', 'gist_stern_r', 'gist_yarg', 'gist_yarg_r', 'gnuplot', 'gnuplot2', 'gnuplot2_r', 'gnuplot_r', 'gray', 'gray_r', 'hot', 'hot_r', 'hsv', 'hsv_r', 'inferno', 'inferno_r', 'jet', 'jet_r', 'ma', 'magma', 'magma_r', 'mpl', 'nipy_spectral', 'nipy_spectral_r', 'np', 'ocean', 'ocean_r', 'os', 'pink', 'pink_r', 'plasma', 'plasma_r', 'print_function', 'prism', 'prism_r', 'rainbow', 'rainbow_r', 'register_cmap', 'revcmap', 'seismic', 'seismic_r', 'six', 'spectral', 'spectral_r', 'spring', 'spring_r', 'summer', 'summer_r', 'tab10', 'tab10_r', 'tab20', 'tab20_r', 'tab20b', 'tab20b_r', 'tab20c', 'tab20c_r', 'terrain', 'terrain_r', 'unicode_literals', 'viridis', 'viridis_r', 'winter', 'winter_r']

 「アホ・・・」と思われた方もいるかもしれませんが、実際にこうなっています。すべてのカラーマップに対応する属性があります。

 ではカラーマップ自体と、その型を見ましょう。

>>> cm.Paired
<matplotlib.colors.ListedColormap object at 0x7fea0de2b2b0>
>>> type(cm.Paired)
<class 'matplotlib.colors.ListedColormap'>

 あまり参考にならなかったですね・・・でも実はこれのhelpには深いことが書いてあります。

>>> help(cm.Paired)
# 中略
 |  __call__(self, X, alpha=None, bytes=False)
 |      Parameters
 |      ----------
 |      X : scalar, ndarray
 |          The data value(s) to convert to RGBA.
 |          For floats, X should be in the interval ``[0.0, 1.0]`` to
 |          return the RGBA values ``X*100`` percent along the Colormap line.
 |          For integers, X should be in the interval ``[0, Colormap.N)`` to
 |          return RGBA values *indexed* from the Colormap with index ``X``.
 |      alpha : float, None
 |          Alpha must be a scalar between 0 and 1, or None.
 |      bytes : bool
 |          If False (default), the returned RGBA values will be floats in the
 |          interval ``[0, 1]`` otherwise they will be uint8s in the interval
 |          ``[0, 255]``.
 |      
 |      Returns
 |      -------
 |      Tuple of RGBA values if X is scalar, othewise an array of
 |      RGBA values with a shape of ``X.shape + (4, )``.

 __call__というのは、インスタンスを関数っぽく呼び出したときに呼ばれるメソッドです。そしてそれぞれのカラーマップは、クラスではなくオブジェクト(インスタンス)としてmatplotlib.cmの属性になっています。つまり呼ぶことができます。

 __call__自体の内容は、0.0から1.0までの値を引数に取り、RGBAのタプル等に変換して返すというものです。配列もnumpyのユニバーサル関数と同様に受け付けます。

 ということは? こうやって使えます。

>>> cm.Paired(0)
(0.6509803921568628, 0.807843137254902, 0.8901960784313725, 1.0)
>>> cm.Paired(1)
(0.12156862745098039, 0.47058823529411764, 0.7058823529411765, 1.0)
>>> cm.Paired([0,0.5,1])
array([[0.65098039, 0.80784314, 0.89019608, 1.        ],
       [0.99215686, 0.74901961, 0.43529412, 1.        ],
       [0.69411765, 0.34901961, 0.15686275, 1.        ]])

 もうお分かり頂けましたね。この機能は、その気になればmatplotlib.pyplotと組み合わせなくても使えるようなものになっています。独立性があるのです。

スポンサーリンク


実践編

 とりあえず乱数列を生成し、適当にplotして画像として保存してみます。

import numpy as np
import matplotlib.cm as cm
import matplotlib.pyplot as plt

np.random.seed(0)
a = np.random.random((100,100))*100

plt.figure()
plt.imshow(a, cmap=cm.Paired)
plt.savefig("a.png")

a.png
a.png

 この結果自体は特に意味とかはないんですが。ここでどんな処理が走っているのかを追いかけて、matplotlibのplot機能を使わずに同じ画像を再現してみようと思います。

 とりあえず、入力の配列はcm.Pairedに渡ってRGBAに変換されています。それはそれで良いとして、入力の配列は(概ね)0~100のスケールになっているはずです。カラーマップのインスタンスの引数には0~1を指定してあげる必要があると、上で理解しました。誰がスケーリングしているのでしょう?

 愚直にmatplotlib.cmのドキュメントを読みに行きます。

cm (colormap) — Matplotlib 3.0.2 documentation

norm : matplotlib.colors.Normalize instance
The normalizing object which scales data, typically into the interval [0, 1]. If None, norm defaults to a colors.Normalize object which initializes its scaling based on the first data processed.

 わかりました。まあ、違うクラス(ScalarMappable)の引数っぽいんですが、とにかくこれを使っておけば良いのね。

matplotlib.colors.Normalize — Matplotlib 3.0.2 documentation

 まあ、これは適当に眺めておきます。難しいこと何も書いてないし。

 ちょっと色々やってみましょう。

>>> from matplotlib.colors import Normalize
>>> import matplotlib.cm as cm
>>> Normalize()([100.0, 30.0, 0.0])
masked_array(data=[1. , 0.3, 0. ],
             mask=False,
       fill_value=1e+20)
>>> cm.Paired(Normalize()([100.0, 30.0, 0.0]))
array([[0.69411765, 0.34901961, 0.15686275, 1.        ],
       [0.2       , 0.62745098, 0.17254902, 1.        ],
       [0.65098039, 0.80784314, 0.89019608, 1.        ]])
>>> sm = cm.ScalarMappable(norm=None, cmap=cm.Paired)
>>> sm.to_rgba([100.0, 30.0, 0.0])
array([[0.69411765, 0.34901961, 0.15686275, 1.        ],
       [0.2       , 0.62745098, 0.17254902, 1.        ],
       [0.65098039, 0.80784314, 0.89019608, 1.        ]])

 まあ、使えそうという感触があります。

 では、上と同じ画像をmatplotlib.pyplotに依存しないで生成し、画像ファイルとして保存してみましょう。pltの代わりにPILを使います。

import numpy as np
import matplotlib.cm as cm
from PIL import Image

np.random.seed(0)
a = np.random.random((100,100))*100

sm = cm.ScalarMappable(norm=None, cmap=cm.Paired)
im_array = sm.to_rgba(a, bytes=True)
Image.fromarray(im_array).save("b.png")

b.png
b.png

 できました。このように使えます(枠とか画像サイズとかは勘弁してください・・・)。

まとめ

 この記事みたいなことを実際にやる必要があるか? というと微妙ですが、matplotlib.cmの実装自体はわかりやすいものだ、ということを理解してもらえたと思います。

 皆さんも、matplotlib.cmのいろいろな可愛がり方を工夫してあげてはいかがでしょうか*1

*1:たとえばこういう風に使っている人がいて、かっこいいなーと思ったりする訳です: matplotlibで色をグラデーション的に選択 - Qiita

【python】複数のin演算子を一つにまとめる方法

はじめに

 こういう状況を考える。

>>> s = "hoge! fuga! piyo!"
>>> if "hoge" in s and "fuga" in s and "piyo" in s:
...     print("fizz!")
... 
fizz!

 文字列の中に部分文字列が含まれているかを判定する、という状況で、ただ判定したい条件が複数ある。

 壮絶にまどろっこしい。できればこんな風に書きたい。

>>> if ("hoge", "fuga", "piyo") in s:

 ただ、こんな文法はpythonにはない(というかstrの__contains__メソッドあたりがこういう引数を想定していない)。

 これもだめ。

>>> if "hoge" or "fuga" or "piyo" in s:

 なんとなくいけそうな気もするけど、実は"hoge" or "fuga" or "piyo"が先に評価されて

>>> "hoge" or "fuga" or "piyo"
'hoge'

 となる。pythonの論理演算子は、基本的に受け取ったオブジェクトを返すので、こういう挙動になってしまう。

Python の or と and 演算子の罠 - Qiita

 最終的な結果は"hoge" in sなのでまったく正しくない。

 なので、これに近づける方法を考える。

スポンサーリンク


all()とany()

 どちらも組み込み関数である。all()は論理積、any()は論理和に対応する。

 実行例はこんな感じ。

>>> all([False,False])
False
>>> all([False,True])
False
>>> all([True,True])
True
>>> any([False,False])
False
>>> any([False,True])
True

 これを踏まえ、冒頭のコードを以下のように書き換えてみる。

>>> s = "hoge! fuga! piyo!"
>>> if all(x in s for x in ("hoge", "fuga", "piyo")):
...     print("fizz!")
... 
fizz!

 まだちょっとダルい。mapでも書いてみる。

>>> s = "hoge! fuga! piyo!"
>>> ins_f = lambda x:x in s
>>> if all(map(ins_f, ("hoge", "fuga", "piyo"))):
...     print("fizz!")
... 
fizz!

 これはエレガントな感じがする。一行増えているけど。一行増やしたくなければ黒魔術を使える。

>>> s = "hoge! fuga! piyo!"
>>> if all(map(s.__contains__, ("hoge", "fuga", "piyo"))):
...     print("fizz!")
... 
fizz!

 anyもまったく同様にやるだけなので、説明は省略。

注意

 ドキュメントを読むとわかるのだが、all(), any()は機能的にはとても単純なようだ。

組み込み関数 — Python 3.7.4 ドキュメント

 それは別に良いのだが、先に引数をすべて渡す必要があるせいで、リストを引数に渡した場合、短絡評価されない(というか短絡評価する前に計算してしまっているので無意味)という問題がある。

 all(), any()を使うときは引数をジェネレータ式などにしてあげた方が良いだろう。map objectもイテレータで、同様の性質があるので、問題ない。

www.haya-programming.com

まとめ

 この記事の方法を使うと、複数のin演算子が入るような場合にまとめて簡潔に書くことができる。

 あまり使う機会は多くないかもしれないが、参考にしてほしい。

【python】numpyで乱数のseedを設定する方法

 「seed(種)」とか「random state」とか呼ばれる奴の設定方法。これを設定することで、乱数の処理に再現性を与えることができる。

方法

 np.random.seed()を呼ぶと、とりあえずseedが引数でリセットされる。

https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.seed.html

 やってみる。

>>> import numpy as np
>>> np.random.seed(0)  # seedを設定
>>> np.random.random((2,3))  # 一様乱数の配列を作ってみる
array([[0.5488135 , 0.71518937, 0.60276338],
       [0.54488318, 0.4236548 , 0.64589411]])
>>> np.random.random((2,3))  # 同じにはならない
array([[0.43758721, 0.891773  , 0.96366276],
       [0.38344152, 0.79172504, 0.52889492]])
>>> np.random.seed(0)  # もう一度設定する
>>> np.random.random((2,3))  # 最初と同じになる
array([[0.5488135 , 0.71518937, 0.60276338],
       [0.54488318, 0.4236548 , 0.64589411]])

 まあ、理解はできる。

スポンサーリンク


 ところで、np.random.seed()のドキュメントにはこう書いてあった。

This method is called when RandomState is initialized. It can be called again to re-seed the generator. For details, see RandomState.

 RandomStateというクラスが内部では動いているということなのだろうか。

https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.RandomState.html

Parameters:
seed : {None, int, array_like}, optional

Random seed used to initialize the pseudo-random number generator. Can be any integer between 0 and 2**32 - 1 inclusive, an array (or other sequence) of such integers, or None (the default). If seed is None, then RandomState will try to read data from /dev/urandom (or the Windows analogue) if available or seed from the clock otherwise.

 よくわからないが、乱数の状態のクラスなので(直訳)、たぶんインスタンス化してよろしく使えるのだと思う。

>>> rand_state = np.random.RandomState(0)
>>> rand_state.random_sample((2,3))  # なぜかrandomメソッドがなくてrandom_sampleメソッドが同じ機能を持っている・・・
array([[0.5488135 , 0.71518937, 0.60276338],
       [0.54488318, 0.4236548 , 0.64589411]])
>>> rand_state.random_sample((2,3))
array([[0.43758721, 0.891773  , 0.96366276],
       [0.38344152, 0.79172504, 0.52889492]])

 できた。それでえっと、何に便利かなこれは・・・。

 まあ、常識的な用途ではnp.random.seed()があれば困らないと思う。わざわざオブジェクト化して取り扱いたいような状況は、たぶんあるにはあるだろうけど、ライブラリとかアルゴリズムのコアなコード書いてるとき以外はほぼないだろう。

まとめ

 簡単にできる。

【python】operator.itemgetterを使うべきか否か問題

はじめに

 この記事を開いた人の大半は「itemgetter? なにそれ」という反応でしょう。
 (いや、検索で来た人はそうでもないかもしれないけど)

 itemgetterは以下のように使えるものです。

>>> lst = list(zip([1,3,5,6,7,1,4], [3,4,1,0,8,5,2]))  # 特に値に意味はない
>>> sorted(lst, key=lambda x:x[0])
[(1, 3), (1, 5), (3, 4), (4, 2), (5, 1), (6, 0), (7, 8)]
>>> sorted(lst, key=lambda x:x[1])
[(6, 0), (5, 1), (4, 2), (1, 3), (3, 4), (1, 5), (7, 8)]
>>> from operator import itemgetter
>>> sorted(lst, key=itemgetter(0))
[(1, 3), (1, 5), (3, 4), (4, 2), (5, 1), (6, 0), (7, 8)]
>>> sorted(lst, key=itemgetter(1))
[(6, 0), (5, 1), (4, 2), (1, 3), (3, 4), (1, 5), (7, 8)]

 つまり、itemgetter(0)はlambda x:x[0]と等価です。

 参考:
 10.3. operator — 関数形式の標準演算子 — Python 3.6.5 ドキュメント

 では、どちらを使うべきなのでしょうか?



 目次

可読性とタイプ数

可読性

 可読性は正直なんとも言い難いです。

 とりあえずlambda式はlambda式の概念さえ理解していればわかるともいえますし、それでもわかりづらいという人もいるでしょう。また、見た目はグロいです。

 itemgetterは見た目はすっきりしています。これはとても大切なことで、lambdaまみれで入り組んだコードはもううんざりです。ただ、itemgetter自体を知らない人は恐らく多いので、初見では「helpを見る」「ググる」という作業が発生します。また、何をやっているか明示的にわかりづらいので、lambdaの方がマシという考え方もあります。

タイプ数

 タイプ数的には、はじめにを見て頂ければわかる通り互角です。itemgetterという途方もなく長い識別子のせいで、lambda式に勝てません。

 一応、asで別名にしてしまうという反則技(?)はあります。

from operator import itemgetter as get  # もうちょっと良い名前を考えて

 こうするとタイプ数的にはlambdaに勝てそうです。ただ、こうするのは如何なものか? と相成ります。混乱を招きそうなので、あまりやりたくない手です。

結論

 可読性・タイプ数では決められない。

速度差

 まさかこんなのに速度差なんてねーだろ、そう思っていた時期が私にもありました。

 都合によりminで比較。こうすると常に全数線形探索されるはずなので、再現性等が良いです。

>>> import timeit
>>> lst = list(zip(range(10000), range(10000)))
>>> timeit.timeit(lambda :min(lst, key=lambda x:x[0]), number=1000)
1.33255122898845
>>> timeit.timeit(lambda :min(lst, key=itemgetter(0)), number=1000)
0.6424821490072645

 倍違うのだった。えぇ・・・

 怪しいので、key関数を外部に定義してみます。

>>> key1 = lambda x:x[0]
>>> key2 = itemgetter(0)
>>> timeit.timeit(lambda :min(lst, key=key1), number=1000)
1.32674505902105
>>> timeit.timeit(lambda :min(lst, key=key2), number=1000)
0.6525059329869691

 変わらず。

結論

 速いので、特にこだわりがなければitemgetterを使いましょう。

 私はわざわざimportするのがダルいのと、個人的にlambdaがそれなりに好きという理由でlambdaを使い続けると思います。競プロとかのときはitemgetter有利なんじゃないでしょうか。