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

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

逆関数法で指数分布に従う乱数を生成する

前回の記事で逆関数法を紹介したので具体的な分布で試そうという趣旨の記事です。
今回は指数分布を使います。(累積分布関数もその逆関数も容易に計算できるから)

まず、指数分布の確率密度関数から。指数分布はパラメータ$\lambda$を一つ持ちます。
$$
f(x) = \left\{
\begin{matrix}
\lambda e^{-\lambda x} & (x \leq 0)\\
0 & (x < 0) \end{matrix} \right. $$ そして、累積分布関数は次のようになります。 $$ F(x) = \left\{ \begin{matrix} 1-e^{-\lambda x} & (x \leq 0)\\ 0 & (x < 0) \end{matrix} \right. $$ さらにその逆関数は、こうなります。 $$ F^{-1}(u) = - \frac{1}{\lambda}\log(1-u) \quad (0\leq u <1). $$ それではPythonで、$[0,1)$の乱数を10000個くらい生成して、 逆関数法で$f(x)$に従う乱数が得られるのか実験してみましょう。 今回は単純のため、$\lambda=1$とします。 Pythonでやってみたコードと結果がこちら。


import numpy as np
import matplotlib.pyplot as plt

# 標準一様分布 に従う乱数を10000個生成する
U_data = np.random.rand(10000)
# 逆関数方法で指数分布に従うデータ生成
result_data = -np.log(1 - U_data)
# ヒストグラムと確率密度関数をプロットする
fig = plt.figure(facecolor="w")
ax = fig.add_subplot(111)
ax.hist(result_data, bins=100, density=True)
ax.plot(np.arange(0, 8, 0.1), np.exp(-1*np.arange(0, 8, 0.1)))
plt.show()

出力。

データ件数が多いので非常に綺麗に出ましたね。

任意の確率分布に従う乱数を逆関数法で一様分布から生成する

学生時代にExcelで色々シミュレーションしていたときによく使った方法です。
エクセル関数で生成できる乱数がrand()で得られる一様分布の乱数くらいしかなく、
他の分布に従う乱数が欲しい時はそこから変換して作っていました。
(Pythonを使うようになってから、Scipyに実装されている確率分布を使えば良いので、滅多に行うことがなくなりました。)

ただ、逆関数法という名前がついていることは最近知りました。
逆関数法 出典: フリー百科事典『ウィキペディア(Wikipedia)』
英語では inverse transform sampling というようです。

前回の記事同様、少々厳密性は犠牲にして、登場する各関数等は(連続性や滑らかさなどの)適切な仮定を満たしているものとします。

まず前提として、区間$[0,1]$上の一様分布に従う乱数$U$は取得できるものとします。
$$
\begin{align}
f_U(u) = \left\{\begin{matrix}
1& u \in[0,1]\\
0& u \notin[0,1]
\end{matrix}\right.
\end{align}
$$

そして欲しい乱数$X$が従う確率密度関数を$f(x)$とし、累積分布関数を$F(x)$とします。
$$
F(x) = \int_{-\infty}^x f(y) dy.
$$

そして$F$の逆関数を$F^{-1}$とした時、
$X=F^{-1}(U)$は、確率密度関数$f(x)$に従う確率変数となります。

証明は前回の記事で紹介した、確率変数の変換を使います。
この記事中の、$g$に相当するのが$F^{-1}$であることに注意して計算します。
まず前回の記事で紹介した返還式に、この記事中で使っている記号に注意しながら代入します。
$$
f_X(x) = f_U(F(x))\frac{du}{dx}.
$$
ここで、累積分布関数の性質より$0\leq F(x)\leq 1$より、$f_U(F(x))=1$です。
さらに、
$$
\frac{du}{dx} = \frac{d}{dx}F(x) = f(x)
$$
となることから、次の式が示されました。
$$
f_X(x) = f(x).
$$

累積分布関数(と、その逆関数)が具体的に計算できる確率分布についてはこれは非常に便利な式です。
学生時代は個人的な調べ物のため、指数分布を使うシミュレーションをよくやっていたので、よく使っていました。

確率変数の変換

対応関係にある確率変数について、一方の確率変数が従う確率分布が分かっているときに、
もう一方の確率変数が従う確率分布を求める方法の紹介です。
大抵の統計学の教科書のかなり前半の方に登場しますし、色々な確率分布の導出などにも使うので、
非常に基本的な内容ですが、意外に知名度が低いように感じています。

早速必要な記号の定義から始めましょう。
二つの確率変数$X$と$Y$が、それぞれ確率密度関数$f_X(x)$と$f_Y(y)$の確率分布に従っているとします。
さらに、$X$と$Y$の間に$y=g(x)$という1対1の対応があると仮定します。

($\frac{dx}{dy}$を使うので、$g$の微分可能性の条件なども必要なはずなのですが、
この辺りは統計学の教科書でもあまり重要視されてないような気がします。
とりあえずこの記事では必要な仮定は満たされているものとします。)

このとき、次の式が成り立ちます。
$$
f_Y(y) = f_X(g^{-1}(y))\frac{dx}{dy}.
$$

証明と呼ぶにはかなり荒い説明になるのですが、自分は次のよう理解しています。
(式をど忘れしたときにさっと導出するため、厳密性は犠牲にしていますのでご注意をお願いします。
例えば、積分した結果が等しいからといって即座に中の関数が等しいことの証明にはならないと思います。)

まず、XとYの対応関係(y=g(x))から定数$a,b (a\leq b)$に対して次の等式が成り立ちます。
$$
P(a\leq Y \leq b) = P(g^{-1}(a)\leq X \leq g^{-1}(b)).
$$
これを確率密度関数の積分で表記すると次のようになります。
$$
\int_a^bf_Y(y)dy = \int_{g^{-1}(a)}^{g^{-1}(b)}f_X(x)dx.
$$
この右辺を$x=g^{-1}(y)$で置換積分すると、積分区間は$x:g^{-1}(a)\rightarrow g^{-1}(b)$に対して、$y:a\rightarrow b$となります。
よって、次のようになります。($dx/dy$のつけ忘れに注意。)
$$
\int_a^bf_Y(y)dy = \int_a^b f_X(g^{-1}(y))\frac{dx}{dy}dy.
$$
この積分の中の式を比較することで、上の確率密度関数の変換の公式を思い出すことができます。

標本平均と不偏分散の独立性の実験

最近数式ばかり増えてプログラムが登場してないのでちょっとした検証をやろうと思います。
今回行うのは、タイトルにある通り、標本平均と不偏分散の独立性です。

少し前の母平均の区間推定のところで、
$\sqrt{n}(\bar X-\mu)/s$は、自由度$n-1$の$t$分布に従う、とさらっと書きましたが、
これの前提になっているのが、
$\frac{\bar X -\mu}{\sigma/\sqrt{n}}$が標準正規分布に従うことと、
$\frac{(n-1)s^2}{\sigma^2}$が自由度$n-1$のカイ二乗分布に従うこと、
そして、この二つが独立であることです。
スチューデントの$t$分布の定義も参照

この二つの値が独立であることって結構不思議な現象だと感じないでしょうか。
同じ標本から算出している値なので何か関係があっても良さそうです。

数式的に計算していって確かめることも可能なのですが、今回は乱数でいっぱい標本をとって、
プロットすることで確かに独立してるっぽいことを眺めて見たいと思います。

標本は$N(7, 2^2)$からとりました。サンプルサイズ$30$で、$500$回標本を取り、
標本平均と不偏分散の相関係数を算出し、散布図にプロットしています。

コードと結果がこちら。


import matplotlib.pyplot as plt
import pandas as pd
from scipy.stats import norm

mu = 7
sigma = 2
sample_size = 30
sample_count = 500

result_list = []
for i in range(sample_count):
    X = norm(loc=mu, scale=sigma).rvs(sample_size)
    mu_ = X.mean()
    var_ = X.var(ddof=1)
    result_list.append({"mu_":  mu_, "var_": var_})

result_df = pd.DataFrame(result_list)
print("相関係数:"+str(np.corrcoef(result_df.mu_, result_df.var_)[0, 1].round(3)))
# 相関係数:-0.021

fig = plt.figure(figsize=(8, 8), facecolor="w")
ax = fig.add_subplot(111)
ax.set_xlabel("標本平均")
ax.set_ylabel("不偏分散")
ax.scatter(result_df.mu_, result_df.var_)
plt.show()

出力された散布図がこちらです。

相関係数は確かに0に近いですし、散布図もいかにも無相関っぽい形になりました。

せっかくなので、各標本ごとに$t$値も算出して、$t$分布と見比べておきましょう。


import numpy as np
from scipy.stats import t

result_df["t_"] = np.sqrt(sample_size-1)*(result_df.mu_ - mu) / result_df.var_**0.5
fig = plt.figure()
ax = fig.add_subplot(111)
ax.hist(result_df["t_"].values, bins=50, density=True)
ax.plot(np.linspace(-4, 4, 101), t(sample_size-1).pdf(np.linspace(-4, 4, 101)))
plt.show()

出力がこちら。

だいたいフィットしてそうですね。