k-means法(k-平均法)による、お任せクラス編成
前回の投稿では、Pandasで学校のテストの成績表のようなものを適当に作り、その合計点を算出して順位付けを行いました。
また、Pandasのグラフ作成機能を使って、積み上げ棒グラフを作成しました。
その合計100人の成績表をもとに、25人ずつ4クラスに分けたい。
その際、なるべく成績を平均化したい。成績表の合計点を使って平均的に分けたとして、ひょっとすると数学・理科が高い人が片寄ったりするかもしれない。教科ごとでも適度に分散し、合計でも平均的にしたい。
と、その前に、
今回は趣旨を変えて、機械学習の勉強がてら k-means法(k-平均法)
による「お任せクラス編成」をやってみたいと思います。
気分屋クラスタです。
【実行環境】
- Windows10
- WSL:Ubuntu:Anaconda
- Python3.8
- Jupyter Notebook
- 使用ライブラリ
- numpy、pandas、matplotlib、scikit-learn、japanize-matplotlib(グラフの日本語表示用)
Anacondaをインストールすると今回使うライブラリは最初から全部入っているようなので(japanize-matplotlib以外は)インストールは不要です。
入ってないようならconda
やpip
で
$ pip install notebook numpy pandas matplotlib sklearn japanize-matplotlib
など環境に合わせてインストールしておきます。
- ヴァージョン確認
# 使用ライブラリのインポート(確認用)import numpy as np import pandas as pd import matplotlib import sklearn print('・Anacondaのヴァージョン') !conda -V print('・Pythonのヴァージョン') !python -V print('・Jupyter Notebookのヴァージョン') !jupyter-notebook -V print('・numpyのヴァージョン') print(np.__version__) print('・pandasのヴァージョン') print(pd.__version__) print('・matplotlibのヴァージョン') print(matplotlib.__version__) print('・scikit-learnのヴァージョン') print(sklearn.__version__)
・Anacondaのヴァージョン
conda 4.10.1
・Pythonのヴァージョン
Python 3.8.10
・Jupyter Notebookのヴァージョン
6.4.0
・numpyのヴァージョン
1.20.2
・pandasのヴァージョン
1.2.4
・matplotlibのヴァージョン
3.3.4
・scikit-learnのヴァージョン
0.24.2
成績表をランダム生成する
前回と同様に、国・数・社・理・英、各100点満点のテストの成績表を100人分、一様分布のランダムな値で生成します。
5教科の合計点を個人ごとに算出し、その値の大きい順から順位付けを行います。
pythonは日本語の文字を変数として使えるので、横着して・・・
import numpy as np import pandas as pd np.random.seed(0) # ランダムの固定# 教科ごとのランダムな配列を生成国 = np.random.randint(0,101,100) 数 = np.random.randint(0,101,100) 社 = np.random.randint(0,101,100) 理 = np.random.randint(0,101,100) 英 = np.random.randint(0,101,100) # 成績表データフレームの作成 df = pd.DataFrame({ "国語" : 国, "数学" : 数, "社会" : 社, "理科" : 理, "英語" : 英 }) # 5教科合計列の追加 df['合計'] = df.sum(axis=1) # 合計点順位列の追加 (降順で順位付け、同一順位は最小値を取る、順位を整数値に変換) df['順位'] = df['合計'].rank(method='min', ascending=False).astype('int') # 表示 df
国語 | 数学 | 社会 | 理科 | 英語 | 合計 | 順位 | |
---|---|---|---|---|---|---|---|
0 | 44 | 48 | 70 | 18 | 51 | 231 | 64 |
1 | 47 | 49 | 85 | 17 | 30 | 228 | 66 |
2 | 64 | 69 | 31 | 93 | 53 | 310 | 17 |
3 | 67 | 41 | 13 | 84 | 58 | 263 | 43 |
4 | 67 | 35 | 71 | 2 | 43 | 218 | 72 |
... | ... | ... | ... | ... | ... | ... | ... |
95 | 58 | 61 | 67 | 91 | 98 | 375 | 2 |
96 | 23 | 83 | 35 | 51 | 97 | 289 | 24 |
97 | 79 | 33 | 30 | 99 | 43 | 284 | 29 |
98 | 13 | 32 | 29 | 18 | 3 | 95 | 98 |
99 | 85 | 100 | 33 | 34 | 12 | 264 | 39 |
100 rows × 7 columns
100人分の成績表ができました。
機械におまかせ、K-平均法(means)でのクラスター分析
k-means法(k-平均法)
は、全体のデータの中から似たり寄ったりの特徴のデータを持つメンバーで構成されたグループを指定個数で分類するデータ分析・機械学習のアルゴリズムで、「教師ラベル」を使わず分類モデルを作るので教師なし学習のクラスタリング手法
の一つ。
まず指定したk個のクラスタにランダムでわけ、重心点をランダムに決定した後クラスタの平均を用いて分類されるように調整されるのでk-平均法と呼ばれるそうです。なんのこっちゃです。
グループ分けされた後で、どういう分類がされたかを検証しなくてはなりません。
最初にランダムなクラス分けが為されるがそのランダムさによってクラス分けの結果に影響が出るとか、クラスターの最適な個数に気を付ける等々、いくつか注意点があるようですが、とりあえずsklearnでk-meansのクラスター分類やってみます。
上で作ったデータフレームをk-means法で4つのクラスタに分類します。
# ライブラリのインポート (k-平均法によるクラスター分析)from sklearn.cluster import KMeans # データを4クラスに分割、乱数固定 model_kmeans = KMeans(n_clusters=4, random_state=0) # 学習させる フィッティング model_kmeans.fit(df.values)
KMeans(n_clusters=4, random_state=0)
# 学習済みデータの重心点
model_kmeans.cluster_centers_
array([[ 31.64705882, 32.58823529, 27.23529412, 22.70588235,
27.29411765, 141.47058824, 92. ],
[ 67.75 , 56. , 44.57142857, 80.35714286,
66.42857143, 315.10714286, 16.42857143],
[ 64.92857143, 55.32142857, 62.03571429, 26.32142857,
51.96428571, 260.57142857, 42.92857143],
[ 23.03703704, 45.51851852, 43.2962963 , 70.33333333,
42.7037037 , 224.88888889, 66.51851852]])
# 重心点の形状
model_kmeans.cluster_centers_.shape
(4, 7)
# 出来たモデルで分割予測(クラスタリング)
cluster = model_kmeans.predict(df.values)
cluster
array([2, 2, 1, 1, 2, 3, 2, 2, 1, 1, 1, 2, 2, 3, 2, 2, 0, 1, 1, 2, 1, 3,
3, 2, 2, 3, 3, 0, 1, 1, 3, 1, 1, 2, 1, 2, 0, 1, 3, 0, 2, 1, 0, 3,
3, 0, 0, 1, 3, 1, 1, 2, 0, 3, 3, 3, 0, 3, 3, 3, 3, 1, 3, 0, 0, 0,
3, 2, 3, 2, 2, 0, 1, 2, 1, 3, 0, 1, 3, 0, 2, 2, 2, 1, 3, 3, 2, 0,
2, 1, 2, 2, 3, 3, 1, 1, 1, 1, 0, 2], dtype=int32)
おまかせ分類して頂きました。100人が指定通り4つのグループに分類され、0~3の値にラベル付けされています。リストの個数は100個あるかと思います。
# クラスタリングのラベル個数len(cluster)
100
では、この配列をデータフレームに「クラス番号」として列を追加します。
# データフレームをコピー df_class = df.copy() # 列追加 df_class['クラス番号'] = cluster # 表示 df_class
国語 | 数学 | 社会 | 理科 | 英語 | 合計 | 順位 | クラス番号 | |
---|---|---|---|---|---|---|---|---|
0 | 44 | 48 | 70 | 18 | 51 | 231 | 64 | 2 |
1 | 47 | 49 | 85 | 17 | 30 | 228 | 66 | 2 |
2 | 64 | 69 | 31 | 93 | 53 | 310 | 17 | 1 |
3 | 67 | 41 | 13 | 84 | 58 | 263 | 43 | 1 |
4 | 67 | 35 | 71 | 2 | 43 | 218 | 72 | 2 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
95 | 58 | 61 | 67 | 91 | 98 | 375 | 2 | 1 |
96 | 23 | 83 | 35 | 51 | 97 | 289 | 24 | 1 |
97 | 79 | 33 | 30 | 99 | 43 | 284 | 29 | 1 |
98 | 13 | 32 | 29 | 18 | 3 | 95 | 98 | 0 |
99 | 85 | 100 | 33 | 34 | 12 | 264 | 39 | 2 |
100 rows × 8 columns
クラスター分析結果の検証
さて、どういう観点でクラス分けされたんでしょうか? テストの点数の良しあしで4段階に分類されてるだろうとは思いますが。
クラスの振り分けの中身
groupby()
で各クラスを見てみます。
# 各クラス番号の中身print(df_class.groupby('クラス番号').groups)
{0: [16, 27, 36, 39, 42, 45, 46, 52, 56, 63, 64, 65, 71, 76, 79, 87, 98], 1: [2, 3, 8, 9, 10, 17, 18, 20, 28, 29, 31, 32, 34, 37, 41, 47, 49, 50, 61, 72, 74, 77, 83, 89, 94, 95, 96, 97], 2: [0, 1, 4, 6, 7, 11, 12, 14, 15, 19, 23, 24, 33, 35, 40, 51, 67, 69, 70, 73, 80, 81, 82, 86, 88, 90, 91, 99], 3: [5, 13, 21, 22, 25, 26, 30, 38, 43, 44, 48, 53, 54, 55, 57, 58, 59, 60, 62, 66, 68, 75, 78, 84, 85, 92, 93]}
学籍番号(インデックス番号)のリストがクラスごとに分けられた辞書型データっぽいものが返ってきました。
グループごとの平均値をgroupby()で算出
groupby()
メソッドを使って「クラス番号」のカテゴリーで平均値を算出してみます。
# クラス番号の平均値を算出 df_class.groupby('クラス番号').mean()
国語 | 数学 | 社会 | 理科 | 英語 | 合計 | 順位 | |
---|---|---|---|---|---|---|---|
クラス番号 | |||||||
0 | 31.647059 | 32.588235 | 27.235294 | 22.705882 | 27.294118 | 141.470588 | 92.000000 |
1 | 67.750000 | 56.000000 | 44.571429 | 80.357143 | 66.428571 | 315.107143 | 16.428571 |
2 | 64.928571 | 55.321429 | 62.035714 | 26.321429 | 51.964286 | 260.571429 | 42.928571 |
3 | 23.037037 | 45.518519 | 43.296296 | 70.333333 | 42.703704 | 224.888889 | 66.518519 |
合計点平均でクラス分けされているっぽい。
成績の合計点平均が高い順に、クラス番号が「1」⇒「2」⇒「3」⇒「0」と分かれている。
しかし、教科によっては若干偏りがあるようです。
パッと見、
- 理科以外の教科は「1,2,3,0」の順番で平均点が高い。
- 「0組」の各教科ごとの平均点が赤点ですね。5教科を捨てた芸術家か体育系グループか何かかな。
- 「2組」の合計点平均は2番目に高いが、理科の平均点は最も低い。
- 「3組」は理科の点数だけ高い。
もう一度実行すれば別のクラス編成になるんでしょうが、後ほど実行してみます。
各クラスを個別に抽出する
個別のデータフレームを取得したい場合はPandasのメソッドのdf.groupby('カテゴリ列').get_group('カテゴリ名')
のようにやると抽出できる。
# 0組だけ抽出する df_class.groupby('クラス番号').get_group(0)
国語 | 数学 | 社会 | 理科 | 英語 | 合計 | 順位 | クラス番号 | |
---|---|---|---|---|---|---|---|---|
16 | 39 | 98 | 1 | 17 | 26 | 181 | 85 | 0 |
27 | 80 | 2 | 61 | 21 | 13 | 177 | 86 | 0 |
36 | 29 | 20 | 37 | 0 | 81 | 167 | 88 | 0 |
39 | 14 | 27 | 27 | 16 | 89 | 173 | 87 | 0 |
42 | 65 | 58 | 23 | 24 | 16 | 186 | 84 | 0 |
45 | 32 | 10 | 46 | 51 | 24 | 163 | 90 | 0 |
46 | 31 | 86 | 20 | 8 | 4 | 149 | 92 | 0 |
52 | 28 | 32 | 9 | 19 | 25 | 113 | 97 | 0 |
56 | 36 | 19 | 3 | 13 | 23 | 94 | 99 | 0 |
63 | 42 | 24 | 10 | 36 | 7 | 119 | 96 | 0 |
64 | 58 | 2 | 27 | 25 | 9 | 121 | 95 | 0 |
65 | 31 | 3 | 45 | 32 | 54 | 165 | 89 | 0 |
71 | 11 | 19 | 44 | 82 | 3 | 159 | 91 | 0 |
76 | 14 | 52 | 5 | 7 | 68 | 146 | 93 | 0 |
79 | 12 | 14 | 75 | 13 | 10 | 124 | 94 | 0 |
87 | 3 | 56 | 1 | 4 | 9 | 73 | 100 | 0 |
98 | 13 | 32 | 29 | 18 | 3 | 95 | 98 | 0 |
この組は合計点が低い人が集まっていますね。順位もみな80位以下です。
他のクラスのメンバーの順位を見てみます。
# 4クラスごとのメンバーの順位をリストで抽出for i inrange(4): print(f'{i}組のメンバーの順位', df_class.groupby('クラス番号').get_group(i)['順位'].tolist())
0組のメンバーの順位 [85, 86, 88, 87, 84, 90, 92, 97, 99, 96, 95, 89, 91, 93, 94, 100, 98]
1組のメンバーの順位 [17, 43, 9, 44, 15, 7, 22, 5, 13, 1, 3, 12, 9, 26, 13, 8, 29, 31, 16, 26, 9, 4, 19, 6, 18, 2, 24, 29]
2組のメンバーの順位 [64, 66, 72, 62, 46, 36, 25, 36, 34, 39, 57, 26, 19, 38, 59, 45, 21, 23, 55, 32, 34, 50, 53, 39, 33, 49, 50, 39]
3組のメンバーの順位 [47, 58, 83, 76, 81, 71, 56, 72, 53, 61, 80, 47, 77, 79, 65, 52, 62, 82, 70, 78, 60, 72, 69, 39, 67, 67, 72]
合計点の平均値が高い順で「1」⇒「2」⇒「3」⇒「0」となっていましたが、メンバーの順位も同じようなカタマリになってるようです。
同一順位がいるので1組に9位が複数いたり、2組や3組に39位が何人もいたりします。
ついでに同一順位の人数を見ておこ。
# 成績表
df_class.head()
国語 | 数学 | 社会 | 理科 | 英語 | 合計 | 順位 | クラス番号 | |
---|---|---|---|---|---|---|---|---|
0 | 44 | 48 | 70 | 18 | 51 | 231 | 64 | 2 |
1 | 47 | 49 | 85 | 17 | 30 | 228 | 66 | 2 |
2 | 64 | 69 | 31 | 93 | 53 | 310 | 17 | 1 |
3 | 67 | 41 | 13 | 84 | 58 | 263 | 43 | 1 |
4 | 67 | 35 | 71 | 2 | 43 | 218 | 72 | 2 |
# 順位列の要素ごとをカウント df_class.value_counts('順位')
順位
72 4
39 4
26 3
9 3
13 2
..
2 1
57 1
58 1
59 1
100 1
Length: 80, dtype: int64
# 同一順位のかぶり最大個数 df_class.value_counts('順位').max()
4
各グループの形状
クラスタリングされた各クラスタのメンバー数は均一ではありません。
形状を見てみましょう。shape
で取り出します。
# 各クラスのデータの形状for i inrange(4): print(df_class.groupby('クラス番号').get_group(i).shape)
(17, 8)
(28, 8)
(28, 8)
(27, 8)
「0組」は人数が少ない。
じゃあ、もう一度クラス編成をやってもらいます。どんな分類になるかな?
##### k-平均法によるクラスター分析 2回目 ###### データを4クラスに分割、乱数固定 model_km2 = KMeans(n_clusters=4, random_state=1) # 学習させる フィッティング model_km2.fit(df.values) # 出来たモデルで分割予測(クラスタリング) cluster2 = model_km2.predict(df.values) # データフレームをコピー df_class2 = df.copy() # 列追加 df_class2['クラス番号'] = cluster2 # 表示 df_class2
国語 | 数学 | 社会 | 理科 | 英語 | 合計 | 順位 | クラス番号 | |
---|---|---|---|---|---|---|---|---|
0 | 44 | 48 | 70 | 18 | 51 | 231 | 64 | 0 |
1 | 47 | 49 | 85 | 17 | 30 | 228 | 66 | 0 |
2 | 64 | 69 | 31 | 93 | 53 | 310 | 17 | 3 |
3 | 67 | 41 | 13 | 84 | 58 | 263 | 43 | 3 |
4 | 67 | 35 | 71 | 2 | 43 | 218 | 72 | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
95 | 58 | 61 | 67 | 91 | 98 | 375 | 2 | 3 |
96 | 23 | 83 | 35 | 51 | 97 | 289 | 24 | 3 |
97 | 79 | 33 | 30 | 99 | 43 | 284 | 29 | 3 |
98 | 13 | 32 | 29 | 18 | 3 | 95 | 98 | 2 |
99 | 85 | 100 | 33 | 34 | 12 | 264 | 39 | 0 |
100 rows × 8 columns
# 各クラス番号の中身print(df_class2.groupby('クラス番号').groups) print('='*20) # 各クラスのデータの形状for i inrange(4): print(df_class2.groupby('クラス番号').get_group(i).shape) print('='*20) # クラス番号の平均値を算出 df_class2.groupby('クラス番号').mean()
{0: [0, 1, 4, 6, 7, 11, 12, 14, 15, 19, 23, 24, 33, 35, 40, 51, 67, 69, 70, 73, 80, 81, 82, 86, 88, 90, 91, 99], 1: [5, 13, 21, 22, 25, 26, 30, 38, 43, 44, 48, 53, 54, 55, 57, 58, 59, 60, 62, 66, 68, 75, 78, 84, 85, 92, 93], 2: [16, 27, 36, 39, 42, 45, 46, 52, 56, 63, 64, 65, 71, 76, 79, 87, 98], 3: [2, 3, 8, 9, 10, 17, 18, 20, 28, 29, 31, 32, 34, 37, 41, 47, 49, 50, 61, 72, 74, 77, 83, 89, 94, 95, 96, 97]}
====================
(28, 8)
(27, 8)
(17, 8)
(28, 8)
====================
国語 | 数学 | 社会 | 理科 | 英語 | 合計 | 順位 | |
---|---|---|---|---|---|---|---|
クラス番号 | |||||||
0 | 64.928571 | 55.321429 | 62.035714 | 26.321429 | 51.964286 | 260.571429 | 42.928571 |
1 | 23.037037 | 45.518519 | 43.296296 | 70.333333 | 42.703704 | 224.888889 | 66.518519 |
2 | 31.647059 | 32.588235 | 27.235294 | 22.705882 | 27.294118 | 141.470588 | 92.000000 |
3 | 67.750000 | 56.000000 | 44.571429 | 80.357143 | 66.428571 | 315.107143 | 16.428571 |
クラス番号の順番が入れ替わっただけで、中身自体は変わってないっぽい。
合計平均点の高い順に、クラス番号は
「1」⇒「2」⇒「3」⇒「0」
から
「3」⇒「0」⇒「1」⇒「2」
になった。クラスのメンバーはどうだろう?
# クラスのメンバー内容確認for i, j in [(1, 3), (2, 0), (3, 1), (0, 2)]: print(df_class.groupby('クラス番号').groups[i] == df_class2.groupby('クラス番号').groups[j])
[ True True True True True True True True True True True True
True True True True True True True True True True True True
True True True True]
[ True True True True True True True True True True True True
True True True True True True True True True True True True
True True True True]
[ True True True True True True True True True True True True
True True True True True True True True True True True True
True True True]
[ True True True True True True True True True True True True
True True True True True]
クラスメンバーの中身は一緒のようです。
k-平均法の引数は色々あって、その値によってクラス編成がもっと変わるかもしれません。今回はそこまでやりません。
集計データで積み上げ棒グラフを作成する
前回と同様に、Pandasのグラフ作成機能を利用して積み上げ棒グラフを描きます。df.plot.bar(stacked=True)
で簡単にイケます。
データは、Pandasのgroupby().mean()
メソッドでクラスごとの平均値を集計したデータフレームを用います。
それぞれ教科ごと平均値で色分けされ積み上げられた、4クラス分の棒があるグラフが出来る予定です。
# 棒グラフに使用する集計データ data = df_class.groupby('クラス番号').mean() # 教科の列に絞る data = data.iloc[:, :5] data
国語 | 数学 | 社会 | 理科 | 英語 | |
---|---|---|---|---|---|
クラス番号 | |||||
0 | 31.647059 | 32.588235 | 27.235294 | 22.705882 | 27.294118 |
1 | 67.750000 | 56.000000 | 44.571429 | 80.357143 | 66.428571 |
2 | 64.928571 | 55.321429 | 62.035714 | 26.321429 | 51.964286 |
3 | 23.037037 | 45.518519 | 43.296296 | 70.333333 | 42.703704 |
##### 積み上げ棒グラフを作成 #####import matplotlib.pyplot as plt import japanize_matplotlib #(グラフの日本語表示用 個人的事情でインポートしています)# グラフのサイズと背景色(白)設定 (jupyterの背景色を黒にしている個人的事情により) fig, ax = plt.subplots(figsize=(12,8), facecolor='w') # 積み上げ棒グラフ data.plot.bar(y=data.columns, ax=ax, alpha=1.0, stacked=True) # グラフのタイトル plt.title('クラスごと積み上げグラフ(5教科各平均点)') # ラベルの設置 plt.xlabel('クラス名') plt.ylabel('平均点別合計点') # 目盛りの回転 (デフォルトは90度反時計回り) plt.xticks(rotation=0) # グリッド線の設置 plt.grid() # グラフ画像の保存 plt.savefig('クラス毎積み上げ平均点.jpg') # グラフ表示 plt.show()
各クラスごとの各教科平均点を各クラスごとに積み上げています。
おわりに
さらっとk-平均法のクラスタリングに触れましたが面白い。引数もいろいろあるので気が向いたらもう少し掘り下げる気分屋クラスタ。
クラスター分析では各クラスターがどういった分布になっているかを図示するために主成分分析や次元削減と言われる手法を同時に用いるようです。厳密な評価基準に基づいて分類してはいないので、大雑把にタイプを振り分けて分類したい時に使うのでしょう。分類後の検証も必要なので分析者の主観や観点の依存度が高そうな感じ。客観的な何かが出てくる魔法の玉手箱ではないね。データ分析や機械学習全般に言えることだろうけど。
今回は以上です。
【Numpy・Pandas・Scikit-learn】成績表のDataFrameを行でシャッフルし、クラス分けする - よちよちpythonにつづく。