Skip to content

Time-Series & DataFrames

gem samples entity state during parsing and assembles per-minute advantage curves, position logs, player snapshots, and event tables. This guide shows how to use those outputs from the high-level API and, when needed, from the lower-level extractor API.

Per-minute advantage curves

match.radiant_gold_adv and match.radiant_xp_adv are lists of integers, one entry per game minute. Positive values favor Radiant; negative values favor Dire.

python
import gem

match = gem.parse("my_replay.dem")

print("Minute  Gold adv  XP adv")
for minute, (gold, xp) in enumerate(zip(match.radiant_gold_adv, match.radiant_xp_adv)):
    gold_sign = "+" if gold >= 0 else ""
    xp_sign = "+" if xp >= 0 else ""
    print(f"{minute:>6}  {gold_sign}{gold:>8,}  {xp_sign}{xp:>7,}")

The advantage curves use total earned gold and XP, not current spendable gold or current level XP:

FieldEntityBehavior
m_iTotalEarnedGoldCDOTA_DataRadiant/DireMonotonic; use for gold advantage
m_iTotalEarnedXPCDOTA_DataRadiant/DireMonotonic; use for XP advantage
m_iGoldCDOTAPlayerControllerSpendable cash; drops when items are bought
m_iCurrentXPHero entityResets to 0 on level-up

DataFrame export

gem.parse_to_dataframe() returns a dict of pandas DataFrames:

python
import gem

frames = gem.parse_to_dataframe("my_replay.dem")

print(sorted(frames))
players = frames["players"]
combat = frames["combat_log"]

Available DataFrames:

KeyContents
playersPer-player sampled state with terminal scalar stats repeated on each row
players_minutePer-player series resampled to one row per game minute
positionsPer-player world (x, y) positions over time
combat_logRaw normalized combat log entries
wardsWard placement events with coordinates
objectivesTyped Gem objective rows such as towers, barracks, Roshan, tormentors, couriers
opendota_objectivesOpenDota-shaped unified objective timeline
chatChat messages
matchSingle-row match metadata and final status bitmasks
radiant_advantageRadiant gold/XP advantage per minute
draftPick and ban events
teamfightsGem teamfight windows with participant stats
opendota_teamfightsOpenDota-compatible 3+ death temporal teamfight windows
smoke_eventsSmoke of Deceit usages and grouped heroes
courier_snapshotsCourier state over time
neutral_item_findsNeutral item find events from DOTA_UM_FoundNeutralItem
player_kills_logPer-player kill log rows
player_purchase_logPer-player purchase log rows
player_runes_logPer-player rune pickup log rows
player_buyback_logPer-player buyback log rows

Players table

python
df = frames["players"]

print(df[["player_id", "hero_name", "tick", "gold", "net_worth", "lh", "dn"]].head())

The players table contains sampled state rows. End-of-game scalars such as final_net_worth, final_last_hits, kills, deaths, assists, hero_damage, and lane_role are repeated on each sampled row for convenient grouping.

Positions table

python
positions = frames["positions"]
axe_positions = positions[positions["hero_name"] == "npc_dota_hero_axe"]

print(axe_positions[["tick", "x", "y"]].head())

Positions are split into a dedicated table so movement-heavy analysis does not bloat the main player-state table.

Plot gold advantage

python
import matplotlib.pyplot as plt
import gem

match = gem.parse("my_replay.dem")

minutes = list(range(len(match.radiant_gold_adv)))
gold_adv = match.radiant_gold_adv

fig, ax = plt.subplots(figsize=(12, 4))
ax.plot(minutes, gold_adv)
ax.axhline(0, color="gray", linewidth=0.8)
ax.set_xlabel("Game minute")
ax.set_ylabel("Radiant gold advantage")
fig.tight_layout()
fig.savefig("gold_adv.png", dpi=150)

Low-level player sampling

When you need a custom sampling interval, attach PlayerExtractor directly to a ReplayParser:

python
from gem.extractors.players import PlayerExtractor
from gem.parser import ReplayParser

parser = ReplayParser("my_replay.dem")
players = PlayerExtractor(sample_interval=150)  # every 150 ticks, roughly 5 seconds
players.attach(parser)

parser.parse()

series = players.time_series(player_id=0)

print(series.ticks[:5])
print(series.gold_t[:5])
print(series.x_t[:5])
print(series.y_t[:5])

PlayerTimeSeries fields include player_id, ticks, gold_t, total_earned_gold_t, total_earned_xp_t, net_worth_t, lh_t, dn_t, xp_t, hp_t, mana_t, x_t, y_t, total_hero_damage_t, total_hero_healing_t, total_deaths_t, and total_stuns_t.

For most analysis code, prefer gem.parse() or gem.parse_to_dataframe() and use the lower-level extractor only when you need a different sampling interval or custom parser callbacks.