matplotlibでxkcd風にグラフを描く

xkcdってなんだ?って方はこちらをどうぞ。
https://xkcd.com/
wikipedia: xkcd

誰が何の目的で実装されたのか不明ですが、matplotlibにはグラフをxkcdのコミック風に出力する機能があります。
面白いので僕はこういう機能は結構好きです。

ドキュメント: matplotlib.pyplot.xkcd

使い方は簡単で、グラグを書く前、要はplotやbarなどの関数を使う前に、plt.xkcd()を差し込むだけ。
ただ、この手軽さに落とし穴がありました。一回呼び出すと戻せなくなるのです。
(調査にかなり手こずったので、この記事もどちらかというとxkcdの使い方より元への戻し方を伝えたい。)

ドキュメントにも、pcParamsを上書きしてしまうと書いてあります。

Notes
This function works by a number of rcParams, so it will probably override others you have set before.
f you want the effects of this function to be temporary, it can be used as a context manager, for example:

context manager ってのは要は with句の事のようです。(あとでちゃんと調べたい。)

要するに、 with plt.xkcd(): で有効な範囲をあらかじめ絞りましょう
参考に、以下のコードでは二つのグラフをxkcd風に書いた後に、普通のグラフを2つ作成しました。


import matplotlib.pyplot as plt
import numpy as np

data = [5, 4, 3, 2, 1]
label = [f"item {i}" for i in range(5)]
X = np.linspace(0, 2*np.pi, 200)
Y_sin = np.sin(X)
Y_cos = np.cos(X)

fig = plt.figure(figsize=(12, 9), facecolor="w")

# xkcd オプションの影響を局所化するため with で使う。
with plt.xkcd():
    ax = fig.add_subplot(2, 2, 1, title="xkcd pie chart")
    ax.pie(
        data,
        labels=label,
        autopct='%3.1f%%',  # 割合をグラフ中に明記
        counterclock=False,  # 時計回りに変更
        startangle=90,  # 開始点の位置を変更
    )
    ax = fig.add_subplot(2, 2, 2, title="xkcd plot")
    ax.plot(X, Y_sin, label="$y=\\sin(x)$")
    ax.plot(X, Y_cos, label="$y=\\cos(x)$")
    ax.legend()

ax = fig.add_subplot(2, 2, 3, title="normal pie chart")
ax.pie(
    data,
    labels=label,
    autopct='%3.1f%%',  # 割合をグラフ中に明記
    counterclock=False,  # 時計回りに変更
    startangle=90,  # 開始点の位置を変更
)

ax = fig.add_subplot(2, 2, 4, title="normal plot")
ax.plot(X, Y_sin, label="$y=\\sin(x)$")
ax.plot(X, Y_cos, label="$y=\\cos(x)$")
ax.legend()

plt.show()

出力されたグラフがこちら。

場面を選べば、プレゼンや資料などで使いやすそうなグラフですね。
あと、注意点としては、対応したフォントがないので日本語文字が使えません。

もし、 with を使わずに plt.xkcd() してしまったら、それ以降のグラフは全部、
xkcdモードで出力されてしまいます。
jupyter notebookであれば戻すためにカーネルの再起動が必要になるので気をつけましょう。

matplotlibで円グラフ

自分の分析のために円グラフを描きたい場面というのがあまりなく、誰かのためのダッシュボードや、稀に自分でも必要になるときはTableauで作成するので、
Pythonで円グラフを作成することはあまりないのですが、最近機会があったので方法メモしておきます。

円グラフは英語で pie chart というので、
matplotlibでも pie という名前の関数で作成できます。
ドキュメント: matplotlib.pyplot.pie

最小構成で作成するなら、データとラベルを渡してあげれば、それだけで描いてくれます。
データも割合ではなく数量で渡しても、勝手に合計100%になるように描いてくれるので楽です。
ただ、初期設定だと(個人的に)少し見慣れないデザインになるのでいくつかオプション設定します。
比較用にほぼ何も設定しないバージョンとそれぞれ出力したのが次のコードです。


import matplotlib.pyplot as plt

data = [5, 4, 3, 2, 1]
label = ["項目1", "項目2", "項目3", "項目4", "項目5"]

fig = plt.figure(figsize=(12, 7), facecolor="w")
ax = fig.add_subplot(1, 2, 1, title="円グラフ (初期設定)")
ax.pie(
    data,
    labels=label,
)

ax = fig.add_subplot(1, 2, 2, title="円グラフ (見た目修正)")
ax.pie(
    data,
    labels=label,
    autopct='%3.1f%%',  # 割合をグラフ中に明記
    counterclock=False,  # 時計回りに変更
    startangle=90,  # 開始点の位置を変更
)
plt.show()

結果がこちら。

どこまで工数を使うかにもよりますが、色とかももう少し工夫した方が使いやすそうですね。
(ただ、あまり凝ったことをするなら別のBIツールに任す方がオススメです。)

plot_roc_curveを試す

今回もscikit-learn 0.22.0 の新機能を試します。
今回はROC曲線を書いてくれる、plot_roc_curveです。
ドキュメント: sklearn.metrics.plot_roc_curve

元々、 roc_curve という機能はあったのですが、これを可視化するのは少しだけ面倒だったので結構期待していました。

ただ、試してみたところ、plot_roc_curveは2値分類のモデルにしか対応していないようです。
roc_curveは引数のpos_labelに対象のラベルを指定してあげれば多値分類にも対応しているので、今後に期待します。

ということで、2値分類のダミーデータを作成して、試してみます。
綺麗に分類できる問題だとROC曲線の価値がよくわからないので線形分離不可能な問題にしています。
最後にグラフを二つ出力していますが、左のが、生成したテストデータとモデルの決定境界、右側が、ROC曲線です。


from sklearn.metrics import plot_roc_curve
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import numpy as np
import matplotlib.pyplot as plt

# 綺麗に線形分類することのできないダミーデータの生成。
X0 = np.clip(2 * np.random.randn(500, 2) + np.array([1, 1]), -7, 7)
X1 = np.clip(2 * np.random.randn(500, 2) + np.array([-1, -1]), -7, 7)
X = np.concatenate([X0, X1])
y = np.array([0]*500 + [1]*500)

# 学習データとテストデータに分ける
X_train, X_test, y_train, y_test = train_test_split(
                                    X,
                                    y,
                                    test_size=0.2,
                                    stratify=y,
                                )
# モデルの作成と学習
clf = LogisticRegression()
clf.fit(X_train, y_train)

# ここから可視化
fig = plt.figure(figsize=(12, 6), facecolor="w")
ax = fig.add_subplot(1, 2, 1, aspect='equal', xlim=(-7, 7), ylim=(-7, 7))
ax.set_title("テストデータと決定境界")

# データを散布図で表示する
ax.scatter(X_test[y_test == 0, 0], X_test[y_test == 0, 1], alpha=0.7)
ax.scatter(X_test[y_test == 1, 0], X_test[y_test == 1, 1], alpha=0.7)

# 決定境界を可視化する
X_mesh, Y_mesh = np.meshgrid(np.linspace(-7, 7, 401), np.linspace(-7, 7, 401))
Z_mesh = clf.predict(np.array([X_mesh.ravel(), Y_mesh.ravel()]).T)
Z_mesh = Z_mesh.reshape(X_mesh.shape)
ax.contourf(X_mesh, Y_mesh, Z_mesh, alpha=0.1)

ax = fig.add_subplot(1, 2, 2, aspect='equal', xlim=(0, 1), ylim=(0, 1))
ax.set_title("ROC曲線",)
plot_roc_curve(clf, X_test, y_test, ax=ax)

plt.show()

出力された図がこちら。

コード全体が長いのでわかりにくくて恐縮ですが、 ROC曲線自体は1行で出力できており、非常に手軽です。
また、AUCも同時に出力してくれています。

scikit-learnにスタッキングのクラスが追加されたので使ってみる

前回に引き続き、scikit-learn 0.22.0 の新機能の紹介です。

複数の機械学習モデルを組み合わせて使う方法の一つに、スタッキング(Stacking)があります。
簡単に言えば、いつくかの機械学習のモデルの予測結果を特徴量として、別のモデルが最終的な予測を行うものです。
アイデアは単純で、効果もあるらしいのですが、これをscikit-learnでやろうとすると、少し面倒だったので僕はあまり使ってきませんでした。

それが、 StackingClassifierStackingRegressor という 二つの機能がscikit-learnに実装され、
手軽にできそうな期待が出てきたので、試してみました。
とりあえず、 StackingClassifier (分類)の方をやってみます。

サンプルのコードを参考しながら、 iris を digitsに変えてやってみました。
ランダムフォレストと、線形SVMの出力を特徴量にして、ロジスティック回帰で予測しています。


from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.ensemble import StackingClassifier

# データの準備
X, y = load_digits(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
        X,
        y,
        stratify=y,
        random_state=42,
    )


# 1段目として、二つのモデルを構築。
# 初期値の max_iter では収束しなかったので大きめの値を設定
estimators = [
        ('rf', RandomForestClassifier(n_estimators=10, random_state=42)),
        ('svr', make_pipeline(
                StandardScaler(),
                LinearSVC(max_iter=4000, random_state=42)
                )
            )
        ]
clf = StackingClassifier(
    estimators=estimators,
    final_estimator=LogisticRegression(max_iter=600),
)
clf.fit(X_train, y_train)
print("正解率:", clf.score(X_test, y_test))
# 正解率: 0.96

そこそこの正解率が出ましたね。
ちなみに、同じパラメータのランダムフォレストと線形SVM、の単体の正解率は次のようになります。


make_pipeline(
            StandardScaler(),
            LinearSVC(max_iter=4000, random_state=42)
        ).fit(X_train, y_train).score(X_test, y_test)

# 0.9511111111111111

RandomForestClassifier(
    n_estimators=10,
    random_state=42
).fit(X_train, y_train).score(X_test, y_test)

# 0.9333333333333333

確かにスタッキングしたモデルの方が正解率が高くなっていました。

scikit-learnのversion 0.22.0 がリリースされたので、混同行列の可視化機能を試す

先日、scikit-learnの新しいバージョンがリリースされていたことに気づきました。
Version 0.22.0 December 3 2019

色々、機能が追加されていたり、改善が施されていたりしますが、何かパッと試せるものを試してみようと眺めてみたのですが、
新機能の中に metrics.plot_confusion_matrix というのが目についたのでこれをやってみることにしました。

元々、 confusion_matrix を計算する関数はあるのですが、
出力がそっけないarray で、自分でlabelを設定したりしていたのでこれは便利そうです。

まず、元々存在する confusion_matrix で混同行列を出力してみます。


from sklearn.svm import SVC
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix

# データの読み込み
iris = load_iris()
X = iris.data
y = iris.target
class_names = iris.target_names

X_train, X_test, y_train, y_test = train_test_split(
                                        X,
                                        y,
                                        test_size=0.2,
                                        random_state=0,
                                        stratify=y,
                                    )

# モデルの作成と学習
classifier = SVC(kernel='linear', C=0.01).fit(X_train, y_train)
y_pred = classifier.predict(X_test)
print(confusion_matrix(y_test, y_pred))

"""
[[10  0  0]
 [ 0 10  0]
 [ 0  3  7]]
"""

最後の行列が出力された混同行列です。各行が正解のラベル、各列が予測したラベルに対応し、
例えば一番下の行の中央の3は、正解ラベルが2なのに、1と予測してしまったデータが3件あることを意味します。
(とても便利なのですが、行と列のどちらがどっちだったのかすぐ忘れるのが嫌でした。)

さて、次に sklearn.metrics.plot_confusion_matrix を使ってみます。
どうやら、confusion_matrixのように、正解ラベルと予測ラベルを渡すのではなく、
モデルと、データと、正解ラベルを引数に渡すようです。
こちらにサンプルコードもあるので、参考にしながらやってみます。
normalizeに4種類の設定を渡せるのでそれぞれ試しました。

データとモデルは上のコードのものをそのまま使います。


import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import plot_confusion_matrix

# 表示桁数の設定
np.set_printoptions(precision=2)

# 可視化時のタイトルと、正規化の指定
titles_options = [
        ("Confusion matrix, without normalization", None),
        ("Normalized confusion matrix: true", 'true'),
        ("Normalized confusion matrix: pred", 'pred'),
        ("Normalized confusion matrix: all", 'all'),
    ]

fig = plt.figure(figsize=(10, 10), facecolor="w")
fig.subplots_adjust(hspace=0.2, wspace=0.4)
i = 0
for title, normalize in titles_options:
    i += 1
    ax = fig.add_subplot(2, 2, i)
    disp = plot_confusion_matrix(
                        classifier,
                        X_test,
                        y_test,
                        display_labels=class_names,
                        cmap=plt.cm.Blues,
                        normalize=normalize,
                        ax=ax,
                    )

    # 画像にタイトルを表示する。
    disp.ax_.set_title(title)

    print(title)
    print(disp.confusion_matrix)
plt.show()

"""
Confusion matrix, without normalization
[[10  0  0]
 [ 0 10  0]
 [ 0  3  7]]
Normalized confusion matrix: true
[[1.  0.  0. ]
 [0.  1.  0. ]
 [0.  0.3 0.7]]
Normalized confusion matrix: pred
[[1.   0.   0.  ]
 [0.   0.77 0.  ]
 [0.   0.23 1.  ]]
Normalized confusion matrix: all
[[0.33 0.   0.  ]
 [0.   0.33 0.  ]
 [0.   0.1  0.23]]
"""

最後に表示された画像がこちら。

今回例なので4つ並べましたが、一つだけ表示する方が カラーバーの割合がいい感じにフィットします。
軸に True label、 Predicated label の表記を自動的につけてくれるのありがたいです。

pandasのデータフレームの行をランダムにシャッフルする

てっきり専用のメソッドがあると思っていたら、無さそうだったのでやり方のメモです。
pandas のデータフレームのデータをランダムにシャッフルする方法を紹介します。

これを使う場面の一例ですが、たとえばkerasで作ったモデルを学習する時にvalidation_split を使う場合、
データをシャッフルせずに後ろの方から指定した割合をvalidationデータとして使うため、
事前にデータをシャッフルしておかないとtrainとvalidationでデータの傾向が違うということが発生し得ます。
参考:validation splitはどのように実行されますか?

pandasのデータフレームをランダムに混ぜ合わせる方法は何通りも考えらるのですが、
sample()メソッドに、frac=1を渡して実行するのが一番簡単そうです。
参考:DataFrameのsampleメソッドのドキュメントを読む
そうすることによって、データフレームの全ての行がサンプリングされ、さらに、sample()の仕様により、データはシャッフルされます。

ついでにインデックスの振り直しまでやっておくと便利なので、次の例のように使うと良さそうです。


import pandas as pd
# サンプルデータ生成
df = pd.DataFrame(
    {f"col{i}": [f"value{i}{j}" for j in range(10)] for i in range(3)}
)
print(df)
"""
      col0     col1     col2
0  value00  value10  value20
1  value01  value11  value21
2  value02  value12  value22
3  value03  value13  value23
4  value04  value14  value24
5  value05  value15  value25
6  value06  value16  value26
7  value07  value17  value27
8  value08  value18  value28
9  value09  value19  value29
"""

# シャッフルしてインデックスを振り直す
df = df.sample(frac=1).reset_index(drop=True)
print(df)
"""
      col0     col1     col2
0  value08  value18  value28
1  value06  value16  value26
2  value00  value10  value20
3  value03  value13  value23
4  value05  value15  value25
5  value02  value12  value22
6  value07  value17  value27
7  value04  value14  value24
8  value09  value19  value29
9  value01  value11  value21
"""

jupyter notebook で HTMLを表示する

諸事情あって、notebookで出力するテキストに色を塗りたかったので、方法を調べました。
結論として、spanタグでbackground-colorを設定したHTMLを組み立てて、それを表示する方法をとりました。

notebookで、変数に格納されたHTMLテキストを表示するには、次の関数を使います。

class IPython.display.HTML

使い方のイメージ


from IPython.display import HTML
# 変数 html_text に表示したいhtmlが文字列で入ってるものとする。
HTML(html_text)

ただ、変数に格納されたhtmlを表示するのではなく、普通にhtmlを書くのであれば、
セルをマークダウンモードにしておけば自動的にhtmlとして解釈して表示してくれます。

この他、 %%html というマジックコマンドを使うことも可能です。
参考:cellmagic-html

kerasで使える活性化関数を可視化する

kerasではあらかじめ様々な活性化関数が用意されています。
正直普段使ってないものもあるのですが、どのようなもがあるのか一通り把握しておきたかったので可視化を試みました。
原論文か何か読んで数式を順番に実装しようかと思ったのですが、
Activationレイヤーだけから構成される超単純なモデルを作るのが手軽だと気付いたのでやってみました。

今回はこちらのページにある、 利用可能な活性化関数 を対象にしました。
(この他にもより高度な活性化関数はいくつか用意されています。)

コードは次のようになります。


from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Activation
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 101).reshape(-1, 1)

# 絶対値の範囲が 有限なもの
activations_0 = [
    "sigmoid",
    "tanh",
    "hard_sigmoid",
    "softsign",
    ]

# 絶対値の範囲に上限がないもの
activations_1 = [
    "elu",
    "selu",
    "softplus",
    "relu",
    "linear",
]

fig = plt.figure(figsize=(10, 8))
for i, activations in enumerate([activations_0, activations_1]):
    ax = fig.add_subplot(2, 1,  i+1)
    for activation_str in activations:
        model = Sequential()
        model.add(
            Activation(
                activation_str,
                input_shape=(1, ),
            )
        )
        y = model.predict(x).ravel()
        ax.plot(x, y, label=activation_str)

    ax.legend()

plt.show()

出力結果はこちら。

bashコマンドで標準出力に出力する内容をファイルにも書き出す

久々に使おうとしたら忘れてしまっていたのでメモです。

長時間かかるコマンドの実行を待つ時、標準出力にずらずらと出てくるメッセージを見ながら、
それをファイルにも残しておきたいことがあります。
だいたい、リダイレクション(>や>>)を使って、ファイルに書き出しながら、
ファイルを tail -f コマンドで監視することが多いのですが、
tee という専用のコマンドも用意されています。

次のように、パイプラインで出力をtee コマンドに渡してあげて、 teeコマンドの引数に出力したいファイルを指定しておくと、
標準出力にはそのまま出力され、ファイルにも同じ内容が残ります。


$ echo 'tee Test' | tee file.txt
tee Test
$ cat file.txt
tee Test

-aをつけると追記です。(つけないと上書き)


$ echo 'tee Test2' | tee -a file.txt
tee Test2
$ cat file.txt
tee Test
tee Test2

printでお手軽プログレスバー

前回のprintのオプションの記事の応用です。
参考:Python3のprintの引数

大きめのデータの前処理など待ち時間が長い処理をする時に、進捗を表示したくなる時があります。
専用の良いライブラリもたくさんあるので、実用上はそれで十分なのですが、
前回のprintのオプションを使うと、簡易的なプログレスバーを作成できます。
(何も考えずに printしても進捗はわかるのですが、notebookが見にくくなったりします。)

ポイントは end を使って改行を止めることと、 “\r”(キャリッジリターン)を出力して、
次の文字の出力先を行の先頭にし、前回の出力を上書きすることです。

一例としてやってみます。
本当は重い処理のループなどの中でやるのですが、今回例として time の sleepで無理やり待ち時間を入れています。


import time
for i in range(1, 101):
    print("■" * (i//10), "□"*(10-i//10), sep="", end=" : ")
    print(str(i).zfill(3), "/", 100, "\r", sep="", end="")
    time.sleep(1)

手軽に、という割にコードが汚くなりました。
■と□は余計かもしれませんね。

これを実行すると、こんな感じの表示が出て数字が増えながら、徐々に黒く塗りつぶされていきます。
■■■■□□□□□□ : 040/100