You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Narrow tomes that mix old and compact csds player tables - #42
csgo-ppp is about to write player_vector and player_status in a compact format: integers as int8, int16 or int32, floats as float32, place_name as a dictionary, and no pandas metadata. Files already written keep int64, float64 and strings. Older files also hold some flags (burst_mode and is_silenced, and flags in player_death, other_death and bomb_defuse) as 0 and 1 in int64 columns declared Int64, where newer ones hold bool. This PR makes dsdk's tomes handle all of these, alone or mixed.
Mixed tomes are narrowed, not widened. Where some matches or pages hold a column as int64 or float64 and others narrower, build_basic_tomes and the loaders (get_dataframe with pandas or polars, and scan) cast the wide values to the narrow type. Integers go to the widest narrow type seen, float64 to float32, and a float64 that holds whole numbers (player_id_fixed in some csds versions) to the integer type. Before this, pyarrow and pd.concat widened everything back to int64 and float64, which gives up the memory the compact files save.
Flags are narrowed to bool. Where some files hold a column as bool and others as integers, the integers are cast to bool: 0 to False, 1 to True. Before this, a page holding both couldn't be written (pd.concat made an object column of bools and ints, which pyarrow refused), so a tome spanning those csds versions failed to build, on master too.
Every cast is checked. A value that doesn't fit raises NarrowingError, which names the column, the match (on load, the page and row) and the value. That covers overflow, underflow, a float that isn't a whole number (or is NaN or infinite) for an integer column, a finite float beyond float32's range, and a flag other than 0 or 1.
current_ammo stays int64 in a mixed tome, on every page (the compact files' int16 is widened to it, exactly). Old files hold 4294967295 there for an empty magazine, which int16 can't hold, and dsdk doesn't change values.
Values don't change, except that a float64 narrowed to float32 is rounded to the nearest float32. The raw floats read from the demo convert exactly; the derived ones move by at most 2^-24 relative, as they do in the compact files.
Missing values stay missing. A missing column (player_controller_id is absent from some 2023 files) is filled with nulls at the narrow type: pandas' nullable Int8, not float64. A flag narrowed from a column declared Int64 is pandas' nullable boolean.
Dictionary columns are decoded as each match is read, so a page holding compact player_status is built with pyarrow instead of falling back to pandas. pandas reads place_name as strings, which is what pd.concat gives categories that differ between matches.
widen=True, opt-in, on get_dataframe and scan returns 64-bit integers and float64 for analysis code; flags stay bool. Narrow integers wrap in element-wise arithmetic: an int16 money * 5 gives 14,464 for 16,000.
Tomes of old files only that built before build and load exactly as before (checked below).
Where narrowing happens
Builder. Each page narrows its tables before joining them, on the pyarrow path and in the pandas fallback. When the tome is finished, every page not yet at the tome's types is read back, cast to them and written again. That covers a page written before the first compact match turned up, and one narrowed to int8 before a later match brought int16. It also covers a page holding a value int8 or int16 can't hold: that page keeps the column wide until the end, since a later match may bring a wider type. So every page holds the tome's types, and a value that doesn't fit them stops the build instead of every later load.
Loaders. A column whose type differs across pages this way is read page by page with pyarrow and cast (pandas). For polars, the differing pages' values are checked with pyarrow when the scan is created, then cast lazily.
make_tome is unchanged. It joins a page's frames with pd.concat, so a page that mixes old and compact matches is widened there, and a page mixing bool with Int64 flags still fails there. The loaders still narrow across its pages.
docs/tome-data-model.md has a new section with the rules. CHANGELOG: v3.3.1 was tagged with the entries of #39 to #41 still under "Unreleased", so this titles them 3.3.1 and starts a new Unreleased section.
How it was tested
make test: 306 passed on pandas 3.0.6 with polars, 306 on pandas 2.3.3, and 260 passed and 46 skipped without polars. make lint: 10.00/10, black clean.
New tests in narrowing_test.py:
compact player_vector and player_status matches derived from dsdk's own fixtures, with the old fixtures' Int64 0/1 flags against compact bool flags;
a mixed tome (an old page, a mixed page, a compact page) built and read with pandas and polars;
overflow, underflow, float32 overflow and a non-whole player_id_fixed each stop the build, both when a page is narrowed as it is built and when it is narrowed at the end;
the widest narrow type wins: an int8 match and an old one on a page, an int16 match later, gives int16 on every page (from the first Copilot review);
flags: 0/1 Int64 with bool narrows to bool in pandas and polars, a missing value stays missing, a 2 stops the build and fails a load, and tomes whose flags are all Int64 or all bool load as before;
current_ammo's 4294967295 kept, and a missing column;
pages built with pandas narrowed the same as pages built with pyarrow;
narrowing and its errors on load, and widen.
Existing tests changed on purpose:
Categories that differ now build with pyarrow (still equal to make_tome).
The loader cases "int32 then int64" and "bool then int" moved to the narrowing tests: the first now loads as int32, and the second, which holds a 2, now raises.
The pandas-fallback builder test forces the fallback, since bool against int64 no longer triggers it.
The same output as master. I built tomes with master and with this branch, then compared every page file (data and metadata) and every column as loaded by master's loaders and by this branch's (pandas and polars, on both builds' pages):
dsdk's 3 fixtures, all 31 channels, with 256 MB and 0.05 MB pages: identical.
14 real old-format matches (4 each from the 2023, 2024 and 2026 archive, and 2 written by today's csgo-ppp), all 43 channels: 39 identical. The other 4 (player_status, player_death, other_death, bomb_defuse) fail on master because of the bool and Int64 flags; they build here, and are checked against their sources below.
Real tomes, checked against their sources. The 14 old matches, plus 4 compact copies where marked "mixed". Each match's rows in the tome equal its source file cast to the tome's type, in pandas and in polars, for every column (0 mismatches in every row of the table):
tome
matches
pages
rows
notes
player_vector, 256 MB pages
mixed, 18
10 (1 mixed, 9 rewritten at the end)
13.35M
every page at the compact types, with current_ammo int64 on every page (from the second Copilot review)
player_status, 256 MB pages
mixed, 18
10 (1 mixed, 8 narrowed at the end)
13.35M
failed before; burst_mode and is_silenced are boolean, 9.33M values each narrowed from 0/1
player_status, one match a page
mixed, 18
18 (14 narrowed at the end)
13.35M
player_controller_id is Int8, with nulls for the 2 matches that lack it
player_status, 256 MB pages
old, 14
9 (7 narrowed at the end)
11.50M
failed on master; only the flags narrow, the rest stays int64/float64
player_death
old 14 / mixed 18
1
1,946 / 2,225
failed on master; 4 flags narrowed, 1,584 values each
other_death
old 14 / mixed 18
1
433 / 491
failed on master; 3 flags narrowed, 414 values each
bomb_defuse
old 14 / mixed 18
1
40 / 45
failed on master; has_kit narrowed, 35 values
Float32 rounding shows up only in player_vector's derived columns, for example speed_2d: 6.61M of the 11.5M old values, by at most 5.96e-8 relative. No raw float changed. widen=True gave the same values at int64, Int64 and float64, and left flags bool.
csgo-ppp will write player_vector and player_status with int8 to int32
integers, float32 floats and a dictionary place_name. Where a tome mixes
those files with older int64/float64 ones, build_basic_tomes and the
loaders now cast the wide columns to the narrow types instead of widening
the narrow ones. Every cast is checked: a value that doesn't fit raises
NarrowingError naming the column, the match (or page) and the value.
current_ammo stays int64, since older files hold 4294967295 there.
- build_basic_tomes narrows each page as it builds it (pyarrow or the
pandas fallback), and narrows and rewrites at the end any page written
before the first compact match.
- read_pages_pandas and scan_pages_polars narrow across pages.
- A narrow integer column with missing values is pandas' nullable IntN.
- prepare_table decodes dictionary columns, so compact player_status
pages are built with pyarrow instead of pandas.
- widen=True on get_dataframe and scan returns 64-bit types.
Tomes of old files only build and load as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/tome-data-model.md gets a section on how old and compact csds files
load together, the checked casts, current_ammo, float32 rounding, the
nullable dtypes, narrow arithmetic and widen=True.
CHANGELOG: v3.3.1 was tagged with #39 to #41's entries still under
Unreleased, so they are titled 3.3.1, and this change opens a new
Unreleased section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the Copilot review: a page that joined an int8 match with an old
one was narrowed to int8, and stayed int8 when a later match brought
int16; an old value of 200 even stopped the build, though it fits int16.
- A page whose value doesn't fit its own matches' narrow type keeps that
column wide instead of raising.
- At the end, every page not yet at the tome's types is cast to them:
wide columns narrowed with checked casts, narrower integers widened
exactly. A value that doesn't fit the tome's type still stops the
build, naming the match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Older csds files hold flags such as burst_mode and is_silenced as 0 and
1 in int64 columns declared Int64; newer ones hold bool. A tome page
holding both couldn't be written: pd.concat made an object column of
bools and ints, which pyarrow refused, so tomes spanning those csds
versions failed to build (on master too).
The narrowing rule now casts the integer side to bool where a column is
bool in some tables or pages and integers in others, in the builder
(pyarrow path, pandas fallback, end-of-build rewrite) and both loaders.
0 and 1 become False and True, a missing value stays missing (pandas
boolean), and any other value raises NarrowingError naming the column,
the match (or page and row) and the value. widen=True leaves flags bool.
A page now keeps a column wide instead of raising only where a wider
narrow type may still come (int8, int16); bool, int32 and float32 raise
at once. Polars casts every page whose type differs from the plan's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Promote current_ammo to int64 across mixed-width pages
pureskillgg_dsdk/tome/narrowing.py:80
KEEP_WIDE currently removes current_ammo from every plan, so the final page-rewrite pass cannot make it wide. In the multi-page mixed tome exercised above, the compact-only page therefore remains int16 while old pages are int64 (the schema assertion in test_a_mixed_tome_holds_the_compact_types explicitly skips this column). The combined loader happens to promote it, but this violates the stated invariant that every built page uses the tome-wide types and forces future loads down the schema-drift path. Please plan an int64 promotion for this column when wide and narrow sources coexist, while continuing to prohibit narrowing its old values.
From the second Copilot review: current_ammo is never narrowed, so a
compact-only page kept it int16 while the other pages held int64. Loads
promoted it, but the pages broke the rule that every page holds the
tome's types, and loads took the slower page-by-page path.
Where current_ammo is int64 in some tables and a narrower integer in
others, the plan now targets int64: the narrow pages are widened, which
is exact, and old values are still never narrowed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the second Copilot review (the one point, "Promote current_ammo to int64 across mixed-width pages"): agreed, fixed in e2753d8.
Where current_ammo is int64 in some tables or pages and a narrower integer in others, the plan now targets int64 instead of leaving the column out. Narrow pages are widened, which is exact; old values are still never narrowed. The builder's end-of-build pass therefore rewrites a compact-only page to int64 too, so every page holds the same type, and the loaders cast likewise for tomes whose pages differ.
test_a_mixed_tome_holds_the_compact_types now asserts current_ammo is int64 on every page, including the compact one, and test_narrow_target covers the rule. Full suite: 306 passed on pandas 3 and 2.3.3, and 260 passed with 46 skipped without polars. On the real data, the mixed player_vector tome (18 matches, 13.35M rows) now has int64 current_ammo on all 10 pages, with 0 mismatches against the sources; fixture tomes are still identical to master.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
csgo-ppp is about to write
player_vectorandplayer_statusin a compact format: integers as int8, int16 or int32, floats as float32,place_nameas a dictionary, and no pandas metadata. Files already written keep int64, float64 and strings. Older files also hold some flags (burst_modeandis_silenced, and flags inplayer_death,other_deathandbomb_defuse) as 0 and 1 in int64 columns declaredInt64, where newer ones hold bool. This PR makes dsdk's tomes handle all of these, alone or mixed.build_basic_tomesand the loaders (get_dataframewith pandas or polars, andscan) cast the wide values to the narrow type. Integers go to the widest narrow type seen, float64 to float32, and a float64 that holds whole numbers (player_id_fixedin some csds versions) to the integer type. Before this, pyarrow andpd.concatwidened everything back to int64 and float64, which gives up the memory the compact files save.pd.concatmade an object column of bools and ints, which pyarrow refused), so a tome spanning those csds versions failed to build, on master too.NarrowingError, which names the column, the match (on load, the page and row) and the value. That covers overflow, underflow, a float that isn't a whole number (or is NaN or infinite) for an integer column, a finite float beyond float32's range, and a flag other than 0 or 1.current_ammostays int64 in a mixed tome, on every page (the compact files' int16 is widened to it, exactly). Old files hold 4294967295 there for an empty magazine, which int16 can't hold, and dsdk doesn't change values.player_controller_idis absent from some 2023 files) is filled with nulls at the narrow type: pandas' nullableInt8, not float64. A flag narrowed from a column declaredInt64is pandas' nullableboolean.player_statusis built with pyarrow instead of falling back to pandas. pandas readsplace_nameas strings, which is whatpd.concatgives categories that differ between matches.widen=True, opt-in, onget_dataframeandscanreturns 64-bit integers and float64 for analysis code; flags stay bool. Narrow integers wrap in element-wise arithmetic: an int16money * 5gives 14,464 for 16,000.Where narrowing happens
make_tomeis unchanged. It joins a page's frames withpd.concat, so a page that mixes old and compact matches is widened there, and a page mixing bool withInt64flags still fails there. The loaders still narrow across its pages.docs/tome-data-model.mdhas a new section with the rules. CHANGELOG: v3.3.1 was tagged with the entries of #39 to #41 still under "Unreleased", so this titles them 3.3.1 and starts a new Unreleased section.How it was tested
make test: 306 passed on pandas 3.0.6 with polars, 306 on pandas 2.3.3, and 260 passed and 46 skipped without polars.make lint: 10.00/10, black clean.New tests in
narrowing_test.py:player_vectorandplayer_statusmatches derived from dsdk's own fixtures, with the old fixtures'Int640/1 flags against compact bool flags;player_id_fixedeach stop the build, both when a page is narrowed as it is built and when it is narrowed at the end;Int64with bool narrows to bool in pandas and polars, a missing value stays missing, a 2 stops the build and fails a load, and tomes whose flags are allInt64or all bool load as before;current_ammo's 4294967295 kept, and a missing column;widen.Existing tests changed on purpose:
make_tome).The same output as master. I built tomes with master and with this branch, then compared every page file (data and metadata) and every column as loaded by master's loaders and by this branch's (pandas and polars, on both builds' pages):
player_status,player_death,other_death,bomb_defuse) fail on master because of the bool andInt64flags; they build here, and are checked against their sources below.Real tomes, checked against their sources. The 14 old matches, plus 4 compact copies where marked "mixed". Each match's rows in the tome equal its source file cast to the tome's type, in pandas and in polars, for every column (0 mismatches in every row of the table):
current_ammoint64 on every page (from the second Copilot review)burst_modeandis_silencedareboolean, 9.33M values each narrowed from 0/1player_controller_idisInt8, with nulls for the 2 matches that lack ithas_kitnarrowed, 35 valuesFloat32 rounding shows up only in player_vector's derived columns, for example
speed_2d: 6.61M of the 11.5M old values, by at most 5.96e-8 relative. No raw float changed.widen=Truegave the same values at int64,Int64and float64, and left flags bool.🤖 Generated with Claude Code