概要
WEB系のサービスで色々な試作を実施した後に効果を検証するのは非常に重要だと思いますが、
そのやり方として基本的な統計学が十分に使えると思っています。
今回は基本的な統計学からビジネスで使える試作の効果検証、データ分析を目的にPython+JupyterLab(Docker)を使った統計的データ分析のやり方をまとめました。
また今回使ったnotebookは以下にもありますのでご参考ください。
https://github.com/hikarut/Data-Science/tree/master/notebooks/statisticsSample
環境
以下を参考にDockerでJupyterLabが使える状態を前提とします。
Dockerで起動したJupyterLabでvimキーバインドを使う
分析手法まとめ
- | 質的説明変数 (2分類) |
量的説明変数 (量と質を含む複数の場合も) |
---|---|---|
量的アウトカム(数値的) | 平均値の差をt検定 (or ウィルコクソンの順位和検定) |
重回帰分析 |
質的アウトカム(分類型) | 割合の差をz検定 (カイ二乗検定と同じ) |
ロジスティック回帰分析 |
用語
量的
- 年齢や収入、購買金額といった数で表される情報
質的
- 性別や職業、商品ジャンルといった数ではなく文字で表される情報
- 0,1の二値変数に置き換えて使う場合がある
アウトカム
- データ分析を因果関係の洞察、すなわち、最終的にコントロールしたい結果とそれに影響を与えうる原因の候補、という観点で捉えた時に、「最終的にコントロールしたい結果」のことをアウトカム(成果指標) と呼ぶ
- 一般的には「結果変数」「目的変数」「従属変数」「外的基準」と呼ばれることが多い
説明変数
- アウトカムの違いに影響するかもしれない、あるいはその違いを説明できるかもしれないという要因のことを説明変数と呼ぶ
平均値
- データの合計をデータ数で割った値
- 一般的に「平均」と呼ばれるものは「算術平均」の場合が多いが、それ以外に「相乗平均」「幾何平均」などもある
- Numpyを使った平均値の計算
import numpy as np
np.mean()
中央値
- データを小さい順に並べたとき中央に位置する値
- データが偶数ある場合は中央の2つの値を足して2で割った値が中央値となる
- Numpyを使った中央値の計算
import numpy as np
np.median()
偏差
- データの各値と平均値との差のこと
- データの各値が平均値からどれくらい大きいのか、小さいのかを表す指標
- 偏差の総和は0であるので、偏差の平均も0
分散(Variance)
- 偏差の2乗の平均
- 分布の拡がりを表す統計量の1つで、バラつきの大きいデータなのか、小さいデータなのかが判断できる
- Numpyを使った分散の計算
import numpy as np
np.var()
標準偏差(Standard Deviation)
- 分散の正の平方根
- 分布の拡がりを表す統計量の1つで、バラつきの大きいデータなのか、小さいデータなのかが判断できる
- 分散と違って標準偏差は単位の次元がデータと同じなので、現実のデータの散らばり具合を表現する際には標準偏差が用いられる事が多い
- 「Standard Deviation」と呼び、略して「SD」と書かれる事が多い
- Numpyを使った標準偏差の計算
import numpy as np
np.std()
帰無仮説
- 本来主張したい仮説に対して、あえて「自説を完全に覆すような仮説」のことを帰無仮説と呼ぶ
- 統計的仮説検定の際にとりあえず立てる仮説のことで、対立仮説の方が重要であることが多い
- 例えば、帰無仮説として「差がない」という仮説が立てられた場合、これが棄却されることにより、対立仮説の「差がある」を結論とする。
- 参考:帰無仮説 | 統計用語集 | 統計WEB
対立仮説
- 統計的仮説検定において、帰無仮説が棄却されたときに採択される仮説のこと
- 本来主張したい仮説のことは帰無仮説と対立する仮説ということで対立仮説と呼ぶ
- 参考:対立仮説 | 統計用語集 | 統計WEB
p値
- 統計的仮説検定において、帰無仮説の元で検定統計量がその値となる確率のこと
- 一般的にp値が5%以下の場合に帰無仮説を偽として棄却し、対立仮説を採択する
- 多くの場合「有意差がある」という仮説を主張したい事が多いので、その場合は「有意差が無い」という帰無仮説となる確率p値が0.05以下であれば「有意差がある」と判断できる
- 参考:P値 | 統計用語集 | 統計WEB
分析手法詳細
t検定
- 平均値が偶然のデータのばらつきによって生じるようなものなのか否かを考えるための手法
- →2つの平均値に有意差があるかを確認する手法
- アウトカムが量的(数値的)で、説明変数が質的の場合に使える手法
- z検定は件数が多い時に使い、t検定は件数が少ない時でも使える手法だが、t検定を使っていれば件数が多い場合も少ない場合もカバーできるためt検定を使うのが一般的
- 一般的にp値が0.05以下であれば有意差ありと判定できる
- t検定が使えるのは母集団が正規分布に従うと仮定するパラメトリック検定法であり、母集団が正規分布でない場合(外れ値がある場合)は有意差が出にくい
- t検定の種類には以下4つがある(参考:t検定の考え方)
- 1群のt検定
- 対応のあるt検定
- スチューデントのt検定
- ウェルチのt検定
1標本のt検定(1群のt検定)
- 平均値が特定の値と等しいかを確認する検定
- Pythonを使った1標本のt検定
# 1標本のt検定
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
coffee = np.array([
210.9, 195.4, 202.1 , 211.3, 195.5,
212.9, 210.9, 198.3, 202.1, 215.6,
204.7, 212.2, 200.7, 206.1, 195.8
])
t, p = sp.stats.ttest_1samp(coffee, popmean=200)
print('母平均:', np.mean(coffee))
print('母平均が200のt値:', t)
print('母平均が200である確率(p値):', p)
実行結果
母平均: 204.96666666666664
母平均が200のt値: 2.751076959309973
母平均が200である確率(p値): 0.015611934395473872
対応のあるt検定
- 同じ母集団を対象とした2つの平均値の差を比較する検定
- 参考:対応のあるt検定
- Pythonを使った対応のあるt検定
# 対応のあるt検定
import numpy as np
from scipy import stats
A = np.array([0.7, -1.6, -0.2, -1.2, -0.1, 3.4, 3.7, 0.8, 0.0, 2.0])
B = np.array([1.9, 0.8, 1.1, 0.1, -0.1, 4.4, 5.5, 1.6, 4.6, 3.4])
print('A平均:', np.mean(A))
print('B平均:', np.mean(B))
stats.ttest_rel(A, B)
実行結果
A平均: 0.75
B平均: 2.3299999999999996
Ttest_relResult(statistic=-4.062127683382037, pvalue=0.00283289019738427)
スチューデントのt検定
- 2つのデータ間に対応がなく(母集団が違う場合で)、かつ2つのデータの分散に等分散性が仮定できるときに用いる検定
- 一般的に「t検定」と呼ぶときは「スチューデントのt検定」を指す場合が多い
- 参考:スチューデントのt検定
- Pythonを使ったスチューデントのt検定
# スチューデントのt検定
import numpy as np
from scipy import stats
A = np.array([6.3, 8.1, 9.4, 10.4, 8.6, 10.5, 10.2, 10.5, 10.0, 8.8])
B = np.array([4.8, 2.1, 5.1, 2.0, 4.0, 1.0, 3.4, 2.7, 5.1, 1.4, 1.6])
print('A平均:', np.mean(A))
print('B平均:', np.mean(B))
stats.ttest_ind(A, B)
実行結果
A平均: 9.28
B平均: 3.0181818181818185
Ttest_indResult(statistic=9.851086859836649, pvalue=6.698194360479442e-09)
ウェルチのt検定
- 2つのデータ間に対応がなく(母集団が違う場合で)、2つのデータの母分散が等しいとは限らないときに用いる検定
- 参考:ウェルチのt検定
- Pythonを使ったウェルチのt検定
# ウェルチのt検定
import numpy as np
from scipy import stats
A = np.array([13.8, 10.2, 4.6, 10.0, 4.2, 16.1, 14.4, 4.9, 7.7, 11.4])
B = np.array([3.3, 2.6, 4.0, 4.7, 1.9, 2.9, 4.7, 5.3, 4.3, 3.0, 2.0])
print('A平均:', np.mean(A))
print('B平均:', np.mean(B))
stats.ttest_ind(A, B, equal_var=False)
実行結果
A平均: 9.73
B平均: 3.5181818181818176
Ttest_indResult(statistic=4.426442804187721, pvalue=0.0012285738375064346)
ウィルコクソンの順位和検定
- t検定と同じく平均値が偶然のデータのばらつきによって生じるようなものなのか否かを考えるための手法
- t検定との違いとして、ウィルコクソンの順位和検定はノンパラメトリック検定のひとつで母集団が正規分布に従わなくても使える手法
- 2つのデータ間に対応がないときに用いる検定法で、スチューデントのt検定とかウェルチのt検定に相当する
- 参考
- Pythonを使ったウィルコクソンの順位和検定
import numpy as np
from scipy import stats
A = np.array([1.83, 1.50, 1.62, 2.48, 1.68, 1.88, 1.55, 3.06, 1.30, 2.01, 3.11])
B = np.array([0.88, 0.65, 0.60, 1.05, 1.06, 1.29, 1.06, 2.14, 1.29])
print('A平均:', np.mean(A))
print('B平均:', np.mean(B))
stats.mannwhitneyu(A, B, alternative='two-sided')
実行結果
A平均: 2.0018181818181815
B平均: 1.1133333333333333
MannwhitneyuResult(statistic=91.0, pvalue=0.0018253610099931035)
※マンホイットニーのU検定と同じ事からmannwhitneyu
を使うことに注意
ウィルコクソンの符号順位和検定
- ウィルコクソンの順位和検定と同じく平均値が偶然のデータのばらつきによって生じるようなものなのか否かを考えるための手法
- 「ウィルコクソンの順位和検定」と「ウィルコクソンの符号順位和検定」は別物
- 「ウィルコクソンの順位和検定」は2つのデータ間に対応がないときに用いる検定法、「ウィルコクソンの符号順位和検定」は2つのデータ間に対応があるときに用いる検定法
- 対応のあるt検定に相当する検定
- 参考:ウィルコクソンの符号順位検定
- Pythonを使ったウィルコクソンの符号順位検定
import numpy as np
from scipy import stats
A = np.array([1.83, 1.50, 1.62, 2.48, 1.68, 1.88, 1.55, 3.06, 1.30])
B = np.array([0.88, 0.65, 0.60, 1.05, 1.06, 1.29, 1.06, 2.14, 1.29])
print('A平均:', np.mean(A))
print('B平均:', np.mean(B))
stats.wilcoxon(A, B)
実行結果
A平均: 1.8777777777777775
B平均: 1.1133333333333333
WilcoxonResult(statistic=0.0, pvalue=0.007685794055213263)
※wilcoxon
を使うことに注意
割合の差の検定(比率の差の検定)
- 独立性の検定の1つで、2つの数値に差があるのかを判断するための手法
- アウトカムが質的で、説明変数も質的の場合に使える手法
- WEBサービスのABテストでCVR,CTRの違いを検定する時に用いられる手法
- ※CVR,CTRはコンバージョンした/してない、クリックした/してないというアウトカムを質的と捉える
- 参考:母比率の差の検定
- WEBツールでも簡単に割合の差を確認できるツールがある
- Pythonを使った割合の差の検定
import math
import scipy.stats as st
a_pattern = 80000
a_pattern_size = 1600
b_pattern = 80000
b_pattern_size = 1720
a_proportion = a_pattern_size/a_pattern
b_proportion = b_pattern_size/b_pattern
print("Aパーン:", '{:.2%}'.format(a_proportion))
print("Bパターン:", '{:.2%}'.format(b_proportion))
pool = (a_pattern*a_proportion + b_pattern*b_proportion)/(a_pattern + b_pattern)
z = (a_proportion - b_proportion)/math.sqrt(pool*(1 - pool)*(1/a_pattern + 1/b_pattern))
print("母比率の差の統計検定量(z検定) = ", z)
p_one_side = st.norm.cdf(z) #片側検定
p_both_side = st.norm.cdf(z)*2 #両側検定
print("母比率の差の統計検定量(p値 片側) = ", p_one_side)
print("母比率の差の統計検定量(p値 両側) = ", p_both_side)
実行結果
Aパーン: 2.00%
Bパターン: 2.15%
母比率の差の統計検定量(z検定) = -2.1045798795315243
母比率の差の統計検定量(p値 片側) = 0.017663947290316492
母比率の差の統計検定量(p値 両側) = 0.035327894580632985
カイ二乗検定
- 独立性の検定の1つで、2つの変数に関連が言えるのか否かを判断するための手法
- アウトカムが質的で、説明変数も質的の場合に使える手法
- 実際に計測されたデータと、2つの変数が独立していた場合(関連してない場合)に得られるであろう結果を比較する事で2つの変数の関連性を調べるやり方
- t検定と同様に帰無仮説として「関連しない」という仮説が立てられた場合、一般的にp値が0.05以下の場合にこれが棄却され対立仮説の「関連する」を結論とする。
- 参考
- Pythonを使ったカイ二乗検定
# カイ二乗検定
import numpy as np
import pandas as pd
from scipy import stats
# サンプルデータ
sex = np.random.choice(['male', 'female'], size=20)
vote = np.random.choice(['agree', 'against'], size=20)
cross = pd.crosstab(index=sex, columns=vote)
print(cross)
x2, p, dof, expected = stats.chi2_contingency(cross, correction=False)
print("カイ二乗値:", x2)
print("p値:", p)
print("自由度は", dof)
print(expected)
実行結果
vote against agree
sex
female 5 5
male 6 4
カイ二乗値: 0.20202020202020202
p値: 0.653095114932182
自由度は 1
[[5.5 4.5]
[5.5 4.5]]
※correction=False
は補正を行わないという指定
- 参考:クロス集計とカイ二乗検定
重回帰分析
- 量的な説明変数が増えるごとに、平均してどれだけアウトカムが増えるか(減るか)という傾向性を示す分析手法
- 1つの説明変数と1つのアウトカムとの間の関係性を分析するのが単回帰分析で、複数の説明変数とアウトカムの関係性を一気に分析できるのが重回帰分析
- アウトカムが量的で、説明変数も量的の場合に使える手法
- 前提としては「他の説明変数が同じ」だとするとそれ以外の説明変数が変化した時にアウトカムがどう変化するかを調べるもので、変数同士に相関関係がある場合は 「多重共線性(マルチコ=multicolinearity)」 に陥る危険があることに注意する
- 説明変数がアウトカム(目的変数)の値を変化させるため、説明変数からアウトカム(目的変数)の「値」を予測することも可能
- 参考:重回帰分析とは
- Pythonを使った単回帰分析(scikit-learn)
# テストデータの登録
import pandas as pd
import numpy as np
data = pd.DataFrame({'output': [10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
'input01': [1, 3, 5, 6, 10, 14, 8, 17, 15, 20],
'input02': [5, 10, 20, 30, 35, 35, 50, 70, 85, 100]},)
data.head()
# input01とoutputで単回帰分析
from sklearn import linear_model
model = linear_model.LinearRegression()
# 説明変数にinput01を利用
X = data.loc[:, ['input01']].values
# 目的変数にoutputを利用
Y = data['output'].values
# 予測モデルを作成
model.fit(X, Y)
print('モデルのパラメータ:', model.get_params())
print('回帰係数:', model.coef_)
print('切片 (誤差):', model.intercept_)
print('決定係数(X,Yの相関):', model.score(X, Y))
print('回帰式:[alcohol] = %s × [density] + %s' % (model.coef_[0], model.intercept_))
実行結果
モデルのパラメータ: {'copy_X': True, 'fit_intercept': True, 'n_jobs': None, 'normalize': False}
回帰係数: [4.45327487]
切片 (誤差): 10.912578788709233
決定係数(X,Yの相関): 0.8771602016326598
回帰式:[alcohol] = 4.45327486982735 × [density] + 10.912578788709233
- Pythonを使った単回帰分析(statsmodels)
# statsmodelsを使った回帰分析
import statsmodels.api as sm
model = sm.OLS(Y, sm.add_constant(X))
result = model.fit(disp=0)
print(result.summary())
実行結果
OLS Regression Results
==============================================================================
Dep. Variable: y R-squared: 0.877
Model: OLS Adj. R-squared: 0.862
Method: Least Squares F-statistic: 57.13
Date: Fri, 20 Mar 2020 Prob (F-statistic): 6.56e-05
Time: 23:33:37 Log-Likelihood: -37.282
No. Observations: 10 AIC: 78.56
Df Residuals: 8 BIC: 79.17
Df Model: 1
Covariance Type: nonrobust
==============================================================================
coef std err t P>|t| [0.025 0.975]
------------------------------------------------------------------------------
const 10.9126 6.833 1.597 0.149 -4.845 26.670
x1 4.4533 0.589 7.558 0.000 3.095 5.812
==============================================================================
Omnibus: 5.725 Durbin-Watson: 2.878
Prob(Omnibus): 0.057 Jarque-Bera (JB): 2.315
Skew: 1.150 Prob(JB): 0.314
Kurtosis: 3.513 Cond. No. 22.4
==============================================================================
# グラフ化
import matplotlib.pyplot as plt
# 散布図
plt.scatter(X, Y)
# 回帰直線
plt.plot(X, model.predict(X), color='black')
- Pythonを使った重回帰分析(scikit-learn)
# 正規化して重回帰分析
from sklearn import linear_model
model = linear_model.LinearRegression()
# データフレームの各列を正規化
data2 = data.apply(lambda x: (x - np.mean(x)) / (np.max(x) - np.min(x)))
data2.head()
# 説明変数にoutput以外を利用
data2_except_output = data2.drop("output", axis=1)
X = data2_except_output
# 目的変数にoutputを利用
Y = data2['output'].values
# 予測モデルを作成
model.fit(X, Y)
# 偏回帰係数
print(pd.DataFrame({"name":data2_except_output.columns,
"result":np.abs(model.coef_)}).sort_values(by='result') )
print('切片(誤差):', model.intercept_)
実行結果
name result
0 input01 0.295143
1 input02 0.707205
切片(誤差): 1.6679414843100476e-17
- Pythonを使った重回帰分析(statsmodels)
# statsmodelsを使った重回帰分析
import statsmodels.api as sm
# 予測モデルを作成
model = sm.OLS(Y, sm.add_constant(X))
result = model.fit(disp=0)
print(result.summary())
実行結果
OLS Regression Results
==============================================================================
Dep. Variable: y R-squared: 0.962
Model: OLS Adj. R-squared: 0.951
Method: Least Squares F-statistic: 87.64
Date: Fri, 20 Mar 2020 Prob (F-statistic): 1.11e-05
Time: 23:36:48 Log-Likelihood: 13.530
No. Observations: 10 AIC: -21.06
Df Residuals: 7 BIC: -20.15
Df Model: 2
Covariance Type: nonrobust
==============================================================================
coef std err t P>|t| [0.025 0.975]
------------------------------------------------------------------------------
const -1.388e-17 0.024 -5.87e-16 1.000 -0.056 0.056
input01 0.2951 0.180 1.636 0.146 -0.132 0.722
input02 0.7072 0.180 3.923 0.006 0.281 1.133
==============================================================================
Omnibus: 6.793 Durbin-Watson: 1.330
Prob(Omnibus): 0.033 Jarque-Bera (JB): 2.620
Skew: 1.171 Prob(JB): 0.270
Kurtosis: 3.896 Cond. No. 10.5
==============================================================================
- ※
scikit-learn
は機械学習、statsmodels
は統計学として使う場合が多い。scikit-learn
だとp値は自前で計算する必要があるがstatsmodels
は自動で計算してくれる - 参考
ロジスティック回帰分析
- 「説明変数が1増える事にアウトカムが1になる割合(確率=オッズ)が約何倍になるか」というオッズ比を求めて比較する手法
- アウトカムが質的で、説明変数が量的の場合に使える手法
- 2値論理に関するアウトカムを分析するための回帰分析(「確率」について回帰分析で、単回帰か重回帰かという区別はない)
- 説明変数からアウトカム(目的変数)が1になる確率が分かるため、説明変数からアウトカム(目的変数)が1になる確率を予測することも可能
- 参考:【ロジスティック回帰分析】使用例やオッズ比、エクセルでの使い方も紹介!
- Pythonを使ったロジスティック回帰分析
# テストデータの登録
import pandas as pd
import numpy as np
data = pd.DataFrame({'性別': [1, 1, 0, 1, 0, 0, 1, 1, 0, 0],
'学生': [0, 0, 0, 0, 1, 0, 0, 1, 0, 1],
'滞在時間(秒)': [34, 28, 98, 70, 67, 23, 67, 56, 41, 90],
'ユーザー登録': [0, 0, 1, 0, 1, 0, 1, 1, 0, 1]},)
data
# statsmodelsを使ったロジスティック回帰分析
from sklearn.linear_model import LogisticRegression
import statsmodels.api as sm
# 説明変数にユーザー登録以外を利用
X = data[['性別', '学生', '滞在時間(秒)']]
# 目的変数にユーザー登録を利用
Y = data['ユーザー登録'].values
model = sm.Logit(Y, sm.add_constant(X))
result = model.fit(disp=0)
print('---サマリー---')
print(result.summary())
print('---対数オッズ---')
print(result.params)
print('---p値---')
print(result.pvalues)
print('---変数が1単位増加したとき、事象が発生する確率が何%に増加するか(量的評価)---')
print('滞在時間(秒):', 1 / (1 + np.exp(-result.params['滞在時間(秒)'])))
print('滞在時間(秒):', np.exp(result.params['滞在時間(秒)']) / (1 + np.exp(result.params['滞在時間(秒)'])))
print('---変数が1になると事象が発生する確率は、事象が発生しない確率の何倍か(質的評価)---')
print('性別:', np.exp(result.params['性別']))
print('学生:', np.exp(result.params['学生']))
実行結果
---サマリー---
Logit Regression Results
==============================================================================
Dep. Variable: y No. Observations: 10
Model: Logit Df Residuals: 6
Method: MLE Df Model: 3
Date: Fri, 20 Mar 2020 Pseudo R-squ.: 0.7610
Time: 10:12:42 Log-Likelihood: -1.6565
converged: False LL-Null: -6.9315
Covariance Type: nonrobust LLR p-value: 0.01443
==============================================================================
coef std err z P>|z| [0.025 0.975]
------------------------------------------------------------------------------
const -9.5614 10.745 -0.890 0.374 -30.622 11.499
性別 0.1543 5.170 0.030 0.976 -9.979 10.287
学生 22.7574 3.26e+04 0.001 0.999 -6.38e+04 6.38e+04
滞在時間(秒) 0.1370 0.139 0.988 0.323 -0.135 0.409
==============================================================================
Possibly complete quasi-separation: A fraction 0.30 of observations can be
perfectly predicted. This might indicate that there is complete
quasi-separation. In this case some parameters will not be identified.
---対数オッズ---
const -9.561361
性別 0.154283
学生 22.757416
滞在時間(秒) 0.136965
dtype: float64
---p値---
const 0.373568
性別 0.976193
学生 0.999442
滞在時間(秒) 0.323037
dtype: float64
---変数が1単位増加したとき、事象が発生する確率が何%に増加するか(量的評価)---
滞在時間(秒): 0.5341877701226888
滞在時間(秒): 0.5341877701226888
---変数が1になると事象が発生する確率は、事象が発生しない確率の何倍か(質的評価)---
性別: 1.1668207000698392
学生: 7645749443.830123