libs.commands.report.stats_report
libs/commands/report/results_report.py
1""" 2libs/commands/report/results_report.py 3""" 4 5import logging 6import os 7from datetime import datetime 8from io import BytesIO 9from typing import TYPE_CHECKING, Any, Literal 10 11import matplotlib.font_manager as fm 12import matplotlib.pyplot as plt 13import pandas as pd 14from reportlab.lib import colors 15from reportlab.lib.enums import TA_LEFT, TA_RIGHT 16from reportlab.lib.pagesizes import A4, landscape 17from reportlab.lib.styles import ParagraphStyle 18from reportlab.lib.units import mm 19from reportlab.pdfbase import pdfmetrics 20from reportlab.pdfbase.ttfonts import TTFont 21from reportlab.platypus import Image, LongTable, PageBreak, Paragraph, SimpleDocTemplate, Spacer, TableStyle 22 23import libs.global_value as g 24from libs.functions import lookup, message 25from libs.types import StyleOptions 26from libs.utils import dbutil, formatter 27 28if TYPE_CHECKING: 29 from integrations.protocols import MessageParserProtocol 30 31 32def get_game_results() -> list[list[str]]: 33 """ 34 月/年単位のゲーム結果集計 35 36 Returns: 37 list[list[str]]: 集計結果のリスト 38 39 """ 40 resultdb = dbutil.connection(g.cfg.setting.database_file) 41 rows = resultdb.execute( 42 g.params.query_modification(dbutil.query("REPORT_PERSONAL_DATA")), 43 g.params.placeholder(), 44 ) 45 46 # --- データ収集 47 results: list[list[str]] = [ 48 [ 49 "", 50 "ゲーム数", 51 "通算\nポイント", 52 "平均\nポイント", 53 "1位", 54 "", 55 "2位", 56 "", 57 "3位", 58 "", 59 "4位", 60 "", 61 "平均\n順位", 62 "トビ", 63 "", 64 ] 65 ] 66 67 for row in rows.fetchall(): 68 if row["ゲーム数"] == 0: 69 break 70 71 results.append( 72 [ 73 row["集計"], 74 row["ゲーム数"], 75 str(row["通算ポイント"]).replace("-", "▲") + "pt", 76 str(row["平均ポイント"]).replace("-", "▲") + "pt", 77 row["1位"], 78 f"{row['1位率']:.2f}%", 79 row["2位"], 80 f"{row['2位率']:.2f}%", 81 row["3位"], 82 f"{row['3位率']:.2f}%", 83 row["4位"], 84 f"{row['4位率']:.2f}%", 85 f"{row['平均順位']:.2f}", 86 row["トビ"], 87 f"{row['トビ率']:.2f}%", 88 ] 89 ) 90 logging.debug("return record: %s", len(results)) 91 resultdb.close() 92 93 if len(results) == 1: # ヘッダのみ 94 return [] 95 96 return results 97 98 99def get_count_results(game_count: int) -> list[list[str]]: 100 """ 101 指定間隔区切りのゲーム結果集計 102 103 Args: 104 game_count (int): 区切るゲーム数 105 106 Returns: 107 list[list[str]]: 集計結果のリスト 108 109 """ 110 g.params.interval = game_count 111 resultdb = dbutil.connection(g.cfg.setting.database_file) 112 rows = resultdb.execute( 113 g.params.query_modification(dbutil.query("REPORT_COUNT_DATA")), 114 g.params.placeholder(), 115 ) 116 117 # --- データ収集 118 results = [ 119 [ 120 "開始", 121 "終了", 122 "ゲーム数", 123 "通算\nポイント", 124 "平均\nポイント", 125 "1位", 126 "", 127 "2位", 128 "", 129 "3位", 130 "", 131 "4位", 132 "", 133 "平均\n順位", 134 "トビ", 135 "", 136 ] 137 ] 138 139 for row in rows.fetchall(): 140 if row["ゲーム数"] == 0: 141 break 142 143 results.append( 144 [ 145 row["開始"], 146 row["終了"], 147 row["ゲーム数"], 148 str(row["通算ポイント"]).replace("-", "▲") + "pt", 149 str(row["平均ポイント"]).replace("-", "▲") + "pt", 150 row["1位"], 151 f"{row['1位率']:.2f}%", 152 row["2位"], 153 f"{row['2位率']:.2f}%", 154 row["3位"], 155 f"{row['3位率']:.2f}%", 156 row["4位"], 157 f"{row['4位率']:.2f}%", 158 f"{row['平均順位']:.2f}", 159 row["トビ"], 160 f"{row['トビ率']:.2f}%", 161 ] 162 ) 163 logging.debug("return record: %s", len(results)) 164 resultdb.close() 165 166 if len(results) == 1: # ヘッダのみ 167 return [] 168 169 return results 170 171 172def get_count_moving(game_count: int) -> list[dict[str, Any]]: 173 """ 174 移動平均を取得する 175 176 Args: 177 game_count (int): 平滑化するゲーム数 178 179 Returns: 180 list[dict[str, Any]]: 集計結果のリスト 181 182 """ 183 resultdb = dbutil.connection(g.cfg.setting.database_file) 184 g.params.interval = game_count 185 rows = resultdb.execute( 186 g.params.query_modification(dbutil.query("REPORT_COUNT_MOVING")), 187 g.params.placeholder(), 188 ) 189 190 # --- データ収集 191 results = [] 192 for row in rows.fetchall(): 193 results.append(dict(row)) 194 195 logging.debug("return record: %s", len(results)) 196 resultdb.close() 197 198 return results 199 200 201def graphing_mean_rank(df: pd.DataFrame, title: str, whole: bool = False) -> BytesIO: 202 """ 203 平均順位の折れ線グラフを生成 204 205 Args: 206 df (pd.DataFrame): 描写データ 207 title (str): グラフタイトル 208 whole (bool, optional): 集計種別. Defaults to False. 209 - *True*: 全体集計 210 - *False*: 指定範囲集計 211 212 Returns: 213 BytesIO: 画像データ 214 215 """ 216 imgdata = BytesIO() 217 218 if whole: 219 df.plot( 220 kind="line", 221 figsize=(12, 5), 222 fontsize=14, 223 ) 224 plt.legend( 225 title="開始 - 終了", 226 ncol=int(len(df.columns) / 5) + 1, 227 ) 228 else: 229 df.plot( 230 kind="line", 231 y="rank_avg", 232 x="game_no", 233 legend=False, 234 figsize=(12, 5), 235 fontsize=14, 236 ) 237 238 plt.title(title, fontsize=18) 239 plt.grid(axis="y") 240 241 # Y軸設定 242 plt.ylabel("平均順位", fontsize=14) 243 plt.yticks([4.0, 3.5, 3.0, 2.5, 2.0, 1.5, 1.0]) 244 for ax in plt.gcf().get_axes(): # 逆向きにする 245 ax.invert_yaxis() 246 247 # X軸設定 248 plt.xlabel("ゲーム数", fontsize=14) 249 250 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 251 return imgdata 252 253 254def graphing_total_points(df: pd.DataFrame, title: str, whole: bool = False) -> BytesIO: 255 """ 256 通算ポイント推移の折れ線グラフを生成 257 258 Args: 259 df (pd.DataFrame): 描写データ 260 title (str): グラフタイトル 261 whole (bool, optional): 集計種別. Defaults to False. 262 - *True*: 全体集計 / 移動平均付き 263 - *False*: 指定範囲集計 264 Returns: 265 BytesIO: 画像データ 266 267 """ 268 imgdata = BytesIO() 269 270 if whole: 271 df.plot( 272 kind="line", 273 figsize=(12, 8), 274 fontsize=14, 275 ) 276 plt.legend( 277 title="通算 ( 開始 - 終了 )", 278 ncol=int(len(df.columns) / 5) + 1, 279 ) 280 else: 281 point_sum = df.plot( 282 kind="line", 283 y="point_sum", 284 label="通算", 285 figsize=(12, 8), 286 fontsize=14, 287 ) 288 if len(df) > 50: 289 point_sum = ( 290 df["point_sum"] 291 .rolling(40) 292 .mean() 293 .plot( 294 kind="line", 295 label="移動平均(40ゲーム)", 296 ax=point_sum, 297 ) 298 ) 299 if len(df) > 100: 300 point_sum = ( 301 df["point_sum"] 302 .rolling(80) 303 .mean() 304 .plot( 305 kind="line", 306 label="移動平均(80ゲーム)", 307 ax=point_sum, 308 ) 309 ) 310 plt.legend() 311 312 plt.title(title, fontsize=18) 313 plt.grid(axis="y") 314 315 # Y軸設定 316 plt.ylabel("ポイント", fontsize=14) 317 ylocs, ylabs = plt.yticks() 318 new_ylabs = [ylab.get_text().replace("−", "▲") for ylab in ylabs] 319 plt.yticks(list(ylocs[1:-1]), new_ylabs[1:-1]) 320 321 # X軸設定 322 plt.xlabel("ゲーム数", fontsize=14) 323 324 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 325 return imgdata 326 327 328def graphing_rank_distribution(df: pd.DataFrame, title: str) -> BytesIO: 329 """ 330 順位分布の棒グラフを生成 331 332 Args: 333 df (pd.DataFrame): 描写データ 334 title (str): グラフタイトル 335 336 Returns: 337 BytesIO: 画像データ 338 339 """ 340 imgdata = BytesIO() 341 342 df.plot( 343 kind="bar", 344 stacked=True, 345 figsize=(12, 7), 346 fontsize=14, 347 ) 348 349 plt.title(title, fontsize=18) 350 plt.legend( 351 bbox_to_anchor=(0.5, 0), 352 loc="lower center", 353 ncol=4, 354 fontsize=12, 355 ) 356 357 # Y軸設定 358 plt.yticks([0, 25, 50, 75, 100]) 359 plt.ylabel("(%)", fontsize=14) 360 for ax in plt.gcf().get_axes(): # グリッド線を背後にまわす 361 ax.set_axisbelow(True) 362 plt.grid(axis="y") 363 364 # X軸設定 365 if len(df) > 10: 366 plt.xticks(rotation=30, ha="right") 367 else: 368 plt.xticks(rotation=30) 369 370 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 371 return imgdata 372 373 374def gen_pdf(m: "MessageParserProtocol") -> None: 375 """ 376 成績レポートを生成する 377 378 Args: 379 m (MessageParserProtocol): メッセージデータ 380 381 """ 382 if g.adapter.conf.plotting_backend == "plotly": 383 m.post.reset() 384 m.set_headline(message.random_reply(m, "not_implemented"), StyleOptions()) 385 return 386 387 if not g.params.player_name: # レポート対象の指定なし 388 m.set_headline(message.random_reply(m, "no_target"), StyleOptions(title="成績レポート")) 389 m.status.result = False 390 return 391 392 # 対象メンバーの記録状況 393 target_info = lookup.member_info(g.params.placeholder()) 394 logging.debug(target_info) 395 396 if not target_info["game_count"]: # 記録なし 397 m.set_headline(message.random_reply(m, "no_hits"), StyleOptions(title="成績レポート")) 398 m.status.result = False 399 return 400 401 # 書式設定 402 font_path = os.path.join(os.path.realpath(os.path.curdir), g.cfg.setting.font_file) 403 pdf_path = g.cfg.setting.work_dir / (f"{g.params.filename}.pdf" if g.params.filename else "results.pdf") 404 pdfmetrics.registerFont(TTFont("ReportFont", font_path)) 405 406 doc = SimpleDocTemplate( 407 str(pdf_path), 408 pagesize=landscape(A4), 409 topMargin=10.0 * mm, 410 bottomMargin=10.0 * mm, 411 # leftMargin=1.5 * mm, 412 # rightMargin=1.5 * mm, 413 ) 414 415 style: dict[str, Any] = {} 416 style["Title"] = ParagraphStyle(name="Title", fontName="ReportFont", fontSize=24) 417 style["Normal"] = ParagraphStyle(name="Normal", fontName="ReportFont", fontSize=14) 418 style["Left"] = ParagraphStyle(name="Left", fontName="ReportFont", fontSize=14, alignment=TA_LEFT) 419 style["Right"] = ParagraphStyle(name="Right", fontName="ReportFont", fontSize=14, alignment=TA_RIGHT) 420 421 plt.rcdefaults() 422 font_prop = fm.FontProperties(fname=font_path) 423 plt.rcParams["font.family"] = font_prop.get_name() 424 fm.fontManager.addfont(font_path) 425 426 # レポート作成 427 elements: list[Any] = [] 428 elements.extend(cover_page(style, target_info)) # 表紙 429 elements.extend(entire_aggregate(style)) # 全期間 430 elements.extend(periodic_aggregation(style)) # 期間集計 431 elements.extend(sectional_aggregate(style, target_info)) # 区間集計 432 433 doc.build(elements) 434 logging.debug("report generation: %s", g.params.player_name) 435 436 m.set_message(pdf_path, StyleOptions(title=f"成績レポート({g.params.player_name})", use_comment=True, header_hidden=True)) 437 438 439def cover_page(style: dict[str, Any], target_info: dict[str, Any]) -> list[Any]: 440 """ 441 表紙生成 442 443 Args: 444 style (dict[str, Any]): レイアウトスタイル 445 target_info (dict[str, Any]): プレイヤー情報 446 447 Returns: 448 list[Any]: 生成内容 449 450 """ 451 elements: list[Any] = [] 452 453 first_game = datetime.fromtimestamp( # 最初のゲーム日時 454 float(target_info["first_game"]) 455 ) 456 last_game = datetime.fromtimestamp( # 最後のゲーム日時 457 float(target_info["last_game"]) 458 ) 459 460 if g.params.anonymous: 461 mapping_dict = formatter.anonymous_mapping([g.params.player_name]) 462 target_player = next(iter(mapping_dict.values())) 463 else: 464 target_player = g.params.player_name 465 466 # 表紙 467 elements.append(Spacer(1, 40 * mm)) 468 elements.append(Paragraph(f"成績レポート:{target_player}", style["Title"])) 469 elements.append(Spacer(1, 10 * mm)) 470 elements.append( 471 Paragraph( 472 f"集計期間:{first_game.strftime('%Y-%m-%d %H:%M')} - {last_game.strftime('%Y-%m-%d %H:%M')}", 473 style["Normal"], 474 ) 475 ) 476 elements.append(Spacer(1, 100 * mm)) 477 elements.append(Paragraph(f"作成日:{datetime.now().strftime('%Y-%m-%d')}", style["Right"])) 478 elements.append(PageBreak()) 479 480 return elements 481 482 483def entire_aggregate(style: dict[str, Any]) -> list[Any]: 484 """ 485 全期間 486 487 Args: 488 style (dict[str, Any]): レイアウトスタイル 489 490 Returns: 491 list[Any]: 生成内容 492 493 """ 494 elements: list[Any] = [] 495 496 elements.append(Paragraph("全期間", style["Left"])) 497 elements.append(Spacer(1, 5 * mm)) 498 data: list[list[str]] = [] 499 g.params.aggregate_unit = "A" 500 tmp_data = get_game_results() 501 502 if not tmp_data: 503 return [] 504 505 for _, val in enumerate(tmp_data): # ゲーム数を除外 506 data.append(val[1:]) 507 tt = LongTable(data, repeatRows=1) 508 tt.setStyle( 509 TableStyle( 510 [ 511 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 512 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 513 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 514 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 515 ("SPAN", (3, 0), (4, 0)), 516 ("SPAN", (5, 0), (6, 0)), 517 ("SPAN", (7, 0), (8, 0)), 518 ("SPAN", (9, 0), (10, 0)), 519 ("SPAN", (12, 0), (13, 0)), 520 # ヘッダ行 521 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 522 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 523 ] 524 ) 525 ) 526 elements.append(tt) 527 528 # 順位分布 529 imgdata = BytesIO() 530 gdata = pd.DataFrame( 531 { 532 "順位分布": [ 533 float(str(data[1][4]).replace("%", "")), 534 float(str(data[1][6]).replace("%", "")), 535 float(str(data[1][8]).replace("%", "")), 536 float(str(data[1][10]).replace("%", "")), 537 ], 538 }, 539 index=["1位率", "2位率", "3位率", "4位率"], 540 ) 541 gdata.plot( 542 kind="pie", 543 y="順位分布", 544 labels=None, 545 figsize=(6, 6), 546 fontsize=14, 547 autopct="%.2f%%", 548 wedgeprops={"linewidth": 1, "edgecolor": "white"}, 549 ) 550 plt.title("順位分布 ( 全期間 )", fontsize=18) 551 plt.ylabel("") 552 plt.legend( 553 labels=list(gdata.index), 554 bbox_to_anchor=(0.5, -0.1), 555 loc="lower center", 556 ncol=4, 557 fontsize=12, 558 ) 559 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 560 561 elements.append(Spacer(1, 5 * mm)) 562 elements.append(Image(imgdata, width=600 * 0.5, height=600 * 0.5)) 563 564 df = pd.DataFrame(get_count_moving(0)) 565 df["playtime"] = pd.to_datetime(df["playtime"]) 566 567 # 通算ポイント推移 568 imgdata = graphing_total_points(df, "通算ポイント推移 ( 全期間 )", False) 569 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 570 571 # 平均順位 572 imgdata = graphing_mean_rank(df, "平均順位推移 ( 全期間 )", False) 573 elements.append(Image(imgdata, width=1200 * 0.5, height=500 * 0.5)) 574 575 elements.append(PageBreak()) 576 577 return elements 578 579 580def periodic_aggregation(style: dict[str, Any]) -> list[Any]: 581 """ 582 期間集計 583 584 Args: 585 style (dict[str, Any]): レイアウトスタイル 586 587 Returns: 588 list[Any]: 生成内容 589 590 """ 591 elements: list[Any] = [] 592 593 pattern: list[tuple[str, str, Literal["A", "M", "Y"]]] = [ 594 # 表タイトル, グラフタイトル, フラグ 595 ("月別集計", "順位分布(月別)", "M"), 596 ("年別集計", "順位分布(年別)", "Y"), 597 ] 598 599 for table_title, graph_title, flag in pattern: 600 elements.append(Paragraph(table_title, style["Left"])) 601 elements.append(Spacer(1, 5 * mm)) 602 603 data: list[list[str]] = [] 604 g.params.aggregate_unit = flag 605 tmp_data = get_game_results() 606 607 if not tmp_data: 608 return [] 609 610 for _, val in enumerate(tmp_data): # 日時を除外 611 data.append(val[:15]) 612 613 tt = LongTable(data, repeatRows=1) 614 ts = TableStyle( 615 [ 616 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 617 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 618 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 619 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 620 ("SPAN", (4, 0), (5, 0)), 621 ("SPAN", (6, 0), (7, 0)), 622 ("SPAN", (8, 0), (9, 0)), 623 ("SPAN", (10, 0), (11, 0)), 624 ("SPAN", (13, 0), (14, 0)), 625 # ヘッダ行 626 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 627 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 628 ] 629 ) 630 631 if len(data) > 4: 632 for i in range(len(data) - 2): 633 if i % 2 == 0: 634 ts.add("BACKGROUND", (0, i + 2), (-1, i + 2), colors.lightgrey) 635 tt.setStyle(ts) 636 elements.append(tt) 637 elements.append(Spacer(1, 10 * mm)) 638 639 # 順位分布 640 df = pd.DataFrame( 641 { 642 "1位率": [float(str(data[x + 1][5]).replace("%", "")) for x in range(len(data) - 1)], 643 "2位率": [float(str(data[x + 1][7]).replace("%", "")) for x in range(len(data) - 1)], 644 "3位率": [float(str(data[x + 1][9]).replace("%", "")) for x in range(len(data) - 1)], 645 "4位率": [float(str(data[x + 1][11]).replace("%", "")) for x in range(len(data) - 1)], 646 }, 647 index=[data[x + 1][0] for x in range(len(data) - 1)], 648 ) 649 650 imgdata = graphing_rank_distribution(df, graph_title) 651 elements.append(Spacer(1, 5 * mm)) 652 elements.append(Image(imgdata, width=1200 * 0.5, height=700 * 0.5)) 653 654 elements.append(PageBreak()) 655 656 return elements 657 658 659def sectional_aggregate(style: dict[str, Any], target_info: dict[str, Any]) -> list[Any]: 660 """ 661 区間集計 662 663 Args: 664 style (dict[str, Any]): レイアウトスタイル 665 target_info (dict[str, Any]): プレイヤー情報 666 667 Returns: 668 list[Any]: 生成内容 669 670 """ 671 elements: list[Any] = [] 672 673 pattern: list[tuple[int, int, str]] = [ 674 # 区切り回数, 閾値, タイトル 675 (80, 100, "短期"), 676 (200, 240, "中期"), 677 (400, 500, "長期"), 678 ] 679 680 for count, threshold, title in pattern: 681 if target_info["game_count"] > threshold: 682 # テーブル 683 elements.append(Paragraph(f"区間集計 ( {title} )", style["Left"])) 684 elements.append(Spacer(1, 5 * mm)) 685 data = get_count_results(count) 686 687 if not data: 688 return [] 689 690 tt = LongTable(data, repeatRows=1) 691 ts = TableStyle( 692 [ 693 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 694 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 695 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 696 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 697 ("SPAN", (5, 0), (6, 0)), 698 ("SPAN", (7, 0), (8, 0)), 699 ("SPAN", (9, 0), (10, 0)), 700 ("SPAN", (11, 0), (12, 0)), 701 ("SPAN", (14, 0), (15, 0)), 702 # ヘッダ行 703 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 704 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 705 ] 706 ) 707 if len(data) > 4: 708 for i in range(len(data) - 2): 709 if i % 2 == 0: 710 ts.add("BACKGROUND", (0, i + 2), (-1, i + 2), colors.lightgrey) 711 tt.setStyle(ts) 712 elements.append(tt) 713 714 # 順位分布 715 df = pd.DataFrame( 716 { 717 "1位率": [float(str(data[x + 1][6]).replace("%", "")) for x in range(len(data) - 1)], 718 "2位率": [float(str(data[x + 1][8]).replace("%", "")) for x in range(len(data) - 1)], 719 "3位率": [float(str(data[x + 1][10]).replace("%", "")) for x in range(len(data) - 1)], 720 "4位率": [float(str(data[x + 1][12]).replace("%", "")) for x in range(len(data) - 1)], 721 }, 722 index=[f"{str(data[x + 1][0])} - {str(data[x + 1][1])}" for x in range(len(data) - 1)], 723 ) 724 725 imgdata = graphing_rank_distribution(df, f"順位分布 ( 区間 {title} )") 726 elements.append(Spacer(1, 5 * mm)) 727 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 728 729 # 通算ポイント推移 730 tmp_df = pd.DataFrame(get_count_moving(count)) 731 df = pd.DataFrame() 732 for i in sorted(tmp_df["interval"].unique().tolist()): 733 list_data = tmp_df[tmp_df.interval == i]["point_sum"].to_list() 734 game_count = tmp_df[tmp_df.interval == i]["total_count"].to_list() 735 df[f"{min(game_count)} - {max(game_count)}"] = [None] * (count - len(list_data)) + list_data 736 737 imgdata = graphing_total_points(df, f"通算ポイント推移(区間 {title})", True) 738 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 739 740 # 平均順位 741 df = pd.DataFrame() 742 for i in sorted(tmp_df["interval"].unique().tolist()): 743 list_data = tmp_df[tmp_df.interval == i]["rank_avg"].to_list() 744 game_count = tmp_df[tmp_df.interval == i]["total_count"].to_list() 745 df[f"{min(game_count)} - {max(game_count)}"] = [None] * (count - len(list_data)) + list_data 746 747 imgdata = graphing_mean_rank(df, f"平均順位推移(区間 {title})", True) 748 elements.append(Image(imgdata, width=1200 * 0.5, height=500 * 0.5)) 749 750 elements.append(PageBreak()) 751 752 return elements
def
get_game_results() -> list[list[str]]:
33def get_game_results() -> list[list[str]]: 34 """ 35 月/年単位のゲーム結果集計 36 37 Returns: 38 list[list[str]]: 集計結果のリスト 39 40 """ 41 resultdb = dbutil.connection(g.cfg.setting.database_file) 42 rows = resultdb.execute( 43 g.params.query_modification(dbutil.query("REPORT_PERSONAL_DATA")), 44 g.params.placeholder(), 45 ) 46 47 # --- データ収集 48 results: list[list[str]] = [ 49 [ 50 "", 51 "ゲーム数", 52 "通算\nポイント", 53 "平均\nポイント", 54 "1位", 55 "", 56 "2位", 57 "", 58 "3位", 59 "", 60 "4位", 61 "", 62 "平均\n順位", 63 "トビ", 64 "", 65 ] 66 ] 67 68 for row in rows.fetchall(): 69 if row["ゲーム数"] == 0: 70 break 71 72 results.append( 73 [ 74 row["集計"], 75 row["ゲーム数"], 76 str(row["通算ポイント"]).replace("-", "▲") + "pt", 77 str(row["平均ポイント"]).replace("-", "▲") + "pt", 78 row["1位"], 79 f"{row['1位率']:.2f}%", 80 row["2位"], 81 f"{row['2位率']:.2f}%", 82 row["3位"], 83 f"{row['3位率']:.2f}%", 84 row["4位"], 85 f"{row['4位率']:.2f}%", 86 f"{row['平均順位']:.2f}", 87 row["トビ"], 88 f"{row['トビ率']:.2f}%", 89 ] 90 ) 91 logging.debug("return record: %s", len(results)) 92 resultdb.close() 93 94 if len(results) == 1: # ヘッダのみ 95 return [] 96 97 return results
月/年単位のゲーム結果集計
Returns:
list[list[str]]: 集計結果のリスト
def
get_count_results(game_count: int) -> list[list[str]]:
100def get_count_results(game_count: int) -> list[list[str]]: 101 """ 102 指定間隔区切りのゲーム結果集計 103 104 Args: 105 game_count (int): 区切るゲーム数 106 107 Returns: 108 list[list[str]]: 集計結果のリスト 109 110 """ 111 g.params.interval = game_count 112 resultdb = dbutil.connection(g.cfg.setting.database_file) 113 rows = resultdb.execute( 114 g.params.query_modification(dbutil.query("REPORT_COUNT_DATA")), 115 g.params.placeholder(), 116 ) 117 118 # --- データ収集 119 results = [ 120 [ 121 "開始", 122 "終了", 123 "ゲーム数", 124 "通算\nポイント", 125 "平均\nポイント", 126 "1位", 127 "", 128 "2位", 129 "", 130 "3位", 131 "", 132 "4位", 133 "", 134 "平均\n順位", 135 "トビ", 136 "", 137 ] 138 ] 139 140 for row in rows.fetchall(): 141 if row["ゲーム数"] == 0: 142 break 143 144 results.append( 145 [ 146 row["開始"], 147 row["終了"], 148 row["ゲーム数"], 149 str(row["通算ポイント"]).replace("-", "▲") + "pt", 150 str(row["平均ポイント"]).replace("-", "▲") + "pt", 151 row["1位"], 152 f"{row['1位率']:.2f}%", 153 row["2位"], 154 f"{row['2位率']:.2f}%", 155 row["3位"], 156 f"{row['3位率']:.2f}%", 157 row["4位"], 158 f"{row['4位率']:.2f}%", 159 f"{row['平均順位']:.2f}", 160 row["トビ"], 161 f"{row['トビ率']:.2f}%", 162 ] 163 ) 164 logging.debug("return record: %s", len(results)) 165 resultdb.close() 166 167 if len(results) == 1: # ヘッダのみ 168 return [] 169 170 return results
指定間隔区切りのゲーム結果集計
Arguments:
- game_count (int): 区切るゲーム数
Returns:
list[list[str]]: 集計結果のリスト
def
get_count_moving(game_count: int) -> list[dict[str, typing.Any]]:
173def get_count_moving(game_count: int) -> list[dict[str, Any]]: 174 """ 175 移動平均を取得する 176 177 Args: 178 game_count (int): 平滑化するゲーム数 179 180 Returns: 181 list[dict[str, Any]]: 集計結果のリスト 182 183 """ 184 resultdb = dbutil.connection(g.cfg.setting.database_file) 185 g.params.interval = game_count 186 rows = resultdb.execute( 187 g.params.query_modification(dbutil.query("REPORT_COUNT_MOVING")), 188 g.params.placeholder(), 189 ) 190 191 # --- データ収集 192 results = [] 193 for row in rows.fetchall(): 194 results.append(dict(row)) 195 196 logging.debug("return record: %s", len(results)) 197 resultdb.close() 198 199 return results
移動平均を取得する
Arguments:
- game_count (int): 平滑化するゲーム数
Returns:
list[dict[str, Any]]: 集計結果のリスト
def
graphing_mean_rank(df: pandas.DataFrame, title: str, whole: bool = False) -> _io.BytesIO:
202def graphing_mean_rank(df: pd.DataFrame, title: str, whole: bool = False) -> BytesIO: 203 """ 204 平均順位の折れ線グラフを生成 205 206 Args: 207 df (pd.DataFrame): 描写データ 208 title (str): グラフタイトル 209 whole (bool, optional): 集計種別. Defaults to False. 210 - *True*: 全体集計 211 - *False*: 指定範囲集計 212 213 Returns: 214 BytesIO: 画像データ 215 216 """ 217 imgdata = BytesIO() 218 219 if whole: 220 df.plot( 221 kind="line", 222 figsize=(12, 5), 223 fontsize=14, 224 ) 225 plt.legend( 226 title="開始 - 終了", 227 ncol=int(len(df.columns) / 5) + 1, 228 ) 229 else: 230 df.plot( 231 kind="line", 232 y="rank_avg", 233 x="game_no", 234 legend=False, 235 figsize=(12, 5), 236 fontsize=14, 237 ) 238 239 plt.title(title, fontsize=18) 240 plt.grid(axis="y") 241 242 # Y軸設定 243 plt.ylabel("平均順位", fontsize=14) 244 plt.yticks([4.0, 3.5, 3.0, 2.5, 2.0, 1.5, 1.0]) 245 for ax in plt.gcf().get_axes(): # 逆向きにする 246 ax.invert_yaxis() 247 248 # X軸設定 249 plt.xlabel("ゲーム数", fontsize=14) 250 251 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 252 return imgdata
平均順位の折れ線グラフを生成
Arguments:
- df (pd.DataFrame): 描写データ
- title (str): グラフタイトル
- whole (bool, optional): 集計種別. Defaults to False.
- True: 全体集計
- False: 指定範囲集計
Returns:
BytesIO: 画像データ
def
graphing_total_points(df: pandas.DataFrame, title: str, whole: bool = False) -> _io.BytesIO:
255def graphing_total_points(df: pd.DataFrame, title: str, whole: bool = False) -> BytesIO: 256 """ 257 通算ポイント推移の折れ線グラフを生成 258 259 Args: 260 df (pd.DataFrame): 描写データ 261 title (str): グラフタイトル 262 whole (bool, optional): 集計種別. Defaults to False. 263 - *True*: 全体集計 / 移動平均付き 264 - *False*: 指定範囲集計 265 Returns: 266 BytesIO: 画像データ 267 268 """ 269 imgdata = BytesIO() 270 271 if whole: 272 df.plot( 273 kind="line", 274 figsize=(12, 8), 275 fontsize=14, 276 ) 277 plt.legend( 278 title="通算 ( 開始 - 終了 )", 279 ncol=int(len(df.columns) / 5) + 1, 280 ) 281 else: 282 point_sum = df.plot( 283 kind="line", 284 y="point_sum", 285 label="通算", 286 figsize=(12, 8), 287 fontsize=14, 288 ) 289 if len(df) > 50: 290 point_sum = ( 291 df["point_sum"] 292 .rolling(40) 293 .mean() 294 .plot( 295 kind="line", 296 label="移動平均(40ゲーム)", 297 ax=point_sum, 298 ) 299 ) 300 if len(df) > 100: 301 point_sum = ( 302 df["point_sum"] 303 .rolling(80) 304 .mean() 305 .plot( 306 kind="line", 307 label="移動平均(80ゲーム)", 308 ax=point_sum, 309 ) 310 ) 311 plt.legend() 312 313 plt.title(title, fontsize=18) 314 plt.grid(axis="y") 315 316 # Y軸設定 317 plt.ylabel("ポイント", fontsize=14) 318 ylocs, ylabs = plt.yticks() 319 new_ylabs = [ylab.get_text().replace("−", "▲") for ylab in ylabs] 320 plt.yticks(list(ylocs[1:-1]), new_ylabs[1:-1]) 321 322 # X軸設定 323 plt.xlabel("ゲーム数", fontsize=14) 324 325 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 326 return imgdata
通算ポイント推移の折れ線グラフを生成
Arguments:
- df (pd.DataFrame): 描写データ
- title (str): グラフタイトル
- whole (bool, optional): 集計種別. Defaults to False.
- True: 全体集計 / 移動平均付き
- False: 指定範囲集計
Returns:
BytesIO: 画像データ
def
graphing_rank_distribution(df: pandas.DataFrame, title: str) -> _io.BytesIO:
329def graphing_rank_distribution(df: pd.DataFrame, title: str) -> BytesIO: 330 """ 331 順位分布の棒グラフを生成 332 333 Args: 334 df (pd.DataFrame): 描写データ 335 title (str): グラフタイトル 336 337 Returns: 338 BytesIO: 画像データ 339 340 """ 341 imgdata = BytesIO() 342 343 df.plot( 344 kind="bar", 345 stacked=True, 346 figsize=(12, 7), 347 fontsize=14, 348 ) 349 350 plt.title(title, fontsize=18) 351 plt.legend( 352 bbox_to_anchor=(0.5, 0), 353 loc="lower center", 354 ncol=4, 355 fontsize=12, 356 ) 357 358 # Y軸設定 359 plt.yticks([0, 25, 50, 75, 100]) 360 plt.ylabel("(%)", fontsize=14) 361 for ax in plt.gcf().get_axes(): # グリッド線を背後にまわす 362 ax.set_axisbelow(True) 363 plt.grid(axis="y") 364 365 # X軸設定 366 if len(df) > 10: 367 plt.xticks(rotation=30, ha="right") 368 else: 369 plt.xticks(rotation=30) 370 371 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 372 return imgdata
順位分布の棒グラフを生成
Arguments:
- df (pd.DataFrame): 描写データ
- title (str): グラフタイトル
Returns:
BytesIO: 画像データ
375def gen_pdf(m: "MessageParserProtocol") -> None: 376 """ 377 成績レポートを生成する 378 379 Args: 380 m (MessageParserProtocol): メッセージデータ 381 382 """ 383 if g.adapter.conf.plotting_backend == "plotly": 384 m.post.reset() 385 m.set_headline(message.random_reply(m, "not_implemented"), StyleOptions()) 386 return 387 388 if not g.params.player_name: # レポート対象の指定なし 389 m.set_headline(message.random_reply(m, "no_target"), StyleOptions(title="成績レポート")) 390 m.status.result = False 391 return 392 393 # 対象メンバーの記録状況 394 target_info = lookup.member_info(g.params.placeholder()) 395 logging.debug(target_info) 396 397 if not target_info["game_count"]: # 記録なし 398 m.set_headline(message.random_reply(m, "no_hits"), StyleOptions(title="成績レポート")) 399 m.status.result = False 400 return 401 402 # 書式設定 403 font_path = os.path.join(os.path.realpath(os.path.curdir), g.cfg.setting.font_file) 404 pdf_path = g.cfg.setting.work_dir / (f"{g.params.filename}.pdf" if g.params.filename else "results.pdf") 405 pdfmetrics.registerFont(TTFont("ReportFont", font_path)) 406 407 doc = SimpleDocTemplate( 408 str(pdf_path), 409 pagesize=landscape(A4), 410 topMargin=10.0 * mm, 411 bottomMargin=10.0 * mm, 412 # leftMargin=1.5 * mm, 413 # rightMargin=1.5 * mm, 414 ) 415 416 style: dict[str, Any] = {} 417 style["Title"] = ParagraphStyle(name="Title", fontName="ReportFont", fontSize=24) 418 style["Normal"] = ParagraphStyle(name="Normal", fontName="ReportFont", fontSize=14) 419 style["Left"] = ParagraphStyle(name="Left", fontName="ReportFont", fontSize=14, alignment=TA_LEFT) 420 style["Right"] = ParagraphStyle(name="Right", fontName="ReportFont", fontSize=14, alignment=TA_RIGHT) 421 422 plt.rcdefaults() 423 font_prop = fm.FontProperties(fname=font_path) 424 plt.rcParams["font.family"] = font_prop.get_name() 425 fm.fontManager.addfont(font_path) 426 427 # レポート作成 428 elements: list[Any] = [] 429 elements.extend(cover_page(style, target_info)) # 表紙 430 elements.extend(entire_aggregate(style)) # 全期間 431 elements.extend(periodic_aggregation(style)) # 期間集計 432 elements.extend(sectional_aggregate(style, target_info)) # 区間集計 433 434 doc.build(elements) 435 logging.debug("report generation: %s", g.params.player_name) 436 437 m.set_message(pdf_path, StyleOptions(title=f"成績レポート({g.params.player_name})", use_comment=True, header_hidden=True))
成績レポートを生成する
Arguments:
- m (MessageParserProtocol): メッセージデータ
def
cover_page( style: dict[str, typing.Any], target_info: dict[str, typing.Any]) -> list[typing.Any]:
440def cover_page(style: dict[str, Any], target_info: dict[str, Any]) -> list[Any]: 441 """ 442 表紙生成 443 444 Args: 445 style (dict[str, Any]): レイアウトスタイル 446 target_info (dict[str, Any]): プレイヤー情報 447 448 Returns: 449 list[Any]: 生成内容 450 451 """ 452 elements: list[Any] = [] 453 454 first_game = datetime.fromtimestamp( # 最初のゲーム日時 455 float(target_info["first_game"]) 456 ) 457 last_game = datetime.fromtimestamp( # 最後のゲーム日時 458 float(target_info["last_game"]) 459 ) 460 461 if g.params.anonymous: 462 mapping_dict = formatter.anonymous_mapping([g.params.player_name]) 463 target_player = next(iter(mapping_dict.values())) 464 else: 465 target_player = g.params.player_name 466 467 # 表紙 468 elements.append(Spacer(1, 40 * mm)) 469 elements.append(Paragraph(f"成績レポート:{target_player}", style["Title"])) 470 elements.append(Spacer(1, 10 * mm)) 471 elements.append( 472 Paragraph( 473 f"集計期間:{first_game.strftime('%Y-%m-%d %H:%M')} - {last_game.strftime('%Y-%m-%d %H:%M')}", 474 style["Normal"], 475 ) 476 ) 477 elements.append(Spacer(1, 100 * mm)) 478 elements.append(Paragraph(f"作成日:{datetime.now().strftime('%Y-%m-%d')}", style["Right"])) 479 elements.append(PageBreak()) 480 481 return elements
表紙生成
Arguments:
- style (dict[str, Any]): レイアウトスタイル
- target_info (dict[str, Any]): プレイヤー情報
Returns:
list[Any]: 生成内容
def
entire_aggregate(style: dict[str, typing.Any]) -> list[typing.Any]:
484def entire_aggregate(style: dict[str, Any]) -> list[Any]: 485 """ 486 全期間 487 488 Args: 489 style (dict[str, Any]): レイアウトスタイル 490 491 Returns: 492 list[Any]: 生成内容 493 494 """ 495 elements: list[Any] = [] 496 497 elements.append(Paragraph("全期間", style["Left"])) 498 elements.append(Spacer(1, 5 * mm)) 499 data: list[list[str]] = [] 500 g.params.aggregate_unit = "A" 501 tmp_data = get_game_results() 502 503 if not tmp_data: 504 return [] 505 506 for _, val in enumerate(tmp_data): # ゲーム数を除外 507 data.append(val[1:]) 508 tt = LongTable(data, repeatRows=1) 509 tt.setStyle( 510 TableStyle( 511 [ 512 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 513 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 514 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 515 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 516 ("SPAN", (3, 0), (4, 0)), 517 ("SPAN", (5, 0), (6, 0)), 518 ("SPAN", (7, 0), (8, 0)), 519 ("SPAN", (9, 0), (10, 0)), 520 ("SPAN", (12, 0), (13, 0)), 521 # ヘッダ行 522 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 523 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 524 ] 525 ) 526 ) 527 elements.append(tt) 528 529 # 順位分布 530 imgdata = BytesIO() 531 gdata = pd.DataFrame( 532 { 533 "順位分布": [ 534 float(str(data[1][4]).replace("%", "")), 535 float(str(data[1][6]).replace("%", "")), 536 float(str(data[1][8]).replace("%", "")), 537 float(str(data[1][10]).replace("%", "")), 538 ], 539 }, 540 index=["1位率", "2位率", "3位率", "4位率"], 541 ) 542 gdata.plot( 543 kind="pie", 544 y="順位分布", 545 labels=None, 546 figsize=(6, 6), 547 fontsize=14, 548 autopct="%.2f%%", 549 wedgeprops={"linewidth": 1, "edgecolor": "white"}, 550 ) 551 plt.title("順位分布 ( 全期間 )", fontsize=18) 552 plt.ylabel("") 553 plt.legend( 554 labels=list(gdata.index), 555 bbox_to_anchor=(0.5, -0.1), 556 loc="lower center", 557 ncol=4, 558 fontsize=12, 559 ) 560 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 561 562 elements.append(Spacer(1, 5 * mm)) 563 elements.append(Image(imgdata, width=600 * 0.5, height=600 * 0.5)) 564 565 df = pd.DataFrame(get_count_moving(0)) 566 df["playtime"] = pd.to_datetime(df["playtime"]) 567 568 # 通算ポイント推移 569 imgdata = graphing_total_points(df, "通算ポイント推移 ( 全期間 )", False) 570 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 571 572 # 平均順位 573 imgdata = graphing_mean_rank(df, "平均順位推移 ( 全期間 )", False) 574 elements.append(Image(imgdata, width=1200 * 0.5, height=500 * 0.5)) 575 576 elements.append(PageBreak()) 577 578 return elements
全期間
Arguments:
- style (dict[str, Any]): レイアウトスタイル
Returns:
list[Any]: 生成内容
def
periodic_aggregation(style: dict[str, typing.Any]) -> list[typing.Any]:
581def periodic_aggregation(style: dict[str, Any]) -> list[Any]: 582 """ 583 期間集計 584 585 Args: 586 style (dict[str, Any]): レイアウトスタイル 587 588 Returns: 589 list[Any]: 生成内容 590 591 """ 592 elements: list[Any] = [] 593 594 pattern: list[tuple[str, str, Literal["A", "M", "Y"]]] = [ 595 # 表タイトル, グラフタイトル, フラグ 596 ("月別集計", "順位分布(月別)", "M"), 597 ("年別集計", "順位分布(年別)", "Y"), 598 ] 599 600 for table_title, graph_title, flag in pattern: 601 elements.append(Paragraph(table_title, style["Left"])) 602 elements.append(Spacer(1, 5 * mm)) 603 604 data: list[list[str]] = [] 605 g.params.aggregate_unit = flag 606 tmp_data = get_game_results() 607 608 if not tmp_data: 609 return [] 610 611 for _, val in enumerate(tmp_data): # 日時を除外 612 data.append(val[:15]) 613 614 tt = LongTable(data, repeatRows=1) 615 ts = TableStyle( 616 [ 617 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 618 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 619 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 620 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 621 ("SPAN", (4, 0), (5, 0)), 622 ("SPAN", (6, 0), (7, 0)), 623 ("SPAN", (8, 0), (9, 0)), 624 ("SPAN", (10, 0), (11, 0)), 625 ("SPAN", (13, 0), (14, 0)), 626 # ヘッダ行 627 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 628 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 629 ] 630 ) 631 632 if len(data) > 4: 633 for i in range(len(data) - 2): 634 if i % 2 == 0: 635 ts.add("BACKGROUND", (0, i + 2), (-1, i + 2), colors.lightgrey) 636 tt.setStyle(ts) 637 elements.append(tt) 638 elements.append(Spacer(1, 10 * mm)) 639 640 # 順位分布 641 df = pd.DataFrame( 642 { 643 "1位率": [float(str(data[x + 1][5]).replace("%", "")) for x in range(len(data) - 1)], 644 "2位率": [float(str(data[x + 1][7]).replace("%", "")) for x in range(len(data) - 1)], 645 "3位率": [float(str(data[x + 1][9]).replace("%", "")) for x in range(len(data) - 1)], 646 "4位率": [float(str(data[x + 1][11]).replace("%", "")) for x in range(len(data) - 1)], 647 }, 648 index=[data[x + 1][0] for x in range(len(data) - 1)], 649 ) 650 651 imgdata = graphing_rank_distribution(df, graph_title) 652 elements.append(Spacer(1, 5 * mm)) 653 elements.append(Image(imgdata, width=1200 * 0.5, height=700 * 0.5)) 654 655 elements.append(PageBreak()) 656 657 return elements
期間集計
Arguments:
- style (dict[str, Any]): レイアウトスタイル
Returns:
list[Any]: 生成内容
def
sectional_aggregate( style: dict[str, typing.Any], target_info: dict[str, typing.Any]) -> list[typing.Any]:
660def sectional_aggregate(style: dict[str, Any], target_info: dict[str, Any]) -> list[Any]: 661 """ 662 区間集計 663 664 Args: 665 style (dict[str, Any]): レイアウトスタイル 666 target_info (dict[str, Any]): プレイヤー情報 667 668 Returns: 669 list[Any]: 生成内容 670 671 """ 672 elements: list[Any] = [] 673 674 pattern: list[tuple[int, int, str]] = [ 675 # 区切り回数, 閾値, タイトル 676 (80, 100, "短期"), 677 (200, 240, "中期"), 678 (400, 500, "長期"), 679 ] 680 681 for count, threshold, title in pattern: 682 if target_info["game_count"] > threshold: 683 # テーブル 684 elements.append(Paragraph(f"区間集計 ( {title} )", style["Left"])) 685 elements.append(Spacer(1, 5 * mm)) 686 data = get_count_results(count) 687 688 if not data: 689 return [] 690 691 tt = LongTable(data, repeatRows=1) 692 ts = TableStyle( 693 [ 694 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 695 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 696 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 697 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 698 ("SPAN", (5, 0), (6, 0)), 699 ("SPAN", (7, 0), (8, 0)), 700 ("SPAN", (9, 0), (10, 0)), 701 ("SPAN", (11, 0), (12, 0)), 702 ("SPAN", (14, 0), (15, 0)), 703 # ヘッダ行 704 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 705 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 706 ] 707 ) 708 if len(data) > 4: 709 for i in range(len(data) - 2): 710 if i % 2 == 0: 711 ts.add("BACKGROUND", (0, i + 2), (-1, i + 2), colors.lightgrey) 712 tt.setStyle(ts) 713 elements.append(tt) 714 715 # 順位分布 716 df = pd.DataFrame( 717 { 718 "1位率": [float(str(data[x + 1][6]).replace("%", "")) for x in range(len(data) - 1)], 719 "2位率": [float(str(data[x + 1][8]).replace("%", "")) for x in range(len(data) - 1)], 720 "3位率": [float(str(data[x + 1][10]).replace("%", "")) for x in range(len(data) - 1)], 721 "4位率": [float(str(data[x + 1][12]).replace("%", "")) for x in range(len(data) - 1)], 722 }, 723 index=[f"{str(data[x + 1][0])} - {str(data[x + 1][1])}" for x in range(len(data) - 1)], 724 ) 725 726 imgdata = graphing_rank_distribution(df, f"順位分布 ( 区間 {title} )") 727 elements.append(Spacer(1, 5 * mm)) 728 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 729 730 # 通算ポイント推移 731 tmp_df = pd.DataFrame(get_count_moving(count)) 732 df = pd.DataFrame() 733 for i in sorted(tmp_df["interval"].unique().tolist()): 734 list_data = tmp_df[tmp_df.interval == i]["point_sum"].to_list() 735 game_count = tmp_df[tmp_df.interval == i]["total_count"].to_list() 736 df[f"{min(game_count)} - {max(game_count)}"] = [None] * (count - len(list_data)) + list_data 737 738 imgdata = graphing_total_points(df, f"通算ポイント推移(区間 {title})", True) 739 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 740 741 # 平均順位 742 df = pd.DataFrame() 743 for i in sorted(tmp_df["interval"].unique().tolist()): 744 list_data = tmp_df[tmp_df.interval == i]["rank_avg"].to_list() 745 game_count = tmp_df[tmp_df.interval == i]["total_count"].to_list() 746 df[f"{min(game_count)} - {max(game_count)}"] = [None] * (count - len(list_data)) + list_data 747 748 imgdata = graphing_mean_rank(df, f"平均順位推移(区間 {title})", True) 749 elements.append(Image(imgdata, width=1200 * 0.5, height=500 * 0.5)) 750 751 elements.append(PageBreak()) 752 753 return elements
区間集計
Arguments:
- style (dict[str, Any]): レイアウトスタイル
- target_info (dict[str, Any]): プレイヤー情報
Returns:
list[Any]: 生成内容