0人もデータに入れる

This commit is contained in:
2026-07-17 17:03:00 +09:00
parent d42b03071b
commit 8724d25f74
+486 -144
View File
@@ -1,11 +1,16 @@
#!/usr/bin/env python #!/usr/bin/env python
import argparse import argparse
import re import re
import numpy as np 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):
@@ -15,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(修正済)」のような値にも対応
""" """
@@ -28,17 +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*$", table_part, r"^\s*(-?\d+(?:\.\d+)?)\s+(\d+)\s*$",
flags=re.MULTILINE table_part,
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)
@@ -65,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)))
@@ -74,25 +89,31 @@ 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]*?)"
@@ -106,7 +127,7 @@ 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]*?)"
@@ -128,41 +149,105 @@ 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)
if len(values) != n_answers: 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: 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(): 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()
# 在校生と編入生を分類する。
df_regular = df_answers.loc[
df_answers[""] > 10
]
df_transfer = df_answers.loc[
df_answers[""] <= 10
]
rows.append( rows.append(
{ {
"研究室": lab_no, "研究室": lab_no,
"回答者数": df_answers.shape[0],
"在校生": df_regular.shape[0], "在校生": df_regular.shape[0],
"編入生": df_lab.shape[0] - df_regular.shape[0], "編入生": df_transfer.shape[0],
"GP mean": df_regular[""].mean(), "GP mean": df_regular[""].mean(),
"GP std": df_regular[""].std(), "GP std": df_regular[""].std(),
} }
@@ -172,33 +257,59 @@ def summarize_by_lab(df):
def print_summary(df, df_r): 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(): # NaNは回答者0人の研究室を表すため数えない。
mean = "-" if pd.isna(row["GP mean"]) else f"{row['GP mean']:.1f}" n_respondents = int(df[""].notna().sum())
std = "-" if pd.isna(row["GP std"]) else f"{row['GP std']:.1f}"
print("-" * 66)
print("回答者数:", n_respondents)
print( print(
f"{n:>6} " 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"{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(): def apply_plot_style():
""" """
論文図として使いやすい matplotlib の描画設定を適用する。 論文図として使いやすいmatplotlibの描画設定を適用する。
仕様: 仕様:
- seabornscienceplots には依存しない - seabornscienceplotsには依存しない
- タイトルは付けず、軸ラベルと目盛だけで情報を伝える - タイトルは付けず、軸ラベルと目盛だけで情報を伝える
- フォントサイズ、線幅、余白、保存時 DPI を論文図向けに調整する - フォントサイズ、線幅、余白、保存時DPIを論文図向けに調整する
- カラーマップは既定で tab10 を使う - カラーマップは既定でtab10を使う
""" """
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@@ -209,7 +320,11 @@ def apply_plot_style():
"savefig.dpi": 300, "savefig.dpi": 300,
"savefig.bbox": "tight", "savefig.bbox": "tight",
"font.family": "sans-serif", "font.family": "sans-serif",
"font.sans-serif": ["Arial", "Helvetica", "DejaVu Sans"], "font.sans-serif": [
"Arial",
"Helvetica",
"DejaVu Sans",
],
"font.size": 11, "font.size": 11,
"axes.labelsize": 12, "axes.labelsize": 12,
"axes.linewidth": 1.0, "axes.linewidth": 1.0,
@@ -232,14 +347,15 @@ def apply_plot_style():
def filter_plot_data(data, lower=10, upper=None): def filter_plot_data(data, lower=10, upper=None):
""" """
GP の描画対象データを抽出する。 GPの描画対象データを抽出する。
Parameters Parameters
---------- ----------
data : pandas.DataFrame data : pandas.DataFrame
「研究室番号」と「値」列を持つデータフレーム。 「研究室番号」と「値」列を持つデータフレーム。
lower : float, default 10 lower : float, default 10
この値より大きい回答を描画対象にする。既定値では編入生を除く。 この値より大きい回答を描画対象にする。
既定値では編入生と回答者なしのNaNを除く。
upper : float or None, default None upper : float or None, default None
指定した場合、この値未満の回答だけを描画対象にする。 指定した場合、この値未満の回答だけを描画対象にする。
@@ -247,46 +363,76 @@ def filter_plot_data(data, lower=10, upper=None):
------- -------
pandas.DataFrame pandas.DataFrame
フィルタ後のデータ。元データは変更しない。 フィルタ後のデータ。元データは変更しない。
Raises
------
KeyError
必須列が存在しない場合。
ValueError
フィルタ後に描画対象データが残らない場合。
""" """
required_columns = {"研究室番号", ""} required_columns = {
missing_columns = required_columns - set(data.columns) "研究室番号",
if missing_columns: "",
raise KeyError(f"必須列がありません: {', '.join(sorted(missing_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: if upper is not None:
df = df.loc[df[""] < upper].copy() df = df.loc[
df[""] < upper
].copy()
if df.empty: if df.empty:
raise ValueError("描画対象のデータがありません。lower/upper を確認してください。") raise ValueError(
"描画対象のデータがありません。"
"lower/upperを確認してください。"
)
# matplotlib/numpyで扱いやすい通常のfloat型に変換する。
df[""] = df[""].astype(float)
return df return df
def make_histogram_bins(values, bin_width): 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: if bin_width <= 0:
raise ValueError("bin_width は正の数を指定してください。") raise ValueError(
"bin_widthは正の数を指定してください。"
)
min_edge = np.floor(values.min() / bin_width) * bin_width min_edge = (
max_edge = np.ceil(values.max() / bin_width) * bin_width + bin_width np.floor(values.min() / bin_width)
return np.arange(min_edge, max_edge, 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"): 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["top"].set_visible(False)
ax.spines["right"].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) ax.set_axisbelow(True)
def draw_lab_boxplot(ax, df, labs, color, rng): 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 = [ 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( box = ax.boxplot(
@@ -327,17 +493,38 @@ def draw_lab_boxplot(ax, df, labs, color, rng):
widths=0.55, widths=0.55,
patch_artist=True, patch_artist=True,
showfliers=False, showfliers=False,
medianprops={"color": "black", "linewidth": 1.2}, medianprops={
boxprops={"facecolor": color, "edgecolor": "black", "linewidth": 1.0}, "color": "black",
whiskerprops={"color": "black", "linewidth": 1.0}, "linewidth": 1.2,
capprops={"color": "black", "linewidth": 1.0}, },
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"]: for patch in box["boxes"]:
patch.set_alpha(0.55) patch.set_alpha(0.55)
for x_position, values in zip(positions, grouped_values): for x_position, values in zip(
jitter = rng.uniform(-0.16, 0.16, size=len(values)) positions,
grouped_values,
):
jitter = rng.uniform(
-0.16,
0.16,
size=len(values),
)
ax.scatter( ax.scatter(
np.full(len(values), x_position) + jitter, np.full(len(values), x_position) + jitter,
values, values,
@@ -348,53 +535,98 @@ def draw_lab_boxplot(ax, df, labs, color, rng):
zorder=3, 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_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_xlabel("Laboratory")
ax.set_ylabel("GP") 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() unique_values = (
for i, (value, count) in enumerate(unique_values.items()): pd.Series(target_values)
ax.axvline(value, color=color, lw=1.3, alpha=0.85, linestyle="--") .value_counts()
label = f"{value:g}" if count == 1 else f"{value:g} x{count}" .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( ax.text(
value, value,
0.96 - 0.11 * (i % 3), 0.96 - 0.11 * (i % 3),
label, label,
transform=ax.get_xaxis_transform(), transform=ax.get_xaxis_transform(),
ha="center", horizontalalignment="center",
va="top", verticalalignment="top",
rotation=90, rotation=90,
color=color, color=color,
fontsize=9, fontsize=9,
bbox={"facecolor": "white", "edgecolor": "none", bbox={
"alpha": 0.75, "pad": 1.2}, "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 ごとの頻度を表示する - bin_widthごとの頻度を表示する
- 対象研究室の GP は点ではなく縦線で示し、全体分布内の位置を読みやすくする。 - 対象研究室のGPは縦線で示す
- 研究室別箱ひげ図と同じ主色を使い、対象研究室は tab10 の別色で強調する。 - 研究室別箱ひげ図と同じ主色を使
""" """
bins = make_histogram_bins(df[""], bin_width) bins = make_histogram_bins(
df[""],
bin_width,
)
ax.hist( ax.hist(
df[""], df[""],
bins=bins, bins=bins,
@@ -405,74 +637,122 @@ def draw_gp_histogram(ax, df, target_values, bin_width, color, target_color):
) )
if len(target_values) > 0: 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_xlabel("GP")
ax.set_ylabel("Frequency") 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 Parameters
---------- ----------
data : pandas.DataFrame data : pandas.DataFrame
「研究室番号」と「値」列を持つデータフレーム。 「研究室番号」と「値」列を持つデータフレーム。
lower : float, default 10 lower : float, default 10
描画対象に含める GP の下限。値が lower より大きい回答だけを描く。 値がlowerより大きい回答だけを描く。
upper : float or None, default None upper : float or None, default None
描画対象に含める GP の上限。None の場合は上限を設けない。 GPの上限。Noneの場合は上限を設けない。
target_lab : int, default 10 target_lab : int, default None
ヒストグラム上で GP 値を縦線表示する研究室番号。 GP値を縦線表示する研究室番号。
bin_width : float, default 10 bin_width : float, default 10
ヒストグラムのビン幅。 ヒストグラムのビン幅。
Returns Returns
------- -------
tuple[matplotlib.figure.Figure, numpy.ndarray] tuple
生成した Figure と 2 個の Axes。 生成したFigureと2個のAxes。
仕様: 仕様:
- matplotlibpandas/numpy のみで描画する - matplotlibpandas/numpyのみで描画する
- 図タイトルは付けない - 図タイトルは付けない
- 上段に研究室別箱ひげ図と個別点、下段に全体 GP ヒストグラムを描く - 上段に研究室別箱ひげ図と個別点を描く
- 編入生は既定で除外するため、内部値 0 など lower 以下の値は描画しない。 - 下段に全体GPヒストグラムを描く
- 色は基本的に tab10 を使用する。 - 編入生および回答者なしのNaNは描画しない
""" """
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
df = data.copy() df = filter_plot_data(
df = filter_plot_data(df, lower=lower, upper=upper) data.copy(),
labs = sorted(df["研究室番号"].unique()) lower=lower,
target_values = df.loc[df["研究室番号"] == upper=upper,
target_lab, ""].to_numpy(dtype=float) )
labs = sorted(
df["研究室番号"].unique()
)
target_values = df.loc[
df["研究室番号"] == target_lab,
"",
].to_numpy(dtype=float)
apply_plot_style() apply_plot_style()
colors = plt.get_cmap("tab10").colors colors = plt.get_cmap("tab10").colors
rng = np.random.default_rng(20260709)
rng = np.random.default_rng(
20260709
)
fig, axs = plt.subplots( fig, axs = plt.subplots(
2, 2,
1, 1,
figsize=(8.0, 6.2), figsize=(8.0, 6.2),
gridspec_kw={"height_ratios": [1.1, 1]}, gridspec_kw={
"height_ratios": [1.1, 1],
},
constrained_layout=True, constrained_layout=True,
) )
draw_lab_boxplot(axs[0], df, labs, colors[0], rng) draw_lab_boxplot(
draw_gp_histogram(axs[1], df, target_values, axs[0],
bin_width, colors[0], colors[3]) df,
labs,
colors[0],
rng,
)
describe_text = df[""].describe() draw_gp_histogram(
describe_text = describe_text.to_string(float_format=lambda x: f"{x:.1f}") axs[1],
# 下段ヒストグラムの右上に describe() を表示 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( axs[1].text(
0.01, 0.99, # Axes 内の右上 0.01,
0.99,
describe_text, describe_text,
transform=axs[1].transAxes, transform=axs[1].transAxes,
ha="left", va="top", fontsize=8, horizontalalignment="left",
fontfamily="monospace", # 桁を揃える verticalalignment="top",
fontsize=8,
fontfamily="monospace",
bbox={ bbox={
"facecolor": "white", "facecolor": "white",
"edgecolor": "gray", "edgecolor": "gray",
@@ -481,53 +761,115 @@ def plot_data(data, lower=10, upper=None, target_lab=10, bin_width=10):
}, },
zorder=1, zorder=1,
) )
for ax in axs: for ax in axs:
style_axis(ax) style_axis(ax)
plt.show() plt.show()
return fig, axs 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", help="Excel 出力ファイル名。指定しない場合は出力しない" "googleform_str",
help=(
"Google Formsからコピーした"
"テキストファイル"
),
)
parser.add_argument(
"-o",
"--output",
help=(
"Excel出力ファイル名。"
"指定しない場合は出力しない"
),
) )
parser.add_argument( parser.add_argument(
"-p", "-p",
"--plot", "--plot",
action="store_true", action="store_true",
help="各種plotを作成するかどうか。指定しない場合は作成しない。", 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).sort_values(by="GP mean", ascending=False)
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: if args.plot:
fig, axs = plot_data(df) 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: 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__":