libs.commands.graph.personal

libs/commands/graph/personal.py

  1"""
  2libs/commands/graph/personal.py
  3"""
  4
  5import os
  6
  7import matplotlib.font_manager as fm
  8import matplotlib.pyplot as plt
  9import pandas as pd
 10from matplotlib import gridspec
 11
 12import libs.global_value as g
 13from cls.timekit import ExtendedDatetime as ExtDt
 14from libs.data import loader
 15from libs.functions import configuration, message
 16from libs.utils import formatter
 17
 18
 19def plot() -> tuple[int, str]:
 20    """個人成績のグラフを生成する
 21
 22    Returns:
 23        tuple[int,str]:
 24        - int: グラフにプロットしたゲーム数
 25        - str: 検索結果が0件のときのメッセージ or グラフ画像保存パス
 26    """
 27
 28    plt.close()
 29    # データ収集
 30    g.params.update(guest_skip=g.params.get("guest_skip2"))
 31    df = loader.read_data("summary/gamedata.sql")
 32    player = formatter.name_replace(g.params["player_name"], add_mark=True)
 33
 34    if df.empty:
 35        return (0, message.reply(message="no_hits"))
 36
 37    if g.params.get("anonymous"):
 38        mapping_dict = formatter.anonymous_mapping([g.params["player_name"]])
 39        player = next(iter(mapping_dict.values()))
 40
 41    # 最終値(凡例追加用)
 42    point_sum = f"{float(df["point_sum"].iloc[-1]):+.1f}".replace("-", "▲")
 43    point_avg = f"{float(df["point_avg"].iloc[-1]):+.1f}".replace("-", "▲")
 44    rank_avg = f"{float(df["rank_avg"].iloc[-1]):.2f}"
 45
 46    # --- グラフ生成
 47    save_file = os.path.join(
 48        g.cfg.setting.work_dir,
 49        f"{g.params["filename"]}.png" if g.params.get("filename") else "graph.png",
 50    )
 51
 52    configuration.graph_setup(plt, fm)
 53
 54    fig = plt.figure(figsize=(12, 8))
 55
 56    if g.params.get("target_count", 0) == 0:
 57        title_text = f"『{player}』の成績 ({ExtDt(g.params["starttime"]).format("ymdhm")} - {ExtDt(g.params["endtime"]).format("ymdhm")})"
 58    else:
 59        title_text = f"『{player}』の成績 (直近 {len(df)} ゲーム)"
 60
 61    grid = gridspec.GridSpec(nrows=2, ncols=1, height_ratios=[3, 1])
 62    point_ax = fig.add_subplot(grid[0])
 63    rank_ax = fig.add_subplot(grid[1], sharex=point_ax)
 64
 65    # ---
 66    df.filter(items=["point_sum", "point_avg"]).plot.line(
 67        ax=point_ax,
 68        ylabel="ポイント(pt)",
 69        marker="." if len(df) < 50 else None,
 70    )
 71    df.filter(items=["point"]).plot.bar(
 72        ax=point_ax,
 73        color="blue",
 74    )
 75    point_ax.legend(
 76        [f"通算ポイント ({point_sum}pt)", f"平均ポイント ({point_avg}pt)", "獲得ポイント"],
 77        bbox_to_anchor=(1, 1),
 78        loc="upper left",
 79        borderaxespad=0.5,
 80    )
 81    point_ax.axhline(y=0, linewidth=0.5, ls="dashed", color="grey")
 82
 83    # Y軸修正
 84    ylabs = point_ax.get_yticks()[1:-1]
 85    point_ax.set_yticks(ylabs)
 86    point_ax.set_yticklabels(
 87        [str(int(ylab)).replace("-", "▲") for ylab in ylabs]
 88    )
 89
 90    # ---
 91    df.filter(items=["rank", "rank_avg"]).plot.line(
 92        ax=rank_ax,
 93        marker="." if len(df) < 50 else None,
 94        yticks=[1, 2, 3, 4],
 95        ylabel="順位",
 96        xlabel=f"ゲーム終了日時({len(df)} ゲーム)",
 97    )
 98    rank_ax.legend(
 99        ["獲得順位", f"平均順位 ({rank_avg})"],
100        bbox_to_anchor=(1, 1),
101        loc="upper left",
102        borderaxespad=0.5,
103    )
104
105    rank_ax.set_xticks(list(df.index)[::int(len(df) / 25) + 1])
106    rank_ax.set_xticklabels(
107        list(df["playtime"])[::int(len(df) / 25) + 1],
108        rotation=45,
109        ha="right"
110    )
111    rank_ax.axhline(y=2.5, linewidth=0.5, ls="dashed", color="grey")
112    rank_ax.invert_yaxis()
113
114    fig.suptitle(title_text, fontsize=16)
115    fig.tight_layout()
116    plt.savefig(save_file, bbox_inches="tight")
117
118    return (len(df), save_file)
119
120
121def statistics_plot() -> tuple[int, str]:
122    """個人成績の統計グラフを生成する
123
124    Returns:
125        tuple[int,str]:
126        - int: 集計対象のゲーム数
127        - str: 検索結果が0件のときのメッセージ or グラフ画像保存パス
128    """
129
130    plt.close()
131    # データ収集
132    g.params.update(guest_skip=g.params.get("guest_skip2"))
133    df = loader.read_data("summary/details.sql")
134
135    if df.empty:
136        return (0, message.reply(message="no_hits"))
137
138    if g.params.get("individual"):  # 個人成績
139        player = formatter.name_replace(g.params["player_name"], add_mark=True)
140    else:  # チーム成績
141        df = df.rename(columns={"team": "name"})
142        player = g.params["player_name"]
143
144    df = df.filter(items=["playtime", "name", "rpoint", "rank", "point"])
145    df["rpoint"] = df["rpoint"] * 100
146
147    player_df = df.query("name == @player").reset_index(drop=True)
148
149    if player_df.empty:
150        return (0, message.reply(message="no_hits"))
151
152    player_df["sum_point"] = player_df["point"].cumsum()
153
154    # --- グラフ生成
155    save_file = os.path.join(
156        g.cfg.setting.work_dir,
157        f"{g.params["filename"]}.png" if g.params.get("filename") else "graph.png",
158    )
159
160    if g.params.get("anonymous"):
161        mapping_dict = formatter.anonymous_mapping([g.params["player_name"]])
162        player = next(iter(mapping_dict.values()))
163
164    title_text = f"『{player}』の成績 (検索範囲:{message.item_date_range("ymd_o")})"
165
166    rpoint_df = get_data(player_df["rpoint"], g.params["interval"])
167    point_sum_df = get_data(player_df["point"], g.params["interval"])
168    point_df = get_data(player_df["sum_point"], g.params["interval"]).iloc[-1]
169    rank_df = get_data(player_df["rank"], g.params["interval"])
170    total_index = "全区間"
171
172    rpoint_stats = {
173        "ゲーム数": rpoint_df.count().astype("int"),
174        "平均値(x)": rpoint_df.mean().round(1),
175        "最小値": rpoint_df.min().astype("int"),
176        "第一四分位数": rpoint_df.quantile(0.25).astype("int"),
177        "中央値(|)": rpoint_df.median().astype("int"),
178        "第三四分位数": rpoint_df.quantile(0.75).astype("int"),
179        "最大値": rpoint_df.max().astype("int"),
180    }
181
182    stats_df = pd.DataFrame(rpoint_stats)
183    stats_df.loc[total_index] = pd.Series(
184        {
185            "ゲーム数": int(player_df["rpoint"].count()),
186            "平均値(x)": float(round(player_df["rpoint"].mean(), 1)),
187            "最小値": int(player_df["rpoint"].min()),
188            "第一四分位数": int(player_df["rpoint"].quantile(0.25)),
189            "中央値(|)": int(player_df["rpoint"].median()),
190            "第三四分位数": int(player_df["rpoint"].quantile(0.75)),
191            "最大値": int(player_df["rpoint"].max()),
192        }
193    )
194    stats_df = stats_df.apply(lambda col: col.map(lambda x: f"{int(x)}" if isinstance(x, int) else f"{x:.1f}"))
195
196    count_stats = {
197        "ゲーム数": rank_df.count().astype("int"),
198        "1位": rank_df[rank_df == 1].count().astype("int"),
199        "2位": rank_df[rank_df == 2].count().astype("int"),
200        "3位": rank_df[rank_df == 3].count().astype("int"),
201        "4位": rank_df[rank_df == 4].count().astype("int"),
202        "1位(%)": ((rank_df[rank_df == 1].count()) / rank_df.count() * 100).round(2),
203        "2位(%)": ((rank_df[rank_df == 2].count()) / rank_df.count() * 100).round(2),
204        "3位(%)": ((rank_df[rank_df == 3].count()) / rank_df.count() * 100).round(2),
205        "4位(%)": ((rank_df[rank_df == 4].count()) / rank_df.count() * 100).round(2),
206        "平均順位": rank_df.mean().round(2),
207        "区間ポイント": point_sum_df.sum().round(1),
208        "区間平均": point_sum_df.mean().round(1),
209        "通算ポイント": point_df.round(1),
210    }
211    count_df = pd.DataFrame(count_stats)
212
213    count_df.loc[total_index] = pd.Series(
214        {
215            "ゲーム数": int(count_df["ゲーム数"].sum()),
216            "1位": int(count_df["1位"].sum()),
217            "2位": int(count_df["2位"].sum()),
218            "3位": int(count_df["3位"].sum()),
219            "4位": int(count_df["4位"].sum()),
220            "1位(%)": float(round((count_df["1位"].sum() / count_df["ゲーム数"].sum() * 100), 2)),
221            "2位(%)": float(round((count_df["2位"].sum() / count_df["ゲーム数"].sum() * 100), 2)),
222            "3位(%)": float(round((count_df["3位"].sum() / count_df["ゲーム数"].sum() * 100), 2)),
223            "4位(%)": float(round((count_df["4位"].sum() / count_df["ゲーム数"].sum() * 100), 2)),
224            "平均順位": float(round(player_df["rank"].mean(), 2)),
225            "区間ポイント": float(round(player_df["point"].sum(), 1)),
226            "区間平均": float(round(player_df["point"].mean(), 1)),
227        }
228    )
229    # テーブル用データ
230    rank_table = pd.DataFrame()
231    rank_table["ゲーム数"] = count_df["ゲーム数"].astype("int")
232    rank_table["1位"] = count_df.apply(lambda row: f"{row["1位(%)"]:.2f}% ({row["1位"]:.0f})", axis=1)
233    rank_table["2位"] = count_df.apply(lambda row: f"{row["2位(%)"]:.2f}% ({row["2位"]:.0f})", axis=1)
234    rank_table["3位"] = count_df.apply(lambda row: f"{row["3位(%)"]:.2f}% ({row["3位"]:.0f})", axis=1)
235    rank_table["4位"] = count_df.apply(lambda row: f"{row["4位(%)"]:.2f}% ({row["4位"]:.0f})", axis=1)
236    rank_table["平均順位"] = count_df.apply(lambda row: f"{row["平均順位"]:.2f}", axis=1)
237
238    # グラフ設定
239    configuration.graph_setup(plt, fm)
240    fig = plt.figure(figsize=(20, 10))
241    fig.suptitle(title_text, size=20, weight="bold")
242    gs = gridspec.GridSpec(figure=fig, nrows=3, ncols=2)
243
244    ax_point1 = fig.add_subplot(gs[0, 0])
245    ax_point2 = fig.add_subplot(gs[0, 1])
246    ax_rank1 = fig.add_subplot(gs[1, 0])
247    ax_rank2 = fig.add_subplot(gs[1, 1])
248    ax_rpoint1 = fig.add_subplot(gs[2, 0])
249    ax_rpoint2 = fig.add_subplot(gs[2, 1])
250
251    plt.subplots_adjust(wspace=0.22, hspace=0.18)
252
253    # ポイントデータ
254    subplot_point(point_df, ax_point1)
255    subplot_table(count_df.filter(items=["ゲーム数", "区間ポイント", "区間平均", "通算ポイント"]), ax_point2)
256
257    # 順位データ
258    subplot_rank(count_df, ax_rank1, total_index)
259    subplot_table(rank_table, ax_rank2)
260
261    # 素点データ
262    subplot_box(rpoint_df, ax_rpoint1)
263    subplot_table(stats_df, ax_rpoint2)
264
265    plt.savefig(save_file, bbox_inches="tight")
266    plt.close()
267    return (len(player_df), save_file)
268
269
270def get_data(df: pd.Series, interval: int) -> pd.DataFrame:
271    """データフレームを指定範囲で分割する
272
273    Args:
274        df (pd.Series): 分割するデータ
275        interval (int): 1ブロックに収めるデータ数
276
277    Returns:
278        pd.DataFrame: 分割されたデータ
279    """
280
281    # interval単位で分割
282    rpoint_data: dict = {}
283
284    fraction = 0 if not len(df) % interval else interval - len(df) % interval  # 端数
285    if fraction:
286        df = pd.concat([pd.Series([None] * fraction), df], ignore_index=True)
287
288    for x in range(int(len(df) / interval)):
289        s = len(df) % interval + interval * x
290        e = s + interval
291        rpoint_data[f"{max(1, s + 1 - fraction):3d}G - {(e - fraction):3d}G"] = df.iloc[s:e].tolist()
292
293    return pd.DataFrame(rpoint_data)
294
295
296def subplot_box(df: pd.DataFrame, ax: plt.Axes) -> None:
297    """箱ひげ図を生成する
298
299    Args:
300        df (pd.DataFrame): プロットデータ
301        ax (plt.Axes): プロット先オブジェクト
302    """
303
304    p = [x + 1 for x in range(len(df.columns))]
305    df.plot(
306        ax=ax,
307        kind="box",
308        title="素点分布",
309        showmeans=True,
310        meanprops={"marker": "x", "markeredgecolor": "b", "markerfacecolor": "b", "ms": 3},
311        flierprops={"marker": ".", "markeredgecolor": "r"},
312        ylabel="素点(点)",
313        sharex=True,
314    )
315    ax.axhline(y=25000, linewidth=0.5, ls="dashed", color="grey")
316    ax.set_xticks(p)
317    ax.set_xticklabels(df.columns, rotation=45, ha="right")
318
319    # Y軸修正
320    ylabs = ax.get_yticks()[1:-1]
321    ax.set_yticks(ylabs)
322    ax.set_yticklabels(
323        [str(int(ylab)).replace("-", "▲") for ylab in ylabs]
324    )
325
326
327def subplot_table(df: pd.DataFrame, ax: plt.Axes) -> None:
328    """テーブルを生成する
329
330    Args:
331        df (pd.DataFrame): プロットデータ
332        ax (plt.Axes): プロット先オブジェクト
333    """
334
335    # 有効桁数の調整
336    for col in df.columns:
337        match col:
338            case "ゲーム数":
339                df[col] = df[col].apply(lambda x: int(float(x)))
340            case "区間ポイント" | "区間平均" | "通算ポイント":
341                df[col] = df[col].apply(lambda x: f"{float(x):+.1f}")
342            case "平均順位":
343                df[col] = df[col].apply(lambda x: f"{float(x):.2f}")
344            case "平均値(x)":
345                df[col] = df[col].apply(lambda x: f"{float(x):.1f}")
346            case "最小値" | "第一四分位数" | "中央値(|)" | "第三四分位数" | "最大値":
347                df[col] = df[col].apply(lambda x: int(float(x)))
348
349    df = df.apply(lambda col: col.map(lambda x: str(x).replace("-", "▲")))
350    df.replace("+nan", "-----", inplace=True)
351
352    table = ax.table(
353        cellText=df.values.tolist(),
354        colLabels=df.columns.tolist(),
355        rowLabels=df.index.tolist(),
356        cellLoc="center",
357        loc="center",
358    )
359    table.auto_set_font_size(False)
360    ax.axis("off")
361
362
363def subplot_point(df: pd.Series, ax: plt.Axes) -> None:
364    """ポイントデータ
365
366    Args:
367        df (pd.Series): プロットデータ
368        ax (plt.Axes): プロット先オブジェクト
369    """
370
371    df.plot(  # レイアウト調整用ダミー
372        ax=ax,
373        kind="bar",
374        alpha=0,
375    )
376    df.plot(
377        ax=ax,
378        kind="line",
379        title="ポイント推移",
380        ylabel="通算ポイント(pt)",
381        marker="o",
382        color="b",
383    )
384    # Y軸修正
385    ylabs = ax.get_yticks()[1:-1]
386    ax.set_yticks(ylabs)
387    ax.set_yticklabels(
388        [str(int(ylab)).replace("-", "▲") for ylab in ylabs]
389    )
390
391
392def subplot_rank(df: pd.DataFrame, ax: plt.Axes, total_index: str) -> None:
393    """順位データ
394
395    Args:
396        df (pd.DataFrame): プロットデータ
397        ax (plt.Axes): プロット先オブジェクト
398        total_index (str): 合計値格納index
399    """
400
401    ax_rank_avg = ax.twinx()
402    df.filter(items=["平均順位"]).drop(index=total_index).plot(
403        ax=ax_rank_avg,
404        kind="line",
405        ylabel="平均順位",
406        yticks=[1, 2, 3, 4],
407        ylim=[0.85, 4.15],
408        marker="o",
409        color="b",
410        legend=False,
411        grid=False,
412    )
413    ax_rank_avg.invert_yaxis()
414    ax_rank_avg.axhline(y=2.5, linewidth=0.5, ls="dashed", color="grey")
415
416    filter_items = ["1位(%)", "2位(%)", "3位(%)", "4位(%)"]
417    df.filter(items=filter_items).drop(index=total_index).plot(
418        ax=ax,
419        kind="bar",
420        title="獲得順位",
421        ylabel="獲得順位(%)",
422        colormap="Set2",
423        stacked=True,
424        rot=90,
425        ylim=[-5, 105],
426    )
427    h1, l1 = ax.get_legend_handles_labels()
428    h2, l2 = ax_rank_avg.get_legend_handles_labels()
429    ax.legend(
430        h1 + h2,
431        l1 + l2,
432        bbox_to_anchor=(0.5, 0),
433        loc="lower center",
434        ncol=5,
435    )
def plot() -> tuple[int, str]:
 20def plot() -> tuple[int, str]:
 21    """個人成績のグラフを生成する
 22
 23    Returns:
 24        tuple[int,str]:
 25        - int: グラフにプロットしたゲーム数
 26        - str: 検索結果が0件のときのメッセージ or グラフ画像保存パス
 27    """
 28
 29    plt.close()
 30    # データ収集
 31    g.params.update(guest_skip=g.params.get("guest_skip2"))
 32    df = loader.read_data("summary/gamedata.sql")
 33    player = formatter.name_replace(g.params["player_name"], add_mark=True)
 34
 35    if df.empty:
 36        return (0, message.reply(message="no_hits"))
 37
 38    if g.params.get("anonymous"):
 39        mapping_dict = formatter.anonymous_mapping([g.params["player_name"]])
 40        player = next(iter(mapping_dict.values()))
 41
 42    # 最終値(凡例追加用)
 43    point_sum = f"{float(df["point_sum"].iloc[-1]):+.1f}".replace("-", "▲")
 44    point_avg = f"{float(df["point_avg"].iloc[-1]):+.1f}".replace("-", "▲")
 45    rank_avg = f"{float(df["rank_avg"].iloc[-1]):.2f}"
 46
 47    # --- グラフ生成
 48    save_file = os.path.join(
 49        g.cfg.setting.work_dir,
 50        f"{g.params["filename"]}.png" if g.params.get("filename") else "graph.png",
 51    )
 52
 53    configuration.graph_setup(plt, fm)
 54
 55    fig = plt.figure(figsize=(12, 8))
 56
 57    if g.params.get("target_count", 0) == 0:
 58        title_text = f"『{player}』の成績 ({ExtDt(g.params["starttime"]).format("ymdhm")} - {ExtDt(g.params["endtime"]).format("ymdhm")})"
 59    else:
 60        title_text = f"『{player}』の成績 (直近 {len(df)} ゲーム)"
 61
 62    grid = gridspec.GridSpec(nrows=2, ncols=1, height_ratios=[3, 1])
 63    point_ax = fig.add_subplot(grid[0])
 64    rank_ax = fig.add_subplot(grid[1], sharex=point_ax)
 65
 66    # ---
 67    df.filter(items=["point_sum", "point_avg"]).plot.line(
 68        ax=point_ax,
 69        ylabel="ポイント(pt)",
 70        marker="." if len(df) < 50 else None,
 71    )
 72    df.filter(items=["point"]).plot.bar(
 73        ax=point_ax,
 74        color="blue",
 75    )
 76    point_ax.legend(
 77        [f"通算ポイント ({point_sum}pt)", f"平均ポイント ({point_avg}pt)", "獲得ポイント"],
 78        bbox_to_anchor=(1, 1),
 79        loc="upper left",
 80        borderaxespad=0.5,
 81    )
 82    point_ax.axhline(y=0, linewidth=0.5, ls="dashed", color="grey")
 83
 84    # Y軸修正
 85    ylabs = point_ax.get_yticks()[1:-1]
 86    point_ax.set_yticks(ylabs)
 87    point_ax.set_yticklabels(
 88        [str(int(ylab)).replace("-", "▲") for ylab in ylabs]
 89    )
 90
 91    # ---
 92    df.filter(items=["rank", "rank_avg"]).plot.line(
 93        ax=rank_ax,
 94        marker="." if len(df) < 50 else None,
 95        yticks=[1, 2, 3, 4],
 96        ylabel="順位",
 97        xlabel=f"ゲーム終了日時({len(df)} ゲーム)",
 98    )
 99    rank_ax.legend(
100        ["獲得順位", f"平均順位 ({rank_avg})"],
101        bbox_to_anchor=(1, 1),
102        loc="upper left",
103        borderaxespad=0.5,
104    )
105
106    rank_ax.set_xticks(list(df.index)[::int(len(df) / 25) + 1])
107    rank_ax.set_xticklabels(
108        list(df["playtime"])[::int(len(df) / 25) + 1],
109        rotation=45,
110        ha="right"
111    )
112    rank_ax.axhline(y=2.5, linewidth=0.5, ls="dashed", color="grey")
113    rank_ax.invert_yaxis()
114
115    fig.suptitle(title_text, fontsize=16)
116    fig.tight_layout()
117    plt.savefig(save_file, bbox_inches="tight")
118
119    return (len(df), save_file)

個人成績のグラフを生成する

Returns:

tuple[int,str]:

  • int: グラフにプロットしたゲーム数
  • str: 検索結果が0件のときのメッセージ or グラフ画像保存パス
def statistics_plot() -> tuple[int, str]:
122def statistics_plot() -> tuple[int, str]:
123    """個人成績の統計グラフを生成する
124
125    Returns:
126        tuple[int,str]:
127        - int: 集計対象のゲーム数
128        - str: 検索結果が0件のときのメッセージ or グラフ画像保存パス
129    """
130
131    plt.close()
132    # データ収集
133    g.params.update(guest_skip=g.params.get("guest_skip2"))
134    df = loader.read_data("summary/details.sql")
135
136    if df.empty:
137        return (0, message.reply(message="no_hits"))
138
139    if g.params.get("individual"):  # 個人成績
140        player = formatter.name_replace(g.params["player_name"], add_mark=True)
141    else:  # チーム成績
142        df = df.rename(columns={"team": "name"})
143        player = g.params["player_name"]
144
145    df = df.filter(items=["playtime", "name", "rpoint", "rank", "point"])
146    df["rpoint"] = df["rpoint"] * 100
147
148    player_df = df.query("name == @player").reset_index(drop=True)
149
150    if player_df.empty:
151        return (0, message.reply(message="no_hits"))
152
153    player_df["sum_point"] = player_df["point"].cumsum()
154
155    # --- グラフ生成
156    save_file = os.path.join(
157        g.cfg.setting.work_dir,
158        f"{g.params["filename"]}.png" if g.params.get("filename") else "graph.png",
159    )
160
161    if g.params.get("anonymous"):
162        mapping_dict = formatter.anonymous_mapping([g.params["player_name"]])
163        player = next(iter(mapping_dict.values()))
164
165    title_text = f"『{player}』の成績 (検索範囲:{message.item_date_range("ymd_o")})"
166
167    rpoint_df = get_data(player_df["rpoint"], g.params["interval"])
168    point_sum_df = get_data(player_df["point"], g.params["interval"])
169    point_df = get_data(player_df["sum_point"], g.params["interval"]).iloc[-1]
170    rank_df = get_data(player_df["rank"], g.params["interval"])
171    total_index = "全区間"
172
173    rpoint_stats = {
174        "ゲーム数": rpoint_df.count().astype("int"),
175        "平均値(x)": rpoint_df.mean().round(1),
176        "最小値": rpoint_df.min().astype("int"),
177        "第一四分位数": rpoint_df.quantile(0.25).astype("int"),
178        "中央値(|)": rpoint_df.median().astype("int"),
179        "第三四分位数": rpoint_df.quantile(0.75).astype("int"),
180        "最大値": rpoint_df.max().astype("int"),
181    }
182
183    stats_df = pd.DataFrame(rpoint_stats)
184    stats_df.loc[total_index] = pd.Series(
185        {
186            "ゲーム数": int(player_df["rpoint"].count()),
187            "平均値(x)": float(round(player_df["rpoint"].mean(), 1)),
188            "最小値": int(player_df["rpoint"].min()),
189            "第一四分位数": int(player_df["rpoint"].quantile(0.25)),
190            "中央値(|)": int(player_df["rpoint"].median()),
191            "第三四分位数": int(player_df["rpoint"].quantile(0.75)),
192            "最大値": int(player_df["rpoint"].max()),
193        }
194    )
195    stats_df = stats_df.apply(lambda col: col.map(lambda x: f"{int(x)}" if isinstance(x, int) else f"{x:.1f}"))
196
197    count_stats = {
198        "ゲーム数": rank_df.count().astype("int"),
199        "1位": rank_df[rank_df == 1].count().astype("int"),
200        "2位": rank_df[rank_df == 2].count().astype("int"),
201        "3位": rank_df[rank_df == 3].count().astype("int"),
202        "4位": rank_df[rank_df == 4].count().astype("int"),
203        "1位(%)": ((rank_df[rank_df == 1].count()) / rank_df.count() * 100).round(2),
204        "2位(%)": ((rank_df[rank_df == 2].count()) / rank_df.count() * 100).round(2),
205        "3位(%)": ((rank_df[rank_df == 3].count()) / rank_df.count() * 100).round(2),
206        "4位(%)": ((rank_df[rank_df == 4].count()) / rank_df.count() * 100).round(2),
207        "平均順位": rank_df.mean().round(2),
208        "区間ポイント": point_sum_df.sum().round(1),
209        "区間平均": point_sum_df.mean().round(1),
210        "通算ポイント": point_df.round(1),
211    }
212    count_df = pd.DataFrame(count_stats)
213
214    count_df.loc[total_index] = pd.Series(
215        {
216            "ゲーム数": int(count_df["ゲーム数"].sum()),
217            "1位": int(count_df["1位"].sum()),
218            "2位": int(count_df["2位"].sum()),
219            "3位": int(count_df["3位"].sum()),
220            "4位": int(count_df["4位"].sum()),
221            "1位(%)": float(round((count_df["1位"].sum() / count_df["ゲーム数"].sum() * 100), 2)),
222            "2位(%)": float(round((count_df["2位"].sum() / count_df["ゲーム数"].sum() * 100), 2)),
223            "3位(%)": float(round((count_df["3位"].sum() / count_df["ゲーム数"].sum() * 100), 2)),
224            "4位(%)": float(round((count_df["4位"].sum() / count_df["ゲーム数"].sum() * 100), 2)),
225            "平均順位": float(round(player_df["rank"].mean(), 2)),
226            "区間ポイント": float(round(player_df["point"].sum(), 1)),
227            "区間平均": float(round(player_df["point"].mean(), 1)),
228        }
229    )
230    # テーブル用データ
231    rank_table = pd.DataFrame()
232    rank_table["ゲーム数"] = count_df["ゲーム数"].astype("int")
233    rank_table["1位"] = count_df.apply(lambda row: f"{row["1位(%)"]:.2f}% ({row["1位"]:.0f})", axis=1)
234    rank_table["2位"] = count_df.apply(lambda row: f"{row["2位(%)"]:.2f}% ({row["2位"]:.0f})", axis=1)
235    rank_table["3位"] = count_df.apply(lambda row: f"{row["3位(%)"]:.2f}% ({row["3位"]:.0f})", axis=1)
236    rank_table["4位"] = count_df.apply(lambda row: f"{row["4位(%)"]:.2f}% ({row["4位"]:.0f})", axis=1)
237    rank_table["平均順位"] = count_df.apply(lambda row: f"{row["平均順位"]:.2f}", axis=1)
238
239    # グラフ設定
240    configuration.graph_setup(plt, fm)
241    fig = plt.figure(figsize=(20, 10))
242    fig.suptitle(title_text, size=20, weight="bold")
243    gs = gridspec.GridSpec(figure=fig, nrows=3, ncols=2)
244
245    ax_point1 = fig.add_subplot(gs[0, 0])
246    ax_point2 = fig.add_subplot(gs[0, 1])
247    ax_rank1 = fig.add_subplot(gs[1, 0])
248    ax_rank2 = fig.add_subplot(gs[1, 1])
249    ax_rpoint1 = fig.add_subplot(gs[2, 0])
250    ax_rpoint2 = fig.add_subplot(gs[2, 1])
251
252    plt.subplots_adjust(wspace=0.22, hspace=0.18)
253
254    # ポイントデータ
255    subplot_point(point_df, ax_point1)
256    subplot_table(count_df.filter(items=["ゲーム数", "区間ポイント", "区間平均", "通算ポイント"]), ax_point2)
257
258    # 順位データ
259    subplot_rank(count_df, ax_rank1, total_index)
260    subplot_table(rank_table, ax_rank2)
261
262    # 素点データ
263    subplot_box(rpoint_df, ax_rpoint1)
264    subplot_table(stats_df, ax_rpoint2)
265
266    plt.savefig(save_file, bbox_inches="tight")
267    plt.close()
268    return (len(player_df), save_file)

個人成績の統計グラフを生成する

Returns:

tuple[int,str]:

  • int: 集計対象のゲーム数
  • str: 検索結果が0件のときのメッセージ or グラフ画像保存パス
def get_data( df: pandas.core.series.Series, interval: int) -> pandas.core.frame.DataFrame:
271def get_data(df: pd.Series, interval: int) -> pd.DataFrame:
272    """データフレームを指定範囲で分割する
273
274    Args:
275        df (pd.Series): 分割するデータ
276        interval (int): 1ブロックに収めるデータ数
277
278    Returns:
279        pd.DataFrame: 分割されたデータ
280    """
281
282    # interval単位で分割
283    rpoint_data: dict = {}
284
285    fraction = 0 if not len(df) % interval else interval - len(df) % interval  # 端数
286    if fraction:
287        df = pd.concat([pd.Series([None] * fraction), df], ignore_index=True)
288
289    for x in range(int(len(df) / interval)):
290        s = len(df) % interval + interval * x
291        e = s + interval
292        rpoint_data[f"{max(1, s + 1 - fraction):3d}G - {(e - fraction):3d}G"] = df.iloc[s:e].tolist()
293
294    return pd.DataFrame(rpoint_data)

データフレームを指定範囲で分割する

Arguments:
  • df (pd.Series): 分割するデータ
  • interval (int): 1ブロックに収めるデータ数
Returns:

pd.DataFrame: 分割されたデータ

def subplot_box(df: pandas.core.frame.DataFrame, ax: matplotlib.axes._axes.Axes) -> None:
297def subplot_box(df: pd.DataFrame, ax: plt.Axes) -> None:
298    """箱ひげ図を生成する
299
300    Args:
301        df (pd.DataFrame): プロットデータ
302        ax (plt.Axes): プロット先オブジェクト
303    """
304
305    p = [x + 1 for x in range(len(df.columns))]
306    df.plot(
307        ax=ax,
308        kind="box",
309        title="素点分布",
310        showmeans=True,
311        meanprops={"marker": "x", "markeredgecolor": "b", "markerfacecolor": "b", "ms": 3},
312        flierprops={"marker": ".", "markeredgecolor": "r"},
313        ylabel="素点(点)",
314        sharex=True,
315    )
316    ax.axhline(y=25000, linewidth=0.5, ls="dashed", color="grey")
317    ax.set_xticks(p)
318    ax.set_xticklabels(df.columns, rotation=45, ha="right")
319
320    # Y軸修正
321    ylabs = ax.get_yticks()[1:-1]
322    ax.set_yticks(ylabs)
323    ax.set_yticklabels(
324        [str(int(ylab)).replace("-", "▲") for ylab in ylabs]
325    )

箱ひげ図を生成する

Arguments:
  • df (pd.DataFrame): プロットデータ
  • ax (plt.Axes): プロット先オブジェクト
def subplot_table(df: pandas.core.frame.DataFrame, ax: matplotlib.axes._axes.Axes) -> None:
328def subplot_table(df: pd.DataFrame, ax: plt.Axes) -> None:
329    """テーブルを生成する
330
331    Args:
332        df (pd.DataFrame): プロットデータ
333        ax (plt.Axes): プロット先オブジェクト
334    """
335
336    # 有効桁数の調整
337    for col in df.columns:
338        match col:
339            case "ゲーム数":
340                df[col] = df[col].apply(lambda x: int(float(x)))
341            case "区間ポイント" | "区間平均" | "通算ポイント":
342                df[col] = df[col].apply(lambda x: f"{float(x):+.1f}")
343            case "平均順位":
344                df[col] = df[col].apply(lambda x: f"{float(x):.2f}")
345            case "平均値(x)":
346                df[col] = df[col].apply(lambda x: f"{float(x):.1f}")
347            case "最小値" | "第一四分位数" | "中央値(|)" | "第三四分位数" | "最大値":
348                df[col] = df[col].apply(lambda x: int(float(x)))
349
350    df = df.apply(lambda col: col.map(lambda x: str(x).replace("-", "▲")))
351    df.replace("+nan", "-----", inplace=True)
352
353    table = ax.table(
354        cellText=df.values.tolist(),
355        colLabels=df.columns.tolist(),
356        rowLabels=df.index.tolist(),
357        cellLoc="center",
358        loc="center",
359    )
360    table.auto_set_font_size(False)
361    ax.axis("off")

テーブルを生成する

Arguments:
  • df (pd.DataFrame): プロットデータ
  • ax (plt.Axes): プロット先オブジェクト
def subplot_point(df: pandas.core.series.Series, ax: matplotlib.axes._axes.Axes) -> None:
364def subplot_point(df: pd.Series, ax: plt.Axes) -> None:
365    """ポイントデータ
366
367    Args:
368        df (pd.Series): プロットデータ
369        ax (plt.Axes): プロット先オブジェクト
370    """
371
372    df.plot(  # レイアウト調整用ダミー
373        ax=ax,
374        kind="bar",
375        alpha=0,
376    )
377    df.plot(
378        ax=ax,
379        kind="line",
380        title="ポイント推移",
381        ylabel="通算ポイント(pt)",
382        marker="o",
383        color="b",
384    )
385    # Y軸修正
386    ylabs = ax.get_yticks()[1:-1]
387    ax.set_yticks(ylabs)
388    ax.set_yticklabels(
389        [str(int(ylab)).replace("-", "▲") for ylab in ylabs]
390    )

ポイントデータ

Arguments:
  • df (pd.Series): プロットデータ
  • ax (plt.Axes): プロット先オブジェクト
def subplot_rank( df: pandas.core.frame.DataFrame, ax: matplotlib.axes._axes.Axes, total_index: str) -> None:
393def subplot_rank(df: pd.DataFrame, ax: plt.Axes, total_index: str) -> None:
394    """順位データ
395
396    Args:
397        df (pd.DataFrame): プロットデータ
398        ax (plt.Axes): プロット先オブジェクト
399        total_index (str): 合計値格納index
400    """
401
402    ax_rank_avg = ax.twinx()
403    df.filter(items=["平均順位"]).drop(index=total_index).plot(
404        ax=ax_rank_avg,
405        kind="line",
406        ylabel="平均順位",
407        yticks=[1, 2, 3, 4],
408        ylim=[0.85, 4.15],
409        marker="o",
410        color="b",
411        legend=False,
412        grid=False,
413    )
414    ax_rank_avg.invert_yaxis()
415    ax_rank_avg.axhline(y=2.5, linewidth=0.5, ls="dashed", color="grey")
416
417    filter_items = ["1位(%)", "2位(%)", "3位(%)", "4位(%)"]
418    df.filter(items=filter_items).drop(index=total_index).plot(
419        ax=ax,
420        kind="bar",
421        title="獲得順位",
422        ylabel="獲得順位(%)",
423        colormap="Set2",
424        stacked=True,
425        rot=90,
426        ylim=[-5, 105],
427    )
428    h1, l1 = ax.get_legend_handles_labels()
429    h2, l2 = ax_rank_avg.get_legend_handles_labels()
430    ax.legend(
431        h1 + h2,
432        l1 + l2,
433        bbox_to_anchor=(0.5, 0),
434        loc="lower center",
435        ncol=5,
436    )

順位データ

Arguments:
  • df (pd.DataFrame): プロットデータ
  • ax (plt.Axes): プロット先オブジェクト
  • total_index (str): 合計値格納index