静かなる名辞

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


【python】np.matrixの速度を測る

 numpyで行列演算を行う方法としては、普通のnumpy配列に行列演算系の関数を適用していく方法と、あまり知られていないがnp.matrix型やnp.mat型を使う方法がある。

 速度が違ったりするのだろうか? 仮に違うと困る(というか場面によって適切な方を選ぶ必要が生じてくる)ので、こんなコードを書いてみた。

import timeit

import numpy as np 

def main():
    a = np.random.random((100, 100))
    b = np.random.random((100, 100))
    a_m = np.matrix(a)
    b_m = np.matrix(b)
    a_mt = np.mat(a)
    b_mt = np.mat(b)
   
    print("add")
    print(np.allclose(a+b, np.array(a_m+b_m), np.array(a_mt+b_mt)))
    print(timeit.timeit(lambda : a+b, number=100000))
    print(timeit.timeit(lambda : a_m+b_m, number=10000))    
    print(timeit.timeit(lambda : a_mt+b_mt, number=10000))    

    print("mul")
    print(np.allclose(a*b, 
                      np.array(np.multiply(a_m, b_m)),
                      np.array(np.multiply(a_mt, b_mt))))
    print(timeit.timeit(lambda : a+b, number=10000))
    print(timeit.timeit(lambda : np.multiply(a_m, b_m), number=10000))        
    print(timeit.timeit(lambda : np.multiply(a_mt, b_mt), number=10000))        
    
    print("dot")
    print(np.allclose(np.dot(a,b),
                      np.array(a_m*b_m),
                      np.array(a_mt*b_mt)))
    print(timeit.timeit(lambda : np.dot(a,b), number=10000))
    print(timeit.timeit(lambda : a_m*b_m, number=10000))
    print(timeit.timeit(lambda : a_mt*b_mt, number=10000))

if __name__ == "__main__":
    main()

 100*100の行列の加算、要素積、行列積の計算を、np.arrayを使う方法とnp.matrix、np.matを使う方法で書いて、速度を比較している(ついでに計算結果が同じになるかも確認しているけど、これは念のためにやっているだけで、無論Trueになる)。

 結果は以下の通り。

add
True
0.6425584320095368
0.1307114879891742
0.1383463980164379
mul
True
0.07879749499261379
0.13494228100171313
0.1337542689871043
dot
True
0.8895792970142793
0.9789612090098672
1.0360321149928495

 変わるのか(困惑)。一瞬計測誤差かと思って何回も回したけど、傾向は変わらず。add, mulはnp.arrayの方が速い。dotはほとんど速度差はないようだが。

 何かの間違いだろうと思って検索したら、こんなスタックオーバーフローの質問が出てきてしまった。

python - numpy np.array versus np.matrix (performance) - Stack Overflow

 np.arrayの方が速い、と言われてしまっている。こうなると認めるしかないようだ。

 こうなると、それなりに速度重視の場面ではnp.matrixは使えないですねぇ。かといってnp.arrayで行列演算は苦行(Cとかフォートランでforループ回して書くのに比べればずっとマシだろうけど)。

 嫌な現実を見せられてしまった感がある。

 2018年11月15日追記:
 そうこうしているうちに、numpy 1.15.0から「no longer recommended」になってしまいました。

numpy.matrix — NumPy v1.15 Manual

 配列型をいろいろな関数と組み合わせて行列演算してくれということなのでしょう。

【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()

meshgridを使って三次元プロット
meshgridを使って三次元プロット

 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])

 こうすると色々な関数に渡せそうで便利。結合については、こちらの記事を参照してください。

www.haya-programming.com

 たとえばxyzで(n_samples, 3)にしたければnp.stack([x, y, z], axis=1)などでよさそうです。

逆にxyz座標からmeshgridを生成する

 x, y, zの座標データが与えられていて、プロットするためにmeshgridに変換したいというシチュエーションもあります。

 この場合は元のデータが都合よくグリッド状になっていないことが多いので、補完という処理をした上で変換することが可能です。

 詳細はこちらの記事を御覧ください。

www.haya-programming.com

まとめ

 いろいろ動かしてみると、それなりにやっていることが単純なのがわかって、親近感が湧いてきました。とにかく三次元プロットでは必需品なので、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 で置き換えられます。

 説明の内容はこの記事と基本的に同じですが、グダグダなこの記事と比べると、さすがによくまとまっています。公式は偉い。

*1:細かいことを言うと、lst[0] = "hoge"など代入する場合、del dct[0]などdel文を呼ぶ場合などは__getitem__とは異なったメソッドが呼ばれます。あくまでも単純に値を参照する場合の話です

*2:sliceオブジェクトに変換されるので単純な処理ではないが

複数の目的変数で回帰を行う方法

はじめに

 回帰分析を行う際、複数の目的変数に対して回帰をしたい場合があります。普通のモデルではできないのでちょっと面食らいますが、やり方は色々あるようです。

 目次

スポンサーリンク


目的変数の数だけ回帰モデルを作る方法

 単純に考えると、一つの目的変数を出力する回帰モデルを目的変数の数だけ用意してやれば、所要を達しそうです。

 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種類の方法を説明しました。

  • 単純に目的変数の数だけ回帰モデルを用意する方法
  • 複数の目的変数を出力できるモデルを用いる方法

 どちらが良いかは一概には言えません。データや目的に応じて、あるいは実際に走らせてみて評価指標や計算コストを勘案して考える必要があります。複数の目的変数に最初から対応したモデルの方が良いような気もしますが、そうとも言えないんじゃという話もあったりします。

 でもまあ、色々な選択肢があることは良いことです。いろいろ勘案して選べば良いでしょう。適当ですがこんな感じでシメます。

*1:ということだと思う・・実装読んでいないので断言しかねます

*2:回帰の記事なのでそう呼ぶが、ぶっちゃけ妥当ではない。CCAの枠組みではどっちがどっちでも大して構わないのだし

【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))
          42018
 日   月   火   水   木   金   土
  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の平均と標準偏差を計算する方法

列の平均と標準偏差を計算したい

 とても簡単にできます。

>>> 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-ja 1.0 ドキュメント

 えぇぇぇぇ!? と思ったんだけど、何回読み直しても「デフォルトエンコーディング使うならエンコーディング宣言は書くなよ!」と書いてあるようにしか読めない。

 デフォルトエンコーディングを使うなら不要なのは知っていたけど、コーディング規約で非推奨にされてたのですね・・・。

 ということで、「PEP8に準拠しろよ!」というのはpython使いの常識なので(本当か?)、そして私はpython3しか書かないので、個人的には今後コーディング宣言は使わないことにしました。シェバンも必要に迫られない限りは書かない人間なので、今後私のプログラムは一行目からimportで始まることに。・・・ちょっと寂しい気もするけど、すっきりはする。

 「入れるべきではない」とまで言い切っているのは少し不思議な感じはするけど、不要なものをわざわざ書けというよりは良いのかもしれませんね。

*1:そのくせemacs使いの僕は面倒くさくて上ので済ませてきたんだけど