gem
v0.13.0

Quickstart

Get from installation to useful replay data with the high-level Python API.

Install

pip install gem-dota

Or with uv:

uv add gem-dota

Get a replay

gem.parse() expects a decompressed Source 2 .dem replay file.

You can download replays from OpenDota by opening a match and clicking "Download Replay". The Dota 2 client also stores downloaded replays under Watch -> Recent Games.

Parse a match

import gem
from gem.constants import hero_display

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

winner = "Radiant" if match.radiant_win else "Dire" if match.radiant_win is False else "?"
print(f"Match {match.match_id} | {match.duration_minutes:.1f} min | winner: {winner}")

for player in match.players:
    print(
        f"{hero_display(player.hero_name):<18}"
        f" KDA {player.kills}/{player.deaths}/{player.assists}"
        f" NW {player.net_worth:>6,}"
        f" LH/DN {player.last_hits}/{player.denies}"
    )

gem.parse() returns a ParsedMatch. Most consumers can stay at this level and work with fields such as match.players, match.combat_log, match.wards, match.fights, match.opendota_teamfights, match.draft, and match.radiant_gold_adv.

Find one player

axe = gem.find_player(match, "Axe")

if axe:
    print(axe.hero_name)
    print(axe.kills, axe.deaths, axe.assists)
    print(axe.damage_by_type)

find_player() accepts display names ("Anti-Mage"), loose names ("anti mage"), and full NPC names ("npc_dota_hero_antimage").

Export data

JSON

json_str = gem.parse_to_json("my_replay.dem", indent=2)

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

Use JSON when you want the full nested ParsedMatch shape.

DataFrames

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

print(sorted(frames))
print(frames["player_summary"].head())
print(frames["combat_log"].head())

Use DataFrames for pandas, notebooks, and ML pipelines. Every table is flat and starts with a match_id column. Common tables include player_summary (one row per player), player_timeseries, players_minute, positions, combat_log, wards, objectives, fights, neutral_item_finds, and per-player event logs. Pass include=["analysis"] or include=["opendota"] for the optional post-parse analysis and OpenDota-shaped tables; see Time-Series & DataFrames.

Parquet

paths = gem.parse_to_parquet("my_replay.dem", output_dir="./out")
print(paths[:3])

Command line

# Match summary
python -m gem my_replay.dem

# JSON to stdout
python -m gem my_replay.dem --format json > match.json

# Parquet files
python -m gem parse my_replay.dem --format parquet --output ./out

# Batch parse a folder
python -m gem batch replays/ --format parquet --output ./out --workers 4

See the CLI Reference for all options, including the report asset-cache commands used by HTML report generation.

Next steps

TaskRead
Inspect every ParsedMatch fieldFull Match Data
Work with pandas tables and time seriesTime-Series & DataFrames
Analyze combat log eventsCombat Log
Work with fight windowsFight Detection
Build custom parser callbacksCustom Extractors
Understand parser internalsParser Internals