SPSS Modelerの平均値ノードをpythonに書き換える

SPSS Modelerの平均値ノードをPythonのscipyとstatsmodelsで書き換えてみます。


以下の記事でModelerでの使い方は解説していますので、この内容をscipyとstatsmodelsで行ってみます。

SPSS Modeler ノードリファレンス 6-9 平均値 - Qiita 

https://qiita.com/416nishimaki/items/f5b957a24430b9a3aa9e



# 1m．２つのフィールドの平均を比較　Modeler

同じ顧客の施策前と施策後の平均を比較します。これは対応ある2標本のt検定になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/ebbd6fc4-1d7a-7252-f60e-57933cf18559.png)

平均値ノードの「フィールドのペア間で比較」を選び「実施後」と「実施前」の列のペアを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/ddda5581-7afd-1a02-0a93-d56ab0477d90.png)

「実施後」と「実施前」の相関係数と平均値差、そして「重要度」はt検定の「1-p値」をあらわしています。
デフォルトでは、相関係数もt検定も「1-p値」が0.95以上だと「強い」または「重要」と判定されます。つまり相関または平均値差が統計的に優位であるかどうかを示しています。これで施策の効果に優位な差があったのかを判断することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/73e8daed-d14f-4ae3-4f91-9a210d9c47f5.png)


なお、表示を「詳細」にすると各列と平均値差の標準偏差、標準誤差、度数、また平均値差の95％信頼区間、T値、自由度が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/b5a2ebf3-e9bc-c4ba-c006-52c247eeefd0.png)


* 参考　24-5. 対応のある2標本t検定 | 統計学の時間 | 統計WEB 

https://bellcurve.jp/statistics/course/9453.html

# 1p．２つのフィールドの平均を比較　scipy
まず列の基本指標を、pandasのメソッドなどで計算します。
mean:平均
std:標準偏差
sem:標準誤差
len:度数

```python:列の基本指標
columns=['実施後','実施前']

-----中略------

#列の基本指標
for i,c in enumerate(columns):
    sttest['平均'+str(i+1)+'m']=df[c].mean()
    if details==True:
        sttest['平均'+str(i+1)+'s']=df[c].std()
        sttest['平均'+str(i+1)+'se']=df[c].sem()
        sttest['平均'+str(i+1)+'n']=len(df[c])

```

次に相関係数とp値をscipyのstats.pearsonrで計算します。
```python:相関係数
from scipy import stats
r,p=stats.pearsonr(df[columns[0]],df[columns[1]])
sttest['相関係数']=r
sttest['相関係数検定1-p']=1-p
```

次に２列の平均値差を絶対値absで求めます。

```python:平均値差
m = abs(df[columns[0]].mean()-df[columns[1]].mean())
s = (df[columns[0]]-df[columns[1]]).std()
```

そして、stats.ttest_relをつかって、対応ある2標本のt検定を行います。
stats.t.intervalをつかって95％信頼区間を求めます。
```python:対応ある2標本のt検定
t,p=stats.ttest_rel(df[columns[0]],df[columns[1]])
m = abs(df[columns[0]].mean()-df[columns[1]].mean())
s = (df[columns[0]]-df[columns[1]]).std()
se =  m/t
n = len(df[columns[0]])

d_f = len(df[columns[0]])-1
ci = stats.t.interval( alpha=0.95, loc=m, scale=se, df=d_f )
```

Modelerの「シンプル」表示と同等の項目だと以下の結果が得られます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/aa1edb84-dae1-fe3e-f706-9ee6687bad66.png)

Modelerの「詳細」表示と同じ項目だと以下の結果が得られます。
pandasでは複数項目を１セルに入れられないので、横に長くなってしまっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/73ae9557-16da-e20b-1c2e-c003811b27e1.png)

* 参考
* Python で無相関検定をしてみる - GIS奮闘記 

https://www.gis-py.com/entry/hypothesis-testing

* 【統計】Pythonでt検定をやってみる | naoの学習＆学習 
 
https://www.learning-nao.com/?p=2620

* Pythonでt検定 2クラスの試験成績の比較 - Qiita 

https://qiita.com/code0327/items/a96dd2fbd8a491d2eeaa

# 2m．複数カテゴリの平均を比較　Modeler

今度は6ブランドのイメージスコアにブランド毎の平均に差があるかを検定します。3つ以上のカテゴリ比較が可能になるように、一元配置分散分析で検定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/9d45c2b0-7d74-9d3c-2e2c-b76713f794d0.png)

「フィールド内のグループ間で比較」を選び「グループ化フィールド」に比較したいカテゴリ列を指定します。この例では「ブランド」です。
「テストフィールド」に比較したい値を設定します。この例では「イメージスコア」です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/6de0e4a1-e04a-0789-183c-ea5fb708cd13.png)

各ブランド列内にあったA,B,L,M,N,Oというブランド毎に平均が計算され、この平均の差が統計的に優位かどうかを検定しています。「重要度」はF検定の「1-p値」をあらわしています。
デフォルトでは、F検定も「1-p値」が0.95以上だと「重要」と判定されます。つまり平均値差が統計的に優位であるかどうかを示しています。これでブランド毎のイメージスコアに優位な差があったのかを判断することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/3b9c6449-e7ad-0c8e-2be7-54ccfde5c3e8.png)

表示を「詳細」にすると各列の標準偏差、標準誤差、度数、またF値、自由度が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/3a3fbbd9-edb6-1599-8e69-a40a7af7c0f6.png)


「グラフボード」ノードで箱ひげ図をつくると視覚的にも確認ができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/45f15f7c-61e4-1196-30ee-fc557ed4eddf.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/e939e88c-444b-8495-62a2-246e813809c3.png)



# 2p．複数カテゴリの平均を比較 scipyとstatsmodels

最初に「グループ化フィールド」に比較したいカテゴリ列を指定します。この例では「ブランド」です。
「テストフィールド」に比較したい値を設定します。この例では「イメージスコア」です。
「グループ化フィールド」になる列のデータ型はastype("category")でカテゴリ型に変更しておきます。

```python:対象列定義
grpcolumn='ブランド'
testcolumns=['イメージスコア']
df1[grpcolumn]=df1[grpcolumn].astype("category")
```

各カテゴリ(cat)の集計対象フィールド（col)の平均値meanなどを集計します。
query(grpcolumn+'==@cat')で、各カテゴリごとに集計しています。
具体的には
df1.query('ブランド=="A"')['イメージスコア'].mean()
のような計算が行われます。

```python:カテゴリの基本指標
for cat in df1[grpcolumn].cat.categories:
    sanova[cat+'平均']=df1.query(grpcolumn+'==@cat')[col].mean()
    if details==True:
        sanova[cat+'s']=df1.query(grpcolumn+'==@cat')[col].std()
        sanova[cat+'se']=df1.query(grpcolumn+'==@cat')[col].sem()
        sanova[cat+'n']=len(df1.query(grpcolumn+'==@cat')[col])
```
次にstatsmodelsで一元配置分散分析を行います。
最小二乗法で回帰モデルをつくって、そこから分散分析結果を取り出す手順になります。
回帰モデルは以下で作ります。
ols(col+' ~ ' + grpcolumn, data=df1).fit() 
具体的には
ols('イメージスコア ~ ブランド', data=df1).fit()
のような計算が行われます。
回帰モデルから分散分析の結果を抽出します。
anov = sm.stats.anova_lm(model)

分散分析の結果は以下のようなpandasのDataFrameで取得できますのでここからloc[grpcolumn,'F']などで必要な指標を取り出します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/4eeca0cf-0be9-bbb9-ee8e-4a49a6b103f3.png)


```python:一元配置分散分析
from statsmodels.formula.api import ols
model = ols(col+' ~ ' + grpcolumn, data=df1).fit()                
import statsmodels.api as sm
anov = sm.stats.anova_lm(model)
if details==True:
    sanova['F検定']=anov.loc[grpcolumn,'F']
    sanova['自由度']=anov.loc[grpcolumn,'df']
sanova['F検定1-p']=1-anov.loc[grpcolumn,'PR(>F)']
```

Modelerの「シンプル」表示と同等の項目だと以下の結果が得られます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/02b3971b-1939-499a-34bb-a2da30d31fc5.png)

Modelerの「詳細」表示と同じ項目だと以下の結果が得られます。
pandasでは複数項目を１セルに入れられないので、横に長くなってしまっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/88a47cc8-4614-423b-d557-a7be6565b7ab.png)


箱ひげ図はseabornで書きました。
具体的には
sns.boxplot(x='ブランド', y='イメージスコア',
                data=df, showfliers=False, ax=ax)
のように書いています。

```python:一元配置分散分析
sns.set(font='Meiryo')
for col in testcolumns:
    fig = plt.figure(figsize=(20, 3), constrained_layout=True)
    ax = fig.add_subplot(1, 1, 1)
    sns.boxplot(x=grpcolumn, y=col,
                data=df, showfliers=False, ax=ax)
    plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/caa5eb3d-e142-c530-79c3-737f6047b673.png)

* 参考

* pythonで統計学基礎：03 検定・分散分析 | 化学の新しいカタチ 

https://future-chem.com/statistics-intro-03/

* Pythonを用いた分散分析（ANOVA）あれこれ（1） - 工場裏のアーカイブス 

https://chemicalfactory.hatenablog.com/entry/2022/05/04/234408#

* statsmodel

https://www.statsmodels.org/dev/generated/statsmodels.stats.anova.anova_lm.html


# 3m．複数カテゴリの複数フィールドの平均を比較　Modeler

ノードリファレンスにはなかったのですが、Modelerの「平均値」ノードを使うと複数カテゴリで複数フィールドの平均値を比較して、特徴量を重要度順に並べ替えることができます。これはとても強力なので例をご紹介します。

以下のようなデータを用意しました。
各顧客に対して、アクセサリの購入金額合計などのさまざまな集計がなされています。
「マネタリー５」というフラグ列があり、これは購入金額が全体の上位20%の優良顧客であるということを示しています。
この優良顧客とそうでない顧客でどんな差があるかどうかを「平均値」ノードを使って調べてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/3c68d8f3-ad0f-85ba-47bf-68d140a75208.png)

「フィールド内のグループ間で比較」を選び「グループ化フィールド」に比較したいカテゴリ列を指定します。この例では「マネタリー5」です。
「テストフィールド」に比較したい値を設定します。ここではすべての数値列を選びました。この中から特に平均値の差を注目すべき特徴量を探します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/56500bcf-02a0-eb25-ed38-aff3b8e0b397.png)

表示を「詳細」にして、「F検定」の降順で並べ替えます。
そうするとF値が大きい順に並びますので、優良顧客とそれ以外で平均値の違いが顕著な順に特徴量が並べ替えられます。（「重要度」で並べ替えても本来は同じ順序になるべきなのですが、p値がとても小さいと丸められてしまって順位がつかなくなるのでF値で並べ替えるのがコツです）


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/12366b45-213b-c845-c388-fa2a9d6ae852.png)

中身をみてみると、「Monetary」や「マネタリースコア」が最上位に来ています。これは「購入金額が全体の上位20%の優良顧客」が「マネタリー5」＝Trueなのでチートになり、当たり前の結果です。
次点の「Frequency」は優良顧客は77.9回、そうでない顧客は21回なので大きな差があることがわかります。
このようにチートも含まれますが、たくさんの特徴量候補があるときに、とりあえず、すべての特徴量から重要そうな特徴量を知りたいときにとても便利です。

ちなみに「特徴量選択」ノードでも、目的変数がカテゴリ型で説明変数が数値型の場合は、同様のロジックを使っています。ただ「平均値」ノードの場合は各カテゴリの平均値も表示されるので、具体的な値で各カテゴリでどちらがどれだけ大きいのか小さいのかも比べることができるのがメリットです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/41105bb1-4180-fb3e-5ea4-72b206859348.png)



# 3p．複数カテゴリの複数フィールドの平均を比較　scipyとstatsmodels

Pythonの場合は各列の中から
re.sub(r'[0-9]+', '', df2[col].dtype.name) in ['int', 'float']:
で数値列を探して分散分析を行います。
そして、sort_values('F検定',ascending=False)でF値順に並べ替えています。

```python:一元配置分散分析
import re
grpcolumn='マネタリー5'
testcolumns=[]
for col in df2:
    if re.sub(r'[0-9]+', '', df2[col].dtype.name) in ['int', 'float']:
        testcolumns.append(col)

df2[grpcolumn]=df2[grpcolumn].astype("category")
df_anova=anova(df2, testcolumns,grpcolumn,details=True).sort_values('F検定',ascending=False)
```

以下のようにF値の大きな順で特徴量を並べ替えることができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/a2ba0dd0-186f-1d74-6e8f-5afc75bd44c3.png)

さらにこの並べ替えた特徴量順に箱ひげ図を表示します。

```python:箱ひげ図
boxplot(df2,df_anova['フィールド'],grpcolumn)
```
確かにFrequencyで優良顧客かどうかで大きな差があることがよくわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/d53b0240-3857-5e49-e6df-7802f4e1a9a6.png)


# サンプル
サンプルは以下に置きました。

#### サンプルストリーム

https://github.com/hkwd/200611Modeler2Python/raw/master/mean/mean.str


#### notebook
https://github.com/hkwd/200611Modeler2Python/blob/master/mean/mean.ipynb

#### 利用データ

https://raw.githubusercontent.com/hkwd/200611Modeler2Python/master/mean/t-test.csv

https://raw.githubusercontent.com/hkwd/200611Modeler2Python/master/mean/anova1.csv

https://raw.githubusercontent.com/hkwd/200611Modeler2Python/master/mean/goodcustomer.csv

■テスト環境
Modeler 18.4
Windows 10 64bit
Python 3.8.10
pandas 1.4.1
scipy 1.8.0
statsmodels 0.13.2