Skip to content

Commit 337d193

Browse files
committed
added static analysis and graph zoom, and full screen mode
1 parent b510e3c commit 337d193

21 files changed

Lines changed: 275472 additions & 282576 deletions

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
venv
22
.env
3-
tmp
3+
tmp
4+
5+
__pycache__
6+
.DS_Store

dataset/.DS_Store

-6 KB
Binary file not shown.
-12.9 KB
Binary file not shown.
-9.38 KB
Binary file not shown.
-9.85 KB
Binary file not shown.

dataset/processing/join.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import pandas as pd
1717
from rapidfuzz import fuzz, process
1818

19-
2019
RAWG_NAME_COL = "name"
2120
RAWG_DATE_COL = "released"
2221
STEAM_ID_COL = "appID"
@@ -44,6 +43,29 @@
4443
"required_age",
4544
]
4645

46+
# Manual overrides: RAWG name → Steam appID.
47+
# Use this when the game was renamed, replaced, or the date mismatch is too large
48+
# for the fuzzy passes to bridge (e.g. CS:GO → Counter-Strike 2, same appID 730).
49+
MANUAL_OVERRIDES: dict[str, str] = {
50+
"Counter-Strike: Global Offensive": "730",
51+
"Total War: SHOGUN 2": "34330",
52+
"Blasphemous 2": "2114740",
53+
"Batman: Arkham City": "200260",
54+
"Grand Theft Auto V": "271590",
55+
"PlayerUnknown’s Battlegrounds": "578080",
56+
"Monster Hunter Wilds": "2246340",
57+
"Apex Legends": "1172470",
58+
"ARK: Survival Evolved": "346110",
59+
"Garry's Mod": "4000",
60+
"DayZ": "221100",
61+
"7 Days to Die": "251570",
62+
"Helldivers 2": "553850",
63+
"Anthem": "2656490",
64+
"Battlefield 2042": "1517290",
65+
"Fall Guys": "1097150",
66+
"Destiny 2": "1085660",
67+
}
68+
4769
EDITION_RE = re.compile(
4870
r"\b(goty|game of the year|definitive|remastered|remaster|deluxe|"
4971
r"complete|ultimate|enhanced|anniversary|collectors?|standard|"
@@ -107,14 +129,25 @@ def match_rawg_steam(rawg_df: pd.DataFrame, steam_df: pd.DataFrame) -> pd.DataFr
107129
)
108130
print(f"[join] RAWG rows: {len(rawg_df)} | Steam clean rows: {len(steam_clean)}")
109131

110-
# Pass 1
132+
# Pass 0 — manual overrides (RAWG name → Steam appID)
133+
rawg_df["steam_appid"] = rawg_df[RAWG_NAME_COL].map(MANUAL_OVERRIDES)
134+
rawg_df["match_type"] = None
135+
rawg_df.loc[rawg_df["steam_appid"].notna(), "match_type"] = "manual"
136+
print(f"[join] Pass 0: {(rawg_df['match_type'] == 'manual').sum():>6}")
137+
138+
# Pass 1 — exact key+year on still-unmatched rows
111139
p1 = rawg_df.merge(
112-
steam_clean[[STEAM_ID_COL, "key", "year"]],
140+
steam_clean[[STEAM_ID_COL, "key", "year"]].rename(
141+
columns={STEAM_ID_COL: "_steam_p1"}
142+
),
113143
on=["key", "year"],
114144
how="left",
115-
).rename(columns={STEAM_ID_COL: "steam_appid"})
116-
p1["match_type"] = p1["steam_appid"].notna().map({True: "exact", False: None})
117-
print(f"[join] Pass 1: {p1['steam_appid'].notna().sum():>6}")
145+
)
146+
new_exact = p1["steam_appid"].isna() & p1["_steam_p1"].notna()
147+
p1.loc[new_exact, "steam_appid"] = p1.loc[new_exact, "_steam_p1"]
148+
p1.loc[new_exact, "match_type"] = "exact"
149+
p1 = p1.drop(columns=["_steam_p1"])
150+
print(f"[join] Pass 1: {(p1['match_type'] == 'exact').sum():>6}")
118151

119152
# Pass 2
120153
missing_mask = p1["steam_appid"].isna()

dataset/processing/processing.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,20 @@ def run():
4444

4545
# Step 3: Scrape Steamcharts for every matched Steam app ID
4646
print("STEP 3: Scraping Steamcharts time series")
47-
games_dict = (
47+
_df = (
4848
matched_df[matched_df["steam_appid"].notna()]
4949
.drop_duplicates(subset=["steam_appid"])
5050
.assign(appid_int=lambda d: pd.to_numeric(d["steam_appid"], errors="coerce"))
5151
.dropna(subset=["appid_int"])
52-
.set_index("appid_int")["name"]
53-
.rename(index=int)
54-
.to_dict()
52+
.query("ratings_count >= 30")
5553
)
56-
# Keys must be plain Python ints for the URL builder
57-
games_dict = {int(k): v for k, v in games_dict.items()}
54+
games_dict = {
55+
int(r["appid_int"]): (r["name"], float(r["ratings_count"] or 0))
56+
for _, r in _df.iterrows()
57+
}
5858

5959
# Scrape game by game, append immediately to a temp CSV
60-
scrape_to_disk(games_dict, TMP_STEAMCHARTS)
60+
scrape_to_disk(games_dict, TMP_STEAMCHARTS, 0.5)
6161
sc_df = build_timeseries(TMP_STEAMCHARTS)
6262

6363
# Step 4: Final intersection to keep only games in all three sources

dataset/processing/steamcharts.py

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,17 @@
1313
import os
1414
import time
1515
from io import StringIO
16-
17-
import pandas as pd
18-
import requests
1916
import random
2017

21-
22-
USER_AGENTS = [
23-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
24-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
25-
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
26-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0",
27-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15",
28-
"Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0",
29-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36",
30-
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1",
31-
]
18+
import pandas as pd
19+
from curl_cffi import requests
3220

3321

3422
def fetch_steamcharts(app_id: int, name: str) -> pd.DataFrame | None:
3523
"""Fetch monthly player count history for a single game"""
3624
url = f"https://steamcharts.com/app/{app_id}"
37-
headers = {
38-
"User-Agent": random.choice(USER_AGENTS),
39-
}
4025
try:
41-
r = requests.get(url, headers=headers, timeout=15)
26+
r = requests.get(url, impersonate="chrome136", timeout=15)
4227
if r.status_code != 200:
4328
print(f"{name}: HTTP {r.status_code}")
4429
return None
@@ -75,17 +60,25 @@ def scrape_to_disk(games: dict, output_path: str, delay: float = 4) -> int:
7560
Number of new rows written in this run.
7661
"""
7762
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
63+
failed_path = output_path.replace(".csv", "_failed.csv")
7864

7965
safely_done: set[int] = set()
66+
failed_ids: set[int] = set()
8067
header_written = False
8168

69+
# Load permanently failed IDs from previous runs
70+
if os.path.exists(failed_path):
71+
failed_df = pd.read_csv(failed_path)
72+
failed_ids = set(failed_df["app_id"].dropna().astype(int).tolist())
73+
print(f"[steamcharts] Skipping {len(failed_ids)} permanently failed app_ids")
74+
8275
if os.path.exists(output_path):
8376
try:
8477
existing = pd.read_csv(output_path)
8578
all_ids = existing["app_id"].dropna().astype(int).tolist()
8679
if all_ids:
8780
last_id = all_ids[-1]
88-
# Drop the last game's rows because it they may be truncated
81+
# Drop the last game's rows because they may be truncated
8982
existing_clean = existing[existing["app_id"].astype(int) != last_id]
9083
existing_clean.to_csv(output_path, index=False)
9184
safely_done = set(
@@ -99,14 +92,18 @@ def scrape_to_disk(games: dict, output_path: str, delay: float = 4) -> int:
9992
except Exception:
10093
pass
10194

102-
remaining = {k: v for k, v in games.items() if int(k) not in safely_done}
95+
remaining = {
96+
k: v
97+
for k, v in sorted(games.items(), key=lambda x: x[1][1], reverse=True)
98+
if int(k) not in safely_done and int(k) not in failed_ids
99+
}
103100
print(f"[steamcharts] {len(remaining)} games to scrape: {output_path}")
104101

105102
rows_written = 0
106103
fetched = 0
107104

108-
for app_id, name in remaining.items():
109-
print(f" ({len(safely_done) + fetched + 1}/{len(games)}) {name}...")
105+
for app_id, (name, _) in remaining.items():
106+
print(f" ({len(safely_done) + fetched + 1}/{len(games)}) {name} {app_id}...")
110107
df_game = fetch_steamcharts(int(app_id), name)
111108
if df_game is not None:
112109
df_game.to_csv(
@@ -118,7 +115,14 @@ def scrape_to_disk(games: dict, output_path: str, delay: float = 4) -> int:
118115
rows_written += len(df_game)
119116
header_written = True
120117
fetched += 1
121-
time.sleep(delay)
118+
else:
119+
pd.DataFrame({"app_id": [int(app_id)], "name": [name]}).to_csv(
120+
failed_path,
121+
mode="a",
122+
index=False,
123+
header=not os.path.exists(failed_path),
124+
)
125+
time.sleep(delay + random.uniform(0, 2))
122126

123127
print(
124128
f"[steamcharts] Done: {fetched} new games fetched, {rows_written} new rows written"

0 commit comments

Comments
 (0)