Quantcast
Channel: pandas - よちよちpython
Viewing all articles
Browse latest Browse all 30

【Scikit-learn】k-平均法(k-means)を使って成績表からおまかせクラス編成する

$
0
0

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以外は)インストールは不要です。
入ってないようならcondapip

$ 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
国語数学社会理科英語合計順位
0444870185123164
1474985173022866
2646931935331017
3674113845826343
467357124321872
........................
9558616791983752
96238335519728924
97793330994328429
981332291839598
998510033341226439

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
国語数学社会理科英語合計順位クラス番号
04448701851231642
14749851730228662
26469319353310171
36741138458263431
4673571243218722
...........................
95586167919837521
962383355197289241
977933309943284291
9813322918395980
9985100333412264392

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()
国語数学社会理科英語合計順位
クラス番号
031.64705932.58823527.23529422.70588227.294118141.47058892.000000
167.75000056.00000044.57142980.35714366.428571315.10714316.428571
264.92857155.32142962.03571426.32142951.964286260.57142942.928571
323.03703745.51851943.29629670.33333342.703704224.88888966.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)
国語数学社会理科英語合計順位クラス番号
16399811726181850
27802612113177860
36292037081167880
391427271689173870
426558232416186840
453210465124163900
4631862084149920
52283291925113970
5636193132394990
63422410367119960
6458227259121950
65313453254165890
71111944823159910
7614525768146930
791214751310124940
87356149731000
9813322918395980

この組は合計点が低い人が集まっていますね。順位もみな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()
国語数学社会理科英語合計順位クラス番号
04448701851231642
14749851730228662
26469319353310171
36741138458263431
4673571243218722
# 順位列の要素ごとをカウント
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
国語数学社会理科英語合計順位クラス番号
04448701851231640
14749851730228660
26469319353310173
36741138458263433
4673571243218720
...........................
95586167919837523
962383355197289243
977933309943284293
9813322918395982
9985100333412264390

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)
====================
国語数学社会理科英語合計順位
クラス番号
064.92857155.32142962.03571426.32142951.964286260.57142942.928571
123.03703745.51851943.29629670.33333342.703704224.88888966.518519
231.64705932.58823527.23529422.70588227.294118141.47058892.000000
367.75000056.00000044.57142980.35714366.428571315.10714316.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
国語数学社会理科英語
クラス番号
031.64705932.58823527.23529422.70588227.294118
167.75000056.00000044.57142980.35714366.428571
264.92857155.32142962.03571426.32142951.964286
323.03703745.51851943.29629670.33333342.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()

f:id:chayarokurokuro:20210702063700j:plain

各クラスごとの各教科平均点を各クラスごとに積み上げています。

おわりに

さらっとk-平均法のクラスタリングに触れましたが面白い。引数もいろいろあるので気が向いたらもう少し掘り下げる気分屋クラスタ
クラスター分析では各クラスターがどういった分布になっているかを図示するために主成分分析や次元削減と言われる手法を同時に用いるようです。厳密な評価基準に基づいて分類してはいないので、大雑把にタイプを振り分けて分類したい時に使うのでしょう。分類後の検証も必要なので分析者の主観や観点の依存度が高そうな感じ。客観的な何かが出てくる魔法の玉手箱ではないね。データ分析や機械学習全般に言えることだろうけど。

今回は以上です。



【Numpy・Pandas・Scikit-learn】成績表のDataFrameを行でシャッフルし、クラス分けする - よちよちpythonにつづく。


Viewing all articles
Browse latest Browse all 30

Trending Articles