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

【Pandas】任意の教科数と人数の成績表から学力を平均的に指定数でクラス分けする

$
0
0

成績表をもとに学力が平均的になるようにグループ分けする最終回

成績表をもとに学力が平均化するようにグループ分けするシリーズ、今回は最終回。
任意の教科数と人数の入った成績表を指定クラス数に分け、バラツキ最小と最大のクラス分けを返す関数を作る。



【実行環境】

  • Windows10
  • WSL:Ubuntu:Anaconda4.10.1
  • Python3.8.10
  • Jupyter Notebook6.4.0
  • 使用ライブラリ
    • numpy1.20.2、pandas1.2.4、matplotlib3.3.4、scikit-learn0.24.2、japanize-matplotlib(グラフの日本語表示用)



Anacondaをインストールすると今回使うライブラリは最初から全部入っているようなので(japanize-matplotlib以外は)インストー ルは不要です。
入ってないようならcondapip

$ pip install notebook numpy pandas matplotlib sklearn japanize-matplotlib

など環境に合わせてインストールしておきます。



【クラス分けの方法について】

方法の手順として、

  1. 学年全員分の成績表データフレームを読み込む
  2. そのデータフレームをランダムにシャッフルして、指定グループ数に分け、クラス番号を割り振る
  3. クラスごとで教科や合計等の平均値を出す
  4. クラス間で平均値の標準偏差を出しバラツキを見る
  5. クラス間の教科や合計の総合的な標準偏差を算出する
  6. 2~5の手順を指定回繰り返し、ランダム生成した全てのデータフレームと総合的な標準偏差を保存する
  7. 最も平均的にグループ分けしたデータフレームを出力する



成績表を生成する

成績表の例として前回までと同様、国・数・社・理・英、各100点満点のテストの成績表を100人分、一様分布のランダムな値で生成します。

# 成績表の生成import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib
np.random.seed(0) # ランダムの固定# 100行5列でランダムな配列を生成
arr = np.random.randint(0,101,500).reshape(100, 5)
# 成績表データフレームの作成
col = ["国語", "数学", "社会", "理科", "英語"]
df = pd.DataFrame(arr, columns=col)

df
国語数学社会理科英語
04447646767
1983213687
27088881258
36539874688
48137257772
..................
953668804710
969491436331
97207096091
983583761874
99989743312

100 rows × 5 columns



関数の作成

上記の手順を処理する関数を作っていきます。

個人ごとに合計点を算出し、順位付けを行う関数

上記のような形式の成績表を引数に与えると、合計点を個人ごとに算出し、その値の大きい順から順位付けを行う関数。

defranking(df):
    '''元の成績表から個人ごとに合計点を算出し、その値の大きい順から順位付けを行います。「合計」「順位」列を追加します。'''# コピー
    ranked_df = df.copy()
    # 合計点算出と合計列追加
    ranked_df['合計'] = df.sum(axis=1)
    # 合計点順位列の追加 (降順で順位付け、同一順位は最小値を取る、順位を整数値に変換)
    ranked_df['順位'] = ranked_df['合計'].rank(method='min', ascending=False).astype('int')

    return ranked_df



指定の個数にグループ分けする関数

上記の関数で生成された成績表を行でランダムにシャッフルし、指定個数num個のグループに分け、クラス番号を割り振ります。

defshuffle_df(ranked_df, num):
    '''成績表を基にランダムで指定グループ数にクラス分けします。「クラス番号」列を追加します。'''# 成績表dfの行をランダムに入れ替える
    df_shuffled = ranked_df.sample(frac=1) # 乱数固定なし# 指定グループ数に分割し、クラス番号列を追加
    group_name_list = np.array_split(np.zeros(df_shuffled.shape[0]), num) # 成績表の行数個のゼロ配列をnumグループに分割for group, n inzip(group_name_list, [i for i inrange(num)]): # 各グループの要素をグループ名に置換
        group.fill(n)
    group_name_arr = np.hstack(group_name_list)  # 結合して1次元配列にする
    df_shuffled['クラス番号'] = group_name_arr.astype(np.int64)  # データフレームに列追加return df_shuffled



クラスごと平均点、クラス間標準偏差、全体的な標準偏差を算出する関数

↑のデータフレームに対して、下記の3つの戻り値を返す関数を作ります。

  • Pandasのgroupby().mean()メソッドでクラス別の平均点データフレーム
  • 平均点データフレームに対し、std()メソッドで標準偏差を算出
  • 総合的なばらつきを見るために、クラス間の標準偏差を算出
defoutput_std(_df):
    '''ランダムにクラス分けしたデータフレーム保存リストから、・クラスごとの平均値データフレーム、・クラス間の標準偏差、・総合的な標準偏差の3つを格納したリストを返します。'''# クラスごとの平均値データフレーム
    mean_df = _df.groupby('クラス番号').mean()
    # 平均値のクラス間標準偏差
    mean_std = _df.groupby('クラス番号').mean().std()
    # 総合的な標準偏差
    total_std = _df.groupby('クラス番号').mean().std().std()

    return [mean_df, mean_std, total_std]



クラスごとの平均値積み上げ棒グラフ関数

確認用で、クラスごとの平均値データフレームから積み上げ棒グラフを作成する関数を作ります。
各教科ごと平均値が積み上がったクラスごとの棒グラフができます。

defdraw_graph(mean_df, save_fname='クラスごと平均値積み上げ棒グラフ'):
    '''クラスごとの平均値データフレームから積み上げ棒グラフを描画します。'''
    fig, ax = plt.subplots(figsize=(15, 8), facecolor='w')
    mean_df.plot.bar(y=mean_df.columns[:-2], ax=ax, alpha=1.0, stacked=True)
    plt.xticks(rotation=0, size=16)
    plt.title(save_fname, size=15)
    plt.xlabel('クラス名')
    plt.ylabel('平均値・合計値')
    plt.grid()
    plt.savefig(save_fname+'.jpg')
    plt.show()

複数のデータフレームをリストに保存する

データフレームのオブジェクトをリストに次々と追加していく。

# 複数のデータフレームをリストに追加・保存する関数defsave_dfs(df_lst, random_df):
    '''複数のデータフレームをリストに追加・保存します。'''
    df_lst.append(random_df)
    return df_lst



メイン関数

全体を指定の分割グループ数に分けます。

defmain(df, split_num=4):
    '''成績表dfをランダムでシャッフルし、指定のグループ数に分けることを100回行います。全てのグループ分けを保存しリストにします。その中からバラツキ最小なベストクラス編成とバラツキ最大のワーストクラス編成をリストで返します。戻り値:リスト    [        [ベストクラス編成済成績表df、ベストクラス編成平均値df、最小標準偏差],        [ワーストクラス編成済成績表df、ワースト平均値df、最大標準偏差]    ]'''# 生成データフレーム保存用リスト
    all_lst = []

    # クラス分けをtrial回繰り返すfor i inrange(100):
        df_each_lst = [] # 試行ごとのデータ保存用リスト
        ranked_df = ranking(df) # 合計列と順位列の追加
        df_shuffled_class = shuffle_df(ranked_df, split_num) # ランダムにクラス分け
        std_lst = output_std(df_shuffled_class) # 平均値・標準偏差の算出# シャッフルされた試行ごとのデータフレームと標準偏差の保存
        each_lst = save_dfs(df_each_lst, df_shuffled_class)
        each_lst.extend(std_lst)
        # 全生成データの保存
        all_lst.append(df_each_lst)

    # 最もバラツキの少ない標準偏差
    min_std = np.min([i[3] for i in all_lst])
    # バラツキ最小の試行回インデックス
    min_index = [i[3] for i in all_lst].index(min_std)
    # バラツキ最小の平均点データフレーム
    min_std_df = all_lst[min_index][1]
    # バラツキ最小のクラス分け
    best_df = all_lst[min_index][0]

    # 最もバラツキの大きい標準偏差
    max_std = np.max([i[3] for i in all_lst])
    # バラツキ最大の試行回インデックス
    max_index = [i[3] for i in all_lst].index(max_std)
    # バラツキ最大の平均点データフレーム
    max_std_df = all_lst[max_index][1]
    # バラツキ最大のクラス分け
    worst_df = all_lst[max_index][0]

    return [[best_df, min_std_df, min_std], [worst_df, max_std_df, max_std]]

実行

5教科の成績表をもとに5クラスに編成する。
最もバラツキのない編成と、最もいびつな編成を取り出す。

# 実行
data = main(df, split_num=5)

# 表示print('最小標準偏差',data[0][2])
print('最大標準偏差',data[1][2])
最小標準偏差 1.2918541020351155
最大標準偏差 7.15542116374981
  • ベストなクラス分け
# バラツキ最小のベストなクラス分け
data[0][0]
国語数学社会理科英語合計順位クラス番号
805130535843235620
36964671186264380
649243834941308140
26359467824632460
1983213687236600
...........................
765087979171914
838026355849248504
799151991834293234
225036344893261414
784526745249246534

100 rows × 8 columns

  • いびつなクラス分け
# バラツキ最大のいびつなクラス分け
data[1][0]
国語数学社会理科英語合計順位クラス番号
742082682299291260
47543728227148960
25235929862244540
67580866316223640
5920806979257450
...........................
549744343488297194
143511468291265374
424054791138222654
596735302933194844
204849694135242554

100 rows × 8 columns

# バラツキ最小のベストなクラス分けグラフ
draw_graph(data[0][1], '5教科5クラス編成ベスト')

f:id:chayarokurokuro:20210715042446j:plain

# バラツキ最大のワーストなクラス分けグラフ
draw_graph(data[1][1], '5クラス編成ワースト')

f:id:chayarokurokuro:20210715042529j:plain



教科数と人数を増やして実行

5教科以外でも動くかどうか確認。
元の成績表を9教科にし、1000人に増やして確かめる。

# 9教科の成績表を1000人分生成する
arr = np.random.randint(0,101,9000).reshape(1000, 9)
# 成績表データフレームの作成
col = ["国語", "数学", "社会", "理科", "英語", '体育', '音楽', '美術', '家庭']
df_9 = pd.DataFrame(arr, columns=col)
# 3行だけ表示
df_9
国語数学社会理科英語体育音楽美術家庭
068801567266404752
151380307262917
224905545036542122
362635880592748863
4308280147991704112
..............................
99544931645299612769
996921552946793988724
99726627454178281070
9983718836755202245
9995811142952784188

1000 rows × 9 columns

# 実行 6クラス編成
data_9 = main(df_9, split_num=6)

# 表示print('最小標準偏差',data_9[0][2])
print('最大標準偏差',data_9[1][2])
最小標準偏差 1.6666133026238075
最大標準偏差 13.120630584238242
# バラツキ最小のベストなクラス分け
data_9[0][0]
国語数学社会理科英語体育音楽美術家庭合計順位クラス番号
809784845388373743767940
231918054331182988124694080
853208096033458247934694080
2262950711514997881935301820
7708989249377580844514970
.......................................
713918648637857465275192125
828855767884654779355092405
244943387428452813249155
1407271408139655360355162205
565418034824343214283049395

1000 rows × 12 columns

# バラツキ最大のいびつなクラス分け
data_9[1][0].head()
国語数学社会理科英語体育音楽美術家庭合計順位クラス番号
664864532860561575293887450
3995034178541004184705212050
874926517731598766671573870
130635760186616357374226260
59366425616343583564156490
# バラツキ最小のベストなクラス分けグラフ
draw_graph(data_9[0][1], '9教科6クラス編成ベスト')

f:id:chayarokurokuro:20210715042628j:plain

# バラツキ最大のワーストなクラス分けグラフ
draw_graph(data_9[1][1], '9教科6クラス編成ワースト')

f:id:chayarokurokuro:20210715042656j:plain



おわりに

任意の教科数と人数の入った成績表のデータフレームをランダムでシャッフルし、指定のクラス数に編成するプログラムができた。100回試行のうち最もバラツキの少ない編成と最も大きい編成とを抽出できる。

最終的にどういう値をどういう型で返す関数を作るのかを先にある程度決めておかないと、どうにでもなるというか、作りながら適当にやっているので締まりのない物が出来てしまう。そういうものしか作れないというのもあるけど。設計は大事ですね。それと想定外の使い方がされた場合の対処も。今回のは全く未対応。ドキュメントも適当。後で見返したら使い方わからんだろうw

気まぐれで始めてしまったものの大して面白くないシリーズをこの回で終わりにしますが、Pandasのグラフ機能を使うと積み上げ棒グラフがとても簡単に描けることと、リストが何でもかんでも入って便利だと分かったのは収穫でございました。



以上です。


Viewing all articles
Browse latest Browse all 30

Trending Articles