静かなる名辞

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

2019/03/22:TechAcademyがteratailの質問・回答を盗用していた件
2019/03/26:TechAcademy盗用事件 公式発表と深まる疑念



君はKNN(k nearest neighbor)の本当のすごさを知らない

はじめに

 KNNといえば機械学習入門書の最初の方に載っている、わかりやすいけど性能はいまいちな初心者向けの手法という認識の人も多いと思います。

 しかし、本当はけっこう優秀なのです。

2次元で予測させてみる

 予測させます。コードは軽く読み流して結果の図だけ見てください。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

from sklearn.datasets import make_moons, make_circles,\
    make_classification
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import BaggingClassifier
from sklearn.neighbors import KNeighborsClassifier

def main():
    knn3 = KNeighborsClassifier(n_neighbors=3)
    knn5 = KNeighborsClassifier(n_neighbors=5)
    knn7 = KNeighborsClassifier(n_neighbors=7)
    knnb = BaggingClassifier(base_estimator=knn5,
                             n_estimators=1000,
                             n_jobs=-1)
    rfc = RandomForestClassifier(n_estimators=1000)

    X, y = make_classification(
        n_features=2, n_redundant=0, n_informative=2,
        random_state=1, n_clusters_per_class=1)
    rng = np.random.RandomState(2)
    X += 2 * rng.uniform(size=X.shape)
    linearly_separable = (X, y)

    datasets = [make_moons(noise=0.3, random_state=0),
                make_circles(
                    noise=0.2, factor=0.5, random_state=1),
                linearly_separable]

    fig, axes = plt.subplots(
        nrows=3, ncols=5, figsize=(16, 9))
    plt.subplots_adjust(wspace=0.2, hspace=0.3)

    cm = plt.cm.RdBu
    cm_bright = ListedColormap(['#FF0000', '#0000FF'])
    for i, (X, y) in enumerate(datasets):
        x_min = X[:, 0].min()-0.5
        x_max = X[:, 0].max()+0.5
        y_min = X[:, 1].min()-0.5
        y_max = 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(
                [("KNN k=3", knn3), ("KNN k=5", knn5),
                 ("KNN k=7", knn7), ("Bagg-KNN", knnb),
                 ("RFC", rfc)]):
            clf.fit(X, y)
            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)
    plt.savefig("result.png", bbox_inches="tight")

if __name__ == "__main__":
    main()

result.png
result.png

 KNNの方が決定境界が綺麗ですね。まあ、sklearnの実装でpredict_probaすると1/k刻みの確率で出てくるので、そのままではちょっとですが。バギングするとめったに見られない感じの綺麗な確率が出てきます。

digitsを予測させてみる

 先に結論を書くと、ランダムフォレストに勝てます。

from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import BaggingClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import StratifiedKFold,\
    cross_validate

def main():
    digits = load_digits()

    knn3 = KNeighborsClassifier(n_neighbors=3)
    knn5 = KNeighborsClassifier(n_neighbors=5)
    knn7 = KNeighborsClassifier(n_neighbors=7)
    knnb = BaggingClassifier(base_estimator=knn3,
                             n_estimators=50,
                             n_jobs=-1)
    rfc = RandomForestClassifier(n_estimators=1000)
    skf = StratifiedKFold(n_splits=10, random_state=0)
    for name, clf in [("KNN k=3", knn3), ("KNN k=5", knn5),
                      ("KNN k=7", knn7), ("Bagg-KNN", knnb),
                      ("RFC", rfc)]:

        result = cross_validate(
            clf, digits.data, digits.target,
            cv=skf, scoring="f1_macro",
            return_train_score=False)

        tf = result["fit_time"].mean()
        ts = result["score_time"].mean()
        f1 = result["test_score"].mean()

        print(name)
        print("fit time:{:.3f} test time:{:.3f}".format(tf, ts))
        print("f1-macro:{:.3f}".format(f1))
        print()

if __name__ == "__main__":
    main()
KNN k=3
fit time:0.003 test time:0.039
f1-macro:0.978

KNN k=5
fit time:0.004 test time:0.038
f1-macro:0.974

KNN k=7
fit time:0.003 test time:0.040
f1-macro:0.970

Bagg-KNN
fit time:0.178 test time:1.467
f1-macro:0.979

RFC
fit time:2.624 test time:0.120
f1-macro:0.950

 ランダムフォレストは不甲斐ないなんてレベルじゃなく負けています。

まとめ

 ということで、KNNはけっこう優秀です。学習時間がほとんど必要ないのも魅力的です。

 ただ、ノイズが多いデータに対しては脆弱な面もあります。実際のデータではランダムフォレストに負けることの方が多い印象ですが、それでも頑張るときはがんばります。

 性能重視のときでも試しに使ってみる価値はあると言えるでしょう。

【python】高次元の分離超平面をなんとか2次元で見る

はじめに

 分類器の特性を把握するために2次元データで分離超平面を見るということが行われがちですが、高次元空間における分離器の特性を正確に表している訳ではありません。

 ということがずっと気になっていたので、なんとか高次元空間で分類させて2次元で見る方法を考えます。

方法

 PCAで2次元に落とせれば、線形変換で逆変換もできるので、それでやります。当然ながら情報は落ちますし、2次元でもなんとか見える程度のデータしか扱えませんが、妥協します。

 sklearnならinverse_transformという便利なメソッドがあるので、簡単です。

 というあたりまで考えた上で、こんなコードを書きました。

show_hyperplane.py

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

def show_hyperplane(dataset, clf, filename):
    pca = PCA(n_components=2)
    X = pca.fit_transform(dataset.data)
    plt.scatter(X[:,0], X[:,1], c=dataset.target)

    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.01),
                         np.arange(y_min, y_max, 0.01))

    clf.fit(dataset.data, dataset.target)
    Z = clf.predict(
        pca.inverse_transform(np.c_[xx.ravel(), yy.ravel()]))
    plt.pcolormesh(xx, yy, Z.reshape(xx.shape),
                   alpha=0.03, shading="gouraud")
    plt.savefig(filename)

 汎用的に作ったので、これでいろいろなものを見てみようという算段です。

実験

 まずirisとSVM。

from show_hyperplane import show_hyperplane
from sklearn.datasets import load_iris
from sklearn.svm import SVC

iris = load_iris()
svm = SVC(C=50, gamma="scale")    
show_hyperplane(iris, svm, "iris_svm.png")

iris_svm.png
iris_svm.png

 特に興味深い知見は得られませんでした。

 次、irisとランダムフォレスト。

from show_hyperplane import show_hyperplane
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier

iris = load_iris()
rfc = RandomForestClassifier(n_estimators=500, n_jobs=-1)    
show_hyperplane(iris, rfc, "iris_rf.png")

iris_rf.png
iris_rf.png

 ランダムフォレストで斜めの分離超平面の図を出したサイトはここくらいしかないのでは? だからどうしたって話ですが。

 簡単なのでAdaBoostも試します。

from show_hyperplane import show_hyperplane
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier

iris = load_iris()
ada = AdaBoostClassifier(
    base_estimator=DecisionTreeClassifier(max_depth=4),
    n_estimators=200)    
show_hyperplane(iris, ada, "iris_ada.png")

iris_ada.png
iris_ada.png

 面白いんですが、性能はいまいち悪そう。

 ちなみに、base_estimatorのパラメータでコロコロ結果が変わります。パラメータ設定については、以下の2記事を参照してください。

www.haya-programming.com
www.haya-programming.com

 ただの決定木もやっておきましょう。

from show_hyperplane import show_hyperplane
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier

iris = load_iris()
dtc = DecisionTreeClassifier()
show_hyperplane(iris, dtc, "iris_tree.png")

iris_tree.png
iris_tree.png

 つまらない。

 さて、irisは飽きてきたのでdigitsで同じことをやります。こちらは何しろ元が64次元で、2次元に落とすとかなり重なり合うので、カオスな結果になってくれそうです。

 が、その前にshow_hyperplane.pyをいじります。元のままだといろいろうまくいかなかったからです。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

def show_hyperplane(dataset, clf, filename):
    pca = PCA(n_components=2)
    X = pca.fit_transform(dataset.data)
    plt.scatter(X[:,0], X[:,1], s=5, c=dataset.target)

    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.3),
                         np.arange(y_min, y_max, 0.3))

    clf.fit(dataset.data, dataset.target)
    Z = clf.predict(
        pca.inverse_transform(np.c_[xx.ravel(), yy.ravel()]))
    plt.pcolormesh(xx, yy, Z.reshape(xx.shape),
                   alpha=0.05, shading="gouraud")
    plt.savefig(filename)

 よし、やろう。

 まずSVM。今回からついでに学習データに対するスコアを見ます。コメントで記します。

from show_hyperplane import show_hyperplane
from sklearn.datasets import load_digits
from sklearn.svm import SVC

digits = load_digits()
svm = SVC(C=0.1, gamma="scale")    
score = svm.fit(
    digits.data, digits.target).score(
        digits.data, digits.target)
print(score) # => 0.9744017807456873
show_hyperplane(digits, svm, "digits_svm.png")

digits_svm.png
digits_svm.png

 あたりまえですが、64→2次元で情報落ちしているので、こんなふうにしか見えません。それでも、後々出てくるやつに比べればまともな方です。

 次。ランダムフォレスト。

from show_hyperplane import show_hyperplane
from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier

digits = load_digits()
rfc = RandomForestClassifier(n_estimators=500, n_jobs=-1)    
score = rfc.fit(
    digits.data, digits.target).score(
        digits.data, digits.target)
print(score) # => 1.0
show_hyperplane(digits, rfc, "digits_rfc.png")

digits_rfc.png
digits_rfc.png

 これ面白いですね。ところどころ凹凸がありますが、それでもぱっと見SVMと同じくらい滑らかな分離超平面に見えます。高次元データほど強いというのもわかる気がします。

 アダブースト。

from show_hyperplane import show_hyperplane
from sklearn.datasets import load_digits
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier

digits = load_digits()
ada = AdaBoostClassifier(
    base_estimator=DecisionTreeClassifier(max_depth=3),
    n_estimators=200)    
score = ada.fit(
    digits.data, digits.target).score(
        digits.data, digits.target)
print(score) # 0.9660545353366722
show_hyperplane(digits, ada, "digits_ada.png")

digits_ada.png
digits_ada.png

 大丈夫なんかこれ。決定木のアダブーストはランダムフォレストと比べて個人的にいまいち信頼していないのですが、こういうの見るとその思いが強まります。

 決定木もやりましょう。irisではつまらなかったけど、こちらではどうなるでしょうか。
 

from show_hyperplane import show_hyperplane
from sklearn.datasets import load_digits
from sklearn.tree import DecisionTreeClassifier

digits = load_digits()
dtc = DecisionTreeClassifier()
score = dtc.fit(
    digits.data, digits.target).score(
        digits.data, digits.target)
print(score) # 1.0
show_hyperplane(digits, dtc, "digits_tree.png")

digits_tree.png
digits_tree.png

 あははー、なにこれカオス。デフォルトのまま高次元で使うな、ということですね。

まとめ

 元が64次元くらいでもだいぶ情報落ちするので、本当の高次元データでも使えるかというと微妙なのですが、それでもなんとなく傾向はつかめますし、面白かったです。

 SVMとランダムフォレストはどっちも優秀ですね。

matplotlibのpcolormeshでalphaを小さくすると網目が出てくる対策

概要

 デフォルト設定だとタイトルに書いた通りの現象が起こります。網目模様が出て図が汚くなります。

実験

 こんな単純なコード。

import numpy as np
import matplotlib.pyplot as plt

def main():
    xx, yy = np.meshgrid(np.arange(0, 10, 0.1),
                         np.arange(0, 10, 0.1))
    zz = np.zeros(xx.shape)
    zz[xx < 5] = 1
    plt.pcolormesh(xx, yy, zz, alpha=0.03)
    plt.savefig("result.png")

if __name__ == "__main__":
    main()

結果1
結果1

 こんな感じで汚くなります。ちょっと人には見せられない図です。

対策

 これはシェーディングに問題があります。shading="gouraud"を指定するとよくなります。

matplotlib.axes.Axes.pcolormesh — Matplotlib 3.1.0 documentation

 また、それでも不満が残る場合はメッシュを細くすると相対的に解消されます。

 この組み合わせでちょっと(というかだいぶ)遅くなりますが、我慢しましょう。

import numpy as np
import matplotlib.pyplot as plt

def main():
    xx, yy = np.meshgrid(np.arange(0, 10, 0.02),
                         np.arange(0, 10, 0.02))
    zz = np.zeros(xx.shape)
    zz[xx < 5] = 1
    plt.pcolormesh(xx, yy, zz, 
                   alpha=0.03, shading="gouraud")
    plt.savefig("result.png")

if __name__ == "__main__":
    main()

結果2
結果2

 なんか濃さが違う気もしますが、よくなりました。濃さはalphaで調整してください。

まとめ

 これで変なダサい図を出さなくても済むようになると思います。

【python】statsmodelsでt検定する方法

はじめに

 この前はscipyでやる方法をまとめたわけですが、

www.haya-programming.com


 片側のオプションほしいなーと思ったのでstatsmodelsに浮気することにしました。

使い方の概要

 対応のないt検定はこれです。

statsmodels.stats.weightstats.ttest_ind — statsmodels 0.9.0 documentation

 引数は以下のようなものです。

statsmodels.stats.weightstats.ttest_ind(x1, x2,
alternative='two-sided', usevar='pooled', weights=(None, None), value=0)

 x1とx2が検定対象です。オプションで重要そうなのはalternativeとusevarです。

  • alternative

 "two-sided"と"larger"と"smaller"が渡せます。後ろの2つは片側のオプションです。

 こうしちゃうと、どっちがどっちに対して大きい/小さいのかわからん、とか考えないのかしら(x1 larger/smaller than x2(x1はx2より大きい/小さい)です。)。

  • usevar

 "pooled"と"unequal"が渡せます。スチューデントのt検定とウェルチのt検定に対応しているはずです。

 難しくはないけど、いちいちstrで重要なオプションを渡さないといけないのは少し面倒ですかね。間違った値にするとエラー出してくれるみたいなので、そういう意味では親切ですが。

実際にやってみる

 完全に前回と同じノリでやります。

 分布をとりあえず作る。

>>> from scipy import stats
>>> from statsmodels.stats.weightstats import ttest_ind
>>> d1 = stats.norm(loc=0, scale=1)
>>> d2 = stats.norm(loc=1, scale=1)

 やる。

>>> a = d1.rvs(3)
>>> b = d2.rvs(3)
>>> a
array([ 0.90831032, -0.88621515,  0.09060862])
>>> b
array([1.87384828, 2.92258811, 0.8539749 ])
>>> ttest_ind(a, b)
(-2.333627132285731, 0.07993392828229898, 4.0)

 t統計量、p値、自由度が返るみたいですね。

 ここで仮に有意水準0.05とすると、もう少しでいけそうだったけど切れなかったという残念な例です(そうなるまで何回か回しました)。そこで、片側検定にしてみます。

>>> ttest_ind(a, b, alternative="smaller")
(-2.333627132285731, 0.03996696414114949, 4.0)

 教科書通り半分のp値になって、めでたく「有意差」が出ました。教科書には「こういうことはやるな」と書いてあると思います。気をつけましょう。

 なお、片側の方向を間違えて指定した場合は、

>>> ttest_ind(a, b, alternative="larger")
(-2.333627132285731, 0.9600330358588506, 4.0)

 だいたい大きいp値になるので気づきます。でも、元のp値が0.42とかだったりすると案外気づかないかもしれない(どっちにしろ有意差ではないし、実害ないかもですが・・・)。

まとめ

 こっちの方が高機能だし、これでいいんじゃ? という感じもします。でもscipy入れててもstatsmodels入れてない人は多いと思うので、微妙っちゃ微妙ですね。

【python】setのandとorには要注意

はじめに結論

 and/orではなく&/|演算子を使う

概要

 setに対して積集合・和集合を計算したいときがあると思うのですが、うっかり&/|の代わりにand, orを使ってしまうとひどい目に遭います。

 たとえばこんな感じ。

>>> a = {0,1,2,3}
>>> b = {1,3,5,7}
>>> a and b
{1, 3, 5, 7}

 なにごと? と思いますよね。

and/orはブール値を返す訳ではない

 pythonのこの手の演算子はboolを返さないで、オペランドをそのまま返します。

>>> "hoge" and ""
''
>>> "hoge" or ""
'hoge'

 boolに変換してよしなに扱うのは外側にあるもの(if文やwhileの条件節など)の役割です。

 返ってくるのは最後に真理値が判定されたオペランドです。たとえば"hoge" and ""では"hoge"(True)だけでは全体の真理値が確定しないので""(False)も見て*1、真理値が確定したので""を返します。

 参考:
teru0rc4.hatenablog.com
qiita.com

 set型の場合は空のset以外はTrueなので、右側のオペランドが返ることが多いと思います。それが最初の結果になる理由です。

これの凶悪なところ

 python的には普通の動作なのですが、ある意味とても凶悪なハマりどころを提供してくれています。

  • 積集合、和集合はよく使う(と思う)
  • 返り値の型がsetで、型だけ見れば期待通りなので気づかない(ロジックエラーが出てから「あれ?」となる)

 怖いですね!

まとめ

 知っていればハマらないので、気をつけましょう。とにかく&/|で計算すると覚えることが一番大切です。

*1:ここが意味不な人は 組み込み型 — Python 3.7.3 ドキュメントを見てください

【python】scipyでt検定する方法まとめ

概要

 いっっっつも使い方を忘れて調べているので、自分で備忘録を書いておくことにしました。

t検定の概要

 2群の標本の平均に差異があるかどうかを検定します。帰無仮説は「両者の平均に差はない」、対立仮説は「両者の平均に差がある」です。

 詳しいことはwikipediaとかを見てください(手抜き)。

t検定 - Wikipedia

使う関数

 scipyのt検定を行う関数としては、

  • scipy.stats.ttest_ind
  • scipy.stats.ttest_rel

 の2つがあります。ttest_indは対応のないt検定、ttest_relは対応のあるt検定で使えます。

 使い所が多いのは対応のないt検定を行うttest_indの方なので、こちらだけ取り扱います。

引数と注意点

 いろいろあります。

scipy.stats.ttest_ind(a, b, axis=0, equal_var=True, nan_policy='propagate')

 a,bは普通にデータの入った1次元配列を渡して使うことが多いでしょう。axisという引数があることから想像が付く通り、多次元配列でも渡せるようです。使ったことはありません。

 equal_var=Trueだとスチューデントのt検定、equal=var=Falseだとウェルチのt検定です。これは等分散かどうかに関わらずウェルチのt検定で良いという話題があるので、Falseを指定してやると良いと思います。

qiita.com

 他の引数はあまり重要ではないので、説明を省略します。

 結果は(t統計量, p値)というtupleっぽいオブジェクト*1で返ります。p値が設定した有意水準(たとえば0.05)より小さいときに有意差があったと言えます(不慣れだと毎回「どっちだっけ?」と思うポイント)。

 また気になる点として、t検定は母集団が正規分布に従うというけっこうきつい仮定を置いています。しかし、実はあまり気にする必要はないという議論もあります。

実際には  X が正規分布でなくても, n が大きければ中心極限定理により  \overline{X} は正規分布に近づくので,この検定は母集団が正規分布かどうかには鈍感です。データの分布が正規分布かどうかの検定をしてから t検定を行う必要はまったくありません。

t検定

 そういうことらしいです。

 なお、scipyのt検定に片側検定のオプションはありません。両側検定の結果から計算するか、他のライブラリ(statmodelsなど)でやることになります。両側検定の結果から計算する場合は、

t, p = stats.ttest_ind(male, female, equal_var=True)
pval3 = p
pval2 = p / 2.0
pval1 = 1.0 - pval2
if t < 0.0:
    w = pval2
    pval2 = pval1
    pval1 = w

pythonのpandasによる簡単な統計処理:第3回 F検定,t検定その他

 みたいな感じになるようです。

やってみる

 まず適当な確率分布のオブジェクトを生成する。

>>> from scipy import stats
>>> d1 = stats.norm(loc=0, scale=1)
>>> d2 = stats.norm(loc=1, scale=1)

 N(0, 1)とN(1, 1)です。

 標本はこんな感じで取れます。

>>> d1.rvs(10)
array([ 0.18711344,  0.3534579 , -0.52046706,  0.47855615, -0.51033227,
        0.70266909,  0.19253524,  0.28232341,  1.24373963, -0.70771188])

 参考:
www.haya-programming.com

 念のために正しいパラメータになっているか確かめます。

>>> np.mean(d1.rvs(1000))
0.031232377764520782
>>> np.var(d1.rvs(1000))
0.9921694887020086
>>> np.mean(d2.rvs(1000))
0.97389464006143
>>> np.var(d2.rvs(1000))
1.0268883977332324

 大丈夫そうなのでt検定します。有意水準0.05とします(先に決めるのがルールなので・・・)。

 最初は標本サイズ3でやってみます。

>>> a = d1.rvs(3)
>>> b = d2.rvs(3)
>>> a
array([-1.29621283,  0.42129238, -0.13701242])
>>> b
array([ 0.81419163,  1.21399486, -1.40737252])
>>> stats.ttest_ind(a, b, equal_var=False)
Ttest_indResult(statistic=-0.5672127490081906, pvalue=0.6064712381602329)

 pvalue=0.6064712381602329で、0.05より圧倒的におおきいので有意差なしということになります。サンプルサイズが少なすぎて有意差が出せないのです。

 10まで増やしてみます。

>>> c = d1.rvs(10)
>>> d = d2.rvs(10)
>>> stats.ttest_ind(c, d, equal_var=False)
Ttest_indResult(statistic=-2.8617115251996275, pvalue=0.011903486818782736)

 今度は出ました。ただし何回かやると有意になったりならなかったりするので、出方のばらつき次第で変わる可能性があります。

 サンプルサイズの見積もりは以下の方法があるそうです。

 n=16\frac{s^2}{d^2}
幾つデータが必要か?―平均値の差の検定 | ブログ | 統計WEB
 ※引用者注:
  sは標準偏差、dは期待される二群間の差
 上式は有意水準5%の設定の場合に80%の検出力になるサンプル数
 (詳細はリンク先で読んでください)

 今回は標準偏差、二群間の差ともに1という簡単な設定なので、16サンプルあれば良い計算です。

>>> sum(stats.ttest_ind(
...       d1.rvs(16), d2.rvs(16), equal_var=False)[1] < 0.05
...       for _ in range(100))
78

 よさそうです。

まとめ

 簡単なことなのですが、割とやり方を忘れやすいので書き記しました。これで今後は忘れないで済むでしょう(フラグ)。

*1:厳密にはTtest_indResultという型だが、tupleの派生クラスで実質的にtupleとして扱えるので気にしなくて良い。中身はともにfloat

emacsでpythonを書くための設定 2019年版

概要

 emacsライトユーザーの私が、新規環境にemacs25を導入してpythonを書くにあたってやった設定を書いておきます。目的はpythonを書くことだけです。

 前提として、以下の記事のように環境を作っています(読まなくてもなんとかなります)。

www.haya-programming.com

 あれこれやってもそこまで快適にならないので、flymakeとjediの設定をやっただけです。

インデントの設定

 以前のinit.elからそのまま引き継ぎましたが、要らんかも。

(add-hook 'python-mode-hook
  (lambda () (setq python-indent-offset 4)))

pyflakes・flymakeを入れる

 これは入れないとはかどらないので入れました。

 まずpython側で、仮想環境をactivateした状態でpyflakesを入れます。

$ pip install pyflakes

 flymakeはemacsにデフォルトで入っていますが、設定が要ります。

; これも昔どこかからコピペして使っている秘伝のタレ・・・
(setq flymake-allowed-file-name-masks '())
(add-hook 'find-file-hook 'flymake-find-file-hook)
(when (load "flymake" t)
  (defun flymake-pyflakes-init ()
    (let* ((temp-file (flymake-init-create-temp-buffer-copy
                       'flymake-create-temp-inplace))
           (local-file (file-relative-name
                        temp-file
                        (file-name-directory buffer-file-name))))
      (list "ほげほげ/bin/pyflakesの絶対パスを書く"  (list local-file))))
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.py\\'" flymake-pyflakes-init)))
; show message on mini-buffer
(defun flymake-show-help ()
  (when (get-char-property (point) 'flymake-overlay)
    (let ((help (get-char-property (point) 'help-echo)))
      (if help (message "%s" help)))))
(add-hook 'post-command-hook 'flymake-show-help)

; デフォルトだと赤波線になって見づらかったんで直した
; 参考:https://suer.hatenablog.com/entry/20090307/1236403449
(custom-set-faces
  '(flymake-errline 
     ((((class color)) 
       (:foreground "red" :bold t :underline t))))
  '(flymake-warnline 
     ((((class color)) 
       (:foreground "red" :bold t :underline t))))); :background "white")))))

 これだけで使えるはずです。.pyを開けば効くようになります。

jediを入れる

 コード補完のない環境で書いていた期間も割と長く、なければないでやれることは実感として知っていたので入れるかどうか迷いましたが、せっかくなので入れることにしました。

 一応公式のマニュアルの通りにやればだいたいうまく行くはずですが、一回失敗してel-getから導入しなおしたら治ったみたいなこともあったので、割とハマりやすいと思います。要注意です(つーか正直emacsが面倒くさい。pythonに比べれば)。

tkf.github.io


 とりあえず、el-getが動くようにしておきましょう。

M-x el-get-install RET jedi RET

 これを打ったら(しばらく時間はかかるけど)そのうちぜんぶ導入終わったというメッセージがミニバッファに出てきますので、その後にinit.elを編集します。

(require 'jedi)
(add-hook 'python-mode-hook 'jedi:setup)
(setq jedi:complete-on-dot t)

 で、残念なことにjediはこれだけでは使えず、サーバなるものをpython側に入れないといけません。

 とりあえず、

$ pip install jedi

 するんですが(当然仮想環境で)、そもそもvenvで仮想環境を組んでいるのでどうしたものか(どう連携させるか)と考え込んでしまいました。virtualenvのやり方は調べると出てくるんですが、venvの場合はググってもよくわかりません。ドキュメントを読んでもさっぱり。

 思案した結果、「まあいいや、ダメ元でやろう」とpython仮想環境にvirtualenvを導入し(依存があるらしい。他にepcとかも要求されますが、jediと一緒に入ったような気もする)、仮想環境をactivateしたターミナルからemacsを立ち上げて、

M-x jedi:install-server RET 

 してみました。そしたらぜんぶ良い感じにやってくれました。警戒して以下のような2行も書いてみたりしたのですが、けっきょくなくてもそこそこちゃんと動いたので消すかどうか迷い中。

(setq jedi:server-command
      (list "仮想環境をactivateした状態のwhich pythonの結果" "ほげほげ/lib/python3.7/site-packages/jediepcserver.pyみたいなの"))

 ただし、この2行がないと「仮想環境をactivateしたターミルから立ち上げないとちゃんと動かない」状態になります。それはそれで良いような気もするし、不便といえば不便。まあ、仕組みがよくわからないので触っていません。

まとめ

 終わってみれば大したことやってないのですが、正直けっこう苦労しました。

 ライトユーザーにはけっこう難しいですが、とにかく環境は揃ったのでがんばります。