Shiv item decisions — research dataset

Phase 1 of 5 · built 2026-08-25 16:52 UTC

This page contains no advice. Phase 1 builds the data foundation only: it turns each Shiv item purchase into a record of what was knowable at the moment of the purchase and what happened in the two and five minutes afterwards. Nothing has been compared, tested for significance, or ranked. There is deliberately no "buy this instead of that" here, and there will not be until the statistical work in a later phase says something survives.

Current size: 54,824 decision records from 3,393 hydrated matches. Collection is incremental and deliberately slow; this page reflects whatever exists right now.

Collection

HeroShiv (id 19, resolved from /v1/assets/heroes/by-name/Shiv)
Index endpoint/v1/matches/metadata?hero_ids=19&game_mode=normal
Hydrate endpoint/v1/matches/metadata?match_ids=… with info, player info, items, stats, objectives and death details
Matches indexed7,162
Matches hydrated3,393
Hydration failures6
Excluded at discovery0
Match window covered2026-07-06 00:33:09 to 2026-08-18 03:52:27
Modes keptRanked (2,552), Unranked (841)
Date buckets2026-07 (658), 2026-08 (2,735)
Skill rank available2,986 of 3,393 (88.0%)

Two passes are mandatory rather than tidy: combining hero_ids with any player include returns HTTP 500 from the API, verified against every include combination. Hydration therefore addresses matches by explicit id, which is exactly what the index pass produces. Street Brawl and any non-standard mode are dropped at discovery, the same rule the coaching history uses.

The dataset

Decision records54,824
Matches represented3,392
Distinct Shiv players3,047
Distinct items bought147
Decisions per match16.2
Median purchase time16:42

By slot

SlotDecisionsShare
spirit31,92458.2%
vitality21,07338.4%
weapon1,8273.3%

By tier

TierDecisionsShare
112,02521.9%
219,00134.7%
315,01627.4%
48,78216.0%

Most-bought items

ItemSlotTierCostTimes bought
Radiant Regenerationspirit33,2003,271
Healbanevitality21,6003,094
Mystic Regenerationspirit18003,071
Extra Chargespirit18003,023
Compress Cooldownspirit21,6002,515
Extra Regenvitality18002,473
Mystic Vulnerabilityspirit21,6002,409
Torment Pulsespirit33,2002,380
Superior Cooldownspirit33,2002,129
Escalating Exposurespirit46,4002,114
Healing Boostervitality21,6002,070
Dispel Magicvitality33,2001,962

Frequency only. A common purchase is not a good one, and this page makes no claim either way.

What one record looks like

One real record, unedited: Slowing Hex bought at 6:15 in match 100108683.

State field (knowable at purchase)Value
net_worth4050
net_worth_sourcestats_at_or_before
stats_staleness_s15
level8
kills0
deaths1
assists1
deaths_last_2m1
last_hits16
player_damage1626
team_soul_advantage-1160
lobby_economy_rank3
objectives_lost0
objectives_taken0
existing_item_count3
existing_items[3862866912, 1439347412, 3776945997]
ability_points_spent6
enemy_heroes[1, 16, 31, 35, 63, 66]
enemy_items_observed[7409189, 84321454, 98582110, 668299740, 968099481, 1009965641, 1437614329, 1548066885, 1998374645, 2010028405, 2356412290, 2462046703, 2829638276, 3399065363, 3403085434, 4139877411]
Outcome field (after the purchase)Value
horizon_ok_2m1
horizon_ok_5m1
stats_samples_2m0
stats_samples_5m1
deaths_next_2m0
deaths_next_5m0
souls_gained_next_2m
souls_gained_next_5m2609
damage_next_5m1414
damage_taken_next_5m1350
team_soul_change_next_5m555
objective_events_next_5m3
won_match0

Data quality

Net worth measured from a stats sample50,448 of 54,824 (92.0%)
Net worth unobserved (purchase before the first sample)4,376 (8.0%) — stored as NULL, never as 0
API net_worth_at_buy agrees with our value50,189 of 50,448 measured (99.5%)
Median state staleness113 s
Worst state staleness299 s
Lobby economy rank resolvable50,448 (92.0%)
Full 2-minute future exists53,135 (96.9%)
Full 5-minute future exists49,457 (90.2%)
Soul gain unmeasurable at 2 min (no stats sample in window)25,574 (46.6%)
Soul gain unmeasurable at 5 min5 (0.0%)
Death count unmeasurable at 2 min299 (0.5%)
Records with both a measured state and a full 5-minute future45,081 (82.2%)

The API's items.net_worth_at_buy is an as-of join onto the stats series, and for a purchase made before the first sample it back-fills the player's final net worth. Measured on live data during design: 44 of 44 purchases after the first sample matched an independent as-of lookup exactly, and 12 of 12 purchases before it carried the end-of-match value. The field is therefore recomputed here and the API value kept only as the cross-check reported above.

What this dataset cannot do

Stats are sampled every ~180 seconds. Souls, damage, level and lobby economy rank in a state are up to three minutes stale; the exact lag is stored per record as stats_staleness_s. Purchases, deaths and objectives carry exact timestamps and do not have this problem, which is why they are the load-bearing measurements.

A two-minute outcome window often contains no stats sample. Soul and damage deltas are then recorded as NULL rather than 0. A fabricated zero would read as "this purchase earned nothing", which is a different claim from "we cannot measure it".

Skill rank is mostly unavailable. average_badge is frequently null on this endpoint, and extra_player_columns=player_rank_initial_display_rank returns HTTP 500. Per-player rank exists only in the ClickHouse SQL endpoint, which allows 2 requests a minute. Until that is joined in, the dataset cannot be split by skill bracket, so any pattern found in it may simply be a pattern of the average lobby.

The sample is only as spread out as the collector has managed so far. Shiv is popular enough that a single 200-row index page can cover twenty minutes of real time, so a naive backwards walk never leaves the current hour - the first run collected 105 matches spanning 26 minutes. The collector now steps its cursor by a fixed stride and hydrates an evenly spaced slice of what it has indexed, but the window actually covered is reported above and should be read before trusting anything about "the current meta".

There is no patch identifier in the API. Records are bucketed by calendar month as a proxy. The raw start time is stored, so a real patch calendar can be applied later without refetching anything.

Purchases made before the first stats sample have no observable net worth. Those states are stored with NULL rather than a guess, and the rate is reported above. They are early-game buys, so excluding them biases the set toward later decisions - a trade-off to make deliberately, not silently.

Ability points are not purchases and are excluded. The items array mixes shop purchases, ability points and weapon rows under one name. They are separated using the asset catalogue's item type. Ability choices are a real strategic decision, but a different one, and are out of scope.

This is observational data. Nobody was assigned an item. Players who buy a given item differ from those who do not in ways this dataset does not record - and skill, the most important of them, is exactly the variable that is missing. Any comparison made later has to carry that caveat, and no comparison has been made yet.

Correctness checks

18 tests, all passing.

TestWhat it holds down
test_state_is_a_pure_function_of_the_pastRebuilds every state from a match physically truncated at the decision time and requires an identical result. Any leak - a final stat, a later purchase, an objective that has not fallen yet - makes the two disagree.
test_net_worth_is_recomputed_not_taken_from_the_leaky_api_fieldPins the API back-fill bug: the fixture reproduces it, extraction must not use it.
test_no_final_match_values_appear_in_any_stateThe synthetic match ends on absurd values (99,999 souls, 50 kills) so a leak is unmistakable rather than plausible.
test_state_reconstructed_before_the_purchaseOwned items exclude the item being bought, and exclude anything already sold.
test_enemy_build_reconstructionEnemy items are those bought by the decision time, team-relative, abilities excluded.
test_death_countingExact death timestamps, correct half-open windows.
test_horizon_flagsA purchase too near the end is marked, not silently truncated.
test_windows_without_a_sample_are_none_not_zeroAn unmeasurable window must not be reported as a measured zero.
test_soul_and_damage_deltasDeltas read from the correct pair of samples.
test_team_advantageTeam soul advantage and lobby economy rank at the cutoff.
test_objective_eventsObjectives before and after the cutoff; a never-destroyed objective is not counted as destroyed at t=0.
test_purchase_ordering_and_timestampsTime order, stable tie-breaking, and sequence numbers that survive reclassification.
test_abilities_and_repeat_acquisitions_are_not_decisionsAbility points, and re-acquiring something already owned, are not new choices.
test_upgrade_id_is_not_used_to_classify_purchasesPins a bug that was live in this code: the API's upgrade_id means different things in different patch eras (0 on old shop rows, 1 on current ones), and using it as the purchase filter silently discarded every modern purchase. Classification comes from the item catalogue and nothing else.
test_hero_identification_and_player_selectionOnly Shiv players, both of them when both teams have one, and no teammate rows.
test_parallel_array_payload_shape_is_acceptedBoth payload encodings produce identical decisions.
test_never_destroyed_objectives_are_not_counted_at_zeroGuards the sentinel value.
test_won_match_and_decision_idsResult attribution and id uniqueness.

Mutation checks

Deliberate bug introducedResult
Classify purchases by upgrade_id again (the bug that actually shipped)caught by 15 tests
Trust the leaky API net worthcaught by 1 test
Read the next stats sample instead of the previous onecaught by 5 tests
Observe every enemy item regardless of timecaught by 2 tests
Count sold items as still ownedcaught by 1 test
Report 0 for an unmeasurable windowcaught by 1 test

A test suite that passes proves nothing on its own. Each of these bugs was introduced into the extractor on purpose and the suite was re-run; all six were caught, and the code was restored. The first one is not hypothetical: it was the real behaviour of this extractor against live data, and it produced zero decisions from 105 perfectly good matches.

What comes next

Phase 2 is the statistical engine: grouping decisions by comparable game states, measuring outcome differences with confidence intervals and correction for multiple comparisons, and reporting how much of any difference survives once the obvious confounders are accounted for. Until that exists and reports something that holds up, this dataset describes decisions - it does not evaluate them.

← Match timeline library