forked from takahiro/analyze_lab_placement_survey
235 lines
5.9 KiB
Python
Executable File
235 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python
|
|
import argparse
|
|
import re
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
|
|
ZEN_TO_ASCII = str.maketrans("0123456789", "0123456789")
|
|
|
|
|
|
def z2a(s):
|
|
"""全角数字を半角数字に変換する。"""
|
|
return s.translate(ZEN_TO_ASCII)
|
|
|
|
|
|
def extract_values_from_body(body):
|
|
"""
|
|
研究室ごとの回答本文から GPA 値を取り出す。
|
|
|
|
- 「値 カウント」表がある場合は表を優先
|
|
- 表がない場合は 1 行 1 回答として読む
|
|
- 「編入生」は GPA としては読まない
|
|
- 「365(修正済)」のような値にも対応
|
|
"""
|
|
|
|
body = z2a(body)
|
|
|
|
# --------------------------------------------------
|
|
# Case 1: 「値 カウント」表がある場合
|
|
# --------------------------------------------------
|
|
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
|
|
)
|
|
|
|
values = []
|
|
for value_str, count_str in count_rows:
|
|
value = float(value_str)
|
|
count = int(count_str)
|
|
values.extend([value] * count)
|
|
|
|
return values
|
|
|
|
# --------------------------------------------------
|
|
# Case 2: 回答値が1行ずつ並んでいる場合
|
|
# --------------------------------------------------
|
|
values = []
|
|
|
|
for line in body.splitlines():
|
|
line = line.strip()
|
|
|
|
if not line:
|
|
continue
|
|
|
|
if "編入生" in line:
|
|
values.append(0.0)
|
|
continue
|
|
|
|
if "この質問にはまだ回答がありません" in line:
|
|
continue
|
|
|
|
# 例: 365(修正済)
|
|
m = re.match(r'^(-?\d+(?:\.\d+)?)', line)
|
|
if m:
|
|
values.append(float(m.group(1)))
|
|
|
|
return values
|
|
|
|
|
|
def parse_google_form_results(text):
|
|
"""
|
|
Google Forms のアンケート結果をコピペしたテキストから、
|
|
|
|
研究室番号 | 値
|
|
|
|
の DataFrame を生成する。
|
|
"""
|
|
|
|
text = z2a(text)
|
|
|
|
# 「研究室第1希望」などの集計部分を除外し、
|
|
# 最初の個別研究室ブロックから開始する。
|
|
#
|
|
# 以下の両方に対応:
|
|
#
|
|
# 研究室1
|
|
# GP7 件の回答
|
|
#
|
|
# 研究室1
|
|
# 7 件の回答
|
|
m = re.search(
|
|
r'^研究室\s*[0-9]+[^\n]*\n'
|
|
r'(?:[^\d\n]*?)'
|
|
r'[0-9]+\s*件の回答',
|
|
text,
|
|
flags=re.MULTILINE
|
|
)
|
|
|
|
if not m:
|
|
raise ValueError("個別研究室ブロックが見つかりません")
|
|
|
|
text = text[m.start():]
|
|
|
|
# 各研究室ブロックを取得
|
|
pattern = re.compile(
|
|
r'^研究室\s*([0-9]+)[^\n]*\n'
|
|
r'(?:[^\d\n]*?)'
|
|
r'([0-9]+)\s*件の回答\s*\n'
|
|
r'(.*?)'
|
|
r'(?='
|
|
r'^研究室\s*[0-9]+[^\n]*\n'
|
|
r'(?:[^\d\n]*?)'
|
|
r'[0-9]+\s*件の回答'
|
|
r'|\Z'
|
|
r')',
|
|
flags=re.MULTILINE | re.DOTALL
|
|
)
|
|
|
|
rows = []
|
|
|
|
for match in pattern.finditer(text):
|
|
lab_no = int(match.group(1))
|
|
n_answers = int(match.group(2))
|
|
body = match.group(3).strip()
|
|
|
|
if n_answers == 0:
|
|
continue
|
|
|
|
values = extract_values_from_body(body)
|
|
|
|
if len(values) != n_answers:
|
|
print(
|
|
f"警告: 研究室{lab_no}: "
|
|
f"回答数={n_answers}, 抽出数={len(values)}"
|
|
)
|
|
|
|
for value in values:
|
|
rows.append({
|
|
"研究室番号": lab_no,
|
|
"値": value
|
|
})
|
|
|
|
df = pd.DataFrame(rows)
|
|
|
|
if not df.empty and (df["値"] % 1 == 0).all():
|
|
df["値"] = df["値"].astype(int)
|
|
|
|
return df
|
|
|
|
|
|
def summarize_by_lab(df):
|
|
"""研究室ごとの在校生数・編入生数・GPA平均・標準偏差を計算する。"""
|
|
|
|
rows = []
|
|
|
|
for lab_no in sorted(df["研究室番号"].unique()):
|
|
df_lab = df.loc[df["研究室番号"] == lab_no].copy()
|
|
|
|
# 編入生は 0 として入れてある。
|
|
df_regular = df_lab.loc[df_lab["値"] > 10]
|
|
|
|
rows.append({
|
|
"研究室": lab_no,
|
|
"在校生": df_regular.shape[0],
|
|
"編入生": df_lab.shape[0] - df_regular.shape[0],
|
|
"GPA mean": df_regular["値"].mean(),
|
|
"GPA std": df_regular["値"].std(),
|
|
})
|
|
|
|
return pd.DataFrame(rows).set_index("研究室")
|
|
|
|
|
|
def print_summary(df, df_r):
|
|
print("-" * 52)
|
|
print("回答者数:", df.shape[0])
|
|
print(
|
|
f"{'研究室':>6} "
|
|
f"{'在校生':>6} "
|
|
f"{'編入生':>6} "
|
|
f"{'GPA mean':>10} "
|
|
f"{'GPA std':>10}"
|
|
)
|
|
print("-" * 52)
|
|
|
|
for n, row in df_r.iterrows():
|
|
mean = "-" if pd.isna(row["GPA mean"]) else f"{row['GPA mean']:.1f}"
|
|
std = "-" if pd.isna(row["GPA std"]) else f"{row['GPA std']:.1f}"
|
|
|
|
print(
|
|
f"{n:>6} "
|
|
f"{int(row['在校生']):>8} "
|
|
f"{int(row['編入生']):>8} "
|
|
f"{mean:>10} "
|
|
f"{std:>10}"
|
|
)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Google Forms のコピペ結果から研究室別 GPA を集計する"
|
|
)
|
|
parser.add_argument("googleform_str", help="Google Forms からコピーしたテキストファイル")
|
|
parser.add_argument(
|
|
"-o", "--output",
|
|
help="Excel 出力ファイル名。指定しない場合は出力しない"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
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)
|
|
|
|
print_summary(df, df_r)
|
|
|
|
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")
|
|
|
|
print(f"{args.output} was created.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|