【python】numpy.meshgridの基本的な使い方まとめ
はじめに
numpyのmeshgridの使い方があまり理解できなくて、なんとなくコピペで動かしていたので、どのようなものなのかまとめておくことにしました。
目次
とりあえずmeshgridを作ってみる
meshgrid自体はこのように何の変哲もないものです。
>>> import numpy as np >>> a = np.arange(0,3,0.5) >>> b = np.arange(0,10,2) >>> a array([0. , 0.5, 1. , 1.5, 2. , 2.5]) >>> b array([0, 2, 4, 6, 8]) >>> X, Y = np.meshgrid(a, b) >>> X array([[0. , 0.5, 1. , 1.5, 2. , 2.5], [0. , 0.5, 1. , 1.5, 2. , 2.5], [0. , 0.5, 1. , 1.5, 2. , 2.5], [0. , 0.5, 1. , 1.5, 2. , 2.5], [0. , 0.5, 1. , 1.5, 2. , 2.5]]) >>> Y array([[0, 0, 0, 0, 0, 0], [2, 2, 2, 2, 2, 2], [4, 4, 4, 4, 4, 4], [6, 6, 6, 6, 6, 6], [8, 8, 8, 8, 8, 8]])
つまり(0,0)の値を(X[0,0],Y[0,0])として表現できる。ここまでは常識的な話。
計算する
numpy配列なので、配列全体でそのまま計算することができます。numpyでいうところの[1, 2, 3] + [4, 5, 6]が[5, 7, 9]になるのと同じです(この表記はリストのリテラルなのでできませんが)。
>>> Z = np.sin(X+Y) >>> Z array([[ 0. , 0.47942554, 0.84147098, 0.99749499, 0.90929743, 0.59847214], [ 0.90929743, 0.59847214, 0.14112001, -0.35078323, -0.7568025 , -0.97753012], [-0.7568025 , -0.97753012, -0.95892427, -0.70554033, -0.2794155 , 0.21511999], [-0.2794155 , 0.21511999, 0.6569866 , 0.93799998, 0.98935825, 0.79848711], [ 0.98935825, 0.79848711, 0.41211849, -0.07515112, -0.54402111, -0.87969576]])
ま、これはmeshgridの機能というより、numpyの機能。
plotしてみる
ぶっちゃけ3Dのplot以外でmeshgrid使うことってあるんですか・・・?
>>> import matplotlib.pyplot as plt >>> from mpl_toolkits.mplot3d import Axes3D >>> fig = plt.figure() >>> ax = Axes3D(fig) >>> ax.plot_wireframe(X, Y, Z) <mpl_toolkits.mplot3d.art3d.Line3DCollection object at 0x7f04d0367860> >>> plt.show()
XとYを足してsinに通すという謎の関数にしてしまったので見た目は気持ち悪いですが、それはさておきうまくプロットすることができています。matplotlibには同じような3次元データのプロット用の関数がたくさんあり、だいたいmeshgridを受け付けるので、いろいろな方法でプロットすることができます。
xyz座標の配列に変換する
meshgridからx,y,zの座標列に変換したい、ということもあるでしょう。あまり難しく考える必要はなくて、Xをflattenすればx座標列が、Yをflattenすれば……というように座標を得ることができ、あとは適当に結合すれば任意の形にできます。
>>> x = X.ravel() >>> y = Y.ravel() >>> z = Z.ravel() >>> x.shape (30,) >>> y.shape (30,) >>> z.shape (30,) >>> x array([0. , 0.5, 1. , 1.5, 2. , 2.5, 0. , 0.5, 1. , 1.5, 2. , 2.5, 0. , 0.5, 1. , 1.5, 2. , 2.5, 0. , 0.5, 1. , 1.5, 2. , 2.5, 0. , 0.5, 1. , 1.5, 2. , 2.5]) >>> y array([0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 4, 4, 4, 4, 4, 4, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8]) >>> z array([ 0. , 0.47942554, 0.84147098, 0.99749499, 0.90929743, 0.59847214, 0.90929743, 0.59847214, 0.14112001, -0.35078323, -0.7568025 , -0.97753012, -0.7568025 , -0.97753012, -0.95892427, -0.70554033, -0.2794155 , 0.21511999, -0.2794155 , 0.21511999, 0.6569866 , 0.93799998, 0.98935825, 0.79848711, 0.98935825, 0.79848711, 0.41211849, -0.07515112, -0.54402111, -0.87969576])
こうすると色々な関数に渡せそうで便利。結合については、こちらの記事を参照してください。
たとえばxyzで(n_samples, 3)にしたければnp.stack([x, y, z], axis=1)などでよさそうです。
逆にxyz座標からmeshgridを生成する
x, y, zの座標データが与えられていて、プロットするためにmeshgridに変換したいというシチュエーションもあります。
この場合は元のデータが都合よくグリッド状になっていないことが多いので、補完という処理をした上で変換することが可能です。
詳細はこちらの記事を御覧ください。
まとめ
いろいろ動かしてみると、それなりにやっていることが単純なのがわかって、親近感が湧いてきました。とにかく三次元プロットでは必需品なので、meshgridは使いこなそうということです。
【python】sklearnのclass_weightの挙動
はじめに
先に断っておくと、class_weightの挙動はモデルによって異なる可能性が十分ある。今回はsklearn.svm.SVCとsklearn.ensemble.RandomForestClassifierのドキュメントを参照して、一応基本的に共通する部分を抜き出した。
class_weightを調整する必要が出てきたときは、自分が使うモデルで確認してください。
参考:
3.2.4.3.1. sklearn.ensemble.RandomForestClassifier — scikit-learn 0.20.1 documentation
sklearn.svm.SVC — scikit-learn 0.20.1 documentation
解説
与えられる引数はNone(デフォルト)、辞書、"balanced"という文字列の3種類というのが基本だと思う*1。
Noneを渡した場合、class_weightはすべてのクラスに対して1であると仮定される。これは悩むような話ではない。
辞書を渡す場合、キーはクラスラベル、値は重みになる。すべてのクラスラベルと対応する重みを辞書の要素に与えなくても動くようだが、まあ与えた方が無難。詳しく検証はしていない。
重みは大きいほどそのクラスを重視する意味になる。なので、たとえばクラス0に100件、クラス1に1000件のデータがあったとしたら、何もしないとクラス1が重視されすぎてしまう懸念がある。なので、クラス1を1/10してやるか、クラス0を10倍してやれば同じ比率になるだろう、という気がする。
{0:1000/100,1:1} {0:1, 1:100/1000}
直感的にはこんな感じで良い。
"balanced"を指定すると似たような演算をしてくれる。ただし、ちょっとやっていることが異なる。公式によると、この演算が行われるらしい。
n_samples / (n_classes * np.bincount(y))
n_samplesは全サンプル数の合計(恐らくfit時のサンプルである)。n_classesはクラス数だが、np.bincountというのが見慣れない処理である。でもやることは簡単。
>>> import numpy as np >>> np.bincount([0,0,0,1,1,1,1,2,2,2,2,3,4,5,6,5,4,4,4,3,2,1]) array([3, 5, 5, 2, 4, 2, 1])
つまりクラスラベルごとに出現回数を数えている(index=0の要素が0の出現回数・・・という仕組み)。これの逆数を取って、あとは定数をかけているだけの式と解釈できる。そこそこ無難そうな式ではある。
まとめ
普通は意識しないと思います・・・。
クラスごとのサンプル数の偏りが無視できそうな程度に小さかったり、無視できそうな程度にサンプルが大量にある場合はNone(デフォルトのまま)、
偏っている場合でも特殊なケース以外は"balanced"で良いと思う。
これをパラメタチューニングしてどうこうとかは、考えない方が良いんじゃないかなぁ・・・。
*1:ランダムフォレストは複数値への分類に対応しているのでlistも受け取るようだ。また、ランダムフォレストはブートストラップの絡みで“balanced_subsample”というのも指定できるようだ。これらは特殊だと思うので説明しない(ドキュメントに書いてあるし)
【python】sklearnで「何もしない」モデルがほしい
sklearnで「何もしない」モデルがあると、チョー便利。個人的にはそう思う。
どうやって使うかというと、具体的には以前の記事で書いたFeatureUnionと組み合わせて使う。
参考(以前の記事):【python】複数の特徴をまとめるFeatureUnion - 静かなる名辞
たとえば、100次元の特徴量があったとき、「入力をそのまま出力する(何もしない)モデル」と「何らかの加工をして出力するモデル」を組み合わせたモデルに入力すれば、100次元+加工した特徴量の次元の新たな特徴量とすることができる。これが有効な場面はそれなりに多そう。
・・・なのだが、sklearnにはそれに当てはまるようなモデルは用意されてなさそうだし、ググっても出てこない。自分で書いている人はいた。
from sklearn.base import BaseEstimator, TransformerMixin class IdentityTransformer(BaseEstimator, TransformerMixin): def __init__(self): pass def fit(self, input_array, y=None): return self def transform(self, input_array, y=None): return input_array*1
引用元:sklearn Identity-transformer – Laurent H. – Medium
原文通りだけど*1は要らないと思う。とにかく、同じようなことを考える人はいるものだ。簡単に書けるのもわかったけど、そのために自分でクラスを定義するのもなんだかなぁ、という気がする。もっと色々なメソッドに対応させようとすると面倒臭さが増していきそうなのも、あまりよくない。
そこで改めてドキュメントを漁ったら、使えそうなのがあった。FunctionTransformerなるもの。
sklearn.preprocessing.FunctionTransformer — scikit-learn 0.20.1 documentation
任意の関数を渡してTransformerを生成できるというなかなかのスグレモノである。こいつは関数が渡されないとき、入力をそのまま出力するとドキュメントに書いてある。使えそう。簡単に確認する。
>>> from sklearn.datasets import load_iris >>> from sklearn.preprocessing import FunctionTransformer >>> iris = load_iris() >>> identity_transformer = FunctionTransformer() >>> iris.data[:10] array([[5.1, 3.5, 1.4, 0.2], [4.9, 3. , 1.4, 0.2], [4.7, 3.2, 1.3, 0.2], [4.6, 3.1, 1.5, 0.2], [5. , 3.6, 1.4, 0.2], [5.4, 3.9, 1.7, 0.4], [4.6, 3.4, 1.4, 0.3], [5. , 3.4, 1.5, 0.2], [4.4, 2.9, 1.4, 0.2], [4.9, 3.1, 1.5, 0.1]]) >>> identity_transformer.fit_transform(iris.data[:10]) array([[5.1, 3.5, 1.4, 0.2], [4.9, 3. , 1.4, 0.2], [4.7, 3.2, 1.3, 0.2], [4.6, 3.1, 1.5, 0.2], [5. , 3.6, 1.4, 0.2], [5.4, 3.9, 1.7, 0.4], [4.6, 3.4, 1.4, 0.3], [5. , 3.4, 1.5, 0.2], [4.4, 2.9, 1.4, 0.2], [4.9, 3.1, 1.5, 0.1]])
使えた。
本来の使い方ではないと思うけど、これがベター・・・なのかなぁ。もっと良いやり方を知っている方がいたら、教えてください。
【python】複数の特徴をまとめるFeatureUnion
単一の入力データから、複数の処理方法で幾つもの異なる特徴量が得られる・・・というシチュエーションがある。
この場合、「どれが最善か」という観点でどれか一つを選ぶこともできるけど、そうすると他の特徴量の情報は捨ててしまうことになる。総合的な性能では他に一歩譲るが、有用な情報が含まれている特徴量がある・・・というような場合は、ちょっと困る。
こういう状況で役に立つのがFeatureUnion。特徴抽出や次元削減などのモデルを複数まとめることができる。
結果はConcatenateされる。Concatenateというのがわかりづらい人もいると思うけど、たとえば手法1で10次元、手法2で20次元の特徴量ベクトルが得られたら、これをそのまま横に繋げて30次元のベクトルとして扱うということ。
sklearn.pipeline.FeatureUnion — scikit-learn 0.20.1 documentation
ちなみに、こいつはsklearn.pipeline以下に存在する。Pipelineの兄弟みたいな扱い。引数の渡し方とかもほとんど同じである。
簡単に試してみよう。digitsの分類を行うことにする。PCA+GaussianNB, LDA+GNB, FeatureUnion(PCA, LDA)+GNBの3パターンでスコアを見比べる。
import warnings warnings.filterwarnings('ignore') from sklearn.datasets import load_digits from sklearn.decomposition import PCA from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA from sklearn.naive_bayes import GaussianNB from sklearn.pipeline import Pipeline, FeatureUnion from sklearn.model_selection import cross_validate, StratifiedKFold def main(): digits = load_digits() pca = PCA(n_components=30) lda = LDA() gnb = GaussianNB() pca_gnb = Pipeline([("pca", pca), ("gnb", gnb)]) lda_gnb = Pipeline([("lda", lda), ("gnb", gnb)]) pca_lda_gnb = Pipeline([("reduction", FeatureUnion([("pca", pca), ("lda", lda)])), ("gnb", gnb)]) scoring = {"p": "precision_macro", "r": "recall_macro", "f":"f1_macro"} for name, model in zip(["pca_gnb", "lda_gnb", "pca_lda_gnb"], [pca_gnb, lda_gnb, pca_lda_gnb]): skf = StratifiedKFold(shuffle=True, random_state=0) scores = cross_validate(model, digits.data, digits.target, cv=skf, scoring=scoring) p = scores["test_p"].mean() r = scores["test_r"].mean() f = scores["test_f"].mean() print(name) print("precision:{0:.3f} recall:{1:.3f} f1:{2:.3f}".format(p,r,f)) if __name__ == "__main__": main()
結果は、
pca_gnb precision:0.947 recall:0.944 f1:0.945 lda_gnb precision:0.955 recall:0.953 f1:0.953 pca_lda_gnb precision:0.959 recall:0.957 f1:0.957
ちょっと微妙だけど、誤差ではないみたい。このように比較的手軽に性能を改善できることがわかる(効くかどうかはケースバイケースだけど)。
有意水準5%の論文が100本あったら
この記事は思いついたままに書いたポエム。
有意水準5%とは、その判断(主張)の妥当性が95%である、ということを意味する。
よって、有意水準5%で検定したら、100回に5回は第1種の過誤を犯す。
有意水準5%の論文が100本あったら、(いちおうすべての論文が正しいプロセスを踏んでいると仮定しても)うち5本は間違っている。
恐らく現実の論文はそんなに酷いことにはなっていないと思う(ただし、100本あったらそもそもプロセスが正しくないものは一定数入ってくるだろうけど)。これがどういうことなのかというと、
- そもそも5%なんて甘い有意水準は使っていない(これはあると思うけど、とりあえず無視することにする)
- 最初からある程度有力な仮説を立てて検証しているので、95%に「仮説の妥当性」がかかってくると考えられる(ちょっと異論もあるかもしれないが、「ある仮説を妥当だと思って検証し、けっきょく妥当ではなかったという事象の可能性」を考えるとけっきょく効いてくると思われる)
- 論文に発表した以外にも色々実験をしたりして、妥当性を判断している。一回検定しただけ、というのは考えづらい(逆に言えば、再現が難しい話(世の中から取ってきた統計量をそのまま使うような奴)だと5%は割とそのまま5%かもしれないので、注意が必要)
恐らくこのような事情が絡んでくるので、有意水準5%の妥当性は実際には98%とか99%とか、それぐらいには信頼できるように(個人的には)思える*1。
*1:だからって98%とか99%でも割ときつい水準だと思うので、これ以上緩める理由はないと思うが
【python】1つおきにリスト・文字列などから抽出する
スライスの基本的な話なんだけど、意外と知らない人が多いと思うので。
スライスで1つおきに取り出すには、こうする。
>>> "hogehoge~"[::2] 'hghg~'
スライスで指定できるのはstart, stop, stepであり、上のように指定するとstart, stopはNoneでstepが2になる。
参考:【python】sliceのちょっと深イイ(かもしれない)話 - 静かなる名辞
ではstepを1にすると? これは元の文字列が返る。
>>> "hogehoge~"[::1] 'hogehoge~'
たまにこれをreverseに悪用(?)する人がいる。[::-1]と指定するとそういう挙動になるので。
>>> "hogehoge~"[::-1] '~egohegoh'
でもこれは可読性が悪い、というか完全に初見殺しなので、やめた方が良いと思う。reversedというものがあるので、そっちを使おう。
・・・と思って書いてみたけど、腹立たしいことに空気を読まない子(iteratorで返してくれるアレ)なので、こんな真似をする必要がある。
>>> "".join(reversed("hogehoge~")) '~egohegoh'
どっちがマシかなぁ・・・。
話をもとに戻して、[::-1]でこうなるということは、これもできる。1つおきに取り出して逆転させる。
>>> "hogehoge~"[::-2] '~ghgh'
便利、なのだろうか(どんなときに?)。
【python】sliceのちょっと深イイ(かもしれない)話
リスト(じゃなくてもだけど)に次のようにアクセスするとき、内部的には__getitem__が呼ばれていることは、歴戦のpythonistaの皆さんには常識でしょう。
>>> lst = [1,2,3,4,5] >>> lst[0] 1
この様子を自作クラスで観察してみましょう。
>>> class Hoge: ... def __getitem__(self, k): ... print("__getitem__!") ... return k ... >>> h = Hoge() >>> h[0] __getitem__! 0 >>> h["hogehoge~"] __getitem__! 'hogehoge~'
Hogeクラスは__getitem__が呼ばれると、「__getitem__!」とprintしてから__getitem__の引数をそのまま返します。上の結果から、実際に__getitem__が呼ばれていることがわかります。
[]は__getitem__の糖衣構文と言っても、まあ良いでしょう*1。
これで「ほーん、そうか」と納得しかけてしまいますが、「ちょっと待て、じゃあスライスはどうなるんだ・・・?」というのが疑問として浮かんできますね。
lst[0:5]
一体何が渡るというんでしょう。「0:5」なんてオブジェクトはありませんから(そのまま書いても構文エラー)、スライスの場合は__getitem__とは違う仕組みで処理されるのでしょうか?
そうはなっていません。
>>> h[0:5] __getitem__! slice(0, 5, None) >>> type(h[0:5]) __getitem__! <class 'slice'>
おおお、sliceオブジェクトなんていうのが渡っている・・・。
このsliceオブジェクトは、普通にpythonを書いている限り目にする機会はほとんどないと思います。
それでも、公式ドキュメントにはしっかり載っています。
class slice(start, stop[, step])
range(start, stop, step) で指定されるインデクスの集合を表す、スライス (slice) オブジェクトを返します。引数 start および step はデフォルトでは None です。スライスオブジェクトは読み出し専用の属性 start、stop および step を持ち、これらは単に引数で使われた 値 (またはデフォルト値) を返します。これらの値には、その他のはっきりと した機能はありません。しかしながら、これらの値は Numerical Python および、その他のサードパーティによる拡張で利用されています。スライスオブジェクトは拡張されたインデクス指定構文が使われる際にも生成されます。例えば a[start:stop:step] や a[start:stop, i] です。この関数の代替となるイテレータを返す関数、itertools.islice() も参照してください。
2. 組み込み関数 — Python 3.6.5 ドキュメント
なるほど~、こいつが渡ることで、あとは受けるオブジェクトの__getitem__が然るべき処理をしてくれればスライスが実現する仕組みになっているんですね。よくできてる・・・。
「いや、ちょっと待て。numpyで使うアレはどうなってるんだ」
こういう奴のことですね。
a[:,0]
上記sliceオブジェクトはstart, stop, stepしか持たないので、こういうスライスは手に負えなさそうです。
なんてこった、今度こそ__getitem__では処理しきれなくて、なにか違う仕組みで処理されているのか・・・
いません。
>>> h[:,0] __getitem__! (slice(None, None, None), 0) >>> type(h[:,0]) __getitem__! <class 'tuple'>
__getitem__なのは間違いないみたいですが、 なぜかtupleが返ります。0要素目は空のslice, 二番目は入力がそのまま・・・? 一体どうなっているんだ?
実はこうなっています。
>>> h[::,0:1:2,0:-1:-2,0,1,2,3] __getitem__! (slice(None, None, None), slice(0, 1, 2), slice(0, -1, -2), 0, 1, 2, 3)
なるほど、カンマで区切られたものごとにtupleの要素になっているのか!
……と、書きましたが一応ちゃんと説明しておくと、そもそも「pythonのtuple」はカンマで区切られた要素によって成立する構文です。関数の引数リストなど、他の構文と被る場合はそちらが優先的に扱われますが。
>>> 1,2,3 (1, 2, 3)
これについてはドキュメントに説明があります。
4. 組み込み型 — Python 3.6.5 ドキュメント
要するに、単に添字の中にtupleを書いているだけ、とみなしてもまあ良いでしょう*2。
わかっちゃえばなんてことはないですね。とても自然な仕様です。あとは受け取った側で然るべき処理をするだけ。
このように、スライスの裏ではsliceオブジェクトが暗躍しています。なんとなくこういうことを知っていると深イイと思えますね。また、なんとなくスライスの構文がよくわからなかった人も、この仕様が理解できれば自然に複雑なスライスを書けるようになることでしょう(その結果、難読コードが量産されてしまうかもしれないが・・・)。
余談
>>> lst = [0,1,2] >>> lst[:,0] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: list indices must be integers or slices, not tuple
上のことがわかっていれば理解できますが、そうでないと絶対理解不能なエラーメッセージです(tupleなんかないじゃんかって)。
追記:2018/06/21
この話はドキュメントのどこに載っているんだろうとずっと思っていましたが、見つけました。
6. 式 (expression) — Python 3.6.5 ドキュメント
スライス表記に対する意味付けは、以下のようになります。プライマリの値評価結果は、以下に述べるようにしてスライスリストから生成されたキーによって (通常の添字表記と同じ __getitem__() メソッドを使って) インデクス指定できなければなりません。スライスリストに一つ以上のカンマが含まれている場合、キーは各スライス要素を値変換したものからなるタプルになります; それ以外の場合、単一のスライス要素自体を値変換したものがキーになります。一個の式であるスライス要素は、その式に変換されます。適切なスライスは、スライスオブジェクト (標準型の階層 参照) に変換され、その start, stop および step 属性は、それぞれ指定した下境界、上境界、およびとび幅 (stride) になります。式がない場所は None で置き換えられます。
説明の内容はこの記事と基本的に同じですが、グダグダなこの記事と比べると、さすがによくまとまっています。公式は偉い。
複数の目的変数で回帰を行う方法
はじめに
回帰分析を行う際、複数の目的変数に対して回帰をしたい場合があります。普通のモデルではできないのでちょっと面食らいますが、やり方は色々あるようです。
目次
スポンサーリンク
目的変数の数だけ回帰モデルを作る方法
単純に考えると、一つの目的変数を出力する回帰モデルを目的変数の数だけ用意してやれば、所要を達しそうです。
python+sklearnを使えば、これに対応したモデルが最初から用意されています。
sklearn.multioutput.MultiOutputRegressor — scikit-learn 0.20.2 documentation
コンストラクタには好きな回帰モデルを渡してあげることができます。それが目的変数の数だけコピーされ、内部で束ねられて回帰に使われます*1。
複数の目的変数に対応したモデルを使う
上の方法は単純ですが、回帰モデルの中には自然に複数の出力に対応しているものもあります。
そういったモデルを使うことにどんなメリットがあるのか? というと、まず目的変数の数だけ回帰モデルを作るのと比べて無駄が減るので、計算コストがケチれる可能性があります(あくまでも「可能性」の話)。
また、複数存在する目的変数の間に何らかの相関性があれば、それも踏まえて上手く学習することでモデルの性能が上がる可能性があります(こちらもあくまでも「可能性」)。
そういった複数の目的変数に対応したモデルを幾つか紹介します。すべては網羅しきれないので、その点はご承知ください。
正準相関分析
正準相関分析はこの手の話で出てくる代表的なモデルです。単純な手法ですが、けっこう奥深いといえば奥深いです。
参考(過去に書いた記事):【python】正準相関分析(Canonical Correlation Analysis)を試してみる - 静かなる名辞
これの良いところは、説明変数と目的変数*2のそれぞれでPCAみたく新たな軸を張り、次元削減を行ってくれることです。説明変数ン百次元、目的変数20次元みたいなケースだったとしても、次元削減の効果で「わかりやすい」結果が得られる可能性があります。つまり、現象を説明するモデルとしては非常に適しています。
欠点は、回帰モデルとして考えると性能が高いと言えるかは微妙なこと、非線形への対応は基本的にはないことです。カーネルPCAみたくカーネル法で非線形対応させたモデルもありますが、良さげなライブラリが見当たらないのと、そこまでするなら他の手法を使いたいという気持ちがあるので紹介しません。
sklearnのモデルはこれです。上に書いた通りカーネル正準相関の実装はありません。
sklearn.cross_decomposition.CCA — scikit-learn 0.20.2 documentation
predictメソッドでXからYを予測できるので、普通に回帰に使えます。
入出力が割と線形なデータで、「説明」を重視したいときは使えると思います。
ランダムフォレスト回帰
なぜかランダムフォレスト回帰は複数出力に対応しています。解説論文を見つけたので貼っておきます。興味のある方はどうぞ(私は読んでいません)。
とにかく使いたければsklearnのRandomForestRegressorはそのまま使えます。目的変数も説明変数と同様に配列で入れてあげてください。
3.2.4.3.2. sklearn.ensemble.RandomForestRegressor — scikit-learn 0.20.2 documentation
多層パーセプトロン(ニューラルネットワーク回帰)
ニューラルネットですからできて当然。複数出力にするためにやることといったら出力層ユニット数を増やすだけですから、一番シンプルかもしれません。これもsklearnのがそのまま使えます。
sklearn.neural_network.MLPRegressor — scikit-learn 0.20.2 documentation
まとめ
複数の目的変数に対して回帰を行う場合について、2種類の方法を説明しました。
- 単純に目的変数の数だけ回帰モデルを用意する方法
- 複数の目的変数を出力できるモデルを用いる方法
どちらが良いかは一概には言えません。データや目的に応じて、あるいは実際に走らせてみて評価指標や計算コストを勘案して考える必要があります。複数の目的変数に最初から対応したモデルの方が良いような気もしますが、そうとも言えないんじゃという話もあったりします。
でもまあ、色々な選択肢があることは良いことです。いろいろ勘案して選べば良いでしょう。適当ですがこんな感じでシメます。
【python】リストの各要素に違う処理をする
問題設定
想定しているのは、たとえばこんなシチュエーションです。
s = "hoge! 1234" tmp = s.split() lst = [tmp[0], int(tmp[1])]
要するに、比較的短いリストだが性質の違うものが入っており、それぞれ違う処理をして返したいのです。
それだけなら良いのですが、上の例だとs.split()の結果を一時変数に入れないとどうしようもないので(二回呼ぶと再計算されてしまう)、まどろっこしいことになります。ここでリスト内包を使おうとしても、まともな処理は書けません(ん、zipでlambdaと一緒に回せばできなくはないか)。
(私が思いつく)解決策は2つあります。どちらも関数を使います。
解決策1:愚直に関数定義
関数の引数に渡せばローカル変数として束縛されるので、できるという考え方です。
def f(x): return [x[0], int(x[1])] s = "hoge! 1234" lst = f(s.split())
もういっそsを渡す関数にした方が素直ですが、趣旨からずれるのでNG。
この方法は記述が短くならないという欠点があります。
解決策2:lambda
lispのletと同じテクニックで一時変数を束縛しています。
s = "hoge! 1234" lst = (lambda t:[t[0], int(t[1])])(s.split())
とても短く書け、簡潔な記述です。可読性は人によって異なると思いますが、個人的には読みづらいと思います。
まとめ
このテクニックはリスト内包表記の中でどうしても一時変数が必要になったときに使えます。覚えておいて損はないです。
【python】calendarモジュールの使い方
calendarモジュールは標準ライブラリに入っていて、曜日や日付の計算にはけっこう便利なモジュールらしいです。
でもあまり周知されていないので、使い方を(自分用に)メモっておきます。
ドキュメントはここです。
8.2. calendar — 一般的なカレンダーに関する関数群 — Python 3.6.5 ドキュメント
目次
スポンサーリンク
introduction
>>> import calendar >>> print(calendar.month(2018, 4)) April 2018 Mo Tu We Th Fr Sa Su 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
テキストのカレンダーが出てきました。ちょっとほっこりします。
日曜から週が始まるようにもできます。
>>> calendar.setfirstweekday(calendar.SUNDAY) >>> print(calendar.month(2018, 4)) April 2018 Su Mo Tu We Th Fr Sa 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
オブジェクト指向でも関数型でもない、手続き型パラダイムって感じです>setfirstweekday()。ちょっとおっかない。このへんに依存する処理をしたいときは、import先やimport元で変なことにならないよう、一々確認してあげる必要があるということであります。
まあ、ちゃんとクラスインスタンスを作れるので、そっちを使うようにすれば良いのですが。
calendarモジュールのクラス
以下のクラスがあります。
- Calendar
- TextCalendar
- HTMLCalendar
- LocaleTextCalendar
- LocaleHTMLCalendar
なんとなく酷い気もしますが、大目に見ましょう。HTMLはどうでも良いので(いや、使いたいって人もいるだろうけど)、TextCalendarでintroductionと同じことをやってみます。
>>> from calendar import TextCalendar >>> tcalendar = TextCalendar(firstweekday=6) >>> print(tcalendar.formatmonth(2018, 4)) April 2018 Su Mo Tu We Th Fr Sa 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
firstweekday=6は、0が月曜日で6が日曜日という仕様なので、こうしております。
ロケールも一応やってみます。
>>> from calendar import LocaleTextCalendar >>> ltc = LocaleTextCalendar(firstweekday=6, locale="ja_JP.UTF-8") >>> print(ltc.formatmonth(2018, 4, w=3)) April 2018 Su Mo Tu We Th Fr Sa 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 >>> ltc = LocaleTextCalendar(firstweekday=6, locale="ja_JP.UTF-8") >>> print(ltc.formatmonth(2018, 4, w=5)) 4月 2018 日 月 火 水 木 金 土 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
フォントの問題があるので、何をやってもあまり綺麗には見えません。半角英数と全角文字を同じ文字幅で表示するフォントがあれば、綺麗に見えることでしょう。
機能
さて、存在するクラスのうち、
- TextCalendar
- HTMLCalendar
- LocaleTextCalendar
- LocaleHTMLCalendar
これらはCalendarのサブクラスです。出力形式をformatするだけに存在しています。
曜日や日付の計算に使いたいと思う機能は、
- Calendar
クラスに集約されています。
そしてここには大したメソッド数はありません。なので、一つずつ紹介していきましょう。
iterweekdays()
曜日の数字を一週間分生成するイテレータを返します。イテレータから得られる最初の数字は firstweekday が返す数字と同じになります。
>>> from calendar import Calendar >>> cl = Calendar() >>> cl.iterweekdays() <generator object Calendar.iterweekdays at 0x7fca5a3d3ca8> >>> list(cl.iterweekdays()) [0, 1, 2, 3, 4, 5, 6]
これだけ。
itermonthdates(year, month)
year 年 month (1–12) 月に対するイテレータを返します。 このイテレータはその月の全ての日、およびその月が始まる前の日とその月が終わった後の日のうち、週の欠けを埋めるために必要な日を (datetime.date オブジェクトとして) 返します。
指定した年月の日を返しますが、週の中途半端なところから始まったり、中途半端なところで終わったりすると、前後の月も週が中途半端じゃなくなる範囲まで出してくれます。長いので整形した出力を見せます。
>>> cl = Calendar(firstweekday=6) >>> list(cl.itermonthdates(2018, 4)) [datetime.date(2018, 4, 1), datetime.date(2018, 4, 2), datetime.date(2018, 4, 3), datetime.date(2018, 4, 4), datetime.date(2018, 4, 5), datetime.date(2018, 4, 6), datetime.date(2018, 4, 7), datetime.date(2018, 4, 8), datetime.date(2018, 4, 9), datetime.date(2018, 4, 10), datetime.date(2018, 4, 11), datetime.date(2018, 4, 12), datetime.date(2018, 4, 13), datetime.date(2018, 4, 14), datetime.date(2018, 4, 15), datetime.date(2018, 4, 16), datetime.date(2018, 4, 17), datetime.date(2018, 4, 18), datetime.date(2018, 4, 19), datetime.date(2018, 4, 20), datetime.date(2018, 4, 21), datetime.date(2018, 4, 22), datetime.date(2018, 4, 23), datetime.date(2018, 4, 24), datetime.date(2018, 4, 25), datetime.date(2018, 4, 26), datetime.date(2018, 4, 27), datetime.date(2018, 4, 28), datetime.date(2018, 4, 29), datetime.date(2018, 4, 30), datetime.date(2018, 5, 1), datetime.date(2018, 5, 2), datetime.date(2018, 5, 3), datetime.date(2018, 5, 4), datetime.date(2018, 5, 5)]
あんまり嬉しくないかも・・・。
itermonthdays2(year, month)
year 年 month 月に対する itermonthdates() と同じようなイテレータを返します。生成されるのは日付の数字と曜日を表す数字のタプルです。
上とほぼ同じ。返り値の型だけ違います。
>>> list(cl.itermonthdays2(2018, 4)) [(1, 6), (2, 0), (3, 1), (4, 2), (5, 3), (6, 4), (7, 5), (8, 6), (9, 0), (10, 1), (11, 2), (12, 3), (13, 4), (14, 5), (15, 6), (16, 0), (17, 1), (18, 2), (19, 3), (20, 4), (21, 5), (22, 6), (23, 0), (24, 1), (25, 2), (26, 3), (27, 4), (28, 5), (29, 6), (30, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5)]
前後月の日付は0で返されるようです。タプルの二番目の要素は例の曜日を表す数字。
これを処理すると簡単そうで良いですね。
itermonthdays(year, month)
year 年 month 月に対する itermonthdates() と同じようなイテレータを返します。生成されるのは日付の数字だけです。
なんで同じようなメソッドがいくつもあるんだろうか。
>>> list(cl.itermonthdays(2018, 4)) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 0, 0, 0, 0, 0]
(命名が)投げやりだなー(棒)。
monthdatescalendar(year, month)
year 年 month 月の週のリストを返します。週は全て七つの datetime.date オブジェクトからなるリストです。
週のリストを返すんだって。メソッド名からは想像できない機能で、ちょっとびっくりしています。
>>> list(cl.monthdatescalendar(2018, 4)) [[datetime.date(2018, 4, 1), datetime.date(2018, 4, 2), datetime.date(2018, 4, 3), datetime.date(2018, 4, 4), datetime.date(2018, 4, 5), datetime.date(2018, 4, 6), datetime.date(2018, 4, 7)], [datetime.date(2018, 4, 8), datetime.date(2018, 4, 9), datetime.date(2018, 4, 10), datetime.date(2018, 4, 11), datetime.date(2018, 4, 12), datetime.date(2018, 4, 13), datetime.date(2018, 4, 14)], [datetime.date(2018, 4, 15), datetime.date(2018, 4, 16), datetime.date(2018, 4, 17), datetime.date(2018, 4, 18), datetime.date(2018, 4, 19), datetime.date(2018, 4, 20), datetime.date(2018, 4, 21)], [datetime.date(2018, 4, 22), datetime.date(2018, 4, 23), datetime.date(2018, 4, 24), datetime.date(2018, 4, 25), datetime.date(2018, 4, 26), datetime.date(2018, 4, 27), datetime.date(2018, 4, 28)], [datetime.date(2018, 4, 29), datetime.date(2018, 4, 30), datetime.date(2018, 5, 1), datetime.date(2018, 5, 2), datetime.date(2018, 5, 3), datetime.date(2018, 5, 4), datetime.date(2018, 5, 5)]]
monthdays2calendar(year, month)
year 年 month 月の週のリストを返します。週は全て七つの日付の数字と曜日を表す数字のタプルからなるリストです。
>>> list(cl.monthdays2calendar(2018, 4)) [[(1, 6), (2, 0), (3, 1), (4, 2), (5, 3), (6, 4), (7, 5)], [(8, 6), (9, 0), (10, 1), (11, 2), (12, 3), (13, 4), (14, 5)], [(15, 6), (16, 0), (17, 1), (18, 2), (19, 3), (20, 4), (21, 5)], [(22, 6), (23, 0), (24, 1), (25, 2), (26, 3), (27, 4), (28, 5)], [(29, 6), (30, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5)]]
・・・説明、要る?
monthdayscalendar(year, month)
year 年 month 月の週のリストを返します。週は全て七つの日付の数字からなるリストです。
>>> list(cl.monthdayscalendar(2018, 4)) [[1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14], [15, 16, 17, 18, 19, 20, 21], [22, 23, 24, 25, 26, 27, 28], [29, 30, 0, 0, 0, 0, 0]]
このモジュールの世界観は理解してしまえばとてもわかりやすいので、そういう意味では良いと思います。
yeardatescalendar(year, width=3)
指定された年のデータを整形に向く形で返します。返される値は月の並びのリストです。月の並びは最大で width ヶ月(デフォルトは3ヶ月)分です。各月は4ないし6週からなり、各週は1ないし7日からなります。各日は datetime.date オブジェクトです。
以下のメソッドの出力は長いので省略。どんなものが返るのかは、実行すればすぐ理解できます。
yeardays2calendar(year, width=3)
指定された年のデータを整形に向く形で返します (yeardatescalendar() と同様です)。週のリストの中が日付の数字と曜日の数字のタプルになります。月の範囲外の部分の日付はゼロです。
yeardayscalendar(year, width=3)
指定された年のデータを整形に向く形で返します (yeardatescalendar() と同様です)。週のリストの中が日付の数字になります。月の範囲外の日付はゼロです。
あー、疲れた。モジュールのリファレンスを書き写すような真似してもあまり意味なかったですね。
便利なusage
そのうち(思いついたら)書きます。第n何曜日の計算とかに使えると思います。
まとめ
ちょっと残念だけど、たまに使えるシチュエーションがありそうではある。
【python】pandasでデータを標準得点(z得点)に変換
データの正規化(標準化)をpandasでもやってみる。
正規化、標準化とは、データを分散1、平均0に変換する操作である。
スポンサーリンク
自分で書いてもできるが、scipyの関数を使うと簡単にできる。
>>> import pandas as pd >>> df = pd.DataFrame([[1,2,3,4,5,6], [6,5,4,3,2,1], [0,1,2,3,4,5], [5,4,3,2,1,0]], columns=[*"ABCDEF"]) >>> df.apply(stats.zscore, axis=0) A B C D E F 0 -0.784465 -0.632456 0.000000 1.414214 1.264911 1.176697 1 1.176697 1.264911 1.414214 0.000000 -0.632456 -0.784465 2 -1.176697 -1.264911 -1.414214 0.000000 0.632456 0.784465 3 0.784465 0.632456 0.000000 -1.414214 -1.264911 -1.176697 >>> df.apply(stats.zscore, axis=1) A B C D E F 0 -1.46385 -0.87831 -0.29277 0.29277 0.87831 1.46385 1 1.46385 0.87831 0.29277 -0.29277 -0.87831 -1.46385 2 -1.46385 -0.87831 -0.29277 0.29277 0.87831 1.46385 3 1.46385 0.87831 0.29277 -0.29277 -0.87831 -1.46385
axis=0だと列で計算した標準得点、axis=1で行で計算した標準得点になる。
【python】pandasでDataFrameの平均と標準偏差を計算する方法
概要
DataFrameから平均と標準偏差を計算する方法をメモしておきます。
目次
列の平均と標準偏差を計算したい
とても簡単にできます。
>>> import pandas as pd >>> df = pd.DataFrame([[1,2,3,4,5,6], [6,5,4,3,2,1], [0,1,2,3,4,5], [5,4,3,2,1,0]], columns=[*"ABCDEF"]) >>> df.mean() A 3.0 B 3.0 C 3.0 D 3.0 E 3.0 F 3.0 dtype: float64 >>> df.std() A 2.943920 B 1.825742 C 0.816497 D 0.816497 E 1.825742 F 2.943920 dtype: float64
何も考える必要はないのだった。
リファレンス:
pandas.DataFrame.mean — pandas 0.24.2 documentation
pandas.DataFrame.std — pandas 0.24.2 documentation
行の平均と標準偏差を計算したい
「転置しとけば?」という天の声が聞こえたのを無視してやります。
numpy配列のようにaxisを指定するだけなのでこれも簡単です。
>>> import pandas as pd >>> df = pd.DataFrame([[1,2,3,4,5,6], [6,5,4,3,2,1], [0,1,2,3,4,5], [5,4,3,2,1,0]], columns=[*"ABCDEF"]) >>> df.mean(axis=1) 0 3.5 1 3.5 2 2.5 3 2.5 dtype: float64 >>> df.std(axis=1) 0 1.870829 1 1.870829 2 1.870829 3 1.870829 dtype: float64
よくできてますね。
特定の列・行だけ取り出してから計算する
基本的なindexing操作と組み合わせて使うことで、特定の行・列だけに対して計算するということも可能です。
A, Bに対してのみ出力させたい場合。
>>> df[["A", "B"]].mean() A 3.0 B 3.0 dtype: float64
describeメソッドで全体の雰囲気を掴む
describeメソッドを使うと様々な統計量を勝手に出してくれます。
>>> df.describe() # 列ごとに A B C D E F count 4.00000 4.000000 4.000000 4.000000 4.000000 4.00000 mean 3.00000 3.000000 3.000000 3.000000 3.000000 3.00000 std 2.94392 1.825742 0.816497 0.816497 1.825742 2.94392 min 0.00000 1.000000 2.000000 2.000000 1.000000 0.00000 25% 0.75000 1.750000 2.750000 2.750000 1.750000 0.75000 50% 3.00000 3.000000 3.000000 3.000000 3.000000 3.00000 75% 5.25000 4.250000 3.250000 3.250000 4.250000 5.25000 max 6.00000 5.000000 4.000000 4.000000 5.000000 6.00000 >>> df.T.describe() # describeで行ごとに処理したい場合は転置する 0 1 2 3 count 6.000000 6.000000 6.000000 6.000000 mean 3.500000 3.500000 2.500000 2.500000 std 1.870829 1.870829 1.870829 1.870829 min 1.000000 1.000000 0.000000 0.000000 25% 2.250000 2.250000 1.250000 1.250000 50% 3.500000 3.500000 2.500000 2.500000 75% 4.750000 4.750000 3.750000 3.750000 max 6.000000 6.000000 5.000000 5.000000
参考:
pandas.DataFrame.describe — pandas 0.24.2 documentation
pandasのdescribeで各列の要約統計量(平均、標準偏差など)を取得 | note.nkmk.me
【python】辞書で同じキーに複数の値を登録する
ちょっとしたTips。
辞書(dict)は通常、一つのキーには一つの値しか登録できない。代入しても上書きされる。
>>> d = {} >>> d["hoge"] = 1 >>> d {'hoge': 1} >>> d["hoge"] = 2 >>> d {'hoge': 2}
こういうときどうすれば良いのかというと、値をリスト等にしておいて、そのリストにappendしていけば良い。どのように使いたいかにもよるのだが、大抵はこれで用が済む。
スポンサーリンク
defaultdictを使うと面倒な初期化処理が省けて便利。
>>> from collections import defaultdict >>> d = defaultdict(list) >>> d["hoge"].append(1) >>> d["hoge"].append(2) >>> d defaultdict(<class 'list'>, {'hoge': [1, 2]}) >>> d["hoge"] [1, 2]
別にlistじゃなくても、setだろうがdictだろうが何でも指定できる。tupleも指定はできるが、変更できないと何の役にも立たない。
>>> d = defaultdict(set) # setの場合 >>> d["hoge"].add(1) # setの場合はaddを使う >>> d["hoge"].add(1) >>> d["hoge"].add(2) >>> d["hoge"] {1, 2} # 重複しないことに注目 >>> d = defaultdict(dict) # dictの場合 >>> d["hoge"][1] = 1 >>> d["hoge"][2] = 2 >>> d["fuga"][-1] = -1 >>> d["fuga"][-2] = -2 >>> d defaultdict(<class 'dict'>, {'hoge': {1: 1, 2: 2}, 'fuga': {-2: -2, -1: -1}}) # よくわからないけど何でもできそう
参考:
8.3. collections — コンテナデータ型 — Python 3.6.5 ドキュメント
mutableなコレクション型を辞書の値にしておく、という発想があればそれほど難しい話ではない。
【python】# coding: utf-8はもうやめる
pythonのプログラムは先頭行(あるいはシェバンの次の二行目)でファイルの文字コードを指定することができます。エンコーディング宣言といいます。
こんなのとか
# coding: UTF-8
こういうのもありますね。これはemacsに自動認識させるための書式らしい*1。
# -*- coding: utf-8 -*-
これをずっと書いてたんだけど、PEP8を読んでいたらこんな記述に気づきました。
ASCII (Python 2) や UTF-8 (Python 3) を使用しているファイルにはエンコーディング宣言を入れるべきではありません。
えぇぇぇぇ!? と思ったんだけど、何回読み直しても「デフォルトエンコーディング使うならエンコーディング宣言は書くなよ!」と書いてあるようにしか読めない。
デフォルトエンコーディングを使うなら不要なのは知っていたけど、コーディング規約で非推奨にされてたのですね・・・。
ということで、「PEP8に準拠しろよ!」というのはpython使いの常識なので(本当か?)、そして私はpython3しか書かないので、個人的には今後コーディング宣言は使わないことにしました。シェバンも必要に迫られない限りは書かない人間なので、今後私のプログラムは一行目からimportで始まることに。・・・ちょっと寂しい気もするけど、すっきりはする。
「入れるべきではない」とまで言い切っているのは少し不思議な感じはするけど、不要なものをわざわざ書けというよりは良いのかもしれませんね。
*1:そのくせemacs使いの僕は面倒くさくて上ので済ませてきたんだけど
map・filterとリスト内包表記はどちらを使うべきか?
はじめに
pythonにはmap・filterという関数と、リスト内包表記という独自の記法があります。
どちらを使っても同じようなことができますが、どちらを使うべきなのでしょうか?
色々な視点から考えてみます。
目次
スポンサーリンク
返り値の型
map・filterにはmapオブジェクト、filterオブジェクトというジェネレータのようなものを返すという厄介な性質があります。ただしこれはpython3以降の仕様なので、python2を使っている方には当てはまりません(python3とpython2のソースコード互換性に効いてくるので、それはそれで難しい問題ではある)。
リスト内包表記でジェネレータの返り値を望むのならジェネレータ式を使うことができますが、map・filterでリストを返したければlist()を使ってリストに変換するしかありません。
>>> [x*2 for x in range(10)] [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] >>> list(map(lambda x:x*2, range(10))) [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
list()は厄介で、何しろ6文字も余計なものが増えてしまいます。ソースコードが冗長になり、list()が増えすぎると可読性も損ないます。
なお、star operatorを使って3文字で済ませる裏技もあります。これができるのは比較的新しいバージョンのpythonだけのはずですが・・・。
>>> [*map(lambda x:x*2, range(10))] [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
可読性は悪いです(というか初見殺し。そしてこの書き方は流行っていない)。
冗長さ
上述の通り、map・filterはリストに変換してやる必要があるので、基本的に冗長です。lambdaが必要になるとなおさら、という気がします。再掲しますが、
>>> [x*2 for x in range(10)] [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] >>> list(map(lambda x:x*2, range(10))) [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
リスト内包表記の方は24文字、mapの方は34文字(どちらもスペース込み)ですから、勝ち目はなさそうです。
しかし、「返り値はジェネレータのようなもので構わない(もしくは積極的にジェネレータを使いたい)」かつ「既存の関数を使い、lambdaを使わない」という条件だと、mapも輝き始めるようです。数字の文字列を一文字ずつintのリストに変換する例。
>>> (int(c) for c in "1234") <generator object <genexpr> at 0x7fefabac23b8> >>> map(int, "1234") <map object at 0x7fefaba4b0f0>
上は24文字、下は16文字です。内包表記の「for * in」がなくなるのと、呼び出しを省けるので短くなります。
filterも事情は同様です。よって、冗長さについては「ケースバイケース」であり、より詳しく言うと「返り値がジェネレータで構わず、使う関数がすでに定義されている」場合はmap・filterが有利と言い切れます。逆に言うと、それ以外のケースでは内包表記の方が簡潔に書けるようです。
たとえば、入力された数値n個をint型に変換する、といったコードでは、
a,b,c = map(int, input())
のように、極めて簡潔な記述がmapを使うことで可能になります。
可読性
可読性ははっきり言って、一長一短です。
一般的なプログラミング言語の構文をベースに考えると、わかりやすいのはmap・filterの方で、何しろ普通の関数です。初心者でも安心して使えると言えます(無名関数と高階関数の概念さえ理解すれば)。ただし、現実的には増えまくるlist()のせいでコードがごちゃごちゃし、かえって可読性が下がります。また、mapとfilterはそれぞれ独立に使う必要があるので、なおさらコードがごちゃごちゃします。
リスト内包表記は一見するとわかりづらいですが、慣れてしまえば簡潔で、書きやすく、読みやすいと言えます(本当か? 個人差は確実にあると思う)。また、一つの内包表記でfilterとmapを同時に適用できるので簡潔になります。
多重の複雑なものになると、そもそも簡潔に書け、改行とインデントでかなりわかりやすく整理できる内包表記の方が相対的にかなり有利と感じています。
from pprint import pprint pprint([[x*y for y in range(1,10)] for x in range(1,10)]) pprint(list(map( lambda x:list(map( lambda y:x*y, range(1,10))), range(1,10))))
かけ算九九の表を表示するプログラムですが、mapの方ははっきり言ってひでーと思います。この程度ならまだ読めなくはありませんが、条件が加わってfilterを追加するとかやり始めるともはや読めなくなります。
事故
これはmap・filterの欠点というよりはジェネレータの欠点ですが、割と事故のもとになってくれます。
>>> result = map(lambda x:x*2, range(10)) >>> result[2] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'map' object is not subscriptable >>> for x in result: ... print(x) ... 0 2 4 6 8 10 12 14 16 18 >>> for x in result: ... print(x) ... >>> # 何も表示されない
「んなもん、ちゃんと理解して使えば良いだろ」という意見も当然あると思います。それはそれで正しいと思いますが、理想論です。私は自分自身を含めて人間を100%信頼はしていません。事故る要素があれば、必ず事故るのです。
そして、これが嫌という理由で私は普段、ほとんどリスト内包表記を使っています。*1
まとめ
基本的には内包表記がずいぶん有利です。特別な理由がなければ内包表記で書けば良いと思います。
map・filterはリストに既存の関数を適用するジェネレータを作るときに威力を発揮する可能性があります。あるいは、代入でunpackするときとか。
というか、それ以外なさそうです。無名関数と組み合わせてまで使う必然性はないと思います。
*1:余談ですが、私はpythonのジェネレータというものはあまり好きではありません。mapやfilterやzip, rangeなどの返り値は、基本的にすべてlistでも別に良いと思います。これらの関数にはgeneratorオプションを追加してdefault=Falseとするか、python2よろしく対応するxrangeなどを作る、あるいはジェネレータを示す糖衣構文を新しく作って、その糖衣構文で囲むと値の評価時にジェネレータとして処理されるような仕組みを導入すれば良いと思います。これらについては、python4に期待です。