データフレームの背景に棒グラフを表示する

データフレームの書式設定シリーズの最後の記事です。
今回は背景にセルの値の大きさを表す棒グラフを表示します。
これ、地味に便利です。
ドキュメントはこちら

使い方は簡単で、df.style.bar()を呼び出すだけです。
オプションも色々指定子できますので、いくつか設定してやってみます。


# 適当なデータフレームを作成
df = pd.DataFrame(
        np.random.randint(-100, 100, size=(10, 4)),
        columns=["col0", "col1", "col2", "col3"]
    )

df.style.bar(
    align="mid",
    width=90,
    axis=None,
    color=['#d65f5f', '#5fba7d']
)

出力結果がこちら。

引数をいくつか説明しておきます。
まず align。 以下の3種類の値のどれかを取ります。

left
値が最小のセルの値が左端。デフォルト。
zero
ゼロがセルの中心
mid
最大値と最小値の平均か、もし正の数と負の数を両方含む場合は0が中心。

次に width は0〜100の値を取り、棒グラフの最大長がセルの何%を締めるかを表します。
colerは棒グラフの色です。文字列を一つ渡せば全てその色、配列にして2つ渡せばそれぞれ負の値の時と正の値の時の色です。
axis は最大最小値の基準が列方向(0)、行方向(1)、テーブル全体(None)のどれかを表します。
例では使っていませんが、vmin, vmax で最小値、最大値を指定することもできます。
外れ値があるような時は便利です。

データフレームの書式設定に組込書式を使う

データフレームを表示するときにCSSで書式設定する方法(apply,applymapを使う)を紹介してきましたが、
最初の方で少し書いた通り、これらの方法を使わなくてもよく使う書式はあらかじめ関数が用意されています。

ドキュメント: Builtin styles

非常によく使う、最大値最小値やNullの強調には、それぞれ次の3つの関数が使えます。

  • df.style.highlight_null(null_color=’red’)
  • df.style.highlight_max(subset=None, color=’yellow’, axis=0)
  • df.style.highlight_min(subset=None, color=’yellow’, axis=0)

subsetは対象の列を指定でき、colorは背景色、axisは最大値や最小値を評価する軸方向を指定します。

また、値の大小を色の濃淡(もしくは色相など)で、表現するには、df.style.background_gradientが使えます。
引数は次の通り。だいたいイメージ通り動きます。
df.style.background_gradient(cmap=’PuBu’, low=0, high=0, axis=0, subset=None, text_color_threshold=0.408)

cmapに渡すのはMatplotlibのcolormapです。
具体的な名前と色はこちらを参照して選びます。
Choosing Colormaps in Matplotlib

そして最後に、値によらず、データフレーム全体の書式を一括変更する時は、
set_propertiesを使います。
ドキュメントに例が載っていますが、これだけ少し引数の渡し方が独特なので注意です。
サンプル:


df.style.set_properties(**{'background-color': 'black',
                           'color': 'lawngreen',
                           'border-color': 'white'})

データフレームの書式を列や行ごとに設定する

前回の記事でデータフレームの書式設定をセル単位で設定する方法を紹介しました。
個々のセルの値だけで書式が決められうる場合はそれで十分なのですが、
実際の業務では列ごとの最大値や最小値などを目立たせたい場合がよくあります。
この場合、他のセルも参照しなければ書式を決められません。(厳密に言えば別途変数か何かを定義して実装できますが面倒です。)

このような時は、style.applymapの代わりに、style.applyを使います。
そして、渡す関数は、セルの値ではなく、DataFrameの行や列、つまりSeriesを受け取り、
CSS書式の配列を返す関数です。
引数axisに0(既定)を渡すと列ごと、1を渡すと行ごとに書式を設定できます。

試しに、最大値に色を塗ってみます。


def max_style(values):
    max_value = max(values)
    styles = [
        "background-color: yellow" if value == max_value else ""
        for value in values
    ]
    return styles


# 適当なデータフレームを作成
df = pd.DataFrame(
        np.random.randint(0, 100, size=(5, 3)),
        columns=["col0", "col1", "col2"]
    )

# 各列の最大値に着色 (axis=0 は省略可能)
print(df.style.apply(max_style).render())
col0 col1 col2
0 9 5 47
1 6 99 17
2 46 11 63
3 27 92 54
4 6 35 15


# 各行の最大値に着色
print(df.style.apply(max_style, axis=1).render())
col0 col1 col2
0 9 5 47
1 6 99 17
2 46 11 63
3 27 92 54
4 6 35 15

キャプチャ貼るのが面倒だったので、
結果はrender()で貼りました。

notebookでデータフレームを表示するときにセルの書式を設定する

pandasのデータフレームの値をjupyter notebookで確認するとき、
エクセルの条件付き書式のようにセルの値によって色を塗ったりするとわかりやすくなることが多くあります。

ネットで少し探せば、すぐにコードが出てくるのでよく理解せずに background_gradient などを使っていましたが、
先日のPyConで、@komo_frさんのセッション、pandasのStyling機能で強化するJupyter実験レポートを聞いて、ちゃんと体系立てて覚えて使おうというモチベーションが湧いてきたので、ドキュメントを読み始めました。

先述の background_gradient とか、 highlight_null とか 便利関数が用意されているのですが、
その前に基本から紹介していこうと思います。

今回は、単純にセルの値によって書式を指定する Styler.applymapです。
ドキュメントはここ

「データフレームの値を引数として受け取り、セルに設定したいCSS文字列を返す関数」をapplymapに渡すことで、
DataFrameの書式を設定します。

CSSっぽいな、というのは前々から感じてたのですが、CSSそのものだったんですね。
(CSSとよく似た独自構文を覚えなきゃ使えないのかと思ってました。)
ドキュメントにもそのまま「スタイル設定は、CSSを使用して行われます。」と書いてあるのでちゃんと読んでおけばよかったです。
The styling is accomplished using CSS.

では早速ですが、適当なデータフレームを作ってみて、値が入ってないセル、一定値より小さいセル、
その他のセルで書式を変えて表示してみました。


def cell_style(value):
    if value != value:
        return "background-color: gray; color: white"
    if value <= 40:
        return "background-color: yellow; font-weight: bold"
    else:
        return ""


# 適当なデータフレームを作成
df = pd.DataFrame(
        np.random.randint(0, 100, size=(5, 3)),
        columns=["col0", "col1", "col2"]
    )
df.loc[3, "col0"] = None
df.loc[1, "col2"] = None
df.style.applymap(cell_style)

jupyter notebookで実行したときに表示されるのがこちら。

また、.render()を使ってHTML出力もできます。
スタイルが思ったように適用されてないように感じたら、これを使って確認すると良いそうです。


print(df.style.applymap(cell_style).render())

実行して出力されたHTMLを記事中にそのまま貼り付けたのがこちらです。便利ですね。

col0 col1 col2
0 52 35 48
1 81 18 nan
2 37 80 33
3 nan 80 72
4 91 4 63

Type Hintで引数と戻り値の型を注記する

Python 3.5 から実装されている機能で、関数を定義するときに引数や戻り値の型を注記(アノテーション)する
Type Hint という機能があります。
ドキュメント 

次の例のように、引数の後ろには「:」をつけて型を書き、戻り値は行末の「:」の前に「->」を付けて型を書きます。
このように定義しておくと、help関数などでその関数が想定しているデータ型を確認できます。


def add_sample(x: int, y: float) -> float:
    return x + y


help(add_sample)
"""
Help on function add_sample in module __main__:

add_sample(x:int, y:float) -> float
"""

注意としては、あくまでもこれは注記で、本当にその型しか受け付けなくなったり、その方の戻り値を返すことを保証したりしないことです。
サンプルの例で言えば、float同士を受け取っても普通に計算しますし、文字列を渡せば結合します。


print(add_sample(2.5, 3.7))
# 6.2
print(add_sample("Type ", "Hint"))
# Type Hint

あくまでも可読性のための機能ですが、
便利に使える場面は多そうなので今後積極的に使っていこうと思います。


個人的な話になりますが、エンジニア?としてのキャリアの初期にJavaやExcel VBAばかり触っていた影響か、
実は静的型付け言語のほうが好きだったりします。(Pythonは動的型付け)
Python自体はかなり気に入っているので別に良いのですが。

SciPyを使って特定の確率分布にしたがう乱数を生成する

ここまでの数回の記事でいろいろな方法で特定の確率分布に従う乱数を得る方法を紹介してきましたが、
SciPyで生成する方法についてきちんと紹介してないことに気づいたので書いておきます。
numpyについてはこちらで書いてます。

といってもこれまでの実験中で使っている通り、SciPyのstatsモジュールに定義されている各確率分布ごとに、
rvsという関数があるのでそれを使うだけです。
確率分布が連続であっても、離散であっても同じ名前です。

ドキュメント:
(連続の例)正規分布の場合 scipy.stats.norm
(離散の例)二項分布の場合 scipy.stats.binom

最近の記事でも一様分布からのサンプリングで使いまくってるでほぼ説明不要なのですが、
以下の例のように各確率分布に従う乱数を得ることができます。


from scipy.stats import norm
from scipy.stats import binom
print(norm.rvs(loc=2, scale=5, size=5))
# [-1.46417053 -2.76659505  0.80006028  4.83473226  4.05597588]
print(binom.rvs(n=20, p=0.3, size=10))
# [9 7 7 5 9 5 8 4 9 8]

rvs ってなんの略だろう? 特にsは何かということが気になって調べていたのですが、
今の所、明確な答えは見つけられていません。(なんの略語かわからないと覚えにくい。)

sampling かな? と思っていたこともあるのですが、GitHubでソースを見ると _rvs_sampling ってのも登場するので違いそう。
チュートリアルの中に、
random variables (RVs)という記載があるので、random variablesの略である可能性が一番高いかなと思います。

PyConJP 2019に参加しました

9月16日と17日の二日間、大田区産業プラザPiOで開催されたPyConJP 2019に参加してきました。
昨年の2018も参加したのでこれで2回連続の参加です。

今年も非常に面白いセッションがたくさんあり、多くの学びがあった2日間でした。
ありがたいことに、connpassの資料ページや、
公式サイトのタイムテーブルページに発表資料をまとめていただいていて、
時間の被り等で聞けなかった発表の資料もすごい手軽に確認できるようになっています。

ちなみに僕は以下の講演を聞きました。

1日目

基調講演 Why Python is Eating the World
PythonとAutoML
機械学習におけるハイパーパラメータ最適化の理論と実践
Dashとオープンデータでインタラクティブに日本経済を可視化する
Pythonを使ったAPIサーバー開発を始める際に整備したCIとテスト機構
pandasのStyling機能で強化するJupyter実験レポート
LT

2日目

基調講演 Pythonで切り開く新しい農業
Pythonで始めてみよう関数型プログラミング
婚活・恋活領域におけるPythonを使ったマッチング最適化
知ろう!使おう!HDF5ファイル!
Anaconda環境運用TIPS 〜Anacondaの環境構築について知る・質問に答えられるようになる〜
チームメイトのためにdocstringを書こう
LT

2日間通して、今までなんとなくやっていたことの詳細を知れたり、
いつか試したいなと思っていたライブラリをいよいよ触ってみようというモチベーションが上がったり、
全く知らなかった手法を知れたりと本当に参考になる話がたくさんありました。

とりあえず、手軽なところからになると思いますが順次試していって、このブログでも紹介していこうと思います。
また、聞けなかったセッションの資料も順次確認していきます。

イベントを通してですが、昨年と比べて、ちょっとした些細なところにも多くの改善の工夫がされていて、
運営の皆さんのより良いイベントにしていこうという熱意を感じる2日間でした。
1000人を超える人が集まるイベントをスムーズに開催するだけでも相当大変なことだと思いますが、
このような素敵な場を提供していただけて、本当にありがたいなと思います。

来年は8月の開催とのことですが、また是非とも参加したいです。

棄却法の例

前回の記事で紹介した棄却法を実際にやってみましょう。

今回の例はベータ分布です。(とりうる値の範囲が有限のものの方が適用しやすいので)
とりあえず、$B(2,3)$でやってみましょう。
確率密度関数は区間$x\in[0,1]$の範囲では次の式で表されます。(それ以外の$x$に対しては$0$です)。
$$
f(x) = \frac{x(1-x)^2}{B(2, 3)} = 12x(1-x)^2.
$$
この関数は$x=1/3$で最大値$f(1/3)=16/9$をとります。
(単純な式なので微分してすぐに確認できます。)

さて、実際にプログラムで実行してみたのがこちらです。
念のためですが、今回の目的は前回の記事で紹介したアルゴリズムで目的とする乱数が得られることを確認することです。
単にベータ分布に従う乱数が必要な場合は、scipyのrvs関数を使いましょう。


# ベータ分布の確率密度関数
f = beta.freeze(a=2, b=3)


def beta_rejection_sampling():
    while True:
        u, v = uniform().rvs(2)
        x = u  # 今回は 0<= x <= 1 なのでu をそのまま使用
        y = 16/9 * v
        if y <= f.pdf(x):
            return x


# 棄却法で10000データ生成する
data = [beta_rejection_sampling() for _ in range(10000)]

# 生成された乱数のヒストグラムと、確率密度関数を可視化
fig = plt.figure(facecolor="w")
ax = fig.add_subplot(111)
ax.hist(data, bins=100, density=True, label="棄却法により生成")
ax.set_xlim([0, 1])
x = np.linspace(0, 1, 100)
ax.plot(x, f.pdf(x), label="確率密度関数")
ax.legend()
plt.show()

出力がこちら。

しっかり機能していますね。

フォンノイマンの棄却法

今回も乱数を生成するお話。
累積分布関数の逆関数が求まるなら逆関数法、正規分布の場合はボックス=ミュラー法が使えるという話を書きましたが、
もっと一般の分布で使える方法として、フォンノイマンの棄却法というのがあることを最近知りました。

Wikipediaでは英語版のみページがあるようです:Rejection sampling
(自然科学の統計学に紹介されている 別名法もこれは離散版ですがアイデアが似てるので参考になるかも)

これは次のステップで行います。
まず、生成したい分布の確率密度関数$f(x)$の最大値$M$を求めておきます。
また、取得する乱数の区間$[x_{min}, x_{max}]$をきめます。
(ベータ分布や2項分布のような有限区間の値しか取らない乱数なら容易ですが、そうでない場合は十分大きな範囲をとって適当なところで区切るしかないですね)

そして、次の手順で乱数を生成します。
1. 標準一様分布$U(0, 1)$から二つの乱数$u, v$を生成する。
2. $x = x_{min}+(x_{max}-x_{min})u$ を計算し、区間$[x_{min}, x_{max}]$の乱数を得る。
3. $y = M*v$ を計算し、 $y$と$f(x)$を比較する。
4. 結果が$y<=f(x)$であれば、乱数として$x$を採用し、そうでない場合は、二つの乱数生成に戻ります。 $x$が乱数として採用される確率が$f(x)$の値に比例するため、 結果として確率密度関数$f$に従う乱数を得ることができます。

ボックス=ミュラー法

Scipyが使える今となっては使う機会はほぼありませんが、
一様分布から正規分布に従う乱数を作成できる方法である、ボックス=ミュラー法(Box–Muller’s method)を紹介します。

正規分布は累積分布関数やその逆関数が初等関数では表現できず、
最近紹介した逆関数法で乱数を生成するのは少々困難です。

そこでこのボックス=ミュラー法が使われます。
参考:自然科学の統計学(東京大学出版会)の 11.3 正規乱数の発生法

まず、確率変数$X$,$Y$が互いに独立で、共に$(0,1)$上の一様分布に従うとします。
この時、
$$
\begin{align}
Z_1 & = \sqrt{-2\log{X}}\cos{2\pi Y},\\
Z_2 & = \sqrt{-2\log{X}}\sin{2\pi Y}
\end{align}
$$
とすると、$Z_1$と$Z_2$は標準正規分布$N(0,1)$に従う互いに独立な確率変数になります。

厳密な証明は今回は省略します。
ただ、数式をみれば、正規分布の確率密度関数が指数関数の形をしているので$\log$が出てくるのも、
円周率も出てくるなど円に関係しそうな気配があるので三角関数が出てくるのもなんとなく納得性があります。

ただ、「互いに独立な」ってのは正直驚きます。
$Z_1^2+Z_2^2=-2\log{X}$って関係式が成り立つのに独立ってことはないんじゃないかと思えますね。 
(この$X$が定数ではないのがキモで、$0<X<1$から、$-2\log{X}$が
非常に大きな値も含めて実に自由に動くので独立性が生まれるようです。)

ここだけ実験して相関係数が0に近いことを確認しておきましょう。


import numpy as np

# 一様分布に従うX, Yをそれぞれ10000個生成
X = np.random.rand(10000)
Y = np.random.rand(10000)

# Z_1, Z_2 を計算
Z_1 = np.sqrt(-2*np.log(X))*np.cos(2*np.pi*Y)
Z_2 = np.sqrt(-2*np.log(X))*np.sin(2*np.pi*Y)

# 相関係数を算出
print(np.corrcoef(Z_1, Z_2)[0, 1])
# -0.0043281961119066865

確かに独立っぽいですね。
散布図等も出してみましたが何か特別な関係性は見当たらないようです。