libs.commands.graph.rating

libs/commands/graph/rating.py

  1"""
  2libs/commands/graph/rating.py
  3"""
  4
  5from typing import TYPE_CHECKING
  6
  7import matplotlib.pyplot as plt
  8import plotly.express as px  # type: ignore
  9
 10import libs.global_value as g
 11from libs.domain import aggregate
 12from libs.domain.datamodels import GameInfo
 13from libs.functions import message
 14from libs.functions.compose import text_item
 15from libs.types import StyleOptions
 16from libs.utils import formatter, graphutil, textutil
 17from libs.utils.timekit import Format
 18
 19if TYPE_CHECKING:
 20    from pathlib import Path
 21
 22    import pandas as pd
 23
 24    from integrations.protocols import MessageParserProtocol
 25
 26
 27def plot(m: "MessageParserProtocol") -> None:
 28    """
 29    レーティング推移グラフを生成する
 30
 31    Args:
 32        m (MessageParserProtocol): メッセージデータ
 33
 34    """
 35    # 情報ヘッダ
 36    title: str = "レーティング推移グラフ"
 37
 38    if g.params.mode == 3 or g.params.target_mode == 3:  # todo: 未実装
 39        m.set_headline(message.random_reply(m, "not_implemented"), StyleOptions(title=title))
 40        m.status.result = False
 41        return
 42
 43    # --- データ収集
 44    game_info = GameInfo()
 45    df_ratings = aggregate.calculation_rating()
 46
 47    if df_ratings.empty:
 48        m.set_headline(message.random_reply(m, "no_hits"), StyleOptions())
 49        m.status.result = False
 50        return
 51
 52    # 足切り
 53    df_count = (
 54        g.params.read_data("SUMMARY_GAMEDATA")
 55        .filter(
 56            items=["name", "count"],
 57        )
 58        .set_index("name")
 59        .query("count >= @g.params.stipulated")
 60    )
 61    df_dropped = df_ratings.filter(items=df_count.index.to_list())
 62
 63    # 並び変え
 64    df_sorted = df_dropped[df_dropped.iloc[-1].sort_values(ascending=False).index]
 65
 66    new_index = {}
 67    for x in df_sorted[1:].index:
 68        new_index[x] = str(x).replace("-", "/")
 69    df_sorted = df_sorted.rename(index=new_index)
 70    df_sorted.ffill(inplace=True)
 71
 72    if g.params.anonymous:
 73        mapping_dict = formatter.anonymous_mapping(df_sorted.columns.to_list())
 74        df_sorted = df_sorted.rename(columns=mapping_dict)
 75
 76    if df_sorted.empty:
 77        m.set_headline(message.random_reply(m, "no_hits"), StyleOptions())
 78        m.status.result = False
 79        return
 80
 81    # --- グラフ生成
 82    graphutil.setup()
 83    m.set_headline(message.header(game_info, m), StyleOptions(title=title))
 84    match g.adapter.conf.plotting_backend:
 85        case "matplotlib":
 86            save_file = _graph_generation(game_info, df_sorted, "rating.png")
 87        case "plotly":
 88            save_file = _graph_generation_plotly(game_info, df_sorted, "rating.html")
 89
 90    m.set_message(save_file, StyleOptions(title=title, use_comment=True, header_hidden=True, key_title=False))
 91
 92
 93def _graph_generation(game_info: GameInfo, df: "pd.DataFrame", filename: str) -> "Path":
 94    """
 95    レーティング推移グラフ生成(matplotlib)
 96
 97    Args:
 98        game_info (GameInfo): ゲーム情報
 99        df (pd.DataFrame): 描写データ
100        filename (str): 保存先デフォルトファイル名
101
102    Returns:
103        Path: 保存先ファイル名
104
105    """
106    save_file = textutil.save_file_path(filename)
107    title_text, xlabel_text = _graph_title(game_info)
108    legend_text = []
109    count = 1
110    for name, rate in df.iloc[-1].sort_values(ascending=False).items():
111        legend_text.append(f"{count:2d}位:{name}{rate:.1f})")
112        count += 1
113
114    # ---
115    df.plot(
116        figsize=(21, 7),
117        xlabel=xlabel_text,
118        ylabel="レート",
119        marker="." if len(df) < 20 else None,
120        linewidth=2 if len(df) < 40 else 1,
121    )
122    plt.title(title_text, fontsize=16)
123    plt.legend(
124        legend_text,
125        bbox_to_anchor=(1, 1),
126        loc="upper left",
127        borderaxespad=0.5,
128        ncol=int(len(df.columns) / 25 + 1),
129    )
130
131    plt.xticks(**graphutil.xticks_parameter(df[1:].index.to_list()))
132    plt.axhline(y=1500, linewidth=0.5, ls="dashed", color="grey")
133
134    plt.savefig(save_file, bbox_inches="tight")
135    return save_file
136
137
138def _graph_generation_plotly(game_info: GameInfo, df: "pd.DataFrame", filename: str) -> "Path":
139    """
140    レーティング推移グラフ生成(plotly)
141
142    Args:
143        game_info (GameInfo): ゲーム情報
144        df (pd.DataFrame): 描写データ
145        filename (str): 保存先デフォルトファイル名
146
147    Returns:
148        Path: 保存先ファイル名
149
150    """
151    save_file = textutil.save_file_path(filename)
152    # グラフタイトル/ラベル
153    title_text, xlabel_text = _graph_title(game_info)
154    # 凡例用テキスト
155    legend_text = []
156    count = 1
157    for name, rate in df.iloc[-1].sort_values(ascending=False).items():
158        legend_text.append(f"{count:2d}位:{name}{rate:.1f})")
159        count += 1
160
161    df.rename(index={"initial_rating": ""}, inplace=True)
162    df.columns = legend_text
163
164    fig = px.line(df, markers=True)
165
166    # グラフレイアウト調整
167    fig.update_layout(
168        width=1280,
169        height=800,
170        title={
171            "text": title_text,
172            "font": {"size": 30},
173            "x": 0.1,
174        },
175        xaxis_title={
176            "text": xlabel_text,
177            "font": {"size": 18},
178        },
179        yaxis_title={
180            "text": "レート",
181            "font": {"size": 18},
182        },
183        legend_title=None,
184        legend={
185            "itemclick": "toggleothers",
186            "itemdoubleclick": "toggle",
187        },
188    )
189
190    # 軸/目盛調整
191    if all(df.count() > 20):
192        fig.update_traces(mode="lines")  # マーカー非表示
193    if len(fig.data) > 40:
194        fig.update_traces(mode="lines", line={"width": 1})  # ラインを細く
195
196    fig.write_html(save_file, full_html=False)
197    return save_file
198
199
200def _graph_title(game_info: GameInfo) -> tuple[str, str]:
201    """
202    グラフタイトル/ラベル生成
203
204    Args:
205        game_info (GameInfo): ゲームデータ
206
207    Returns:
208        tuple[str, str]: タイトル文字列
209
210    """
211    match g.params.collection:
212        case "daily":
213            kind = Format.YMD_O
214            xlabel_text = f"集計日(総ゲーム数:{game_info.count} ゲーム)"
215        case "monthly":
216            kind = Format.JYM_O
217            xlabel_text = f"集計月(総ゲーム数:{game_info.count} ゲーム)"
218        case "yearly":
219            kind = Format.JY_O
220            xlabel_text = f"集計年(総ゲーム数:{game_info.count} ゲーム)"
221        case "all":
222            kind = Format.YMDHM
223            xlabel_text = f"総ゲーム数:{game_info.count} ゲーム"
224        case _:
225            kind = Format.YMDHM
226            xlabel_text = f"ゲーム終了日時(総ゲーム数:{game_info.count} ゲーム)"
227
228    title_text = f"レーティング推移 ({text_item.date_range(kind)})"
229
230    return title_text, xlabel_text
def plot(m: integrations.protocols.MessageParserProtocol) -> None:
28def plot(m: "MessageParserProtocol") -> None:
29    """
30    レーティング推移グラフを生成する
31
32    Args:
33        m (MessageParserProtocol): メッセージデータ
34
35    """
36    # 情報ヘッダ
37    title: str = "レーティング推移グラフ"
38
39    if g.params.mode == 3 or g.params.target_mode == 3:  # todo: 未実装
40        m.set_headline(message.random_reply(m, "not_implemented"), StyleOptions(title=title))
41        m.status.result = False
42        return
43
44    # --- データ収集
45    game_info = GameInfo()
46    df_ratings = aggregate.calculation_rating()
47
48    if df_ratings.empty:
49        m.set_headline(message.random_reply(m, "no_hits"), StyleOptions())
50        m.status.result = False
51        return
52
53    # 足切り
54    df_count = (
55        g.params.read_data("SUMMARY_GAMEDATA")
56        .filter(
57            items=["name", "count"],
58        )
59        .set_index("name")
60        .query("count >= @g.params.stipulated")
61    )
62    df_dropped = df_ratings.filter(items=df_count.index.to_list())
63
64    # 並び変え
65    df_sorted = df_dropped[df_dropped.iloc[-1].sort_values(ascending=False).index]
66
67    new_index = {}
68    for x in df_sorted[1:].index:
69        new_index[x] = str(x).replace("-", "/")
70    df_sorted = df_sorted.rename(index=new_index)
71    df_sorted.ffill(inplace=True)
72
73    if g.params.anonymous:
74        mapping_dict = formatter.anonymous_mapping(df_sorted.columns.to_list())
75        df_sorted = df_sorted.rename(columns=mapping_dict)
76
77    if df_sorted.empty:
78        m.set_headline(message.random_reply(m, "no_hits"), StyleOptions())
79        m.status.result = False
80        return
81
82    # --- グラフ生成
83    graphutil.setup()
84    m.set_headline(message.header(game_info, m), StyleOptions(title=title))
85    match g.adapter.conf.plotting_backend:
86        case "matplotlib":
87            save_file = _graph_generation(game_info, df_sorted, "rating.png")
88        case "plotly":
89            save_file = _graph_generation_plotly(game_info, df_sorted, "rating.html")
90
91    m.set_message(save_file, StyleOptions(title=title, use_comment=True, header_hidden=True, key_title=False))

レーティング推移グラフを生成する

Arguments:
  • m (MessageParserProtocol): メッセージデータ