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
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): メッセージデータ