WikifitaGitHub live67e8de5
pesquisa · kaggle/pokemon_tcg_parquet_dataset

Pokémon TCG AI Battle — Parquet Dataset Format

Day-partitioned Parquet dataset: schema (90 columns, fixed_size_list vectors), row groups as the physical I/O unit, streaming column projection, aux targets, per-row metadata, manifest sidecar and SQLite catalog integration.

Baixar raw

Pokémon TCG AI Battle — Parquet Dataset Format

Boundary

This page documents the current on-disk dataset format used by pokemon_tcg_training_pipeline. The historical .npy shard format lives in pokemon_tcg_data_pipeline and is superseded by everything on this page.

Terminology on this page (row, episode, row_group, aux_valid, manifest, source='remote', etc.) is defined in pokemon_tcg_glossary.

Layout

One Parquet file per calendar day, plus a JSON manifest sidecar per file:

data/bc_data/
  2026-07-28.parquet
  2026-07-28.manifest.json
  2026-07-29.parquet
  2026-07-29.manifest.json
  ...
  2026-08-01.parquet
  2026-08-01.manifest.json

Each Parquet file is discovered and validated through the SQLite catalog table datasets (see pokemon_tcg_sqlite_schema_current). A row in datasets carries the file path, schema_version, row count, SHA256, and aux_targets flag.

Row semantics

One Parquet row is one classified decision by one side of one match. Every row carries:

  • the full encoded observation (state entities, options, masks),
  • the action label y and its legality mask action_mask,
  • four aux targets (aux_ko, aux_prize_delta, aux_terminal, aux_return) with aux_valid gating them,
  • metadata sufficient to reconstruct temporal order and provenance (episode_id, side, step_id, decision_id, substep, new_episode, terminal, is_self, player_name, opponent_name, day_id, deck hashes).

Currently the largest days sit at ~813k rows. Data volume in the SQLite catalog:

dayrows
2026-08-01813,124
2026-07-31810,573
2026-07-30812,765
2026-07-29811,539
2026-07-28813,079

Column schema

90 columns total. Types (pyarrow):

Scalar state

columntypenotes
select_typeint32wire select-type ordinal
select_contextint32wire select-context ordinal
day_index_normfloatnormalized competition-day scalar
opponent_agent_bucketint320–10 meta bucket (Elo decile + UNKNOWN)
opponent_deck_bucketint320–10 meta bucket

State entity vectors (fixed_size_list<float>[K] or <int32>[K])

columnlengthpurpose
cls_scalars1913 board/turn + 5 select-dynamics + 1 our-turn offensive buff
effect_id / effect_mask2 / 2active effect tokens
self_deck_id/mask/flag60 / 60 / 60our decklist tokens (drawable flag)
opp_deck_id/mask/flag60 / 60 / 60opponent's decklist
self_prize_id/mask6 / 6our prize row
opp_prize_id/mask6 / 6opponent prize row
self_hand_id/mask30 / 30our hand (public+hidden)
opp_hand_id/mask/flag30 / 30 / 30opponent hand (only visible cards concrete)
self_discard_id/mask60 / 60public discard
opp_discard_id/mask60 / 60public discard
stadium_id/mask2 / 2active stadium slot per side
self_unit_top_id/preevo_id/tool_id/energy_id/attr/mask9 / 18 / 36 / 36 / 216 / 91 active + 8 bench units (attr has 24 floats/unit)
opp_unit_*(same shapes)opponent field

Option stream (up to MAX_OPTIONS = 192 legal actions per decision)

columnlengthnotes
opt_attr6912192 × 36 float feats per option (would_ko trio, structural, already-picked flag)
opt_verb192option-type ordinal (0..16)
opt_src_pos / opt_tgt_pos192 / 192global state-token positions the option references
opt_src_card / opt_tgt_card192 / 192resolved card IDs
opt_attack_id192attack identity for attack options; 0 = no-attack padding
action_mask193legal-action mask, index 192 = SUBMIT_ACTION
opt_group193equivalence-class ids used by the --dedup collapse

Meta buckets (Elo decile + UNKNOWN, 0..10; applied per token slot)

self_deck_meta_bucket, self_hand_meta_bucket, self_discard_meta_bucket, self_unit_top_meta_bucket, self_unit_preevo_meta_bucket, self_unit_tool_meta_bucket, self_unit_energy_meta_bucket, and their opp_* twins — each fixed_size_list<int32>[K] matching the corresponding _id column.

Label + supervision

columntypenotes
yint32true action index (0..192)
is_attackint8val-metric flag
outcomeint8game outcome sign in {-1,0,1}
rewardfloatnormalized episode reward
is_selfint8row belongs to our-side agent (per bc_self_aliases)

Aux targets

columntypepurpose
aux_koint8did the decision produce an immediate KO? BCE target
aux_prize_deltafloatprize delta at the decision boundary. MSE target
aux_terminalint8is this the terminal row? BCE target
aux_returnfloatnormalized return-to-go. MSE target
aux_validint80/1 mask: whether targets exist and should contribute to loss

Sequential metadata (needed by TBPTT and val split)

columntypenotes
episode_idint64one match
sideint80 or 1
step_idint32raw engine step
decision_idint32groups substeps of the same multi-select
substepint8order inside a multi-select decision
new_episodeint8reset boundary flag for scratch memory
terminalint8final row flag
day_idint32FK into sqlite:days
player_namestringagent name from Kaggle replay
opponent_namestringopponent name
player_deck_hashstringdeck fingerprint
opponent_deck_hashstringdeck fingerprint

Row groups

Row groups are the physical read unit inside a Parquet file. They matter operationally because the trainer's cache retention, SSD spill, and streaming projection are all row-group-scoped, not row-scoped.

For a representative day (2026-08-01):

n_row_groups   23
n_rows_total   813,124
rows / row_group   ~35,000 (34,613,862 bytes for row_group 0)

Every day currently sits around ~800k rows and ~23 row groups. The row-group boundary is chosen by the writer at build time and is invariant afterwards — the loader cannot re-chunk without rewriting the file.

Because the KV cache retains row groups (not rows), the effective working-set size is (bytes_per_row_group × resident_row_groups). With _HOT_ZONE_FRACTION=0.6 and 5 row groups resident (typical steady-state), the cache holds ~5 × 35k = ~175k rows warm at any time.

Streaming reads

The trainer opens each Parquet file with pyarrow.dataset.dataset(paths, format="parquet") and iterates via a Scanner that:

  • projects only the columns the current training step needs (drops everything else),
  • reads by row group in order (or in a scan-key sequence when --top-elo / --max-rows-per-day prune episodes),
  • reads ahead internally (no separate manual prefetch thread — pyarrow's Scanner overlaps I/O and decode on its own).

Because reads are column-projected, dropping unused metadata columns from a run is essentially free. The exception is the TBPTT path, which projects everything the model needs plus episode_id, side, step_id for lane construction.

Row-group-level operations (retention, promotion, eviction, SSD spill) live in the KV cache — see pokemon_tcg_kv_cache_hierarchical.

Aux target contract

The trainer requires aux_valid, aux_ko, aux_prize_delta, aux_terminal, aux_return to be present. It refuses to start if any is missing. When aux_valid[i]==0 for a row, the four losses on that row are ignored (masked); the row still contributes to the primary cross-entropy on y.

A dataset's aux_targets column in sqlite:datasets records 1 when the builder emitted aux columns, 0 otherwise. Days built without aux cannot be used by the current trainer.

Manifest sidecar

data/bc_data/<date>.manifest.json records the provenance of the corresponding Parquet:

  • source zip SHA256 (the Kaggle replay archive)
  • encoder version and card-catalog SHA256
  • aux-target contract version
  • writer parameters (bc_would_ko, bc_wk_nvar, bc_both_sides, bc_self_aliases, bc_flush, bc_ep_timeout)
  • row count
  • build timestamp

The trainer does not read the manifest at run time; it is used by the dataset builder for idempotency (already-built days skip) and by tcg-data for the catalog view.

SQLite catalog

Every Parquet file appears exactly once in the SQLite table datasets. The trainer resolves --days 2026-07-28,2026-08-01 (or --last-n-days N, or --all-days) by selecting rows from datasets joined against days, then opening the resolved path values. If a Parquet exists on disk but is missing from datasets, the trainer will not see it.

Population and full schema in pokemon_tcg_sqlite_schema_current.

Historical: NPY shards

Before 2026-08 the pipeline used .npy shards per episode, described in pokemon_tcg_data_pipeline. That page is preserved for context; do not build against it. The current builder writes Parquet directly.

Provenance / anchor commits

  • 277a5b5 (2026-07-25) — feat(D): option compaction, episode metadata, episode-level val split — landed the sequential metadata contract (originally as episode_meta.npy).
  • a942373 (2026-08-03) — sidecar removed, aux heads + meta features + parquet pipeline — the pivot: dataset writer moved from .npy shards to day-partitioned Parquet; aux target columns and meta buckets integrated as first-class row columns.
  • 504118d (2026-08-03) — smoke pipeline validated: strict semantics, competition_day, streaming TBPTT — first end-to-end validation of the Parquet path.
  • e772fe0 (2026-08-06) — train loader: KV-style parquet cache — row groups become the physical retention unit downstream, cementing the row-group boundary as a load-time contract.

Full timeline in pokemon_tcg_repository_timeline.

Cross-references