commit 5f7698f79484905b88dd7b7566ef026cfe668909 Author: Takahiro OHKUBO Date: Thu Jul 9 07:06:51 2026 +0900 first commit diff --git a/2026-07-09.txt b/2026-07-09.txt new file mode 100644 index 0000000..b2f255b --- /dev/null +++ b/2026-07-09.txt @@ -0,0 +1,145 @@ +研究室配属アンケート +86 件の回答 +第一希希望研究室86 件の回答 +研究室1(バイオプロセス化…研究室2(バイオマテリアル研…研究室3(ソフト材料化学研究室)研究室6(精密有機化学研究室)研究室7(環境調和有機合成研…研究室8(高分子化学研究室)研究室9(セラミックス化学研…研究室10(極限環境材料科学…1/210.5%7%11.6%9.3%15.1%9.3% + +研究室1(バイオプロセス化学研究室) 9 +研究室2(バイオマテリアル研究室) 6 +研究室3(ソフト材料化学研究室) 8 +研究室6(精密有機化学研究室) 3 +研究室7(環境調和有機合成研究室) 4 +研究室8(高分子化学研究室) 13 +研究室9(セラミックス化学研究室) 8 +研究室10(極限環境材料科学研究室) 7 +研究室11(計測化学研究室) 0 +研究室12(触媒化学研究室) 2 +研究室13(表面電気化学研究室) 1 +研究室14(資源反応工学研究室) 5 +研究室15(環境化学研究室) 2 +研究室16(分子構造解析化学) 1 +研究室18(分子集合体化学研究室) 7 +研究室19(環境マネジメント工学研究室) 10 +研究室1(バイオプロセス化学研究室) +9 件の回答 +3003503703803854004010122 (22.2%)2 (22.2%)1 (11.1%)1 (11.1%)1 (11.1%)1 (11.1%)1 (11.1%) +値 カウント +300 2 +350 2 +370 1 +380 1 +385 1 +400 1 +401 1 +研究室2(バイオマテリアル研究室) +6 件の回答 +210 +333 +300 +350 +223 +200 +研究室3(ソフト材料化学研究室) +8 件の回答 +370 +300 +353 +350 +390 +320 +428 +0 +研究室6(精密有機化学研究室) +3 件の回答 +356 +395 +280 +研究室7(環境調和有機合成研究室) +4 件の回答 +226 +210 +392 +379 +研究室8(高分子化学研究室) +13 件の回答 +435 +243.04 +428 +398 +410 +360 +380 +240 +432 +440 +317 +315 +441 +研究室9(セラミックス化学研究室) +8 件の回答 +360 +293 +392 +220 +192 +329 +391 +376 +研究室10(極限環境材料科学研究室) +7 件の回答 +380 +288 +300 +375 +280 +310 +359 +研究室11(計測化学研究室) +0 件の回答 +この質問にはまだ回答がありません。 +研究室12(触媒化学研究室) +2 件の回答 +1 +190.9 +研究室13(表面電気化学研究室) +1 件の回答 +250 +研究室14(資源反応工学研究室) +5 件の回答 +230 +224 +235 +300 +332 +研究室15(環境化学研究室) +2 件の回答 +258 +200 +研究室16(分子構造解析化学) +1 件の回答 +194 +研究室18(分子集合体化学研究室) +7 件の回答 +3103503553643694030121 (14.3%)2 (28.6%)1 (14.3%)1 (14.3%)1 (14.3%)1 (14.3%) +値 カウント +310 1 +350 2 +355 1 +364 1 +369 1 +403 1 +研究室19(環境マネジメント工学研究室) +10 件の回答 +19159.282002502603200121 (10%)1 (10%)1 (10%)2 (20%)2 (20%)2 (20%)1 (10%) +値 カウント +1 1 +9 1 +159.28 1 +200 2 +250 2 +260 2 +320 1 +このコンテンツは Google が作成または承認したものではありません。 - フォームのオーナーに問い合わせる - 利用規約 - プライバシー ポリシー + +このフォームが不審だと思われる場合 報告 +Google フォーム +. diff --git a/Readme.org b/Readme.org new file mode 100644 index 0000000..c89aa31 --- /dev/null +++ b/Readme.org @@ -0,0 +1,185 @@ +#+TITLE: 研究室配属希望調査 解析スクリプト +#+AUTHOR: Takahiro Ohkubo +#+OPTIONS: toc:2 num:nil + +* 概要 + +Google Forms で実施した3年生の研究室配属希望調査について、フォーム結果 +画面からコピーしたテキストを解析し、研究室ごとの希望者数および GPA の +統計量を集計する Python スクリプトです。 + +Google Forms の管理者権限がなくCSVファイルを直接取得できない場合でも、 +結果画面に表示されたテキストをコピーしてファイルに保存することで解析で +きます。 + +主な処理内容は以下のとおりです。 + +- Google Forms の結果テキストから研究室番号を抽出 +- 各研究室の回答値を抽出 +- 在校生と編入生を分類 +- 研究室ごとの希望者数を集計 +- 在校生 GPA の平均値を計算 +- 在校生 GPA の標準偏差を計算 +- 元データを Excel ファイルとして保存 + +* 必要な環境 + +Python 3 が必要です。 +使用する Python パッケージは以下です。 +- numpy +- pandas +- openpyxl +必要に応じて以下のようにインストールします。 + +#+begin_src sh +condat install numpy pandas openpyxl +#+end_src + +* 入力データ + +Google Forms のアンケート結果画面に表示されたテキストをコピーし、テキ +ストファイルとして保存します。 + +例えば、 +#+begin_example +googleform.txt +#+end_example + +というファイル名で保存します。入力データには、以下のような研究室ごとの +回答結果が含まれていることを想定しています。 + +#+begin_example +研究室10(極限環境材料科学研究室) +7 件の回答 +380 +288 +300 +375 +280 +310 +359 +#+end_example + +また、Google Forms の表示形式によっては、 +同じ値が複数回出現する場合に以下のような +「値 / カウント」形式になることがあります。 + +#+begin_example +値 カウント +300 2 +350 2 +370 1 +380 1 +385 1 +400 1 +401 1 +#+end_example + +この場合、カウント数に応じて値を展開します。例えば、 + +#+begin_example +300 2 +350 2 +#+end_example + +は内部的に、 + +#+begin_example +300 +300 +350 +350 +#+end_example + +として処理されます。 + +* 実行方法 +#+begin_src sh +./analyze_lab_assignment_survey.py googleform.txt +#+end_src + +* 出力 +実行すると、研究室ごとの集計結果が標準出力に表示されます。 + +出力例: + +#+begin_example + 研究室 在校生 編入生 GPA mean GPA std +---------------------------------------------------- + 1 9 0 359.6 38.4 + 2 6 0 269.3 66.3 + 3 8 1 358.7 42.7 + 6 3 0 343.7 58.5 + 7 4 0 301.8 97.1 + 8 13 0 372.2 72.3 + 9 8 0 319.1 77.6 + 10 7 0 327.4 42.6 + 12 2 1 190.9 - +#+end_example + + +* 在校生と編入生の判定 + +このスクリプトでは、回答値について + +#+begin_src python +df_["値"] > 10 +#+end_src + +を満たす回答を在校生の GPA データとして扱います。 + +したがって、 + +- 値 > 10 : 在校生の GPA +- 値 <= 10 : 編入生 + +として分類します。 + +* GPA の統計量 + +研究室ごとに、在校生のみを抽出して GPA の平均値と標準偏差を計算します。 + +平均値: + +#+begin_src python +df__["値"].mean() +#+end_src + +標準偏差: + +#+begin_src python +df__["値"].std() +#+end_src + +* 注意事項 + +- 入力テキストは Google Forms の表示形式に依存します。 +- Google Forms 側の HTML やコピー形式が変更された場合、正規表現の修正が必要になる可能性があります。 +- 編入生の判定は「値が10以下」という運用上の仮定に基づいています。 +- GPA が実際に10以下となる別形式のデータを使用する場合は、判定条件を変更してください。 +- 回答値の誤入力は自動修正しません。 +- 研究室番号が存在しても回答数が0件の場合、その研究室は出力から除外されます。 + +* ファイル構成例 + +#+begin_example +. +├── README.org +├── analyze_lab_assignment_survey.py +└── googleform.txt +#+end_example + +実行後: + +#+begin_example +. +├── README.org +├── analyze_lab_assignment_survey.py +├── analyze_lab_assignment_survey.xlsx +└── googleform.txt +#+end_example + +* ライセンス + +研究室内または教育業務での利用を想定しています。 +必要に応じてライセンス条件を追記してください。 diff --git a/analyze_lab_placement_survey.py b/analyze_lab_placement_survey.py new file mode 100755 index 0000000..2c72537 --- /dev/null +++ b/analyze_lab_placement_survey.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python +import numpy as np +import re +import pandas as pd + + +def parse_google_form_results(text): + """ + Google Forms のアンケート結果をコピペしたテキストから、 + + 研究室番号 | 値 + + の DataFrame を生成する。 + + Parameters + ---------- + text : str + Google Forms 結果ページからコピーしたテキスト + + Returns + ------- + pandas.DataFrame + """ + + # 「第一希希望研究室...」の集計部分は除外したいので、 + # 最初の個別研究室ブロックから開始する + m = re.search( + r'^研究室\s*[0-90-9]+.*?\n[0-90-9]+\s*件の回答', + text, + flags=re.MULTILINE + ) + + if not m: + raise ValueError("個別研究室ブロックが見つかりません") + + text = text[m.start():] + + # 各研究室ブロックを取得 + pattern = re.compile( + r'^研究室\s*([0-90-9]+).*?\n' + r'([0-90-9]+)\s*件の回答\s*\n' + r'(.*?)' + r'(?=^研究室\s*[0-90-9]+.*?\n[0-90-9]+\s*件の回答|\Z)', + flags=re.MULTILINE | re.DOTALL + ) + + rows = [] + + for match in pattern.finditer(text): + lab_no_str = match.group(1) + n_answers_str = match.group(2) + body = match.group(3).strip() + + # 全角数字対応 + trans = str.maketrans("0123456789", "0123456789") + + lab_no = int(lab_no_str.translate(trans)) + n_answers = int(n_answers_str.translate(trans)) + + # 回答数 0 はスキップ + if n_answers == 0: + continue + + # -------------------------------------------------- + # Case 1: + # 「値 カウント」表がある場合 + # + # 例: + # 値 カウント + # 300 2 + # 350 2 + # -------------------------------------------------- + if re.search(r'^値\s+カウント\s*$', body, flags=re.MULTILINE): + table_part = re.split( + r'^値\s+カウント\s*$', + body, + maxsplit=1, + flags=re.MULTILINE + )[1] + + 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) + + # -------------------------------------------------- + # Case 2: + # 回答値が1行ずつ並んでいる場合 + # + # 例: + # 210 + # 333 + # 300 + # -------------------------------------------------- + else: + values = [] + + for line in body.splitlines(): + line = line.strip() + + if re.fullmatch(r'-?\d+(?:\.\d+)?', line): + values.append(float(line)) + + # 回答数との整合性チェック + 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: + if (df["値"] % 1 == 0).all(): + df["値"] = df["値"].astype(int) + + return df + + +if __name__ == "__main__": + import argparse + + par = argparse.ArgumentParser(description="test") + par.add_argument('googleform_str') + args = par.parse_args() + + # コピペしたテキストをファイルに保存しておく + with open(args.googleform_str, encoding="utf-8") as f: + text = f.read() + + df = parse_google_form_results(text) + df_r = pd.DataFrame() + for n in np.unique(df["研究室番号"]): + df_ = df.loc[df["研究室番号"] == n, :] + df__ = df_.loc[df_["値"] > 10, :] + ave, std = df__.mean(), df__.std() + df_r.loc[n, "在校生"] = df_.shape[0] + df_r.loc[n, "編入生"] = df_.shape[0] - df__.shape[0] + df_r.loc[n, "GPA mean"] = df__["値"].mean() + df_r.loc[n, "GPA std"] = df__["値"].std() + df_r[["在校生", "編入生"]] = df_r[["在校生", "編入生"]].astype(int) + # 表示用 + df_show = df_r.copy() + df_show.index.name = "研究室" + df_show["GPA mean"] = df_show["GPA mean"].round(1) + df_show["GPA std"] = df_show["GPA std"].round(1) + 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 = 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}") + + # outfile = __file__.replace(".py", ".xlsx") + # df.to_excel(outfile) + # print(f"{outfile} was created.")