静かなる名辞

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


【python】io.StringIOは便利なので使いこなそう

はじめに

 io.StringIOというものがあります。標準モジュールのioに属します。

io --- ストリームを扱うコアツール — Python 3.7.1 ドキュメント

 これがどう便利かというと、「ファイルオブジェクトのように見えるオブジェクト」を作れます。

スポンサーリンク



読み込みに使ってみる

 こんな感じですかね。

import io

txt = """hoge
fuga
piyo
"""

f = io.StringIO(txt)
for line in f:
    print(line, end="")

""" =>
hoge
fuga
piyo
"""

 何が嬉しいかって? たとえば、これを見てください。

import io
import pandas as pd

txt = """
number,name,score
1,hoge,100
2,fuga,200
3,piyo,300
"""

df = pd.read_csv(io.StringIO(txt), index_col="number")
print(df)
""" =>
        name  score
number             
1       hoge    100
2       fuga    200
3       piyo    300
"""

 CSVの文字列を読み込むのに使っています。

 pandas.read_csvの第一引数はファイルパスかストリームしか受け付けませんが、io.StringIOをかましてやることでstrを渡せている訳です。

pandas.read_csv — pandas 0.23.4 documentation

 「ファイルを作るまでもない、作るのが面倒くさい」ような軽い確認やデバッグ作業に重宝します。

書き込んでみる

 io.StringIOが読み込みモードのファイルオブジェクトの代用品として使えることはわかりました。では、書き込みはどうでしょう?

import io
f = io.StringIO()
f.write("hoge\n")
f.close()

 このコードはエラーなく終了しますが、特に副作用は得られません。

 中身を見たければ、こうします。

import io
f = io.StringIO()
f.write("hoge\n")
print(f.getvalue())  # => hoge
f.close()

 StringIO.getvalue()はバッファに書き込まれた内容をすべて吐き出します。closeすると呼べなくなるので注意が必要です。

 こちらもpandasと組み合わせて使ってみます。

import io
import pandas as pd

txt = """
number,name,score
1,hoge,100
2,fuga,200
3,piyo,300
"""

df = pd.read_csv(io.StringIO(txt), index_col="number")

with io.StringIO() as f:
    f.write(df.to_csv())
    print(f.getvalue())

""" =>
number,name,score
1,hoge,100
2,fuga,200
3,piyo,300
"""

 使えているようです。

仲間たち

 ドキュメントを見るとbytes版のBytesIOもあることがわかる。他にも色々あるようだが、あまり目を通していないのでコメントしない。

io --- ストリームを扱うコアツール — Python 3.7.1 ドキュメント

まとめ

 テキストファイルを相手にするとき、データが小さければソースコードに埋め込めます。そうするとソースコードだけで再現性を確保できて、けっこう良いです。

オブジェクト指向の教育にPythonが向いていると思うこれだけの理由

はじめに

 オブジェクト指向は今となっては常識である。

 常識であるがゆえに、いかに初心者にわかりやすく教えるかが課題になる。

 世の中でオブジェクト指向の「教材」として使われている言語は、

  • Java
  • Ruby

 の二択くらいだと思う。が、あえて僕はPythonを推してみるよ、という記事。ぶっちゃけポエム。

 内容は、Javaでオブジェクト指向を理解するのはしんどいし、RubyとPythonだと僅差でPythonが勝つんじゃないかなぁ、という主張。以下で理由を書いていくよ。

 目次

スポンサーリンク



Pythonが向いていると思う理由

理由1:すべてがオブジェクト

 すべてがオブジェクト。これは重要なことである。

 オブジェクトとオブジェクト以外で異なる扱いをしなければならない、プリミティブ型のある言語でオブジェクト指向の教育をするなんて、正気の沙汰ではない。初心者はintと配列ばっかり使う訳だしさ。

 なので、Javaはぶっちゃけ論外だと思う。Ruby使いの人は「ならPythonとRubyは互角だ」と言いたくなるかもしれないけど、Rubyには関数が第一級オブジェクトではないという弱点があり、「すべてがオブジェクト」ははっきり言って誇大広告である。

 関数オブジェクトが自然に使えないと困るのかって? まあ、そんなに困らないかもしれない。でも「関数オブジェクト」は理解しておいた方が、オブジェクト指向がよくわかるようになると思わない?(上級者向けすぎるか)

理由2:仕様がスリムで綺麗。書きやすい

 Javaにはまず、publicやらprivateやらある。interfaceというわかりづらい概念もある。静的型付けなのも相まって、メソッドの宣言なんかカオス。public static void mainはどう考えても初心者向けではない*1

 Rubyは、なんかブロックとかいうよくわからないものがあるね。あと、メソッド呼び出しのカッコを省略できるとか。教育上あんまりよくないと思います。

 Pythonにはそういう問題はない。

理由3:self

 Pythonのselfはよく批判されるけど、なんだかんだでわかりやすい。

a.hoge("fuga")

 は

type(a).hoge(a, "fuga")

 と実質的に等価というルールがあることを理解すれば、後は不自然な点はなにもない。インスタンスの外側ではaとして見えているものは、内側ではselfとして見えていると考えれば良いということで、自然な発想でコードを書いていくことができる。

 selfを省略する言語はこれがないので、クラス変数とインスタンス変数の区別を付けるだけでも一苦労だし、ローカル変数まで混ざってくると本格的に訳がわからなくなる(から、EclipseでJavaを書くとぜんぶ違う色で見せてくれる)。

理由4:普通にプログラミングしているだけでオブジェクト指向への理解が深まる

 Python初心者はlistをよく使う。そうするとappendやextendが出てくる。これはもうメソッドだ。

 関数の引数に渡したリストにappendするとリストの中身が変わるけど、intだと足し算しても変わらない。mutableなオブジェクトとimmutableなオブジェクトの違いを理解するだろうし、オブジェクトは変数に束縛されているだけというオブジェクト指向の基本的なモデル*2への理解も深まる。

 list.sort()とsorted(list)の違いを理解すれば、破壊的なメソッドには注意しないといけないこともわかる。

 だから、「オブジェクト指向の勉強のためにJavaを半年学んだ人」と「ただ単にプログラミングの勉強として半年Pythonを学んだ人」だと、オブジェクト指向に対する理解度は同程度か、ヘタしたら後者のほうが勝るくらいになっているかもしれない。

 まあ、これに関してはたぶんRubyも互角。

理由5:書いてて楽しい

 タイプ数も少ないし、なんかPythonはパズルみたいな技巧的な面があるし、書いてて楽しい。

向いていない理由も一応書く

 ダメな理由もなんか色々あるといえばあるような気もする。

罠が多い

 Pythonはシンプルなクセに罠が多い言語だと思う(「文字コード」とか「test.py作っただけでまともに動かない」とか「IndentationError」とか)。

 エラーメッセージも、わからないときはとことんわからないのが出るし。

 初心者のうちは、よくわからないところで詰む。そして初心者は自己解決できない。

 罠は回避するように教育していくとかで軽減は可能。ただ、本質的じゃない問題で初心者を悩ませるのもなんだかなぁという気がする。

そういうコンセプトの解説記事とかが少ない。あっても古い

 困るよね。

しょせんスクリプト言語

 カプセル化はないし、ポリモーフィズムの実現方法もすごく簡単(メソッド作るだけ!)。ので、Pythonやってから他の言語のオブジェクト指向を理解しようとすると、追加の学習コストがかかる。

独自の風習

 避けては通れないけど避けなかったところで得るものの少ないデコレータとか、

 初心者を惑わす内包表記とかジェネレータとか、

 やればやるほど実感するtuple周りのキモさとか*3

 これどうなんだ、と思う側面はたくさんあります。

windowsとそんなに相性がよくない

 最近はマシにはなりましたが、まだまだハマると思います。linux環境でやれとなるといきなり敷居が高くなります。

でもまあ、

 なんだかんだで学習コストの低さ、とっつきやすさでは、総合的には初心者向けのわかりやすい言語と言っても別に構わないくらいだとは思うよ(震え声)。

まとめ

 Pythonのオブジェクト指向はわかりやすいよね、最初からこれで教えてもらえたらなぁ。Python良いよね! って気持ちの記事です。

 特に内容に責任は持たないが、意見等はご自由にどうぞ。

*1:「そんなのIDEが補完してくれるから良いんだよ!」という意見が当然あると思うが、それをやると馬鹿の一つ覚えのようにIDEの補完と修正任せでコードを書こうとする奴が続出するのでダメだ

*2:異論はあるだろうけど

*3:tupleは丸括弧によって作られるのではない、カンマによって作られるのである

共有渡しと参照の値渡しと

はじめに

 関数やメソッドに引数を渡す方法は、一般的には

  • 値渡し
  • 参照渡し

 の2通りがあると認知されている。

 ところで、『参照の値渡し』という言葉も(ほぼ日本語Web圏限定で)存在する。これは「いわゆる『参照渡し』は参照自体を書き換えるんじゃなくて、参照する対象を変えるだけだから、そう呼んだ方が適当だよ!」という思想に基づくもの、だと思う。

 このページを見るとわかりやすい。

キミの言語は参照渡しできる? - Qiita

 つまり、こういうことができたら『参照渡し』で

a = "hoge"
b = "fuga"
swap(a, b)
print(a, b)  # => fuga, hoge

 できなかったら「キミの言語は『参照渡し』できないよ、キミが『参照渡し』だと思っているのは『参照の値渡し』だよ!」ということか。言いたいことはわかる。

 上のリンクにはC言語で「参照渡し」をやる方法としてこういう例が載っている。

#include <stdio.h>

void swap(char *a, char *b){
  char tmp;
  tmp = *a;
  *a = *b;
  *b = tmp;
}

int main(void){
  char x = 'A', y = 'B';
  swap(&x, &y);
  printf("%c\n", x);
  printf("%c\n", y);
  return 0;
}

 ちなみに、こっちの「参照渡し」は歴史が古く、少なくともFORTRANからあるっぽい。というか、FORTRANはデフォルトですべて参照渡し。

subroutine f(x)
  integer x
  x = 42
end subroutine

program main
  integer a
  a = 3
  print *, a
  call f(a)
  print *, a
end program

!           3
!          42

 C言語とかに慣れた目には奇異に映るけど、よくよく考えてみるとメモリ番地だけ渡せばいいので効率的だし(まあ実際にどういう実装なのかまでは確認していないけど)、多少注意していればプログラムも書きやすいので、これはこれで合理的だと思う。

 話が逸れた。参照の値渡しでは、こういう「参照渡し」チックな動作はできない。pythonの例。

def swap(a, b):
    a, b = b, a

a = 10
b = 20
swap(a, b)

 pythonの変数はすべてJavaなどでいうところの参照型ではあるのだけど、swapの中のa,bは単なるswap関数のローカル変数であって、呼び出し元のa, bの参照するものを書き換えたりはしない。受け取っているのは「参照の値」であって、「参照の値を格納している変数への参照」ではない。

 参照型の「真の」参照渡しについては、以下のCの例を考えてみるとわかりやすい。

#include <stdio.h>

void swap(char **a, char **b){
  char *tmp;
  tmp = *a;
  *a = *b;
  *b = tmp;
}

int main(void){
  char x = 'A', y = 'B';
  char *xptr, *yptr;
  xptr = &x;
  yptr = &y;
  swap(&xptr, &yptr);
  printf("%c\n", *xptr);
  printf("%c\n", *yptr);
  return 0;
}

 xptr, yptrのアドレスを渡す訳ですね。pythonではこれはできない(というか、そもそも変数の概念そのものが違うけど)。で、こういうものを称して参照の値渡し、とする。

共有渡しについて考える

 さて、『参照の値渡し』とよく似た概念として、『共有渡し』がある。

 ちなみに、日本語wikipediaの「引数」ページには『共有渡し』は存在しない代わり参照の値渡しがあり、英語版wikipediaの「Evaluation strategy」ページには『Call by sharing』*1はあって参照の値渡しはない。なんだかなぁ。

引数 - Wikipedia
Evaluation strategy - Wikipedia

 『共有渡し』が何者かというと、これは英語版wikipediaのページを読むのがわかりやすいのだが、一行引用してくると

also referred to as call by object or call by object-sharing

 ということであり、つまりは関数(メソッド)の呼び出し元と呼び出し先で同じオブジェクトを「共有」する方法である。

 僕のようなpython使いにとっては「それ普通じゃね?」なのだが、確かに値渡しとも『参照渡し』とも(何を示す言葉かは不問として)異なる概念と言われればそんな気はする。この言葉は、1974年、CLUという初期のオブジェクト指向言語とともに生み出された言葉だそうな。

 これに関連して、こんな議論もある。comp.lang.python *2の昔の議論らしい。pythonの呼び出しモデルは『Call by sharing』だと結論が着いている感じ。

Call By Object

 なんか、同じものを呼ぶ名前がいっぱいある。

The most accurate description is CLU’s “call by object” or “call by sharing“. Or, if you prefer, “call by object reference“.

 こんなに呼び方が多いのはちょっと酷いんだが、“call by object reference“あたりだと言いたいことはよく伝わってくる。

どっちが良いのか

 べつに『参照の値渡し』≒『共有渡し』とみなしても良いのだが、言葉のニュアンスは違うし、他にも考えるべきことがあって「どっちを使うか、あるいはどっちでも良いのか」という問題には答えを出しづらいと思うのだ・・・。

  • 使える言語と使えない言語がそれぞれ違う

  『参照の値渡し』は『参照の値』が定義されない言語では使うべきではないと思う。また逆に、C言語に対して『共有渡し』を使うのにはなんとなく躊躇する。メモリ領域を共有しているには違いないけど、なんかもう少し低レイヤな感じなので。

  • あくまでも値渡し+参照渡し的な世界観で説明しようとする『参照の値渡し』と、オブジェクトが呼び出し元・先で「共有」されるという現象を重視する『共有渡し』

 同じ現象でも見方が違うのだと思う。

 この違いは意外と効いてきて、前者の立場を取るとJavaは「基本的に値渡し。プリミティブ型はそのまま値で渡るが、それ以外はアドレスの値が渡る」と値渡し的な世界観で説明できるが、後者の立場を取ると「プリミティブ型の値渡し、配列の『参照の値渡し』、オブジェクトの『共有渡し』の折衷」という苦しい説明にせざるを得ないと思う(配列に対して『共有渡し』の言葉を使うことを許すなら、真ん中は削れるけど)。Java使いの人たちが「Javaは値渡し!」と主張したがるのは、つまるところそういうことだろう*3

 逆にpythonやrubyのような「すべてがオブジェクト」な言語では、プリミティブ型やら何やらのことは考える必要はなく、また言語仕様の表面で参照の値(要するにアドレス)が見えてくる訳でもないので、『共有渡し』の方がすっきりすると思う。

  • 認知度とか言葉としての良し悪し

 なんか、どっちもどっちという気がする。
 単純な好みの問題だと思うけど強いて選ぶとすれば、国際的に(一応は)通用するであろうこと、言葉の良し悪しについて議論の余地が少ない*4ことから、共有渡しの方が筋は良さそう

 まあ、個人的には『共有渡し』の方がスッキリするし、好きです。でも、『参照の値渡し』が絶対に駄目かというと・・・難しいです。呼び方の問題は厄介。

まとめ

 ややこしくない(動作としてはよくわかる)けど、ややこしい(名前がうまく決められない)話だよね。

続き

 某所でこの問題が再燃していたので、続きを書いた。

www.haya-programming.com

*1:念のため書いておくと、「渡し」に対応するのは「Pass by」、「Call by」は「呼び出し」に対応するのだが、どちらにせよ意味は大して変わらないのでどちらで訳しても問題はない。この記事では日本語圏で一般的な「渡し」で統一している

*2:筆者はこれが何なのかはよくわからないが

*3:個人的には「Cみたいに明示的にアドレス渡す訳でもないのに値渡しって呼ぶのは逆説的でわかりづらいよ」と思うのだが、そのコストに勝るメリットがあるとする考えなのだろう

*4:というか議論になっていない程度に流行っていないだけかも・・・

【python】immutableなオブジェクトは1つしか存在しないという迷信

 たまに誤解している人がいるので、書いておく。

 pythonのオブジェクトにはimmutableという概念がある。これはオブジェクトが変更不可能であるということを示す。intやstr, tupleなどが代表的なimmutableなオブジェクトである。

 オブジェクトがimmutableであるかどうかは、オブジェクトがいわゆるシングルトンであるかどうかとはまったく関係ない

 確かに、そんな風に振る舞っている、ように見える状況もある。

>>> a = 1
>>> b = 1
>>> a is b
True
>>> a = "a"
>>> b = "a"
>>> a is b
True

 しかし、tupleで試すと、同様に結果にはならないことがわかる。

>>> a = (1,2,3)
>>> b = (1,2,3)
>>> a is b
False

 もう少し、いろいろ見てみる。

>>> all([a is b for a,b in zip(range(100), range(100))])
True
>>> all([a is b for a,b in zip(range(1000), range(1000))])
False

 不可解だって? まあ、不可解なんだけど、これはpythonの実装の問題である。

 pythonは参照カウントGCを持つオブジェクト指向言語である。どんなオブジェクトであろうと生成にはインスタンス生成分のコストがかかるし、参照されなくなったオブジェクトはGCが連れて行く。

 それはそれで良いのだが、「小さめの数字」とか「短めの文字列」みたいなしょっちゅう使われるオブジェクトを必要になるたびに生成し、不要になるたびにGCを呼ぶのは無駄である。よって、pythonは内部にキャッシュを持ち、オブジェクトのインスタンスを残しておく。いわゆるインターン*1だと思えば良い。

 ここのページでその辺のことには触れられている。
Cool Python Tips: is演算子はimmutable変数に使わないこと

 結論を言うと、immutableだからといってシングルトンだと思いこんではいけないし、ましてやis比較などしてはいけない。

*1:python インターンで検索すると就業体験の「インターンシップ」ばっかり出てきて、有益な情報が出てこない・・・

GridSearchCV『の』パラメータ・チューニング 高速化中心に

はじめに

 機械学習でパラメータ・チューニングをしたい場合、グリッドサーチを行うのが定石とされています。sklearnではグリッドサーチはGridSearchCVで行うことができます。

sklearn.model_selection.GridSearchCV — scikit-learn 0.21.2 documentation

 それで何の問題もないかというと、さにあらず。

 グリッドサーチは計算コストの高い処理ですから*1、素直に書くとデータとアルゴリズム次第ではとんでもない処理時間がかかります。

 もちろん「寝ている間、出かけている間に回すから良い」と割り切るという方法もありますが、可能なら速くしたいですよね。

 そうすると、パラメータ・チューニングのために使うGridSearchCV『の』パラメータを弄り回すという本末転倒気味な目に遭います。そういうとき、どうしたら良いのかを、この記事で書きます。

 先に結論を言ってしまうと、本質的に計算コストの高い処理なので、劇的に速くすることは不可能です。それでも、ちょっとの工夫で2倍程度なら速くすることができます。その2倍で救われる人も結構いると思うので*2、単純なことですがまとめておきます。

 目次

スポンサーリンク


下準備とベースライン

 とりあえず、何も考えずに「GridSearchCVをデフォルトパラメタで使ってみた場合」の時間を測ります。

 そのためには適当なタスクを回してやる必要がありますが、今回はPCA+SVMでdigitsの分類でもやってみることにします。

 コードはこんな感じです。

import timeit
import pandas as pd
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
from sklearn.svm import SVC
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV

digits = load_digits()
svm = SVC()
pca = PCA(svd_solver="randomized")
pl = Pipeline([("pca", pca), ("svm", svm)])

params = {"pca__n_components":[10, 15, 30, 45],
          "svm__C":[1, 5, 10, 20], 
          "svm__gamma":[0.0001, 0.0005, 0.001, 0.01]}

def print_df(df):
    print(df[["param_pca__n_components",
              "param_svm__C", "param_svm__gamma", 
              "mean_score_time", 
              "mean_test_score"]])

def main1():
    clf = GridSearchCV(pl, params, n_jobs=-1)
    clf.fit(digits.data, digits.target)
    df = pd.DataFrame(clf.cv_results_)
    print_df(df)
    
if __name__ == "__main__":
    print(timeit.timeit(main1, number=1))

 色々なテクニックを使っているコードなので多少解説すると、とりあえずPipelineを使っています。また、GridSearchCV.cv_results_はそのままpandas.DataFrameに変換できる辞書として扱えることも利用しています。

www.haya-programming.com

 digits, svm, pca, pl, paramsの変数はmain関数の外でグローバル変数として作っていますが、これはあとでmain2とかmain3を作って使い回すための処置です。

 あと、速くするために必要と思われる常識的なこと(PCAのsvd_solver="randomized"とか、GridSearchCVのn_jobs=-1とか)はすでに実施しています。

 そんなことより本当にやりたいことは、この処理にどれだけ時間がかかるかを知ることです。そのために、timeitを使って時間を計測しています。

timeit --- 小さなコード断片の実行時間計測 — Python 3.7.4 ドキュメント

 さて、私の環境(しょぼいノートパソコン)ではこのプログラムの実行には42.2秒かかりました。

 これをベースラインにします。ここからどれだけ高速化できるかが今回のテーマです。

cvを指定する(効果:大)

 さて、GridSearchCVにはcvというパラメータがあります。default=3であり、この設定だと3分割交差検証になります。交差検証について理解していれば、特に不自然なところはないと思います。

 これを2にしてみます。交差検証できる最低の数字です。こうすると、

  • 交差検証のループ回数が3回→2回になり、それだけで1.5倍速くなる
  • チューニング対象のモデルの計算量が学習データサイズnに対してO(n)以上なら、それ(nが小さくなること)によるご利益もある。なお予測データサイズmに対する予測時間は普通O(m)なので、影響はない

 この相乗効果で、高速化が期待できます。

 この方法のデメリットは学習データを減らしたことで性能が低めになることですが、チューニングのときはパラメータの良し悪し(スコアの大小関係)がわかれば良いので、あまり問題になりません。とにかくやってみます。

def main2():
    clf = GridSearchCV(pl, params, cv=2, n_jobs=-1)
    clf.fit(digits.data, digits.target)
    df = pd.DataFrame(clf.cv_results_)
    print_df(df)

if __name__ == "__main__":
    # print(timeit.timeit(main1, number=1))
    print(timeit.timeit(main2, number=1))

 上のコードと重複する部分は削ってあります。見比べると、ほとんど変わっていないことが、おわかりいただけるかと思います。

 この処置で、処理時間は28.0秒に改善しました。ちょっといじっただけで、2/3くらいに改善してしまった訳です。そして「mean_test_score」はやはり全体的に低くなりましたが、傾向は同じでした。よってパラメータチューニングには使えます。

return_train_score=Falseする(効果:それなり)

 さて、GridSearchCVはデフォルトの設定ではreturn_train_score='warn'になっています。「'warn'って何さ?」というと、こんな警告を出します。

FutureWarning: You are accessing a training score ('std_train_score'), which will not be available by default any more in 0.21. If you need training scores, please set return_train_score=True

 return_train_scoreは要するに学習データに対するスコアを計算するかどうかを指定できる引数です。この警告は割とくだらないことが書いてあるのですが、将来的にはこれがdefault=Falseにされるという警告です。

 基本的に、パラメータチューニングで見たいのはテストデータに対するスコアであるはずです。なのに、現在のデフォルト設定では学習データに対する評価指標も計算されてしまいます。

 これは無駄なので、return_train_score=Falseすると学習データに対する評価指標の計算分の計算コストをケチれます。予測時間なんてたかが知れていますが、それでも一応やってみましょう。

def main3():
    clf = GridSearchCV(pl, params, cv=2,
                       return_train_score=False,
                       n_jobs=-1)
    clf.fit(digits.data, digits.target)
    df = pd.DataFrame(clf.cv_results_)
    print_df(df)
    
if __name__ == "__main__":
    # print(timeit.timeit(main1, number=1))
    # print(timeit.timeit(main2, number=1))
    print(timeit.timeit(main3, number=1))

 この措置によって、処理時間は22.1秒まで短縮されました。ベースラインと比較すると1/2強の時間で済んでいる訳です。

まとめ

 この記事では

  • cv=2にする
  • return_train_score=Falseにする

 という方法で、パラメータチューニングの機能を損なわないまま2倍弱の速度の改善を実現しました。

工夫なし cv=2 cv=2&return_train_score=False
42.2秒 28.0秒 22.1秒

 このテクニックはきっと皆さんの役に立つと思います。

それでも時間がかかりすぎるときは

 そもそもグリッドサーチしようとしているパラメータ候補が多すぎる可能性があります。

 たとえば、3つのパラメータでそれぞれ10個の候補を調べるとなると、10*10*10=1000回の交差検証が行われます。いつまで経っても終わらない訳です。

 今回の記事では4*4*4=64回としましたが、これでもけっこう多い方だと思います。それでも解こうとしている問題が単純なので、デフォルトパラメータでも1分以内には処理できていますが、ちょっと重いモデルにちょっと多量のデータを突っ込んだりすると、もうダメです。何十分とか何時間もかかってしまいます。

 そういう場合、まずは粗いステップ(少ないパラメータ候補数)でざっくりパラメータチューニングしてしまい、どの辺りのパラメータが良いのかわかったら、その周辺に絞ってもう一回パラメータチューニングを行います。こういうのを二段グリッドサーチと言ったりします。

 あるいはベイズ最適化とか、他のアルゴリズムに走るのも一つの手かもしれません。

 粗いグリッドである程度チューニングしてから、RandomizedSearchCVを使うというのもいい手だと思います。

www.haya-programming.com

*1:なにせすべての組み合わせを計算する

*2:たとえば「今から回して、朝までにデータを出さないと教授への報告が間に合わないんだ!」みたいな状況を想定しています

【python】sklearnのVarianceThresholdを試してみる

はじめに

 VarianceThresholdは名前の通り、分散がしきい値以下の特徴量を捨てます。

sklearn.feature_selection.VarianceThreshold — scikit-learn 0.20.2 documentation

 これといってすごいところはありませんが、気楽に使えそうなので試してみました。

 目次

スポンサーリンク



とりあえず試す

 しきい値の設定でどれだけ特徴量のshapeが減るか見てみました。

 データは20newsgroupsです。
Pipelineにしてあるのは、あとでこれを使って分類のチューニングをしてみるためです。

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_selection import VarianceThreshold
from sklearn.pipeline import Pipeline

def test_shape():
    news20 = fetch_20newsgroups()
    cv = CountVectorizer(min_df=0.005,
                         max_df=0.5,
                         stop_words="english")
    vth = VarianceThreshold()
    pl = Pipeline([("cv", cv), ("vth", vth)])
    for v in [0.0,0.05,0.1,0.15]:
        pl.set_params(vth__threshold=v)
        print(pl.fit_transform(news20.data).shape)

if __name__ == "__main__":
    test_shape()

 関連記事:


 結果は、

(11314, 3705)
(11314, 1476)
(11314, 859)
(11314, 573)

 なるほど。
 (実際にはいろいろ試してちょうど良いshapeの減り具合になる値を探しています。これを使うならそういう作業が必要になると思います)

分類を試してみる

 これをうまく設定すると、分類精度が上がったりするのでしょうか?

import pandas as pd
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.preprocessing import FunctionTransformer
from sklearn.feature_selection import VarianceThreshold
from sklearn.naive_bayes import GaussianNB
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV

def convert(x):
    return x.toarray()
def test_best_v():
    news20 = fetch_20newsgroups()
    cv = CountVectorizer(min_df=0.005,
                         max_df=0.5,
                         stop_words="english")
    vth = VarianceThreshold()
    sparse_to_dense = FunctionTransformer(func=convert,
                                          accept_sparse=True)
    gnb = GaussianNB()
    pl = Pipeline([("cv", cv),
                   ("vth", vth),
                   ("s2d", sparse_to_dense),
                   ("gnb", gnb)])
    
    params = {"vth__threshold":[0.0,0.05,0.1,0.15]}
    
    clf = GridSearchCV(pl, params, 
                       return_train_score=False,
                       n_jobs=-1)
    clf.fit(news20.data, news20.target)
    cv_result_df = pd.DataFrame(clf.cv_results_)
    df = cv_result_df[["param_vth__threshold", 
                       "mean_score_time", 
                       "mean_test_score"]]
    print(df)

if __name__ == "__main__":
    test_best_v()

 ただ単にナイーブベイズに入れて性能を見ているだけですが、かなり色々なテクニックを使っているコードなので、初見だと読みづらいと思います。

  • FunctionTransformer:

 ナイーブベイズが疎行列を受け付けてくれないので変換している。こんな関数ラムダ式で良いじゃんと思う向きもあるかもしれませんが、GridSearchCVでn_jobs=-1を指定するためにはトップレベル関数として定義してあげる必要があります(中でpickleを使うので)

  • GridSearchCV:

 return_train_score=Falseにすると速くなります。

  • pd.DataFrame

 GridSearchCV.cv_results_はそのままpandas.DataFrameに変換できるとドキュメントに書いてあるので、それを使ってpandasで取り扱っています。

 走らせた結果は、

  param_vth__threshold  mean_score_time  mean_test_score
0                    0         9.646958         0.656178
1                 0.05         4.504278         0.587149
2                  0.1         2.804257         0.512551
3                 0.15         1.909767         0.453244

 改善する訳ではない。CountVectorizerのmin_dfで予めゴミ変数を削っていること、スパースな空間なので分散が低くてもそれはそれで構わず、ナイーブベイズが意外とスパースに強いのも相まって優秀に働いていることが原因でしょう。

 それより注目すべきはmean_score_timeで、今回のデータで変数を削っていくと、しきい値を0.05上げるたびに、0.07ポイントくらいの性能低下と引き換えに半減するような傾向です。性能と時間のトレードオフになったときは、これをいじって調整する手はあるのかも。

まとめ

 微妙といえば微妙だし、データによっては効くのかもしれない気もします。とりあえず確実に速くはなります。

 性能はあまり重視していないとき、気楽に変数を捨てて速くするのに使えそうです。

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
 こちらもうまく元通りにはならないようです。そもそもデータが悪いという話はあると思います。

結論

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

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