spells-mtg 0.10.11__py3-none-any.whl → 0.11.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ # Automatically created by ruff.
2
+ *
@@ -0,0 +1 @@
1
+ Signature: 8a477f597d28d172789f06886806bc55
spells/cache.py CHANGED
@@ -7,6 +7,7 @@ and groupbys.
7
7
  Caches are cleared per-set when new files are downloaded.
8
8
  """
9
9
 
10
+ import datetime as dt
10
11
  from enum import StrEnum
11
12
  import os
12
13
  from pathlib import Path
@@ -31,6 +32,8 @@ class EventType(StrEnum):
31
32
  class DataDir(StrEnum):
32
33
  CACHE = "cache"
33
34
  EXTERNAL = "external"
35
+ RATINGS = "ratings"
36
+ DECK_COLOR = "deck_color"
34
37
 
35
38
 
36
39
  def spells_print(mode, content):
@@ -141,6 +144,8 @@ def data_dir_path(cache_dir: DataDir) -> str:
141
144
  ext = {
142
145
  DataDir.CACHE: "Cache" if is_win else "cache",
143
146
  DataDir.EXTERNAL: "External" if is_win else "external",
147
+ DataDir.RATINGS: "Ratings" if is_win else "ratings",
148
+ DataDir.DECK_COLOR: "DeckColor" if is_win else "deck_color",
144
149
  }[cache_dir]
145
150
 
146
151
  data_dir = os.path.join(data_home(), ext)
@@ -163,6 +168,38 @@ def data_file_path(set_code, dataset_type: str, event_type=EventType.PREMIER):
163
168
  )
164
169
 
165
170
 
171
+ def card_ratings_file_path(
172
+ set_code: str,
173
+ format: str,
174
+ user_group: str,
175
+ deck_color: str,
176
+ start_date: dt.date,
177
+ end_date: dt.date,
178
+ ) -> tuple[str, str]:
179
+ return os.path.join(
180
+ data_dir_path(DataDir.RATINGS),
181
+ set_code,
182
+ ), (
183
+ f"{format}_{user_group}_{deck_color}_{start_date.strftime('%Y-%m-%d')}"
184
+ f"_{end_date.strftime('%Y-%m-%d')}.json"
185
+ )
186
+
187
+ def deck_color_file_path(
188
+ set_code: str,
189
+ format: str,
190
+ user_group: str,
191
+ start_date: dt.date,
192
+ end_date: dt.date,
193
+ ) -> tuple[str, str]:
194
+ return os.path.join(
195
+ data_dir_path(DataDir.DECK_COLOR),
196
+ set_code,
197
+ ), (
198
+ f"{format}_{user_group}_{start_date.strftime('%Y-%m-%d')}"
199
+ f"_{end_date.strftime('%Y-%m-%d')}.json"
200
+ )
201
+
202
+
166
203
  def cache_dir_for_set(set_code: str) -> str:
167
204
  return os.path.join(data_dir_path(DataDir.CACHE), set_code)
168
205
 
@@ -0,0 +1,222 @@
1
+ import datetime as dt
2
+ import os
3
+ import wget
4
+ from time import sleep
5
+
6
+ import polars as pl
7
+
8
+ from spells import cache
9
+ from spells.enums import ColName
10
+
11
+ RATINGS_TEMPLATE = (
12
+ "https://www.17lands.com/card_ratings/data?expansion={set_code}&format={format}"
13
+ "{user_group_param}{deck_color_param}&start_date={start_date_str}&end_date={end_date_str}"
14
+ )
15
+
16
+ DECK_COLOR_DATA_TEMPLATE = (
17
+ "https://www.17lands.com/color_ratings/data?expansion={set_code}&event_type={format}"
18
+ "{user_group_param}&start_date={start_date_str}&end_date={end_date_str}&combine_splash=true"
19
+ )
20
+
21
+ START_DATE_MAP = {
22
+ "PIO": dt.date(2024, 12, 10),
23
+ "DFT": dt.date(2025, 2, 11),
24
+ "TDM": dt.date(2025, 4, 8),
25
+ "FIN": dt.date(2025, 6, 10),
26
+ "EOE": dt.date(2025, 7, 29),
27
+ "OM1": dt.date(2025, 9, 23),
28
+ "Cube+-+Powered": dt.date(2025, 10, 28),
29
+ }
30
+
31
+ ratings_col_defs = {
32
+ ColName.NAME: pl.col("name").cast(pl.String),
33
+ ColName.COLOR: pl.col("color").cast(pl.String),
34
+ ColName.RARITY: pl.col("rarity").cast(pl.String),
35
+ ColName.IMAGE_URL: pl.col("url").cast(pl.String),
36
+ ColName.NUM_SEEN: pl.col("seen_count").cast(pl.Int64),
37
+ ColName.LAST_SEEN: pl.col("seen_count") * pl.col("avg_seen").cast(pl.Float64),
38
+ ColName.NUM_TAKEN: pl.col("pick_count").cast(pl.Int64),
39
+ ColName.TAKEN_AT: pl.col("pick_count") * pl.col("avg_pick").cast(pl.Float64),
40
+ ColName.DECK: pl.col("game_count").cast(pl.Int64),
41
+ ColName.WON_DECK: pl.col("win_rate") * pl.col("game_count").cast(pl.Float64),
42
+ ColName.SIDEBOARD: (pl.col("pool_count") - pl.col("game_count")).cast(pl.Int64),
43
+ ColName.OPENING_HAND: pl.col("opening_hand_game_count").cast(pl.Int64),
44
+ ColName.WON_OPENING_HAND: pl.col("opening_hand_game_count")
45
+ * pl.col("opening_hand_win_rate").cast(pl.Float64),
46
+ ColName.DRAWN: pl.col("drawn_game_count").cast(pl.Int64),
47
+ ColName.WON_DRAWN: pl.col("drawn_win_rate")
48
+ * pl.col("drawn_game_count").cast(pl.Float64),
49
+ ColName.NUM_GIH: pl.col("ever_drawn_game_count").cast(pl.Int64),
50
+ ColName.NUM_GIH_WON: pl.col("ever_drawn_game_count")
51
+ * pl.col("ever_drawn_win_rate").cast(pl.Float64),
52
+ ColName.NUM_GNS: pl.col("never_drawn_game_count").cast(pl.Int64),
53
+ ColName.WON_NUM_GNS: pl.col("never_drawn_game_count")
54
+ * pl.col("never_drawn_win_rate").cast(pl.Float64),
55
+ }
56
+
57
+ deck_color_col_defs = {
58
+ ColName.MAIN_COLORS: pl.col("short_name").cast(pl.String),
59
+ ColName.NUM_GAMES: pl.col("games").cast(pl.Int64),
60
+ ColName.NUM_WON: pl.col("wins").cast(pl.Int64),
61
+ }
62
+
63
+
64
+ def deck_color_df(
65
+ set_code: str,
66
+ format: str = "PremierDraft",
67
+ player_cohort: str = "all",
68
+ start_date: dt.date | None = None,
69
+ end_date: dt.date | None = None,
70
+ ):
71
+ if start_date is None:
72
+ start_date = START_DATE_MAP[set_code]
73
+ if end_date is None:
74
+ end_date = dt.date.today() - dt.timedelta(days=1)
75
+
76
+ target_dir, filename = cache.deck_color_file_path(
77
+ set_code,
78
+ format,
79
+ player_cohort,
80
+ start_date,
81
+ end_date,
82
+ )
83
+
84
+ if not os.path.isdir(target_dir):
85
+ os.makedirs(target_dir)
86
+
87
+ deck_color_file_path = os.path.join(target_dir, filename)
88
+
89
+ if not os.path.isfile(deck_color_file_path):
90
+ user_group_param = (
91
+ "" if player_cohort == "all" else f"&user_group={player_cohort}"
92
+ )
93
+
94
+ url = DECK_COLOR_DATA_TEMPLATE.format(
95
+ set_code=set_code,
96
+ format=format,
97
+ user_group_param=user_group_param,
98
+ start_date_str=start_date.strftime("%Y-%m-%d"),
99
+ end_date_str=end_date.strftime("%Y-%m-%d"),
100
+ )
101
+
102
+ wget.download(
103
+ url,
104
+ out=deck_color_file_path,
105
+ )
106
+
107
+ df = (
108
+ pl.read_json(deck_color_file_path)
109
+ .filter(~pl.col("is_summary"))
110
+ .select(
111
+ [
112
+ pl.lit(set_code).alias(ColName.EXPANSION),
113
+ pl.lit(format).alias(ColName.EVENT_TYPE),
114
+ (pl.lit("Top") if player_cohort == "top" else pl.lit(None))
115
+ .alias(ColName.PLAYER_COHORT)
116
+ .cast(pl.String),
117
+ *[val.alias(key) for key, val in deck_color_col_defs.items()],
118
+ ]
119
+ )
120
+ )
121
+
122
+ return df
123
+
124
+
125
+ def base_ratings_df(
126
+ set_code: str,
127
+ format: str = "PremierDraft",
128
+ player_cohort: str = "all",
129
+ deck_colors: str | list[str] = "any",
130
+ start_date: dt.date | None = None,
131
+ end_date: dt.date | None = None,
132
+ ) -> pl.DataFrame:
133
+ if start_date is None:
134
+ start_date = START_DATE_MAP[set_code]
135
+ if end_date is None:
136
+ end_date = dt.date.today() - dt.timedelta(days=1)
137
+
138
+ if isinstance(deck_colors, str):
139
+ deck_colors = [deck_colors]
140
+
141
+ concat_list = []
142
+ for i, deck_color in enumerate(deck_colors):
143
+ ratings_dir, filename = cache.card_ratings_file_path(
144
+ set_code,
145
+ format,
146
+ player_cohort,
147
+ deck_color,
148
+ start_date,
149
+ end_date,
150
+ )
151
+
152
+ if not os.path.isdir(ratings_dir):
153
+ os.makedirs(ratings_dir)
154
+
155
+ ratings_file_path = os.path.join(ratings_dir, filename)
156
+
157
+ if not os.path.isfile(ratings_file_path):
158
+ if i > 0:
159
+ sleep(5)
160
+ user_group_param = (
161
+ "" if player_cohort == "all" else f"&user_group={player_cohort}"
162
+ )
163
+ deck_color_param = "" if deck_color == "any" else f"&colors={deck_color}"
164
+
165
+ url = RATINGS_TEMPLATE.format(
166
+ set_code=set_code,
167
+ format=format,
168
+ user_group_param=user_group_param,
169
+ deck_color_param=deck_color_param,
170
+ start_date_str=start_date.strftime("%Y-%m-%d"),
171
+ end_date_str=end_date.strftime("%Y-%m-%d"),
172
+ )
173
+
174
+ wget.download(
175
+ url,
176
+ out=ratings_file_path,
177
+ )
178
+
179
+ concat_list.append(
180
+ pl.read_json(ratings_file_path, infer_schema_length=500)
181
+ .with_columns(
182
+ (pl.lit(deck_color) if deck_color != "any" else pl.lit(None))
183
+ .alias(ColName.MAIN_COLORS)
184
+ .cast(pl.String)
185
+ )
186
+ .select(
187
+ [
188
+ pl.lit(set_code).alias(ColName.EXPANSION),
189
+ pl.lit(format).alias(ColName.EVENT_TYPE),
190
+ (pl.lit("Top") if player_cohort == "top" else pl.lit(None))
191
+ .alias(ColName.PLAYER_COHORT)
192
+ .cast(pl.String),
193
+ ColName.MAIN_COLORS,
194
+ *[val.alias(key) for key, val in ratings_col_defs.items()],
195
+ ]
196
+ )
197
+ )
198
+
199
+ raw_df = pl.concat(concat_list)
200
+
201
+ group_cols = [
202
+ ColName.NAME,
203
+ ColName.EXPANSION,
204
+ ColName.MAIN_COLORS,
205
+ ]
206
+
207
+ attr_cols = [
208
+ ColName.EVENT_TYPE,
209
+ ColName.PLAYER_COHORT,
210
+ ColName.COLOR,
211
+ ColName.RARITY,
212
+ ColName.IMAGE_URL,
213
+ ]
214
+
215
+ sum_cols = list(set(ratings_col_defs) - set(group_cols + attr_cols))
216
+
217
+ attr_df = raw_df.select(group_cols + attr_cols).group_by(group_cols).first()
218
+ sum_df = raw_df.select(group_cols + sum_cols).group_by(group_cols).sum()
219
+
220
+ df = attr_df.join(sum_df, on=group_cols, join_nulls=True)
221
+
222
+ return df
spells/columns.py CHANGED
@@ -40,6 +40,11 @@ default_columns = [
40
40
  ColName.GIH_WR,
41
41
  ]
42
42
 
43
+
44
+ def agg_col(expr: pl.Expr) -> ColSpec:
45
+ return ColSpec(col_type=ColType.AGG, expr=expr)
46
+
47
+
43
48
  _specs: dict[str, ColSpec] = {
44
49
  ColName.NAME: ColSpec(
45
50
  col_type=ColType.GROUP_BY,
@@ -168,7 +173,8 @@ _specs: dict[str, ColSpec] = {
168
173
  ),
169
174
  ColName.PICK_INDEX: ColSpec(
170
175
  col_type=ColType.GROUP_BY,
171
- expr=lambda set_context: pl.col(ColName.PICK_NUMBER) + pl.col(ColName.PACK_NUMBER) * set_context['picks_per_pack']
176
+ expr=lambda set_context: pl.col(ColName.PICK_NUMBER)
177
+ + pl.col(ColName.PACK_NUMBER) * set_context["picks_per_pack"],
172
178
  ),
173
179
  ColName.TAKEN_AT: ColSpec(
174
180
  col_type=ColType.PICK_SUM,
@@ -441,149 +447,61 @@ _specs: dict[str, ColSpec] = {
441
447
  ColName.IMAGE_URL: ColSpec(
442
448
  col_type=ColType.CARD_ATTR,
443
449
  ),
444
- ColName.PICKED_MATCH_WR: ColSpec(
445
- col_type=ColType.AGG,
446
- expr=pl.col(ColName.EVENT_MATCH_WINS_SUM) / pl.col(ColName.EVENT_MATCHES_SUM),
447
- ),
448
- ColName.TROPHY_RATE: ColSpec(
449
- col_type=ColType.AGG,
450
- expr=pl.col(ColName.IS_TROPHY_SUM) / pl.col(ColName.NUM_TAKEN),
451
- ),
452
- ColName.GAME_WR: ColSpec(
453
- col_type=ColType.AGG,
454
- expr=pl.col(ColName.NUM_WON) / pl.col(ColName.NUM_GAMES),
455
- ),
456
- ColName.ALSA: ColSpec(
457
- col_type=ColType.AGG,
458
- expr=pl.col(ColName.LAST_SEEN) / pl.col(ColName.NUM_SEEN),
459
- ),
460
- ColName.ATA: ColSpec(
461
- col_type=ColType.AGG,
462
- expr=pl.col(ColName.TAKEN_AT) / pl.col(ColName.NUM_TAKEN),
463
- ),
464
- ColName.NUM_GP: ColSpec(
465
- col_type=ColType.AGG,
466
- expr=pl.col(ColName.DECK),
467
- ),
468
- ColName.PCT_GP: ColSpec(
469
- col_type=ColType.AGG,
470
- expr=pl.col(ColName.DECK) / (pl.col(ColName.DECK) + pl.col(ColName.SIDEBOARD)),
471
- ),
472
- ColName.GP_WR: ColSpec(
473
- col_type=ColType.AGG,
474
- expr=pl.col(ColName.WON_DECK) / pl.col(ColName.DECK),
475
- ),
476
- ColName.NUM_OH: ColSpec(
477
- col_type=ColType.AGG,
478
- expr=pl.col(ColName.OPENING_HAND),
479
- ),
480
- ColName.OH_WR: ColSpec(
481
- col_type=ColType.AGG,
482
- expr=pl.col(ColName.WON_OPENING_HAND) / pl.col(ColName.OPENING_HAND),
483
- ),
484
- ColName.NUM_GIH: ColSpec(
485
- col_type=ColType.AGG,
486
- expr=pl.col(ColName.OPENING_HAND) + pl.col(ColName.DRAWN),
487
- ),
488
- ColName.NUM_GIH_WON: ColSpec(
489
- col_type=ColType.AGG,
490
- expr=pl.col(ColName.WON_OPENING_HAND) + pl.col(ColName.WON_DRAWN),
491
- ),
492
- ColName.GIH_WR: ColSpec(
493
- col_type=ColType.AGG,
494
- expr=pl.col(ColName.NUM_GIH_WON) / pl.col(ColName.NUM_GIH),
495
- ),
496
- ColName.GNS_WR: ColSpec(
497
- col_type=ColType.AGG,
498
- expr=pl.col(ColName.WON_NUM_GNS) / pl.col(ColName.NUM_GNS),
499
- ),
500
- ColName.IWD: ColSpec(
501
- col_type=ColType.AGG,
502
- expr=pl.col(ColName.GIH_WR) - pl.col(ColName.GNS_WR),
503
- ),
504
- ColName.NUM_IN_POOL: ColSpec(
505
- col_type=ColType.AGG,
506
- expr=pl.col(ColName.DECK) + pl.col(ColName.SIDEBOARD),
507
- ),
508
- ColName.NUM_IN_POOL_TOTAL: ColSpec(
509
- col_type=ColType.AGG,
510
- expr=pl.col(ColName.NUM_IN_POOL).sum(),
511
- ),
512
- ColName.IN_POOL_WR: ColSpec(
513
- col_type=ColType.AGG,
514
- expr=(pl.col(ColName.WON_DECK) + pl.col(ColName.WON_SIDEBOARD))
515
- / pl.col(ColName.NUM_IN_POOL),
516
- ),
517
- ColName.DECK_TOTAL: ColSpec(
518
- col_type=ColType.AGG,
519
- expr=pl.col(ColName.DECK).sum(),
520
- ),
521
- ColName.WON_DECK_TOTAL: ColSpec(
522
- col_type=ColType.AGG,
523
- expr=pl.col(ColName.WON_DECK).sum(),
524
- ),
525
- ColName.GP_WR_MEAN: ColSpec(
526
- col_type=ColType.AGG,
527
- expr=pl.col(ColName.WON_DECK_TOTAL) / pl.col(ColName.DECK_TOTAL),
528
- ),
529
- ColName.GP_WR_EXCESS: ColSpec(
530
- col_type=ColType.AGG,
531
- expr=pl.col(ColName.GP_WR) - pl.col(ColName.GP_WR_MEAN),
532
- ),
533
- ColName.GP_WR_VAR: ColSpec(
534
- col_type=ColType.AGG,
535
- expr=(pl.col(ColName.GP_WR_EXCESS).pow(2) * pl.col(ColName.NUM_GP)).sum()
536
- / pl.col(ColName.DECK_TOTAL),
537
- ),
538
- ColName.GP_WR_STDEV: ColSpec(
539
- col_type=ColType.AGG,
540
- expr=pl.col(ColName.GP_WR_VAR).sqrt(),
541
- ),
542
- ColName.GP_WR_Z: ColSpec(
543
- col_type=ColType.AGG,
544
- expr=pl.col(ColName.GP_WR_EXCESS) / pl.col(ColName.GP_WR_STDEV),
545
- ),
546
- ColName.GIH_TOTAL: ColSpec(
547
- col_type=ColType.AGG,
548
- expr=pl.col(ColName.NUM_GIH).sum(),
549
- ),
550
- ColName.WON_GIH_TOTAL: ColSpec(
551
- col_type=ColType.AGG,
552
- expr=pl.col(ColName.NUM_GIH_WON).sum(),
553
- ),
554
- ColName.GIH_WR_MEAN: ColSpec(
555
- col_type=ColType.AGG,
556
- expr=pl.col(ColName.WON_GIH_TOTAL) / pl.col(ColName.GIH_TOTAL),
557
- ),
558
- ColName.GIH_WR_EXCESS: ColSpec(
559
- col_type=ColType.AGG,
560
- expr=pl.col(ColName.GIH_WR) - pl.col(ColName.GIH_WR_MEAN),
561
- ),
562
- ColName.GIH_WR_VAR: ColSpec(
563
- col_type=ColType.AGG,
564
- expr=(pl.col(ColName.GIH_WR_EXCESS).pow(2) * pl.col(ColName.NUM_GIH)).sum()
565
- / pl.col(ColName.GIH_TOTAL),
566
- ),
567
- ColName.GIH_WR_STDEV: ColSpec(
568
- col_type=ColType.AGG,
569
- expr=pl.col(ColName.GIH_WR_VAR).sqrt(),
570
- ),
571
- ColName.GIH_WR_Z: ColSpec(
572
- col_type=ColType.AGG,
573
- expr=pl.col(ColName.GIH_WR_EXCESS) / pl.col(ColName.GIH_WR_STDEV),
574
- ),
575
- ColName.DECK_MANA_VALUE_AVG: ColSpec(
576
- col_type=ColType.AGG,
577
- expr=pl.col(ColName.DECK_MANA_VALUE) / pl.col(ColName.DECK_SPELLS),
578
- ),
579
- ColName.DECK_LANDS_AVG: ColSpec(
580
- col_type=ColType.AGG,
581
- expr=pl.col(ColName.DECK_LANDS) / pl.col(ColName.NUM_GAMES),
582
- ),
583
- ColName.DECK_SPELLS_AVG: ColSpec(
584
- col_type=ColType.AGG,
585
- expr=pl.col(ColName.DECK_SPELLS) / pl.col(ColName.NUM_GAMES),
586
- ),
450
+ ColName.PICKED_MATCH_WR: agg_col(
451
+ pl.col(ColName.EVENT_MATCH_WINS_SUM) / pl.col(ColName.EVENT_MATCHES_SUM)
452
+ ),
453
+ ColName.TROPHY_RATE: agg_col(
454
+ pl.col(ColName.IS_TROPHY_SUM) / pl.col(ColName.NUM_TAKEN),
455
+ ),
456
+ ColName.GAME_WR: agg_col(
457
+ pl.col(ColName.NUM_WON) / pl.col(ColName.NUM_GAMES),
458
+ ),
459
+ ColName.ALSA: agg_col(pl.col(ColName.LAST_SEEN) / pl.col(ColName.NUM_SEEN)),
460
+ ColName.ATA: agg_col(pl.col(ColName.TAKEN_AT) / pl.col(ColName.NUM_TAKEN)),
461
+ ColName.NUM_GP: agg_col(pl.col(ColName.DECK)),
462
+ ColName.PCT_GP: agg_col(
463
+ pl.col(ColName.DECK) / (pl.col(ColName.DECK) + pl.col(ColName.SIDEBOARD))
464
+ ),
465
+ ColName.GP_WR: agg_col(pl.col(ColName.WON_DECK) / pl.col(ColName.DECK)),
466
+ ColName.NUM_OH: agg_col(pl.col(ColName.OPENING_HAND)),
467
+ ColName.OH_WR: agg_col(
468
+ pl.col(ColName.WON_OPENING_HAND) / pl.col(ColName.OPENING_HAND)
469
+ ),
470
+ ColName.NUM_GIH: agg_col(pl.col(ColName.OPENING_HAND) + pl.col(ColName.DRAWN)),
471
+ ColName.NUM_GIH_WON: agg_col(
472
+ pl.col(ColName.WON_OPENING_HAND) + pl.col(ColName.WON_DRAWN)
473
+ ),
474
+ ColName.GIH_WR: agg_col(pl.col(ColName.NUM_GIH_WON) / pl.col(ColName.NUM_GIH)),
475
+ ColName.GNS_WR: agg_col(pl.col(ColName.WON_NUM_GNS) / pl.col(ColName.NUM_GNS)),
476
+ ColName.IWD: agg_col(pl.col(ColName.GIH_WR) - pl.col(ColName.GNS_WR)),
477
+ ColName.NUM_IN_POOL: agg_col(pl.col(ColName.DECK) + pl.col(ColName.SIDEBOARD)),
478
+ ColName.NUM_IN_POOL_TOTAL: agg_col(pl.col(ColName.NUM_IN_POOL).sum()),
479
+ ColName.IN_POOL_WR: agg_col(
480
+ (pl.col(ColName.WON_DECK) + pl.col(ColName.WON_SIDEBOARD))
481
+ / pl.col(ColName.NUM_IN_POOL)
482
+ ),
483
+ ColName.DECK_TOTAL: agg_col(pl.col(ColName.DECK).sum()),
484
+ ColName.WON_DECK_TOTAL: agg_col(pl.col(ColName.WON_DECK).sum()),
485
+ ColName.GP_WR_MEAN: agg_col(pl.col(ColName.WON_DECK_TOTAL) / pl.col(ColName.DECK_TOTAL)),
486
+ ColName.GP_WR_EXCESS: agg_col(pl.col(ColName.GP_WR) - pl.col(ColName.GP_WR_MEAN)),
487
+ ColName.GP_WR_VAR: agg_col((pl.col(ColName.GP_WR_EXCESS).pow(2) * pl.col(ColName.NUM_GP)).sum()
488
+ / pl.col(ColName.DECK_TOTAL)
489
+ ),
490
+ ColName.GP_WR_STDEV: agg_col(pl.col(ColName.GP_WR_VAR).sqrt()),
491
+ ColName.GP_WR_Z: agg_col(pl.col(ColName.GP_WR_EXCESS) / pl.col(ColName.GP_WR_STDEV)),
492
+ ColName.GIH_TOTAL: agg_col(pl.col(ColName.NUM_GIH).sum()),
493
+ ColName.WON_GIH_TOTAL: agg_col(pl.col(ColName.NUM_GIH_WON).sum()),
494
+ ColName.GIH_WR_MEAN: agg_col(pl.col(ColName.WON_GIH_TOTAL) / pl.col(ColName.GIH_TOTAL)),
495
+ ColName.GIH_WR_EXCESS: agg_col(pl.col(ColName.GIH_WR) - pl.col(ColName.GIH_WR_MEAN)),
496
+ ColName.GIH_WR_VAR: agg_col(
497
+ (pl.col(ColName.GIH_WR_EXCESS).pow(2) * pl.col(ColName.NUM_GIH)).sum()
498
+ / pl.col(ColName.GIH_TOTAL)
499
+ ),
500
+ ColName.GIH_WR_STDEV: agg_col(pl.col(ColName.GIH_WR_VAR).sqrt()),
501
+ ColName.GIH_WR_Z: agg_col(pl.col(ColName.GIH_WR_EXCESS) / pl.col(ColName.GIH_WR_STDEV)),
502
+ ColName.DECK_MANA_VALUE_AVG: agg_col(pl.col(ColName.DECK_MANA_VALUE) / pl.col(ColName.DECK_SPELLS)),
503
+ ColName.DECK_LANDS_AVG: agg_col(pl.col(ColName.DECK_LANDS) / pl.col(ColName.NUM_GAMES)),
504
+ ColName.DECK_SPELLS_AVG: agg_col(pl.col(ColName.DECK_SPELLS) / pl.col(ColName.NUM_GAMES)),
587
505
  }
588
506
 
589
507
  for item in ColName:
spells/config.py CHANGED
@@ -1,4 +1,6 @@
1
1
  all_sets = [
2
+ "EOE",
3
+ "FIN",
2
4
  "TDM",
3
5
  "DFT",
4
6
  "PIO",
spells/draft_data.py CHANGED
@@ -6,6 +6,8 @@ Aggregate dataframes containing raw counts are cached in the local file system
6
6
  for performance.
7
7
  """
8
8
 
9
+ from dataclasses import dataclass
10
+ import datetime
9
11
  import functools
10
12
  import hashlib
11
13
  import re
@@ -23,9 +25,19 @@ from spells import manifest
23
25
  from spells.columns import ColDef, ColSpec, get_specs
24
26
  from spells.enums import View, ColName, ColType
25
27
  from spells.log import make_verbose
28
+ from spells.card_data_files import base_ratings_df
26
29
 
27
30
  DF = TypeVar("DF", pl.LazyFrame, pl.DataFrame)
28
31
 
32
+ @dataclass
33
+ class CardDataFileSpec():
34
+ set_code: str
35
+ format: str = "PremierDraft"
36
+ player_cohort: str = "all"
37
+ deck_colors: str | list[str] = "any"
38
+ start_date: datetime.date | None = None
39
+ end_date: datetime.date | None = None
40
+
29
41
 
30
42
  def _cache_key(args) -> str:
31
43
  """
@@ -37,20 +49,25 @@ def _cache_key(args) -> str:
37
49
  @functools.lru_cache(maxsize=None)
38
50
  def get_names(set_code: str) -> list[str]:
39
51
  card_fp = cache.data_file_path(set_code, View.CARD)
40
- card_view = pl.read_parquet(card_fp)
41
- card_names_set = frozenset(card_view.get_column("name").to_list())
52
+ try:
53
+ card_view = pl.read_parquet(card_fp)
54
+ card_names_set = frozenset(card_view.get_column("name").to_list())
42
55
 
43
- draft_fp = cache.data_file_path(set_code, View.DRAFT)
44
- draft_view = pl.scan_parquet(draft_fp)
45
- cols = draft_view.collect_schema().names()
56
+ draft_fp = cache.data_file_path(set_code, View.DRAFT)
57
+ draft_view = pl.scan_parquet(draft_fp)
58
+ cols = draft_view.collect_schema().names()
46
59
 
47
- prefix = "pack_card_"
48
- names = [col[len(prefix) :] for col in cols if col.startswith(prefix)]
49
- draft_names_set = frozenset(names)
60
+ prefix = "pack_card_"
61
+ names = [col[len(prefix) :] for col in cols if col.startswith(prefix)]
62
+ draft_names_set = frozenset(names)
63
+
64
+ assert (
65
+ draft_names_set == card_names_set
66
+ ), "names mismatch between card and draft file"
67
+ except FileNotFoundError:
68
+ ratings_data = base_ratings_df(set_code)
69
+ names = list(ratings_data['name'])
50
70
 
51
- assert (
52
- draft_names_set == card_names_set
53
- ), "names mismatch between card and draft file"
54
71
  return names
55
72
 
56
73
 
@@ -330,7 +347,10 @@ def _view_select(
330
347
  cdefs = [col_def_map[c] for c in sorted(view_cols)]
331
348
  select = []
332
349
  for cdef in cdefs:
333
- if is_agg_view:
350
+ if isinstance(df, pl.DataFrame) and cdef.name in df.columns:
351
+ base_cols = base_cols.union(frozenset({cdef.name}))
352
+ select.append(cdef.name)
353
+ elif is_agg_view:
334
354
  if cdef.col_type == ColType.AGG:
335
355
  base_cols = base_cols.union(cdef.dependencies)
336
356
  select.append(cdef.expr)
@@ -454,7 +474,7 @@ def _base_agg_df(
454
474
 
455
475
  if group_by:
456
476
  joined_df = functools.reduce(
457
- lambda prev, curr: prev.join(curr, on=group_by, how="outer", coalesce=True),
477
+ lambda prev, curr: prev.join(curr, on=group_by, how="full", coalesce=True),
458
478
  join_dfs,
459
479
  )
460
480
  else:
@@ -476,6 +496,7 @@ def summon(
476
496
  write_cache: bool = True,
477
497
  card_context: pl.DataFrame | dict[str, Any] | None = None,
478
498
  set_context: pl.DataFrame | dict[str, Any] | None = None,
499
+ cdfs: CardDataFileSpec | None = None,
479
500
  ) -> pl.DataFrame:
480
501
  specs = get_specs()
481
502
 
@@ -515,33 +536,51 @@ def summon(
515
536
  else:
516
537
  this_set_context = None
517
538
 
518
- col_def_map = _hydrate_col_defs(code, specs, set_card_context, this_set_context)
539
+ col_def_map = _hydrate_col_defs(
540
+ code,
541
+ specs,
542
+ set_card_context,
543
+ this_set_context,
544
+ card_only=cdfs is not None,
545
+ )
519
546
  m = manifest.create(col_def_map, columns, group_by, filter_spec)
520
547
 
521
- calc_fn = functools.partial(_base_agg_df, code, m, use_streaming=use_streaming)
522
- agg_df = _fetch_or_cache(
523
- calc_fn,
524
- code,
525
- (
548
+ if cdfs is None:
549
+ calc_fn = functools.partial(_base_agg_df, code, m, use_streaming=use_streaming)
550
+ agg_df = _fetch_or_cache(
551
+ calc_fn,
526
552
  code,
527
- sorted(m.view_cols.get(View.DRAFT, set())),
528
- sorted(m.view_cols.get(View.GAME, set())),
529
- sorted(c.signature or "" for c in m.col_def_map.values()),
530
- sorted(m.base_view_group_by),
531
- filter_spec,
532
- ),
533
- read_cache=read_cache,
534
- write_cache=write_cache,
535
- )
536
-
537
- if View.CARD in m.view_cols:
538
- card_cols = m.view_cols[View.CARD].union({ColName.NAME})
539
- fp = cache.data_file_path(code, View.CARD)
540
- card_df = pl.read_parquet(fp)
541
- select_df = _view_select(
542
- card_df, card_cols, m.col_def_map, is_agg_view=False
553
+ (
554
+ code,
555
+ sorted(m.view_cols.get(View.DRAFT, set())),
556
+ sorted(m.view_cols.get(View.GAME, set())),
557
+ sorted(c.signature or "" for c in m.col_def_map.values()),
558
+ sorted(m.base_view_group_by),
559
+ filter_spec,
560
+ ),
561
+ read_cache=read_cache,
562
+ write_cache=write_cache,
563
+ )
564
+ if View.CARD in m.view_cols:
565
+ card_cols = m.view_cols[View.CARD].union({ColName.NAME})
566
+ fp = cache.data_file_path(code, View.CARD)
567
+ card_df = pl.read_parquet(fp)
568
+ select_df = _view_select(
569
+ card_df, card_cols, m.col_def_map, is_agg_view=False
570
+ )
571
+ agg_df = agg_df.join(select_df, on="name", how="full", coalesce=True)
572
+ else:
573
+ assert len(codes) == 1, "Only one set supported for loading from card data file"
574
+ assert codes[0] == cdfs.set_code, "Wrong set file specified"
575
+ agg_df = base_ratings_df(
576
+ set_code=cdfs.set_code,
577
+ format=cdfs.format,
578
+ player_cohort=cdfs.player_cohort,
579
+ deck_colors=cdfs.deck_colors,
580
+ start_date=cdfs.start_date,
581
+ end_date=cdfs.end_date,
543
582
  )
544
- agg_df = agg_df.join(select_df, on="name", how="outer", coalesce=True)
583
+
545
584
  concat_dfs.append(agg_df)
546
585
 
547
586
  full_agg_df = pl.concat(concat_dfs, how="vertical")
spells/extension.py CHANGED
@@ -101,6 +101,10 @@ def context_cols(attr, silent: bool = True) -> dict[str, ColSpec]:
101
101
  [pl.col(f"seen_{attr}_{name}") for name in names]
102
102
  ),
103
103
  ),
104
+ f"not_picked_{attr}_sum": ColSpec(
105
+ col_type=ColType.PICK_SUM,
106
+ expr=pl.col(f"seen_{attr}_pack_sum") - pl.col(f"pick_{attr}_sum")
107
+ ),
104
108
  f"least_{attr}_seen": ColSpec(
105
109
  col_type=ColType.PICK_SUM,
106
110
  expr=lambda names: pl.min_horizontal(
spells/external.py CHANGED
@@ -30,7 +30,6 @@ RESOURCE_TEMPLATE = (
30
30
  "https://17lands-public.s3.amazonaws.com/analysis_data/{dataset_type}_data/"
31
31
  )
32
32
 
33
-
34
33
  class FileFormat(StrEnum):
35
34
  CSV = "csv"
36
35
  PARQUET = "parquet"
@@ -121,7 +120,8 @@ def _context() -> int:
121
120
  for code in all_sets:
122
121
  write_card_file(code, force_download=True)
123
122
  get_set_context(code, force_download=True)
124
- return 0
123
+ return 0
124
+
125
125
 
126
126
  def _refresh(set_code: str):
127
127
  return _add(set_code, force_download=True)
@@ -359,4 +359,5 @@ def get_set_context(set_code: str, force_download=False) -> int:
359
359
  context_df.write_parquet(context_fp)
360
360
 
361
361
  cache.spells_print(mode, f"Wrote file {context_fp}")
362
+
362
363
  return 0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spells-mtg
3
- Version: 0.10.11
3
+ Version: 0.11.16
4
4
  Summary: analaysis of 17Lands.com public datasets
5
5
  Author-Email: Joel Barnes <oelarnes@gmail.com>
6
6
  License: MIT
7
7
  Requires-Python: >=3.11
8
- Requires-Dist: polars>=1.14.0
8
+ Requires-Dist: polars==1.22.0
9
9
  Requires-Dist: wget>=3.2
10
10
  Description-Content-Type: text/markdown
11
11
 
@@ -0,0 +1,23 @@
1
+ spells/.ruff_cache/.gitignore,sha256=njpg8ebsSuYCFcEdVLFxOSdF7CXp3e1DPVvZITY68xY,35
2
+ spells/.ruff_cache/0.8.6/17785301476771359756,sha256=gPYLG8psuOSo37IefLzTu5wgRrbKLWEfhWP_yLC4VIY,159
3
+ spells/.ruff_cache/CACHEDIR.TAG,sha256=WVMVbX4MVkpCclExbq8m-IcOZIOuIZf5FrYw5Pk-Ma4,43
4
+ spells/__init__.py,sha256=0pnh2NLn9FrNKncE9-tBsighp3e8YAsN--_ovUpgWGs,333
5
+ spells/cache.py,sha256=6cl0q62erR3LCANPSfxG5-J7JQfLwNdWjzlBpfiL4IE,7174
6
+ spells/card_data_files.py,sha256=x_oiFXw-mH-xZrSLrnE6dV8f1C2mQr5iFp9skR_YGhY,7246
7
+ spells/cards.py,sha256=6stFPhJOzHqvQnkSv9cDeylRa_7L9Y8sOaowiZhzz6I,4174
8
+ spells/columns.py,sha256=HsmOSunZHs0RMyXoSo-gAVWMNZqMYs3IrO5v5-99BqM,16678
9
+ spells/config.py,sha256=Nym660bbYt4ijzC5scVJ6PVOh-4vBi4hfFusBIPxcZk,257
10
+ spells/draft_data.py,sha256=iYsfmVRPy67GS3Epv8xakYVZQZIr0JbrZMFG0_wpLm4,21380
11
+ spells/enums.py,sha256=gbwfon6tQCoKDb-m4hSaHWi9slj82yqaH3qhYMVrsck,4991
12
+ spells/extension.py,sha256=iTR_2a_elM-8q8qxeeZFV8145i97fW2c3PfE3rN-HFg,8625
13
+ spells/external.py,sha256=I-f_vMx-h2kvunUlZthsauaeDn42vcRk0wvTURfImzs,11848
14
+ spells/filter.py,sha256=J-YTOOAzOQpvIX29tviYL04RVoOUlfsbjBXoQBDCEdQ,3380
15
+ spells/log.py,sha256=3avmg65hru8K9npKLvPp1wWWxq-hoEYDUCbxqhPkKUw,2175
16
+ spells/manifest.py,sha256=ExWVk17BRw615UmvrV817xwz457yfTNdNMNE_M00aEg,8338
17
+ spells/schema.py,sha256=DbMvV8PIThJTp0Xzp_XIorlW6JhE1ud1kWRGf5SQ4_c,6406
18
+ spells/utils.py,sha256=IO3brrXVvZla0LRTEB5v6NgGqZb_rYA46XtKBURGMNk,1944
19
+ spells_mtg-0.11.16.dist-info/METADATA,sha256=G-rx2_1SCdYXRT15YWkgTNA7kKnUdqVDFMf6qvFQSy0,47370
20
+ spells_mtg-0.11.16.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
21
+ spells_mtg-0.11.16.dist-info/entry_points.txt,sha256=a9Y1omdl9MdnKuIj3aOodgrp-zZII6OCdvqwgP6BFvI,63
22
+ spells_mtg-0.11.16.dist-info/licenses/LICENSE,sha256=tS54XYbJSgmq5zuHhbsQGbNQLJPVgXqhF5nu2CSRMig,1068
23
+ spells_mtg-0.11.16.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: pdm-backend (2.4.4)
2
+ Generator: pdm-backend (2.4.5)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,19 +0,0 @@
1
- spells/__init__.py,sha256=0pnh2NLn9FrNKncE9-tBsighp3e8YAsN--_ovUpgWGs,333
2
- spells/cache.py,sha256=CjMsIOdUz43heJ4IZ5kTWfS_rAeOMFQ1qVAuq3mTWao,6192
3
- spells/cards.py,sha256=6stFPhJOzHqvQnkSv9cDeylRa_7L9Y8sOaowiZhzz6I,4174
4
- spells/columns.py,sha256=s_PYyg2QaRL6kLWFNKCBEfbMF0x7O7-Yd9SeG1ttYL4,18206
5
- spells/config.py,sha256=zpRUZ-6JKALE149L0yfeD1AgqKPaVBGHfD_TdHi-jqE,235
6
- spells/draft_data.py,sha256=1HKYjDu0O8fD2AhUL9Zgw0jVu4lKweJ4KBFMDkH5JLU,19937
7
- spells/enums.py,sha256=gbwfon6tQCoKDb-m4hSaHWi9slj82yqaH3qhYMVrsck,4991
8
- spells/extension.py,sha256=LBqGbJbe7iSRQkxJK7npkADCfzhdnIwwVvlmTn8xvjQ,8454
9
- spells/external.py,sha256=USqOtOVY7Mn39rdUPhmIeGm08s6TDnEV8-N3D14qJzE,11844
10
- spells/filter.py,sha256=J-YTOOAzOQpvIX29tviYL04RVoOUlfsbjBXoQBDCEdQ,3380
11
- spells/log.py,sha256=3avmg65hru8K9npKLvPp1wWWxq-hoEYDUCbxqhPkKUw,2175
12
- spells/manifest.py,sha256=ExWVk17BRw615UmvrV817xwz457yfTNdNMNE_M00aEg,8338
13
- spells/schema.py,sha256=DbMvV8PIThJTp0Xzp_XIorlW6JhE1ud1kWRGf5SQ4_c,6406
14
- spells/utils.py,sha256=IO3brrXVvZla0LRTEB5v6NgGqZb_rYA46XtKBURGMNk,1944
15
- spells_mtg-0.10.11.dist-info/METADATA,sha256=Z2vzKqgfKDsX85L8A26nhbRYiA83APhCgnN6DRZrQcc,47370
16
- spells_mtg-0.10.11.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
17
- spells_mtg-0.10.11.dist-info/entry_points.txt,sha256=a9Y1omdl9MdnKuIj3aOodgrp-zZII6OCdvqwgP6BFvI,63
18
- spells_mtg-0.10.11.dist-info/licenses/LICENSE,sha256=tS54XYbJSgmq5zuHhbsQGbNQLJPVgXqhF5nu2CSRMig,1068
19
- spells_mtg-0.10.11.dist-info/RECORD,,