静かなる名辞

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

百発百中の砲一門と百発一中の砲百門について計算してみる

はじめに

 「百発百中の砲一門は百発一中の砲百門に勝る」という東郷平八郎のものとされる発言があります。

 発言の真偽や是非、ましてや東郷や旧日本海軍について何か述べようという訳ではありません。確率論的にどんな感じになるのかを考えてみようという試みです。

 参考リンク
AXION's Physical Laboratory -Analysis(No.001) contents page-

理論的な解析

  • 問題設定

 百発百中の砲一門と百発一中の砲百門が向かい合い、どちらかが全滅するまで同じ発射速度で撃ち合います。

私が一門の側の砲手なら即逃げる
私が一門の側の砲手なら即逃げる

 百発百中の砲一門の側が圧倒的不利であることの直感的な説明は、以下の通りです。

 簡単のためにターン制のゲームとして捉えます。1ターン目で、百発百中の砲は相手の砲を1減らします。一方、百発一中の砲百門の側は期待値は1門撃破。つまり運が悪ければ百発百中の砲は一ターン目で死にます。

 運良く一ターン目をしのいだとしましょう。百発一中の砲は99門残存しています。百発一中の砲百門の側の期待値はほとんど下がりません。……ということを100ターン続けないと勝てません。

 確率で考えてみましょう。百発一中の砲100門は1ターンに1門ずつ減らされていきます。ということは、(いつまでも百発一中側が命中を出さなかったとして)各ターンの発射数は100発,99発,98発,...,1発と減っていきます。要するに最大で5050回の砲撃が可能なのですが、これがすべて外れる確率を計算すれば百発百中の砲一門の勝率が出ます。……ということで計算すると、 0.99^{5050} \fallingdotseq 9.07 \times 10^{-23}で、ほとんど勝ち目はありません。

シミュレーション

 シミュレーションやってみるか・・・と思ったのですが、なにせ 10^{-23}なので 10^{23}回くらい回さないと百発百中の砲一門が勝つ場面が見れません。仕方がないので妥協して 10^5回回してみました。

import numpy as np
import matplotlib.pyplot as plt

def sim():
    team_A = 1
    team_B = 100
    
    while team_A > 0 and team_B > 0:
        hit_B = np.random.binomial(n=team_B, p=0.01)
        team_B -= 1
        if hit_B > 0:
            team_A -= 1
            break
    return team_A, team_B

def main():
    result = []
    for _ in range(10**6):
        result.append(sim())
    print(min(result))
    print(np.mean(result, axis=0))
    plt.hist([x[1] for x in result])
    plt.yscale("log")
    plt.savefig("result1.png")

if __name__ == "__main__":
    main()

 百発百中の砲一門は敵を84門まで削ったのが最高記録でした。試合終了時の百発一中サイドの門数は98.4くらいが平均のようです。何かちゃんと計算すればこの数字は出るのでしょう(たぶん)。

終了時の百発一中側残存数の分布
終了時の百発一中側残存数の分布

 対数目盛注意。まあ、無理ゲーだよね……。

百発百中の砲を何門持ってくれば勝てるのか

 ここまではやる前からわかっていた話です。なんとか百発百中の砲が勝つのを見てみたいので、その条件を探ります。

 たとえば百発百中の砲2門vs百発一中の砲100門ではどうでしょう。百発百中の砲3門vs百発一中の砲100門では? 百発百中の砲2門vs百発一中の砲200門とかもあるかもしれません。

 たぶん、頑張れば計算できるのだと思います。が、この場合、双方の砲の数が変化していく過程を計算しないといけないので、ちょっと面倒くさいという問題があります。だるいのでシミュレーションに丸投げします。

 シミュレーションのコードを書く前に考えるべきこととしては、百発一中の砲は撃ち方を選べるということです。敵の砲が2門あったとして、50門ずつそれぞれに振り向けるか、100門を片方に指向するか。どちらが良いでしょうか?

 現実の戦争だと相手の混乱・妨害を狙って分火した方が良いという話もあるそうですが、ここでは純粋に確率論的に考えます。命中率1%を100発撃つ期待値は当然1です。ということは、期待値が出て1目標しか撃破できない訳です。

 もし期待値が2とか3なら、本来は2門とか3門撃破できる火力を1門潰すのにつぎ込む訳で、どう考えても損です。2分割、3分割して撃った方が良いでしょう。

 では百発一中の砲が50門に減ってしまったら? これは極論すればどう撃っても同じではないでしょうか。

 今回の条件では期待値が1を超えることはないので、百発一中側は毎回全砲門を1門に指向することにします。こうするとシミュレーションが単純になり、お得です。命中が1以上出れば1門撃破です。

from collections import Counter
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def sim(A_n, B_n):
    while A_n > 0 and B_n > 0:
        hit_B = np.random.binomial(n=B_n, p=0.01)
        B_n -= A_n
        if hit_B > 0:
            A_n -= 1
    if B_n < 0:
        B_n = 0
    return A_n, B_n

def winner(AB):
    if AB[0] == AB[1]:
        return "-"
    elif AB[0] > AB[1]:
        return "A"
    else:
        return "B"

def main():
    n_AB = [(1, 100), (2, 100), (3, 100), (4, 100), (5, 100), 
            (6, 100), (7, 100), (8, 100), (9, 100), (10, 100),
            (11, 100), (12, 100), (13, 100), (14, 100), (15, 100), 
            (16, 100), (17, 100), (18, 100), (19, 100), (20, 100)]

    lst = []
    for A_n, B_n in n_AB:
        result = []
        for _ in range(10**4):
            result.append(sim(A_n, B_n))
        counter = Counter([winner(x) for x in result])
        lst.append([counter[x] for x in ["A", "B", "-"]])
    df = pd.DataFrame(lst, columns=["百発百中", "百発一中", "引き分け"], index=list(zip(*n_AB))[0])/10**4
    ax = df.plot()
    ax.set_xticks(df.index)
    ax.set_xlabel("百発百中の砲の門数(百発一中の砲は100門で固定)")
    ax.set_ylabel("勝率")
    plt.savefig("result2.png")

if __name__ == "__main__":
    main()

シミュレーション結果
シミュレーション結果

 この結果からわかることは、百発百中の砲9門は百発一中の砲100門に勝てるということのようです。直感的にもなんとなくそんな気はします(うまく説明できないが、互いに敵を1割くらいずつ削れる数字がそのあたりにありそう)。

結論

 百発百中の砲で百発一中の砲100門に勝とうとすると9門要る。

【python】sklearnのRidgeとLassoを使ってみる

はじめに

 Rdige、Lassoといえば割と定番の正則化アルゴリズムです。

 特にLassoはスパースな解を得てくれるという触れ込みです。なんだかカッコいいので、昔から触ってみたいと思っていました。

実験

 このような関数fを考えます。

def f(x):
    return -3*x + 5*(x**2) + 6*(x**3) - (x**4)  + 5

 (pythonの関数として記述していますが、数学的な関数です)

 これを回帰してみましょう。

  • ただの線形回帰
  • Ridge
  • Lasso

 で実験します。関数を外挿して、どこまでまともな結果が得られるかそれぞれで確認します。

 回帰に使うモデルは10次多項式回帰です。単純にやったらぐねぐねになってまともな結果は得られないんですが、さてどうなるか。

 なお、各モデルのクラスはsklearn.linear_model以下にあります。また、多項式回帰はsklearn.preprocessing.PolynomialFeaturesとsklearn.pipeline.Pipelineを組み合わせて行えます(他の方法もあるかもだけど)。

 参考:

hayataka2049.hatenablog.jp



 以上のことを踏まえて、以下のようなプログラムを書きました。長く見えますが、同じような処理の繰り返しが多いだけで(要するにちゃんとループ等にしていないだけで)、処理としては単純に「データを作る」「回帰する」「予測する」「プロットする」という流れをやっているだけです。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline

def main():
    def f(x):
        return -3*x + 5*(x**2) + 6*(x**3) - (x**4)  + 5

    x = np.linspace(-10, 10, 50)
    y_true = f(x)

    x_test = np.linspace(-13, 13, 100)
    y_test = f(x_test)
    
    seed = np.random.RandomState(seed=0)
    y_samples = y_true + seed.randn(*x.shape)*500

    # least squares model
    poly = PolynomialFeatures(degree=10)
    lin = LinearRegression()
    lin_pl = Pipeline([("poly_10", poly), ("lin", lin)])
    lin_pl.fit(x.reshape(-1, 1), y_samples)
    y_lin = lin_pl.predict(x_test.reshape(-1, 1))
    
    # ridge model
    poly = PolynomialFeatures(degree=10)
    ridge = Ridge(alpha=10, max_iter=2000)
    ridge_pl = Pipeline([("poly_10", poly), ("ridge", ridge)])
    ridge_pl.fit(x.reshape(-1, 1), y_samples)
    y_ridge = ridge_pl.predict(x_test.reshape(-1, 1))

    # lasso model
    poly = PolynomialFeatures(degree=10)
    lasso = Lasso(alpha=10, max_iter=5000)
    lasso_pl = Pipeline([("poly_10", poly), ("lasso", lasso)])
    lasso_pl.fit(x.reshape(-1, 1), y_samples)
    y_lasso = lasso_pl.predict(x_test.reshape(-1, 1))

    print("最小二乗法")
    print(lin_pl.named_steps.lin.intercept_)
    print(lin_pl.named_steps.lin.coef_)

    print("Ridge")
    print(ridge_pl.named_steps.ridge.intercept_)
    print(ridge_pl.named_steps.ridge.coef_)

    print("Lasso")
    print(lasso_pl.named_steps.lasso.intercept_)
    print(lasso_pl.named_steps.lasso.coef_)

    plt.plot(x_test, y_test, label="本当の値")
    plt.plot(x_test, y_lin, label="予測値(最小二乗法)", linestyle="--")
    plt.plot(x_test, y_ridge, label="予測値(Ridge)", linestyle="--")
    plt.plot(x_test, y_lasso, label="予測値(Lasso)", linestyle="--")

    plt.scatter(x, y_samples, label="サンプル")
    plt.legend()
    plt.savefig("result.png")

if __name__ == "__main__":
    main()

結果

 とりあえず、切片と係数はこんな感じになります。

最小二乗法
44.889422533642346
[ 0.00000000e+00  1.04897038e+02  7.73241212e+00 -8.56765602e+00
 -9.18049844e-01  4.80313010e-01 -1.00932865e-02 -6.16744609e-03
  1.90299005e-04  2.67071826e-05 -9.88817760e-07]
Ridge
47.06589279466334
[ 0.00000000e+00  7.07372729e+01  6.90235252e+00 -5.10402716e+00
 -8.60138166e-01  3.71899115e-01 -1.15878477e-02 -4.83914018e-03
  2.06482860e-04  2.11304610e-05 -1.05109554e-06]
Lasso
45.1551327659522
[ 0.00000000e+00  7.31967685e+00  1.08654718e+01  3.66382021e+00
 -1.30143108e+00  4.95092775e-02  2.99149652e-03 -4.74590934e-04
  2.19344863e-05  1.51480557e-06 -2.59682534e-07]

 どれも微妙かも・・・ノイズをまぶしすぎたのと、データ量が少ないので(要するに条件を人為的に悪くしたので*1)こんな感じです。

 本来必要ない高次の係数を見ると、比較的Lassoが健闘しているかな?

 肝心のグラフは、

回帰結果のグラフ
回帰結果のグラフ

 こんな感じで、相対的にLassoがマシに見えます。

 試しに、RidgeとLassoの正則化係数alphaを100倍の1000にしてみた結果が以下のものです(どの程度のスケールにすれば良いのかよくわからないので適当にやっている)。

最小二乗法
44.889422533642346
[ 0.00000000e+00  1.04897038e+02  7.73241212e+00 -8.56765602e+00
 -9.18049844e-01  4.80313010e-01 -1.00932865e-02 -6.16744609e-03
  1.90299005e-04  2.67071826e-05 -9.88817760e-07]
Ridge
63.546256794998726
[ 0.00000000e+00  2.30805586e+00  5.71618427e-01  1.66536218e+00
 -4.16707117e-01  1.62932068e-01 -2.30602486e-02 -2.30145211e-03
  3.30923961e-04  1.05415628e-05 -1.53055663e-06]
Lasso
89.0329535932658
[ 0.00000000e+00  0.00000000e+00  0.00000000e+00  2.79840777e+00
 -8.39251562e-01  9.44222102e-02 -3.28942545e-03 -1.10602103e-03
  4.33327616e-05  4.27414852e-06 -2.01546485e-07]

結果のグラフ2
結果のグラフ2

 それなりに改善しているように見えます。

まとめ

 これですべてを解決してくれるかというと微妙な感じですが、それなりに有効ではあるみたいです。

*1:そうしないとどれも似通った結果になる

【python】線形な分類器の比較

はじめに

 線形な分類器は癒やし

 やれ、RBFカーネルだ、決定木だ、ニューラルネットだ、深層学習だ、と流行り物に乗っかって、言うことを聞かない非線形な分類器をなんとかねじ伏せている綿牛たちは、きっと心が荒んでいるのでしょう。

 そんな私たちに、線形分類器は癒やしを提供してくれます。

 といってもいろいろありますよね。そこで比較してみることにしました。

実験

 sklearnの線形モデルは本当にたくさんあってどれを選べば良いのかわからないのですが(参考:API Reference — scikit-learn 0.19.2 documentation)、3つに絞りました。

  • 線形判別分析
  • ロジスティック回帰
  • 線形SVM

 「あっ、教科書に載ってる奴だ」というモデルたちですね。

 二次元で適当なデータを分類させて、決定関数を見ます。

 プログラムを以下に示します。以下のページを大いに参考にしました。

Classifier comparison — scikit-learn 0.19.2 documentation

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.linear_model import LogisticRegression 
from sklearn.svm import LinearSVC
from sklearn.metrics import accuracy_score
from sklearn.datasets import make_blobs, make_moons

def main():
    moons_X, moons_y = make_moons(noise=0.2, random_state=0)
    blobs_X, blobs_y = make_blobs(centers=2, random_state=0)
    blobs2_X, blobs2_y = make_blobs(centers=3, random_state=1)
    blobs2_y[blobs2_y == 2] = 1

    lda = LinearDiscriminantAnalysis()
    logistic = LogisticRegression()
    svm = LinearSVC()

    fig, axes = plt.subplots(nrows=3, ncols=3)
    plt.subplots_adjust(hspace=0.5)
    cm = plt.cm.RdBu
    cm_bright = ListedColormap(['#FF0000', '#0000FF'])
    for i, (X, y) in enumerate([(moons_X, moons_y),
                                (blobs_X, blobs_y),
                                (blobs2_X, blobs2_y)]):

        x_min, x_max = X[:, 0].min()-0.5, X[:, 0].max()+0.5
        y_min, y_max = X[:, 1].min()-0.5, X[:, 1].max()+0.5

        xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
                             np.arange(y_min, y_max, 0.1))

        for j, (cname, clf) in enumerate([("LDA", lda),
                                          ("LogisticRegression", logistic), 
                                          ("LinearSVM", svm)]):
            clf.fit(X, y)
            if hasattr(clf, "decision_function"):
                Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()])
            else:
                Z = clf.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]
            Z = Z.reshape(xx.shape)
            axes[i,j].contourf(xx, yy, Z, 20, cmap=cm, alpha=.8)
            axes[i,j].scatter(X[:,0], X[:,1], c=y, s=20,
                              cmap=cm_bright, edgecolors="black")
            
            axes[i,j].set_title(cname)
            
            preds = clf.predict(X)
            axes[i,j].text(xx.max() - .3, yy.min() + .1,
                           "{:.3f}".format(accuracy_score(y, preds)),
                           size=10, horizontalalignment='right')

    plt.savefig("result.png")

if __name__ == "__main__":
    main()

結果

 行がデータ、列が分類機に対応。

 色の濃淡が決定関数の値か出力値のラベルの確率に対応。右下の値は学習に使ったデータでテストして計算した正解率です*1

実験結果のグラフ
実験結果のグラフ

 基本的に変わり映えがしない。そこがいい。

 と言っていては何の考察にもならないので、ざっくりと説明。

 えーっと、まず正解率の大小に大した意味はありません。誤差です。

 最上段の三日月状のデータはもともと線形分離できないようなデータです。どれも似通った決定関数になっています。

 中段は正規分布2つが微妙に重なっているようなデータで、ロジスティック回帰だけ微妙に傾き方が大きい気がします。正解率も(大した意味はないとはいえ)微妙に下がっている。何を思ってこうしたのか。

 最下段は正規分布3つを1つ+2つで2つのクラスに分けたデータです。どれも正解率100%になっていますが、LDAはデータ全体の傾向からそれっぽい決定関数を導いているのに対し、ロジスティック回帰とSVMはそういう配慮はないようです。線形判別分析がよくてロジスティック回帰やSVMが駄目という話ではないのですが、こういうデータに対しては単純な判別分析の方がそれっぽく動くということでしょうか。

まとめ

 この中からどれか選ぶとしたら? という建設的な話を期待した方には申し訳ありませんが、やっぱり「どれを選んでもそんなに変わらない」という結論になる気がします。

 強いて言えば、わかりやすい判別分析か、とりあえずSVM的な線形SVMの二択? ロジスティック回帰は非等分散、非正規分布のデータに対する有効性や、確率モデルとして取り扱えることにどこまで価値を見出すかによって使うか使わないかが決まる気がします。実際的には評価指標の比較をやっても良いと思いますが、正直そこまで劇的な差は出てこないと思う。

*1:交差検証とかはやっていません。この記事は癒やしが目的なので

【python】ImportError: No module named '***'の対処法

はじめに

 ライブラリをインストールして、いざ使おうと思ったら「ImportError: No module named '***'」が出ちゃった、という経験をされる方は多いと思います。

 その対処法、トラブルシューティング手順についてまとめておきます。

 なお、この記事はpipでインストールした場合について説明します。

  • setuptools
  • easy_install
  • conda

 などを用いた場合(あるいは自作モジュールの場合)、この記事の内容をそのまま使うことはできません(ある程度は参考になると思います)。

 目次

凡ミスに注意

 色々な凡ミスの可能性が考えられます。

  • ライブラリの名前を間違えていた(スペルミス。numpyをnampyにしてしまうなど)
  • 仮想環境をactivateしていなかった
  • (自作モジュールの場合)相対パスを間違えた
  • その他……

 思いつく範囲でチェックします。

 思いつかない場合は、

 あたりが主な原因の候補です。

まずはインストール作業を見直す

 最低限把握しておくべきこととして、

  • 打ったコマンド
  • インストール時にコマンドを打った結果出たメッセージ

 の2つがあります。これがわからないと困ります。

 この2つを問題なく把握できていれば、以下の手順でトラブルシュートしていきます。

ちゃんとインストールできているか確認する

 インストール作業をチェックします。

 まず、インストール時のメッセージを確認します。

 メッセージの最後の方に

Successfully installed ***

 というようなメッセージが出ていれば、とりあえずインストールは完走したということになります。「Error」とか「Failed」といった単語が最後の方で出ているのなら、インストールに失敗していますから、ライブラリ名とエラーメッセージの組み合わせで検索したりして対策を模索することになります。

 なお、

You are using pip version 9.0.3, however version 18.0 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

 のようなメッセージが出ることがありますが(バージョンの数字は環境によって異なります)、これはインストールの成否とは関係ありません

 「古いバージョンのpipを使っているみたいだけど、アップデートできますよ」というそれだけのメッセージですね。pipで色々な操作をするたびに出てきます。

 インストールが完走していることがわかったら、「pip show ***」でライブラリの情報を確認します。以下に例を示します。

$ pip show numpy
Name: numpy
Version: 1.14.1
Summary: NumPy: array processing for numbers, strings, records, and objects.
Home-page: http://www.numpy.org
Author: NumPy Developers
Author-email: numpy-discussion@python.org
License: BSD
Location: インストールされているディレクトリ
Requires: 

 インストールされているディレクトリを控えておきます。

 なお、この作業を行う際に重要なのは、インストール時に打ったpipコマンドと同じpipを使うことです。

  • pip
  • sudo pip
  • pip2/pip3

 色々あると思いますが、とにかくインストール時に「pip install ***」を打ったなら「pip show ***」としますし、「sudo pip install ***」だったのなら「sudo pip show ***」としてください。仮想環境の有無やアクティベートしていたかどうかなども揃えてください。

pythonを起動して確認する

 pythonの対話的インタプリタを起動し、以下のプログラムを実行します。あるいは、以下のプログラムを書いた.pyファイルを作成し、それをインタプリタから実行します。

import sys
print(sys.version)
print(sys.prefix)
print(sys.path)

 pythonのバージョン、インストール先、import時に参照されるパスなどの情報が得られます。

 また、コマンドライン(shellやコマンドプロンプト)から以下のコマンドを打って得られる情報も有用です。

# unix系の場合
which python
# windowsの場合
where python

 こちらも普段使っているpythonインタプリタで行います。pythonコマンドを用いているならpythonコマンドを、python3コマンドを用いているならpython3コマンドで実施してください。jupyter notebookを使っている人なら、jupyter上で行ってください。

 ここで、上で確認したライブラリのインストール先と、これらの情報が一致しているかを確認します。見方としては、

  • 普通、ライブラリはsys.prefix以下のディレクトリに存在します(たとえばsys.prefixのディレクトリ/lib/python3.5/site-packages)など
  • sys.path内にライブラリがインストールされているディレクトリが存在しているはずです

 の2点に気をつけます。

 これが変なことになっているケースが相当多いと思います。

インストール先が変だったら

 使いたいpythonインタプリタと紐付いたpipを使ってインストールし直してください。確実な方法としては、

python -m pip install ***

 があります(pythonコマンドで使いたいpythonが起動することが大前提。python3コマンドを使っているならpython3コマンドを使う必要がありますし、仮想環境を使っているならactivateした状態で行ってください)。

 とりあえずこれでインストールできると思いますが、うまく行ったら「python -m pip」に該当するpipコマンドを探しておくと今後はかどります*1

なぜこの対処で良いの?

 「pip installは成功したのにimportできない」という症状の95%くらいは、pipでインストールした先のpythonとimportを実行したpythonが異なっていることに起因しているからです。

 そもそもpipは特定のpythonインタプリタに紐付いているモジュールです。複数のpythonがインストールされているような環境では、使いたいpythonに紐付いたpipを適切に使う必要があります。

 たとえばpython2がデフォルトの環境にpython3をインストールしてpython3コマンドで使っているというような場合、pipコマンドで立ち上がるのはシステムデフォルトのpython2です。pipコマンドがパスの読み込みの優先順位で先に位置しているからです。

 こういう場合、pip3やpip3.6などのコマンドが用意されていてパスで読み込めれば、それらのコマンドを用いてインストールすることができます。

 また、仮想環境を立ち上げた状態で「sudo pip ~」と打ち込んでしまうパターンもあります。大抵の仮想環境はパスをいじって仮想環境としての動作を実現していますが、sudoを使った場合は一般ユーザーとは異なった環境変数で実行されるため、仮想環境に対してのインストールがなされません。仮想環境は管理者権限がなくても書き込み/変更できる場所に作るのが一般的なので、「pip ~」でインストールしてください。

まとめ

 pip周りは意外と慣れないうちはハマるので、仕組みを理解してハマらないようにしましょう。「ネットに書いてあるインストール手順をそのままコピペしています」という人は要注意です。

*1:とはいえ混乱の元なので、いっそ公式で「python -m pip」を推奨してくれないものか。「pyvenv」みたいにさ

【python】pandasのgroupbyで結果をlistにする

 pandasのgroupbyを使って、平均や標準偏差を計算する方法は検索するとすぐ出てきます。

 ただ、「そういうの良いから、項目ごとに使いやすいイテレータにしてまとめてくれよ!」と思うときがありますよね。

>>> import pandas as pd
>>> df = pd.DataFrame({"A":[c for _ in range(5) for c in ["a", "b"]], "B":range(10)})
>>> df
   A  B
0  a  0
1  b  1
2  a  2
3  b  3
4  a  4
5  b  5
6  a  6
7  b  7
8  a  8
9  b  9
>>> df.groupby("A")
<pandas.core.groupby.DataFrameGroupBy object at 0x7fd0742ec4e0>  # よくわからない

 とりあえず愚直にlistにしてみます。

>>> list(df.groupby("A"))
[('a',    A  B
0  a  0
2  a  2
4  a  4
6  a  6
8  a  8), ('b',    A  B
1  b  1
3  b  3
5  b  5
7  b  7
9  b  9)]

 それぞれがデータフレームか。悪くはないけど、グルーピング対象の項目は要らないかな。というか、特定の列だけあれば良いシチュエーションの方が多いでしょう。

>>> list(df.groupby("A")["B"])
[('a', 0    0
2    2
4    4
6    6
8    8
Name: B, dtype: int64), ('b', 1    1
3    3
5    5
7    7
9    9
Name: B, dtype: int64)]

 けっこうそれらしくなってきた? 辞書にしてみましょう。

>>> dict(list(df.groupby("A")["B"]))["a"]
{'a': 0    0
2    2
4    4
6    6
8    8
Name: B, dtype: int64, 'b': 1    1
3    3
5    5
7    7
9    9
Name: B, dtype: int64}
>>> dict(list(df.groupby("A")["B"]))["a"]
0    0
2    2
4    4
6    6
8    8
Name: B, dtype: int64

 Seriesはそんなに好きになれないので、スマートなコードでリストにできないか……と思って検索したら、こんなのがありました。

python - grouping rows in list in pandas groupby - Stack Overflow

>>> df.groupby("A")["B"].apply(list)
A
a    [0, 2, 4, 6, 8]
b    [1, 3, 5, 7, 9]
Name: B, dtype: object


 なるほどねー。これで良いでしょう。df.groupby("A")["B"].apply(list)["a"]とかで取り出せるみたいだし。

【python】ネストされた辞書をflattenしてみる(一つの辞書にまとめる)

 こんな辞書を考える。

d = {1:"a",
     2:{"b":"hoge"},
     3:{"c":"fuga",
        "piyo":["foo", "bar"], 
        "buzz":{"d":"hogehoge"}}}

 次のような結果を得るにはどうすれば良いか。

{(1,): 'a',
 (2, 'b'): 'hoge',
 (3, 'c'): 'fuga',
 (3, 'piyo'): ['foo', 'bar'],
 (3, 'buzz', 'd'): 'hogehoge'}

 やってみよう。再帰で。

from pprint import pprint
d = {1:"a",
     2:{"b":"hoge"},
     3:{"c":"fuga",
        "piyo":["foo", "bar"], 
        "buzz":{"d":"hogehoge"}}}

def flatten_dict(d, pre_lst=None, result=None):
    if result is None:
        result = {}
    if pre_lst is None:
        pre_lst = []
    for k,v in d.items():
        if isinstance(v, dict):
            flatten_dict(v, pre_lst=pre_lst+[k], result=result)
        else:
            result[tuple(pre_lst+[k])] = v
    return result

pprint(flatten_dict(d))
""" =>
{(1,): 'a',
 (2, 'b'): 'hoge',
 (3, 'buzz', 'd'): 'hogehoge',
 (3, 'c'): 'fuga',
 (3, 'piyo'): ['foo', 'bar']}
"""

 できた。途中のキーをリストにしてたどりながら末端まで再帰でたどり、子が辞書でなくなったときにキーのリストをtupleにしてキーとして結果の辞書に入れる。

 ただ、リストオブジェクトを毎回作り直しているのがダサい。これはスタック風に使えば一つのオブジェクトで行けるはずなので、そうしてみよう。

from pprint import pprint
d = {1:"a",
     2:{"b":"hoge"},
     3:{"c":"fuga",
        "piyo":["foo", "bar"], 
        "buzz":{"d":"hogehoge"}}}

def flatten_dict(d, pre_lst=[], result=None):
    if result is None:
        result = {}
    for k,v in d.items():
        pre_lst.append(k)
        if isinstance(v, dict):
            flatten_dict(v, pre_lst=pre_lst, result=result)
        else:
            result[tuple(pre_lst)] = v
        pre_lst.pop(-1)
    return result

pprint(flatten_dict(d))
""" =>
{(1,): 'a',
 (2, 'b'): 'hoge',
 (3, 'buzz', 'd'): 'hogehoge',
 (3, 'c'): 'fuga',
 (3, 'piyo'): ['foo', 'bar']}
"""

 たぶんちゃんと動いている。だからどうしたって話なのだが。

 頭の体操としては面白かった。実用的な用途は? あまりないと思う。

【python】sysを使ってpythonインタプリタについて調べる

はじめに

 実行しているpythonインタプリタはどこにあるの? という疑問が生まれたときは、標準モジュールのsysを使って調べることができます。

 見るものの選択肢は幾つかあります。なお、この記事の内容はすべて公式ドキュメントに基づきます。

29.1. sys — システムパラメータと関数 — Python 3.6.5 ドキュメント

見るべきもの

sys.executable

 インタプリタ本体のパス。linuxでいう「which python」(windowsではwhere)と同じだと思います。

sys.path

 pythonのモジュール読み込みに使われるパス。これを見ればインタプリタがどこにあるのかもだいたいわかります。

その他

 prefixというものがあります。関連するものとしては、

  • sys.base_exec_prefix
  • sys.base_prefix
  • sys.exec_prefix
  • sys.prefix

 たぶんこれですべてだと思います。これらには仮想環境(といってもvenvのことです)を反映するかどうかの違いがあります。

 venvのページによると、

仮想環境が有効な場合 (すなわち、仮想環境の Python インタープリタを実行しているとき)、 sys.prefix と sys.exec_prefix は仮想環境のベースディレクトリを示します。 代わりに sys.base_prefix と sys.base_exec_prefix が仮想環境を作るときに使った、仮想環境ではない環境の Python がインストールされている場所を示します。 仮想環境が無効の時は、 sys.prefix は sys.base_prefix と、 sys.exec_prefix は sys.base_exec_prefix と同じになります (全て仮想環境ではない環境の Python のインストール場所を示します)。

28.3. venv — 仮想環境の作成 — Python 3.6.5 ドキュメント

 ということらしいのですが、基本的にはvenvを反映するsys.prefixを見ておけば確実だと思います。

使い方

 次のようなワンライナーを実行して確認します。

import sys;print(sys.prefix)

 対話的インタプリタを立ち上げて実行しても良いですし、-cオプションで直接実行することもできます。

$ python -c "import sys;print(sys.prefix)"
$ # あるいは
$ python
>>> import sys;print(sys.prefix)