科学技術計算のためのパッケージSciPyについて解説する.

SciPy (http://www.scipy.org/) は様々な科学技術計算のための実装を含むパッケージである.

ここでは,以下のサブパッケージについて解説する.

  • optimize: 非線形最適化と方程式の根
  • spatial: 計算幾何学
  • stats: 確率・統計
  • interpolate: 補間
  • integrate: 積分

最適化

SciPyの最適化サブパッケージoptimizeをとりあげる.

これには,非線形最適化ならびに非線形方程式の根を求めるための解法が含まれる.

1 変数用のminimize_scalar

最初の例として,以下の1変数関数の最小化を考える.

$$f(x)= x/(1+x^2)$$

これは, $x=-1$ のとき最小値 $-1/2$ をとる. 以下の方法で確認しよう.

  • グラフによる確認

  • Brent法

  • 黄金分割法

  • 引数boundsで範囲を与えたBrent法

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt


def f(x):
    return x / (1 + x**2)


x = np.linspace(-10, 10, 100)
y = f(x)
plt.plot(x, y);
from scipy.optimize import minimize_scalar

print("Brent=========================")
print(minimize_scalar(f, method="Brent"))

print("Golden=========================")
print(minimize_scalar(f, method="Golden"))

print("Bounded=========================")
print(minimize_scalar(f, bounds=(-2.0, -1.5), method="Bounded"))
Brent=========================
     fun: -0.5
    nfev: 14
     nit: 9
 success: True
       x: -0.9999999915247861
Golden=========================
     fun: -0.5
    nfev: 45
     nit: 39
 success: True
       x: -0.9999999904634918
Bounded=========================
     fun: -0.4615379352317438
 message: 'Solution found.'
    nfev: 24
  status: 0
 success: True
       x: -1.500004447286288

複数変数用の minimize(f,x0)

minimize(f,x0) は, 複数の変数をもつ関数 $f$ を初期解 $x0$ からの探索で最小化をする.

引数methodで探索のためのアルゴリズムを設定することができる.以下のアルゴリズムが準備されている.

  • "Nelder-Mead":Nelder–Mead法(単体法)

  • "Powell":Powellの共役方向法

  • "CG":共役勾配法(conjugate gradient)

  • "BFGS":Broyden–Fletcher–Goldfarb–Shanno(BFGS)法

  • "Newton-CG":Newton共役勾配法

  • "L-BFGS-B":記憶制限付きBFGS法

  • "TNC":打ち切りNewton共役勾配法

  • "COBYLA":Constrained Optimization BY Linear Approximation (線形近似による解法)

  • "SLSQP":Sequential Least SQuares Programming

  • "dogleg":ドッグレッグ信頼領域法

  • "trust-ncg":信頼領域Newton共役勾配法

その他の引数には, 以下のものがある.

  • jac : 勾配ベクトル(Jacobian)

  • hess : Hesse行列(Hessian)

  • bounds :上下限(限界値)制約

  • constraints: 一般の制約式($=0$ もしくは $\geq 0$) を表す辞書で,以下のキーと値の組をもつ.

    • 制約の種類: 文字列 "type" をキー, 制約の種類を表す文字列 ("eq", "ineq")を値

    • 制約関数: 文字列 "fun"をキー, 制約を表す関数を値

    • 制約の勾配ベクトル(Jacobian): 文字列 "jac"をキー, Jacobianを表す関数を値

制約の例:不等式 $x_0 +2x_1 \geq 10$ を表す制約

{"type": "ineq", "fun": lambda x: x[0] + 2*x[1] -10}

制約なし最適化

制約なし最適化の具体的な例を示す.

Rosenbrock関数

Rosenbrock関数は,以下のように定義されるテスト用の非線形関数である.

$$f(x_1, x_2, \ldots, x_n) = \sum_{i=1}^{n-1} (100(x_i^2 - x_{i+1})^2 + (1-x_i)^2)$$

これは,optimizeサブパッケージに含まれており,以下の関数が提供されている.

  • 関数: rosen
  • Jacovian: rosen_der
  • Hessian: rosen_hess

$x=(1,1,...,1)$ のとき最小値をとる.

2次元のRosenbrock関数をplotlyを用いて描画する.

import plotly.graph_objs as go
from scipy.optimize import rosen

X, Y = np.mgrid[-2:2:0.05, -1:3:0.05]
Z = rosen([X, Y])

fig = go.Figure(go.Surface(x=X, y=Y, z=Z, surfacecolor=np.log(Z + 0.01)))
fig.update_layout(autosize=False, width=800, height=500)
fig.show()

5次元の場合に対して,以下の解法を比較する.

  • 単体法(Nelder-Mead法:勾配情報なし)

  • BFGS(準Newton法)(勾配情報のみ使う)

  • 信頼領域Newton共役勾配法(勾配とHessian)

from scipy.optimize import minimize, rosen, rosen_der, rosen_hess

x0 = [2.0 for i in range(5)]
res = minimize(rosen, x0, method="Nelder-Mead")
print(res.x)

res = minimize(rosen, x0, jac=rosen_der, method="BFGS")
print(res.x)

res = minimize(rosen, x0, jac=rosen_der, hess=rosen_hess, method="trust-ncg")
print(res.x)
[1.00000047 1.00000079 1.0000027  1.00000292 1.00000473]
[0.99999999 0.99999998 0.99999995 0.99999992 0.99999985]
[1.         1.         1.         0.99999999 0.99999993]

勾配の情報や2次導関数の情報 (Hessian)を使うと,誤差が減少することが見てとれる.

Beale関数

Beale関数は,以下のように定義される.

$$ f(x_1, x_2) = \displaystyle\sum_{i=1}^{3} (c_i - x_1 (1- (x_2)^i )^2 $$

定数を $c_1=1.5, c_2=2.25, c_3=2.625$ と設定すると, $x_2=1$ 付近で湾曲し, $x=(3,0.5)$ のとき最小値 $f^* =0$ をとることが知られている.

以下では,次の2つの解法で比較する.

  • 単体法(Nelder-Mead法)

  • BFGS(準Newton法)

c = {1: 1.5, 2: 2.25, 3: 2.625}
X, Y = np.mgrid[-5:5:0.05, -5:5:0.05]
Z = sum((c[i] - X * (1 - Y**i)) ** 2 for i in range(1, 4))
fig = go.Figure(go.Surface(x=X, y=Y, z=Z, surfacecolor=np.log(Z + 0.01)))
fig.update_layout(autosize=False, width=800, height=500)
fig.show()
f  = lambda x: (1.5 - x[0] + x[0]*x[1])**2 + (2.25 - x[0] + x[0]*x[1]**2)**2 + (2.625 - x[0] + x[0]*x[1]**3)**2

x0 = np.array([3., 4.])
res = minimize(f, x0, method="Nelder-Mead")
print(res.x)

res = minimize(f, x0, method="BFGS")
print(res.x)
[2.99998442 0.49999542]
[2.99999973 0.49999993]

Powell関数

Powell関数は以下のように定義される.

$$ f(x_1, x_2, x_3, x_4) = (x_1-10x_2)^2 + 5(x_3-x_4)^2 + (x_2-2x_3)^4+ 10(x_1-x_4)^4$$

これは, $x = (0,0,0,0)$ が最適(最小)解で,最適値は $f^*=0$ となる.

この関数は, 最適解におけるHesse行列が特異であるという特徴をもつ.

f = (
    lambda x: (x[0] - 10 * x[1]) ** 2
    + 5 * (x[2] - x[3]) ** 2
    + (x[1] - 2 * x[2]) ** 4
    + 10 * (x[0] - x[3]) ** 4
)

x0 = np.array([1.0, 1.0, 1.0, 1.0])
res = minimize(f, x0, method="Nelder-Mead")
print(res.x)

res = minimize(f, x0, method="BFGS")
print(res.x)
[-0.00177249 -0.00017725 -0.00096623 -0.00096655]
[-0.00481907 -0.00048192  0.00110423  0.00110421]

上の結果から,単体法(Nelder-Mead法)とBFGS(準Newton法)は,ほぼ同等の精度の解を算出することが分かる.

Ackley関数

Ackley関数は以下のように定義される.

$$f(x,y) = -20\exp\left(-0.2\sqrt{0.5\left(x^{2}+y^{2}\right)}\right)-\exp\left(0.5\left(\cos\left(2\pi x\right)+\cos\left(2\pi y\right)\right)\right) + e + 20$$

ただし, 定義域は $-5 \le x,y \le 5$ である.

この関数は,以下の図に示すように多くの局所的最適解をもち, 最適(最小)解は $x=(0,0)$ で, 最適値は $f^*= 0$ である.

X, Y = np.mgrid[-3:3:0.05, -3:3:0.05]
Z = (
    -20 * np.exp(-0.2 * np.sqrt(0.5 * (X**2 + Y**2)))
    - np.exp(0.5 * (np.cos(2 * np.pi * X) + np.cos(2 * np.pi * Y)))
    + np.exp(1)
    + 20
)
fig = go.Figure(go.Surface(x=X, y=Y, z=Z, surfacecolor=Z))
fig.update_layout(autosize=False, width=800, height=500)
fig.show()
f = (
    lambda x: -20 * np.exp(-0.2 * np.sqrt(0.5 * (x[0] ** 2 + x[1] ** 2)))
    - np.exp(0.5 * (np.cos(2 * np.pi * x[0]) + np.cos(2 * np.pi * x[1])))
    + np.exp(1)
    + 20
)

x0 = np.array([1.0, 1.0])
res = minimize(f, x0, method="Nelder-Mead")
print(res.x)

res = minimize(f, x0, method="BFGS")
print(res.x)
[0.96852082 0.96848094]
[-5.57330182e-09 -5.57330182e-09]

上の結果から,多くの局所的最適解をもつ多峰性関数に対しては, 最適解の近くから探索をしないと, 最適解に到達しない場合があることが分かる.

問題

Ackley関数の初期点 $x0=(x,y)$ を定義域 $-5 \le x,y \le 5$ 内で色々変えて実験を行え.

問題

$f(x,y) = x^2 +xy+y^2$ を最小にする解を共役勾配法(引数methodは"CG")を用いて最小化せよ.

問題

$f(x,y)=\sqrt{1+ x^2+y^2}$をNelder–Mead法(引数methodは"Nelder-Mead")とBFGS法(引数methodは"BFGS")を用いて最小化せよ. また解の精度を比較せよ.

問題

$f(x,y) = x^3 -3xy + y^3$ をBFGS法(引数methodは"BFGS")で最小化せよ. 初期点を色々変えてみて,どのような解が得られるか観察せよ.

X, Y = np.mgrid[-3:3:0.05, -3:3:0.05]
Z = X**3 - 3 * X * Y + Y**3
fig = go.Figure(go.Surface(x=X, y=Y, z=Z, surfacecolor=Z))
fig.update_layout(autosize=False, width=800, height=500)
fig.show()

問題(動的価格付け問題)

あなたはスーパーの店長だ.普段100円で販売している卵の値段を変えて収益を最大化することを考えている. 現在の価格での販売量は1日50個で,1円値下げすると2個余計に売れるようになり,逆に1円値上げすると2個売れないようになると推測されている. さて,おおよそ何円で売れば良いだろうか?

問題(経済発注量問題)

あなたは工場の在庫管理部長だ.いま,ある品目の発注費用が1回あたり1000円で,在庫費用が1日あたり1個あたり1円と推定されている. 1日あたりの需要量を100個としたとき,何日おきに発注すれば良いだろうか?

変数は品目の発注間隔(サイクル時間)であり,それを $x$ とすると,1日あたりの発注費用は $1000/x$, 在庫費用は $ (1 \cdot 100 \cdot x)/2$ と書くことができる.

発注費用と在庫費用の和を最小化する発注間隔を求めよ.ただし,発注間隔は整数でなくても良いものとする.

問題(放物線)

鳥が放物線 $y=x^2 + 10$ を描いて飛んでいる.いま $(10,0)$ の位置にいるカメラマンが,鳥との距離が最小の地点で シャッターを押そうとしている.鳥がどの座標に来たときにシャッターを押せば良いだろうか?

問題(楕円軌道)

彗星が地球に接近している.地球を原点 $(0,0,0)$ としたとき彗星の描く軌道は曲面 $2x^2 + y^2 + z^2 = 1000$ 上にあることが 予測されている.地球に最も接近するときの距離を求めよ.

制約付き最適化

半径 $\sqrt{2}$ の円内の制約の下で $x+y$ を最大化する. 制約付きの場合は,以下の解法を用いる必要がある.

  • COBYLA (線形近似法; Constrained Optimization BY Linear Approximation)

  • SLSQP(逐次最小2乗法; Sequential Least SQuares Programming)

目的関数や制約式は,普通の関数(defで定義)で書いても,lambda関数として書いても良い.以下の例では2通りの書き方で目的関数を書いている (SciPyでの目的関数は最小化なので, $-1$ を乗じてある).

制約は辞書を用いて定義する. 辞書には,以下のキーと値の対を入れる.

  • "type": 制約のタイプを表すキーで, 等式のときは "eq",不等式のときには "ineq"を値に入れる.
  • "fun": 制約を表す関数を入れるためのキーで, 制約の左辺を表す関数を値に入れる. 制約は,左辺が $0$ 以上という不等式制約になる.

以下の例では, $x^2 + y^2 \leq 2$ を $-x^2 -y^2 +2 \geq 0$ に直してから入れている.

その後に,minimmize関数で最小化するが,制約が複数ある場合には,制約(辞書)のリストを constraints引数に入れる.

from scipy.optimize import minimize

def f(x):
    return -x[0] - x[1]


bnds = ((0, None), (0, None))  # 上下限制約の入れ方
f = lambda x: -x[0] - x[1]
con = {"type": "ineq", "fun": lambda x: -x[0] ** 2 - x[1] ** 2 + 2}
res = minimize(fun=f, x0=[2, 2], method="COBYLA", constraints=con)
print("COBYLA========= \n", res)
res = minimize(fun=f, x0=[2, 2], method="SLSQP", constraints=con)
print("SLSQP========== \n", res)
COBYLA========= 
      fun: -2.000000006134913
   maxcv: 2.0000961775679116e-08
 message: 'Optimization terminated successfully.'
    nfev: 47
  status: 1
 success: True
       x: array([1.00006218, 0.99993783])
SLSQP========== 
      fun: -2.000000092931198
     jac: array([-1., -1.])
 message: 'Optimization terminated successfully'
    nfev: 15
     nit: 5
    njev: 5
  status: 0
 success: True
       x: array([1.00000005, 1.00000005])

問題(体積)

断面の面積の和が $100$ cm${}^2$ の直方体で,体積最大のものは何か? また体積最小のものは何か?

問題(制約付きWeber問題)

以下のような位置と人数をもった7件の家がある.

x座標 y座標 人数
1 24 54 2
2 60 63 1
3 1 84 2
4 23 100 3
5 84 48 4
6 15 64 5
7 52 74 4

みんなで出資して新しく井戸を掘ることになったが, 話し合いの末 「各家が水を運ぶ距離 $\times$ 各家の水の消費量」の総和が最小になる場所に 井戸を掘ることにした. ただし, 各家の水の消費量は人数に比例するものとする.

  1. どこを掘っても水が出るものとしたとき,どのようにして掘る場所を決めれば良いだろうか?

  2. $(50,50)$ から半径 $40$ の円状の領域が砂漠になっていて,そこには井戸が掘れないときにはどうしたら良いだろうか?

家の集合を $H$,家 $i (\in H)$ の位置を$(x_i, y_i)$ とする. 井戸の位置を $(X, Y)$ とすれば,家 $i$ から井戸までの距離は $$ \sqrt{(x_i - X)^2 + (y_i - Y)^2} $$ である.家 $i$ が $1$日に必要とする水の量(人数)を $w_i$ としたとき, $$ \sum_{i \in H} w_i \sqrt{(x_i-X)^2 + (y_i-Y)^2} $$ を最小にする $(X, Y)$ を求めよ.

定式化: $$ \begin{array}{ll} minimize & \sum_{i \in H} w_i \sqrt{(x_i-X)^2 + (y_i-Y)^2} \\ s.t. & (50-X)^2 +(50-Y)^2 \geq 40^2 \end{array} $$

x = [24, 60, 1, 23, 84, 15, 62]
y = [54, 63, 84, 100, 48, 64, 74]
w = [2, 1, 2, 3, 4, 5, 4]
import pandas as pd
import plotly.express as px

df = pd.DataFrame({"x": x, "y": y, "w": w})
fig = px.scatter(df, x="x", y="y", size="w", text=df.index)
fig.update_layout(autosize=False, width=600, height=500)
fig.show()

問題(ポートフォリオ最適化)

100万円のお金を5つの株に分散投資したいと考えている. 株の価格は,現在はすべて $1$株あたり $1$円だが, 証券アナリストの報告によると,それらの株の $1$年後の価格と標準偏差はそれぞれ 以下の表のように確率的に変動すると予測されている.

株式 1 2 3 4 5
期待値 ($r_i$) 1.01 1.05 1.08 1.10 1.20
標準偏差 ($\sigma$) 0.07 0.09 0.1 0.2 0.3

目的は$1$年後の資産価値を最大化することである. しかしながら,よく知られているように,1つの株式に集中投資するのは 危険であり,大損をすることがある. 期待利回りを $\alpha$%以上としたとき,標準偏差の2乗和を最小にするように投資するにはどうすれば良いだろうか?

ポートフォリオ最適化(定式化):

$$ \begin{array}{lll} minimize & \sum_{i=1}^n \sigma_i^2 x_i^2 & \\ s.t. & \sum_{i=1}^n r_i x_i \geq \alpha & \\ & \sum_{i=1}^n x_i = 1 & \\ & x_i \geq 0 & \forall i=1, 2, \ldots, n \end{array} $$

問題(交通量割り当て問題)

4 本の道への交通量配分を考える. 5000台の車が移動する総時間を最小化するには,どうすれば良いだろうか?

ここで,道を通過する台数と移動時間は,以下の関係があるものとする.

$$移動時間 = 基本移動時間 \times \left(1+ \left(\frac{台数}{交通容量}\right)^4 \right)$$

基本移動時間 交通容量
1 15 1000
2 20 2000
3 30 3000
4 35 4000

問題(学区割り当て問題) (難)

2 つの学校(定員100人)を作る.このとき, 学生たちの歩く距離の合計を最小したい. 新しい学校の位置と学生の学校への最適な割り当てを求めよ.

学区 x座標 $x_i$ y座標 $y_i$ 人数 $n_i$
1 0 0 40
2 0 100 40
3 100 0 40
4 100 100 40
5 50 50 40

学区割り当て問題(定式化)

$$ \begin{array}{l l l} minimize & \sum_{i \in I} \sum_{j=0}^1 z_{ij} \sqrt{ (x_i-X_j)^2+(y_i-Y_j)^2 } & \\ s.t. & \sum_{i \in I} z_{ij} \leq 100 & \forall j=0,1 \\ & \sum_{j=0}^1 z_{ij} = n_i & \forall i \in I \\ & z_{ij} \geq 0 & \forall i \in I; j=0,1 \end{array} $$

この問題は,非凸関数の非線形最適化問題であり, 適切な初期解を与えないと妥当な解が得られない可能性がある.

問題(複数品目の経済発注量問題)

古典的な経済発注量モデルの複数品目への拡張(容量制約付き)を考える.

  • $I$ : 品目の集合
  • $w_i$ : 品目 $i~(\in I)$ の大きさ
  • $W$ : 倉庫に置ける量の上限
  • $F_i$ : 品目 $i$ の発注費用
  • $h_i$ : 品目 $i$ の在庫保管費用
  • $d_i$ : 品目 $i$ 需要量

容量制約を破らない条件の下で,総費用が最小になる発注方策を求める.

複数品目の経済発注量問題は, 以下の変数を用いて定式化できる.

$T_i$:品目 $i$ の発注間隔(発注頻度の逆数)

$$ \begin{array}{l l l} minimize &\sum_{i \in I} \frac{F_i}{T_i} + \frac{h_i d_i T_i}{2} & \\ s.t. & \sum_{i \in I} w_i d_i T_i \leq W & \\ & T_i > 0 & \forall i \in I \end{array} $$

数値は自分で適当に定めて解け.

曲線へのあてはめ(最小自乗法)

関数 curve_fit(f,x,y) は, 配列 $x,y$ で与えたデータに対して誤差の最小2 乗和を最小にするような関数 $f$ のパラメータを求める.

返値は,以下のタプルである.

  • 最小自乗和を最小にするような推定パラメータを表す配列

  • 共分散行列を表す2次元配列

例として, ランダムな誤差を加えた2次関数 $a x^2+ bx+c$ へのあてはめを考える.

from scipy.optimize import curve_fit


def f(x, a, b, c):
    return a * x**2 + b * x + c


xdata = np.linspace(-10, 10, 20)
ydata = f(xdata, 1, 1, 1) + np.random.normal(size=len(xdata))
param, cov = curve_fit(f, xdata, ydata)
print(param)
[0.99747056 1.01099454 0.85088764]
plt.plot(xdata, ydata, "ro")
plt.plot(xdata, f(xdata, param[0], param[1], param[2]));

問題(コーヒーショップの売上)

あなたはコーヒーショップを開店した.オープン当初の5日間の売上は, $15,20,34,42,58$ と右肩上がりである.1日あたりどのくらい増えているかを 線形関数 $y=ax+b$ へ当てはめることによって予測せよ.

1変数関数の根

$x^2 -4 \sin x = 0$ の解($x^2 -4 \sin x$ の根)を求めることを考える. この関数は, $x=0$ だけでなく $x=1.9$ 付近でも根をもつ.

scipy.optimizeのrootもしくはbrentq関数を用いる.

$f(x)=0$ の解を出すためには, rootは引数として関数 $f$ と初期値 $x0$ を与え, brentqは関数 $f$ と範囲 $a$ と $b$ を与える.

def f(x):
    return x**2 - 4 * np.sin(x)

x = np.linspace(-1, 3, 100)
plt.plot(x, np.linspace(0, 0, 100))  # x=0
y = f(x)
plt.plot(x, y);
from scipy.optimize import root, brentq

print("Root finding ======\n ", root(f, x0=2.5))
print("Brentq=", brentq(f, a=1.0, b=3.0))
Root finding ======
      fjac: array([[-1.]])
     fun: array([1.84741111e-13])
 message: 'The solution converged.'
    nfev: 8
     qtf: array([-3.12600879e-08])
       r: array([-5.2877011])
  status: 1
 success: True
       x: array([1.93375376])
Brentq= 1.9337537628270214

問題(4次方程式の根)

$x^4 + x^3 -7x^2-x+6 =0$ の根をすべて求めよ.

ちなみに根と図は以下のサイトで x^4 + x^3 -7x^2-x+6 =0 と入力することによって確認できる.

https://www.wolframalpha.com/

計算幾何学

ここでは,計算幾何とデータ構造に関するサブパッケージ spatial をとりあげる.

計算幾何学 (computational geometry)とは,幾何的問題を効率よく解くための基本アルゴリズムの体系化をめざす学問分野である. 1970年代の中頃に生まれたこの分野は, その有用性のためその後急速に発展している.

以下の実務上有用なサブパッケージを紹介する.

  • $K$ 次元木 KDTree

  • 凸包 ConvexHull

  • Delaunay 3角形分割 Delaunay

  • Voronoi図 Voronoi

  • 距離計算のためのサブパッケージ distance

$K$-d木

KDTreeは$K$次元の点データに対する$K$-d木とよばれるデータ構造のクラスである. ここで$K$-d木($K$-dimensional tree)とは, $K$次元のEuclid空間にある点を保管するための空間分割データ構造である. $K$-d木を用いることによって,範囲探索や最近点探索などを高速に行うことができる.

$K$-d木は空間の情報を2分木として保管している. ここで2分木とは,(root)とよばれる点(これは与えた点全体を表す)を2つの子点(child node) に分割する操作を繰り返すことによって生成されるデータ構造である.

2次元を例にすると,$K$-d木は以下のように構築される. 点集合を含む領域(長方形)を $x,y$軸のいずれか長い方の中央値で分割することによって2つの長方形を生成し,これらを子点とする. 子点には長方形に含まれる点を保持する. 各子点に対して同様の操作を繰り返し,長方形に含まれる点の数が一定数以下になったら,終了する. 子点をもたない点を葉(leaf)とよぶ.

コンストラクタの引数は,以下の通り.

  • points: NumPyの形状 (npoints, ndim) をもつ浮動小数点数の配列.ここで npoints は点数, ndim は次元数を表す.

  • leafsize: 葉(子をもたない点)に含まれる点数を表す自然数

メソッド query(x) は,点(もしくは配列) $x$ に含まれる各点からの最近点探索を行う. ここで $x$ (の各要素)は,$K$-d木と同じ次元をもつ点の座標を表す配列とする. 返値は点 $x$ から近い順に並べ替えられた距離(の配列)と近い点の位置(を表す配列)である. オプションで与えることができる引数は以下の通り.

  • $k$: 近い点の数を表すパラメータであり,自然数を与える.既定値は $1$ である.
  • $p$: 距離を計算する際のノルムを表すパラメータである.

$n$ 次元の点 $x,y$ 間のMinkowski $p$-ノルムは以下のように計算される. $$ \left( \sum_{i=1}^n |x_i -y_i|^p \right)^{1/p} $$

$p$ は $1$ 以上の浮動小数点数を与え,$1$ のときは $L_1$(マンハッタン)ノルム,$2$ のときはEuclidノルム, 無限大のときは $L_{\infty}$ノルムになる.既定値は $2$ である.

例として, $[0,1]^2$ にランダムに発生させた $1000$個の2次元の点に対して, $(0.1,0.1)$ と $(0.5,0.5)$ から近い順に10個の点を表示する.

%matplotlib inline
from scipy.spatial import KDTree
import numpy as np
import matplotlib.pyplot as plt

points = np.random.rand(1000, 2)
tree = KDTree(points)
dis, near = tree.query(x=[[0.1, 0.1], [0.5, 0.5]], k=10)
print(dis)  # 各点から近い点への距離(近い順)
plt.plot(points[:, 0], points[:, 1], "b+")
plt.plot(points[near, 0], points[near, 1], "ro");
[[0.00300927 0.00890621 0.01084919 0.0122167  0.02220156 0.02856137
  0.03270805 0.0330415  0.03786016 0.0413629 ]
 [0.02218606 0.03033958 0.03055716 0.03091861 0.03451831 0.03561243
  0.03631823 0.04584094 0.04612901 0.04826072]]

メソッド query_ball_point(x, r) は点(もしくは配列) $x$ から距離 $r$ 以内のすべての点のリスト(もしくはリストの配列)を返す.

その他の引数はqueryと同じである.

$[0,1]^2$ にランダムに発生させた $1000$ 個の2次元の点に対して, 中心 $(0.5,0.5)$ から $L_1$ノルムでの距離 $0.3$ 以内の点を表示を行う.

points = np.random.rand(1000, 2)
tree = KDTree(points)
ball = tree.query_ball_point(x=[0.5, 0.5], r=0.3, p=1)
plt.plot(points[:, 0], points[:, 1], "b+")
plt.plot(points[ball, 0], points[ball, 1], "ro");

凸包クラス ConvexHull


凸包 (convex heull) とは,計算幾何学の基本となる概念であり,与えられたEuclid空間内の点を含む最小の凸多面体を指す.

凸包クラス ConvexHull のコンストラクタの引数は,以下のpoints配列である.

  • points: NumPyの形状 (npoints, ndim) をもつ浮動小数点数の配列.ここで npointsは点数,ndimは次元数である.

凸包クラスConvexHullは以下の属性をもつ.

  • points: 入力された点の座標を表す配列である.
  • vertices: 凸包を構成する点を表す配列である.2次元の場合には,反時計回りの順に保管されるが,3次元以上の場合には任意の順になる.
  • simplices: 凸包を構成する面(単体)を保持する配列である. 面の数の配列であり,配列の各要素は点の次元の長さの配列である.
  • neighbors: 凸包を構成する面(単体)の隣接関係を表す配列である.配列の各要素は隣接する面の番号の配列である.

また,2次元の凸包を描画するための関数convex_hull_plot_2dも準備されている. これは凸包インスタンスを引数としmatplotlibの図を返す.

from scipy.spatial import ConvexHull, convex_hull_plot_2d

points = np.random.rand(300, 2)
hull = ConvexHull(points)
convex_hull_plot_2d(hull)
print(hull.vertices)
[254 152  23  56  89  40  14 167 189 236  28 113 204 228  57 230]

Delaunay 3角形分割クラス Delaunay

Delaunay3角形分割 (Delaunay triangulation) とは, (2次元のときは)領域を点を通る3角形に分割したとき,どの3角形の頂点を通る円も,他の点を含まない3角形分割を指し. $K$-d木と同様に,近い点同士を見つけるのに便利であり,次に説明するVoronoi図と双対の関係にある.

Delaunay 3角形分割クラス Delaunay のコンストラクタの引数は,凸包と同じで点の配列である.

  • points: NumPyの形状 (npoints, ndim) をもつ浮動小数点数の配列. ここで npointsは点数,ndimは次元数である.

Delaunay3角形分割クラスDelaunayは以下の属性をもつ.

  • points: 入力された点の座標を表す配列である.
  • simplices: Delaunay3角形分割に含まれる単体(2次元のときは3角形)を保持する配列である. 単体の数の配列であり,各要素は次元 $+1$ の長さの配列に単体の頂点番号が保管される.
  • neighbors: Delaunay3角形分割に含まれる単体の隣接関係を表す配列である.配列の各要素は, 単体内の各点の反対側に隣接する単体の番号の配列である.対応する単体がない場合には $-1$ が保管される.

また,2次元のDelaunay3角形分割を描画するための関数delaunay_plot_2dも準備されている. これはDelaunay3角形分割インスタンスを引数としmatplotlibの図を返す.

from scipy.spatial import Delaunay, delaunay_plot_2d

np.random.seed(5)
points = np.random.rand(8, 2)
tri = Delaunay(points, furthest_site=False)
fig = delaunay_plot_2d(tri)

Voronoi図クラス Voronoi

Voronoi図 (Voronoi diagram)とは, 各点に近い空間で領域分けされた図であり,Delaunay3角形分割の双対グラフである.

与えられた点を母点,各母点に近い空間から成る領域をVoronoi領域. Voronoi領域の境界をVoronoi境界,Voronoi境界の交点をVoronoi点とよぶ.

コンストラクタの引数は,以下の母点の情報である.

  • points: NumPyの形状 (npoints, ndim) をもつ浮動小数点数の配列. ここで npointsは点数,ndimは次元数である,

Voronoi図クラスVoronoiは以下の属性をもつ.

  • points: 入力された点の座標を表す配列である.
  • vertices: Voronoi点の座標を表す配列である.
  • ridge_points: Voronoi境界を垂直に横切る母点対を表す. 実は,これはDelaunay3角形分割の枝(辺)に対応し,これがVoronoi図とDelaunay3角形分割が互いに双対といわれる所以である.
  • ridge_vertices: Voronoi境界を表す点対である. Voronoi点の番号,もしくは点がVoronoi図の外側にある場合には $-1$ を保管する.
  • regions: Voronoi領域を表す点の配列である.点がVoronoi図の外側にあるときには $-1$ を保管する.
  • point_region: 各母点が含まれるVoronoi領域の番号を表す.

また,2次元のVoronoi図を描画するための関数voronoi_plot_2dも準備されている. これはVoronoi図インスタンスを引数としmatplotlibの図を返す.

例として格子上に配置した点に対するVoronoi図を求め,それを描画する.

同時に,対応するDelaunay3角形分割も示す.

from scipy.spatial import Voronoi, voronoi_plot_2d

points = np.array(
    [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2]]
)
vor = Voronoi(points)
fig = voronoi_plot_2d(vor)
print("vertices=", vor.vertices)
print("reige_points", vor.ridge_points)
vertices= [[0.5 0.5]
 [0.5 1.5]
 [1.5 0.5]
 [1.5 1.5]]
reige_points [[0 3]
 [0 1]
 [2 5]
 [2 1]
 [1 4]
 [7 8]
 [7 6]
 [7 4]
 [8 5]
 [6 3]
 [4 5]
 [4 3]]
tri = Delaunay(points)
fig = delaunay_plot_2d(tri)

Delaunay3角形分割の例題のVoronoi図を計算し,対応するDelaunay3角形分割と重ねて描画する.

np.random.seed(5)
points = np.random.rand(8, 2)
fig, ax = plt.subplots()
vor = Voronoi(points)
voronoi_plot_2d(vor, ax, show_vertices=False)
tri = Delaunay(points)
delaunay_plot_2d(tri, ax);

距離計算のためのサブパッケージdistance

距離計算のためのサブパッケージdistanceには,以下の関数が含まれている.

  • euclidean(u,v): $u$ と $v$ の間のEuclid距離(直線距離)

    $\|u-v\|_2 = \left(\sum_{i} (u_i-v_i)^2 \right)^{1/2}$

  • chebyshev(u,v): $u$ と $v$ の間のChebyshev距離

    $(L_{\infty}$ ノルム) $\max_{i} |u_i -v_i|$

  • cityblock: $u$ と $v$ の間のManhattan距離($L_{1}$ ノルム)

    $\sum_{i} |u_i-v_i|$

  • minkowski(u,v,p): $u$ と $v$ の間のMinkowski距離((Minkowski $p$-ノルム)

    $\| u-v \|_p =\left(\sum_{i} |u_i-v_i|^p \right)^{1/p}$

例題:最近点

2次元Euclid平面上にランダムに分布した10000個の点に対する最も近い点を求める. 最も近い点は,点を母点としたVoronoi図の隣接する領域の母点であることを利用する. さらに,計算速度を $K$-d木と比較する.

from scipy.spatial.distance import euclidean


def NN_KD(points):
    tree = KDTree(points)
    near = {}
    total_cost = 0.0
    for i, p in enumerate(points):
        dis, near_list = tree.query(p, k=2)
        near[i] = near_list[1]


def NN_Voronoi(points):
    vor = Voronoi(points)
    dis, near = {}, {}
    for i in range(len(points)):
        dis[i] = np.inf
    for (i, j) in vor.ridge_points:
        d = euclidean(points[i], points[j])
        if d < dis[i]:
            dis[i] = d
            near[i] = j
        if d < dis[j]:
            dis[j] = d
            near[j] = i


n = 10000
points = np.random.rand(n, 2)
%time NN_KD(points)
%time NN_Voronoi(points)
CPU times: user 981 ms, sys: 9.92 ms, total: 990 ms
Wall time: 1.03 s
CPU times: user 556 ms, sys: 4.54 ms, total: 560 ms
Wall time: 560 ms

例題:Euclid最小木問題

点間の距離はEuclid距離の最小木問題を考える.ここで最小木とは,閉路を含まない連結グラフで,距離の合計が最小のものである. Euclid最小木問題の最適解に含まれる枝は,必ずVoronoi図の隣接する領域間の母点対の枝集合に含まれるという性質を利用すると,高速に解ける. Voronoi図の隣接する領域間の母点対は ridge_points を用いて列挙する. また,最小木を求めるアルゴリズムは,後述するNetworkXパッケージを用いる.

すべての枝をグラフに追加して最小木を求めたときと,Voronoi図を用いて必要な枝だけを追加して高速化した場合を比較する.

import networkx as nx


def MST(points):
    G = nx.Graph()
    for i, p in enumerate(points):
        for j, q in enumerate(points):
            if i < j:
                G.add_edge(i, j, weight=euclidean(p, q))
    E = nx.minimum_spanning_edges(G)


def MST_Voronoi(points):
    G = nx.Graph()
    vor = Voronoi(points)
    for (i, j) in vor.ridge_points:
        G.add_edge(i, j, weight=euclidean(points[i], points[j]))
    E = nx.minimum_spanning_edges(G)
    return G, E


n = 200
points = np.random.rand(n, 2)

%time MST(points)
%time MST_Voronoi(points)

G, E = MST_Voronoi(points)
pos = {i: (points[i][0], points[i][1]) for i in range(n)}
nx.draw(G, pos=pos, node_size=10, edgelist=list(E))
CPU times: user 164 ms, sys: 649 µs, total: 164 ms
Wall time: 164 ms
CPU times: user 7.19 ms, sys: 37 µs, total: 7.23 ms
Wall time: 7.02 ms

問題: 色々な距離

2次元座標空間上の点[0,0] と点 [1,1] の間の距離を求めよ.

  1. Euclid距離(直線距離)

  2. Chebyshev距離 $(L_{\infty}$ ノルム)

  3. Manhattan距離($L_{1}$ ノルム)

  4. $p=3$ のMinkowski距離((Minkowski $p$-ノルム)

問題:$K$-d木

日本の都道府県の県庁所在地の経度・緯度のリストが以下のように与えられている. これを2次元平面上の座標と考えて$K$-d木を構築し,仙台市 (140.87194,38.26889) に直線距離で近い点の番号を順に3つ求めよ.

lon = [141.34694, 140.74, 141.1525, 140.87194, 140.1025, 140.36333, 140.46778, 140.44666999999998, 139.88361, 139.06083, 139.64889,
        140.12333, 139.69167, 139.6425, 139.02361000000002, 137.21139, 136.62556, 136.22194, 138.56833, 138.18111000000002, 136.72222,
        138.38306, 136.90667, 136.50861, 135.86833, 135.75556, 135.52, 135.18306, 135.83278, 135.1675, 134.23833, 133.05056000000002,
        133.935, 132.45944, 131.47139, 134.55944, 134.04333, 132.76611, 133.53111, 130.41806, 130.29889, 129.87361, 130.74167, 131.6125, 131.42389, 130.55806, 127.68111]
lat = [43.06417, 40.82444, 39.70361, 38.26889, 39.71861, 38.24056, 37.75, 36.34139, 36.56583, 36.39111, 35.85694, 35.60472, 35.68944000000001,
       35.44778, 37.90222, 36.69528, 36.59444000000001, 36.06528, 35.66389, 36.65139, 35.39111, 34.97694, 35.18028, 34.73028, 35.00444,
       35.021390000000004, 34.68639, 34.69139000000001, 34.68528, 34.22611, 35.50360999999999, 35.47222, 34.66167, 34.396390000000004,
       34.18583, 34.06583, 34.34028, 33.84167, 33.55972, 33.606390000000005, 33.24944, 32.74472, 32.78972, 33.23806, 31.91111, 31.56028, 26.2125]

問題: 凸包

上の日本の都道府県の県庁所在地の経度・緯度のリストに対して, 凸包を構築せよ. 凸包上にある(すなわち日本の都道府県を包むような)点の番号を求めよ.

問題: Voronoi図

上の日本の都道府県の県庁所在地の経度・緯度のリストに対して, Voronoi図を描画せよ. この図の領域は何を意味するか考察せよ.

問題: Euclid最小費用完全マッチング (難)

2次元平面 $[0,1]^2$ 上にランダムに分布した偶数個の点に対する最小費用の完全マッチング (すべての点の次数が $1$ の部分グラフ)を求める問題を考える. ただし枝の費用は点間のEuclid距離とする.このような問題をEuclid最小費用マッチング問題とよぶ. $K$-d木を用いて,各点に対して近い順に $10$個の点の間にだけ枝をはった疎なグラフを作成し, それ上で最小費用マッチングを求める近似解法を考える. $100$点のEuclid最小費用マッチング問題をランダムに生成し, 疎なグラフ上で求めた近似マッチングと, すべての点間の枝を用いて最適解を求めた場合の費用と計算時間を測定せよ (ヒント: 最小費用マッチング問題の求解には,後述するNetworkXの関数max_weight_matchingを用いることができる. この関数は負の重みには対応していない. したがって,費用の合計を最小化するためには,大きな数(たとえば枝の費用の最大値)から枝の費用を減じた値を新たに枝の費用と 定義して最大化を行う必要がある).

問題:Euclid最短路 (難)

$(0,0)$ の地点から $(1,1)$ の地点にロボットが移動する経路を考える. ロボットは平面上のどの地点でも通過できるようにプログラムできるが, 平面 $[0,1]^2$ 上にランダムに配置された障害物を表す点の内側の領域は通過することができない. ランダムに $100$個の障害物を配置し,その外側を通過する最短距離を求めよ(ヒント:$(0,0)$ と $(1,1)$ を加えた点に対する凸包を用いる).

確率・統計

ここでは,確率・統計に関するサブパッケージ stats をとりあげ,以下について解説する.

  • 確率分布の基礎
  • 共通のメソッド
  • 連続確率変数
  • 離散確率変数
  • データのあてはめ
  • 仮説検定
  • 分布テスト
  • 信頼区間

確率分布の基礎

例として平均 $100$,標準偏差 $10$ の正規分布 $N(100,10^2)$ の統計量を分布に付随するメソッドstatで表示してみる. 引数のmoments="mvsk"は,平均(mean),分散(variance),歪度(skewness),尖度(kurtosis)を意味する.

正規分布インスタンスを生成するnormは,位置パラメータlocで平均を,尺度パラメータscaleで標準偏差を与える.

密度関数 $f(x)$ をもつ連続な確率変数に対して, $X$ の期待値(平均)を $$ E[X]= \int_{-\infty}^{\infty} x f(x) dx $$ と定義する.

確率変数 $X$ と正数 $k$ に対する $k$次モーメント(moment)は,$m=E[X]$ としたとき, $E[ (X-m)^k ]$ で定義される.

分散(variance) $Var(X)$ は,2次モーメント $E[ (X-m)^2]$ と定義される. 分散は分布のばらつきを表す尺度である. $\sqrt{Var(X)}$ を標準偏差(standard deviation)とよび $\sigma$ と記す.

歪度(skewness)は $E[ (X-m)^3]/\sigma^3$,尖度(kurtosis)は $E[ (X-m)^4]/\sigma^4$ で定義され, それぞれ分布の非対称性ならびに尖りを表す尺度である.

正規分布の歪度は $0$ であり左右対称である.歪度が正の分布は右側の裾野が長くなり,逆に負の分布は左側の裾野が長くなる.

正規分布の尖度は $0$($3$とする場合もあるが,SciPyでは$0$と定義されている)であり, 尖度が正の分布は,正規分布と比べて山の頂上付近で尖って,裾野が長い形状であり, 尖度が負の分布は,頂上付近が丸く裾野が短い形状をもつ.

from scipy.stats import norm

print(norm(loc=100, scale=10).stats(moments="mvsk"))
(array(100.), array(100.), array(0.), array(0.))

通常は,パラメータを固定した分布インスタンスを生成し, 生成した固定分布に対して,メソッドを用いて計算する. これを分布の 固定化 (freezing)とよぶ,

たとえば,パラメータ(平均100,標準偏差100)を固定した正規分布インスタンス n を生成した後で,分布関数メソッド cdf を用いて 確率変数が $80$ 以下になる確率を求めるコードは,以下のようになる.

n = norm(loc=100, scale=100)
print(n.cdf(80))
0.42074029056089696

分布によっては, (正規分布生成で用いた位置パラメータlocと尺度パラメータscaleの他に) 形状 (shape)パラメータが必要になる.

例として,ガンマ分布を生成する. この分布は, 正のパラメータ $\lambda ,a$ て定義され, 平均 $a/\lambda$,分散 $a/\lambda^2$ をもつ.

パラメータ $a$ が形状パラメータである. 形状パラメータは分布によって名前が異なるが, 形状パラメータは必ず第1引数なので,名前を省略して入力する. また, 尺度パラメータ scale は $1/\lambda$ に対応する.

$a=1, \gamma =1/10$ のガンマ分布を生成して,平均と分散を計算する.

from scipy.stats import gamma

g = gamma(1, scale=10)  # a=1 (shape). 1/gamma=10 (scale)
print(g.stats(moments="mv"))  # mean =a/gamma, variance =1/gamma^2
(array(10.), array(100.))

共通メソッド

rvs: 確率変数にしたがう擬似乱数を生成するメソッド

引数 size は,生成する乱数の数(タプルを引数としたときは多次元配列)である.

from scipy.stats import norm

n = norm(loc=100, scale=10)
print("array=", n.rvs(size=5))
print("matrix=", n.rvs(size=(2, 3)))
array= [101.84651023 129.17878502 103.55253976 114.27602703  87.39188029]
matrix= [[ 95.0525987  110.6083437   76.65235316]
 [ 87.21302208  96.00269644  79.62090203]]

pdf: 連続確率変数の密度関数(probability density function)

任意の区間 $[a,b]$ に対して,確率変数 $X$ が区間内に入る確率 $P(X \in [a,b])$ が,

$$ P(X \in [a,b])=\int_{a}^{b} f(x) dx $$

と表されるとき,$f(x)$ を密度関数とよぶ. 密度関数で定義される連続な確率変数が $1$点をとる確率は $0$ である. SciPyにおいては,pdf(a)は $P(X \in [a,a+1])$ を返す.

pmf: 離散確率変数の確率関数(probability mass function)

確率関数 $p(x)$ は確率変数 $X$ が $x$ になる確率 $P(X=x)$ を表す.

cdf: 分布関数(cumulative distribution function)

(離散,連続)確率変数 $X$ の分布関数 $F(x)$ は $$ F(x) =P(X \leq x) $$ と定義される.

ppf: パーセント点関数 (percent point function) (分布関数の逆関数)

例として平均 $100$,標準偏差 $10$ の正規分布にしたがう需要をもつ商品に対して品切れ率を $5$% にするための在庫量を求める.

from scipy.stats import norm

print(norm.ppf(0.95, loc=100, scale=10))
116.44853626951472

sf: 生存関数(survival function) ($1-F(x)$)

(離散,連続)確率変数 $X$ の生存関数は $$ P(X > x) $$ と定義される.

分布関数が与えた値以下の確率を返すのに対して,生存関数は与えた値より大きい確率を返す. (離散確率変数の場合には,「以上」でなく「より大きい」確率であることに注意する必要がある.)

例として平均 $100$,標準偏差 $10$ の正規分布にしたがう需要をもつ商品に対して, $123$ 個仕入れたときの品切れ確率を求める.

from scipy.stats import norm

print(norm.sf(123, loc=100, scale=10))
0.010724110021675809

例題: 確率変数に対するメソッド

  1. 平均24,標準偏差8の正規分布にしたがう確率変数$X$が, $k=30$ 以下になる確率を求めよ.

  2. 同じ分布に対して,90%の確率で $k$以下になる $k$ を求めよ.

  3. 同じ分布に対して,$k=30$ 以上になる確率を求めよ.

  4. 同じ分布に対して,10%の確率で $k$以上になる $k$ を求めよ.

  5. 同じ分布に対して,20以上,28以下になる確率を求めよ.

  6. 同じ分布に対して, $[24-L,24+L]$ の間に入る確率が90%になるような $L$ を求めよ.

n = norm(loc=24, scale=8)
print(n.cdf(30))
print(n.ppf(0.9))
print(n.sf(30))  # もしくは 1- n.cdf(30)
print(n.ppf(1 - 0.1))  # 2と同じ
print(n.cdf(28) - n.cdf(20))  # 28以下の確率から20以下の確率を引く
print(24 - n.ppf(0.05))  # 確率が0.05(10%の半分)になるkを求め,平均24から引く
0.7733726476231317
34.2524125243568
0.2266273523768682
34.2524125243568
0.38292492254802624
13.158829015611783

例題: 関数funcの分布に対する期待値 expect(func)

標準正規分布の人口密度をもつ直線上の町に住む人たちが,町の中心にある町役場まで歩いて行くときの平均距離を求めてみよう. これは,標準正規分布に対して,引数を絶対値を表す関数として与えたときの期待値になる.

print(norm.expect(lambda x: abs(x)))
0.7978845608028651

例題: 損出関数

在庫理論では,品切れ量を計算するために確率変数 $x$ が $k$以上になるときの期待値を 用いる.これは損出関数(loss function)とよばれ,以下のように定義される. $$ G(k)= \int_{k}^{\infty} (x-k) f(x) dx $$

たとえば,$k=0.1$ のときの標準正規分布の損出関数は, 期待値の計算する際の下限lbを $0.1$ としたときの $x-k$ の期待値として以下のように計算される.

損出関数は,密度関数 $f(x)$ と生存関数 $1-F(x)$ を用いて,解析的に $G(k)= f(k) -k(1-F(k))$ と計算できる.

以下のコードから, 結果が一致していることが確認できる.

k = 0.1
print(norm.expect(lambda x: x - k, lb=k))
print(norm.pdf(k) - k * norm.sf(k))
0.35093533120471415
0.3509353312047147

問題: 在庫

平均 $100$,標準偏差 $10$ の正規分布にしたがう需要をもつ商品に対して,$120$個仕入れたとき, 品切れ費用が 1個あたり $100$円,在庫費用が 1個あたり $10$円としたときの期待費用を求めよ.

代表的な連続確率変数

uniform: 一様分布(uniform distribution)

$[\alpha,\beta]$ 上で定義された一様分布は密度関数 $1/(\beta-\alpha)$ をもつ. (SciPyでは離散な一様分布randintも使うことができる.)

位置パラメータlocは下限値 $\alpha$ を表し, 尺度パラメータscaleは上限値と下限値の差 $\beta-\alpha$ を表す. 平均は $(\beta-\alpha)/2$,分散は $(\beta-\alpha)^2 /12$ である.

from scipy.stats import uniform

u = uniform(loc=0, scale=10)
print("平均と分散 ", u.stats(moments="mv"))

x = np.linspace(-3, 15, 100)
y = u.pdf(x)
plt.plot(x, y)
平均と分散  (array(5.), array(8.33333333))

expon: 指数分布(exponential distribution)

指数分布とは,正のパラメータ $\lambda$ に対して,密度関数が $$ f(x) = \lambda e^{-\lambda x} \quad \forall x \geq 0 $$ で与えられる分布である.引数として指定する尺度パラメータscaleは $1/\lambda$ である. 分布関数は $$ F(x) = 1 - e^{-\lambda x} \quad \forall x \geq 0 $$ となる.平均は $1/\lambda$,分散は $1/\lambda^2$ である.

指数分布は無記憶性(memoryless property)をもつ唯一の連続分布 (離散分布では,後述する幾何分布が無記憶性をもつ) として知られ, 稀な現象の時間間隔として用いられる

from scipy.stats import expon

ex = expon(scale=10)
print("平均と分散 ", ex.stats(moments="mv"))

x = np.linspace(-3, 100, 100)
y = ex.pdf(x)
plt.plot(x, y)
平均と分散  (array(10.), array(100.))

cauchy: Cauchy分布(Cauchy distribution)

Cauchy分布とは,分布の最頻値を表すパラメータ $x_0$ と広がりを表すパラメータ $\gamma$ に対して,密度関数が $$ \frac{1}{\pi\gamma\,\left[1 + \left(\frac{x-x_0}{\gamma}\right)^2\right]} $$ で与えられる分布である.

分布関数は, $$ \frac{1}{\pi} \arctan\left(\frac{x-x_0}{\gamma}\right)+\frac{1}{2} $$ である.Cauchy分布は平均をもたず,2次モーメントが無限大になる分布として知られている.

試しにSciPyで平均,分散,歪度,尖度を計算してみる.

from scipy.stats import cauchy

cau = cauchy(0, 1)
print(cau.stats(moments="mvsk"))

x = np.linspace(-10, 10, 100)
y = cau.pdf(x)
plt.plot(x, y)
(array(nan), array(nan), array(nan), array(nan))

平均,分散,歪度,尖度ともに,数値ではないことを表すnan(not a number)が表示された.

norm: 正規分布(normal distribution)

正規分布 $N(\mu,\sigma^2)$ は最も良く使われる連続分布であり, その密度関数は平均を表す位置パラメータ $\mu$ と標準偏差を表す尺度パラメータ $\sigma$ を用いて,

$$ f(x)=\frac{1}{\sqrt{2\pi\sigma^{2}}} \exp\!\left(-\frac{(x-\mu)^2}{2\sigma^2} \right) $$

と記述される.

中心極限定理から(標準偏差をもつ)独立な分布の平均の分布は正規分布に近づくことがいえる. 特に,平均 $0$,標準偏差 $1$ の正規分布 $N(0,1)$ は標準正規分布とよばれ,その密度関数と分布関数は,それぞれ $$ \phi (x) = \frac{1}{\sqrt{2\pi}} \exp\!\left(-\frac{x^2}{2} \right) $$ と $$ \Phi (x) = \int_{-\infty}^x \phi (t) dt = \frac{1}{2} +\frac{1}{2} erf \left( \frac{x}{\sqrt{2}} \right) $$ で定義される.ここで $\mathrm{erf}(x)$ は誤差関数であり, $$ \mathrm{erf}(x) = \frac{2}{\sqrt{\pi}} \int_0^x e^{-t^2} dt $$ である.

lognorm: 対数正規分布(log-normal distribution)

対数をとったときに正規分布になる分布である. 正規分布にしたがう確率変数が負の値も許すのに対して,対数正規分布にしたがう確率変数は正の値しかとらないという性質をもつ. また,独立同分布に従う確率変数の積は漸近的に対数正規分布にしたがうという,大数の法則の積バージョンの性質をもつ.

密度関数は位置パラメータ $\mu$ と形状パラメータ $\sigma$(正規分布と異なり尺度パラメータでないことを注意)を用いて, $$ f(x)=\frac{1}{\sqrt{2\pi\sigma^{2}}} \exp\!\left(-\frac{(\ln x-\mu)^2}{2\sigma^2} \right) \quad \forall x >0 $$ と記述される.分布関数は標準正規分布の分布関数を $\Phi$ としたとき,$\Phi \left(\frac{\ln x - \mu}{\sigma} \right)$ となる.

平均は $e^{\mu+\frac{ \sigma^2 }{2} }$,分散は $e^{2\mu+\sigma^2}(e^{\sigma^2}-1)$ である.

from scipy.stats import lognorm

ln = lognorm(1, loc=5)
print(ln.stats(moments="mv"))

x = np.linspace(4, 10, 100)
y = ln.pdf(x)
plt.plot(x, y);
(array(6.64872127), array(4.67077427))

halfnorm: 半正規分布(half-normal distribution)

$X$ が正規分布 $N(0,\sigma^2)$ のとき, $Y=|X|$ がしたがう分布を半正規分布とよぶ. 一般に平均が $0$ でない場合には,折り畳み正規分布 (folded-normal distribution)とよばれるが, SciPyでは両方とも halfnorm で生成できる.

密度関数は位置パラメータ $\mu$ と尺度パラメータ $\sigma$ を用いて, $$ f(x)= \frac{1}{\sqrt{2\pi\sigma^2}} \, e^{ -\frac{(x-\mu)^2}{2\sigma^2} } + \frac{1}{\sqrt{2\pi\sigma^2}} \, e^{ -\frac{(x+\mu)^2}{2\sigma^2} } $$ と記述される.分布関数は標準正規分布の誤差関数を $erf(x)$ としたとき, $$ \frac{1}{2}\left[ erf\left(\frac{x+\mu}{\sqrt{2\sigma^2}}\right) + erf\left(\frac{x-\mu}{\sqrt{2\sigma^2}}\right)\right] $$ となる.

平均は $$ \mu_Y = \sigma \sqrt{\frac{2}{\pi}} \,\, \exp\left(\frac{-\mu^2}{2\sigma^2}\right) + \mu \, erf\left(\frac{\mu}{\sqrt{2\sigma^2}}\right) $$ であり, 分散は $\mu^2 + \sigma^2 - \mu_Y^2$ である.

from scipy.stats import halfnorm

hn = halfnorm(loc=0, scale=np.sqrt(2) * 10)
print(ln.stats(moments="mv"))

x = np.linspace(-1, 100, 100)
y = hn.pdf(x)
plt.plot(x, y)
(array(6.64872127), array(4.67077427))

erlang: Erlang分布(Erlang distribution)

Erlang分布は,独立で同一の $n$ 個の指数分布の和の分布であり, パラメータ $\lambda>0$ と自然数 $n$ に対して,密度関数が $$ f(x)={\lambda^{n} x^{n-1} e^{-\lambda x} \over (n-1)!} \quad \forall x \geq 0 $$ で与えられる分布である.ここで $n$ は形状パラメータ,$1/\lambda$ は尺度パラメータである. 分布関数は $$ F(x)= 1 - \sum_{k=0}^{n-1} {(\lambda x)^{k} \over k!} e^{-\lambda x} \quad \forall x \geq 0 $$ となる.平均は $n/\lambda$,分散は $n/\lambda^2$ である.

Erlang分布は待ち行列理論に応用をもつ.

from scipy.stats import erlang

er = erlang(3, scale=10)
print(er.stats(moments="mv"))

x = np.linspace(-1, 100, 100)
y = er.pdf(x)
plt.plot(x, y)
(array(30.), array(300.))

gamma: ガンマ分布(gamma distribution)

ガンマ分布は,正のパラメータ $\lambda, a$ に対して,密度関数が $$ f(x) = \frac{ \lambda^a x^{a-1} e^{-\lambda x}}{\Gamma(a)} \quad \forall x \geq 0 $$ で与えられる分布である.ここで $\Gamma$ はガンマ関数である. $a$ が形状パラメータ,$\lambda$ が尺度パラメータである. 分布関数は $$ F(x) = \frac{\gamma(a, \lambda x)}{\Gamma(a)} \quad \forall x \geq 0 $$ となる.ここで $\gamma$ は不完全ガンマ関数である. 平均は $a/\lambda$,分散は $a/\lambda^2$ である.

ガンマ分布はErlang分布のパラメータ $n$ を実数値 $a$ に一般化した分布である.

from scipy.stats import gamma

ga = gamma(3.5, scale=10)
print(ga.stats(moments="mv"))

x = np.linspace(-1, 100, 100)
y = ga.pdf(x)
plt.plot(x, y)
(array(35.), array(350.))

logistic: ロジスティック分布(logistic distribution)

ロジスティック分布は,パラメータ $\mu, s$ に対して,密度関数が $$ f(x) = \frac{\exp(-\frac{x-\mu}{s})}{s(1+\exp(-\frac{x-\mu}{s}))^2} $$ で与えられる分布である.分布関数は $$ F(x) = \frac{1}{1 + e^{-(x - \mu)/s}} = \frac{1}{2}\left\{\tanh\left(\frac{x - \mu}{2 s}\right) + 1 \right\} $$ となる.平均は $\mu$,分散は $\pi^s \mu^2/3$ である.

ロジスティック分布は正規分布と同様に釣鐘型の密度関数と$S$字(シグモイド)型の分布関数をもつが, ロジスティック分布の方が裾野が長いという特徴をもつ. ロジスティック分布の尖度は $1.2$ であり,正規分布の $0$ と比べて大きいので,より裾野が長いことが分かる.

from scipy.stats import logistic, norm

lo = logistic(0, 1)
print(lo.stats(moments="mvsk"))

x = np.linspace(-10, 10, 100)
y = lo.pdf(x)
y2 = norm(0, 1).pdf(x)
plt.plot(x, y)
plt.plot(x, y2)
(array(0.), array(3.28986813), array(0.), array(1.2))

weibull_min: Weibull分布(Weibull distribution)

Weibull分布は正のパラメータ $c, \lambda$ に対して,密度関数が $$ f(x) = c \lambda \left(\lambda x \right)^{c-1} \exp(-(\lambda x)^{c}) \quad \forall x \geq 0 $$ で与えられる分布である.ここで $c$ は形状パラメータ,$1/\lambda$ は尺度パラメータである. 分布関数は $$ F(x)=1-\exp( -(\lambda x)^c) $$ であり,平均は $\Gamma (1+1/c)/\lambda$,分散は $\left(\Gamma (1+2/c)-\Gamma (1+1/c)\right)^2/\lambda^2$ である. ここで $\Gamma$ はガンマ関数である.

Weibull分布は故障現象や寿命を記述するために用いられる. パラメータ $c<1$ のとき故障率が時間とともに小さくなり,$c>1$ のとき時間とともに大きくなる. $c=1$ のとき故障率は一定であり,この場合には指数分布と一致する.

from scipy.stats import weibull_min

wei = gamma(1, scale=10)
print(wei.stats(moments="mv"))

x = np.linspace(-1, 100, 100)
y = wei.pdf(x)
plt.plot(x, y)
(array(10.), array(100.))

beta: ベータ分布(beta distribution)

ベータ分布は正のパラメータ $a, b$ に対して,密度関数が $$ f(x)= \frac{\Gamma(a+b)}{\Gamma(a)\Gamma(b)}\, x^{a-1}(1-x)^{b-1} \quad \forall 0 \leq x \leq 1 $$ で与えられる分布である.ここで $\Gamma$ はガンマ関数であり,$a,b$ はともに形状パラメータである.

分布関数は $$ F(x)=I_x(a,b) $$ である.ここで $I_x$ は正則不完全ベータ関数である. 平均は $a/(a+b)$,分散は $ab/ \left( (a+b)^2 (a+b+1) \right)$ である.

ベータ分布は特定の区間の値をとる確率分布を表すときに有効であり,パラメータ $a,b$ を変えることによって様々な形状をとることができる. プロジェクト管理の手法として有名なPERT(Program Evaluation and Review Technique)においても確率的に変動する作業時間の近似として この分布を用いている.

from scipy.stats import beta

be = beta(2, 4)
print(be.stats(moments="mv"))

x = np.linspace(0, 2, 100)
y = be.pdf(x)
plt.plot(x, y)
(array(0.33333333), array(0.03174603))

t: $t$分布($t$ distribution)

$t$分布は正のパラメータ $\nu$ に対して,密度関数が $$ f(t) = \frac{\Gamma((\nu+1)/2)}{\sqrt{\nu\pi\,}\,\Gamma(\nu/2)} (1+ \frac{t^2}{\nu} )^{-(\nu+1)/2} $$ で与えられる分布である.ここで $\Gamma$ はガンマ関数であり,$\nu$ は自由度(degree of freedom)とよばれる形状パラメータである. ちなみに $\nu$ の引数名はdegree of freedomの略でdfである.

分布関数は $$ F(t)=I_x( \frac{\nu}{2}, \frac{\nu}{2} ) $$ である.ここで$I_x$ は正則不完全ベータ関数であり, $$ x = \frac{t+\sqrt{t^2+\nu}}{2\sqrt{t^2+\nu}} $$ とする. 平均は $0$,分散は $\nu>2$ のとき $\nu/(\nu-2)$,$1< \nu \le 2$ のとき無限大である.

いま $x_1,x_2,\ldots,x_n$ を平均 $\mu$ の独立な正規分布のサンプルとする. 標本平均と不偏分散を $$ \bar{x} = \frac{x_1+x_2+ \cdots+x_n}{n} $$ $$ s = \sqrt{ \frac{1}{n-1} \sum_{i=1}^n (x_i - \bar{x})^2 } $$ とする. このとき, $$ t = \frac{\bar{x} - \mu}{s/\sqrt{n}} $$ は,自由度 $\nu=n-1$ の $t$分布にしたがう. この性質を利用して,$t$分布は,標本値から母集団の平均値を統計的に推定する区間推定や,母集団の平均値の仮説検定に利用される.

平均との仮説検定はttest_1samp,独立な2つのサンプル間の仮説検定はttest_ind, 対応がある2つのサンプル間の仮説検定はttest_relで行うことができる.

ちなみに$t$分布は,$\nu=1$ のときCauchy分布になり,$\nu$ が無限大に近づくと正規分布に漸近する.

from scipy.stats import t

tdist = t(1)
print(lo.stats(moments="mvsk"))

x = np.linspace(-10, 10, 100)
y = tdist.pdf(x)
y2 = norm(0, 1).pdf(x)
plt.plot(x, y)
plt.plot(x, y2)
(array(0.), array(3.28986813), array(0.), array(1.2))

chi2: $\chi^2$分布($\chi^2$distribution)

$\chi^2$分布は正のパラメータ $k$ に対して,密度関数が $$ f(x)= \frac{1}{2^{\frac{k}{2}}\Gamma\left(\frac{k}{2}\right)}\; x^{\frac{k}{2}-1} e^{-\frac{x}{2}} \quad \forall x \geq 0 $$ となる分布である. ここで $\Gamma$ はガンマ関数であり, $k$ は自由度を表す形状パラメータである. $k$ の引数名はdfである.

分布関数は $$ F(x)= \frac{1}{\Gamma\left(\frac{k}{2}\right)}\;\gamma\left(\frac{k}{2},\,\frac{x}{2}\right) \quad \forall x \geq 0 $$ である.ここで $\gamma$ は不完全ガンマ関数である. 平均は $k$,分散は $2k$ である.

$x_1,x_2,\ldots,x_k$ を独立な標準正規分布のサンプルとするとき, $$ \sum_{i=1}^k (x_i)^2 $$ は自由度 $k$ の $\chi^2$分布にしたがう. この性質を利用して $\chi^2$分布は $\chi^2$検定chisquareやFriedman検定friedmanchisquareに用いられる.

from scipy.stats import chi2

ch = chi2(3)
print(ch.stats(moments="mv"))

x = np.linspace(0, 10, 100)
y = ch.pdf(x)
plt.plot(x, y)
(array(3.), array(6.))

f: $F$分布 ($F$ distribution)

$F$分布は正のパラメータ $d_1, d_2$ に対して,密度関数が $$ f(x) = \frac{\sqrt{\frac{(d_1\,x)^{d_1}\,\,d_2^{d_2}} {(d_1\,x+d_2)^{d_1+d_2}}}} {x\,\mathrm{B}\!\left(\frac{d_1}{2},\frac{d_2}{2}\right)} \quad \forall x \geq 0 $$ で与えられる分布である.ここで $\mathrm{B}$ はベータ関数であり,$d_1, d_2$ は自由度を表す形状パラメータである. $d_1, d_2$ の引数名は,それぞれdfn,dfdである.

分布関数は $$ F(x)=I_{\frac{d_1 x}{d_1 x + d_2}} \left(\tfrac{d_1}{2}, \tfrac{d_2}{2} \right) \quad \forall x \geq 0 $$ である. 平均は $d_2>2$ のとき $d_2/(d_2-2)$,分散は $d_2>4$ のとき $$ \frac{2\,d_2^2\,(d_1+d_2-2)}{d_1 (d_2-2)^2 (d_2-4)} $$ である.

$U_1, U_2$ を独立な $\chi^2$分布にしたがう自由度 $d_1, d_2$ の確率変数としたとき, $$ \frac{U_1/d_1}{U_2/d_2} $$ は $F$分布にしたがうことが知られている. この性質を利用して$F$分布は $F$検定f_onewayに用いられる.

from scipy.stats import f

fdist = f(3, 3)
print(fdist.stats(moments="mv"))

x = np.linspace(0, 10, 100)
y = fdist.pdf(x)
plt.plot(x, y)
(array(3.), array(inf))

triang: 3角分布 (triangular distribution)

3角分布は,区間 $[\alpha,\beta]$ で $\gamma$ で最大になる密度関数をもった連続分布である. 密度関数は, $$ f(x)=\begin{cases} \frac{2(x-\alpha)}{(\beta-\alpha)(c-\alpha)} & \forall \alpha \le x < \gamma \\ \frac{2}{\beta-\alpha} & \forall x = \gamma \\ \frac{2(\beta-x)}{(\beta-\alpha)(\beta-\gamma)} & \forall c < x \le \beta \end{cases} $$ であり,分布関数は $$ F(x)=\begin{cases} \cfrac{(x-\alpha)^2}{(\beta-\alpha)(\gamma-\alpha)} &\forall \alpha \le x \le \gamma \\ 1-\cfrac{(\beta-x)^2}{(\beta-\alpha)(\beta-\gamma)} & \forall \gamma<x\le \beta \end{cases} $$ となる.

位置パラメータlocは下限値 $\alpha$ を表し, 尺度パラメータscaleは上限値と下限値の差 $\beta-\alpha$ を表し, 形状パラメータcは最頻値 $\gamma$ がloc+c*scaleになるように定められる. (つまり,形状パラメータcは $(\gamma-\alpha)/(\beta-\alpha)$ に設定する.)

平均は $$\frac{\alpha+b\beta\gamma}{3}$$ であり,分散は $$ \frac{\alpha^2+\beta^2+\gamma^2-\alpha \beta-\alpha \gamma-\beta \gamma}{12} $$ となる.

主に,最小値 $\alpha$ と最大値 $\beta$ と最頻値 $\gamma$ しか分かっていない場合に,簡易的に用いられる.

from scipy.stats import triang

tri = triang(0.2, loc=0, scale=10)
print(tri.stats(moments="mv"))

x = np.linspace(0, 10, 100)
y = tri.pdf(x)
plt.plot(x, y)
(array(4.), array(4.66666667))

multivariate_normal: 多変量正規分布(multivariate normal distribution)

多変量正規分布は多次元の正規分布であり,平均を表す多次元ベクトル $\mu$ と共分散行列を表す尺度パラメータ $\Sigma$ を用いて, $$ f(x) = \frac{1}{\sqrt{(2 \pi)^k \det \Sigma}} \exp\left( -\frac{1}{2} (x - \mu)^T \Sigma^{-1} (x - \mu) \right) $$ と定義される.ここで $\det \Sigma$ は行列 $\Sigma$ の行列式を表す.

平均と共分散行列を固定化した分布を生成するには,引数meanとcovを用いる. 平均が $(0,0)$ で共分散行列が単位行列の2次元多変量正規分布を生成するには,以下のように記述する.

from scipy.stats import multivariate_normal

n = multivariate_normal([0.0, 0.0])
print(n.rvs(3))
print(n.pdf([0.0, 0.0]))
[[ 2.12179385  1.40583912]
 [ 1.70707168  0.96422565]
 [ 0.76107922 -0.78667625]]
0.15915494309189532

代表的な離散確率変数

poisson: Poisson分布 (Poisson distribution)

Poisson分布とは,パラメータ $\mu >0$ に対して確率関数が $$ P(X=k)=\frac{\mu^k e^{-\mu}}{k!} \quad \forall k=0,1,\ldots $$ で与えられる離散分布である.パラメータ $\mu$ は形状パラメータを表す引数muとして入力する. Poisson分布の確率関数 $P(X=k)$ は,単位時間中に平均で $\mu$ 回発生する稀な事象がちょうど $k$ 回発生する確率を表す. 平均,分散ともに $\mu$ である.

from scipy.stats import poisson

poi = poisson(4)
print(poi.stats(moments="mv"))

x = np.arange(0, 11)
y = poi.pmf(x)
plt.plot(x, y, marker="o")
(array(4.), array(4.))

geom: 幾何分布(geometric distribution)

幾何分布とは,パラメータ $0 < p <1$ に対して,確率関数が $$ P(X=k) = p (1-p)^{k-1} \quad \forall k=1,2,\ldots $$

で与えられる離散分布である.(この分布をファーストサクセス分布とよび,幾何分布を $p (1-p)^{k}$ と定義することもある.)

パラメータ $p$ は形状パラメータpとして入力する. 幾何分布は,独立なコイン投げ(表の出る確率を $p$ とする)を行ったときの表が出るまでに投げる回数を表す. 平均は $1/p$,分散は $(1-p)/p^2$ である.幾何分布は無記憶性 ($P(X >x+y | X>x)=P(X>y)$ がすべての $x,y=1,2,\ldots$ に対して成立すること) をもつ.

from scipy.stats import geom

ge = geom(0.5)
print(ge.stats(moments="mv"))

x = np.arange(0, 11)
y = ge.pmf(x)
plt.plot(x, y, marker="o")
(array(2.), array(2.))

binom: 2項分布(binomial distribution)

2項分布とは,パラメータ $0 < p <1$,自然数 $n$ に対して,確率関数が $$ P(X=k) = {n\choose k} p^k(1-p)^{n-k} \quad \forall k=0,1,\ldots,n $$ で与えられる離散分布である.ただし, $$ {n\choose k} = \frac{n!}{k! (n-k)!} $$ である.パラメータ $n,p$ は形状パラメータn,pとして入力する. 2項分布は,$n$回の独立なコイン投げ(表の出る確率を $p$ とする)を行ったときの表の出る数を表す. 平均は $np$,分散は $np(1-p)$ である.

from scipy.stats import binom

bi = binom(10, 0.3)
print(bi.stats(moments="mv"))

x = np.arange(0, 11)
y = bi.pmf(x)
plt.plot(x, y, marker="o")
(array(3.), array(2.1))

nbinom: 負の2項分布(negative binomial distribution)

負の2項分布とは,パラメータ $p \leq 1$,正数 $n$ に対して,確率関数が $$ {k+n-1 \choose n-1} p^n (1-p)^k \quad \forall k=0,1,\cdots $$ で与えられる離散分布である.パラメータ $n,p$ は形状パラメータn,pとして入力する.

負の2項分布は,独立なコイン投げ(表の出る確率を $p$ とする)を行ったとき, $n$回表が出るまでに裏が出た回数を表す. 平均は $n(1-p)/p$,分散は $n(1-p)/p^2$ である.

from scipy.stats import nbinom

nbi = nbinom(4, 0.3)
print(nbi.stats(moments="mv"))

x = np.arange(0, 20)
y = nbi.pmf(x)
plt.plot(x, y, marker="o")
(array(9.33333333), array(31.11111111))

hypergeom: 超幾何分布(hypergeometric distribution)

超幾何分布とは,自然数 $M,n,N$ に対して,確率関数が $$ P(X=k) = \frac{\binom{n}{k}\binom{M-n}{N-k}}{\binom{M}{N}} \quad \forall \max (0, N - (M-n)) \leq k \leq \min (n, N) $$ で与えられる離散分布である. パラメータ $M, n, N$ は形状パラメータM, n, Nとして入力する. 超幾何分布は, $N$ 個の当たりをもつ $M$ 個入りのくじから, $n$ 個を非復元抽出したときに $k$ 個の当たりが含まれている確率を与える. 平均は $nN/M$,分散は $$ \frac{(M-n)n(M-N)N}{(M-1)M^2} $$ である.

from scipy.stats import hypergeom

hy = hypergeom(100, 10, 30)
print(hy.stats(moments="mv"))

x = np.arange(0, 20)
y = hy.pmf(x)
plt.plot(x, y, marker="o")
(array(3.), array(1.90909091))

問題: 一様分布

範囲 $[2.75, 6.50]$ に一様に分布した需要を考える.

  1. 平均と変動係数は?
  2. 5以上になる確率は?
  3. 平均から標準偏差$\sigma$ 以内になる確率は?

問題: 3角分布

需要を推定したところ最低でも1, 最大だと7,最頻値が4であることが判明した. 3角分布を仮定して以下の問いに答えよ.

  1. 平均と変動係数は?
  2. 5以上になる確率は?
  3. 平均から標準偏差$\sigma$ 以内になる確率は?

問題: Poisson分布

顧客の訪問件数が1分間に平均2.2人のPoisson分布にしたがうと仮定したとき,以下の問に答えよ.

  1. 1分間に誰も来ない確率は?
  2. 1分間に2人以下の確率は?
  3. 少なくとも1人の訪問がある確率は?

データのあてはめ

fit(data)メソッドは,与えられたデータdataに対するパラメータの 最尤推定値 を計算するための関数である. 返値は, 形状パラメータshape,位置パラメータloc,尺度パラメータscaleのタプルである.

例として, 平均 $100$,標準偏差 $10$ の正規分布の $10$ 個のランダムサンプルと $10000$ 個のランダムサンプルで比較する.

from scipy.stats import norm

rvs = norm(loc=100, scale=10).rvs(size=10)
print(norm.fit(rvs))
(106.72235167168519, 6.03355761877896)
rvs = norm(loc=100, scale=10).rvs(size=10000)
print(norm.fit(rvs))
(99.94705703634412, 10.03424828222017)

サンプル数が多い方が,良い推定値を返すことが確認できる.

平均や標準偏差を固定して推定することもできる.この場合には, 引数 floc, fscaleで位置パラメータと尺度パラメータの固定値を渡す.

例として,平均を $100$ に固定して推定してみる.

rvs = norm(loc=100, scale=10).rvs(size=100)
print(norm.fit(rvs, floc=100))
(100, 10.77354048097617)

標準偏差の推定値が改善した.

ガンマ分布のように形状パラメータを必要とする場合には,引数名はf0, f1, ... と番号で指定する.

from scipy.stats import gamma

rvs = gamma(1, scale=10).rvs(size=10000)
print(gamma.fit(rvs))
(0.9961759171656837, 0.001265107521498718, 10.002171583894704)

形状パラメータ $a$ を $1$ に固定して予測する.

rvs = gamma(1, scale=10).rvs(size=10000)
print(gamma.fit(rvs, f0=1))
(1, 2.824539933266121e-05, 9.86146008235017)

仮説検定

例題: 仮説検定(両側検定)

顧客の希望価格をヒヤリングしたところ, $7900,11300,13500,6500,5920,6200,6800,1150,8500,7900,8250,5970,20000,7550,6900,10000,1250$ 円という結果を得た.

希望価格の平均が $8000$ 円であるという帰無仮説を有意水準 $5$%で検定せよ.

$t$ 検定を行う.

$p$-値は約 $0.98$ であり, $0.05$ より大きいので帰無仮説は棄却できない.

from scipy import stats

price = np.array(
    [
        7900,
        11300,
        13500,
        6500,
        5920,
        6200,
        6800,
        1150,
        8500,
        7900,
        8250,
        5970,
        20000,
        7550,
        6900,
        10000,
        1250,
    ]
)
t, p = stats.ttest_1samp(price, popmean=8000)
print(price.mean(), p)
7975.882352941177 0.981924410901683

例題:仮説検定 (片側検定)

サーバーの応答時間を調べたところ $50,29, 38, 43, 42, 40, 36, 37$ msという結果を得た.

応答時間の平均値が$35$ msより大きいという対立仮説を有意水準$5$%で検定せよ.

$t$ 検定で得られた $p$-値(片側検定なので半分にする) は $0.05$ より小さいので帰無仮説を棄却する.

speed = np.array([50, 29, 38, 43, 42, 40, 36, 37])
t, p = stats.ttest_1samp(speed, popmean=35)
print(speed.mean(), p / 2)
39.375 0.040901671787554675

分布テスト

probplot関数は,与えられたデータの分布を任意の分布と比較する確率プロットを返す. 分布を指定する引数は dist であり,既定値は正規分布を表す norm である. また返値は,確率プロットの $x,y$ 座標,最小自乗法による回帰分析の結果のタプルである. なお,回帰分析の結果は, 傾き,$y$切片,決定係数の平方根のタプルである.

例として,$[0,10]$ の一様分布にしたがってランダムに発生させたデータに対する確率プロットを示す.

from scipy.stats import norm, probplot
from scipy.stats import uniform

u = uniform(loc=0, scale=10)
rvs = u.rvs(size=100)
xy_pair, corr = probplot(rvs, plot=plt)
print(corr)
(2.9694088950580446, 5.26273513955523, 0.9699380369706573)

正規分布に近いと基準線である赤線に近づく. 今度は,一様分布と近いか否かを引数 dist を uniform にして調べてみる.

xy_pair, corr = probplot(rvs, dist="uniform", plot=plt)
print(corr)
(10.098369966597, 0.01697484919380088, 0.9977752554272767)

決定係数の平方根が $1$ に近づき,基準線と重なっているので,サンプルは正規分布より一様分布に近いと判断できる.

信頼区間

未知の母集団からサンプリングを行い,90%の信頼区間を求めたい.

例として,母集団を平均mu (= $100$), 標準偏差 sigma(= $10$)の正規分布とし,$10000$ 個の擬似乱数を生成する.

from scipy.stats import norm
import numpy as np
from scipy import stats

mu = 100.0
sigma = 10.0
normal = norm(mu, sigma)
population = normal.rvs(10000)
population
array([ 91.8568754 ,  77.30281219, 103.45218038, ..., 102.92834131,
        88.70689383, 112.62455436])

サンプルサイズ (sample size) $50$ のサンプルを, $1000$ 個(これをサンプル数と呼ぶ; number of samplesのこと.統計の用語はあまり良くない!)抽出し,その平均と標準偏差を計算する.

ただし,標準偏差は平均が未知であるので,不偏標準偏差を計算する必要がある. NumPyの標準偏差を計算するためのメソッド std では, 既定値は通常の(不偏ではない)標準偏差であり, 不偏にするためには引数 ddof(Delta Degrees of Freedomの略;自由度の差を表す)を $1$ に設定する必要がある.

$n$ 個のサンプル $x_1, x_2, \ldots, x_n$ があり、その平均値を $\bar{x}$ としたとき, 分散は以下のように計算される. $$ \sum_{i=1}^n (x_i-\bar{x})^2/n $$

一方,不偏分散は以下のように, 分母の $n$ から $1 (=$ ddof)を減じて補正したものである. $$ \sum_{i=1}^n (x_i-\bar{x})^2/(n-1) $$

平均は母集団の平均とほぼ同じで,標準偏差は母集団の標準偏差をサンプルサイズの平方根で割ったものとほぼ同じであることが確認できる(大数の法則).

sample_size = 50
num_samples = 1000
sample_mean, sample_std = [], []
for i in range(num_samples):
    sample = np.random.choice(population, sample_size)
    sample_mean.append(sample.mean())
sample_mean = np.array(sample_mean)
print("標本平均と標本平均の標準偏差 = ", sample_mean.mean(), sample_mean.std(ddof=1))
print("母集団の標準偏差/sqrt(sample_size)= ", sigma / np.sqrt(sample_size))
標本平均と標本平均の標準偏差 =  100.06611426470428 1.4404989689736518
母集団の標準偏差/sqrt(sample_size)=  1.414213562373095

問題:大数の法則

ある店舗の1分あたりの客数は,平均 $\mu=4$ ,標準偏差 $\sigma=5$ であった.分布は特定されないが,時間ごとに独立で同一の分布と仮定できるものとする. また,独立同一な$n$個の確率変数の和は,平均 $\mu n$,標準偏差 $\sigma \sqrt{n}$ の正規分布に近づくことが知られている(大数の法則).

これを利用して,1時間あたりの客数が260以上になる確率を求めよ.

また, 220以上260以下である確率を求めよ.

1つのサンプルの標本平均 $\bar{x}$ と標本標準偏差 $s/\sqrt{n}$ ($n$ はサンプルサイズ)を用いて,90%信頼区間を計算する.

$100(1-\alpha)$%の信頼区間は,分布が正規分布で近似できる場合には,以下のようになる.

$$ \bar{x}-z_{\frac{\alpha}{2}}\sqrt{\frac{s^2}{n}} < \mu < \bar{x}+z_{\frac{\alpha}{2}}\sqrt{\frac{s^2}{n}} $$

ここで $\mu$ は母集団の平均(未知数)であり,上式が成立する確率が $100(1-\alpha)$%になるように信頼区間は設定される.

SciPyのstatsにある正規分布normのinterval(alpha, loc, scale)は, 平均loc,標準偏差scaleの正規分布の 100(1$-$alpha)%点を求めるメソッドであるので, サンプルの90%信頼区間は以下のように計算できる.

stats.norm.interval(0.9, sample.mean(), sample.std(ddof=1) / np.sqrt(sample_size))
(100.60732484701236, 104.82042570453217)

サンプルをたくさん生成して信頼区間を計算し,母集団の平均 $\mu$ がその区間に含まれる割合を計算する.

おおよそ90%になっていることが確認できる.

count = 0
for i in range(num_samples):
    sample = np.random.choice(population, sample_size)
    lb, ub = stats.norm.interval(
        0.9, sample.mean(), sample.std(ddof=1) / np.sqrt(sample_size)
    )
    if mu > lb and mu < ub:
        count += 1
print(count / num_samples)
0.885

今度は,サンプルサイズを $10$ に設定してみる.一般に,サンプルサイズ $n$ が $30$ 未満の場合には,正規分布で近似するのではなく,$t$ 分布を用いることが推奨される.

$100(1-\alpha)$%の信頼区間は,分布が$t$ 分布で近似できる場合には,以下のようになる.

$$ \bar{x}-t_{\frac{\alpha}{2}}(n-1)\sqrt{\frac{s^2}{n}} < \mu < \bar{x}+t_{\frac{\alpha}{2}}(n-1)\sqrt{\frac{s^2}{n}} $$

まずは正規分布と仮定して,信頼区間を計算して,母集団平均 $\mu$ が区間に含まれている割合を計算してみる. 指定した90%ではなく,それより小さい値になっていることが確認できる.

sample_size = 10
num_samples = 1000
sample_mean, sample_std = [], []
for i in range(num_samples):
    sample = np.random.choice(population, sample_size)
    sample_mean.append(sample.mean())
stats.norm.interval(0.9, sample.mean(), sample.std(ddof=1) / np.sqrt(sample_size))
(94.78985048997073, 101.11212635325926)
count = 0
for i in range(num_samples):
    sample = np.random.choice(population, sample_size)
    lb, ub = stats.norm.interval(
        0.9, sample.mean(), sample.std(ddof=1) / np.sqrt(sample_size)
    )
    if mu > lb and mu < ub:
        count += 1
print(count / num_samples)
0.88

$t$ 分布を用いて信頼区間を計算する. t.intervalの第2引数は自由度であり, サンプルサイズ $-1$ と設定する.

正規分布より広い区間となっており,母集団平均もほぼ90%の割合で区間内に入っていることが確認できる.

stats.t.interval(
    0.9, len(sample) - 1, sample.mean(), sample.std(ddof=1) / np.sqrt(sample_size)
)
(94.92475355447223, 107.61525710972265)
count = 0
for i in range(num_samples):
    sample = np.random.choice(population, sample_size)
    lb, ub = stats.t.interval(
        0.9, len(sample) - 1, sample.mean(), sample.std(ddof=1) / np.sqrt(sample_size)
    )
    if mu > lb and mu < ub:
        count += 1
print(count / num_samples)
0.904

補間

ここでは,補間に関するサブパッケージ interpolate について開設する.

補間 (interpolation)とは,特定の領域内に与えられた離散的なデータ点をもとに, 同じ領域内の他の点を近似する連続関数を構築するための方法である. 補間は内挿ともよばれる.

また,与えられた領域外の点の近似を行うことを外挿(extrapolation)とよぶ.

例として1次元の関数 $f(x)=\sin x/(1+x^2)$ の補間を考える.

関数 $f(x)$ と$[-4,4]$ の整数に対応する $9$個のデータ点 $x_i,y_i=f(x_i)$ を生成しておく.

from scipy import interpolate

x = np.arange(-4, 5)
y = np.sin(x) / (1 + x**2)
plt.plot(x, y, "o");

Lagrange多項式

$k+1$ 個のデータ点 $(x_0,y_0), (x_1,y_1),\ldots,(x_k,y_k)$ が $x_i$ の昇順に並んでいるものとする

補間は,すべての点を通る多項式を1つ決めるのか, 区分 $[x_i, x_{i+1}]$ ごとに異なる多項式を用いるのかによって大きく2つに分けられる.

前者の方法の1つとして,Lagrange多項式(Lagrange polynomial)がある.

Lagrange基底多項式 $$ \ell_j(x) = \frac{(x-x_0)}{(x_j-x_0)} \cdots \frac{(x-x_{j-1})}{(x_j-x_{j-1})} \frac{(x-x_{j+1})}{(x_j-x_{j+1})} \cdots \frac{(x-x_k)}{(x_j-x_k)} $$ を用いて $$ L(x) = \sum_{j=0}^{k} y_j \ell_j(x) $$ と定義される.$\ell_j(x_j)=1$ であり,かつ $\ell_j(x_i)=0$ $(i \neq j)$ であるので, $L(x)$ はすべてのデータ点を通過することが分かる.

interpolateパッケージにあるlagrange(x,y)は1次元データに対するLagrange多項式を求めるための関数である.

これは,同じ長さをもつ1次元配列 $x$ と $y$ に対して, 関数 $y=f(x)$ を近似するLagrange多項式を返す. 返値はNumPyの1次元の多項式オブジェクトである.

f = interpolate.lagrange(x, y)
print(f)
xnew = np.arange(-4, 4, 0.01)
ynew = f(xnew)
plt.plot(x, y, "o", xnew, ynew, "-")
            8             7             6           5             4
-1.249e-19 x - 0.0006892 x - 6.583e-18 x + 0.02123 x + 5.436e-17 x
           3             2
 - 0.2016 x + 1.415e-17 x + 0.6018 x

線形補間

区分ごとに異なる多項式を用いる場合には,自由度が増えるので様々な方法が考えられる.

interpolateパッケージにあるinterp1d(x,y)は1次元データに対する補間を行う関数である. 同じ長さをもつ1次元配列 $x$ と $y$ に対して, 関数 $y=f(x)$ を近似する補間を行う. 返値は,関数のように与えられた新しい点xに対して値yを返すオブジェクトである. $x,y$ 以外の主な引数は以下の通り.

  • kind: 補間の種類を表す文字列であり, "linear"(線形補間;既定値), "nearest"(最近点補間), "zero"($0$次補間), "slinear"(スプライン線形補間), "quadratic"(スプライン2次補間), "cubic"(スプライン3次補間)から選択する.

  • copy: 与えられたデータ $x,y$ のコピーを作成してから処理するか否かを表す論理値である.既定値はコピーすることを表すTrueである.

  • fill_value: 領域外の点を与えたときに割り当てられる値である.既定値はNaNである.

  • assume_sorted: 点を表すデータ $x$ が昇順に並んでいるか否かを表す論理値である.既定値は昇順に並んでいるデータを仮定するTrueである.

最も簡単な方法は区分ごとに異なる線分で繋いだ(区分的線形)関数である. これは線形補間(linear interpolation)とよばれる.

f = interpolate.interp1d(x, y, kind="linear")
xnew = np.arange(-4, 4, 0.01)
ynew = f(xnew)
plt.plot(x, y, "o", xnew, ynew, "-");

最近点補間と0次補間

線形補間と同様に線分を用いた補間として,最近点補間(nearest-neighbor interpolation)と $0$次補間(zero-degree interpolation)がある.

これらは両者とも区分的一定な関数を用いたものであり,同じ手法を表すことも多いが,SciPyでは区別している. 最近点補間は最も近いデータ点 $x_i$ の値 $y_i$ をとる関数であり, $0$次補間は $[x_i,x_{i+1}]$ の値を $y_{i}$ とする関数である

f = interpolate.interp1d(x, y, kind="nearest")
xnew = np.arange(-4, 4, 0.01)
ynew = f(xnew)
plt.plot(x, y, "o", xnew, ynew, "-");
f = interpolate.interp1d(x, y, kind="zero")
xnew = np.arange(-4, 4, 0.01)
ynew = f(xnew)
plt.plot(x, y, "o", xnew, ynew, "-");

スプライン補間

区分ごとに異なる多項式を用いる方法としてスプライン補間(spline interpolation)がある.

ここでスプラインとは自在定規のことであり,スプライン補間は与えられた複数の点を通る「滑らかな」曲線で,データ点を繋ぐ.最も良く使われるのは3次のスプライン補間であり, 各データ点において微分値と2次微分値が連続になるように,区分ごとに多項式を決める.

以下に,1次(線形補間を同じ),2次,3次のスプライン補間の結果を順に示す.

f = interpolate.interp1d(x, y, kind="slinear")
xnew = np.arange(-4, 4, 0.01)
ynew = f(xnew)
plt.plot(x, y, "o", xnew, ynew, "-");
f = interpolate.interp1d(x, y, kind="quadratic")
xnew = np.arange(-4, 4, 0.01)
ynew = f(xnew)
plt.plot(x, y, "o", xnew, ynew, "-");
f = interpolate.interp1d(x, y, kind="cubic")
xnew = np.arange(-4, 4, 0.01)
ynew = f(xnew)
plt.plot(x, y, "o", xnew, ynew, "-");

問題: テーマパークの待ち時間

あなたはテーマパークからアトラクションの待ち時間の調査を依頼された. 待ち時間を $30$分おきに測定した結果は, 開園時刻を $0$ としたPythonのリストとして,以下のように与えられている.

[80,90,90,100,110,110,90,90,90,90,90,80,80,60,60,60,60,70,70,70,60,35,40,50,45]

開園から閉園までの任意の時刻における待ち時間を推定するには,どのようにしたら良いだろうか? 補間を用いて解決せよ.

問題: 需要の欠損値

ある新製品の毎日の需要量を調べたところ,以下のリストのようになっていることが分かった. ただし,リスト内の ? は需要を調べるのを忘れたことを表す. 適当な補間を用いて,情報のない日の需要量を予測せよ.

需要量 =  [18, 20, 23 , 25 , ? , 28, ?,  ?, 35 ]

問題: テーマパークの待ち時間 (欠損あり)

再び,アトラクションの待ち時間の調査を行った. 待ち時間を $30$ 分おきに測定した結果は, 開園時刻を $0$ としたPythonのリストとして,以下のように与えられている. 開園から閉園までの任意の時刻における待ち時間をLagrange補間と3次スプライン補間を用いて推定せよ. ただし,リスト内の NaN はNot a Number(数でないもの)を意味し,測定機械のミスでデータが取れなかったことを表す.

[5,55,80,60,70,60,50,60,70,55,40,55,55,NaN,NaN,80,80,80,70,80,80,70,60,45,30,25,20,15]

積分

SciPyのサブパッケージintegrateは,定積分を行うための関数をまとめたものである. 以下では,基本的な1重積分quadと2重積分dblquadを紹介するが, 他にも3重積分,$n$重積分など様々な関数が含まれる.

1重積分関数 quad

quadはFortran言語で書かれたQUADPACKを用いて1重定積分を求める.

引数は以下の通り.

  • func: 積分を行う関数 $f$ である.複数の引数をもつ関数を入力した場合には,第1引数についてのみ積分を行う.
  • a: 積分範囲 $[a,b]$ の下限 $a$ を表す.$-\infty$ にしたい場合には,NumPyの-infを用いる.
  • b: 積分範囲 $[a,b]$ の上限 $b$ を表す. $b> a$ である必要がある.$+\infty$ にしたい場合には, NumPyのinfを用いる.

返値は積分値と誤差のタプルである.

例題:円錐の体積

底辺の半径 $r=10$ cm,高さ $h=10$ cmの円錐の体積を求めてみよう, 頂点から底辺の中心への距離 $x$ での断面の面積は $\pi x^2$ であるので,これを区間 $[0,10]$ で積分することによって, 円錐の体積は, $$ \int_0^{10} \pi x^2 dx $$ と計算できる.

理論値は $\frac{1}{3} \pi r^2 h$ である.

from scipy import integrate

f = lambda x: np.pi * x**2
print(integrate.quad(f, 0.0, 10.0))  # 積分
print(np.pi / 3.0 * 10**3)  # 理論値
(1047.1975511965977, 1.162622832669544e-11)
1047.1975511965977

問題: 楕円の面積

楕円 $x^2 + y^2/10 \leq 1$ の面積を求めよ.

楕円の面積の公式から, $\pi \times \sqrt{1} \times \sqrt{10}$ と計算できるが,積分関数quadを用いて近似計算を行え.

問題:円状の都市

面積 $1$ の円状の都市に一様に住んでいる人たちが,円の中心にあるスーパーに行くときの距離の期待値を求めよ.

ちなみに理論値は $2/ 3 \sqrt{\pi}$ である (拙著「はじめての確率論」(近代科学社) p.31 参照).

2重積分関数 dblquad

dblquadはFortran言語で書かれたQUADPACKを用いて2重定積分を求める.

引数は以下の通り.

  • func: 少なくとも2つの変数(たとえば $x,y$)をもつ関数 $f(x,y)$ である.
  • a: 積分範囲 $[a,b]$ の下限 $a$ を表す.$-\infty$ にしたい場合には,NumPyの-infを用いる.
  • b: 積分範囲 $[a,b]$ の上限 $b$ を表す. $b> a$ である必要がある.$+\infty$ にしたい場合には, NumPyのinfを用いる.
  • gfun: 第2引数である変数 $y$ の積分範囲の下限を規定する関数 $g(x)$ である.
  • hfun: 第2引数である変数 $y$ の積分範囲の上限を規定する関数 $h(x)$ である.

返値は積分値と誤差のタプルである.

例題:正方形の都市

面積 $1$ の正方形の都市に一様に住んでいる人たちが,正方形の中心にあるスーパーに行くときの距離の期待値を求めてみよう. 正方形 $[0,1]^2$ 内の座標 $x,y$ に住んでいる人の中心への距離は $$ \sqrt{ \left(x- \frac{1}{2} \right)^2 + \left(y- \frac{1}{2} \right)^2 } $$ と計算できるので,これを区間 $[0,1]^2$ で積分することによって,期待値が計算できる. $x$ の積分範囲は $[0,1]$ で良いが,$y$ の積分範囲は $x$ の関数として与える必要があるので,ダミーの関数g,hを作成して 引数として渡していることに注意されたい.

理論値 $( \sqrt{2} + \log (1+\sqrt{2}) )/6 $ の導出は結構面倒である(拙著「はじめての確率論」(近代科学社) p.33 参照).

f = lambda x, y: np.sqrt((x - 0.5) ** 2 + (y - 0.5) ** 2)
g = lambda x: 0.0
h = lambda x: 1.0
print(integrate.dblquad(f, 0, 1, g, h))  # 2重積分
print((np.sqrt(2) + np.log(1 + np.sqrt(2))) / 6.0)  # 理論値
(0.38259785823171677, 1.3339283233548827e-08)
0.38259785823210635

問題:高速道路の修理車と故障車の位置

区間 $[0,1]$ の高速道路上に故障車が一様に発生しているものとする. 修理車は高速道路上に $1$台あり,前に修理した位置にいて,どこでもUターンしてかけつけることができる. 故障車と修理車の距離の期待値を求めよ.

ちなみに理論値は $1/3$ である(拙著「はじめての確率論」(近代科学社) p.88 参照) .