- 追加された行はこの色です。
- 削除された行はこの色です。
- 第4回 は削除されています。
- 第4回 の差分を削除
#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回/演習]]