#author("2023-10-25T00:02:19+09:00","default:cmdsadmin","cmdsadmin")
* 第4回:探索的データ分析,教師なし学習・クラスタリング [#s98df3b7]
#author("2024-05-07T10:37:18+09:00","default:cmdsadmin","cmdsadmin")

#contents

* 4.1 探索的データ分析 [#n03d4e46]

** 探索的データ分析 (Exploratory Data Analysis) [#l9fd9db5]

*** EDAって何? [#d370ea70]
- 与えられたデータ・データセットを,様々な角度から分析・調査して,そのデータの性質を理解すること
- 統計では比較的新しい分野 [Tukey-1977]
-- 古典的統計学は,少数の標本から大量データについての結論を引き出す''推定''に焦点を絞っていた
-- ジョン・テューキー「統計的推定は,EDAの一構成要素に過ぎない」
- 機械学習に限らず,あらゆるデータサイエンスのプロジェクトで最初に行われるべき作業

*** データとお友達になる [#k0d4cd63]
- RQとデータが与えられた時,いきなり機械学習にかけても上手くいかない場合が多い
-- データがダメ → 機械学習もダメ → 何が悪いのかわからない
- まずはデータを眺めて,そのデータを理解することが大事
-- データはどれぐらいのサイズをしている?
-- 各変数の分布はどんな形をしている?
-- 変数間の関係はどうなっている?
-- データに周期性やパターンは見られる?
-- データの品質は大丈夫?

*** EDAでやるべきこと [#u39fb9e7]
- (A) データの確認と整形
-- 取得したデータを確認し,必要があれば形を整える
- (B) データを眺める
-- データの要約や可視化を通して,データの性質を理解する
- (C) データを修正する【前処理】
-- データの異常(欠損,外れ値,重複)に対して,対策を行う
- (D) データを変換する【前処理】
-- 分析に適した形にデータを変換する


** 例題:シェアサイクルの需要予測問題 [#o9467fd4]
- RQ: シェアサイクルの日々の貸出履歴から,将来の利用者数を予測できるか?
-- 【[[シェアサイクルデータセット>データ#t9aeb698]]】


CENTER:&attachref(./4_bike_sharing_pic.png,50%);~
CENTER:図1: 【参考】神戸コミュニティサイクル「コベリン」

*** 準備 [#c403da35]
+ Google Colabを開き,新規ノートブックを作成
+ ノートブックの名前 Untitled.ipynb を bike-eda.ipynb に変更する

 #準備(すべてに共通)
 # PandasとNumpyをインポート
 import pandas as pd
 import numpy as np
 
 # 日本語化Matplotlibもインポート
 import matplotlib.pyplot as plt
 !pip install japanize-matplotlib
 import japanize_matplotlib
 
 # Seabornもインポート
 import seaborn as sns
 
 # pickleをインポート(モデルの保存用)
 import pickle
 
 # データの取得
 data = pd.read_csv("https://www2.cmds.kobe-u.ac.jp/~masa-n/dshandson/bike-sharing-day.csv")
 data

** (A) データの確認と整形 [#yfa373f6]
- pandasの基本的な操作は,Python基礎演習第4回を復習すること
-- [[【参考】Python基礎演習第4回:https://www2.cmds.kobe-u.ac.jp/wiki/dsenshu/?%E7%AC%AC4%E5%9B%9E]]
*** データ項目の確認 [#p5d0e915]
- 毎日のシェアサイクルの貸し出しを記録した時系列データ
- 季節や曜日,その日の気象情報から利用者数を予測する需要予測問題
- データの各列の意味は,以下の通り.末尾3変数が目的変数
-- instant: 貸出番号
-- dteday: 貸出年月日
-- season: 1:春,2:夏,3:秋,4:冬
-- yr: 年. 0:2011年,1:2012年
-- mnth: 月. 1~12
-- holiday: 祭日かどうか
-- weekday: 曜日 (0:日,1:月, ... , 6: 土)
-- workingday: 勤務日(土日祝以外)かどうか
-- weathersit: 1: 晴れ,2: 曇り,3: 小雨・小雪,4:豪雨・雷雨
-- temp: 気温(正規化済: -8℃~+39℃)
-- atemp: 体感気温(正規化済:-16℃~50℃)
-- hum: 湿度(正規化済:100分率)
-- windspeed: 風速 (正規化済:0~67)
-- casual: 1日の都度利用者数 【目的変数】
-- registered: 1日の登録利用者数 【目的変数】
-- cnt: 1日の全利用者数 【目的変数】

CENTER:&attachref(./4_bike_data.png,35%);~
CENTER:図2: シェアサイクルのデータ


*** 表データの確認 [#g60872ae]
データが構造化された表にちゃんと収まっているかを確認する

- 整頓された表 (tidy) になっているか?
-- 各行:1つの観測
-- 各列:1つの変数
- 表の大きさは?
-- 表の形を表示する
 data.shape
- 各列の型は合っているか?
-- 各列の型を確認する
 data.dtypes
 data.info()
- データに異常がないか? (対応は前処理で行う)
-- 欠損値がないか?
 data.isnull().sum()
-- 特殊な文字列: N/A や --- など,特殊な文字
--- 数値列なのに型がobjectになっていたら疑う
- 全角・半角問題,表記ゆれ
-- 文字列.value_counts()で値を見てみる
 data["列1"].value_counts()

*** 表データの整形 [#g60872ae]
分析用のデータセットの作成に向けて,データの型やインデクスを整えていく

- まずは生データをコピーしておく(いつでもやり直せるように)
 df = data.copy()
- 各列はふさわしい型になっているか? 現段階でobject型になっているもの,カテゴリ変数なのに数値型になっているものに注目.
 #列を任意の型変換する
 df["列1"] = df["列1"].astype("型名")
 #日時型への変換
 df["日時列2"] = pd.to_datetime(df["日時列2"])
- インデクスは適切か?
 df.set_index("列1", inplace=True)
- 冗長な変数はないか?あれば削除
 df.drop(columns=["列1", "列2", ...], inplace=True)

*** シェアサイクルのデータを整形する [#od88bdac]
 #型を確認
 data.info()

CENTER:&attachref(./4_bike_dtypes.png);~
CENTER:図3: 各変数の型

 #オリジナルデータをコピーして作業
 df = data.copy()

 #dtedayを日付型に変換
 df["dteday"] = pd.to_datetime(df["dteday"])
 #確認
 df.info()
 df

 #season, holiday, weekday, workingday, weathersitをカテゴリ型に変換
 for col in ["season", "holiday", "weekday", "workingday", "weathersit"]:
   df[col] = df[col].astype("category")
 #確認
 df.info()
 df

 #dtedayをインデクスに設定して時系列データにする
 df.set_index("dteday", inplace=True)
 #確認
 df.info()
 df

 #instant, yr, mnth: 冗長なので削除
 df.drop(columns=["instant", "yr", "mnth"], inplace=True)
 #確認
 df.info()
 df

** (B) データを眺める [#o96d1e02]
様々な観点からデータを探索(要約・可視化)して,データが持つ性質を理解する
- 1変数を眺める
- 2変数間の関係を眺める
- グループに分けて比較する

可視化にはpythonのライブラリを使う
- matplotlib
- seaborn

*** 1変数を眺める [#p8a4b74d]
- 代表値の探索
-- 数値の要約
 df.describe()
-- カテゴリの要約
 df["列1"].value_counts()
- 分布の探索
-- ''箱ひげ図【重要】''
 df.plot.box()
 sns.boxplot(df) #Seabornを使う場合
CENTER:&attachref(./4_bike_boxplot.png,75%);~
CENTER:図4: 箱ひげ図
-- ''ヒストグラム【重要】''
 df["列1"].plot.hist()
 sns.histplot(df["列1"]) #Seabornを使う場合
CENTER:&attachref(4_bike_hist.png);~
CENTER:図5: ヒストグラム
-- ヴァイオリン・プロット
 sns.violinplot(df)
CENTER:&attachref(4_bike_violin.png);~
CENTER:図6: ヴァイオリン・プロット

- 値の大きさ,推移の探索
-- 棒グラフ
 df.plot.bar()
 sns.barplot(df) #Seabornを使う場合
-- 折れ線グラフ(推移,時系列)
 df["列1"].plot()
 df["列1"].resample("周期").集約関数().plot() #週や月で時系列を集約して表示する場合
 sns.lineplot(df) #Seabornを使う場合

*** シェアサイクルの1変数を眺める [#d7844033]
 #describe()は基本的に数値列のみを要約する
 df.describe()

 #カテゴリの列を要約
 for col in ["season", "holiday", "weekday", "workingday", "weathersit"]:
   print(f"\n【{col}の要約】")
   print(df[col].value_counts())

 #気象データの分布を箱ひげ図で可視化
 sns.boxplot(df[["temp", "atemp", "hum", "windspeed"]]) 

 #利用者データの分布を箱ひげ図で可視化
 sns.boxplot(df[["casual", "registered", "cnt"]])

 #各変数のヒストグラムを描いてみる.サブプロットを使って,1枚の図に並べる
 fig, axes= plt.subplots(nrows=4, ncols=2, tight_layout=True, squeeze=False)
 
 for i, col in enumerate(["temp", "atemp", "hum", "windspeed", "cnt", "casual", "registered"]):
   sns.histplot(df[col], ax=axes[i//2, i%2], bins=40)

 #利用者データの推移を見てみる
 fig, axes = plt.subplots(nrows=2, ncols=1, tight_layout=True, squeeze=False, figsize=(6,8))
 
 #利用者データの推移
 df[["casual", "registered", "cnt"]].plot(ax=axes[0,0], title="利用者データの推移(日次)")
 #利用者データの月ごとの推移を棒グラフで(登録・都度の内訳)
 df[["casual", "registered"]].resample("M").sum().plot.bar(stacked=True, ax=axes[1,0], title="利用者データの推移(月次)")


*** 2変数間の関係を眺める [#bfdbe38d]

- ''相関行列【重要】''
 df.corr()
 sns.heatmap(df.corr(), annot=True) #ヒートマップで可視化する
CENTER:&attachref(./bike_corr.png,80%);~
CENTER:図7: 相関行列
- ''散布図【重要】''
 df.plot.scatter(x=列1, y=列2)
 sns.scatterplot(df, x=列1, y=列2, hue=列3) #Seabornを使う場合.hueで色分けできる
CENTER:&attachref(./bike_scatter.png,75%);~
CENTER:図8: 散布図
- ペアプロット 
 sns.pairplot(df) #変数が多いと時間がかかるし見づらい

*** シェアサイクルの2変数間の関係を眺める [#k65a8c87]
 #相関係数を求める
 df.corr()

 #カテゴリ変数も含めたければ,get_dummies()を行う
 pd.get_dummies(df).corr()

 #ヒートマップで可視化
 sns.heatmap(df.corr(), annot=True)

 #気温と総利用者数の関係.季節で色分け
 sns.scatterplot(df, x="temp", y="cnt", hue="season")

 #ペアプロット
 sns.pairplot(df)
 

*** グループに分けて比較する [#p8c31074]

- カテゴリ変数でデータをグループ化し,2つのグループ間で変数の要約を行う
 df.groupby("カテゴリ列1").describe()["数値列2"]

- Seabornのプロットでxにカテゴリ変数,yに目的の数値列渡すと簡単に可視化できる.
 sns.boxplot(df, x=カテゴリ列1, y=数値列2, hue=列3)
 sns.violinplot(df, x=カテゴリ列1, y=数値列2, hue=列3)


*** シェアサイクルのデータをグループに分けて比較する [#o3d91bb6]
【比較の観点】
- 年によって利用者に差はあるか?
- 季節によって差はあるか?
- 月によって差はあるか?
- 曜日によって差はあるか?
- 祝日かどうかで差があるか?
- 平日(土日祝以外)かどうかで差はあるか?
- 天気によって差はあるか?

 #統計量の要約
 df.groupby(df.index.year).describe()["cnt"] 
 df.groupby("season").describe()["cnt"]
 #以下同様
 # :
 
 #箱ひげ図で比較する
 fig, axes = plt.subplots(nrows=4, ncols=2, figsize=(18,18))
 sns.boxplot(df, y="cnt", x=df.index.year, ax=axes[0,0])
 sns.boxplot(df, y="cnt", x="season", ax=axes[0,1])
 sns.boxplot(df, y="cnt", x=df.index.month, ax=axes[1,0])
 sns.boxplot(df, y="cnt", x="weekday", ax=axes[1,1])
 sns.boxplot(df, y="cnt", x="holiday", ax=axes[2,0])
 sns.boxplot(df, y="cnt", x="workingday", ax=axes[2,1])
 sns.boxplot(df, y="cnt", x="weathersit", ax=axes[3,0])

 #バイオリンプロットでもやってみてください
 # 略

** (C) データを修正する【前処理】 [#a36ad670]
- データの異常(欠損,外れ値,重複)に対して,対策を行う
-- [[【参考】Python基礎演習第6回:https://www2.cmds.kobe-u.ac.jp/wiki/dsenshu/?%E7%AC%AC6%E5%9B%9E]]

*** 欠損値 (missing value) の処理 [#x3219da8]
- 欠損値の確認
 df.isnull().sum()  #列ごとの欠損値の数を数える
 df[df["列1"].isnull()] #列1が欠損している行を抜き出す
- 行ごと削除する
 df = df.dropna()
 df = df.dropna(subset=[列1, 列2, ...]) #特定の列のNaNのみを対象
- 定数で埋める
 df = df.fillna(0)
- 統計値で埋める
 df = df.fillna(df.集約関数()) #表全体
 df["列1"] = df["列1"].fillna(df["列1"].集約関数())  #列ごと
- 前後の値で埋める
 df = df.fillna(method="ffill" または "bfill")  #表全体
 df["列1"] = df["列1"].fillna(method="ffill" または "bfill")  #列ごと
- 補間する
 df = df.interpolate()  #表全体
 df["列1"] = df["列1"].interpolate() #列ごと

*** 外れ値 (outlier) の処理 [#fdde8c82]
- 外れ値を見つける
 # 箱ひげ図や散布図から見つける
- 消す
 df = df.drop(index=[行1, 行2, ...])
- 値を修正する
 df.loc[行1, 列1] = 正しい値

*** 重複行 (duplicates) の処理 [#zf41d0c1]
- 重複行を見つける
 df[df.duplicated(keep=False)]
- 重複行を消す
 df = df.drop_duplicates()

*** シェアサイクルのデータでやってみる [#p0dabf7f]

 #クリーニングされていないシェアサイクルのデータをロード
 data = pd.read_csv("https://www2.cmds.kobe-u.ac.jp/~masa-n/dshandson/bike-sharing-unclean.csv")
 #コピーしておく
 df = data.copy()

 #欠損値の個数を数える
 df.isnull().sum()

 #tempが欠損している行を抜き出す
 df[df["temp"].isnull()]

 #前後を見てみる
 df[185:195]
 
 #行ごと削除する (dfに代入していないので,dfはそのまま)
 df.dropna(subset=["temp"])[185:195]

 #定数0で埋める (dfに代入していないので,dfはそのまま)
 df.fillna(0)[185:195]

 #中央値で埋める (dfに代入していないので,dfはそのまま)
 df.fillna(df.median())[185:195]

 #線形補間で埋める (dfに代入していないので,dfはそのまま)
 df.interpolate()[185:195]

 #すべての欠損値を線形補間で埋める
 df = df.interpolate()

 #欠損値を数える
 df.isnull().sum()

 #要約統計量を求める
 df.describe()

 #怪しそうな変数を表示する
 df[["casual", "registered", "cnt"]].plot.box()
 
 #条件で特定してみる
 df[df["cnt"] > 80000]

 #周辺のデータを見てみる
 df[510:520] 

 #修正する
 df.loc[517, "casual"] = 533
 df.loc[517, "cnt"] = 4127

 #再度箱ひげ図で確認
 df[["casual", "registered", "cnt"]].plot.box()

 #重複行を見つける
 df[df.duplicated(keep=False)]

 #重複行を削除する
 df = df.drop_duplicates()

 #重複行を再度確認
 df[df.duplicated(keep=False)]

** (D) データを変換する 【前処理】 [#l7bc4f3b]
*** カテゴリデータのダミー変数化 (one-hot encoding) [#p5bed8f7]
- 第3回で説明済み
 pd.get_dummies(df, columns=[ダミー変数化する変数のリスト], drop_first=True)
- ただしダミー変数化するのは,''名義尺度''に限るのが一般的
-- ''順序尺度''をダミー変数化すると情報が失われてしまう.(0,1で順序を保持できないため)
- 【重要】カテゴリ変数の種類
-- 名義尺度: アヤメの品種,性別,血液型,etc.
-- 順序尺度: 年,サイズ,アンケートの回答(5段階),etc.

*** 数値データのスケーリング [#le8591b0]
- 通常,各変数(列)はそれぞれに単位が定められており,その値の意味合いや範囲(値域)が異なる
-- 同じ1でも,列によってその値が持つ意味・重みが異なってくる

CENTER:&attachref(./4_scaling.png,75%);~
CENTER:図9: 単位・値域の違う変数

- 学習アルゴリズムによっては,このデータの重みの偏りによって,うまく動作しないことがある
- 影響があるモデル
-- 距離を用いるモデル:k近傍法,クラスタリング全般
-- 分散を用いるモデル:主成分分析(PCA)
-- 勾配を用いるモデル:ロジスティック回帰,SVN,ニューラルネットワーク
- 影響がないモデル
-- 決定木,ランダムフォレスト
- データの大きさをある基準で揃える(''スケーリング'')する必要がある
-- 標準化
-- 正規化

*** データの標準化 (standardization) [#ja010940]
- データの標準化とは,単位や値域の異なる変数を,''平均値からどれだけ離れているか'' という標準的な観点で揃える
-- いわゆる, ''偏差値'' の考え方
- データX = {x0,x1, ... , xn} の各値を次の Z = {z0,z1, ... ,zn} に変換する
- Z = (xi - X.mean()) / X.std()
-- 平均からどれだけ離れているか(=偏差)を,ばらつきの平均(=標準偏差)で割る
-- Zスコアと呼ぶこともある
-- 偏差値 = 50 + 10 * zi

CENTER:&attachref(./4_standardization.png,75%);~
CENTER:図10: データの標準化


- scikit-learnの StandardScaler() を使用すれば簡単に標準化できる

 #元データを適当に作る
 df = pd.DataFrame(data={"物件No.":[1,2,3,4,5],
                          "駅からの距離":[0.2,0.8,1.5,3.4,4.8],
                          "築年数":[30, 25, 5, 20, 50], 
                          "部屋数":[1,3,3,4,6], 
                          "家賃":[55000,83000,64000,72000, 100000],
                          "管理費":[5000, 10000, 10000, 20000, 30000]},
                    ).set_index("物件No.")
 df

 from sklearn.preprocessing import StandardScaler
 #標準化のためのスケーラー
 sc = StandardScaler()
 
 #各列にフィットさせる
 sc.fit(df) 
 #スケール変換(sc.transform(df))して,データフレームに入れなおす
 df_sc = pd.DataFrame(sc.transform(df), index=df.index,columns=df.columns)
 #確認
 df_sc

*** データの正規化 (normalization) [#of0195d2]
- データの正規化とは,単位や値域の異なる変数を, ''最小値と最大値の間のどこに位置しているか''
という標準的な観点で揃える
-- 最小を0,最大を1として,どのあたりにいるかを表現できる

- データX = {x0,x1, ... , xn} の各値を次の M = {m0,m1, ... ,mn} に変換する
- M = (xi - X.min()) / (X.max() - X.min())
  - 各データから最小値を引き,値域の幅で割る

CENTER:&attachref(./4_normalization.png,75%);~
CENTER:図11: データの正規化

- scikit-learnの MinMaxScaler() を使用すれば簡単に標準化できる

 from sklearn.preprocessing import MinMaxScaler
 #正規化のためのスケーラー
 sc = MinMaxScaler()
 
 #各列にフィットさせる
 sc.fit(df) 
 #スケール変換(sc.transform(df))して,データフレームに入れなおす
 df_sc = pd.DataFrame(sc.transform(df), index=df.index,columns=df.columns)
 #確認
 df_sc

*** データ変換のTips [#db34bcb3]
- 前処理のどのタイミングでデータ変換をするのか?
-- ダミー変数化:前処理の''一番最初''に行う
-- スケーリング:教師あり学習の場合は,''訓練データとテストデータを分割した後''に行う
--- データ分割前にスケーラーをfitさせてはならない(訓練データがテストデータに依存してしまう)
--- 訓練データにスケーラーをfit, transformさせ,''同じスケーラーでテストデータをtransformさせる''こと
--- 訓練データとテストデータで別々のスケーラを使ってはいけない
- スケーリングについては,標準化と正規化のどちらを使えばいいの?
-- 通常は標準化を使う.正規化は外れ値に引っ張られる
-- 正規化は,最大値・最小値が決まっている場合に,用いられる(例:画像処理)
- 決定木系のモデルでは,スケーリング自体が不要
-- データの大小関係しか見ていないため
- スケーリング後のデータdf_scをどのように元に戻すのか?
-- sc.inverse_transform(df_sc)を使う.データフレームに入れなおすことも忘れずに

** シェアサイクルの機械学習モデルの構築 [#xc4877aa]
- 次回以降のお楽しみに!

* 4.2 教師なし学習:クラスタリング [#b5227af5]

** 教師なし学習 (unsupervised learning) [#q029753a]
- 正解データ(ラベル,目的変数) y がない(あるいは,あえて指定しない)
データに対して,''データそのものが持つ性質・法則''を学習する方法
- 本コースでは2種類の代表的なものを学ぶ
-- ''クラスタリング'': サンプル間の関係を学習し,データを似た者同士のグループ(類型)に分ける
-- ''次元削減'':変数間の関係を学習し,多数の変数をより少数の変数で表現する
- 直感的なイメージは次の図の通り

CENTER:&attachref(./4_unsupervised_learning.png,80%);~
CENTER:図12: 教師あり学習と教師なし学習のイメージ

** クラスタリング (clustering) [#m358f16d]
- 似ているデータ(サンプル)同士をまとめてグループ化し,データ全体をいくつかのグループ(=''クラスタ'')にわけること
-- 「似ている」の基準は,データ間の''距離''に基づく
-- 距離に基づく方法なので,''データの標準化''が必須
- 分類(classification)と似ているが,似て非なるもの
-- 分類では,各グループ(''クラス'')が何を表しているのか,正解データとして与えられていた(e.g., アヤメの品種,迷惑メールか否か)
-- クラスタリングでは,分けられたグループ(''クラスタ'')が何を表すのかは与えられないので,分析者が考えることになる
-- クラスは事前に人間が決めるグループ,クラスタは似た者を集めて結果的にできるグループ

*** 応用例 [#z78332ba]
- 顧客情報をクラスタリングして,顧客のセグメンテーション(類型化)を行う
- 膨大な文書をクラスタリングして,話題ごとに分類する
- ラベルの付いていない画像や音声を分類して整理する

*** 距離関数 [#m11e9c90]
- 2つのデータX = (x1,x2,...,xn) と Y = (y1,y2,...,yn)に対して,XとYの間の ''距離 d(X,Y)''を定義したい
-- XとYがどれだけ近いか(遠いか)→どれだけ似ているか?

- ''ユークリッド距離''
-- d(X,Y) = sqrt((x1-y1)**2 + (x2-y2)**2 + ... + (xn-yn)**2)
-- 各項の差の2乗を足し合わせて,平方根をとる.いわゆる直線距離
- ''マンハッタン距離''
-- d(X,Y) = |x1-y1| + |x2-y2| + ... + |xn-yn|
-- 各項の絶対値の和.マンハッタン(碁盤の目の街)を歩く時の距離
- ''コサイン(非)類似度''
-- d(X,Y) = 1 - dot(X,Y) / ||X||*||Y|| = 1 - cosθ
-- ベクトルのなす角θが小さい→似ている→0に近づく

CENTER:&attachref(4_distance.png);~
CENTER:図13: 距離関数

*** 2つのアプローチ [#w7c95e96]
- 階層型クラスタリング 
-- 例:凝集型階層的クラスタリング (agglomerative clustering)
-- デンドログラムによって可視化され,説明性が高い
-- あらかじめクラスタ数を決める必要がない
-- サンプル数が少ない場合に有効
- 非階層型クラスタリング
-- 例:K-Meansクラスタリング (K-means clustering)
-- 計算が軽く収束が早い
-- あらかじめクラスタ数kを与える必要がある
-- サンプル数が多い場合に有効


** 階層的クラスタリング [#i613d35a]
- クラスタを階層的に形成する方法.2つのアプローチがある
-- ''凝集型クラスタリング (agglomerative clustering)'': データ1つ1つから始め,距離の近い者同士をまとめていくボトムアップなアプローチ
-- ''分割型クラスタリング (divisve clustering)'': データ全体から始め,距離の離れたクラスタを分割していくトップダウンなアプローチ
- scikit-learnでは,凝集型が実装されている ([[sklearn.cluster.AgglomerativeClustering:https://scikit-learn.org/stable/modules/generated/sklearn.cluster.AgglomerativeClustering.html#sklearn.cluster.AgglomerativeClustering]])
-- 本コースでは凝集型のみを扱う

*** 凝集型階層的クラスタリングのアルゴリズム [#ode8dd89]
+ 与えられたすべてのデータ点をクラスタとみなす
+ 任意のクラスタCi, Cjのペアの距離d(Ci,Cj)を計算する
+ 最も距離の近い2つのペアを,新しいクラスタCijとしてまとめる
+ クラスタが1つになれば終了.残っていれば,2に戻る

CENTER:&attachref(./4_hiearchical_clustering.png,70%);~
CENTER:図14: 凝集型階層的クラスタリング


*** デンドログラム (dendrogram) [#zd0c9644]
- 階層的クラスタの形成過程を樹形図(''デンドログラム'')で表すことができる
-- データ間の類似性が視覚的にわかりやすい
-- 縦軸は''クラスタ間の距離''を表す.適当な距離で水平線を入れると,好きな数のクラスタを決められる
-- データ数が多すぎると,視認性が悪くなる

CENTER:&attachref(4_dendrogram_sample.png);~
CENTER:図15: デンドログラム

*** 2つのクラスタ間の距離d(Ci,Cj)の計算方法 [#fa189463]
- 単リンク法(single):クラスタCiの中の点xiとクラスタCjの点xjについて,最も近いd(xi,xj)をd(Ci,Cj)とする
-- 細長いクラスタができやすい.外れ値に弱い
- 完全リンク法(complete):クラスタCiの中の点xiとクラスタCjの点xjについて,最も遠いd(xi,xj)をd(Ci,Cj)とする
-- 丸いクラスタができやすい.外れ値に弱い
- 平均法(average):クラスタCiの中の点xiとクラスタCjの点xjについて,すべてのd(xi,xj)の平均をd(Ci,Cj)とする
-- 実用的で外れ値にも強い.上記2つの間のような形になる
- ''ウォード法(ward)'':クラスタCijの分散からそれぞれのクラスタCi,Cjの分散を引いた差を距離d(Ci, Cj)とする
-- CiとCjが合体しても,合体する前と比べて,散らばり度合いが増えない → 近い
-- 実用的で外れ値にも強い.球状のクラスタができやすい

*** sklearn.cluster.AgglomerativeClustering [#v4d5a4f3]
- 書式
 from sklearn.cluster import AgglomerativeClustering
 model = AgglomerativeClustering(ハイパーパラメータ)
 
 #標準化したデータフレームで学習させる
 model.fit(df_sc)
 
 #各データのクラスタ番号
 model.labels_

- 説明書は[[こちら:https://scikit-learn.org/stable/modules/generated/sklearn.cluster.AgglomerativeClustering.html]]
- 主なハイパーパラメータ
-- n_clusters: 分割するクラスタ数.Noneを指定することも可能.
-- metric: 距離関数.デフォルトはユークリッド距離(euclidean)
-- linkage: クラスタ間距離の計算方法.デフォルトはウォード法(ward)
-- distance_threshold: 距離の閾値.これ以上離れているクラスタは合体されない

** 例題:都道府県別にみる麺類の消費傾向 [#ve1f248f]
- RQ: 都道府県によって麺類の消費行動に類似性や違いが見られるか?
--  [[教育用データセット(SSDSE-家計消費)>データ#x2a1f458]]にある''麺類の年間消費金額データ''を活用
-- 階層型クラスタリングで47都道府県を類型化してみる

*** 準備 [#o100def9]
- 新しくGoogle Colabを作成し,名前をnoodle-clustering.ipynb とする
 import pandas as pd
 import numpy as np
 import scipy
 import random
 
 # 日本語化Matplotlibもインポート
 import matplotlib.pyplot as plt
 !pip install japanize-matplotlib
 import japanize_matplotlib
 
 import seaborn as sns


*** データの取得と表の整形 [#ae0d0300]
- まずは生データをExcel等で見てみる
-- https://www2.cmds.kobe-u.ac.jp/~masa-n/dshandson/SSDSE-C-2023.csv

 #そのままだと文字コードが読めないので,encodingを指定.CP932はshift_jisの拡張文字コード
 #1行目に属性のコードが入っているので,読み飛ばす
 data = pd.read_csv("https://www2.cmds.kobe-u.ac.jp/~masa-n/dshandson/SSDSE-C-2023.csv", encoding="CP932", skiprows=1)
 data

 #列数が大きすぎるので,列の番号を書き出してみる
 for i, col in enumerate(data.columns):
   print(f"{i}: {col}")

 : 
 1: 都道府県
 ...
 9: 生うどん・そば
 10: 乾うどん・そば
 11: パスタ
 12: 中華麺
 13: カップ麺
 14: 即席麺
 15: 他の麺類
 ...

 #1:都道府県の列,および,9:生うどん・そば ~ 14:即席麺の列を切り出す
 df = data.iloc[:, [1,9,10,11,12,13,14]].copy()
 df

 #都道府県をインデクスに
 df = df.set_index("都道府県")
 df

 #全国の行を削除
 df = df.drop(index=["全国"])
 df

*** 探索的データ分析 [#s691834c]
- 省略.各自色々と可視化してみてください

*** データの標準化 [#a5ec4970]
 #データを標準化する
 from sklearn.preprocessing import StandardScaler
 
 #スケーラーを学習
 sc = StandardScaler()
 sc.fit(df)
 #標準化してデータフレームに入れる
 df_sc = pd.DataFrame(sc.transform(df), index=df.index, columns=df.columns)
 df_sc

*** クラスタリングを行う [#t8f796bd]
 #凝集型階層的クラスタリングのモデル
 from sklearn.cluster import AgglomerativeClustering
 #クラスタ数の指定なし.distance_threshold=0 とすると,最後までクラスタリングを続ける
 model = AgglomerativeClustering(n_clusters=None, distance_threshold=0)
 #標準化したデータで学習.教師なしなので,yが不要
 model.fit(df_sc)

*** デンドログラムを描く [#j4ec8454]
 # デンドログラムを描く関数(基本的に触らないこと)
 # 参考:https://scikit-learn.org/stable/auto_examples/cluster/plot_agglomerative_dendrogram.html#sphx-glr-auto-examples-cluster-plot-agglomerative-dendrogram-p y
 from scipy.cluster.hierarchy import dendrogram
 def plot_dendrogram(model, **kwargs):
     # Create linkage matrix and then plot the dendrogram  
 
     # create the counts of samples under each node
     counts = np.zeros(model.children_.shape[0])
     n_samples = len(model.labels_)
     for i, merge in enumerate(model.children_):
         current_count = 0
         for child_idx in merge:
             if child_idx < n_samples:
                 current_count += 1  # leaf node
             else:
                 current_count += counts[child_idx - n_samples]
         counts[i] = current_count 
 
     linkage_matrix = np.column_stack(
         [model.children_, model.distances_, counts]
     ).astype(float)
 
     # Plot the corresponding dendrogram
     dendrogram(linkage_matrix, **kwargs)


 #デンドログラムを描画する
 plot_dendrogram(model, labels=df.index)


CENTER:&attachref(4_dendrogram.png);~
CENTER:図16: クラスタリングの結果(デンドログラム)

*** 兵庫県に似ている県,似ていない県を見てみる [#s80112a9]

 #兵庫県と似ている県は?
 df.loc[["兵庫県","愛媛県", "岐阜県", "岡山県"],:]

 #兵庫県と似ていない県は?
 df.loc[["兵庫県","大阪府", "秋田県", "香川県"],:] 

*** クラスタに分けてみる [#ddaad179]

 #上記のデンドログラムから,クラスタ数7ぐらいに分けてみる
 model2 = AgglomerativeClustering(n_clusters=7)
 model2.fit(df_sc)
 #結果を確認する
 model2.labels_

 #元データにクラスタのラベルを付けてみる
 df["クラスタ"] = model2.labels_
 df

 #各クラスタの都道府県を表示
 for i in range(7):
   print(f"\n\n【クラスタ No. {i}】")
   print("\n",df[df["クラスタ"]==i])

*** クラスタごとの特徴を調べてみる [#f9bae401]
 #クラスタごとの各麺類の平均消費額
 df_mean = df.groupby("クラスタ").mean()
 df_mean

 #積み上げ棒グラフで可視化
 df_mean.plot.bar(stacked=True, figsize=(8,8))

CENTER:&attachref(4_noodle_result.png);
CENTER:図17:各クラスタの麺類の消費額の内訳


【考察】
- クラスタ0
-- 兵庫,北海道,岐阜,和歌山,愛媛等を含むグループ.麺類への消費額は少ないが,乾麺・中華麺への支出が大きい
- クラスタ1
-- 大阪,中国,九州を含むグループ.クラスタ0と同様に,麺類への消費額は少ないが,カップ麺,即席麺への支出が大きい
- クラスタ2
-- 青森をはじめ東北のグループ.麺類への支出は全体的に多い.カップ麺消費の多さは特徴的
- クラスタ3
-- 東京を中心とする関東が多く含まれる.麺類への消費額は中程度.パスタの消費額が多い
- クラスタ4
-- 香川県のみからなるグループ.生うどん・そばへの消費がけた違いに多い.間違いなく讃岐うどん
- クラスタ5
-- 秋田県のみからなるグループ.乾うどん・そばへの消費の多さが特徴的.おそらく稲庭うどん
- クラスタ6
-- 中部・関西の東寄りの県からなるグループ.クラスタ3と消費額は似ているが,生うどん・そばの消費が多い



** 非階層的クラスタリング [#c94ae4ff]

- クラスタを一気に形成する方法.K-Means法,DBSCAN法,Mean-shift法などがある
-- 本コースでは,''K-Means法''のみ扱う

*** K-Meansクラスタリングのアルゴリズム [#j7e2dc11]
分析者は,あらかじめ何個のクラスタに分けるかを示すハイパーパラメータ ''k'' を入力として与える
+ k個の重心(centroid)をランダムに生成する
+ 各データ点に対して,最も近い重心の色を塗る(そのクラスタに属するとみなす)
+ それぞれの色のクラスタに対し,重心の位置を再計算する
+ 重心の位置が更新されなくなれば終了.それ以外は,2に戻る

CENTER:&attachref(./4_kmeans.png,60%);
CENTER:図18: K-Meansクラスタリング


*** sklearn.cluster.KMeans [#dc6002b9]
- 書式
 from sklearn.cluster import KMeans
 model = KMeans(ハイパーパラメータ)
 
 #標準化したデータフレームで学習させる
 model.fit(df_sc)
 
 #各データのクラスタ番号
 model.labels_

- 説明書は[[こちら:https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html]]
- データ間の距離関数は,''ユークリッド距離''のみをサポート
- 主なハイパーパラメータ
-- n_clusters: 分割するクラスタ数.デフォルトは8
-- init: 重心の初期値の決め方.デフォルトはk-means++配置.データ点間の距離に応じて重心同士がくっつきすぎないように工夫
-- n_init: 異なる重心初期値で何回K-Meansを回すかを指定.デフォルトは10回
-- max_iter: 重心位置とクラスタの更新を最大何回繰り返すか.デフォルトは300回
-- random_state: 乱数の種
- 主な属性
-- labels_: 各点のクラスタ番号
-- inertia_: 各点から重心への2乗距離の和.クラスタリングが進むと小さくなっていく


*** 例題:都道府県別にみる麺類の消費傾向 (K-Meansバージョン) [#ve1f248f]

- 階層的クラスタリングのデータと標準化済みデータをそのまま使う

 #クラスタ番号が付いたデータフレームを確認
 df

 #標準化済みのデータを確認
 df_sc

*** K-Means法を適用 [#w40f306e]
- 同じようにクラスタ数k=7でクラスタリングを行ってみる

 from sklearn.cluster import KMeans
 
 #K-Mean法を用いる.クラスタ数は7で与える
 model_km = KMeans(n_clusters=7, random_state=0)
 
 #標準化済データでフィット
 model_km.fit(df_sc)
 #ラベルを表示
 model_km.labels_

*** 結果の確認 [#c71523ea]

 #dfの列にK-Meansで分けられたクラスタ番号を入れる
 df["クラスタ2"] = model_km.labels_
 df

 #各クラスタの都道府県を表示
 for i in range(7):
   print(f"\n\n【クラスタ2 No. {i}】")
   print("\n",df[df["クラスタ2"]==i]) 

 #K-Meansで分けられたクラスタ番号でグループ化
 df_mean2 = df.groupby("クラスタ2").mean()
 #元のクラスタの列を消す
 df_mean2.drop(columns=["クラスタ"], inplace=True)
 #積み上げ棒グラフ
 df_mean2.plot.bar(stacked=True,figsize=(8,8))

CENTER:&attachref(4_noodle_result_kmeans.png);~
CENTER:図19:各クラスタの麺類の消費額の内訳(K-Means版)

*** エルボー法 (elbow method): K-Meansのkをいくつにすれば良いか? [#x701bf25]
- K-Means法ではあらかじめ分析者がkを決めて与える必要がある
-- 明確な基準がない場合はどうするか?
- エルボー法
-- 様々なkでK-Meansクラスタリングを試してみて, ''クラスタ内誤差平方和 (SSE)''(model.inertia_ = 各クラスタの重心までの2乗距離の合計)が顕著に下がる点をさがす
-- kを横軸,inertia_を縦軸にグラフを描いてみて,ひじ(=エルボー)のように折れ曲がっているkを見つける

 #麺類消費クラスタリングを色んなkでやってみる
 #誤差を入れるリスト
 dist = []
 
 #2~47(=都道府県数)の範囲
 X=range(2,48)
 
 for k in X:
   #k個のクラスタを作る
   model = KMeans(n_clusters=k, random_state=0, n_init=10)
   model.fit(df_sc)
   #その時の誤差をリストに追加
   dist.append(model.inertia_)
 
 #折れ線グラフにプロット
 plt.plot(X, dist, marker='^')
 plt.grid()

CENTER:&attachref(4_noodle_elbow.png);~
CENTER:図20:エルボー法の結果

 #k=14としてK-meansを適用
 k = 14
 model = KMeans(n_clusters=k, random_state=0, n_init=10)
 model.fit(df_sc)

 #結果をクラスタ3という列で保存
 df["クラスタ3"] = model.labels_
 #各クラスタの都道府県を表示
 for i in range(k):
   print(f"\n\n【クラスタ3 No. {i}】")
   print("\n",df[df["クラスタ3"]==i])

** 演習課題 [#x59f7ef9]
- [[第4回/演習]]


トップ   編集 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS