Compare commits
10 Commits
90cc27af0c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8724d25f74 | |||
| d42b03071b | |||
| 5de1073bd5 | |||
| 730b21f73d | |||
| 1dce5c8ac0 | |||
| 906afbc53d | |||
| 4d6a836cbf | |||
| eb43379ce4 | |||
| 83ce11279f | |||
| 69031cd4db |
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
+18
@@ -272,6 +272,24 @@ Excel ファイルには以下のシートを作成します。
|
|||||||
- 「値 / カウント」表
|
- 「値 / カウント」表
|
||||||
- Google Forms のグラフ由来の連結数字列
|
- Google Forms のグラフ由来の連結数字列
|
||||||
|
|
||||||
|
* 可視化例
|
||||||
|
|
||||||
|
解析スクリプトでは、研究室ごとの GP 分布や、特定研究室の希望者が全体の中で
|
||||||
|
どの位置にいるかを確認できます。
|
||||||
|
|
||||||
|
以下は出力図の例です。
|
||||||
|
|
||||||
|
#+CAPTION: GP 分布と第10研究室希望者の位置を示した可視化例
|
||||||
|
#+NAME: fig:gp-distribution
|
||||||
|
[[file:hoge.png]]
|
||||||
|
|
||||||
|
上段では、研究室ごとの GP 分布を箱ひげ図として示しています。
|
||||||
|
各研究室の希望者の GP のばらつきや中央値を比較できます。
|
||||||
|
|
||||||
|
下段では、全回答者の GP 分布をヒストグラムとして示しています。
|
||||||
|
第10研究室希望者の GP は縦線として重ねて表示しており、
|
||||||
|
第10研究室希望者が全体分布の中でどの程度の位置にいるかを確認できます。
|
||||||
|
|
||||||
* 注意事項
|
* 注意事項
|
||||||
|
|
||||||
- 入力テキストは Google Forms の表示形式に依存します。
|
- 入力テキストは Google Forms の表示形式に依存します。
|
||||||
|
|||||||
+708
-66
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -6,7 +7,10 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
ZEN_TO_ASCII = str.maketrans("0123456789", "0123456789")
|
ZEN_TO_ASCII = str.maketrans(
|
||||||
|
"0123456789",
|
||||||
|
"0123456789",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def z2a(s):
|
def z2a(s):
|
||||||
@@ -16,11 +20,11 @@ def z2a(s):
|
|||||||
|
|
||||||
def extract_values_from_body(body):
|
def extract_values_from_body(body):
|
||||||
"""
|
"""
|
||||||
研究室ごとの回答本文から GP 値を取り出す。
|
研究室ごとの回答本文からGP値を取り出す。
|
||||||
|
|
||||||
- 「値 カウント」表がある場合は表を優先
|
- 「値 カウント」表がある場合は表を優先
|
||||||
- 表がない場合は 1 行 1 回答として読む
|
- 表がない場合は1行1回答として読む
|
||||||
- 「編入生」は GP としては読まない
|
- 「編入生」はGPとしては読まず、内部値0として扱う
|
||||||
- 「365(修正済)」のような値にも対応
|
- 「365(修正済)」のような値にも対応
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -29,18 +33,23 @@ def extract_values_from_body(body):
|
|||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Case 1: 「値 カウント」表がある場合
|
# Case 1: 「値 カウント」表がある場合
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
m = re.search(r'^値\s+カウント\s*$', body, flags=re.MULTILINE)
|
m = re.search(
|
||||||
|
r"^値\s+カウント\s*$",
|
||||||
|
body,
|
||||||
|
flags=re.MULTILINE,
|
||||||
|
)
|
||||||
|
|
||||||
if m:
|
if m:
|
||||||
table_part = body[m.end():]
|
table_part = body[m.end():]
|
||||||
|
|
||||||
count_rows = re.findall(
|
count_rows = re.findall(
|
||||||
r'^\s*(-?\d+(?:\.\d+)?)\s+(\d+)\s*$',
|
r"^\s*(-?\d+(?:\.\d+)?)\s+(\d+)\s*$",
|
||||||
table_part,
|
table_part,
|
||||||
flags=re.MULTILINE
|
flags=re.MULTILINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
values = []
|
values = []
|
||||||
|
|
||||||
for value_str, count_str in count_rows:
|
for value_str, count_str in count_rows:
|
||||||
value = float(value_str)
|
value = float(value_str)
|
||||||
count = int(count_str)
|
count = int(count_str)
|
||||||
@@ -67,7 +76,11 @@ def extract_values_from_body(body):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 例: 365(修正済)
|
# 例: 365(修正済)
|
||||||
m = re.match(r'^(-?\d+(?:\.\d+)?)', line)
|
m = re.match(
|
||||||
|
r"^(-?\d+(?:\.\d+)?)",
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
|
||||||
if m:
|
if m:
|
||||||
values.append(float(m.group(1)))
|
values.append(float(m.group(1)))
|
||||||
|
|
||||||
@@ -76,31 +89,37 @@ def extract_values_from_body(body):
|
|||||||
|
|
||||||
def parse_google_form_results(text):
|
def parse_google_form_results(text):
|
||||||
"""
|
"""
|
||||||
Google Forms のアンケート結果をコピペしたテキストから、
|
Google Formsのアンケート結果をコピペしたテキストから、
|
||||||
|
|
||||||
研究室番号 | 値
|
研究室番号 | 値
|
||||||
|
|
||||||
の DataFrame を生成する。
|
のDataFrameを生成する。
|
||||||
|
|
||||||
|
回答者が0人の研究室についても、
|
||||||
|
|
||||||
|
研究室番号 | NaN
|
||||||
|
|
||||||
|
という行を作成してDataFrameに残す。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
text = z2a(text)
|
text = z2a(text)
|
||||||
|
|
||||||
# 「研究室第1希望」などの集計部分を除外し、
|
# 「第一希望研究室」などの集計部分を除外し、
|
||||||
# 最初の個別研究室ブロックから開始する。
|
# 最初の個別研究室ブロックから開始する。
|
||||||
#
|
#
|
||||||
# 以下の両方に対応:
|
# 以下の両方に対応する。
|
||||||
#
|
#
|
||||||
# 研究室1
|
# 研究室1
|
||||||
# GP7 件の回答
|
# GP7件の回答
|
||||||
#
|
#
|
||||||
# 研究室1
|
# 研究室1
|
||||||
# 7 件の回答
|
# 7件の回答
|
||||||
m = re.search(
|
m = re.search(
|
||||||
r'^研究室\s*[0-9]+[^\n]*\n'
|
r"^研究室\s*[0-9]+[^\n]*\n"
|
||||||
r'(?:[^\d\n]*?)'
|
r"(?:[^\d\n]*?)"
|
||||||
r'[0-9]+\s*件の回答',
|
r"[0-9]+\s*件の回答",
|
||||||
text,
|
text,
|
||||||
flags=re.MULTILINE
|
flags=re.MULTILINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not m:
|
if not m:
|
||||||
@@ -108,19 +127,19 @@ def parse_google_form_results(text):
|
|||||||
|
|
||||||
text = text[m.start():]
|
text = text[m.start():]
|
||||||
|
|
||||||
# 各研究室ブロックを取得
|
# 各研究室ブロックを取得する。
|
||||||
pattern = re.compile(
|
pattern = re.compile(
|
||||||
r'^研究室\s*([0-9]+)[^\n]*\n'
|
r"^研究室\s*([0-9]+)[^\n]*\n"
|
||||||
r'(?:[^\d\n]*?)'
|
r"(?:[^\d\n]*?)"
|
||||||
r'([0-9]+)\s*件の回答\s*\n'
|
r"([0-9]+)\s*件の回答\s*\n"
|
||||||
r'(.*?)'
|
r"(.*?)"
|
||||||
r'(?='
|
r"(?="
|
||||||
r'^研究室\s*[0-9]+[^\n]*\n'
|
r"^研究室\s*[0-9]+[^\n]*\n"
|
||||||
r'(?:[^\d\n]*?)'
|
r"(?:[^\d\n]*?)"
|
||||||
r'[0-9]+\s*件の回答'
|
r"[0-9]+\s*件の回答"
|
||||||
r'|\Z'
|
r"|\Z"
|
||||||
r')',
|
r")",
|
||||||
flags=re.MULTILINE | re.DOTALL
|
flags=re.MULTILINE | re.DOTALL,
|
||||||
)
|
)
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
@@ -130,7 +149,17 @@ def parse_google_form_results(text):
|
|||||||
n_answers = int(match.group(2))
|
n_answers = int(match.group(2))
|
||||||
body = match.group(3).strip()
|
body = match.group(3).strip()
|
||||||
|
|
||||||
|
# 回答者が0人の研究室もDataFrameに残す。
|
||||||
|
#
|
||||||
|
# 編入生を表す内部値0と区別するため、
|
||||||
|
# 回答者なしはNaNで表現する。
|
||||||
if n_answers == 0:
|
if n_answers == 0:
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"研究室番号": lab_no,
|
||||||
|
"値": np.nan,
|
||||||
|
}
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
values = extract_values_from_body(body)
|
values = extract_values_from_body(body)
|
||||||
@@ -142,92 +171,705 @@ def parse_google_form_results(text):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for value in values:
|
for value in values:
|
||||||
rows.append({
|
rows.append(
|
||||||
"研究室番号": lab_no,
|
{
|
||||||
"値": value
|
"研究室番号": lab_no,
|
||||||
})
|
"値": value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
df = pd.DataFrame(rows)
|
df = pd.DataFrame(
|
||||||
|
rows,
|
||||||
|
columns=["研究室番号", "値"],
|
||||||
|
)
|
||||||
|
|
||||||
if not df.empty and (df["値"] % 1 == 0).all():
|
if df.empty:
|
||||||
df["値"] = df["値"].astype(int)
|
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
|
return df
|
||||||
|
|
||||||
|
|
||||||
def summarize_by_lab(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 = []
|
rows = []
|
||||||
|
|
||||||
for lab_no in sorted(df["研究室番号"].unique()):
|
for lab_no in sorted(df["研究室番号"].unique()):
|
||||||
df_lab = df.loc[df["研究室番号"] == lab_no].copy()
|
df_lab = df.loc[
|
||||||
|
df["研究室番号"] == lab_no
|
||||||
|
].copy()
|
||||||
|
|
||||||
# 編入生は 0 として入れてある。
|
# 回答者0人を表すNaN行を除外する。
|
||||||
df_regular = df_lab.loc[df_lab["値"] > 10]
|
df_answers = df_lab.loc[
|
||||||
|
df_lab["値"].notna()
|
||||||
|
].copy()
|
||||||
|
|
||||||
rows.append({
|
# 在校生と編入生を分類する。
|
||||||
"研究室": lab_no,
|
df_regular = df_answers.loc[
|
||||||
"在校生": df_regular.shape[0],
|
df_answers["値"] > 10
|
||||||
"編入生": df_lab.shape[0] - df_regular.shape[0],
|
]
|
||||||
"GP mean": df_regular["値"].mean(),
|
|
||||||
"GP std": df_regular["値"].std(),
|
df_transfer = df_answers.loc[
|
||||||
})
|
df_answers["値"] <= 10
|
||||||
|
]
|
||||||
|
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"研究室": lab_no,
|
||||||
|
"回答者数": df_answers.shape[0],
|
||||||
|
"在校生": df_regular.shape[0],
|
||||||
|
"編入生": df_transfer.shape[0],
|
||||||
|
"GP mean": df_regular["値"].mean(),
|
||||||
|
"GP std": df_regular["値"].std(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return pd.DataFrame(rows).set_index("研究室")
|
return pd.DataFrame(rows).set_index("研究室")
|
||||||
|
|
||||||
|
|
||||||
def print_summary(df, df_r):
|
def print_summary(df, df_r):
|
||||||
print("-" * 52)
|
"""研究室ごとの集計結果を端末に表示する。"""
|
||||||
print("回答者数:", df.shape[0])
|
|
||||||
|
# NaNは回答者0人の研究室を表すため数えない。
|
||||||
|
n_respondents = int(df["値"].notna().sum())
|
||||||
|
|
||||||
|
print("-" * 66)
|
||||||
|
print("回答者数:", n_respondents)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"{'研究室':>6} "
|
f"{'研究室':>6} "
|
||||||
f"{'在校生':>6} "
|
f"{'回答者':>8} "
|
||||||
f"{'編入生':>6} "
|
f"{'在校生':>8} "
|
||||||
|
f"{'編入生':>8} "
|
||||||
f"{'GP mean':>10} "
|
f"{'GP mean':>10} "
|
||||||
f"{'GP std':>10}"
|
f"{'GP std':>10}"
|
||||||
)
|
)
|
||||||
print("-" * 52)
|
|
||||||
|
|
||||||
for n, row in df_r.iterrows():
|
print("-" * 66)
|
||||||
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}"
|
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(
|
print(
|
||||||
f"{n:>6} "
|
f"{lab_no:>6} "
|
||||||
|
f"{int(row['回答者数']):>8} "
|
||||||
f"{int(row['在校生']):>8} "
|
f"{int(row['在校生']):>8} "
|
||||||
f"{int(row['編入生']):>8} "
|
f"{int(row['編入生']):>8} "
|
||||||
f"{mean:>10} "
|
f"{mean:>10} "
|
||||||
f"{std:>10}"
|
f"{std:>10}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print("-" * 66)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_plot_style():
|
||||||
|
"""
|
||||||
|
論文図として使いやすいmatplotlibの描画設定を適用する。
|
||||||
|
|
||||||
|
仕様:
|
||||||
|
- seabornやscienceplotsには依存しない
|
||||||
|
- タイトルは付けず、軸ラベルと目盛だけで情報を伝える
|
||||||
|
- フォントサイズ、線幅、余白、保存時DPIを論文図向けに調整する
|
||||||
|
- カラーマップは既定でtab10を使う
|
||||||
|
"""
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
plt.rcParams.update(
|
||||||
|
{
|
||||||
|
"figure.dpi": 120,
|
||||||
|
"savefig.dpi": 300,
|
||||||
|
"savefig.bbox": "tight",
|
||||||
|
"font.family": "sans-serif",
|
||||||
|
"font.sans-serif": [
|
||||||
|
"Arial",
|
||||||
|
"Helvetica",
|
||||||
|
"DejaVu Sans",
|
||||||
|
],
|
||||||
|
"font.size": 11,
|
||||||
|
"axes.labelsize": 12,
|
||||||
|
"axes.linewidth": 1.0,
|
||||||
|
"axes.spines.top": False,
|
||||||
|
"axes.spines.right": False,
|
||||||
|
"xtick.labelsize": 10,
|
||||||
|
"ytick.labelsize": 10,
|
||||||
|
"xtick.direction": "out",
|
||||||
|
"ytick.direction": "out",
|
||||||
|
"xtick.major.size": 4,
|
||||||
|
"ytick.major.size": 4,
|
||||||
|
"xtick.major.width": 1.0,
|
||||||
|
"ytick.major.width": 1.0,
|
||||||
|
"legend.frameon": False,
|
||||||
|
"pdf.fonttype": 42,
|
||||||
|
"ps.fonttype": 42,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_plot_data(data, lower=10, upper=None):
|
||||||
|
"""
|
||||||
|
GPの描画対象データを抽出する。
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data : pandas.DataFrame
|
||||||
|
「研究室番号」と「値」列を持つデータフレーム。
|
||||||
|
lower : float, default 10
|
||||||
|
この値より大きい回答を描画対象にする。
|
||||||
|
既定値では編入生と回答者なしのNaNを除く。
|
||||||
|
upper : float or None, default None
|
||||||
|
指定した場合、この値未満の回答だけを描画対象にする。
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
pandas.DataFrame
|
||||||
|
フィルタ後のデータ。元データは変更しない。
|
||||||
|
"""
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
if upper is not None:
|
||||||
|
df = df.loc[
|
||||||
|
df["値"] < upper
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
raise ValueError(
|
||||||
|
"描画対象のデータがありません。"
|
||||||
|
"lower/upperを確認してください。"
|
||||||
|
)
|
||||||
|
|
||||||
|
# matplotlib/numpyで扱いやすい通常のfloat型に変換する。
|
||||||
|
df["値"] = df["値"].astype(float)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def make_histogram_bins(values, bin_width):
|
||||||
|
"""
|
||||||
|
GPヒストグラム用のビン境界を作成する。
|
||||||
|
|
||||||
|
仕様:
|
||||||
|
- 最小値側はbin_widthの倍数に切り下げる
|
||||||
|
- 最大値側はbin_widthの倍数に切り上げる
|
||||||
|
- 最大値が最後のビンに入るようにする
|
||||||
|
- bin_widthは正の数でなければならない
|
||||||
|
"""
|
||||||
|
|
||||||
|
if bin_width <= 0:
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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.set_axisbelow(True)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_lab_boxplot(ax, df, labs, color, rng):
|
||||||
|
"""
|
||||||
|
研究室別GP分布を箱ひげ図と実測点で描画する。
|
||||||
|
|
||||||
|
仕様:
|
||||||
|
- 箱ひげ図は外れ値を非表示にする
|
||||||
|
- 実測点を重ねて全回答の分布を見せる
|
||||||
|
- 実測点には固定乱数のjitterを与える
|
||||||
|
- x軸のカテゴリ順はlabsの順序に従う
|
||||||
|
"""
|
||||||
|
|
||||||
|
positions = np.arange(
|
||||||
|
1,
|
||||||
|
len(labs) + 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
grouped_values = [
|
||||||
|
df.loc[
|
||||||
|
df["研究室番号"] == lab,
|
||||||
|
"値",
|
||||||
|
].to_numpy(dtype=float)
|
||||||
|
for lab in labs
|
||||||
|
]
|
||||||
|
|
||||||
|
box = ax.boxplot(
|
||||||
|
grouped_values,
|
||||||
|
positions=positions,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.scatter(
|
||||||
|
np.full(len(values), x_position) + jitter,
|
||||||
|
values,
|
||||||
|
s=18,
|
||||||
|
color="black",
|
||||||
|
alpha=0.45,
|
||||||
|
linewidths=0,
|
||||||
|
zorder=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.set_xlim(
|
||||||
|
0.4,
|
||||||
|
len(labs) + 0.6,
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.set_xticks(positions)
|
||||||
|
|
||||||
|
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,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
ヒストグラム上に対象研究室のGPを縦線と数値で示す。
|
||||||
|
|
||||||
|
仕様:
|
||||||
|
- 同じ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,
|
||||||
|
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(),
|
||||||
|
horizontalalignment="center",
|
||||||
|
verticalalignment="top",
|
||||||
|
rotation=90,
|
||||||
|
color=color,
|
||||||
|
fontsize=9,
|
||||||
|
bbox={
|
||||||
|
"facecolor": "white",
|
||||||
|
"edgecolor": "none",
|
||||||
|
"alpha": 0.75,
|
||||||
|
"pad": 1.2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_gp_histogram(
|
||||||
|
ax,
|
||||||
|
df,
|
||||||
|
target_values,
|
||||||
|
bin_width,
|
||||||
|
color,
|
||||||
|
target_color,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
全研究室のGPヒストグラムを描画する。
|
||||||
|
|
||||||
|
仕様:
|
||||||
|
- bin_widthごとの頻度を表示する
|
||||||
|
- 対象研究室のGPは縦線で示す
|
||||||
|
- 研究室別箱ひげ図と同じ主色を使う
|
||||||
|
"""
|
||||||
|
|
||||||
|
bins = make_histogram_bins(
|
||||||
|
df["値"],
|
||||||
|
bin_width,
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.hist(
|
||||||
|
df["値"],
|
||||||
|
bins=bins,
|
||||||
|
color=color,
|
||||||
|
alpha=0.62,
|
||||||
|
edgecolor="black",
|
||||||
|
linewidth=0.8,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(target_values) > 0:
|
||||||
|
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=None,
|
||||||
|
bin_width=10,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
研究室配属希望調査のGP分布を描画する。
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data : pandas.DataFrame
|
||||||
|
「研究室番号」と「値」列を持つデータフレーム。
|
||||||
|
lower : float, default 10
|
||||||
|
値がlowerより大きい回答だけを描く。
|
||||||
|
upper : float or None, default None
|
||||||
|
GPの上限。Noneの場合は上限を設けない。
|
||||||
|
target_lab : int, default None
|
||||||
|
GP値を縦線表示する研究室番号。
|
||||||
|
bin_width : float, default 10
|
||||||
|
ヒストグラムのビン幅。
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
生成したFigureと2個のAxes。
|
||||||
|
|
||||||
|
仕様:
|
||||||
|
- matplotlibとpandas/numpyのみで描画する
|
||||||
|
- 図タイトルは付けない
|
||||||
|
- 上段に研究室別箱ひげ図と個別点を描く
|
||||||
|
- 下段に全体GPヒストグラムを描く
|
||||||
|
- 編入生および回答者なしのNaNは描画しない
|
||||||
|
"""
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
df = filter_plot_data(
|
||||||
|
data.copy(),
|
||||||
|
lower=lower,
|
||||||
|
upper=upper,
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
fig, axs = plt.subplots(
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
figsize=(8.0, 6.2),
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
|
||||||
|
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():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Google Forms のコピペ結果から研究室別GPを集計する"
|
description=(
|
||||||
|
"Google Formsのコピペ結果から"
|
||||||
|
"研究室別GPを集計する"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
parser.add_argument("googleform_str", help="Google Forms からコピーしたテキストファイル")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-o", "--output",
|
"googleform_str",
|
||||||
help="Excel 出力ファイル名。指定しない場合は出力しない"
|
help=(
|
||||||
|
"Google Formsからコピーした"
|
||||||
|
"テキストファイル"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output",
|
||||||
|
help=(
|
||||||
|
"Excel出力ファイル名。"
|
||||||
|
"指定しない場合は出力しない"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--plot",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"各種plotを作成する。"
|
||||||
|
"指定しない場合は作成しない"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
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()
|
text = f.read()
|
||||||
|
|
||||||
df = parse_google_form_results(text)
|
df = parse_google_form_results(text)
|
||||||
df_r = summarize_by_lab(df)
|
df_r = summarize_by_lab(df)
|
||||||
|
|
||||||
print_summary(df, df_r)
|
print_summary(
|
||||||
|
df,
|
||||||
|
df_r,
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.plot:
|
||||||
|
fig, axs = plot_data(df)
|
||||||
|
|
||||||
|
plot_filename = make_output_filename(
|
||||||
|
args.googleform_str,
|
||||||
|
".png",
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.savefig(
|
||||||
|
plot_filename,
|
||||||
|
dpi=300,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{plot_filename} was created."
|
||||||
|
)
|
||||||
|
|
||||||
if args.output:
|
if args.output:
|
||||||
with pd.ExcelWriter(args.output) as writer:
|
with pd.ExcelWriter(args.output) as writer:
|
||||||
df.to_excel(writer, sheet_name="raw")
|
df.to_excel(
|
||||||
df_r.to_excel(writer, sheet_name="summary")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user