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: 画像データ

def gen_pdf(m: integrations.protocols.MessageParserProtocol) -> None:
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]: 生成内容