libs.commands.graph.personal

libs/commands/graph/personal.py

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

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

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

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

Arguments:
  • m (MessageParserProtocol): メッセージデータ
def get_data(df: pandas.Series, interval: int) -> pandas.DataFrame:
284def get_data(df: pd.Series, interval: int) -> pd.DataFrame:
285    """
286    データフレームを指定範囲で分割する
287
288    Args:
289        df (pd.Series): 分割するデータ
290        interval (int): 1ブロックに収めるデータ数
291
292    Returns:
293        pd.DataFrame: 分割されたデータ
294
295    """
296    # interval単位で分割
297    rpoint_data: dict[str, Any] = {}
298
299    fraction = 0 if not len(df) % interval else interval - len(df) % interval  # 端数
300    if fraction:
301        padding = pd.Series([float("nan")] * fraction, dtype="float64")
302        df = pd.concat([padding, df.astype("float64", copy=False)], ignore_index=True)
303
304    for x in range(int(len(df) / interval)):
305        s = len(df) % interval + interval * x
306        e = s + interval
307        rpoint_data[f"{max(1, s + 1 - fraction):3d}G - {(e - fraction):3d}G"] = df.iloc[s:e].tolist()
308
309    return pd.DataFrame(rpoint_data)

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

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

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

def subplot_box(df: pandas.DataFrame, ax: matplotlib.axes._axes.Axes) -> None:
312def subplot_box(df: pd.DataFrame, ax: Axes) -> None:
313    """
314    箱ひげ図を生成する
315
316    Args:
317        df (pd.DataFrame): プロットデータ
318        ax (plt.Axes): プロット先オブジェクト
319
320    """
321    p = [x + 1 for x in range(len(df.columns))]
322    df.plot(
323        ax=ax,
324        kind="box",
325        title="素点分布",
326        showmeans=True,
327        meanprops={"marker": "x", "markeredgecolor": "b", "markerfacecolor": "b", "ms": 3},
328        flierprops={"marker": ".", "markeredgecolor": "r"},
329        ylabel="素点(点)",
330        sharex=True,
331    )
332    ax.axhline(y=25000, linewidth=0.5, ls="dashed", color="grey")
333    ax.set_xticks(p)
334    ax.set_xticklabels(df.columns, rotation=45, ha="right")
335
336    # Y軸修正
337    ylabs = ax.get_yticks()[1:-1]
338    ax.set_yticks(ylabs)
339    ax.set_yticklabels([str(int(ylab)).replace("-", "▲") for ylab in ylabs])

箱ひげ図を生成する

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

テーブルを生成する

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

ポイントデータ

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

順位データ

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

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

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

Path: 保存先ファイルパス

def plotly_rank( df: pandas.DataFrame, title_range: str, total_game_count: int) -> pathlib.Path:
525def plotly_rank(df: pd.DataFrame, title_range: str, total_game_count: int) -> "Path":
526    """
527    獲得順位グラフ(plotly用)
528
529    Args:
530        df (pd.DataFrame): プロットするデータ
531        title_range (str): 集計範囲(タイトル用)
532        total_game_count (int): ゲーム数
533
534    Returns:
535        Path: 保存先ファイルパス
536
537    """
538    save_file = textutil.save_file_path("rank.html")
539
540    fig = go.Figure()
541    fig.add_trace(
542        go.Scatter(
543            name="獲得順位",
544            zorder=1,
545            mode="lines",
546            x=df["playtime"],
547            y=df["rank"],
548        ),
549    )
550    fig.add_trace(
551        go.Scatter(
552            name="平均順位",
553            zorder=2,
554            mode="lines",
555            x=df["playtime"],
556            y=df["rank_avg"],
557            line={"width": 5},
558        ),
559    )
560
561    fig.update_layout(
562        title={
563            "text": f"獲得順位 {title_range}",
564            "font": {"size": 30},
565            "xref": "paper",
566            "xanchor": "center",
567            "x": 0.5,
568        },
569        xaxis_title={
570            "text": graphutil.gen_xlabel(total_game_count),
571            "font": {"size": 18},
572        },
573        legend_title=None,
574        legend={
575            "itemclick": "toggleothers",
576            "itemdoubleclick": "toggle",
577        },
578    )
579
580    fig.update_yaxes(
581        title={
582            "text": "順位",
583            "font": {"size": 18, "color": "white"},
584        },
585        range=[g.params.mode + 0.2, 0.8],
586        tickvals=list(range(1, g.params.mode + 1))[::-1],
587        zeroline=False,
588    )
589
590    fig.add_hline(
591        y=(1 + g.params.mode) / 2,
592        line_dash="dot",
593        line_color="white",
594        line_width=2,
595        layer="below",
596    )
597
598    fig.write_html(save_file, full_html=False)
599    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.Series) -> pathlib.Path:
602def plotly_line(title_text: str, df: pd.Series) -> "Path":
603    """
604    通算ポイント推移グラフ生成(plotly用)
605
606    Args:
607        title_text (str): グラフタイトル
608        df (pd.DataFrame): プロットするデータ
609
610    Returns:
611        Path: 保存先ファイルパス
612
613    """
614    save_file = textutil.save_file_path("point.html")
615
616    fig = go.Figure()
617    fig.add_traces(
618        go.Scatter(
619            mode="lines+markers",
620            x=df.index,
621            y=df.values,
622        ),
623    )
624
625    fig.update_layout(
626        title={
627            "text": title_text,
628            "font": {"size": 30},
629            "xref": "paper",
630            "xanchor": "center",
631            "x": 0.5,
632        },
633        xaxis_title={
634            "text": "ゲーム区間",
635            "font": {"size": 18},
636        },
637        yaxis_title={
638            "text": "ポイント(pt)",
639            "font": {"size": 18},
640        },
641        showlegend=False,
642    )
643    fig.update_yaxes(
644        tickformat="d",
645    )
646
647    fig.write_html(save_file, full_html=False)
648    return save_file

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

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

Path: 保存先ファイルパス

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

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

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

Path: 保存先ファイルパス

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

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

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

Path: 保存先ファイルパス