libs.domain.stats

libs/domain/stats.py

  1"""
  2libs/domain/stats.py
  3"""
  4
  5import textwrap
  6from dataclasses import dataclass, field, fields
  7from typing import TYPE_CHECKING, Any, Literal, Optional, Union, get_type_hints
  8
  9import pandas as pd
 10
 11from libs.utils.timekit import ExtendedDatetime as ExtDt
 12
 13if TYPE_CHECKING:
 14    from libs.domain.placeholder import PlaceholderBuilder
 15
 16
 17@dataclass
 18class StatsDetailed:
 19    """詳細データ"""
 20
 21    mode: Literal[3, 4] = field(default=4)
 22    """集計モード"""
 23    win: int = field(default=0)
 24    """勝ち数"""
 25    lose: int = field(default=0)
 26    """負け数"""
 27    draw: int = field(default=0)
 28    """引き分け数"""
 29    total_point: float = field(default=0.0)
 30    """通算ポイント"""
 31    avg_point: float = field(default=0.0)
 32    """平均ポイント"""
 33
 34    rank1: int = field(default=0)
 35    """1位獲得数"""
 36    rank2: int = field(default=0)
 37    """2位獲得数"""
 38    rank3: int = field(default=0)
 39    """3位獲得数"""
 40    rank4: int = field(default=0)
 41    """4位獲得数"""
 42    flying: int = field(default=0)
 43    """トビ数"""
 44    yakuman: int = field(default=0)
 45    """役満和了数"""
 46
 47    # 最大/最小
 48    rpoint_max: int = field(default=0)
 49    """最大素点"""
 50    rpoint_min: int = field(default=0)
 51    """最小素点"""
 52    point_max: float = field(default=0.0)
 53    """最大獲得ポイント"""
 54    point_min: float = field(default=0.0)
 55    """最小獲得ポイント"""
 56
 57    # 収支データ
 58    score: int = field(default=0)
 59    """最終素点合計"""
 60    score_rank1: int = field(default=0)
 61    """最終素点合計(1位終了時)"""
 62    score_rank2: int = field(default=0)
 63    """最終素点合計(2位終了時)"""
 64    score_rank3: int = field(default=0)
 65    """最終素点合計(3位終了時)"""
 66    score_rank4: int = field(default=0)
 67    """最終素点合計(4位終了時)"""
 68
 69    # レコード
 70    top1_max: int = field(default=0)
 71    """連続1位獲得最大値"""
 72    top1_cur: int = field(default=0)
 73    """連続1位獲得最終値(現在値)"""
 74    top2_max: int = field(default=0)
 75    """連続連対最大値"""
 76    top2_cur: int = field(default=0)
 77    """連続連対最終値(現在値)"""
 78    top3_max: int = field(default=0)
 79    """連続ラス回避獲得最大値"""
 80    top3_cur: int = field(default=0)
 81    """連続ラス回避最終値(現在値)"""
 82    lose2_max: int = field(default=0)
 83    """連続1位なし最大値"""
 84    lose2_cur: int = field(default=0)
 85    """連続1位なし最終値(現在値)"""
 86    lose3_max: int = field(default=0)
 87    """連続逆連対最大値"""
 88    lose3_cur: int = field(default=0)
 89    """連続逆連対最終値(現在値)"""
 90    lose4_max: int = field(default=0)
 91    """連続ラス最大値"""
 92    lose4_cur: int = field(default=0)
 93    """連続ラス最終値(現在値)"""
 94
 95    # 集計範囲
 96    first_game: ExtDt = field(default=ExtDt("1900-01-01 00:00:00"))
 97    """最初の記録時間"""
 98    last_game: ExtDt = field(default=ExtDt("1900-01-01 00:00:00"))
 99    """最後の記録時間"""
100    first_comment: Optional[str] = field(default=None)
101    """最初の記録時間のコメント"""
102    last_comment: Optional[str] = field(default=None)
103    """最後の記録時間のコメント"""
104
105    def avg_balance(self, pattern: str) -> float:
106        """
107        平均収支計算
108
109        Args:
110            pattern (str): 計算パターン
111
112        Returns:
113            float: 計算結果
114
115        """
116        ret: float = 0.0
117
118        match pattern:
119            case "rank1":
120                if self.rank1:
121                    ret = round(self.score_rank1 * 100 / self.rank1, 1)
122            case "rank2":
123                if self.rank2:
124                    ret = round(self.score_rank2 * 100 / self.rank2, 1)
125            case "rank3":
126                if self.rank3:
127                    ret = round(self.score_rank3 * 100 / self.rank3, 1)
128            case "rank4":
129                if self.rank4:
130                    ret = round(self.score_rank4 * 100 / self.rank4, 1)
131            case "top2":
132                if self.rank1 + self.rank2:
133                    ret = round((self.score_rank1 + self.score_rank2) * 100 / (self.rank1 + self.rank2), 1)
134            case "lose2":
135                if self.rank3 + self.rank4:
136                    ret = round((self.score_rank3 + self.score_rank4) * 100 / (self.rank3 + self.rank4), 1)
137            case _:
138                if self.count:
139                    ret = round(self.score * 100 / self.count, 1)
140
141        return ret
142
143    @property
144    def count(self) -> int:
145        """ゲーム数"""
146        match self.mode:
147            case 3:
148                return sum([self.rank1, self.rank2, self.rank3])
149            case 4:
150                return sum([self.rank1, self.rank2, self.rank3, self.rank4])
151
152    @property
153    def rank_avg(self) -> float:
154        """平均順位"""
155        if self.count:
156            match self.mode:
157                case 3:
158                    return round((self.rank1 + self.rank2 * 2 + self.rank3 * 3) / self.count, 2)
159                case 4:
160                    return round((self.rank1 + self.rank2 * 2 + self.rank3 * 3 + self.rank4 * 4) / self.count, 2)
161        return 0.00
162
163    @property
164    def rank_distr(self) -> str:
165        """順位分布(+平均順位)"""
166        match self.mode:
167            case 3:
168                return f"{self.rank1}-{self.rank2}-{self.rank3} ({self.rank_avg:.2f})"
169            case 4:
170                return f"{self.rank1}-{self.rank2}-{self.rank3}-{self.rank4} ({self.rank_avg:.2f})"
171
172    @property
173    def rank_distr2(self) -> str:
174        """順位分布(+ゲーム数)"""
175        match self.mode:
176            case 3:
177                return f"{self.rank1}+{self.rank2}+{self.rank3}={self.count}"
178            case 4:
179                return f"{self.rank1}+{self.rank2}+{self.rank3}+{self.rank4}={self.count}"
180
181    @property
182    def rank1_rate(self) -> float:
183        """1位獲得率"""
184        if self.count:
185            return round(self.rank1 / self.count, 4)
186        return 0.0
187
188    @property
189    def rank2_rate(self) -> float:
190        """2位獲得率"""
191        if self.count:
192            return round(self.rank2 / self.count, 4)
193        return 0.0
194
195    @property
196    def rank3_rate(self) -> float:
197        """3位獲得率"""
198        if self.count:
199            return round(self.rank3 / self.count, 4)
200        return 0.0
201
202    @property
203    def rank4_rate(self) -> float:
204        """4位獲得率"""
205        if self.count:
206            return round(self.rank4 / self.count, 4)
207        return 0.0
208
209    @property
210    def flying_rate(self) -> float:
211        """トビ率"""
212        if self.count:
213            return round(self.flying / self.count, 4)
214        return 0.0
215
216    @property
217    def yakuman_rate(self) -> float:
218        """役満和了率"""
219        if self.count:
220            return round(self.yakuman / self.count, 4)
221        return 0.0
222
223    def update_from_dict(self, data: dict[Any, Any]) -> None:
224        """
225        辞書から値を更新
226
227        Args:
228            data (dict[Any, Any]): 更新データ(キーはフィールド名)
229
230        """
231        type_hints = get_type_hints(self.__class__)
232        for field_obj in fields(self):
233            if field_obj.name not in data:
234                continue
235
236            value = data[field_obj.name]
237            if value is None:
238                setattr(self, field_obj.name, None)
239                continue
240
241            field_type = type_hints.get(field_obj.name, type(None))
242
243            # 型別の変換処理
244            if field_type in (int, float, str, bool):
245                setattr(self, field_obj.name, field_type(value))
246            elif field_type is ExtDt or (hasattr(field_type, "__origin__") and field_type.__origin__ is Union):
247                # ExtDtまたはOptional型
248                if hasattr(field_type, "__args__"):
249                    # Optional[X]の場合、Xの型を取得
250                    inner_type = next(arg for arg in field_type.__args__ if arg is not type(None))
251                    setattr(self, field_obj.name, inner_type(value) if value is not None else None)
252                else:
253                    setattr(self, field_obj.name, ExtDt(value))
254            else:
255                setattr(self, field_obj.name, value)
256
257    def war_record(self) -> str:
258        """戦績結果"""
259        return f"{self.count} 戦 ({self.win}{self.lose}{self.draw} 分)"
260
261    def best_record(self) -> str:
262        """ベストレコード"""
263        rpoint_max = f"{self.rpoint_max * 100:+}点".replace("-", "▲") if self.rpoint_max else "記録なし"
264        point_max = f"{self.point_max:+.1f}pt".replace("-", "▲") if self.point_max else "記録なし"
265
266        ret = textwrap.dedent(f"""\
267            連続トップ:{self._work(self.top1_cur, self.top1_max)}
268            連続連対:{self._work(self.top2_cur, self.top2_max)}
269            連続ラス回避:{self._work(self.top3_cur, self.top3_max)}
270            最大素点:{rpoint_max}
271            最大獲得ポイント:{point_max}
272            """)
273        return ret.strip()
274
275    def worst_record(self) -> str:
276        """ワーストレコード"""
277        rpoint_min = f"{self.rpoint_min * 100:+}点".replace("-", "▲") if self.rpoint_min else "記録なし"
278        point_min = f"{self.point_min:+.1f}pt".replace("-", "▲") if self.point_min else "記録なし"
279
280        ret = textwrap.dedent(f"""\
281            連続ラス:{self._work(self.lose4_cur, self.lose4_max)}
282            連続逆連対:{self._work(self.lose3_cur, self.lose3_max)}
283            連続トップなし:{self._work(self.lose2_cur, self.lose2_max)}
284            最大素点:{rpoint_min}
285            最大獲得ポイント:{point_min}
286            """)
287        return ret.strip()
288
289    def _work(self, c_num: int, m_num: int) -> str:
290        """
291        単位設定
292
293        Args:
294            c_num (int): 現在値
295            m_num (int): 最大値
296
297        Returns:
298            str: 生成文字列
299
300        """
301        c_str = f"{c_num} 回目" if c_num else f"{c_num} 回"
302        if m_num:
303            m_str = "最大 1 回" if m_num == 1 else f"最大 {m_num} 連続"
304            if m_num == c_num:
305                m_str = "記録更新中"
306        else:
307            m_str = "記録なし"
308
309        return f"{c_str} ({m_str})"
310
311
312@dataclass
313class StatsInfo:
314    """成績情報"""
315
316    name: str = field(default="")
317    """プレイヤー名/チーム名"""
318
319    # 統計データ
320    seat0: StatsDetailed = field(default_factory=StatsDetailed)
321    """全席"""
322    seat1: StatsDetailed = field(default_factory=StatsDetailed)
323    """東家"""
324    seat2: StatsDetailed = field(default_factory=StatsDetailed)
325    """南家"""
326    seat3: StatsDetailed = field(default_factory=StatsDetailed)
327    """西家"""
328    seat4: StatsDetailed = field(default_factory=StatsDetailed)
329    """北家"""
330
331    # 検索条件
332    mode: Literal[3, 4] = field(default=4)
333    """集計モード"""
334    rule_version: list[str] = field(default_factory=list)
335    """ルールセット"""
336    # 検索範囲
337    starttime: ExtDt = field(default=ExtDt("1900-01-01 00:00:00"))
338    endtime: ExtDt = field(default=ExtDt("1900-01-01 00:00:00"))
339
340    # 検索ワード
341    search_word: str = field(default="")
342
343    # 取り込みデータ
344    result_df: pd.DataFrame = field(default_factory=pd.DataFrame)
345    record_df: pd.DataFrame = field(default_factory=pd.DataFrame)
346
347    def read(self, params: "PlaceholderBuilder") -> None:
348        """
349        データ読み込み
350
351        Args:
352            params (PlaceholderBuilder): プレースホルダ
353
354        """
355        self.result_df = params.read_data("RESULTS_INFO")
356        self.record_df = params.read_data("RECORD_INFO")
357
358        if self.result_df.empty or self.record_df.empty:
359            return
360
361        self.set_parameter(**params.placeholder())
362        self.set_data(self.result_df)
363        self.set_data(self.record_df)
364
365    def set_data(self, df: "pd.DataFrame") -> None:
366        """
367        集計結果取り込み
368
369        Args:
370            df (pd.DataFrame): 集計結果
371
372        """
373        seat_map = {0: self.seat0, 1: self.seat1, 2: self.seat2, 3: self.seat3, 4: self.seat4}
374
375        for _, row in df.iterrows():
376            if "id" in df.columns:
377                seat_id = row["id"]
378                if isinstance(seat_id, int) and seat_id in seat_map:
379                    seat_map[seat_id].update_from_dict(row.to_dict())
380
381    def set_parameter(self, **kwargs: Any) -> None:
382        """パラメータ取り込み"""
383        if "mode" in kwargs and isinstance(kwargs["mode"], int):
384            if kwargs["mode"] in (3, 4):
385                self.mode = kwargs["mode"]  # type: ignore[assignment]
386                self.seat0.mode = self.mode
387                self.seat1.mode = self.mode
388                self.seat2.mode = self.mode
389                self.seat3.mode = self.mode
390                self.seat4.mode = self.mode
391            else:
392                raise ValueError(f"Unsupported mode: {kwargs['mode']}")
393
394        if "rule_list" in kwargs and isinstance(kwargs["rule_list"], list):
395            self.rule_version = kwargs["rule_list"]
396        if "rule_set" in kwargs and isinstance(kwargs["rule_set"], dict):
397            self.rule_version = list(kwargs["rule_set"].values())
398        if "player_name" in kwargs and isinstance(kwargs["player_name"], str):
399            self.name = kwargs["player_name"]
400        if "starttime" in kwargs and isinstance(kwargs["starttime"], (ExtDt, str)):
401            self.starttime = ExtDt(kwargs["starttime"])
402        if "endtime" in kwargs and isinstance(kwargs["endtime"], (ExtDt, str)):
403            self.endtime = ExtDt(kwargs["endtime"])
404        if "search_word" in kwargs and isinstance(kwargs["search_word"], str):
405            self.search_word = kwargs["search_word"]
406
407    @property
408    def rank_distr_list(self) -> list[str]:
409        """座席別順位分布(平均順位)"""
410        return [
411            self.seat1.rank_distr,
412            self.seat2.rank_distr,
413            self.seat3.rank_distr,
414            self.seat4.rank_distr,
415        ][: self.mode]
416
417    @property
418    def rank_distr_list2(self) -> list[str]:
419        """座席別順位分布(ゲーム数)"""
420        return [
421            self.seat1.rank_distr2,
422            self.seat2.rank_distr2,
423            self.seat3.rank_distr2,
424            self.seat4.rank_distr2,
425        ][: self.mode]
426
427    @property
428    def rank_avg_list(self) -> list[float]:
429        """座席別平均順位"""
430        return [
431            self.seat1.rank_avg,
432            self.seat2.rank_avg,
433            self.seat3.rank_avg,
434            self.seat4.rank_avg,
435        ][: self.mode]
436
437    @property
438    def flying_list(self) -> list[int]:
439        """座席別トビ率"""
440        return [
441            self.seat1.flying,
442            self.seat2.flying,
443            self.seat3.flying,
444            self.seat4.flying,
445        ][: self.mode]
446
447    @property
448    def yakuman_list(self) -> list[int]:
449        """座席別役満和了率"""
450        return [
451            self.seat1.yakuman,
452            self.seat2.yakuman,
453            self.seat3.yakuman,
454            self.seat4.yakuman,
455        ][: self.mode]
456
457    @property
458    def summary(self) -> pd.DataFrame:
459        """成績サマリ"""
460        ret_df = pd.DataFrame(
461            {
462                "count": [self.seat0.count],
463                "war_record": [f"{self.seat0.win}-{self.seat0.lose}-{self.seat0.draw}"],
464                "rank_avg": [self.seat0.rank_avg],
465                "total_point": [f"{self.seat0.total_point:+.1f}pt".replace("-", "▲")],
466                "avg_point": [f"{self.seat0.avg_point:+.1f}pt".replace("-", "▲")],
467                "top2_rate-count": [f"{(self.seat0.rank1 + self.seat0.rank2) / self.seat0.count:.2%}({self.seat0.rank1 + self.seat0.rank2})"],
468                "top3_rate-count": [
469                    f"{(self.seat0.rank1 + self.seat0.rank2 + self.seat0.rank3) / self.seat0.count:.2%}"
470                    + f"({self.seat0.rank1 + self.seat0.rank2 + self.seat0.rank3})",
471                ],
472                "rank1_rate-count": [f"{self.seat0.rank1_rate:.2%}({self.seat0.rank1})"],
473                "rank2_rate-count": [f"{self.seat0.rank2_rate:.2%}({self.seat0.rank2})"],
474                "rank3_rate-count": [f"{self.seat0.rank3_rate:.2%}({self.seat0.rank3})"],
475                "rank4_rate-count": [f"{self.seat0.rank4_rate:.2%}({self.seat0.rank4})"],
476                "flying_rate-count": [f"{self.seat0.flying_rate:.2%}({self.seat0.flying})"],
477                "yakuman_rate-count": [f"{self.seat0.yakuman_rate:.2%}({self.seat0.yakuman})"],
478                "avg_balance": [f"{self.seat0.avg_balance('all'):+.1f}点".replace("-", "▲")],
479                "top2_balance": [f"{self.seat0.avg_balance('top2'):+.1f}点".replace("-", "▲")],
480                "lose2_balance": [f"{self.seat0.avg_balance('lose2'):+.1f}点".replace("-", "▲")],
481                "rank1_balance": [f"{self.seat0.avg_balance('rank1'):+.1f}点".replace("-", "▲")],
482                "rank2_balance": [f"{self.seat0.avg_balance('rank2'):+.1f}点".replace("-", "▲")],
483                "rank3_balance": [f"{self.seat0.avg_balance('rank3'):+.1f}点".replace("-", "▲")],
484                "rank4_balance": [f"{self.seat0.avg_balance('rank4'):+.1f}点".replace("-", "▲")],
485                "top1_max": [f"{self.seat0.top1_max}連続"],
486                "top2_max": [f"{self.seat0.top2_max}連続"],
487                "top3_max": [f"{self.seat0.top3_max}連続"],
488                "lose2_max": [f"{self.seat0.lose2_max}連続"],
489                "lose3_max": [f"{self.seat0.lose3_max}連続"],
490                "lose4_max": [f"{self.seat0.lose4_max}連続"],
491                "rpoint_max": [f"{self.seat0.rpoint_max * 100}点".replace("-", "▲")],
492                "point_max": [f"{self.seat0.point_max:+.1f}pt".replace("-", "▲")],
493                "rpoint_min": [f"{self.seat0.rpoint_min * 100}点".replace("-", "▲")],
494                "point_min": [f"{self.seat0.point_min:+.1f}pt".replace("-", "▲")],
495            },
496            index=[self.name],
497        )
498
499        return ret_df
@dataclass
class StatsDetailed:
 18@dataclass
 19class StatsDetailed:
 20    """詳細データ"""
 21
 22    mode: Literal[3, 4] = field(default=4)
 23    """集計モード"""
 24    win: int = field(default=0)
 25    """勝ち数"""
 26    lose: int = field(default=0)
 27    """負け数"""
 28    draw: int = field(default=0)
 29    """引き分け数"""
 30    total_point: float = field(default=0.0)
 31    """通算ポイント"""
 32    avg_point: float = field(default=0.0)
 33    """平均ポイント"""
 34
 35    rank1: int = field(default=0)
 36    """1位獲得数"""
 37    rank2: int = field(default=0)
 38    """2位獲得数"""
 39    rank3: int = field(default=0)
 40    """3位獲得数"""
 41    rank4: int = field(default=0)
 42    """4位獲得数"""
 43    flying: int = field(default=0)
 44    """トビ数"""
 45    yakuman: int = field(default=0)
 46    """役満和了数"""
 47
 48    # 最大/最小
 49    rpoint_max: int = field(default=0)
 50    """最大素点"""
 51    rpoint_min: int = field(default=0)
 52    """最小素点"""
 53    point_max: float = field(default=0.0)
 54    """最大獲得ポイント"""
 55    point_min: float = field(default=0.0)
 56    """最小獲得ポイント"""
 57
 58    # 収支データ
 59    score: int = field(default=0)
 60    """最終素点合計"""
 61    score_rank1: int = field(default=0)
 62    """最終素点合計(1位終了時)"""
 63    score_rank2: int = field(default=0)
 64    """最終素点合計(2位終了時)"""
 65    score_rank3: int = field(default=0)
 66    """最終素点合計(3位終了時)"""
 67    score_rank4: int = field(default=0)
 68    """最終素点合計(4位終了時)"""
 69
 70    # レコード
 71    top1_max: int = field(default=0)
 72    """連続1位獲得最大値"""
 73    top1_cur: int = field(default=0)
 74    """連続1位獲得最終値(現在値)"""
 75    top2_max: int = field(default=0)
 76    """連続連対最大値"""
 77    top2_cur: int = field(default=0)
 78    """連続連対最終値(現在値)"""
 79    top3_max: int = field(default=0)
 80    """連続ラス回避獲得最大値"""
 81    top3_cur: int = field(default=0)
 82    """連続ラス回避最終値(現在値)"""
 83    lose2_max: int = field(default=0)
 84    """連続1位なし最大値"""
 85    lose2_cur: int = field(default=0)
 86    """連続1位なし最終値(現在値)"""
 87    lose3_max: int = field(default=0)
 88    """連続逆連対最大値"""
 89    lose3_cur: int = field(default=0)
 90    """連続逆連対最終値(現在値)"""
 91    lose4_max: int = field(default=0)
 92    """連続ラス最大値"""
 93    lose4_cur: int = field(default=0)
 94    """連続ラス最終値(現在値)"""
 95
 96    # 集計範囲
 97    first_game: ExtDt = field(default=ExtDt("1900-01-01 00:00:00"))
 98    """最初の記録時間"""
 99    last_game: ExtDt = field(default=ExtDt("1900-01-01 00:00:00"))
100    """最後の記録時間"""
101    first_comment: Optional[str] = field(default=None)
102    """最初の記録時間のコメント"""
103    last_comment: Optional[str] = field(default=None)
104    """最後の記録時間のコメント"""
105
106    def avg_balance(self, pattern: str) -> float:
107        """
108        平均収支計算
109
110        Args:
111            pattern (str): 計算パターン
112
113        Returns:
114            float: 計算結果
115
116        """
117        ret: float = 0.0
118
119        match pattern:
120            case "rank1":
121                if self.rank1:
122                    ret = round(self.score_rank1 * 100 / self.rank1, 1)
123            case "rank2":
124                if self.rank2:
125                    ret = round(self.score_rank2 * 100 / self.rank2, 1)
126            case "rank3":
127                if self.rank3:
128                    ret = round(self.score_rank3 * 100 / self.rank3, 1)
129            case "rank4":
130                if self.rank4:
131                    ret = round(self.score_rank4 * 100 / self.rank4, 1)
132            case "top2":
133                if self.rank1 + self.rank2:
134                    ret = round((self.score_rank1 + self.score_rank2) * 100 / (self.rank1 + self.rank2), 1)
135            case "lose2":
136                if self.rank3 + self.rank4:
137                    ret = round((self.score_rank3 + self.score_rank4) * 100 / (self.rank3 + self.rank4), 1)
138            case _:
139                if self.count:
140                    ret = round(self.score * 100 / self.count, 1)
141
142        return ret
143
144    @property
145    def count(self) -> int:
146        """ゲーム数"""
147        match self.mode:
148            case 3:
149                return sum([self.rank1, self.rank2, self.rank3])
150            case 4:
151                return sum([self.rank1, self.rank2, self.rank3, self.rank4])
152
153    @property
154    def rank_avg(self) -> float:
155        """平均順位"""
156        if self.count:
157            match self.mode:
158                case 3:
159                    return round((self.rank1 + self.rank2 * 2 + self.rank3 * 3) / self.count, 2)
160                case 4:
161                    return round((self.rank1 + self.rank2 * 2 + self.rank3 * 3 + self.rank4 * 4) / self.count, 2)
162        return 0.00
163
164    @property
165    def rank_distr(self) -> str:
166        """順位分布(+平均順位)"""
167        match self.mode:
168            case 3:
169                return f"{self.rank1}-{self.rank2}-{self.rank3} ({self.rank_avg:.2f})"
170            case 4:
171                return f"{self.rank1}-{self.rank2}-{self.rank3}-{self.rank4} ({self.rank_avg:.2f})"
172
173    @property
174    def rank_distr2(self) -> str:
175        """順位分布(+ゲーム数)"""
176        match self.mode:
177            case 3:
178                return f"{self.rank1}+{self.rank2}+{self.rank3}={self.count}"
179            case 4:
180                return f"{self.rank1}+{self.rank2}+{self.rank3}+{self.rank4}={self.count}"
181
182    @property
183    def rank1_rate(self) -> float:
184        """1位獲得率"""
185        if self.count:
186            return round(self.rank1 / self.count, 4)
187        return 0.0
188
189    @property
190    def rank2_rate(self) -> float:
191        """2位獲得率"""
192        if self.count:
193            return round(self.rank2 / self.count, 4)
194        return 0.0
195
196    @property
197    def rank3_rate(self) -> float:
198        """3位獲得率"""
199        if self.count:
200            return round(self.rank3 / self.count, 4)
201        return 0.0
202
203    @property
204    def rank4_rate(self) -> float:
205        """4位獲得率"""
206        if self.count:
207            return round(self.rank4 / self.count, 4)
208        return 0.0
209
210    @property
211    def flying_rate(self) -> float:
212        """トビ率"""
213        if self.count:
214            return round(self.flying / self.count, 4)
215        return 0.0
216
217    @property
218    def yakuman_rate(self) -> float:
219        """役満和了率"""
220        if self.count:
221            return round(self.yakuman / self.count, 4)
222        return 0.0
223
224    def update_from_dict(self, data: dict[Any, Any]) -> None:
225        """
226        辞書から値を更新
227
228        Args:
229            data (dict[Any, Any]): 更新データ(キーはフィールド名)
230
231        """
232        type_hints = get_type_hints(self.__class__)
233        for field_obj in fields(self):
234            if field_obj.name not in data:
235                continue
236
237            value = data[field_obj.name]
238            if value is None:
239                setattr(self, field_obj.name, None)
240                continue
241
242            field_type = type_hints.get(field_obj.name, type(None))
243
244            # 型別の変換処理
245            if field_type in (int, float, str, bool):
246                setattr(self, field_obj.name, field_type(value))
247            elif field_type is ExtDt or (hasattr(field_type, "__origin__") and field_type.__origin__ is Union):
248                # ExtDtまたはOptional型
249                if hasattr(field_type, "__args__"):
250                    # Optional[X]の場合、Xの型を取得
251                    inner_type = next(arg for arg in field_type.__args__ if arg is not type(None))
252                    setattr(self, field_obj.name, inner_type(value) if value is not None else None)
253                else:
254                    setattr(self, field_obj.name, ExtDt(value))
255            else:
256                setattr(self, field_obj.name, value)
257
258    def war_record(self) -> str:
259        """戦績結果"""
260        return f"{self.count} 戦 ({self.win}{self.lose}{self.draw} 分)"
261
262    def best_record(self) -> str:
263        """ベストレコード"""
264        rpoint_max = f"{self.rpoint_max * 100:+}点".replace("-", "▲") if self.rpoint_max else "記録なし"
265        point_max = f"{self.point_max:+.1f}pt".replace("-", "▲") if self.point_max else "記録なし"
266
267        ret = textwrap.dedent(f"""\
268            連続トップ:{self._work(self.top1_cur, self.top1_max)}
269            連続連対:{self._work(self.top2_cur, self.top2_max)}
270            連続ラス回避:{self._work(self.top3_cur, self.top3_max)}
271            最大素点:{rpoint_max}
272            最大獲得ポイント:{point_max}
273            """)
274        return ret.strip()
275
276    def worst_record(self) -> str:
277        """ワーストレコード"""
278        rpoint_min = f"{self.rpoint_min * 100:+}点".replace("-", "▲") if self.rpoint_min else "記録なし"
279        point_min = f"{self.point_min:+.1f}pt".replace("-", "▲") if self.point_min else "記録なし"
280
281        ret = textwrap.dedent(f"""\
282            連続ラス:{self._work(self.lose4_cur, self.lose4_max)}
283            連続逆連対:{self._work(self.lose3_cur, self.lose3_max)}
284            連続トップなし:{self._work(self.lose2_cur, self.lose2_max)}
285            最大素点:{rpoint_min}
286            最大獲得ポイント:{point_min}
287            """)
288        return ret.strip()
289
290    def _work(self, c_num: int, m_num: int) -> str:
291        """
292        単位設定
293
294        Args:
295            c_num (int): 現在値
296            m_num (int): 最大値
297
298        Returns:
299            str: 生成文字列
300
301        """
302        c_str = f"{c_num} 回目" if c_num else f"{c_num} 回"
303        if m_num:
304            m_str = "最大 1 回" if m_num == 1 else f"最大 {m_num} 連続"
305            if m_num == c_num:
306                m_str = "記録更新中"
307        else:
308            m_str = "記録なし"
309
310        return f"{c_str} ({m_str})"

詳細データ

StatsDetailed( mode: Literal[3, 4] = 4, win: int = 0, lose: int = 0, draw: int = 0, total_point: float = 0.0, avg_point: float = 0.0, rank1: int = 0, rank2: int = 0, rank3: int = 0, rank4: int = 0, flying: int = 0, yakuman: int = 0, rpoint_max: int = 0, rpoint_min: int = 0, point_max: float = 0.0, point_min: float = 0.0, score: int = 0, score_rank1: int = 0, score_rank2: int = 0, score_rank3: int = 0, score_rank4: int = 0, top1_max: int = 0, top1_cur: int = 0, top2_max: int = 0, top2_cur: int = 0, top3_max: int = 0, top3_cur: int = 0, lose2_max: int = 0, lose2_cur: int = 0, lose3_max: int = 0, lose3_cur: int = 0, lose4_max: int = 0, lose4_cur: int = 0, first_game: libs.utils.timekit.ExtendedDatetime = 1900-01-01 00:00:00.000000, last_game: libs.utils.timekit.ExtendedDatetime = 1900-01-01 00:00:00.000000, first_comment: str | None = None, last_comment: str | None = None)
mode: Literal[3, 4] = 4

集計モード

win: int = 0

勝ち数

lose: int = 0

負け数

draw: int = 0

引き分け数

total_point: float = 0.0

通算ポイント

avg_point: float = 0.0

平均ポイント

rank1: int = 0

1位獲得数

rank2: int = 0

2位獲得数

rank3: int = 0

3位獲得数

rank4: int = 0

4位獲得数

flying: int = 0

トビ数

yakuman: int = 0

役満和了数

rpoint_max: int = 0

最大素点

rpoint_min: int = 0

最小素点

point_max: float = 0.0

最大獲得ポイント

point_min: float = 0.0

最小獲得ポイント

score: int = 0

最終素点合計

score_rank1: int = 0

最終素点合計(1位終了時)

score_rank2: int = 0

最終素点合計(2位終了時)

score_rank3: int = 0

最終素点合計(3位終了時)

score_rank4: int = 0

最終素点合計(4位終了時)

top1_max: int = 0

連続1位獲得最大値

top1_cur: int = 0

連続1位獲得最終値(現在値)

top2_max: int = 0

連続連対最大値

top2_cur: int = 0

連続連対最終値(現在値)

top3_max: int = 0

連続ラス回避獲得最大値

top3_cur: int = 0

連続ラス回避最終値(現在値)

lose2_max: int = 0

連続1位なし最大値

lose2_cur: int = 0

連続1位なし最終値(現在値)

lose3_max: int = 0

連続逆連対最大値

lose3_cur: int = 0

連続逆連対最終値(現在値)

lose4_max: int = 0

連続ラス最大値

lose4_cur: int = 0

連続ラス最終値(現在値)

first_game: libs.utils.timekit.ExtendedDatetime = 1900-01-01 00:00:00.000000

最初の記録時間

last_game: libs.utils.timekit.ExtendedDatetime = 1900-01-01 00:00:00.000000

最後の記録時間

first_comment: str | None = None

最初の記録時間のコメント

last_comment: str | None = None

最後の記録時間のコメント

def avg_balance(self, pattern: str) -> float:
106    def avg_balance(self, pattern: str) -> float:
107        """
108        平均収支計算
109
110        Args:
111            pattern (str): 計算パターン
112
113        Returns:
114            float: 計算結果
115
116        """
117        ret: float = 0.0
118
119        match pattern:
120            case "rank1":
121                if self.rank1:
122                    ret = round(self.score_rank1 * 100 / self.rank1, 1)
123            case "rank2":
124                if self.rank2:
125                    ret = round(self.score_rank2 * 100 / self.rank2, 1)
126            case "rank3":
127                if self.rank3:
128                    ret = round(self.score_rank3 * 100 / self.rank3, 1)
129            case "rank4":
130                if self.rank4:
131                    ret = round(self.score_rank4 * 100 / self.rank4, 1)
132            case "top2":
133                if self.rank1 + self.rank2:
134                    ret = round((self.score_rank1 + self.score_rank2) * 100 / (self.rank1 + self.rank2), 1)
135            case "lose2":
136                if self.rank3 + self.rank4:
137                    ret = round((self.score_rank3 + self.score_rank4) * 100 / (self.rank3 + self.rank4), 1)
138            case _:
139                if self.count:
140                    ret = round(self.score * 100 / self.count, 1)
141
142        return ret

平均収支計算

Arguments:
  • pattern (str): 計算パターン
Returns:

float: 計算結果

count: int
144    @property
145    def count(self) -> int:
146        """ゲーム数"""
147        match self.mode:
148            case 3:
149                return sum([self.rank1, self.rank2, self.rank3])
150            case 4:
151                return sum([self.rank1, self.rank2, self.rank3, self.rank4])

ゲーム数

rank_avg: float
153    @property
154    def rank_avg(self) -> float:
155        """平均順位"""
156        if self.count:
157            match self.mode:
158                case 3:
159                    return round((self.rank1 + self.rank2 * 2 + self.rank3 * 3) / self.count, 2)
160                case 4:
161                    return round((self.rank1 + self.rank2 * 2 + self.rank3 * 3 + self.rank4 * 4) / self.count, 2)
162        return 0.00

平均順位

rank_distr: str
164    @property
165    def rank_distr(self) -> str:
166        """順位分布(+平均順位)"""
167        match self.mode:
168            case 3:
169                return f"{self.rank1}-{self.rank2}-{self.rank3} ({self.rank_avg:.2f})"
170            case 4:
171                return f"{self.rank1}-{self.rank2}-{self.rank3}-{self.rank4} ({self.rank_avg:.2f})"

順位分布(+平均順位)

rank_distr2: str
173    @property
174    def rank_distr2(self) -> str:
175        """順位分布(+ゲーム数)"""
176        match self.mode:
177            case 3:
178                return f"{self.rank1}+{self.rank2}+{self.rank3}={self.count}"
179            case 4:
180                return f"{self.rank1}+{self.rank2}+{self.rank3}+{self.rank4}={self.count}"

順位分布(+ゲーム数)

rank1_rate: float
182    @property
183    def rank1_rate(self) -> float:
184        """1位獲得率"""
185        if self.count:
186            return round(self.rank1 / self.count, 4)
187        return 0.0

1位獲得率

rank2_rate: float
189    @property
190    def rank2_rate(self) -> float:
191        """2位獲得率"""
192        if self.count:
193            return round(self.rank2 / self.count, 4)
194        return 0.0

2位獲得率

rank3_rate: float
196    @property
197    def rank3_rate(self) -> float:
198        """3位獲得率"""
199        if self.count:
200            return round(self.rank3 / self.count, 4)
201        return 0.0

3位獲得率

rank4_rate: float
203    @property
204    def rank4_rate(self) -> float:
205        """4位獲得率"""
206        if self.count:
207            return round(self.rank4 / self.count, 4)
208        return 0.0

4位獲得率

flying_rate: float
210    @property
211    def flying_rate(self) -> float:
212        """トビ率"""
213        if self.count:
214            return round(self.flying / self.count, 4)
215        return 0.0

トビ率

yakuman_rate: float
217    @property
218    def yakuman_rate(self) -> float:
219        """役満和了率"""
220        if self.count:
221            return round(self.yakuman / self.count, 4)
222        return 0.0

役満和了率

def update_from_dict(self, data: dict[typing.Any, typing.Any]) -> None:
224    def update_from_dict(self, data: dict[Any, Any]) -> None:
225        """
226        辞書から値を更新
227
228        Args:
229            data (dict[Any, Any]): 更新データ(キーはフィールド名)
230
231        """
232        type_hints = get_type_hints(self.__class__)
233        for field_obj in fields(self):
234            if field_obj.name not in data:
235                continue
236
237            value = data[field_obj.name]
238            if value is None:
239                setattr(self, field_obj.name, None)
240                continue
241
242            field_type = type_hints.get(field_obj.name, type(None))
243
244            # 型別の変換処理
245            if field_type in (int, float, str, bool):
246                setattr(self, field_obj.name, field_type(value))
247            elif field_type is ExtDt or (hasattr(field_type, "__origin__") and field_type.__origin__ is Union):
248                # ExtDtまたはOptional型
249                if hasattr(field_type, "__args__"):
250                    # Optional[X]の場合、Xの型を取得
251                    inner_type = next(arg for arg in field_type.__args__ if arg is not type(None))
252                    setattr(self, field_obj.name, inner_type(value) if value is not None else None)
253                else:
254                    setattr(self, field_obj.name, ExtDt(value))
255            else:
256                setattr(self, field_obj.name, value)

辞書から値を更新

Arguments:
  • data (dict[Any, Any]): 更新データ(キーはフィールド名)
def war_record(self) -> str:
258    def war_record(self) -> str:
259        """戦績結果"""
260        return f"{self.count} 戦 ({self.win}{self.lose}{self.draw} 分)"

戦績結果

def best_record(self) -> str:
262    def best_record(self) -> str:
263        """ベストレコード"""
264        rpoint_max = f"{self.rpoint_max * 100:+}点".replace("-", "▲") if self.rpoint_max else "記録なし"
265        point_max = f"{self.point_max:+.1f}pt".replace("-", "▲") if self.point_max else "記録なし"
266
267        ret = textwrap.dedent(f"""\
268            連続トップ:{self._work(self.top1_cur, self.top1_max)}
269            連続連対:{self._work(self.top2_cur, self.top2_max)}
270            連続ラス回避:{self._work(self.top3_cur, self.top3_max)}
271            最大素点:{rpoint_max}
272            最大獲得ポイント:{point_max}
273            """)
274        return ret.strip()

ベストレコード

def worst_record(self) -> str:
276    def worst_record(self) -> str:
277        """ワーストレコード"""
278        rpoint_min = f"{self.rpoint_min * 100:+}点".replace("-", "▲") if self.rpoint_min else "記録なし"
279        point_min = f"{self.point_min:+.1f}pt".replace("-", "▲") if self.point_min else "記録なし"
280
281        ret = textwrap.dedent(f"""\
282            連続ラス:{self._work(self.lose4_cur, self.lose4_max)}
283            連続逆連対:{self._work(self.lose3_cur, self.lose3_max)}
284            連続トップなし:{self._work(self.lose2_cur, self.lose2_max)}
285            最大素点:{rpoint_min}
286            最大獲得ポイント:{point_min}
287            """)
288        return ret.strip()

ワーストレコード

@dataclass
class StatsInfo:
313@dataclass
314class StatsInfo:
315    """成績情報"""
316
317    name: str = field(default="")
318    """プレイヤー名/チーム名"""
319
320    # 統計データ
321    seat0: StatsDetailed = field(default_factory=StatsDetailed)
322    """全席"""
323    seat1: StatsDetailed = field(default_factory=StatsDetailed)
324    """東家"""
325    seat2: StatsDetailed = field(default_factory=StatsDetailed)
326    """南家"""
327    seat3: StatsDetailed = field(default_factory=StatsDetailed)
328    """西家"""
329    seat4: StatsDetailed = field(default_factory=StatsDetailed)
330    """北家"""
331
332    # 検索条件
333    mode: Literal[3, 4] = field(default=4)
334    """集計モード"""
335    rule_version: list[str] = field(default_factory=list)
336    """ルールセット"""
337    # 検索範囲
338    starttime: ExtDt = field(default=ExtDt("1900-01-01 00:00:00"))
339    endtime: ExtDt = field(default=ExtDt("1900-01-01 00:00:00"))
340
341    # 検索ワード
342    search_word: str = field(default="")
343
344    # 取り込みデータ
345    result_df: pd.DataFrame = field(default_factory=pd.DataFrame)
346    record_df: pd.DataFrame = field(default_factory=pd.DataFrame)
347
348    def read(self, params: "PlaceholderBuilder") -> None:
349        """
350        データ読み込み
351
352        Args:
353            params (PlaceholderBuilder): プレースホルダ
354
355        """
356        self.result_df = params.read_data("RESULTS_INFO")
357        self.record_df = params.read_data("RECORD_INFO")
358
359        if self.result_df.empty or self.record_df.empty:
360            return
361
362        self.set_parameter(**params.placeholder())
363        self.set_data(self.result_df)
364        self.set_data(self.record_df)
365
366    def set_data(self, df: "pd.DataFrame") -> None:
367        """
368        集計結果取り込み
369
370        Args:
371            df (pd.DataFrame): 集計結果
372
373        """
374        seat_map = {0: self.seat0, 1: self.seat1, 2: self.seat2, 3: self.seat3, 4: self.seat4}
375
376        for _, row in df.iterrows():
377            if "id" in df.columns:
378                seat_id = row["id"]
379                if isinstance(seat_id, int) and seat_id in seat_map:
380                    seat_map[seat_id].update_from_dict(row.to_dict())
381
382    def set_parameter(self, **kwargs: Any) -> None:
383        """パラメータ取り込み"""
384        if "mode" in kwargs and isinstance(kwargs["mode"], int):
385            if kwargs["mode"] in (3, 4):
386                self.mode = kwargs["mode"]  # type: ignore[assignment]
387                self.seat0.mode = self.mode
388                self.seat1.mode = self.mode
389                self.seat2.mode = self.mode
390                self.seat3.mode = self.mode
391                self.seat4.mode = self.mode
392            else:
393                raise ValueError(f"Unsupported mode: {kwargs['mode']}")
394
395        if "rule_list" in kwargs and isinstance(kwargs["rule_list"], list):
396            self.rule_version = kwargs["rule_list"]
397        if "rule_set" in kwargs and isinstance(kwargs["rule_set"], dict):
398            self.rule_version = list(kwargs["rule_set"].values())
399        if "player_name" in kwargs and isinstance(kwargs["player_name"], str):
400            self.name = kwargs["player_name"]
401        if "starttime" in kwargs and isinstance(kwargs["starttime"], (ExtDt, str)):
402            self.starttime = ExtDt(kwargs["starttime"])
403        if "endtime" in kwargs and isinstance(kwargs["endtime"], (ExtDt, str)):
404            self.endtime = ExtDt(kwargs["endtime"])
405        if "search_word" in kwargs and isinstance(kwargs["search_word"], str):
406            self.search_word = kwargs["search_word"]
407
408    @property
409    def rank_distr_list(self) -> list[str]:
410        """座席別順位分布(平均順位)"""
411        return [
412            self.seat1.rank_distr,
413            self.seat2.rank_distr,
414            self.seat3.rank_distr,
415            self.seat4.rank_distr,
416        ][: self.mode]
417
418    @property
419    def rank_distr_list2(self) -> list[str]:
420        """座席別順位分布(ゲーム数)"""
421        return [
422            self.seat1.rank_distr2,
423            self.seat2.rank_distr2,
424            self.seat3.rank_distr2,
425            self.seat4.rank_distr2,
426        ][: self.mode]
427
428    @property
429    def rank_avg_list(self) -> list[float]:
430        """座席別平均順位"""
431        return [
432            self.seat1.rank_avg,
433            self.seat2.rank_avg,
434            self.seat3.rank_avg,
435            self.seat4.rank_avg,
436        ][: self.mode]
437
438    @property
439    def flying_list(self) -> list[int]:
440        """座席別トビ率"""
441        return [
442            self.seat1.flying,
443            self.seat2.flying,
444            self.seat3.flying,
445            self.seat4.flying,
446        ][: self.mode]
447
448    @property
449    def yakuman_list(self) -> list[int]:
450        """座席別役満和了率"""
451        return [
452            self.seat1.yakuman,
453            self.seat2.yakuman,
454            self.seat3.yakuman,
455            self.seat4.yakuman,
456        ][: self.mode]
457
458    @property
459    def summary(self) -> pd.DataFrame:
460        """成績サマリ"""
461        ret_df = pd.DataFrame(
462            {
463                "count": [self.seat0.count],
464                "war_record": [f"{self.seat0.win}-{self.seat0.lose}-{self.seat0.draw}"],
465                "rank_avg": [self.seat0.rank_avg],
466                "total_point": [f"{self.seat0.total_point:+.1f}pt".replace("-", "▲")],
467                "avg_point": [f"{self.seat0.avg_point:+.1f}pt".replace("-", "▲")],
468                "top2_rate-count": [f"{(self.seat0.rank1 + self.seat0.rank2) / self.seat0.count:.2%}({self.seat0.rank1 + self.seat0.rank2})"],
469                "top3_rate-count": [
470                    f"{(self.seat0.rank1 + self.seat0.rank2 + self.seat0.rank3) / self.seat0.count:.2%}"
471                    + f"({self.seat0.rank1 + self.seat0.rank2 + self.seat0.rank3})",
472                ],
473                "rank1_rate-count": [f"{self.seat0.rank1_rate:.2%}({self.seat0.rank1})"],
474                "rank2_rate-count": [f"{self.seat0.rank2_rate:.2%}({self.seat0.rank2})"],
475                "rank3_rate-count": [f"{self.seat0.rank3_rate:.2%}({self.seat0.rank3})"],
476                "rank4_rate-count": [f"{self.seat0.rank4_rate:.2%}({self.seat0.rank4})"],
477                "flying_rate-count": [f"{self.seat0.flying_rate:.2%}({self.seat0.flying})"],
478                "yakuman_rate-count": [f"{self.seat0.yakuman_rate:.2%}({self.seat0.yakuman})"],
479                "avg_balance": [f"{self.seat0.avg_balance('all'):+.1f}点".replace("-", "▲")],
480                "top2_balance": [f"{self.seat0.avg_balance('top2'):+.1f}点".replace("-", "▲")],
481                "lose2_balance": [f"{self.seat0.avg_balance('lose2'):+.1f}点".replace("-", "▲")],
482                "rank1_balance": [f"{self.seat0.avg_balance('rank1'):+.1f}点".replace("-", "▲")],
483                "rank2_balance": [f"{self.seat0.avg_balance('rank2'):+.1f}点".replace("-", "▲")],
484                "rank3_balance": [f"{self.seat0.avg_balance('rank3'):+.1f}点".replace("-", "▲")],
485                "rank4_balance": [f"{self.seat0.avg_balance('rank4'):+.1f}点".replace("-", "▲")],
486                "top1_max": [f"{self.seat0.top1_max}連続"],
487                "top2_max": [f"{self.seat0.top2_max}連続"],
488                "top3_max": [f"{self.seat0.top3_max}連続"],
489                "lose2_max": [f"{self.seat0.lose2_max}連続"],
490                "lose3_max": [f"{self.seat0.lose3_max}連続"],
491                "lose4_max": [f"{self.seat0.lose4_max}連続"],
492                "rpoint_max": [f"{self.seat0.rpoint_max * 100}点".replace("-", "▲")],
493                "point_max": [f"{self.seat0.point_max:+.1f}pt".replace("-", "▲")],
494                "rpoint_min": [f"{self.seat0.rpoint_min * 100}点".replace("-", "▲")],
495                "point_min": [f"{self.seat0.point_min:+.1f}pt".replace("-", "▲")],
496            },
497            index=[self.name],
498        )
499
500        return ret_df

成績情報

StatsInfo( name: str = '', seat0: StatsDetailed = <factory>, seat1: StatsDetailed = <factory>, seat2: StatsDetailed = <factory>, seat3: StatsDetailed = <factory>, seat4: StatsDetailed = <factory>, mode: Literal[3, 4] = 4, rule_version: list[str] = <factory>, starttime: libs.utils.timekit.ExtendedDatetime = 1900-01-01 00:00:00.000000, endtime: libs.utils.timekit.ExtendedDatetime = 1900-01-01 00:00:00.000000, search_word: str = '', result_df: pandas.DataFrame = <factory>, record_df: pandas.DataFrame = <factory>)
name: str = ''

プレイヤー名/チーム名

seat0: StatsDetailed

全席

seat1: StatsDetailed

東家

seat2: StatsDetailed

南家

seat3: StatsDetailed

西家

seat4: StatsDetailed

北家

mode: Literal[3, 4] = 4

集計モード

rule_version: list[str]

ルールセット

starttime: libs.utils.timekit.ExtendedDatetime = 1900-01-01 00:00:00.000000
endtime: libs.utils.timekit.ExtendedDatetime = 1900-01-01 00:00:00.000000
search_word: str = ''
result_df: pandas.DataFrame
record_df: pandas.DataFrame
def read(self, params: libs.domain.placeholder.PlaceholderBuilder) -> None:
348    def read(self, params: "PlaceholderBuilder") -> None:
349        """
350        データ読み込み
351
352        Args:
353            params (PlaceholderBuilder): プレースホルダ
354
355        """
356        self.result_df = params.read_data("RESULTS_INFO")
357        self.record_df = params.read_data("RECORD_INFO")
358
359        if self.result_df.empty or self.record_df.empty:
360            return
361
362        self.set_parameter(**params.placeholder())
363        self.set_data(self.result_df)
364        self.set_data(self.record_df)

データ読み込み

Arguments:
  • params (PlaceholderBuilder): プレースホルダ
def set_data(self, df: pandas.DataFrame) -> None:
366    def set_data(self, df: "pd.DataFrame") -> None:
367        """
368        集計結果取り込み
369
370        Args:
371            df (pd.DataFrame): 集計結果
372
373        """
374        seat_map = {0: self.seat0, 1: self.seat1, 2: self.seat2, 3: self.seat3, 4: self.seat4}
375
376        for _, row in df.iterrows():
377            if "id" in df.columns:
378                seat_id = row["id"]
379                if isinstance(seat_id, int) and seat_id in seat_map:
380                    seat_map[seat_id].update_from_dict(row.to_dict())

集計結果取り込み

Arguments:
  • df (pd.DataFrame): 集計結果
def set_parameter(self, **kwargs: Any) -> None:
382    def set_parameter(self, **kwargs: Any) -> None:
383        """パラメータ取り込み"""
384        if "mode" in kwargs and isinstance(kwargs["mode"], int):
385            if kwargs["mode"] in (3, 4):
386                self.mode = kwargs["mode"]  # type: ignore[assignment]
387                self.seat0.mode = self.mode
388                self.seat1.mode = self.mode
389                self.seat2.mode = self.mode
390                self.seat3.mode = self.mode
391                self.seat4.mode = self.mode
392            else:
393                raise ValueError(f"Unsupported mode: {kwargs['mode']}")
394
395        if "rule_list" in kwargs and isinstance(kwargs["rule_list"], list):
396            self.rule_version = kwargs["rule_list"]
397        if "rule_set" in kwargs and isinstance(kwargs["rule_set"], dict):
398            self.rule_version = list(kwargs["rule_set"].values())
399        if "player_name" in kwargs and isinstance(kwargs["player_name"], str):
400            self.name = kwargs["player_name"]
401        if "starttime" in kwargs and isinstance(kwargs["starttime"], (ExtDt, str)):
402            self.starttime = ExtDt(kwargs["starttime"])
403        if "endtime" in kwargs and isinstance(kwargs["endtime"], (ExtDt, str)):
404            self.endtime = ExtDt(kwargs["endtime"])
405        if "search_word" in kwargs and isinstance(kwargs["search_word"], str):
406            self.search_word = kwargs["search_word"]

パラメータ取り込み

rank_distr_list: list[str]
408    @property
409    def rank_distr_list(self) -> list[str]:
410        """座席別順位分布(平均順位)"""
411        return [
412            self.seat1.rank_distr,
413            self.seat2.rank_distr,
414            self.seat3.rank_distr,
415            self.seat4.rank_distr,
416        ][: self.mode]

座席別順位分布(平均順位)

rank_distr_list2: list[str]
418    @property
419    def rank_distr_list2(self) -> list[str]:
420        """座席別順位分布(ゲーム数)"""
421        return [
422            self.seat1.rank_distr2,
423            self.seat2.rank_distr2,
424            self.seat3.rank_distr2,
425            self.seat4.rank_distr2,
426        ][: self.mode]

座席別順位分布(ゲーム数)

rank_avg_list: list[float]
428    @property
429    def rank_avg_list(self) -> list[float]:
430        """座席別平均順位"""
431        return [
432            self.seat1.rank_avg,
433            self.seat2.rank_avg,
434            self.seat3.rank_avg,
435            self.seat4.rank_avg,
436        ][: self.mode]

座席別平均順位

flying_list: list[int]
438    @property
439    def flying_list(self) -> list[int]:
440        """座席別トビ率"""
441        return [
442            self.seat1.flying,
443            self.seat2.flying,
444            self.seat3.flying,
445            self.seat4.flying,
446        ][: self.mode]

座席別トビ率

yakuman_list: list[int]
448    @property
449    def yakuman_list(self) -> list[int]:
450        """座席別役満和了率"""
451        return [
452            self.seat1.yakuman,
453            self.seat2.yakuman,
454            self.seat3.yakuman,
455            self.seat4.yakuman,
456        ][: self.mode]

座席別役満和了率

summary: pandas.DataFrame
458    @property
459    def summary(self) -> pd.DataFrame:
460        """成績サマリ"""
461        ret_df = pd.DataFrame(
462            {
463                "count": [self.seat0.count],
464                "war_record": [f"{self.seat0.win}-{self.seat0.lose}-{self.seat0.draw}"],
465                "rank_avg": [self.seat0.rank_avg],
466                "total_point": [f"{self.seat0.total_point:+.1f}pt".replace("-", "▲")],
467                "avg_point": [f"{self.seat0.avg_point:+.1f}pt".replace("-", "▲")],
468                "top2_rate-count": [f"{(self.seat0.rank1 + self.seat0.rank2) / self.seat0.count:.2%}({self.seat0.rank1 + self.seat0.rank2})"],
469                "top3_rate-count": [
470                    f"{(self.seat0.rank1 + self.seat0.rank2 + self.seat0.rank3) / self.seat0.count:.2%}"
471                    + f"({self.seat0.rank1 + self.seat0.rank2 + self.seat0.rank3})",
472                ],
473                "rank1_rate-count": [f"{self.seat0.rank1_rate:.2%}({self.seat0.rank1})"],
474                "rank2_rate-count": [f"{self.seat0.rank2_rate:.2%}({self.seat0.rank2})"],
475                "rank3_rate-count": [f"{self.seat0.rank3_rate:.2%}({self.seat0.rank3})"],
476                "rank4_rate-count": [f"{self.seat0.rank4_rate:.2%}({self.seat0.rank4})"],
477                "flying_rate-count": [f"{self.seat0.flying_rate:.2%}({self.seat0.flying})"],
478                "yakuman_rate-count": [f"{self.seat0.yakuman_rate:.2%}({self.seat0.yakuman})"],
479                "avg_balance": [f"{self.seat0.avg_balance('all'):+.1f}点".replace("-", "▲")],
480                "top2_balance": [f"{self.seat0.avg_balance('top2'):+.1f}点".replace("-", "▲")],
481                "lose2_balance": [f"{self.seat0.avg_balance('lose2'):+.1f}点".replace("-", "▲")],
482                "rank1_balance": [f"{self.seat0.avg_balance('rank1'):+.1f}点".replace("-", "▲")],
483                "rank2_balance": [f"{self.seat0.avg_balance('rank2'):+.1f}点".replace("-", "▲")],
484                "rank3_balance": [f"{self.seat0.avg_balance('rank3'):+.1f}点".replace("-", "▲")],
485                "rank4_balance": [f"{self.seat0.avg_balance('rank4'):+.1f}点".replace("-", "▲")],
486                "top1_max": [f"{self.seat0.top1_max}連続"],
487                "top2_max": [f"{self.seat0.top2_max}連続"],
488                "top3_max": [f"{self.seat0.top3_max}連続"],
489                "lose2_max": [f"{self.seat0.lose2_max}連続"],
490                "lose3_max": [f"{self.seat0.lose3_max}連続"],
491                "lose4_max": [f"{self.seat0.lose4_max}連続"],
492                "rpoint_max": [f"{self.seat0.rpoint_max * 100}点".replace("-", "▲")],
493                "point_max": [f"{self.seat0.point_max:+.1f}pt".replace("-", "▲")],
494                "rpoint_min": [f"{self.seat0.rpoint_min * 100}点".replace("-", "▲")],
495                "point_min": [f"{self.seat0.point_min:+.1f}pt".replace("-", "▲")],
496            },
497            index=[self.name],
498        )
499
500        return ret_df

成績サマリ