forked from takahiro/analyze_lab_placement_survey
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8724d25f74 | |||
| d42b03071b |
+495
-137
@@ -1,11 +1,16 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
ZEN_TO_ASCII = str.maketrans("0123456789", "0123456789")
|
||||
ZEN_TO_ASCII = str.maketrans(
|
||||
"0123456789",
|
||||
"0123456789",
|
||||
)
|
||||
|
||||
|
||||
def z2a(s):
|
||||
@@ -15,11 +20,11 @@ def z2a(s):
|
||||
|
||||
def extract_values_from_body(body):
|
||||
"""
|
||||
研究室ごとの回答本文から GP 値を取り出す。
|
||||
研究室ごとの回答本文からGP値を取り出す。
|
||||
|
||||
- 「値 カウント」表がある場合は表を優先
|
||||
- 表がない場合は 1 行 1 回答として読む
|
||||
- 「編入生」は GP としては読まない
|
||||
- 表がない場合は1行1回答として読む
|
||||
- 「編入生」はGPとしては読まず、内部値0として扱う
|
||||
- 「365(修正済)」のような値にも対応
|
||||
"""
|
||||
|
||||
@@ -28,17 +33,23 @@ def extract_values_from_body(body):
|
||||
# --------------------------------------------------
|
||||
# Case 1: 「値 カウント」表がある場合
|
||||
# --------------------------------------------------
|
||||
m = re.search(r"^値\s+カウント\s*$", body, flags=re.MULTILINE)
|
||||
m = re.search(
|
||||
r"^値\s+カウント\s*$",
|
||||
body,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
if m:
|
||||
table_part = body[m.end():]
|
||||
|
||||
count_rows = re.findall(
|
||||
r"^\s*(-?\d+(?:\.\d+)?)\s+(\d+)\s*$", table_part,
|
||||
flags=re.MULTILINE
|
||||
r"^\s*(-?\d+(?:\.\d+)?)\s+(\d+)\s*$",
|
||||
table_part,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
values = []
|
||||
|
||||
for value_str, count_str in count_rows:
|
||||
value = float(value_str)
|
||||
count = int(count_str)
|
||||
@@ -65,7 +76,11 @@ def extract_values_from_body(body):
|
||||
continue
|
||||
|
||||
# 例: 365(修正済)
|
||||
m = re.match(r"^(-?\d+(?:\.\d+)?)", line)
|
||||
m = re.match(
|
||||
r"^(-?\d+(?:\.\d+)?)",
|
||||
line,
|
||||
)
|
||||
|
||||
if m:
|
||||
values.append(float(m.group(1)))
|
||||
|
||||
@@ -74,25 +89,31 @@ def extract_values_from_body(body):
|
||||
|
||||
def parse_google_form_results(text):
|
||||
"""
|
||||
Google Forms のアンケート結果をコピペしたテキストから、
|
||||
Google Formsのアンケート結果をコピペしたテキストから、
|
||||
|
||||
研究室番号 | 値
|
||||
|
||||
の DataFrame を生成する。
|
||||
のDataFrameを生成する。
|
||||
|
||||
回答者が0人の研究室についても、
|
||||
|
||||
研究室番号 | NaN
|
||||
|
||||
という行を作成してDataFrameに残す。
|
||||
"""
|
||||
|
||||
text = z2a(text)
|
||||
|
||||
# 「研究室第1希望」などの集計部分を除外し、
|
||||
# 「第一希望研究室」などの集計部分を除外し、
|
||||
# 最初の個別研究室ブロックから開始する。
|
||||
#
|
||||
# 以下の両方に対応:
|
||||
# 以下の両方に対応する。
|
||||
#
|
||||
# 研究室1
|
||||
# GP7 件の回答
|
||||
# GP7件の回答
|
||||
#
|
||||
# 研究室1
|
||||
# 7 件の回答
|
||||
# 7件の回答
|
||||
m = re.search(
|
||||
r"^研究室\s*[0-9]+[^\n]*\n"
|
||||
r"(?:[^\d\n]*?)"
|
||||
@@ -106,7 +127,7 @@ def parse_google_form_results(text):
|
||||
|
||||
text = text[m.start():]
|
||||
|
||||
# 各研究室ブロックを取得
|
||||
# 各研究室ブロックを取得する。
|
||||
pattern = re.compile(
|
||||
r"^研究室\s*([0-9]+)[^\n]*\n"
|
||||
r"(?:[^\d\n]*?)"
|
||||
@@ -128,41 +149,105 @@ def parse_google_form_results(text):
|
||||
n_answers = int(match.group(2))
|
||||
body = match.group(3).strip()
|
||||
|
||||
# 回答者が0人の研究室もDataFrameに残す。
|
||||
#
|
||||
# 編入生を表す内部値0と区別するため、
|
||||
# 回答者なしはNaNで表現する。
|
||||
if n_answers == 0:
|
||||
rows.append(
|
||||
{
|
||||
"研究室番号": lab_no,
|
||||
"値": np.nan,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
values = extract_values_from_body(body)
|
||||
|
||||
if len(values) != n_answers:
|
||||
print(f"警告: 研究室{lab_no}: 回答数={n_answers}, 抽出数={len(values)}")
|
||||
print(
|
||||
f"警告: 研究室{lab_no}: "
|
||||
f"回答数={n_answers}, 抽出数={len(values)}"
|
||||
)
|
||||
|
||||
for value in values:
|
||||
rows.append({"研究室番号": lab_no, "値": value})
|
||||
rows.append(
|
||||
{
|
||||
"研究室番号": lab_no,
|
||||
"値": value,
|
||||
}
|
||||
)
|
||||
|
||||
df = pd.DataFrame(rows)
|
||||
df = pd.DataFrame(
|
||||
rows,
|
||||
columns=["研究室番号", "値"],
|
||||
)
|
||||
|
||||
if not df.empty and (df["値"] % 1 == 0).all():
|
||||
df["値"] = df["値"].astype(int)
|
||||
if df.empty:
|
||||
return df
|
||||
|
||||
# すべての有効値が整数の場合は、NaNを保持できる
|
||||
# pandasのnullable整数型Int64に変換する。
|
||||
non_na_values = df["値"].dropna()
|
||||
|
||||
if (
|
||||
not non_na_values.empty
|
||||
and (non_na_values % 1 == 0).all()
|
||||
):
|
||||
df["値"] = df["値"].astype("Int64")
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def summarize_by_lab(df):
|
||||
"""研究室ごとの在校生数・編入生数・GP平均・標準偏差を計算する。"""
|
||||
"""
|
||||
研究室ごとの在校生数・編入生数・GP平均・標準偏差を計算する。
|
||||
|
||||
- NaNは回答者0人を表すため、人数には含めない
|
||||
- 値が10より大きい回答を在校生として扱う
|
||||
- 値が10以下の回答を編入生として扱う
|
||||
"""
|
||||
|
||||
required_columns = {
|
||||
"研究室番号",
|
||||
"値",
|
||||
}
|
||||
|
||||
missing_columns = required_columns - set(df.columns)
|
||||
|
||||
if missing_columns:
|
||||
raise KeyError(
|
||||
"必須列がありません: "
|
||||
+ ", ".join(sorted(missing_columns))
|
||||
)
|
||||
|
||||
rows = []
|
||||
|
||||
for lab_no in sorted(df["研究室番号"].unique()):
|
||||
df_lab = df.loc[df["研究室番号"] == lab_no].copy()
|
||||
df_lab = df.loc[
|
||||
df["研究室番号"] == lab_no
|
||||
].copy()
|
||||
|
||||
# 編入生は 0 として入れてある。
|
||||
df_regular = df_lab.loc[df_lab["値"] > 10]
|
||||
# 回答者0人を表すNaN行を除外する。
|
||||
df_answers = df_lab.loc[
|
||||
df_lab["値"].notna()
|
||||
].copy()
|
||||
|
||||
# 在校生と編入生を分類する。
|
||||
df_regular = df_answers.loc[
|
||||
df_answers["値"] > 10
|
||||
]
|
||||
|
||||
df_transfer = df_answers.loc[
|
||||
df_answers["値"] <= 10
|
||||
]
|
||||
|
||||
rows.append(
|
||||
{
|
||||
"研究室": lab_no,
|
||||
"回答者数": df_answers.shape[0],
|
||||
"在校生": df_regular.shape[0],
|
||||
"編入生": df_lab.shape[0] - df_regular.shape[0],
|
||||
"編入生": df_transfer.shape[0],
|
||||
"GP mean": df_regular["値"].mean(),
|
||||
"GP std": df_regular["値"].std(),
|
||||
}
|
||||
@@ -172,33 +257,59 @@ def summarize_by_lab(df):
|
||||
|
||||
|
||||
def print_summary(df, df_r):
|
||||
print("-" * 52)
|
||||
print("回答者数:", df.shape[0])
|
||||
print(f"{'研究室':>6} {'在校生':>6} {'編入生':>6} {'GP mean':>10} {'GP std':>10}")
|
||||
print("-" * 52)
|
||||
"""研究室ごとの集計結果を端末に表示する。"""
|
||||
|
||||
for n, row in df_r.iterrows():
|
||||
mean = "-" if pd.isna(row["GP mean"]) else f"{row['GP mean']:.1f}"
|
||||
std = "-" if pd.isna(row["GP std"]) else f"{row['GP std']:.1f}"
|
||||
# NaNは回答者0人の研究室を表すため数えない。
|
||||
n_respondents = int(df["値"].notna().sum())
|
||||
|
||||
print("-" * 66)
|
||||
print("回答者数:", n_respondents)
|
||||
|
||||
print(
|
||||
f"{'研究室':>6} "
|
||||
f"{'回答者':>8} "
|
||||
f"{'在校生':>8} "
|
||||
f"{'編入生':>8} "
|
||||
f"{'GP mean':>10} "
|
||||
f"{'GP std':>10}"
|
||||
)
|
||||
|
||||
print("-" * 66)
|
||||
|
||||
for lab_no, row in df_r.iterrows():
|
||||
mean = (
|
||||
"-"
|
||||
if pd.isna(row["GP mean"])
|
||||
else f"{row['GP mean']:.1f}"
|
||||
)
|
||||
|
||||
std = (
|
||||
"-"
|
||||
if pd.isna(row["GP std"])
|
||||
else f"{row['GP std']:.1f}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"{n:>6} "
|
||||
f"{lab_no:>6} "
|
||||
f"{int(row['回答者数']):>8} "
|
||||
f"{int(row['在校生']):>8} "
|
||||
f"{int(row['編入生']):>8} "
|
||||
f"{mean:>10} "
|
||||
f"{std:>10}"
|
||||
)
|
||||
|
||||
print("-" * 66)
|
||||
|
||||
|
||||
def apply_plot_style():
|
||||
"""
|
||||
論文図として使いやすい matplotlib の描画設定を適用する。
|
||||
論文図として使いやすいmatplotlibの描画設定を適用する。
|
||||
|
||||
仕様:
|
||||
- seaborn や scienceplots には依存しない。
|
||||
- タイトルは付けず、軸ラベルと目盛だけで情報を伝える。
|
||||
- フォントサイズ、線幅、余白、保存時 DPI を論文図向けに調整する。
|
||||
- カラーマップは既定で tab10 を使う。
|
||||
- seabornやscienceplotsには依存しない
|
||||
- タイトルは付けず、軸ラベルと目盛だけで情報を伝える
|
||||
- フォントサイズ、線幅、余白、保存時DPIを論文図向けに調整する
|
||||
- カラーマップは既定でtab10を使う
|
||||
"""
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
@@ -209,7 +320,11 @@ def apply_plot_style():
|
||||
"savefig.dpi": 300,
|
||||
"savefig.bbox": "tight",
|
||||
"font.family": "sans-serif",
|
||||
"font.sans-serif": ["Arial", "Helvetica", "DejaVu Sans"],
|
||||
"font.sans-serif": [
|
||||
"Arial",
|
||||
"Helvetica",
|
||||
"DejaVu Sans",
|
||||
],
|
||||
"font.size": 11,
|
||||
"axes.labelsize": 12,
|
||||
"axes.linewidth": 1.0,
|
||||
@@ -232,14 +347,15 @@ def apply_plot_style():
|
||||
|
||||
def filter_plot_data(data, lower=10, upper=None):
|
||||
"""
|
||||
GP の描画対象データを抽出する。
|
||||
GPの描画対象データを抽出する。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : pandas.DataFrame
|
||||
「研究室番号」と「値」列を持つデータフレーム。
|
||||
lower : float, default 10
|
||||
この値より大きい回答を描画対象にする。既定値では編入生を除く。
|
||||
この値より大きい回答を描画対象にする。
|
||||
既定値では編入生と回答者なしのNaNを除く。
|
||||
upper : float or None, default None
|
||||
指定した場合、この値未満の回答だけを描画対象にする。
|
||||
|
||||
@@ -247,46 +363,76 @@ def filter_plot_data(data, lower=10, upper=None):
|
||||
-------
|
||||
pandas.DataFrame
|
||||
フィルタ後のデータ。元データは変更しない。
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
必須列が存在しない場合。
|
||||
ValueError
|
||||
フィルタ後に描画対象データが残らない場合。
|
||||
"""
|
||||
|
||||
required_columns = {"研究室番号", "値"}
|
||||
missing_columns = required_columns - set(data.columns)
|
||||
if missing_columns:
|
||||
raise KeyError(f"必須列がありません: {', '.join(sorted(missing_columns))}")
|
||||
required_columns = {
|
||||
"研究室番号",
|
||||
"値",
|
||||
}
|
||||
|
||||
missing_columns = required_columns - set(data.columns)
|
||||
|
||||
if missing_columns:
|
||||
raise KeyError(
|
||||
"必須列がありません: "
|
||||
+ ", ".join(sorted(missing_columns))
|
||||
)
|
||||
|
||||
# NaNは比較結果がFalseになるため、ここで自動的に除外される。
|
||||
df = data.loc[
|
||||
data["値"] > lower,
|
||||
["研究室番号", "値"],
|
||||
].copy()
|
||||
|
||||
df = data.loc[data["値"] > lower, ["研究室番号", "値"]].copy()
|
||||
if upper is not None:
|
||||
df = df.loc[df["値"] < upper].copy()
|
||||
df = df.loc[
|
||||
df["値"] < upper
|
||||
].copy()
|
||||
|
||||
if df.empty:
|
||||
raise ValueError("描画対象のデータがありません。lower/upper を確認してください。")
|
||||
raise ValueError(
|
||||
"描画対象のデータがありません。"
|
||||
"lower/upperを確認してください。"
|
||||
)
|
||||
|
||||
# matplotlib/numpyで扱いやすい通常のfloat型に変換する。
|
||||
df["値"] = df["値"].astype(float)
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def make_histogram_bins(values, bin_width):
|
||||
"""
|
||||
GP ヒストグラム用のビン境界を作成する。
|
||||
GPヒストグラム用のビン境界を作成する。
|
||||
|
||||
仕様:
|
||||
- 最小値側は bin_width の倍数に切り下げる。
|
||||
- 最大値側は bin_width の倍数に切り上げ、最大値が最後のビンに入るようにする。
|
||||
- bin_width は正の数でなければならない。
|
||||
- 最小値側はbin_widthの倍数に切り下げる
|
||||
- 最大値側はbin_widthの倍数に切り上げる
|
||||
- 最大値が最後のビンに入るようにする
|
||||
- bin_widthは正の数でなければならない
|
||||
"""
|
||||
|
||||
if bin_width <= 0:
|
||||
raise ValueError("bin_width は正の数を指定してください。")
|
||||
raise ValueError(
|
||||
"bin_widthは正の数を指定してください。"
|
||||
)
|
||||
|
||||
min_edge = np.floor(values.min() / bin_width) * bin_width
|
||||
max_edge = np.ceil(values.max() / bin_width) * bin_width + bin_width
|
||||
return np.arange(min_edge, max_edge, bin_width)
|
||||
min_edge = (
|
||||
np.floor(values.min() / bin_width)
|
||||
* bin_width
|
||||
)
|
||||
|
||||
max_edge = (
|
||||
np.ceil(values.max() / bin_width)
|
||||
* bin_width
|
||||
+ bin_width
|
||||
)
|
||||
|
||||
return np.arange(
|
||||
min_edge,
|
||||
max_edge,
|
||||
bin_width,
|
||||
)
|
||||
|
||||
|
||||
def style_axis(ax, grid_axis="y"):
|
||||
@@ -294,31 +440,51 @@ def style_axis(ax, grid_axis="y"):
|
||||
軸まわりの体裁を統一する。
|
||||
|
||||
仕様:
|
||||
- 上枠と右枠を消す。
|
||||
- 目盛を外向きにする。
|
||||
- 指定軸方向に薄いグリッドを入れ、文字やデータ点と競合しない濃度にする。
|
||||
- 上枠と右枠を消す
|
||||
- 目盛を外向きにする
|
||||
- 指定軸方向に薄いグリッドを入れる
|
||||
"""
|
||||
|
||||
ax.spines["top"].set_visible(False)
|
||||
ax.spines["right"].set_visible(False)
|
||||
ax.tick_params(direction="out", length=4, width=1)
|
||||
ax.grid(axis=grid_axis, color="0.88", linewidth=0.8)
|
||||
|
||||
ax.tick_params(
|
||||
direction="out",
|
||||
length=4,
|
||||
width=1,
|
||||
)
|
||||
|
||||
ax.grid(
|
||||
axis=grid_axis,
|
||||
color="0.88",
|
||||
linewidth=0.8,
|
||||
)
|
||||
|
||||
ax.set_axisbelow(True)
|
||||
|
||||
|
||||
def draw_lab_boxplot(ax, df, labs, color, rng):
|
||||
"""
|
||||
研究室別 GP 分布を箱ひげ図と実測点で描画する。
|
||||
研究室別GP分布を箱ひげ図と実測点で描画する。
|
||||
|
||||
仕様:
|
||||
- 箱ひげ図は外れ値を非表示にし、実測点を重ねて全回答の分布を見せる。
|
||||
- 実測点には固定乱数の jitter を与え、同じ値の点の重なりを避ける。
|
||||
- x 軸のカテゴリ順は labs の順序に従う。
|
||||
- 箱ひげ図は外れ値を非表示にする
|
||||
- 実測点を重ねて全回答の分布を見せる
|
||||
- 実測点には固定乱数のjitterを与える
|
||||
- x軸のカテゴリ順はlabsの順序に従う
|
||||
"""
|
||||
|
||||
positions = np.arange(1, len(labs) + 1)
|
||||
positions = np.arange(
|
||||
1,
|
||||
len(labs) + 1,
|
||||
)
|
||||
|
||||
grouped_values = [
|
||||
df.loc[df["研究室番号"] == lab, "値"].to_numpy(dtype=float) for lab in labs
|
||||
df.loc[
|
||||
df["研究室番号"] == lab,
|
||||
"値",
|
||||
].to_numpy(dtype=float)
|
||||
for lab in labs
|
||||
]
|
||||
|
||||
box = ax.boxplot(
|
||||
@@ -327,17 +493,38 @@ def draw_lab_boxplot(ax, df, labs, color, rng):
|
||||
widths=0.55,
|
||||
patch_artist=True,
|
||||
showfliers=False,
|
||||
medianprops={"color": "black", "linewidth": 1.2},
|
||||
boxprops={"facecolor": color, "edgecolor": "black", "linewidth": 1.0},
|
||||
whiskerprops={"color": "black", "linewidth": 1.0},
|
||||
capprops={"color": "black", "linewidth": 1.0},
|
||||
medianprops={
|
||||
"color": "black",
|
||||
"linewidth": 1.2,
|
||||
},
|
||||
boxprops={
|
||||
"facecolor": color,
|
||||
"edgecolor": "black",
|
||||
"linewidth": 1.0,
|
||||
},
|
||||
whiskerprops={
|
||||
"color": "black",
|
||||
"linewidth": 1.0,
|
||||
},
|
||||
capprops={
|
||||
"color": "black",
|
||||
"linewidth": 1.0,
|
||||
},
|
||||
)
|
||||
|
||||
for patch in box["boxes"]:
|
||||
patch.set_alpha(0.55)
|
||||
|
||||
for x_position, values in zip(positions, grouped_values):
|
||||
jitter = rng.uniform(-0.16, 0.16, size=len(values))
|
||||
for x_position, values in zip(
|
||||
positions,
|
||||
grouped_values,
|
||||
):
|
||||
jitter = rng.uniform(
|
||||
-0.16,
|
||||
0.16,
|
||||
size=len(values),
|
||||
)
|
||||
|
||||
ax.scatter(
|
||||
np.full(len(values), x_position) + jitter,
|
||||
values,
|
||||
@@ -348,53 +535,98 @@ def draw_lab_boxplot(ax, df, labs, color, rng):
|
||||
zorder=3,
|
||||
)
|
||||
|
||||
ax.set_xlim(0.4, len(labs) + 0.6)
|
||||
ax.set_xlim(
|
||||
0.4,
|
||||
len(labs) + 0.6,
|
||||
)
|
||||
|
||||
ax.set_xticks(positions)
|
||||
ax.set_xticklabels([str(lab) for lab in labs])
|
||||
|
||||
ax.set_xticklabels(
|
||||
[str(lab) for lab in labs]
|
||||
)
|
||||
|
||||
ax.set_xlabel("Laboratory")
|
||||
ax.set_ylabel("GP")
|
||||
|
||||
|
||||
def annotate_target_values(ax, target_values, color):
|
||||
def annotate_target_values(
|
||||
ax,
|
||||
target_values,
|
||||
color,
|
||||
):
|
||||
"""
|
||||
ヒストグラム上に対象研究室の GP を縦線と数値で示す。
|
||||
ヒストグラム上に対象研究室のGPを縦線と数値で示す。
|
||||
|
||||
仕様:
|
||||
- 同じ GP 値は 1 本の線にまとめる。
|
||||
- ラベルは軸上端の内側に置き、棒や軸ラベルとの重なりを避ける。
|
||||
- 近い値が複数ある場合はラベル高さを段階的にずらす。
|
||||
- 同じGP値は1本の線にまとめる
|
||||
- ラベルは軸上端の内側に置く
|
||||
- 近い値が複数ある場合はラベル高さをずらす
|
||||
"""
|
||||
|
||||
unique_values = pd.Series(target_values).value_counts().sort_index()
|
||||
for i, (value, count) in enumerate(unique_values.items()):
|
||||
ax.axvline(value, color=color, lw=1.3, alpha=0.85, linestyle="--")
|
||||
label = f"{value:g}" if count == 1 else f"{value:g} x{count}"
|
||||
unique_values = (
|
||||
pd.Series(target_values)
|
||||
.value_counts()
|
||||
.sort_index()
|
||||
)
|
||||
|
||||
for i, (value, count) in enumerate(
|
||||
unique_values.items()
|
||||
):
|
||||
ax.axvline(
|
||||
value,
|
||||
color=color,
|
||||
linewidth=1.3,
|
||||
alpha=0.85,
|
||||
linestyle="--",
|
||||
)
|
||||
|
||||
if count == 1:
|
||||
label = f"{value:g}"
|
||||
else:
|
||||
label = f"{value:g} x{count}"
|
||||
|
||||
ax.text(
|
||||
value,
|
||||
0.96 - 0.11 * (i % 3),
|
||||
label,
|
||||
transform=ax.get_xaxis_transform(),
|
||||
ha="center",
|
||||
va="top",
|
||||
horizontalalignment="center",
|
||||
verticalalignment="top",
|
||||
rotation=90,
|
||||
color=color,
|
||||
fontsize=9,
|
||||
bbox={"facecolor": "white", "edgecolor": "none",
|
||||
"alpha": 0.75, "pad": 1.2},
|
||||
bbox={
|
||||
"facecolor": "white",
|
||||
"edgecolor": "none",
|
||||
"alpha": 0.75,
|
||||
"pad": 1.2,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def draw_gp_histogram(ax, df, target_values, bin_width, color, target_color):
|
||||
def draw_gp_histogram(
|
||||
ax,
|
||||
df,
|
||||
target_values,
|
||||
bin_width,
|
||||
color,
|
||||
target_color,
|
||||
):
|
||||
"""
|
||||
全研究室の GP ヒストグラムを描画する。
|
||||
全研究室のGPヒストグラムを描画する。
|
||||
|
||||
仕様:
|
||||
- bin_width ごとの頻度を表示する。
|
||||
- 対象研究室の GP は点ではなく縦線で示し、全体分布内の位置を読みやすくする。
|
||||
- 研究室別箱ひげ図と同じ主色を使い、対象研究室は tab10 の別色で強調する。
|
||||
- bin_widthごとの頻度を表示する
|
||||
- 対象研究室のGPは縦線で示す
|
||||
- 研究室別箱ひげ図と同じ主色を使う
|
||||
"""
|
||||
|
||||
bins = make_histogram_bins(df["値"], bin_width)
|
||||
bins = make_histogram_bins(
|
||||
df["値"],
|
||||
bin_width,
|
||||
)
|
||||
|
||||
ax.hist(
|
||||
df["値"],
|
||||
bins=bins,
|
||||
@@ -405,113 +637,239 @@ def draw_gp_histogram(ax, df, target_values, bin_width, color, target_color):
|
||||
)
|
||||
|
||||
if len(target_values) > 0:
|
||||
annotate_target_values(ax, target_values, target_color)
|
||||
annotate_target_values(
|
||||
ax,
|
||||
target_values,
|
||||
target_color,
|
||||
)
|
||||
|
||||
ax.set_xlabel("GP")
|
||||
ax.set_ylabel("Frequency")
|
||||
|
||||
|
||||
def plot_data(data, lower=10, upper=None, target_lab=10, bin_width=10):
|
||||
def plot_data(
|
||||
data,
|
||||
lower=10,
|
||||
upper=None,
|
||||
target_lab=None,
|
||||
bin_width=10,
|
||||
):
|
||||
"""
|
||||
研究室配属希望調査の GP 分布を描画する。
|
||||
研究室配属希望調査のGP分布を描画する。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : pandas.DataFrame
|
||||
「研究室番号」と「値」列を持つデータフレーム。
|
||||
lower : float, default 10
|
||||
描画対象に含める GP の下限。値が lower より大きい回答だけを描く。
|
||||
値がlowerより大きい回答だけを描く。
|
||||
upper : float or None, default None
|
||||
描画対象に含める GP の上限。None の場合は上限を設けない。
|
||||
target_lab : int, default 10
|
||||
ヒストグラム上で GP 値を縦線表示する研究室番号。
|
||||
GPの上限。Noneの場合は上限を設けない。
|
||||
target_lab : int, default None
|
||||
GP値を縦線表示する研究室番号。
|
||||
bin_width : float, default 10
|
||||
ヒストグラムのビン幅。
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[matplotlib.figure.Figure, numpy.ndarray]
|
||||
生成した Figure と 2 個の Axes。
|
||||
tuple
|
||||
生成したFigureと2個のAxes。
|
||||
|
||||
仕様:
|
||||
- matplotlib と pandas/numpy のみで描画する。
|
||||
- 図タイトルは付けない。
|
||||
- 上段に研究室別の箱ひげ図と個別点、下段に全体 GP ヒストグラムを描く。
|
||||
- 編入生は既定で除外するため、内部値 0 など lower 以下の値は描画しない。
|
||||
- 色は基本的に tab10 を使用する。
|
||||
- matplotlibとpandas/numpyのみで描画する
|
||||
- 図タイトルは付けない
|
||||
- 上段に研究室別箱ひげ図と個別点を描く
|
||||
- 下段に全体GPヒストグラムを描く
|
||||
- 編入生および回答者なしのNaNは描画しない
|
||||
"""
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
df = data.copy()
|
||||
df = filter_plot_data(
|
||||
data.copy(),
|
||||
lower=lower,
|
||||
upper=upper,
|
||||
)
|
||||
|
||||
df = filter_plot_data(df, lower=lower, upper=upper)
|
||||
labs = sorted(df["研究室番号"].unique())
|
||||
target_values = df.loc[df["研究室番号"] ==
|
||||
target_lab, "値"].to_numpy(dtype=float)
|
||||
labs = sorted(
|
||||
df["研究室番号"].unique()
|
||||
)
|
||||
|
||||
target_values = df.loc[
|
||||
df["研究室番号"] == target_lab,
|
||||
"値",
|
||||
].to_numpy(dtype=float)
|
||||
|
||||
apply_plot_style()
|
||||
|
||||
colors = plt.get_cmap("tab10").colors
|
||||
rng = np.random.default_rng(20260709)
|
||||
|
||||
rng = np.random.default_rng(
|
||||
20260709
|
||||
)
|
||||
|
||||
fig, axs = plt.subplots(
|
||||
2,
|
||||
1,
|
||||
figsize=(8.0, 6.2),
|
||||
gridspec_kw={"height_ratios": [1.1, 1]},
|
||||
gridspec_kw={
|
||||
"height_ratios": [1.1, 1],
|
||||
},
|
||||
constrained_layout=True,
|
||||
)
|
||||
|
||||
draw_lab_boxplot(axs[0], df, labs, colors[0], rng)
|
||||
draw_gp_histogram(axs[1], df, target_values,
|
||||
bin_width, colors[0], colors[3])
|
||||
draw_lab_boxplot(
|
||||
axs[0],
|
||||
df,
|
||||
labs,
|
||||
colors[0],
|
||||
rng,
|
||||
)
|
||||
|
||||
draw_gp_histogram(
|
||||
axs[1],
|
||||
df,
|
||||
target_values,
|
||||
bin_width,
|
||||
colors[0],
|
||||
colors[3],
|
||||
)
|
||||
|
||||
describe_text = (
|
||||
df["値"]
|
||||
.describe()
|
||||
.to_string(
|
||||
float_format=lambda x: f"{x:.1f}"
|
||||
)
|
||||
)
|
||||
|
||||
# 下段ヒストグラムの左上にdescribe()を表示する。
|
||||
axs[1].text(
|
||||
0.01,
|
||||
0.99,
|
||||
describe_text,
|
||||
transform=axs[1].transAxes,
|
||||
horizontalalignment="left",
|
||||
verticalalignment="top",
|
||||
fontsize=8,
|
||||
fontfamily="monospace",
|
||||
bbox={
|
||||
"facecolor": "white",
|
||||
"edgecolor": "gray",
|
||||
"alpha": 0.85,
|
||||
"boxstyle": "round,pad=0.2",
|
||||
},
|
||||
zorder=1,
|
||||
)
|
||||
|
||||
for ax in axs:
|
||||
style_axis(ax)
|
||||
|
||||
plt.show()
|
||||
|
||||
return fig, axs
|
||||
|
||||
|
||||
def make_output_filename(
|
||||
input_filename,
|
||||
extension,
|
||||
):
|
||||
"""
|
||||
入力ファイル名の拡張子を変更した出力ファイル名を作る。
|
||||
"""
|
||||
|
||||
if "." in input_filename:
|
||||
base = input_filename.rsplit(".", 1)[0]
|
||||
else:
|
||||
base = input_filename
|
||||
|
||||
return base + extension
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Google Forms のコピペ結果から研究室別GPを集計する"
|
||||
description=(
|
||||
"Google Formsのコピペ結果から"
|
||||
"研究室別GPを集計する"
|
||||
)
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"googleform_str", help="Google Forms からコピーしたテキストファイル"
|
||||
"googleform_str",
|
||||
help=(
|
||||
"Google Formsからコピーした"
|
||||
"テキストファイル"
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-o", "--output", help="Excel 出力ファイル名。指定しない場合は出力しない"
|
||||
"-o",
|
||||
"--output",
|
||||
help=(
|
||||
"Excel出力ファイル名。"
|
||||
"指定しない場合は出力しない"
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--plot",
|
||||
action="store_true",
|
||||
help="各種plotを作成するかどうか。指定しない場合は作成しない。",
|
||||
help=(
|
||||
"各種plotを作成する。"
|
||||
"指定しない場合は作成しない"
|
||||
),
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.googleform_str, encoding="utf-8") as f:
|
||||
with open(
|
||||
args.googleform_str,
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
text = f.read()
|
||||
|
||||
df = parse_google_form_results(text)
|
||||
# df_r = summarize_by_lab(df).sort_values(by="GP mean", ascending=False)
|
||||
df_r = summarize_by_lab(df)
|
||||
|
||||
print_summary(df, df_r)
|
||||
print_summary(
|
||||
df,
|
||||
df_r,
|
||||
)
|
||||
|
||||
if args.plot:
|
||||
fig, axs = plot_data(df)
|
||||
fig.savefig(args.googleform_str.replace(".txt", ".png"), dpi=300)
|
||||
print(args.googleform_str.replace(".txt", ".png"), "was created.")
|
||||
|
||||
plot_filename = make_output_filename(
|
||||
args.googleform_str,
|
||||
".png",
|
||||
)
|
||||
|
||||
fig.savefig(
|
||||
plot_filename,
|
||||
dpi=300,
|
||||
)
|
||||
|
||||
print(
|
||||
f"{plot_filename} was created."
|
||||
)
|
||||
|
||||
if args.output:
|
||||
with pd.ExcelWriter(args.output) as writer:
|
||||
df.to_excel(writer, sheet_name="raw")
|
||||
df_r.to_excel(writer, sheet_name="summary")
|
||||
df.to_excel(
|
||||
writer,
|
||||
sheet_name="raw",
|
||||
index=False,
|
||||
)
|
||||
|
||||
print(f"{args.output} was created.")
|
||||
df_r.to_excel(
|
||||
writer,
|
||||
sheet_name="summary",
|
||||
)
|
||||
|
||||
print(
|
||||
f"{args.output} was created."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user