libs.commands.graph.personal

libs/commands/graph/personal.py

  1"""
  2libs/commands/graph/personal.py
  3"""
  4
  5from typing import TYPE_CHECKING
  6
  7import matplotlib.pyplot as plt
  8import pandas as pd
  9import plotly.express as px  # type: ignore
 10import plotly.graph_objects as go  # type: ignore
 11from matplotlib import gridspec
 12from plotly.subplots import make_subplots  # type: ignore
 13
 14import libs.global_value as g
 15from cls.timekit import ExtendedDatetime as ExtDt
 16from libs.data import loader
 17from libs.datamodels import GameInfo
 18from libs.functions import compose, message
 19from libs.types import StyleOptions
 20from libs.utils import formatter, graphutil, textutil
 21
 22if TYPE_CHECKING:
 23    from pathlib import Path
 24
 25    from integrations.protocols import MessageParserProtocol
 26
 27
 28def plot(m: "MessageParserProtocol"):
 29    """個人成績のグラフを生成する
 30
 31    Args:
 32        m (MessageParserProtocol): メッセージデータ
 33    """
 34
 35    # データ収集
 36    game_info = GameInfo()
 37    g.params.update(guest_skip=g.params.get("guest_skip2"))
 38    df = loader.read_data("SUMMARY_GAMEDATA")
 39
 40    if df.empty:
 41        m.post.headline = {"0": message.random_reply(m, "no_hits")}
 42        m.status.result = False
 43        return
 44
 45    player = formatter.name_replace(g.params["player_name"], add_mark=True)
 46    if g.params.get("anonymous"):
 47        mapping_dict = formatter.anonymous_mapping([g.params["player_name"]])
 48        player = next(iter(mapping_dict.values()))
 49
 50    # 最終値(凡例/ラベル追加用)
 51    point_sum = f"{float(df["point_sum"].iloc[-1]):+.1f}".replace("-", "▲")
 52    point_avg = f"{float(df["point_avg"].iloc[-1]):+.1f}".replace("-", "▲")
 53    rank_avg = f"{float(df["rank_avg"].iloc[-1]):.2f}"
 54    total_game_count = int(df["count"].iloc[-1])
 55
 56    title_text = f"『{player}』の成績"
 57    if g.params.get("target_count", 0) == 0:
 58        title_range = f"({ExtDt(g.params["starttime"]).format("ymdhm")} - {ExtDt(g.params["endtime"]).format("ymdhm")})"
 59    else:
 60        title_range = f"(直近 {len(df)} ゲーム)"
 61
 62    m.post.headline = {title_text: message.header(game_info, m)}
 63    m.set_data(
 64        "個人成績", formatter.df_rename(df.drop(columns=["count", "name"]), False),
 65        StyleOptions(header_hidden=True, key_title=False)
 66    )
 67
 68    # --- グラフ生成
 69    graphutil.setup()
 70    match g.adapter.conf.plotting_backend:
 71        case "plotly":
 72            m.set_data("通算ポイント", plotly_point(df, title_range, total_game_count), StyleOptions())
 73            m.set_data("獲得順位", plotly_rank(df, title_range, total_game_count), StyleOptions())
 74        case "matplotlib":
 75            save_file = textutil.save_file_path("graph.png")
 76            fig = plt.figure(figsize=(12, 8))
 77            fig.suptitle(f"{title_text} {title_range}", fontsize=16)
 78            fig.tight_layout()
 79
 80            grid = gridspec.GridSpec(nrows=2, ncols=1, height_ratios=[3, 1])
 81            point_ax = fig.add_subplot(grid[0])
 82            rank_ax = fig.add_subplot(grid[1], sharex=point_ax)
 83
 84            # ポイント
 85            point_ax.plot(df["playtime"], df["point_sum"], marker="." if len(df) < 50 else None)
 86            point_ax.plot(df["playtime"], df["point_avg"], marker="." if len(df) < 50 else None)
 87            point_ax.bar(df["playtime"], df["point"], color="blue")
 88
 89            point_ax.tick_params(axis="x", which="both", labelbottom=False, bottom=False)
 90            ylabs = point_ax.get_yticks()[1:-1]
 91            point_ax.set_yticks(ylabs, [str(int(ylab)).replace("-", "▲") for ylab in ylabs])
 92
 93            point_ax.legend(
 94                [f"通算ポイント ({point_sum}pt)", f"平均ポイント ({point_avg}pt)", "獲得ポイント"],
 95                bbox_to_anchor=(1, 1),
 96                loc="upper left",
 97                borderaxespad=0.5,
 98            )
 99
100            point_ax.axhline(y=0, linewidth=0.5, ls="dashed", color="grey")
101
102            # 順位
103            rank_ax.plot(df["playtime"], df["rank"], marker="." if len(df) < 50 else None)
104            rank_ax.plot(df["playtime"], df["rank_avg"], marker="." if len(df) < 50 else None)
105
106            rank_ax.set_xlabel(graphutil.gen_xlabel(len(df)))
107            rank_ax.set_xticks(**graphutil.xticks_parameter(df["playtime"].to_list()))
108            rank_ax.set_ylim(ymin=0.85, ymax=4.15)
109            rank_ax.invert_yaxis()
110
111            rank_ax.legend(
112                ["獲得順位", f"平均順位 ({rank_avg})"],
113                bbox_to_anchor=(1, 1),
114                loc="upper left",
115                borderaxespad=0.5,
116            )
117
118            rank_ax.axhline(y=2.5, linewidth=0.5, ls="dashed", color="grey")
119
120            plt.savefig(save_file, bbox_inches="tight")
121            m.set_data(
122                f"『{player}』の成績", save_file,
123                StyleOptions(use_comment=True, header_hidden=True, key_title=False),
124            )
125
126
127def statistics_plot(m: "MessageParserProtocol"):
128    """個人成績の統計グラフを生成する
129
130    Args:
131        m (MessageParserProtocol): メッセージデータ
132    """
133
134    # データ収集
135    game_info = GameInfo()
136    g.params.update(guest_skip=g.params.get("guest_skip2"))
137    df = loader.read_data("SUMMARY_DETAILS")
138
139    if df.empty:
140        m.post.headline = {"0": message.random_reply(m, "no_hits")}
141        m.status.result = False
142        return
143
144    if g.params.get("individual"):  # 個人成績
145        player = formatter.name_replace(g.params["player_name"], add_mark=True)
146    else:  # チーム成績
147        player = g.params["player_name"]
148
149    df = df.filter(items=["playtime", "name", "rpoint", "rank", "point"])
150    df["rpoint"] = df["rpoint"] * 100
151
152    player_df = df.query("name == @player").reset_index(drop=True)
153
154    if player_df.empty:
155        m.post.headline = {"0": message.random_reply(m, "no_hits")}
156        m.status.result = False
157        return
158
159    player_df["sum_point"] = player_df["point"].cumsum()
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}』の成績 (検索範囲:{compose.text_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        "1位(%)": ((rank_df[rank_df == 1].count()) / rank_df.count()),
201        "2位": rank_df[rank_df == 2].count().astype("int"),
202        "2位(%)": ((rank_df[rank_df == 2].count()) / rank_df.count()),
203        "3位": rank_df[rank_df == 3].count().astype("int"),
204        "3位(%)": ((rank_df[rank_df == 3].count()) / rank_df.count()),
205        "4位": rank_df[rank_df == 4].count().astype("int"),
206        "4位(%)": ((rank_df[rank_df == 4].count()) / rank_df.count()),
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            "1位(%)": float(count_df["1位"].sum() / count_df["ゲーム数"].sum()),
219            "2位": int(count_df["2位"].sum()),
220            "2位(%)": float(count_df["2位"].sum() / count_df["ゲーム数"].sum()),
221            "3位": int(count_df["3位"].sum()),
222            "3位(%)": float(count_df["3位"].sum() / count_df["ゲーム数"].sum()),
223            "4位": int(count_df["4位"].sum()),
224            "4位(%)": float(count_df["4位"].sum() / count_df["ゲーム数"].sum()),
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位(%)"]:.2%} ({row["1位"]:.0f})", axis=1)
234    rank_table["2位"] = count_df.apply(lambda row: f"{row["2位(%)"]:.2%} ({row["2位"]:.0f})", axis=1)
235    rank_table["3位"] = count_df.apply(lambda row: f"{row["3位(%)"]:.2%} ({row["3位"]:.0f})", axis=1)
236    rank_table["4位"] = count_df.apply(lambda row: f"{row["4位(%)"]:.2%} ({row["4位"]:.0f})", axis=1)
237    rank_table["平均順位"] = count_df.apply(lambda row: f"{row["平均順位"]:.2f}", axis=1)
238
239    m.post.headline = {f"『{player}』の成績": message.header(game_info, m)}
240
241    # --- グラフ生成
242    graphutil.setup()
243    match g.adapter.conf.plotting_backend:
244        case "plotly":
245            m.set_data("順位/ポイント情報", count_df, StyleOptions(show_index=True))
246            m.set_data("通算ポイント", plotly_line("通算ポイント推移", point_df), StyleOptions())
247            m.set_data("順位分布", plotly_bar("順位分布", count_df.drop(index=["全区間"])), StyleOptions())
248            m.set_data("素点情報", stats_df, StyleOptions(show_index=True))
249            m.set_data("素点分布", plotly_box("素点分布", rpoint_df), StyleOptions())
250        case "matplotlib":
251            fig = plt.figure(figsize=(20, 10))
252            fig.suptitle(title_text, size=20, weight="bold")
253            gs = gridspec.GridSpec(figure=fig, nrows=3, ncols=2)
254
255            ax_point1 = fig.add_subplot(gs[0, 0])
256            ax_point2 = fig.add_subplot(gs[0, 1])
257            ax_rank1 = fig.add_subplot(gs[1, 0])
258            ax_rank2 = fig.add_subplot(gs[1, 1])
259            ax_rpoint1 = fig.add_subplot(gs[2, 0])
260            ax_rpoint2 = fig.add_subplot(gs[2, 1])
261
262            plt.subplots_adjust(wspace=0.22, hspace=0.18)
263
264            # ポイントデータ
265            subplot_point(point_df, ax_point1)
266            subplot_table(count_df.filter(items=["ゲーム数", "区間ポイント", "区間平均", "通算ポイント"]), ax_point2)
267
268            # 順位データ
269            subplot_rank(count_df.copy(), ax_rank1, total_index)
270            subplot_table(rank_table, ax_rank2)
271
272            # 素点データ
273            subplot_box(rpoint_df, ax_rpoint1)
274            subplot_table(stats_df, ax_rpoint2)
275
276            save_file = textutil.save_file_path("graph.png")
277            plt.savefig(save_file, bbox_inches="tight")
278
279            m.set_data("個人成績", save_file, StyleOptions(use_comment=True, header_hidden=True))
280
281
282def get_data(df: pd.Series, interval: int) -> pd.DataFrame:
283    """データフレームを指定範囲で分割する
284
285    Args:
286        df (pd.Series): 分割するデータ
287        interval (int): 1ブロックに収めるデータ数
288
289    Returns:
290        pd.DataFrame: 分割されたデータ
291    """
292
293    # interval単位で分割
294    rpoint_data: dict = {}
295
296    fraction = 0 if not len(df) % interval else interval - len(df) % interval  # 端数
297    if fraction:
298        df = pd.concat([pd.Series([None] * fraction), df], ignore_index=True)
299
300    for x in range(int(len(df) / interval)):
301        s = len(df) % interval + interval * x
302        e = s + interval
303        rpoint_data[f"{max(1, s + 1 - fraction):3d}G - {(e - fraction):3d}G"] = df.iloc[s:e].tolist()
304
305    return pd.DataFrame(rpoint_data)
306
307
308def subplot_box(df: pd.DataFrame, ax: plt.Axes) -> None:
309    """箱ひげ図を生成する
310
311    Args:
312        df (pd.DataFrame): プロットデータ
313        ax (plt.Axes): プロット先オブジェクト
314    """
315
316    p = [x + 1 for x in range(len(df.columns))]
317    df.plot(
318        ax=ax,
319        kind="box",
320        title="素点分布",
321        showmeans=True,
322        meanprops={"marker": "x", "markeredgecolor": "b", "markerfacecolor": "b", "ms": 3},
323        flierprops={"marker": ".", "markeredgecolor": "r"},
324        ylabel="素点(点)",
325        sharex=True,
326    )
327    ax.axhline(y=25000, linewidth=0.5, ls="dashed", color="grey")
328    ax.set_xticks(p)
329    ax.set_xticklabels(df.columns, rotation=45, ha="right")
330
331    # Y軸修正
332    ylabs = ax.get_yticks()[1:-1]
333    ax.set_yticks(ylabs)
334    ax.set_yticklabels(
335        [str(int(ylab)).replace("-", "▲") for ylab in ylabs]
336    )
337
338
339def subplot_table(df: pd.DataFrame, ax: plt.Axes) -> None:
340    """テーブルを生成する
341
342    Args:
343        df (pd.DataFrame): プロットデータ
344        ax (plt.Axes): プロット先オブジェクト
345    """
346
347    # 有効桁数の調整
348    for col in df.columns:
349        match col:
350            case "ゲーム数":
351                df[col] = df[col].apply(lambda x: int(float(x)))
352            case "区間ポイント" | "区間平均" | "通算ポイント":
353                df[col] = df[col].apply(lambda x: f"{float(x):+.1f}")
354            case "平均順位":
355                df[col] = df[col].apply(lambda x: f"{float(x):.2f}")
356            case "平均値(x)":
357                df[col] = df[col].apply(lambda x: f"{float(x):.1f}")
358            case "最小値" | "第一四分位数" | "中央値(|)" | "第三四分位数" | "最大値":
359                df[col] = df[col].apply(lambda x: int(float(x)))
360
361    df = df.apply(lambda col: col.map(lambda x: str(x).replace("-", "▲")))
362    df.replace("+nan", "-----", inplace=True)
363
364    table = ax.table(
365        cellText=df.values.tolist(),
366        colLabels=df.columns.tolist(),
367        rowLabels=df.index.tolist(),
368        cellLoc="center",
369        loc="center",
370    )
371    table.auto_set_font_size(False)
372    ax.axis("off")
373
374
375def subplot_point(df: pd.Series, ax: plt.Axes) -> None:
376    """ポイントデータ
377
378    Args:
379        df (pd.Series): プロットデータ
380        ax (plt.Axes): プロット先オブジェクト
381    """
382
383    df.plot(  # レイアウト調整用ダミー
384        ax=ax,
385        kind="bar",
386        alpha=0,
387    )
388    df.plot(
389        ax=ax,
390        kind="line",
391        title="ポイント推移",
392        ylabel="通算ポイント(pt)",
393        marker="o",
394        color="b",
395    )
396    # Y軸修正
397    ylabs = ax.get_yticks()[1:-1]
398    ax.set_yticks(ylabs)
399    ax.set_yticklabels(
400        [str(int(ylab)).replace("-", "▲") for ylab in ylabs]
401    )
402
403
404def subplot_rank(df: pd.DataFrame, ax: plt.Axes, total_index: str) -> None:
405    """順位データ
406
407    Args:
408        df (pd.DataFrame): プロットデータ
409        ax (plt.Axes): プロット先オブジェクト
410        total_index (str): 合計値格納index
411    """
412
413    df["1位(%)"] = df["1位(%)"] * 100
414    df["2位(%)"] = df["2位(%)"] * 100
415    df["3位(%)"] = df["3位(%)"] * 100
416    df["4位(%)"] = df["4位(%)"] * 100
417
418    ax_rank_avg = ax.twinx()
419    df.filter(items=["平均順位"]).drop(index=total_index).plot(
420        ax=ax_rank_avg,
421        kind="line",
422        ylabel="平均順位",
423        yticks=[1, 2, 3, 4],
424        ylim=[0.85, 4.15],
425        marker="o",
426        color="b",
427        legend=False,
428        grid=False,
429    )
430    ax_rank_avg.invert_yaxis()
431    ax_rank_avg.axhline(y=2.5, linewidth=0.5, ls="dashed", color="grey")
432
433    filter_items = ["1位(%)", "2位(%)", "3位(%)", "4位(%)"]
434    df.filter(items=filter_items).drop(index=total_index).plot(
435        ax=ax,
436        kind="bar",
437        title="獲得順位",
438        ylabel="獲得順位(%)",
439        colormap="Set2",
440        stacked=True,
441        rot=90,
442        ylim=[-5, 105],
443    )
444    h1, l1 = ax.get_legend_handles_labels()
445    h2, l2 = ax_rank_avg.get_legend_handles_labels()
446    ax.legend(
447        h1 + h2,
448        l1 + l2,
449        bbox_to_anchor=(0.5, 0),
450        loc="lower center",
451        ncol=5,
452    )
453
454
455def plotly_point(df: pd.DataFrame, title_range: str, total_game_count: int) -> "Path":
456    """獲得ポイントグラフ(plotly用)
457
458    Args:
459        df (pd.DataFrame): プロットするデータ
460        title_range (str): 集計範囲(タイトル用)
461        total_game_count (int): ゲーム数
462
463    Returns:
464        Path: 保存先ファイルパス
465    """
466
467    save_file = textutil.save_file_path("point.html")
468
469    fig = go.Figure()
470    fig.add_trace(
471        go.Scatter(
472            name="通算ポイント",
473            zorder=2,
474            mode="lines",
475            x=df["playtime"],
476            y=df["point_sum"],
477        ),
478    )
479    fig.add_trace(
480        go.Bar(
481            name="獲得ポイント",
482            zorder=1,
483            x=df["playtime"],
484            y=df["point"],
485            marker_color=["darkgreen" if v >= 0 else "firebrick" for v in df["point"]],
486        ),
487    )
488
489    fig.update_layout(
490        barmode="overlay",
491        title={
492            "text": f"通算ポイント {title_range}",
493            "font": {"size": 30},
494            "xref": "paper",
495            "xanchor": "center",
496            "x": 0.5,
497        },
498        xaxis_title={
499            "text": graphutil.gen_xlabel(total_game_count),
500            "font": {"size": 18},
501        },
502        legend_title=None,
503    )
504
505    fig.update_yaxes(
506        title={
507            "text": "ポイント(pt)",
508            "font": {"size": 18, "color": "white"},
509        },
510    )
511
512    fig.write_html(save_file, full_html=False)
513    return save_file
514
515
516def plotly_rank(df: pd.DataFrame, title_range: str, total_game_count: int) -> "Path":
517    """獲得順位グラフ(plotly用)
518
519    Args:
520        df (pd.DataFrame): プロットするデータ
521        title_range (str): 集計範囲(タイトル用)
522        total_game_count (int): ゲーム数
523
524    Returns:
525        Path: 保存先ファイルパス
526    """
527
528    save_file = textutil.save_file_path("rank.html")
529
530    fig = go.Figure()
531    fig.add_trace(
532        go.Scatter(
533            name="獲得順位",
534            zorder=1,
535            mode="lines",
536            x=df["playtime"],
537            y=df["rank"],
538        ),
539    )
540    fig.add_trace(
541        go.Scatter(
542            name="平均順位",
543            zorder=2,
544            mode="lines",
545            x=df["playtime"],
546            y=df["rank_avg"],
547            line={"width": 5},
548        ),
549    )
550
551    fig.update_layout(
552        title={
553            "text": f"獲得順位 {title_range}",
554            "font": {"size": 30},
555            "xref": "paper",
556            "xanchor": "center",
557            "x": 0.5,
558        },
559        xaxis_title={
560            "text": graphutil.gen_xlabel(total_game_count),
561            "font": {"size": 18},
562        },
563        legend_title=None,
564    )
565
566    fig.update_yaxes(
567        title={
568            "text": "順位",
569            "font": {"size": 18, "color": "white"},
570        },
571        range=[4.2, 0.8],
572        tickvals=[4, 3, 2, 1],
573        zeroline=False,
574    )
575
576    fig.add_hline(
577        y=2.5,
578        line_dash="dot",
579        line_color="white",
580        line_width=2,
581        layer="below",
582    )
583
584    fig.write_html(save_file, full_html=False)
585    return save_file
586
587
588def plotly_line(title_text: str, df: pd.Series) -> "Path":
589    """通算ポイント推移グラフ生成(plotly用)
590
591    Args:
592        title_text (str): グラフタイトル
593        df (pd.DataFrame): プロットするデータ
594
595    Returns:
596        Path: 保存先ファイルパス
597    """
598
599    save_file = textutil.save_file_path("point.html")
600
601    fig = go.Figure()
602    fig.add_traces(
603        go.Scatter(
604            mode="lines+markers",
605            x=df.index,
606            y=df.values,
607        ),
608    )
609
610    fig.update_layout(
611        title={
612            "text": title_text,
613            "font": {"size": 30},
614            "xref": "paper",
615            "xanchor": "center",
616            "x": 0.5,
617        },
618        xaxis_title={
619            "text": "ゲーム区間",
620            "font": {"size": 18},
621        },
622        yaxis_title={
623            "text": "ポイント(pt)",
624            "font": {"size": 18},
625        },
626        showlegend=False,
627    )
628    fig.update_yaxes(
629        tickformat="d",
630    )
631
632    fig.write_html(save_file, full_html=False)
633    return save_file
634
635
636def plotly_box(title_text: str, df: pd.DataFrame) -> "Path":
637    """素点分布グラフ生成(plotly用)
638
639    Args:
640        title_text (str): グラフタイトル
641        df (pd.DataFrame): プロットするデータ
642
643    Returns:
644        Path: 保存先ファイルパス
645    """
646
647    save_file = textutil.save_file_path("rpoint.html")
648    fig = px.box(df)
649    fig.update_layout(
650        title={
651            "text": title_text,
652            "font": {"size": 30},
653            "xref": "paper",
654            "xanchor": "center",
655            "x": 0.5,
656        },
657        xaxis_title={
658            "text": "ゲーム区間",
659            "font": {"size": 18},
660        },
661        yaxis_title={
662            "text": "素点(点)",
663            "font": {"size": 18},
664        },
665    )
666    fig.update_yaxes(
667        zeroline=False,
668        tickformat="d",
669    )
670    fig.add_hline(
671        y=25000,
672        line_dash="dot",
673        line_color="white",
674        line_width=1,
675        layer="below",
676    )
677
678    fig.write_html(save_file, full_html=False)
679    return save_file
680
681
682def plotly_bar(title_text: str, df: pd.DataFrame) -> "Path":
683    """順位分布グラフ生成(plotly用)
684
685    Args:
686        title_text (str): グラフタイトル
687        df (pd.Series): プロットするデータ
688
689    Returns:
690        Path: 保存先ファイルパス
691    """
692
693    save_file = textutil.save_file_path("rank.html")
694
695    fig = make_subplots(specs=[[{"secondary_y": True}]])
696    # 獲得率
697    fig.add_trace(go.Bar(name="4位率", x=df.index, y=df["4位(%)"] * 100), secondary_y=False)
698    fig.add_trace(go.Bar(name="3位率", x=df.index, y=df["3位(%)"] * 100), secondary_y=False)
699    fig.add_trace(go.Bar(name="2位率", x=df.index, y=df["2位(%)"] * 100), secondary_y=False)
700    fig.add_trace(go.Bar(name="1位率", x=df.index, y=df["1位(%)"] * 100), secondary_y=False)
701    # 平均順位
702    fig.add_trace(
703        go.Scatter(
704            mode="lines+markers",
705            name="平均順位",
706            x=df.index,
707            y=df["平均順位"],
708        ),
709        secondary_y=True,
710    )
711
712    fig.update_layout(
713        barmode="stack",
714        title={
715            "text": title_text,
716            "font": {"size": 30},
717            "xref": "paper",
718            "xanchor": "center",
719            "x": 0.5,
720        },
721        xaxis_title={
722            "text": "ゲーム区間",
723            "font": {"size": 18},
724        },
725        legend_traceorder="reversed",
726        legend_title=None,
727        legend={
728            "xanchor": "center",
729            "yanchor": "bottom",
730            "orientation": "h",
731            "x": 0.5,
732            "y": 0.02,
733        },
734    )
735    fig.update_yaxes(  # Y軸(左)
736        title={
737            "text": "獲得順位(%)",
738            "font": {"size": 18, "color": "white"},
739        },
740        secondary_y=False,
741        zeroline=False,
742    )
743    fig.update_yaxes(  # Y軸(右)
744        secondary_y=True,
745        title={
746            "text": "平均順位",
747            "font": {"size": 18, "color": "white"},
748        },
749        tickfont_color="white",
750        range=[4, 1],
751        showgrid=False,
752        zeroline=False,
753    )
754
755    fig.write_html(save_file, full_html=False)
756    return save_file
 29def plot(m: "MessageParserProtocol"):
 30    """個人成績のグラフを生成する
 31
 32    Args:
 33        m (MessageParserProtocol): メッセージデータ
 34    """
 35
 36    # データ収集
 37    game_info = GameInfo()
 38    g.params.update(guest_skip=g.params.get("guest_skip2"))
 39    df = loader.read_data("SUMMARY_GAMEDATA")
 40
 41    if df.empty:
 42        m.post.headline = {"0": message.random_reply(m, "no_hits")}
 43        m.status.result = False
 44        return
 45
 46    player = formatter.name_replace(g.params["player_name"], add_mark=True)
 47    if g.params.get("anonymous"):
 48        mapping_dict = formatter.anonymous_mapping([g.params["player_name"]])
 49        player = next(iter(mapping_dict.values()))
 50
 51    # 最終値(凡例/ラベル追加用)
 52    point_sum = f"{float(df["point_sum"].iloc[-1]):+.1f}".replace("-", "▲")
 53    point_avg = f"{float(df["point_avg"].iloc[-1]):+.1f}".replace("-", "▲")
 54    rank_avg = f"{float(df["rank_avg"].iloc[-1]):.2f}"
 55    total_game_count = int(df["count"].iloc[-1])
 56
 57    title_text = f"『{player}』の成績"
 58    if g.params.get("target_count", 0) == 0:
 59        title_range = f"({ExtDt(g.params["starttime"]).format("ymdhm")} - {ExtDt(g.params["endtime"]).format("ymdhm")})"
 60    else:
 61        title_range = f"(直近 {len(df)} ゲーム)"
 62
 63    m.post.headline = {title_text: message.header(game_info, m)}
 64    m.set_data(
 65        "個人成績", formatter.df_rename(df.drop(columns=["count", "name"]), False),
 66        StyleOptions(header_hidden=True, key_title=False)
 67    )
 68
 69    # --- グラフ生成
 70    graphutil.setup()
 71    match g.adapter.conf.plotting_backend:
 72        case "plotly":
 73            m.set_data("通算ポイント", plotly_point(df, title_range, total_game_count), StyleOptions())
 74            m.set_data("獲得順位", plotly_rank(df, title_range, total_game_count), StyleOptions())
 75        case "matplotlib":
 76            save_file = textutil.save_file_path("graph.png")
 77            fig = plt.figure(figsize=(12, 8))
 78            fig.suptitle(f"{title_text} {title_range}", fontsize=16)
 79            fig.tight_layout()
 80
 81            grid = gridspec.GridSpec(nrows=2, ncols=1, height_ratios=[3, 1])
 82            point_ax = fig.add_subplot(grid[0])
 83            rank_ax = fig.add_subplot(grid[1], sharex=point_ax)
 84
 85            # ポイント
 86            point_ax.plot(df["playtime"], df["point_sum"], marker="." if len(df) < 50 else None)
 87            point_ax.plot(df["playtime"], df["point_avg"], marker="." if len(df) < 50 else None)
 88            point_ax.bar(df["playtime"], df["point"], color="blue")
 89
 90            point_ax.tick_params(axis="x", which="both", labelbottom=False, bottom=False)
 91            ylabs = point_ax.get_yticks()[1:-1]
 92            point_ax.set_yticks(ylabs, [str(int(ylab)).replace("-", "▲") for ylab in ylabs])
 93
 94            point_ax.legend(
 95                [f"通算ポイント ({point_sum}pt)", f"平均ポイント ({point_avg}pt)", "獲得ポイント"],
 96                bbox_to_anchor=(1, 1),
 97                loc="upper left",
 98                borderaxespad=0.5,
 99            )
100
101            point_ax.axhline(y=0, linewidth=0.5, ls="dashed", color="grey")
102
103            # 順位
104            rank_ax.plot(df["playtime"], df["rank"], marker="." if len(df) < 50 else None)
105            rank_ax.plot(df["playtime"], df["rank_avg"], marker="." if len(df) < 50 else None)
106
107            rank_ax.set_xlabel(graphutil.gen_xlabel(len(df)))
108            rank_ax.set_xticks(**graphutil.xticks_parameter(df["playtime"].to_list()))
109            rank_ax.set_ylim(ymin=0.85, ymax=4.15)
110            rank_ax.invert_yaxis()
111
112            rank_ax.legend(
113                ["獲得順位", f"平均順位 ({rank_avg})"],
114                bbox_to_anchor=(1, 1),
115                loc="upper left",
116                borderaxespad=0.5,
117            )
118
119            rank_ax.axhline(y=2.5, linewidth=0.5, ls="dashed", color="grey")
120
121            plt.savefig(save_file, bbox_inches="tight")
122            m.set_data(
123                f"『{player}』の成績", save_file,
124                StyleOptions(use_comment=True, header_hidden=True, key_title=False),
125            )

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

Arguments:
  • m (MessageParserProtocol): メッセージデータ
def statistics_plot(m: integrations.protocols.MessageParserProtocol):
128def statistics_plot(m: "MessageParserProtocol"):
129    """個人成績の統計グラフを生成する
130
131    Args:
132        m (MessageParserProtocol): メッセージデータ
133    """
134
135    # データ収集
136    game_info = GameInfo()
137    g.params.update(guest_skip=g.params.get("guest_skip2"))
138    df = loader.read_data("SUMMARY_DETAILS")
139
140    if df.empty:
141        m.post.headline = {"0": message.random_reply(m, "no_hits")}
142        m.status.result = False
143        return
144
145    if g.params.get("individual"):  # 個人成績
146        player = formatter.name_replace(g.params["player_name"], add_mark=True)
147    else:  # チーム成績
148        player = g.params["player_name"]
149
150    df = df.filter(items=["playtime", "name", "rpoint", "rank", "point"])
151    df["rpoint"] = df["rpoint"] * 100
152
153    player_df = df.query("name == @player").reset_index(drop=True)
154
155    if player_df.empty:
156        m.post.headline = {"0": message.random_reply(m, "no_hits")}
157        m.status.result = False
158        return
159
160    player_df["sum_point"] = player_df["point"].cumsum()
161
162    if g.params.get("anonymous"):
163        mapping_dict = formatter.anonymous_mapping([g.params["player_name"]])
164        player = next(iter(mapping_dict.values()))
165
166    title_text = f"『{player}』の成績 (検索範囲:{compose.text_item.date_range("ymd_o")})"
167
168    rpoint_df = get_data(player_df["rpoint"], g.params["interval"])
169    point_sum_df = get_data(player_df["point"], g.params["interval"])
170    point_df = get_data(player_df["sum_point"], g.params["interval"]).iloc[-1]
171    rank_df = get_data(player_df["rank"], g.params["interval"])
172    total_index = "全区間"
173
174    rpoint_stats = {
175        "ゲーム数": rpoint_df.count().astype("int"),
176        "平均値(x)": rpoint_df.mean().round(1),
177        "最小値": rpoint_df.min().astype("int"),
178        "第一四分位数": rpoint_df.quantile(0.25).astype("int"),
179        "中央値(|)": rpoint_df.median().astype("int"),
180        "第三四分位数": rpoint_df.quantile(0.75).astype("int"),
181        "最大値": rpoint_df.max().astype("int"),
182    }
183
184    stats_df = pd.DataFrame(rpoint_stats)
185    stats_df.loc[total_index] = pd.Series(
186        {
187            "ゲーム数": int(player_df["rpoint"].count()),
188            "平均値(x)": float(round(player_df["rpoint"].mean(), 1)),
189            "最小値": int(player_df["rpoint"].min()),
190            "第一四分位数": int(player_df["rpoint"].quantile(0.25)),
191            "中央値(|)": int(player_df["rpoint"].median()),
192            "第三四分位数": int(player_df["rpoint"].quantile(0.75)),
193            "最大値": int(player_df["rpoint"].max()),
194        }
195    )
196    stats_df = stats_df.apply(lambda col: col.map(lambda x: f"{int(x)}" if isinstance(x, int) else f"{x:.1f}"))
197
198    count_stats = {
199        "ゲーム数": rank_df.count().astype("int"),
200        "1位": rank_df[rank_df == 1].count().astype("int"),
201        "1位(%)": ((rank_df[rank_df == 1].count()) / rank_df.count()),
202        "2位": rank_df[rank_df == 2].count().astype("int"),
203        "2位(%)": ((rank_df[rank_df == 2].count()) / rank_df.count()),
204        "3位": rank_df[rank_df == 3].count().astype("int"),
205        "3位(%)": ((rank_df[rank_df == 3].count()) / rank_df.count()),
206        "4位": rank_df[rank_df == 4].count().astype("int"),
207        "4位(%)": ((rank_df[rank_df == 4].count()) / rank_df.count()),
208        "平均順位": rank_df.mean().round(2),
209        "区間ポイント": point_sum_df.sum().round(1),
210        "区間平均": point_sum_df.mean().round(1),
211        "通算ポイント": point_df.round(1),
212    }
213    count_df = pd.DataFrame(count_stats)
214
215    count_df.loc[total_index] = pd.Series(
216        {
217            "ゲーム数": int(count_df["ゲーム数"].sum()),
218            "1位": int(count_df["1位"].sum()),
219            "1位(%)": float(count_df["1位"].sum() / count_df["ゲーム数"].sum()),
220            "2位": int(count_df["2位"].sum()),
221            "2位(%)": float(count_df["2位"].sum() / count_df["ゲーム数"].sum()),
222            "3位": int(count_df["3位"].sum()),
223            "3位(%)": float(count_df["3位"].sum() / count_df["ゲーム数"].sum()),
224            "4位": int(count_df["4位"].sum()),
225            "4位(%)": float(count_df["4位"].sum() / count_df["ゲーム数"].sum()),
226            "平均順位": float(round(player_df["rank"].mean(), 2)),
227            "区間ポイント": float(round(player_df["point"].sum(), 1)),
228            "区間平均": float(round(player_df["point"].mean(), 1)),
229        }
230    )
231    # テーブル用データ
232    rank_table = pd.DataFrame()
233    rank_table["ゲーム数"] = count_df["ゲーム数"].astype("int")
234    rank_table["1位"] = count_df.apply(lambda row: f"{row["1位(%)"]:.2%} ({row["1位"]:.0f})", axis=1)
235    rank_table["2位"] = count_df.apply(lambda row: f"{row["2位(%)"]:.2%} ({row["2位"]:.0f})", axis=1)
236    rank_table["3位"] = count_df.apply(lambda row: f"{row["3位(%)"]:.2%} ({row["3位"]:.0f})", axis=1)
237    rank_table["4位"] = count_df.apply(lambda row: f"{row["4位(%)"]:.2%} ({row["4位"]:.0f})", axis=1)
238    rank_table["平均順位"] = count_df.apply(lambda row: f"{row["平均順位"]:.2f}", axis=1)
239
240    m.post.headline = {f"『{player}』の成績": message.header(game_info, m)}
241
242    # --- グラフ生成
243    graphutil.setup()
244    match g.adapter.conf.plotting_backend:
245        case "plotly":
246            m.set_data("順位/ポイント情報", count_df, StyleOptions(show_index=True))
247            m.set_data("通算ポイント", plotly_line("通算ポイント推移", point_df), StyleOptions())
248            m.set_data("順位分布", plotly_bar("順位分布", count_df.drop(index=["全区間"])), StyleOptions())
249            m.set_data("素点情報", stats_df, StyleOptions(show_index=True))
250            m.set_data("素点分布", plotly_box("素点分布", rpoint_df), StyleOptions())
251        case "matplotlib":
252            fig = plt.figure(figsize=(20, 10))
253            fig.suptitle(title_text, size=20, weight="bold")
254            gs = gridspec.GridSpec(figure=fig, nrows=3, ncols=2)
255
256            ax_point1 = fig.add_subplot(gs[0, 0])
257            ax_point2 = fig.add_subplot(gs[0, 1])
258            ax_rank1 = fig.add_subplot(gs[1, 0])
259            ax_rank2 = fig.add_subplot(gs[1, 1])
260            ax_rpoint1 = fig.add_subplot(gs[2, 0])
261            ax_rpoint2 = fig.add_subplot(gs[2, 1])
262
263            plt.subplots_adjust(wspace=0.22, hspace=0.18)
264
265            # ポイントデータ
266            subplot_point(point_df, ax_point1)
267            subplot_table(count_df.filter(items=["ゲーム数", "区間ポイント", "区間平均", "通算ポイント"]), ax_point2)
268
269            # 順位データ
270            subplot_rank(count_df.copy(), ax_rank1, total_index)
271            subplot_table(rank_table, ax_rank2)
272
273            # 素点データ
274            subplot_box(rpoint_df, ax_rpoint1)
275            subplot_table(stats_df, ax_rpoint2)
276
277            save_file = textutil.save_file_path("graph.png")
278            plt.savefig(save_file, bbox_inches="tight")
279
280            m.set_data("個人成績", save_file, StyleOptions(use_comment=True, header_hidden=True))

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

Arguments:
  • m (MessageParserProtocol): メッセージデータ
def get_data( df: pandas.core.series.Series, interval: int) -> pandas.core.frame.DataFrame:
283def get_data(df: pd.Series, interval: int) -> pd.DataFrame:
284    """データフレームを指定範囲で分割する
285
286    Args:
287        df (pd.Series): 分割するデータ
288        interval (int): 1ブロックに収めるデータ数
289
290    Returns:
291        pd.DataFrame: 分割されたデータ
292    """
293
294    # interval単位で分割
295    rpoint_data: dict = {}
296
297    fraction = 0 if not len(df) % interval else interval - len(df) % interval  # 端数
298    if fraction:
299        df = pd.concat([pd.Series([None] * fraction), df], ignore_index=True)
300
301    for x in range(int(len(df) / interval)):
302        s = len(df) % interval + interval * x
303        e = s + interval
304        rpoint_data[f"{max(1, s + 1 - fraction):3d}G - {(e - fraction):3d}G"] = df.iloc[s:e].tolist()
305
306    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:
309def subplot_box(df: pd.DataFrame, ax: plt.Axes) -> None:
310    """箱ひげ図を生成する
311
312    Args:
313        df (pd.DataFrame): プロットデータ
314        ax (plt.Axes): プロット先オブジェクト
315    """
316
317    p = [x + 1 for x in range(len(df.columns))]
318    df.plot(
319        ax=ax,
320        kind="box",
321        title="素点分布",
322        showmeans=True,
323        meanprops={"marker": "x", "markeredgecolor": "b", "markerfacecolor": "b", "ms": 3},
324        flierprops={"marker": ".", "markeredgecolor": "r"},
325        ylabel="素点(点)",
326        sharex=True,
327    )
328    ax.axhline(y=25000, linewidth=0.5, ls="dashed", color="grey")
329    ax.set_xticks(p)
330    ax.set_xticklabels(df.columns, rotation=45, ha="right")
331
332    # Y軸修正
333    ylabs = ax.get_yticks()[1:-1]
334    ax.set_yticks(ylabs)
335    ax.set_yticklabels(
336        [str(int(ylab)).replace("-", "▲") for ylab in ylabs]
337    )

箱ひげ図を生成する

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

テーブルを生成する

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

ポイントデータ

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

順位データ

Arguments:
  • df (pd.DataFrame): プロットデータ
  • ax (plt.Axes): プロット先オブジェクト
  • total_index (str): 合計値格納index
def plotly_point( df: pandas.core.frame.DataFrame, title_range: str, total_game_count: int) -> pathlib.Path:
456def plotly_point(df: pd.DataFrame, title_range: str, total_game_count: int) -> "Path":
457    """獲得ポイントグラフ(plotly用)
458
459    Args:
460        df (pd.DataFrame): プロットするデータ
461        title_range (str): 集計範囲(タイトル用)
462        total_game_count (int): ゲーム数
463
464    Returns:
465        Path: 保存先ファイルパス
466    """
467
468    save_file = textutil.save_file_path("point.html")
469
470    fig = go.Figure()
471    fig.add_trace(
472        go.Scatter(
473            name="通算ポイント",
474            zorder=2,
475            mode="lines",
476            x=df["playtime"],
477            y=df["point_sum"],
478        ),
479    )
480    fig.add_trace(
481        go.Bar(
482            name="獲得ポイント",
483            zorder=1,
484            x=df["playtime"],
485            y=df["point"],
486            marker_color=["darkgreen" if v >= 0 else "firebrick" for v in df["point"]],
487        ),
488    )
489
490    fig.update_layout(
491        barmode="overlay",
492        title={
493            "text": f"通算ポイント {title_range}",
494            "font": {"size": 30},
495            "xref": "paper",
496            "xanchor": "center",
497            "x": 0.5,
498        },
499        xaxis_title={
500            "text": graphutil.gen_xlabel(total_game_count),
501            "font": {"size": 18},
502        },
503        legend_title=None,
504    )
505
506    fig.update_yaxes(
507        title={
508            "text": "ポイント(pt)",
509            "font": {"size": 18, "color": "white"},
510        },
511    )
512
513    fig.write_html(save_file, full_html=False)
514    return save_file

獲得ポイントグラフ(plotly用)

Arguments:
  • df (pd.DataFrame): プロットするデータ
  • title_range (str): 集計範囲(タイトル用)
  • total_game_count (int): ゲーム数
Returns:

Path: 保存先ファイルパス

def plotly_rank( df: pandas.core.frame.DataFrame, title_range: str, total_game_count: int) -> pathlib.Path:
517def plotly_rank(df: pd.DataFrame, title_range: str, total_game_count: int) -> "Path":
518    """獲得順位グラフ(plotly用)
519
520    Args:
521        df (pd.DataFrame): プロットするデータ
522        title_range (str): 集計範囲(タイトル用)
523        total_game_count (int): ゲーム数
524
525    Returns:
526        Path: 保存先ファイルパス
527    """
528
529    save_file = textutil.save_file_path("rank.html")
530
531    fig = go.Figure()
532    fig.add_trace(
533        go.Scatter(
534            name="獲得順位",
535            zorder=1,
536            mode="lines",
537            x=df["playtime"],
538            y=df["rank"],
539        ),
540    )
541    fig.add_trace(
542        go.Scatter(
543            name="平均順位",
544            zorder=2,
545            mode="lines",
546            x=df["playtime"],
547            y=df["rank_avg"],
548            line={"width": 5},
549        ),
550    )
551
552    fig.update_layout(
553        title={
554            "text": f"獲得順位 {title_range}",
555            "font": {"size": 30},
556            "xref": "paper",
557            "xanchor": "center",
558            "x": 0.5,
559        },
560        xaxis_title={
561            "text": graphutil.gen_xlabel(total_game_count),
562            "font": {"size": 18},
563        },
564        legend_title=None,
565    )
566
567    fig.update_yaxes(
568        title={
569            "text": "順位",
570            "font": {"size": 18, "color": "white"},
571        },
572        range=[4.2, 0.8],
573        tickvals=[4, 3, 2, 1],
574        zeroline=False,
575    )
576
577    fig.add_hline(
578        y=2.5,
579        line_dash="dot",
580        line_color="white",
581        line_width=2,
582        layer="below",
583    )
584
585    fig.write_html(save_file, full_html=False)
586    return save_file

獲得順位グラフ(plotly用)

Arguments:
  • df (pd.DataFrame): プロットするデータ
  • title_range (str): 集計範囲(タイトル用)
  • total_game_count (int): ゲーム数
Returns:

Path: 保存先ファイルパス

def plotly_line(title_text: str, df: pandas.core.series.Series) -> pathlib.Path:
589def plotly_line(title_text: str, df: pd.Series) -> "Path":
590    """通算ポイント推移グラフ生成(plotly用)
591
592    Args:
593        title_text (str): グラフタイトル
594        df (pd.DataFrame): プロットするデータ
595
596    Returns:
597        Path: 保存先ファイルパス
598    """
599
600    save_file = textutil.save_file_path("point.html")
601
602    fig = go.Figure()
603    fig.add_traces(
604        go.Scatter(
605            mode="lines+markers",
606            x=df.index,
607            y=df.values,
608        ),
609    )
610
611    fig.update_layout(
612        title={
613            "text": title_text,
614            "font": {"size": 30},
615            "xref": "paper",
616            "xanchor": "center",
617            "x": 0.5,
618        },
619        xaxis_title={
620            "text": "ゲーム区間",
621            "font": {"size": 18},
622        },
623        yaxis_title={
624            "text": "ポイント(pt)",
625            "font": {"size": 18},
626        },
627        showlegend=False,
628    )
629    fig.update_yaxes(
630        tickformat="d",
631    )
632
633    fig.write_html(save_file, full_html=False)
634    return save_file

通算ポイント推移グラフ生成(plotly用)

Arguments:
  • title_text (str): グラフタイトル
  • df (pd.DataFrame): プロットするデータ
Returns:

Path: 保存先ファイルパス

def plotly_box(title_text: str, df: pandas.core.frame.DataFrame) -> pathlib.Path:
637def plotly_box(title_text: str, df: pd.DataFrame) -> "Path":
638    """素点分布グラフ生成(plotly用)
639
640    Args:
641        title_text (str): グラフタイトル
642        df (pd.DataFrame): プロットするデータ
643
644    Returns:
645        Path: 保存先ファイルパス
646    """
647
648    save_file = textutil.save_file_path("rpoint.html")
649    fig = px.box(df)
650    fig.update_layout(
651        title={
652            "text": title_text,
653            "font": {"size": 30},
654            "xref": "paper",
655            "xanchor": "center",
656            "x": 0.5,
657        },
658        xaxis_title={
659            "text": "ゲーム区間",
660            "font": {"size": 18},
661        },
662        yaxis_title={
663            "text": "素点(点)",
664            "font": {"size": 18},
665        },
666    )
667    fig.update_yaxes(
668        zeroline=False,
669        tickformat="d",
670    )
671    fig.add_hline(
672        y=25000,
673        line_dash="dot",
674        line_color="white",
675        line_width=1,
676        layer="below",
677    )
678
679    fig.write_html(save_file, full_html=False)
680    return save_file

素点分布グラフ生成(plotly用)

Arguments:
  • title_text (str): グラフタイトル
  • df (pd.DataFrame): プロットするデータ
Returns:

Path: 保存先ファイルパス

def plotly_bar(title_text: str, df: pandas.core.frame.DataFrame) -> pathlib.Path:
683def plotly_bar(title_text: str, df: pd.DataFrame) -> "Path":
684    """順位分布グラフ生成(plotly用)
685
686    Args:
687        title_text (str): グラフタイトル
688        df (pd.Series): プロットするデータ
689
690    Returns:
691        Path: 保存先ファイルパス
692    """
693
694    save_file = textutil.save_file_path("rank.html")
695
696    fig = make_subplots(specs=[[{"secondary_y": True}]])
697    # 獲得率
698    fig.add_trace(go.Bar(name="4位率", x=df.index, y=df["4位(%)"] * 100), secondary_y=False)
699    fig.add_trace(go.Bar(name="3位率", x=df.index, y=df["3位(%)"] * 100), secondary_y=False)
700    fig.add_trace(go.Bar(name="2位率", x=df.index, y=df["2位(%)"] * 100), secondary_y=False)
701    fig.add_trace(go.Bar(name="1位率", x=df.index, y=df["1位(%)"] * 100), secondary_y=False)
702    # 平均順位
703    fig.add_trace(
704        go.Scatter(
705            mode="lines+markers",
706            name="平均順位",
707            x=df.index,
708            y=df["平均順位"],
709        ),
710        secondary_y=True,
711    )
712
713    fig.update_layout(
714        barmode="stack",
715        title={
716            "text": title_text,
717            "font": {"size": 30},
718            "xref": "paper",
719            "xanchor": "center",
720            "x": 0.5,
721        },
722        xaxis_title={
723            "text": "ゲーム区間",
724            "font": {"size": 18},
725        },
726        legend_traceorder="reversed",
727        legend_title=None,
728        legend={
729            "xanchor": "center",
730            "yanchor": "bottom",
731            "orientation": "h",
732            "x": 0.5,
733            "y": 0.02,
734        },
735    )
736    fig.update_yaxes(  # Y軸(左)
737        title={
738            "text": "獲得順位(%)",
739            "font": {"size": 18, "color": "white"},
740        },
741        secondary_y=False,
742        zeroline=False,
743    )
744    fig.update_yaxes(  # Y軸(右)
745        secondary_y=True,
746        title={
747            "text": "平均順位",
748            "font": {"size": 18, "color": "white"},
749        },
750        tickfont_color="white",
751        range=[4, 1],
752        showgrid=False,
753        zeroline=False,
754    )
755
756    fig.write_html(save_file, full_html=False)
757    return save_file

順位分布グラフ生成(plotly用)

Arguments:
  • title_text (str): グラフタイトル
  • df (pd.Series): プロットするデータ
Returns:

Path: 保存先ファイルパス