CXu0630/mtg-print-distribution
MTG Print Distribution Expected pull rates for every Magic: The Gathering booster product (Play Booster, Collector Booster, Draft Booster, Set Booster, Theme Booster, Jumpstart, ...) across 191 sets — for each product, the expected percentage of a pack made up of every distinguishable card variant (rarity, finish, frame effects, borderless, and promo-tag combination), plus how many distinct cards are eligible for each one. Each product also carries its real current market price.… See the full description on the dataset page: https://huggingface.co/datasets/CXu0630/mtg-print-distribution.
MTG Print Distribution
Expected pull rates for every Magic: The Gathering booster product (Play Booster, Collector Booster, Draft Booster, Set Booster, Theme Booster, Jumpstart, ...) across 191 sets — for each product, the expected percentage of a pack made up of every distinguishable card variant (rarity, finish, frame effects, borderless, and promo-tag combination), plus how many distinct cards are eligible for each one. Each product also carries its real current market price.
Computed from MTGJSON's weighted booster/sheet collation data, with prices from TCGplayer.
Disclaimer
Magic: The Gathering is a trademark of Wizards of the Coast, LLC. This is an unofficial dataset, not produced or endorsed by Wizards of the Coast.
Data sources
- [MTGJSON](https://mtgjson.com) (MIT License, © 2018–Present Zach Halpern) — the primary source: every set's
booster/sheetsweighted collation structure, and each card'srarity,frameEffects,borderColor, andpromoTypes. - [Scryfall](https://scryfall.com) — canonical set identity/metadata only (
scryfall_id,code,name,set_type,release_date,card_count,digital,block,parent_set_code,icon_svg_uri). - [TCGplayer](https://www.tcgplayer.com) — each product's real market price (
booster_products.price_usd), fetched from TCGplayer's public, unauthenticatedpricepointsendpoint at export time. This is a live market snapshot, not part of the print-run probability model, and isn't refreshed automatically — seeprice_fetched_atand "Known limitations". - Everything else (every rate, pool size, and the synthetic
defaultconfig described below) is computed from MTGJSON and Scryfall.
Configs (tables)
This is a normalized, multi-table dataset — four separate configs, linked by id,:
sets (192 rows: 191 real sets + 1 synthetic "default")
PK: scryfall_id
│
▼
booster_products (386 rows)
PK: id · FK: set_scryfall_id → sets.scryfall_id
│
▼
booster_rates (8,063 rows) <- default config
PK: id · FK: booster_product_id → booster_products.id
data_gaps (22 rows) — standalone; sets/products the pipeline couldn't
fully resolveLoad a specific table:
from datasets import load_dataset
rates = load_dataset("CXu0630/mtg-print-distribution", "booster_rates")["train"]
sets = load_dataset("CXu0630/mtg-print-distribution", "sets")["train"]
products = load_dataset("CXu0630/mtg-print-distribution", "booster_products")["train"]Or with pandas, joining across tables:
import pandas as pd
rates = pd.read_parquet("hf://datasets/CXu0630/mtg-print-distribution/booster_rates.parquet")
products = pd.read_parquet("hf://datasets/CXu0630/mtg-print-distribution/booster_products.parquet")
sets = pd.read_parquet("hf://datasets/CXu0630/mtg-print-distribution/sets.parquet")
full = (
rates
.merge(products, left_on="booster_product_id", right_on="id", suffixes=("", "_product"))
.merge(sets, left_on="set_scryfall_id", right_on="scryfall_id", suffixes=("", "_set"))
)
mkm_play = full[(full["code"] == "mkm") & (full["pack_code"] == "play")]
print(mkm_play[["rarity", "finish", "frame_effects", "borderless", "rate_percent"]]
.sort_values("rate_percent", ascending=False))Schema
sets
booster_products
One row per purchasable booster-pack product per set (e.g. a set's Play Booster and Collector Booster are two rows).
booster_rates (default config)
One row per distinguishable card-variant bucket for a booster_products row. Rates for one product sum to ~100%.
The default set (sets.code == "default") is a synthetic all-time average across every real Play/Collector Booster, deliberately keyed on just (rarity, foil, borderless) — frame_effects and promo_tags are always "none" there, since the finer dimensions are mostly set-specific one-offs that don't generalize.
data_gaps
Sets or products the pipeline couldn't fully resolve (no Scryfall match, no MTGJSON booster data, or an ambiguous product match).
Known limitations
- `avg_card_rate_percent` is an average, not exact per-card odds since the same card can be drawn from different sets.
- The `finish`/`frame_effects`/`promo_tags` split relies on naming conventions, not a strictly documented MTGJSON guarantee (e.g. etched finishes are detected by sheet name containing "etched"). See the source repository for full reasoning and edge cases.
- Welcome Boosters aren't modeled — MTGJSON has no collation data for them.
- `price_usd` is a point-in-time market snapshot (see
price_fetched_at), fetched from TCGplayer's public, unauthenticatedpricepointsendpoint — not their documented API, so it could change format without notice. Prices fluctuate day to day and are not refreshed automatically on a schedule; ~10% of products have no resolvable price (old/discontinued products TCGplayer or MTGJSON'ssealedProductdata don't cover). - Snapshot as of the last build — a set released after that won't appear until re-built from a refreshed MTGJSON pull.
