広告/統計/アニメ/映画 等に関するブログ

広告/統計/アニメ/映画 等に関するブログ

Pythonでアンケート調査のクラスター分析と決定木分析を行う

アンケート調査の分析をするのはマーケティング担当者で、恐らく大学時代は社会学や心理学といった文系出身だと思います。昔ならSPSS、最近ならRだと思います。

一方で、Pythonはどちらかというと情報学系の人やシステムエンジニアが使うツール(言語)でPythonでアンケート分析を真っ向からしている書籍は存外少ないものです。最近私はRからPythonへの全面的な移行を考えているのですが、備忘録も兼ねて、Pythonでアンケート調査を行ってみました。

事前準備・前処理

先ずは予め読み込んでおいた方が良いLibrary類をインポートしておきます。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

seabornのテーマをデフォルトで選ぶようにしておきます。

#sns.set()
sns.set(font="Noto Sans CJK JP")

データの準備

アンケート調査のクラスター分析のためには集計されたクロス表ではなく、その前のローデータ(0,1の回答レコードの行列)が必要なのですが、なかなか個票データを公開しているところがなく、UCIのサイトから拝借することにしました。1

http://archive.ics.uci.edu/ml/datasets.php

ある都市に対する満足度の評価です

http://archive.ics.uci.edu/ml/datasets/Somerville+Happiness+Survey

提供されているCSVファイルがWindows文字コードだったようですのでエンコードの指定をして読み込みます2

df = pd.read_csv("data/SomervilleHappinessSurvey2015_2.csv",
                 encoding="cp932",
                 header=0)
df.head(3)
D X1 X2 X3 X4 X5 X6
0 0 3 3 3 4 2 4
1 0 3 2 3 5 4 3
2 1 5 3 3 3 3 5

各設問についてはMCUのサイトに以下の補足があります。 今のままではわかりにくいので簡単な列名に変えたいと思います3

D = decision attribute (D) with values 0 (unhappy) and 1 (happy) 
X1 = the availability of information about the city services 
X2 = the cost of housing 
X3 = the overall quality of public schools 
X4 = your trust in the local police 
X5 = the maintenance of streets and sidewalks 
X6 = the availability of social community events 

Attributes X1 to X6 have values 1 to 5.
df = df.rename(columns={
    "D":"幸福かどうか",
    "X1":"行政サービス情報へのアクセスしやすさ",
    "X2":"住宅供給の高さ",
    "X3":"公立の学校の全般的な質の良さ",
    "X4":"地域警察への信頼の高さ",
    "X5":"道路・歩道のメンテナンス状況",
    "X6":"地域社会行事の利用のしやすさ"})
df.head(3)
幸福かどうか 行政サービス情報へのアクセスしやすさ 住宅供給の高さ 公立の学校の全般的な質の良さ 地域警察への信頼の高さ 道路・歩道のメンテナンス状況 地域社会行事の利用のしやすさ
0 0 3 3 3 4 2 4
1 0 3 2 3 5 4 3
2 1 5 3 3 3 3 5

何を分析するか?」をまだ決めていませんでしたが、例えば、「幸福度の高い人は市のどこを評価しているのか?」を探ることにしてみましょう。

その場合、「幸福だ」と答えているデータだけに絞る必要があります。

df_happy = df[df["幸福かどうか"]==1]
df_happy.head(3)
幸福かどうか 行政サービス情報へのアクセスしやすさ 住宅供給の高さ 公立の学校の全般的な質の良さ 地域警察への信頼の高さ 道路・歩道のメンテナンス状況 地域社会行事の利用のしやすさ
2 1 5 3 3 3 3 5
5 1 5 5 3 5 5 5
7 1 5 4 4 4 4 5

念のためにこれで何件あるか確認しましょう。あまりデータが少なすぎると分析の信憑性が下がります

len(df_happy)
77

また、幸福ではない人のデータは今回の分析では使わないので、「幸福かどうか」の列を削除します。

更に、インデックスの番号を新しいデータに対して振り直します。

df_happy = df_happy.drop("幸福かどうか", axis=1)
df_happy = df_happy.reset_index(drop=True)
df_happy.head(3)
行政サービス情報へのアクセスしやすさ 住宅供給の高さ 公立の学校の全般的な質の良さ 地域警察への信頼の高さ 道路・歩道のメンテナンス状況 地域社会行事の利用のしやすさ
0 5 3 3 3 3 5
1 5 5 3 5 5 5
2 5 4 4 4 4 5

データの標準化

5段階スケールできいている各設問について、「1」に集中しているものや、「5」に集中しているものがある筈です。4

df_happy.describe()
行政サービス情報へのアクセスしやすさ 住宅供給の高さ 公立の学校の全般的な質の良さ 地域警察への信頼の高さ 道路・歩道のメンテナンス状況 地域社会行事の利用のしやすさ
count 77.000000 77.000000 77.000000 77.000000 77.000000 77.000000
mean 4.545455 2.558442 3.415584 3.792208 3.831169 4.389610
std 0.679502 1.117958 1.004603 0.878660 1.056342 0.763576
min 3.000000 1.000000 1.000000 1.000000 1.000000 1.000000
25% 4.000000 2.000000 3.000000 3.000000 3.000000 4.000000
50% 5.000000 2.000000 3.000000 4.000000 4.000000 5.000000
75% 5.000000 3.000000 4.000000 4.000000 5.000000 5.000000
max 5.000000 5.000000 5.000000 5.000000 5.000000 5.000000

どの列も分散を等しくすることで、情報量を同じにできます。5

from sklearn import preprocessing
ss = preprocessing.StandardScaler()
df_happy_s = pd.DataFrame(ss.fit_transform(df_happy))
df_happy_s.head(3)
0 1 2 3 4 5
0 0.673326 0.397559 -0.416393 -0.907521 -0.791997 0.804625
1 0.673326 2.198267 -0.416393 1.383598 1.113745 0.804625
2 0.673326 1.297913 0.585552 0.238038 0.160874 0.804625

また列名が消えてしまいましたので振り直します。

幸い、先程の「df_happy」の列名をそのままコピペすればいいだけです

print(df_happy.columns)
Index(['行政サービス情報へのアクセスしやすさ', '住宅供給の高さ', '公立の学校の全般的な質の良さ', '地域警察への信頼の高さ',
       '道路・歩道のメンテナンス状況', '地域社会行事の利用のしやすさ'],
      dtype='object')
df_happy_s.columns = df_happy.columns
df_happy_s.head(3)
行政サービス情報へのアクセスしやすさ 住宅供給の高さ 公立の学校の全般的な質の良さ 地域警察への信頼の高さ 道路・歩道のメンテナンス状況 地域社会行事の利用のしやすさ
0 0.673326 0.397559 -0.416393 -0.907521 -0.791997 0.804625
1 0.673326 2.198267 -0.416393 1.383598 1.113745 0.804625
2 0.673326 1.297913 0.585552 0.238038 0.160874 0.804625

分析

因子分析

設問が多岐に渡るときは、設問を縮約すべきで、そういうときは因子分析を行いますが、今回は既に6問と少ないので行いません。

↓自分がやるまでもなく丁寧にまとめられた記事がありました。因子得点は fit_transformで出てきます。また、事前に自分で標準化していないと変な値になるので因子分析だけをするときも注意して下さい。

hk29.hatenablog.jp

scikit-learn.org

クラスター分析

「幸福度の高い人は市のどこを評価しているのか?」を探ると言っても、人によって重視ポイントが異なります。それを幾つかにタイプ分けをして把握するためにクラスター分析を行います

適切なクラスター数を探す

適切なクラスター数を決める手法に完全な決まりはありませんが、見た目で判断するために、階層的クラスタ分析を一度行うという手法がママあります。

from scipy.cluster.hierarchy import linkage, dendrogram

統計に細かい人にとってはどの手法を選ぶか?は重要だと思いますが、比較的一般的なユークリッド距離、ウォード法で階層的クラスター分析を行います

df_happy_s_hclust = linkage(df_happy_s,metric="euclidean",method="ward")
plt.figure(figsize=(12,8))
dendrogram(df_happy_s_hclust)
plt.savefig('figure_1.png')
plt.show()

f:id:yyhhyy:20190707224159p:plain

どの辺りで線をひくか?はかなり恣意的ですが、この場合は4グループぐらいが適切でしょうか?

f:id:yyhhyy:20190707224239p:plain

k平均法によるクラスター分析

実務においてはクラスターに分かれればそれでいいというわけではなく、解釈の容易性が求められます。それぞれのクラスターにはどういう違いがあるのか?そういったことを知るには、kmenasクラスター分析の方が便利です

from sklearn.cluster import KMeans

クラスター数を4つと決めたので引数に入れて関数を作成

km = KMeans(n_clusters=4,random_state=42)

skit-learnは直接pandasデータフレームを読み込まないのでnumpyの行列に変換する必要がある

df_happy_s_ar = df_happy_s.values
display(df_happy_s_ar)
array([[ 0.67332598,  0.39755886, -0.41639284, -0.90752108, -0.79199672,
         0.80462467],
       [ 0.67332598,  2.19826665, -0.41639284,  1.38359771,  1.11374539,
         0.80462467],
       [ 0.67332598,  1.29791276,  0.58555244,  0.23803832,  0.16087433,
         0.80462467],
       [ 0.67332598, -0.50279503,  0.58555244,  1.38359771,  1.11374539,
         0.80462467],
       [-2.28930833, -0.50279503,  0.58555244, -0.90752108,  0.16087433,
        -0.51359022],
       [ 0.67332598, -1.40314893,  0.58555244, -0.90752108,  0.16087433,
         0.80462467],
       [-0.80799118, -0.50279503, -0.41639284, -0.90752108,  0.16087433,
        -0.51359022],
       [-0.80799118, -0.50279503, -0.41639284, -0.90752108,  0.16087433,
        -0.51359022],
       [ 0.67332598, -1.40314893, -1.41833812,  1.38359771, -1.74486778,
        -0.51359022],
       [-0.80799118,  0.39755886, -0.41639284, -0.90752108, -0.79199672,
        -0.51359022],
       [-2.28930833,  0.39755886, -0.41639284,  1.38359771,  1.11374539,
         0.80462467],
       [-2.28930833,  0.39755886, -2.4202834 , -0.90752108, -0.79199672,
        -0.51359022],
       [-2.28930833,  0.39755886, -2.4202834 , -0.90752108, -0.79199672,
        -0.51359022],
       [ 0.67332598,  0.39755886, -0.41639284, -0.90752108,  1.11374539,
        -1.83180511],
       [-2.28930833, -0.50279503,  0.58555244,  0.23803832,  0.16087433,
         0.80462467],
       [-2.28930833, -0.50279503,  0.58555244,  0.23803832,  0.16087433,
         0.80462467],
       [-0.80799118, -1.40314893, -0.41639284, -3.19863987, -2.69773883,
        -0.51359022],
       [ 0.67332598,  0.39755886,  0.58555244, -0.90752108,  0.16087433,
         0.80462467],
       [ 0.67332598,  0.39755886,  0.58555244, -0.90752108,  0.16087433,
         0.80462467],
       [ 0.67332598, -0.50279503, -0.41639284, -0.90752108, -1.74486778,
         0.80462467],
       [-0.80799118,  1.29791276, -0.41639284,  0.23803832, -1.74486778,
        -0.51359022],
       [-0.80799118, -0.50279503,  0.58555244, -0.90752108, -1.74486778,
        -0.51359022],
       [-2.28930833, -1.40314893, -1.41833812,  0.23803832, -0.79199672,
         0.80462467],
       [ 0.67332598,  0.39755886,  0.58555244,  0.23803832,  0.16087433,
         0.80462467],
       [ 0.67332598,  0.39755886, -0.41639284,  0.23803832,  0.16087433,
         0.80462467],
       [ 0.67332598, -0.50279503,  1.58749772,  1.38359771,  1.11374539,
        -1.83180511],
       [ 0.67332598, -1.40314893, -0.41639284, -0.90752108,  0.16087433,
        -0.51359022],
       [ 0.67332598, -1.40314893, -0.41639284, -0.90752108,  0.16087433,
        -0.51359022],
       [ 0.67332598, -1.40314893, -0.41639284, -0.90752108,  0.16087433,
        -0.51359022],
       [ 0.67332598, -0.50279503,  0.58555244, -0.90752108,  0.16087433,
         0.80462467],
       [ 0.67332598, -0.50279503,  0.58555244, -0.90752108,  0.16087433,
         0.80462467],
       [-0.80799118,  0.39755886, -1.41833812,  0.23803832, -0.79199672,
        -0.51359022],
       [-0.80799118,  0.39755886, -1.41833812,  0.23803832, -0.79199672,
        -0.51359022],
       [ 0.67332598,  0.39755886,  1.58749772,  1.38359771,  0.16087433,
         0.80462467],
       [ 0.67332598, -0.50279503,  0.58555244, -2.05308048, -1.74486778,
        -0.51359022],
       [-0.80799118,  1.29791276, -0.41639284, -0.90752108, -1.74486778,
         0.80462467],
       [ 0.67332598, -0.50279503,  0.58555244,  0.23803832,  1.11374539,
         0.80462467],
       [ 0.67332598, -0.50279503,  0.58555244,  0.23803832,  1.11374539,
         0.80462467],
       [-0.80799118, -1.40314893, -0.41639284,  0.23803832,  0.16087433,
        -0.51359022],
       [-0.80799118, -1.40314893, -0.41639284,  0.23803832,  0.16087433,
        -0.51359022],
       [ 0.67332598, -1.40314893,  1.58749772,  1.38359771,  1.11374539,
         0.80462467],
       [ 0.67332598,  1.29791276,  1.58749772,  1.38359771,  1.11374539,
         0.80462467],
       [-0.80799118, -0.50279503, -1.41833812,  0.23803832,  0.16087433,
         0.80462467],
       [-0.80799118,  0.39755886, -0.41639284,  0.23803832, -0.79199672,
        -0.51359022],
       [-0.80799118,  0.39755886, -0.41639284,  0.23803832, -1.74486778,
        -0.51359022],
       [ 0.67332598, -0.50279503,  1.58749772,  1.38359771,  1.11374539,
         0.80462467],
       [ 0.67332598, -0.50279503, -0.41639284,  1.38359771,  1.11374539,
         0.80462467],
       [ 0.67332598,  2.19826665,  1.58749772,  1.38359771,  1.11374539,
         0.80462467],
       [-0.80799118,  0.39755886, -1.41833812,  0.23803832,  0.16087433,
        -0.51359022],
       [-0.80799118, -0.50279503,  0.58555244,  0.23803832,  0.16087433,
        -0.51359022],
       [ 0.67332598,  0.39755886, -1.41833812,  0.23803832,  0.16087433,
        -0.51359022],
       [ 0.67332598, -0.50279503, -0.41639284,  0.23803832,  0.16087433,
         0.80462467],
       [ 0.67332598,  0.39755886, -0.41639284, -0.90752108,  1.11374539,
         0.80462467],
       [ 0.67332598, -0.50279503,  0.58555244,  1.38359771,  0.16087433,
         0.80462467],
       [ 0.67332598, -1.40314893, -0.41639284,  0.23803832,  1.11374539,
         0.80462467],
       [ 0.67332598,  1.29791276,  1.58749772,  1.38359771,  1.11374539,
        -0.51359022],
       [ 0.67332598,  2.19826665, -0.41639284,  0.23803832,  0.16087433,
         0.80462467],
       [ 0.67332598,  1.29791276,  0.58555244, -0.90752108, -0.79199672,
        -0.51359022],
       [ 0.67332598,  1.29791276,  0.58555244,  0.23803832,  0.16087433,
         0.80462467],
       [ 0.67332598,  2.19826665,  1.58749772,  1.38359771,  1.11374539,
         0.80462467],
       [ 0.67332598,  1.29791276,  1.58749772,  0.23803832,  1.11374539,
        -0.51359022],
       [-0.80799118, -0.50279503, -0.41639284,  0.23803832, -0.79199672,
        -1.83180511],
       [ 0.67332598, -0.50279503, -1.41833812,  0.23803832,  1.11374539,
         0.80462467],
       [ 0.67332598,  0.39755886, -1.41833812,  0.23803832,  0.16087433,
         0.80462467],
       [ 0.67332598,  0.39755886,  0.58555244, -0.90752108,  0.16087433,
        -1.83180511],
       [-0.80799118,  0.39755886,  0.58555244,  0.23803832, -0.79199672,
        -0.51359022],
       [ 0.67332598, -1.40314893,  0.58555244, -0.90752108,  1.11374539,
         0.80462467],
       [ 0.67332598, -1.40314893,  1.58749772, -0.90752108,  1.11374539,
         0.80462467],
       [ 0.67332598,  0.39755886,  0.58555244,  0.23803832,  0.16087433,
        -0.51359022],
       [ 0.67332598, -0.50279503,  0.58555244,  0.23803832, -1.74486778,
        -1.83180511],
       [-2.28930833,  1.29791276,  0.58555244,  1.38359771, -2.69773883,
        -1.83180511],
       [ 0.67332598, -1.40314893,  1.58749772,  1.38359771,  1.11374539,
         0.80462467],
       [-0.80799118,  0.39755886, -0.41639284,  0.23803832,  0.16087433,
        -0.51359022],
       [ 0.67332598,  2.19826665, -2.4202834 , -3.19863987,  1.11374539,
        -4.4682349 ],
       [ 0.67332598, -0.50279503, -0.41639284,  0.23803832,  0.16087433,
        -1.83180511],
       [ 0.67332598, -0.50279503, -0.41639284,  0.23803832, -1.74486778,
         0.80462467],
       [ 0.67332598,  0.39755886, -0.41639284,  0.23803832,  0.16087433,
         0.80462467]])

kmeansを適用した結果のグルーピングの配列が出力として渡される

df_happy_s_ar_pred = km.fit_predict(df_happy_s_ar)
display(df_happy_s_ar_pred)
array([3, 3, 3, 0, 1, 0, 2, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 3, 3, 2, 1, 2,
       1, 3, 3, 0, 2, 2, 2, 0, 0, 1, 1, 3, 2, 1, 0, 0, 2, 2, 0, 3, 1, 1,
       1, 0, 0, 3, 1, 1, 2, 0, 3, 0, 0, 3, 3, 3, 3, 3, 3, 2, 0, 3, 2, 1,
       0, 0, 3, 2, 1, 0, 1, 2, 2, 1, 3])

割り振られた結果を元の(標準化する前の)データにクラスターIDとして一列追加する

df_happy_clust = df_happy[:]
df_happy_clust["cluster_ID"] = df_happy_s_ar_pred
df_happy_clust.head(3)
行政サービス情報へのアクセスしやすさ 住宅供給の高さ 公立の学校の全般的な質の良さ 地域警察への信頼の高さ 道路・歩道のメンテナンス状況 地域社会行事の利用のしやすさ cluster_ID
0 5 3 3 3 3 5 3
1 5 5 3 5 5 5 3
2 5 4 4 4 4 5 3

因みにこのままではクラスターIDも数値として扱われてしまいます

print(df_happy_clust.dtypes)
行政サービス情報へのアクセスしやすさ    int64
住宅供給の高さ               int64
公立の学校の全般的な質の良さ        int64
地域警察への信頼の高さ           int64
道路・歩道のメンテナンス状況        int64
地域社会行事の利用のしやすさ        int64
cluster_ID            int32
dtype: object

カテゴリカル変数に変えておきましょう

df_happy_clust["cluster_ID"] = df_happy_clust["cluster_ID"].astype("category")
print(df_happy_clust.dtypes)
行政サービス情報へのアクセスしやすさ       int64
住宅供給の高さ                  int64
公立の学校の全般的な質の良さ           int64
地域警察への信頼の高さ              int64
道路・歩道のメンテナンス状況           int64
地域社会行事の利用のしやすさ           int64
cluster_ID            category
dtype: object

クラスタが何人くらいになったのかを把握しておきます

print(df_happy_clust["cluster_ID"].value_counts())
1    22
3    20
2    18
0    17
Name: cluster_ID, dtype: int64

まぁまぁまどのクラスターも同じ人数ですね

クラスターの分析

クラスターでグルーピングし、回答の片寄りがどこに出たのか?を確認します。

今回は、間隔尺度の設問でしたので単純に平均するのは乱暴ですので、踏まえて更にデータを再構築する必要があります。

df_happy_clust = df_happy_clust[:].astype("category")
print(df_happy_clust.dtypes)
行政サービス情報へのアクセスしやすさ    category
住宅供給の高さ               category
公立の学校の全般的な質の良さ        category
地域警察への信頼の高さ           category
道路・歩道のメンテナンス状況        category
地域社会行事の利用のしやすさ        category
cluster_ID            category
dtype: object

ダミー変数化したい列を指定するために列名を取得してリスト化、更にクラスタIDを除きます

dummy_list = list(df_happy_clust.columns)[0:-1]
print(dummy_list)
['行政サービス情報へのアクセスしやすさ', '住宅供給の高さ', '公立の学校の全般的な質の良さ', '地域警察への信頼の高さ', '道路・歩道のメンテナンス状況', '地域社会行事の利用のしやすさ']

ダミー変数化したい列名を指定して全ての設問をダミー変数化します6

df_happy_clust_dmy = pd.get_dummies(df_happy_clust,columns=dummy_list)
df_happy_clust_dmy.head(3)
cluster_ID 行政サービス情報へのアクセスしやすさ_3 行政サービス情報へのアクセスしやすさ_4 行政サービス情報へのアクセスしやすさ_5 住宅供給の高さ_1 住宅供給の高さ_2 住宅供給の高さ_3 住宅供給の高さ_4 住宅供給の高さ_5 公立の学校の全般的な質の良さ_1 ... 地域警察への信頼の高さ_5 道路・歩道のメンテナンス状況_1 道路・歩道のメンテナンス状況_2 道路・歩道のメンテナンス状況_3 道路・歩道のメンテナンス状況_4 道路・歩道のメンテナンス状況_5 地域社会行事の利用のしやすさ_1 地域社会行事の利用のしやすさ_3 地域社会行事の利用のしやすさ_4 地域社会行事の利用のしやすさ_5
0 3 0 0 1 0 0 1 0 0 0 ... 0 0 0 1 0 0 0 0 0 1
1 3 0 0 1 0 0 0 0 1 0 ... 1 0 0 0 0 1 0 0 0 1
2 3 0 0 1 0 0 0 1 0 0 ... 0 0 0 0 1 0 0 0 0 1

3 rows × 28 columns

クラスタIDでグループ化し数値を集約します

df_happy_clust_dmy_gp = df_happy_clust_dmy.groupby("cluster_ID")

グループ別に各設問の回答者数の合計を出します

df_happy_clust_dmy_gp_g = df_happy_clust_dmy_gp.sum().T
display(df_happy_clust_dmy_gp_g)
cluster_ID 0 1 2 3
行政サービス情報へのアクセスしやすさ_3 0 8 0 0
行政サービス情報へのアクセスしやすさ_4 0 12 7 0
行政サービス情報へのアクセスしやすさ_5 17 2 11 20
住宅供給の高さ_1 6 2 6 0
住宅供給の高さ_2 11 6 8 0
住宅供給の高さ_3 0 11 3 10
住宅供給の高さ_4 0 3 0 6
住宅供給の高さ_5 0 0 1 4
公立の学校の全般的な質の良さ_1 0 2 1 0
公立の学校の全般的な質の良さ_2 1 6 1 1
公立の学校の全般的な質の良さ_3 3 8 12 6
公立の学校の全般的な質の良さ_4 8 6 4 7
公立の学校の全般的な質の良さ_5 5 0 0 6
地域警察への信頼の高さ_1 0 0 2 0
地域警察への信頼の高さ_2 0 0 1 0
地域警察への信頼の高さ_3 5 5 9 5
地域警察への信頼の高さ_4 5 14 6 9
地域警察への信頼の高さ_5 7 3 0 6
道路・歩道のメンテナンス状況_1 0 1 1 0
道路・歩道のメンテナンス状況_2 0 5 4 0
道路・歩道のメンテナンス状況_3 0 8 1 2
道路・歩道のメンテナンス状況_4 5 7 10 11
道路・歩道のメンテナンス状況_5 12 1 2 7
地域社会行事の利用のしやすさ_1 0 0 1 0
地域社会行事の利用のしやすさ_3 1 1 5 0
地域社会行事の利用のしやすさ_4 0 14 11 4
地域社会行事の利用のしやすさ_5 16 7 1 16

最近読んだ本で知ったのですが、Jupyter notebookであれば、HTML上で各セルに棒グラフを入れたり、数値によって色をつけたりしてくれるので、是非活用したほうが良いです。

pandas.pydata.org

df_happy_clust_dmy_gp_g.style.bar(color="#4285F4")
cluster_ID 0 1 2 3
行政サービス情報へのアクセスしやすさ_3 0 8 0 0
行政サービス情報へのアクセスしやすさ_4 0 12 7 0
行政サービス情報へのアクセスしやすさ_5 17 2 11 20
住宅供給の高さ_1 6 2 6 0
住宅供給の高さ_2 11 6 8 0
住宅供給の高さ_3 0 11 3 10
住宅供給の高さ_4 0 3 0 6
住宅供給の高さ_5 0 0 1 4
公立の学校の全般的な質の良さ_1 0 2 1 0
公立の学校の全般的な質の良さ_2 1 6 1 1
公立の学校の全般的な質の良さ_3 3 8 12 6
公立の学校の全般的な質の良さ_4 8 6 4 7
公立の学校の全般的な質の良さ_5 5 0 0 6
地域警察への信頼の高さ_1 0 0 2 0
地域警察への信頼の高さ_2 0 0 1 0
地域警察への信頼の高さ_3 5 5 9 5
地域警察への信頼の高さ_4 5 14 6 9
地域警察への信頼の高さ_5 7 3 0 6
道路・歩道のメンテナンス状況_1 0 1 1 0
道路・歩道のメンテナンス状況_2 0 5 4 0
道路・歩道のメンテナンス状況_3 0 8 1 2
道路・歩道のメンテナンス状況_4 5 7 10 11
道路・歩道のメンテナンス状況_5 12 1 2 7
地域社会行事の利用のしやすさ_1 0 0 1 0
地域社会行事の利用のしやすさ_3 1 1 5 0
地域社会行事の利用のしやすさ_4 0 14 11 4
地域社会行事の利用のしやすさ_5 16 7 1 16

このままの表が表示できない環境のであれば、グラフ化する必要があります。

plt.figure(figsize=(12,8))
sns.clustermap(df_happy_clust_dmy_gp_g,cmap="viridis")
plt.savefig('figure_2.png')
plt.show()

f:id:yyhhyy:20190707224604p:plain

ただ、無理にグラフにするより、スプレッドシートに吐き出して条件付き書式で見た方が楽だとは思います

df_happy_clust_dmy_gp_g.to_csv("df_happy_clust_dmy_gp_g.csv")

クラスター分析で各クラスターについての説明を考えるのはいつも恣意的ですが、例えば以下のように分類できます。

  • クラスター「0」はどの項目にも高い評価で満足し、住宅も高くないと感じているようです。
  • クラスター「1」は学校の質は低いと思っていますが、市には満足をしている。
  • クラスター「2」は警察の質は低いと思っていますが、市には満足しています。
  • クラスター「3」は住宅は高いと感じていますが、市には満足している。

それぞれ幸福度は高いわけですから、評価が低い箇所についてはあまり気にしていないということと同義だと判断しました。もちろんこれ以外の着眼点で分けるのも良いでしょう。

決定木分析

最後に、仮に今後新しく取得されたアンケートデータから、各クラスターに回答者を振り分けたい。となれば、様々な機械学習手法が可能なのですが、一般的な人にも理解して貰いやすい、という意味では、決定木分析が良いのではないかと思います。

先ず先程のデータをラベルとデータとにわけ、どちらもnumpyの配列にします

y = np.array(df_happy_clust["cluster_ID"].values)
display(y)
array([3, 3, 3, 0, 1, 0, 2, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 3, 3, 2, 1, 2,
       1, 3, 3, 0, 2, 2, 2, 0, 0, 1, 1, 3, 2, 1, 0, 0, 2, 2, 0, 3, 1, 1,
       1, 0, 0, 3, 1, 1, 2, 0, 3, 0, 0, 3, 3, 3, 3, 3, 3, 2, 0, 3, 2, 1,
       0, 0, 3, 2, 1, 0, 1, 2, 2, 1, 3], dtype=int64)
X = df_happy_clust.drop("cluster_ID",axis=1).values
display(X)
array([[5, 3, 3, 3, 3, 5],
       [5, 5, 3, 5, 5, 5],
       [5, 4, 4, 4, 4, 5],
       [5, 2, 4, 5, 5, 5],
       [3, 2, 4, 3, 4, 4],
       [5, 1, 4, 3, 4, 5],
       [4, 2, 3, 3, 4, 4],
       [4, 2, 3, 3, 4, 4],
       [5, 1, 2, 5, 2, 4],
       [4, 3, 3, 3, 3, 4],
       [3, 3, 3, 5, 5, 5],
       [3, 3, 1, 3, 3, 4],
       [3, 3, 1, 3, 3, 4],
       [5, 3, 3, 3, 5, 3],
       [3, 2, 4, 4, 4, 5],
       [3, 2, 4, 4, 4, 5],
       [4, 1, 3, 1, 1, 4],
       [5, 3, 4, 3, 4, 5],
       [5, 3, 4, 3, 4, 5],
       [5, 2, 3, 3, 2, 5],
       [4, 4, 3, 4, 2, 4],
       [4, 2, 4, 3, 2, 4],
       [3, 1, 2, 4, 3, 5],
       [5, 3, 4, 4, 4, 5],
       [5, 3, 3, 4, 4, 5],
       [5, 2, 5, 5, 5, 3],
       [5, 1, 3, 3, 4, 4],
       [5, 1, 3, 3, 4, 4],
       [5, 1, 3, 3, 4, 4],
       [5, 2, 4, 3, 4, 5],
       [5, 2, 4, 3, 4, 5],
       [4, 3, 2, 4, 3, 4],
       [4, 3, 2, 4, 3, 4],
       [5, 3, 5, 5, 4, 5],
       [5, 2, 4, 2, 2, 4],
       [4, 4, 3, 3, 2, 5],
       [5, 2, 4, 4, 5, 5],
       [5, 2, 4, 4, 5, 5],
       [4, 1, 3, 4, 4, 4],
       [4, 1, 3, 4, 4, 4],
       [5, 1, 5, 5, 5, 5],
       [5, 4, 5, 5, 5, 5],
       [4, 2, 2, 4, 4, 5],
       [4, 3, 3, 4, 3, 4],
       [4, 3, 3, 4, 2, 4],
       [5, 2, 5, 5, 5, 5],
       [5, 2, 3, 5, 5, 5],
       [5, 5, 5, 5, 5, 5],
       [4, 3, 2, 4, 4, 4],
       [4, 2, 4, 4, 4, 4],
       [5, 3, 2, 4, 4, 4],
       [5, 2, 3, 4, 4, 5],
       [5, 3, 3, 3, 5, 5],
       [5, 2, 4, 5, 4, 5],
       [5, 1, 3, 4, 5, 5],
       [5, 4, 5, 5, 5, 4],
       [5, 5, 3, 4, 4, 5],
       [5, 4, 4, 3, 3, 4],
       [5, 4, 4, 4, 4, 5],
       [5, 5, 5, 5, 5, 5],
       [5, 4, 5, 4, 5, 4],
       [4, 2, 3, 4, 3, 3],
       [5, 2, 2, 4, 5, 5],
       [5, 3, 2, 4, 4, 5],
       [5, 3, 4, 3, 4, 3],
       [4, 3, 4, 4, 3, 4],
       [5, 1, 4, 3, 5, 5],
       [5, 1, 5, 3, 5, 5],
       [5, 3, 4, 4, 4, 4],
       [5, 2, 4, 4, 2, 3],
       [3, 4, 4, 5, 1, 3],
       [5, 1, 5, 5, 5, 5],
       [4, 3, 3, 4, 4, 4],
       [5, 5, 1, 1, 5, 1],
       [5, 2, 3, 4, 4, 3],
       [5, 2, 3, 4, 2, 5],
       [5, 3, 3, 4, 4, 5]], dtype=object)
from sklearn import tree

決定木は永遠に細かくできます。仮の数字で段階を決めてしまいます。

dtree = tree.DecisionTreeClassifier(max_depth=4)
dtree = dtree.fit(X,y)

作ったモデルがどの程度の精度なのか?確認してみます

dtree_pred = dtree.predict(X)
display(dtree_pred)
array([3, 3, 3, 0, 1, 0, 2, 2, 0, 1, 1, 1, 1, 2, 1, 1, 2, 3, 3, 1, 1, 2,
       1, 3, 3, 0, 2, 2, 2, 0, 0, 1, 1, 3, 2, 1, 0, 0, 2, 2, 0, 3, 1, 1,
       1, 0, 0, 3, 1, 2, 2, 0, 3, 0, 0, 3, 3, 3, 3, 3, 3, 2, 0, 2, 2, 1,
       0, 0, 3, 2, 1, 0, 1, 2, 2, 1, 3], dtype=int64)

ラベルとどれくらいあっているか?正直、過学習してそうですが、今は面倒なのでこのまま進めます

sum(dtree_pred == y) / len(y)
0.948051948051948

jupyter notebookならgraphvizパッケージを使って可視化が可能です7

import pydotplus
from IPython.display import Image
from graphviz import Digraph

配列にした際に特徴量の名称が消えてしまっているので、列名を代入し、またクラスター名も数字のままだとエラーになるため、ラベルデータはastypeを使って文字列(string)に変更しておきます

dot_data = tree.export_graphviz(dtree,out_file=None,
                                feature_names = df_happy_clust.columns[0:-1],
                                class_names = y.astype("str"))

日本語を出力するにあたっては随分苦労しましたがこのブログが正解のようです。

mk-55.hatenablog.com

graph = pydotplus.graph_from_dot_data(dot_data)

graph.set_fontname('Noto Sans CJK JP')
for node in graph.get_nodes():
    node.set_fontname('Noto Sans CJK JP')
for e in graph.get_edges():
    e.set_fontname('Noto Sans CJK JP')

graph.write_png("dtree.png")
Image(graph.create_png())

f:id:yyhhyy:20190707224750p:plain

例えば、「行政サービス情報のアクセスしやすさ」が4.5以上つまり5で、「住宅供給の高さ」が2.5以上だとクラスター3だと判断されるわけですが、実際のデータと比べても特徴をよく表しています。

df_happy_clust_dmy[df_happy_clust_dmy["cluster_ID"]==3].sum().head(10)
cluster_ID              60.0
行政サービス情報へのアクセスしやすさ_3     0.0
行政サービス情報へのアクセスしやすさ_4     0.0
行政サービス情報へのアクセスしやすさ_5    20.0
住宅供給の高さ_1                0.0
住宅供給の高さ_2                0.0
住宅供給の高さ_3               10.0
住宅供給の高さ_4                6.0
住宅供給の高さ_5                4.0
公立の学校の全般的な質の良さ_1         0.0
dtype: float64

f:id:yyhhyy:20190707224827p:plain

分析からわかること

例えば、市であれば自分たちが改善できることできなことがあるはずで、対応できることを対応する場合、どのクラスタの市民に残って欲しいか?増えて欲しいか?を絞り込むことができます。

また、重点ターゲットとなるクラスタを決めたら、決定木のジャッジのポイントを元に訴求項目を作れば、どういう謳い文句が必要か?ということが自ずと決まってきます。

今回はアンケート項目が少なかったので行えませんでしたが、通常はクラスタ分析に使った質問以外にもっと多くの質問をしているはずなので、その質問で決定木分析を行うと

  • どんなメディアに接触しているのか?
  • どんな世代に多いのか?
  • どんなことに興味関心があるのか?

など、広告でターゲティングしやすいセグメントを元に決定木分析を行うこともできるでしょう


決定木以外にも通常のグラフで日本語が文字化けします。seabornを使う場合は、最初にフォントを指定しておけば大丈夫のようで、今のところ私もこれでうまく行っています

qiita.com


最近、Pythonの本がすごくわかりやすいものが増えてきました。今まではエンジニア出身の人の本が多かったですが、徐々にデータサイエンス側に向けて適切な分量の本が増えています

コンパクトにまとまっているけれどそれでも応用範囲が広い

こちらはドリルのような本でも知っていると便利

Pythonではないもの、データサイエンスにとって必要なものは何か?を丁寧に解説していて、この本は最初の頃に読んでおきたかったと思う。


  1. ここのサイト、機械学習に特化しているので、どういう目的で?どういうタイプのデータが欲しいか?をチェックしていくとデータの候補がセレクトされるんですよ!凄いですね。

  2. 因みにこのファイル文字コードが特殊でうまく読み込めず、一度Googleスプレッドシートで読み込んで再保存しています。

  3. 「cost_of_housing」と5段階スケールで聞いてますが、これは5の方が価格が高い、という意味ですかね?

  4. 片寄っていることの確認のため簡便にdescribeを使っていますが、そもそも間隔尺度は通常は平均などは意味がないので出しません。

  5. 学校のテストの偏差値と根本的な考えは同じです。みんなの点数が高かった数学の90点と、みんなの点数が低かった英語の90点とは平等に足すことはできません。

  6. drop_first=True」は今回は指定しない。

  7. Win10ではgraphivzのdot.exeをシステム環境PATHで通す必要があるようです。