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
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): メッセージデータ
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: 保存先ファイルパス