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