DATA ANALYSIS  ·  STEAM  ·  125,855 GAMES
🎮 What makes a Steam game successful?
Defining success, testing launch-time indicators and predicting hits with SQL and Python

🎯 Goal: define indicators of a successful game, test whether game features go with them, and build a model that predicts success from what is known at launch. 📄 Data: Steam Games Dataset (Kaggle)

👤 About this notebook
I made this analysis as part of my data analytics portfolio. It shows how I work end to end: fixing a broken raw file, modelling the data in SQL, defining a fair target, testing ideas with honest statistics, building and explaining a model, and presenting the results clearly.

Mihail Deligrozev · Data analyst · Python · SQL · Power BI

⭐ Summary

🏆 Success on Steam is winner-takes-most. The top 1% of games hold 80% of all reviews, so success is defined as a top percentile, not "above average".
⚖️ Reach and quality are separate things. Owners and positive-review share are uncorrelated (Spearman 0.00), and only 26% of commercial hits are also well liked.
🧩 Consistent launch-time indicators. More Steam categories, more languages, achievements, a higher price, a longer store description and DLC go with success under all four definitions. RPG is the only genre above average under every definition.
🤖 Launch facts predict success reasonably well. For the strict hit label the model reaches AUC 0.80 on newer games, and its top 10% of predictions contain 43% of the actual hits. A developer's track record adds another +0.03 AUC.
⚠️ These are associations, not causes. Successful games can afford more languages and DLC, so some indicators follow success rather than drive it.

Numbers are from the run with default parameters. Section 8 and the appendix findings recompute them.

🛠️ How the notebook is built

📄 games.csv → 🐼 pandas (fix and clean) → 🗄️ DuckDB tables → 🧮 SQL (join, group, window) → 🐍 Python (statistics, models, charts) → 🖼️ PNG figures

Each tool does what it is best at. Every chart is also saved as a PNG in figures/ (on Kaggle: the Output tab, plus figures.zip at the end).

🧭 Contents

Section Question it answers
📦 1. Setup and loading Is the data read correctly?
🧹 2. Cleaning and features What do we keep and what do we build?
🗄️ 3. Data model and storage How is the data stored, and how small can it be without losing anything?
🔍 4. Exploratory analysis How are attention, quality and price distributed?
🏆 5. Defining success What does "successful" mean, and is the label fair to old and new games?
🧪 6. Testing indicators Which game features go with success?
🤖 7. Modelling How well can launch-time features predict success?
📌 8. Findings and limits What did we learn, and what should we not over-claim?
📎 Appendices A-D Data profile, stability checks, richer features, owner buckets

How to read it: 🧠 purple boxes explain why a step is done, 💡 blue boxes give the computed insight, ⚠️ orange boxes flag caveats. Cells starting with # Parameters hold the numbers you are most likely to change. SQL cells start with %%sql.

📦 1. Setup and loading

🧠 Why this step
The notebook should run on Kaggle and on a laptop without edits. So paths are found rather than hard-coded: the newest attached version of utils.py and the attached games.csv are located automatically. Helper functions live in utils.py so this notebook shows analysis, not plumbing.
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 95.1/95.1 kB 930.3 kB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 192.8/192.8 kB 2.6 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 880.2/880.2 kB 9.7 MB/s eta 0:00:00
Note: you may need to restart the kernel to use updated packages.
Using /kaggle/input/models/mihaildeligrozev/utils/pytorch/default/1/utils.py
Charts are saved to /kaggle/working/figures
Reading /kaggle/input/datasets/fronkongames/steam-games-dataset/games.csv
⚠️ Read with care
The header of this file has 39 names but every row has 40 values, because two names are glued together (DiscountDLC count is really Discount and DLC count). Without a fix every column after it is shifted by one, so AppID would hold game names. The header is read with csv, split, and passed to pandas.
125,855 rows x 40 columns

🧹 2. Cleaning and feature engineering

🧠 Why this step
The quality of the data determines the quality of the model. Checking gaps and duplicates first, and writing each cleaning decision into a table, makes every later number traceable. The full column-by-column profile is in [Appendix A](#sa).
chart
'/kaggle/working/figures/fig01_missing_values.png'
⚠️ Note
The 90% is arbitery and it is manualy determied
Duplicate rows:        0
Duplicate AppIDs:      0
Missing game names:    1

🧽 Cleaning steps

Step What Why
1 Flag has_website, then drop columns that are over 90% empty Almost no information, and the flag keeps the one useful signal from Website
2 Remove duplicate AppIDs and unnamed rows Avoid double counting
3 Parse release date and the owners range Estimated owners is text like "20000 - 50000"
4 Build review features Positive ratio is undefined with zero reviews, so it stays NaN
5 Parse list-like text columns Genres, tags and languages become real lists
6 Count and flag features Simple launch-time signals: languages, platforms, price, text length
Dropped 4 columns: Reviews, Metacritic url, Score rank, Movies
Rows: 125,855 -> 125,854    Columns: 40 -> 37
Missing release dates: 0
Missing owners:        0
125,854 rows, 58 columns
Name release_year owners_mid Peak CCU total_reviews positive_ratio Price
0 Black Dragon Mage Playtest 2023 0.000 0 0 NaN 0.000
1 Supipara - Chapter 1 Spring Has Come! 2016 10,000.000 0 255 0.988 5.240
2 Mystery Solitaire The Black Raven 2019 10,000.000 0 24 0.875 4.990
3 버튜버 파라노이아 - Vtuber Paranoia 2024 10,000.000 1 0 NaN 8.990
4 Maze Quest VR 2025 10,000.000 0 0 NaN 4.990

🗄️ 3. Data model and storage (DuckDB)

🧠 Why this step
Some questions are simplier to answer using SQL: success rate per genre is a join and a GROUP BY, and "how did this studio's earlier games do?" is a window function. Storing the data as proper tables in DuckDB (a database that runs inside Python, no server needed) makes those queries possible and shows how the data would live in a warehouse.

The cleaned data is stored as a small relational model in a DuckDB file (steam.duckdb). DuckDB runs inside Python, needs no server and reads pandas DataFrames directly.

games          one row per game            app_id (key), name, release_day, release_year, price,
                                           owners_low / high / mid, peak_ccu, total_reviews, positive_ratio,
                                           recommendations, median_playtime, developers, publishers
game_genres    one row per game and genre  app_id -> games, genre
game_tags      one row per game and tag    app_id -> games, tag
labels         one row per game            app_id -> games, success labels, in_study   (added in section 5)

Why separate genre and tag tables: a game has several genres. In pandas that is a list inside a cell; in SQL it is a bridge table, so "success rate per genre" becomes a plain join and GROUP BY. Why release_day: dates are stored as days since 1970, so "released at least a year earlier" is simple arithmetic in a window function (Appendix C).

💾 3.1 Storage optimisation

🧠 Why this step
pandas stores every number as 64-bit and every text value as a separate Python string by default. That is safe but wasteful: an AppID fits in 32 bits, a release year in 16, and "Indie" repeated 70,000 times can be stored once as a category. Smaller tables load faster, cost less to store and fit in memory on smaller machines, which matters once data lives in a warehouse or is shipped between teams.

The rule here is lossless only: a column is only changed if every value survives the round trip, and this is checked. Prices are kept as 64-bit floats on purpose, because 32-bit floats would turn 4.99 into 4.9899998.
All 4 tables passed the lossless check. Type changes made:
table before after columns
0 Raw file (40 columns) float64 Int8 1
1 Raw file (40 columns) int64 int16 4
2 Raw file (40 columns) int64 int32 7
3 Raw file (40 columns) int64 int8 4
4 Raw file (40 columns) object category 12
5 game_genres int64 int32 1
6 game_genres object category 1
7 game_tags int64 int32 1
8 game_tags object category 1
9 games float64 Int32 3
10 games int32 int16 1
11 games int64 int16 1
12 games int64 int32 5
chart
'/kaggle/working/figures/fig_storage_memory.png'

📦 3.2 Which file format?

🧠 Why this step
Memory is one side; the file on disk is the other. CSV is readable everywhere but stores numbers as text and has no types. Parquet stores each column together, compressed and typed, which is why data lakes use it. DuckDB keeps everything in one compressed database file that SQL can query directly. The same three tables are written in each format and read back, to compare size and speed.
size_mb read_s vs_csv
format
Parquet (zstd) 10.193 0.190 0.258
CSV + gzip 11.192 1.093 0.283
DuckDB file 12.595 0.361 0.319
Parquet (snappy) 12.982 0.330 0.328
CSV 39.525 0.859 1.000
chart
💡 Insight
lossless type changes cut the stored tables from 148.9 MB to 53.4 MB in memory. On disk the same tables take 39.5 MB as CSV and 10.2 MB as Parquet (zstd) (4x smaller), and columnar formats also keep the types, so nothing has to be parsed again on load.
⚠️ Read with care
Read times come from a single run and vary between machines; the size differences are the robust result. Category types also need care: joining or concatenating two categoricals with different categories silently falls back to text.
table_name n_rows values_per_game
0 games 125854 NaN
1 game_genres 338583 2.880
2 game_tags 1180141 14.160

🔍 4. Exploratory data analysis

Most Steam games get almost no attention, so every attention metric is extremely right-skewed. The first charts show how skewed, and the later ones check that the candidate success metrics agree with each other.

🧠 Why this step
"Successful" only means something against a baseline. Before defining it we need to know what a typical game looks like, how unequal attention is, and whether different success metrics tell the same story.

📈 4.1 Distributions

chart
'/kaggle/working/figures/fig02_releases_and_reviews_by_year.png'
📌 Note
A ridgeline plot stacks one density curve per year. It shows the whole distribution instead of one average, so you can see that the shape barely changes: every year produces a big mass of games with almost no reviews and a long thin tail of hits.
chart
'/kaggle/working/figures/fig03_owners_price_reviews.png'

🏔️ 4.2 How concentrated is attention?

🧠 Why this step
If attention were spread evenly, "above average" would be a sensible definition of success. If a few games take almost everything, a top-percentile definition is needed. One SQL query measures the thresholds; a second builds the Lorenz curve (cumulative share of reviews against share of games), the standard picture of inequality.
chart
💡 Insight
the top 1% of games hold 80% of all reviews and the top 10% hold 98% (Gini 0.98, where 1.0 would mean one game has every review). Success on Steam is winner-takes-most, so "above average" is a misleading target and a top-percentile definition fits better.

🔗 4.3 Do the candidate success metrics agree?

🧠 Why this step
"Successful" can mean commercial reach (owners, Peak CCU), quality (positive reviews) or engagement (playtime). If all metrics moved together one definition would be enough. Spearman rank correlation is used because every metric is heavily skewed, and the hexbin plot shows the raw relationship between reach and quality for every game.
chart
💡 Insight
owners and reviews correlate at 0.66, but owners and positive ratio only at 0.00. Reach and quality are different dimensions of success, so the notebook defines both, plus a strict "hit" that needs both.
metric pos name release_year metric_value
0 owners_mid 1 Dota 2 2013 150000000
1 owners_mid 2 Counter-Strike 2 2012 150000000
2 owners_mid 3 Apex Legends™ 2020 150000000
3 owners_mid 4 PUBG: BATTLEGROUNDS 2017 150000000
4 owners_mid 5 Call of Duty® 2022 75000000
5 peak_ccu 1 Counter-Strike 2 2012 1013936
6 peak_ccu 2 Dota 2 2013 623941
7 peak_ccu 3 PUBG: BATTLEGROUNDS 2017 314682
8 peak_ccu 4 Bongo Cat 2025 164168
9 peak_ccu 5 ELDEN RING NIGHTREIGN 2025 163599
10 total_reviews 1 Counter-Strike 2 2012 8815087
11 total_reviews 2 PUBG: BATTLEGROUNDS 2017 2557944
12 total_reviews 3 Dota 2 2013 2498969
13 total_reviews 4 Grand Theft Auto V Legacy 2015 1990556
14 total_reviews 5 Terraria 2011 1409473

🏆 5. Defining success

Four definitions, each answering a different question:

Label Rule Question
commercial_success_year Top 10% by estimated owners within its release year Did it reach many players, compared with games of the same age?
quality_success At least 50 reviews and at least 80% positive Do players who bought it like it?
engagement_success Among games with 50+ reviews, top 10% by median playtime Do players keep playing?
hit Commercial and quality Both at once, the strictest definition

Why "within its release year": a game from 2012 has had a decade to gather owners and reviews, a game from 2023 has not. A single global cutoff would label old games as successes and new games as failures regardless of quality.

Note on ties: owners come in wide buckets, so many games sit exactly on a year's cutoff and all of them count. That is why commercial_success_year covers more than 10% of games.

🧠 Why this step
The whole analysis depends on this label, so it is defined several ways and checked for bias. A label that secretly measures "how old is the game" would make every later result about age, not success.
All games with date and owners: 125,854    Study set (on sale 1+ year): 120,629
Global owners cutoff 75,000    Median playtime cutoff (50+ reviews) 658 min
% of study games
commercial_success_year 22.860
quality_success 13.980
engagement_success 2.540
hit 5.930
chart
'/kaggle/working/figures/fig06_age_bias_of_success_label.png'
chart
💡 Insight
only 26% of commercial hits are also well liked (14% of all games are). Reaching many players and satisfying them are related but not the same thing, so hit is a much rarer label than either part.

🧪 6. Testing indicators

Each launch-time feature is compared with one target at a time. Change TARGET and rerun the section to compare definitions.

Three tools, each answering a different question: 1. AUC ranks numeric features by how well they separate successful from other games (0.5 = no signal). 2. Success rate with a 95% interval shows the shape of the relationship and how much to trust it. 3. Lift (rate of a group divided by the overall rate) compares genres and tags on one scale: 1.0 = average.

All of these show association, not cause. A high-quality game may add languages because it sells.

🧠 Why this step
A model can say *that* launch facts predict success, but not *which* facts or *how*. Testing features one at a time, with effect sizes and confidence intervals, gives findings a reader can check and an interviewer can question.
Target: hit    overall rate 5.9%    positives 7,150 of 120,629

🎯 6.1 Indicator scorecard

AUC per numeric launch-time feature for the chosen TARGET. Right of 0.5 = higher in successful games, left = lower. Appendix B compares all four labels.

chart
💡 Insight
for hit the strongest numeric indicators are Achievements (AUC 0.73), n_categories (AUC 0.72), n_languages (AUC 0.72). An AUC of 0.60 means a random successful game ranks above a random other game 60% of the time, so values near 0.5 are weak even when a test calls them "significant" on 100k+ games.

📉 6.2 Shape of the relationship

Success rate per bucket. The shaded ribbon is the 95% interval: where ribbons of two buckets overlap, the difference between them is not reliable.

chart
'/kaggle/working/figures/fig09_success_rate_by_bucket.png'

🔀 6.3 Yes / no features

A dumbbell chart: orange = games without the feature, blue = with it, the grey bar is the gap. Thin whiskers are 95% intervals.

chart
💡 Insight
the largest yes/no gap is Has DLC: 19.6% with it vs 3.6% without (+16.1 points). Groups under 300 games are left out of this comparison.

🏷️ 6.4 Genres and tags

Lift = success rate of games with the label / overall rate. Only labels with at least MIN_GROUP games are shown, because rates from small groups swing wildly. Tags are voted by players after release, so tags describe popular games as much as they predict them. Use them to understand, not to predict.

The query computes lift for every genre and tag under all four success labels in one pass: the four label columns are turned into rows (labels_long), so a single GROUP BY covers them all. Appendix B reuses the result.

Shown as a forest plot on a log scale, so 2x and 0.5x are the same distance from average.

chart
💡 Insight
among genres, RPG has the highest lift (1.5x, 9.1%) and Violent the lowest (0.1x, 0.5%). Genre labels overlap (a game has several), so lifts are not additive.

🤖 7. Modelling

Question: using only what is known at (or near) launch, how well can a model separate games that become successful from those that do not?

Rules that keep the result honest: - No outcome columns as features. Owners, Peak CCU, reviews, Positive/Negative, Recommendations, playtime, Metacritic and User score define or follow success. Including them would give a perfect score and teach nothing. - No n_tags. Tags are voted by players after release. - Time-based split. Train on games up to SPLIT_YEAR, test on later ones. A random split would let the model learn from the future. - Only games old enough for outcomes to settle (MIN_YEARS). - Discount is left out because it changes over time. DLC count, Achievements and trading cards can grow after launch, so treat their importance with some caution.

🧠 Why this step
Single-feature tests ignore that features overlap (games with many languages also tend to have achievements). A model combines them and answers the practical question: given only what a studio knows at launch, how well can success be ranked? Gradient boosting is used because it handles skewed features and interactions without manual transformations.
Features: 29  (15 numeric + 14 genre flags)
Train: 50,646 games (released up to 2021)    Test: 69,983 games (released later)
train rate % test rate % test / train
commercial_success_year 13.190 29.850 2.260
quality_success 20.600 9.190 0.450
engagement_success 3.450 1.890 0.550
hit 6.920 5.210 0.750
📌 Note
A lower test rate does not break the model, but it changes how to read precision. AUC compares rankings and is largely unaffected by the base rate; average precision is not, so compare it to the test base rate, not to the training one.
auc avg_precision test_base_rate precision_lift
commercial_success_year 0.633 0.428 0.299 1.435
quality_success 0.782 0.299 0.092 3.251
engagement_success 0.907 0.244 0.019 12.949
hit 0.805 0.235 0.052 4.523
chart
💡 Insight
launch-time features predict engagement_success best (AUC 0.91) and commercial_success_year worst (AUC 0.63). An AUC well above 0.5 means launch-time facts carry real signal, but the gap to 1.0 is what marketing, quality and luck explain.

🔬 7.1 Focus model: TARGET

🧠 Why this step
AUC is abstract. Two pictures make it concrete: the cumulative gains curve answers "if a publisher only looks at the top X% of the model's picks, what share of the real successes do they catch?", and the calibration curve checks whether a predicted 20% chance really means 20%. Then ablation, permutation importance and SHAP show what the model relies on.
chart
💡 Insight
ranking newer games by the model, the top 10% contain 43% of all hit games and the top 30% contain 72% (test AUC 0.80). On average the model predicts 6.1% while 5.2% of test games succeed: the base rate drifted between the training and test years, so the ranking is useful but the raw probabilities need recalibrating before being read as chances.
chart
'/kaggle/working/figures/fig14_feature_group_ablation.png'
chart
'/kaggle/working/figures/fig15_permutation_importance.png'
📌 Note
SHAP goes one step further than importance: every dot is one game, its position shows how much a feature pushed that game's prediction up or down, and its colour shows whether the feature value was high (red) or low (blue). It reveals direction and spread, not just size. The shap package is preinstalled on Kaggle; elsewhere pip install shap.
chart

📌 8. Findings and limits

🧠 Why this step
A reader who skips everything else should still leave with the main numbers and the caveats that come with them.
chart
'/kaggle/working/figures/fig17_key_numbers.png'

📊 What the data says (for hit, 120,629 games)

  1. 🏔️ Success is rare and concentrated. The label covers 5.9% of games; the top 1% of games hold 80% of all reviews.
  2. ⚖️ Reach and quality are different things. Only 26% of commercial hits are also well liked, which is why hit is much rarer than either part.
  3. 🎯 Strongest single indicators: Achievements (AUC 0.73), n_categories (AUC 0.72), n_languages (AUC 0.72).
  4. 🏷️ Genre matters: RPG has the highest lift (1.5x), Violent the lowest (0.1x).
  5. 🤖 Launch-time features give real but limited predictive power: AUC 0.80 on newer games; the top 10% of predictions capture 43% of successes. Most important features: n_languages, n_categories, DLC count.
  6. 🧩 Biggest feature group: Store page and extras (removing it costs 0.026 AUC).

Results from the appendices (stability, richer features, owner buckets) are summarised at the end of the notebook.

⚠️ Limits to keep in mind

⚠️ Read with care
Association, not cause. Games that sell well can afford more languages, DLC and achievements, so those may follow success rather than cause it.

Owners are bucketed and estimated by the dataset author from Steam data, so commercial success is approximate and ties are common. The "top 10%" commercial label covers more than 10% of games for that reason.

The test years have a different base rate than the training years; compare AUC, and read precision and predicted chances against the test base rate.

Missing the biggest drivers. Marketing spend, wishlists, studio reputation beyond past releases, quality of the game itself and luck are not in this data.

Thresholds are choices. 90th percentile, 50 reviews and 80% positive are defaults; rerun with other values (see the Parameters cells) to check the conclusions hold.

📎 Appendices

Supporting checks and extensions. They reuse variables from the main sections, so run the notebook from the top.

🗂️ A. Data overview in detail

row 0 row 1 row 2
AppID 2539430 496350 1034400
Name Black Dragon Mage Playtest Supipara - Chapter 1 Spring Has Come! Mystery Solitaire The Black Raven
Release date Aug 1, 2023 Jul 29, 2016 May 6, 2019
Estimated owners 0 - 0 0 - 20000 0 - 20000
Peak CCU 0 0 0
... ... ... ...
Categories NaN Single-player,Steam Trading Cards,Steam Cloud,... Single-player,Family Sharing
Genres NaN Adventure Casual
Tags NaN Adventure,Visual Novel,Anime,Cute Casual,Card Game,Solitaire,Puzzle,Hidden Objec...
Screenshots https://shared.akamai.steamstatic.com/store_it... https://shared.akamai.steamstatic.com/store_it... https://shared.akamai.steamstatic.com/store_it...
Movies NaN NaN NaN

40 rows × 3 columns

count mean 50% 75% max
AppID 125,855.000 2,032,626.605 1,949,010.000 2,938,410.000 4,437,540.000
Peak CCU 125,855.000 53.187 0.000 0.000 1,013,936.000
Required age 125,855.000 0.165 0.000 0.000 21.000
Price 125,855.000 4.811 2.390 5.545 999.980
Discount 125,855.000 17.897 0.000 37.000 100.000
DLC count 125,855.000 0.535 0.000 0.000 3,703.000
Metacritic score 125,855.000 2.500 0.000 0.000 97.000
User score 125,855.000 0.024 0.000 0.000 100.000
Positive 125,855.000 1,018.182 4.000 35.000 7,642,084.000
Negative 125,855.000 164.879 1.000 10.000 1,173,003.000
Score rank 40.000 99.175 99.000 100.000 100.000
Achievements 125,855.000 17.922 2.000 19.000 9,821.000
Recommendations 125,855.000 938.040 0.000 0.000 4,830,455.000
Average playtime forever 125,855.000 202.757 0.000 0.000 3,429,544.000
Average playtime two weeks 125,855.000 13.508 0.000 0.000 20,088.000
Median playtime forever 125,855.000 169.180 0.000 0.000 3,429,544.000
Median playtime two weeks 125,855.000 14.419 0.000 0.000 20,088.000
Movies 0.000 NaN NaN NaN NaN
dtype unique unique_% missing_% example
Movies float64 0 0.000 100.000
Mac bool 2 0.000 0.000 False
Windows bool 2 0.000 0.000 True
Linux bool 2 0.000 0.000 False
Score rank float64 3 0.000 99.970 99.0
Estimated owners object 14 0.010 0.000 0 - 0
Required age int64 15 0.010 0.000 0
User score int64 31 0.020 0.000 0
Metacritic score int64 73 0.060 0.000 0
Discount int64 88 0.070 0.000 0
DLC count int64 117 0.090 0.000 0
Achievements int64 449 0.360 0.000 0
Price float64 953 0.760 0.000 0.0
Average playtime two weeks int64 995 0.790 0.000 0
Median playtime two weeks int64 999 0.790 0.000 0
Peak CCU int64 1110 0.880 0.000 0
Median playtime forever int64 2512 2.000 0.000 0
Negative int64 2726 2.170 0.000 0
Genres object 2922 2.320 6.690 Adventure
Average playtime forever int64 3037 2.410 0.000 0
Full audio languages object 3785 3.010 0.000 []
Metacritic url object 4162 3.310 96.620 https://www.metacritic.com/game/pc/fanta
Release date object 5141 4.080 0.000 Aug 1, 2023
Recommendations int64 5340 4.240 0.000 0
Positive int64 5541 4.400 0.000 0
Reviews object 11987 9.520 90.320 “And this is the very reason why I belie
Categories object 14058 11.170 7.120 Single-player,Steam Trading Cards,Steam
Notes object 19332 15.360 81.480 The game includes the following elements
Supported languages object 19614 15.580 0.000 []
Support url object 36161 28.730 56.060 http://mangagamer.com
Website object 40353 32.060 59.860 http://mangagamer.org/supipara
Support email object 62321 49.520 17.980 support@mangagamer.com
Publishers object 64607 51.330 7.090 MangaGamer
Developers object 72859 57.890 6.710 minori
Tags object 77184 61.330 33.770 Adventure,Visual Novel,Anime,Cute
About the game object 116776 92.790 6.720 Springtime, April: when the cherry trees
Screenshots object 119718 95.120 4.790 https://shared.akamai.steamstatic.com/st
Name object 124644 99.040 0.000 Black Dragon Mage Playtest
Header image object 125664 99.850 0.060 https://shared.akamai.steamstatic.com/st
AppID int64 125855 100.000 0.000 2539430

🧷 B. Stability across success definitions

Sections 6 and 7 used one TARGET. A finding is only worth trusting if it survives a change of definition, so this section reruns the indicator tests and the model for all four labels and compares them.

Check Stable means
Numeric indicators AUC on the same side of 0.5 for every label, and at least 0.03 away from 0.5 for the weakest label
Genres Lift above 1.15 for every label (or below 0.85 for every label)
Model features Permutation-importance ranking is similar between labels
🧠 Why this step
The success thresholds were choices. A finding that only holds for one definition is a property of the definition, not of games, so every key result is rerun under all four labels.
mean_auc weakest_signal same_direction verdict commercial_success_year quality_success engagement_success hit
n_categories 0.691 0.158 True stable 0.658 0.678 0.708 0.718
n_languages 0.678 0.146 True stable 0.646 0.655 0.694 0.718
about_length 0.660 0.126 True stable 0.632 0.626 0.720 0.661
Achievements 0.693 0.122 True stable 0.622 0.690 0.728 0.733
Price 0.666 0.117 True stable 0.637 0.617 0.759 0.650
DLC count 0.642 0.069 True stable 0.569 0.631 0.681 0.685
release_year 0.466 0.058 False flips sign 0.703 0.337 0.385 0.442
n_audio_languages 0.574 0.053 True stable 0.565 0.553 0.589 0.591
n_platforms 0.551 0.015 True weak 0.515 0.584 0.533 0.570
n_genres 0.531 0.014 True weak 0.555 0.514 0.516 0.538
Required age 0.517 0.008 True weak 0.508 0.511 0.527 0.522
chart
💡 Insight
stable under all four definitions: n_categories, n_languages, about_length, Achievements, Price, DLC count, n_audio_languages. Direction flips between definitions for: release_year. A feature that flips (for example, helps reach but hurts quality) is a trade-off, not a recipe.
chart
💡 Insight
genres that beat the average under every definition: RPG. Genres below average under every definition: Violent. Genres whose colour changes across a row only help one kind of success.
chart
💡 Insight
feature-importance rankings agree between definitions with an average Spearman correlation of 0.66. The feature that matters most on average is n_languages.

🧬 C. Richer features: store-page text and developer history

Two ideas for signal beyond the basic launch facts:

  1. Text of About the game. Words are turned into TF-IDF weights, a linear model learns which words go with success, and its output becomes one numeric feature (text_score) for the main model.
  2. Developer and publisher track record. For each game: how many games the developer released before, and how those earlier games did. This is computed in SQL with window functions over each studio's release history.

Leakage controls (the part that is easy to get wrong): - The TF-IDF vocabulary and the text model are fitted on training years only. For training games the score is out-of-fold (each game is scored by a model that never saw it), so the main model does not see an over-optimistic feature. - Track-record features only count earlier games that were released at least a year before the current game, so their outcomes had time to settle. Nothing from the current game or the future is used. - Developer and publisher are matched on the exact text of the field (a studio spelled two ways counts as two).

🧠 Why this step
The basic features describe the game. Two things a publisher also knows before launch are how the store page is written and how the studio's earlier games did. If they add signal, the model was missing real information; if not, the basic features already capture it.
65% of games are a developer's first release in the data
chart
'/kaggle/working/figures/fig21_developer_track_record.png'
TF-IDF matrix: 50,646 training games x 20,000 words and word pairs
chart
'/kaggle/working/figures/fig22_store_text_words.png'
⚠️ Read with care
Read the word chart with care. Words that name a genre or an era ("pixel", "roguelike", "multiplayer") describe the kind of game, not good writing. Words like "award", "winner" or "bestselling" would be a red flag: they describe success that already happened (the page may have been edited after launch).
commercial_success_year quality_success engagement_success hit
Baseline 0.633 0.782 0.907 0.805
+ developer history 0.633 0.819 0.920 0.835
+ store text 0.655 0.791 0.918 0.818
+ both 0.651 0.819 0.925 0.836
commercial_success_year quality_success engagement_success hit
AUC change vs baseline
Baseline 0.000 0.000 0.000 0.000
+ developer history -0.000 0.037 0.013 0.030
+ store text 0.022 0.010 0.011 0.013
+ both 0.018 0.037 0.018 0.031
chart
💡 Insight
for hit, developer history adds +0.030 AUC and store text adds +0.013; both together add +0.031. The largest gain over all labels is +0.037 (quality_success). Track record is a legitimate pre-launch signal (a studio's past results are public), and it says something the game's own attributes cannot.

🪜 D. Ordinal model of owner buckets

So far owners were reduced to "top 10% or not". Steam publishes owners as ordered buckets (0-20k, 20k-50k, 50k-100k, ...). An ordinal model uses all of that information: for every bucket boundary it predicts the chance that a game ends above it (a cumulative-threshold model, Frank and Hall 2001). Adding the probabilities gives an expected bucket for each game.

Why try it: a regression on the owner midpoint treats the bucket edges as evenly spaced measurements, while the ordinal model only assumes the order. Whether that helps is an empirical question, so both are compared below.

Neighbouring rare buckets are merged so every class has enough training games. Newer games have had less time to gather owners, so the within-year rank correlation (do games rank correctly among games released in the same year?) is the fairest single metric.

🧠 Why this step
"Hit or not" throws away most of the owners information. Steam's buckets are ordered, so a model that predicts *how big* a game gets is a more demanding and more useful test.
train % test %
0-0 3.300 26.700
0-20k 61.800 62.100
20k-50k 14.500 5.800
50k-100k 7.200 2.400
100k-200k 4.900 1.400
200k-500k 4.300 1.000
500k-1M 1.800 0.300
1M-2M 1.200 0.200
2M-200M 1.000 0.100
Ordinal model Midpoint regression
Spearman (all test games) 0.310 0.523
Spearman within release year 0.333 0.546
value
Mean error in classes (ordinal model) 0.479
Mean error in classes (always predict the typical class) 0.491
Quadratic weighted kappa (1 = perfect, 0 = chance) 0.456
Share of games predicted in the exact class 0.678
Share within one class 0.906
chart
💡 Insight
within the same release year the ordinal model ranks games with Spearman 0.33, versus 0.55 for a regression on the log owner midpoint, so the midpoint regression ranks games better here. The easiest boundary to predict is > 2M (AUC 0.97) and the hardest is > 20k (AUC 0.73). The exact class is right for 68% of games and within one class for 91%. The diagonal in the last chart shows whether errors are near misses (good) or far misses (bad).

🧾 Appendix findings

🧷 Which findings survive every definition of success (Appendix B)

  • Stable numeric indicators: n_categories, n_languages, about_length, Achievements, Price, DLC count, n_audio_languages. Direction flips for: release_year.
  • Genres above average under all four definitions: RPG; below average under all four: Violent.
  • Model features agree: importance rankings between definitions correlate at 0.66 on average.

🧬 What the extra features add (Appendix C)

  • Developer track record: +0.030 AUC for hit. Store text: +0.013. Both: +0.031. Best feature set for this target: + both.

🪜 Predicting how big a game gets (Appendix D)

  • Owner buckets: within release years the ordinal model reaches Spearman 0.33 and the midpoint regression 0.55. The ordinal model puts 68% of games in the exact class and 91% within one class.

⚠️ Extra limits for these analyses

  • Developer history uses exact name matches and the labels of earlier games are measured at the end of the data, not at the time of the later launch. Only games a year or more older are used to limit this, but it is not a perfect point-in-time record.
  • Store text may have been edited after launch, and text weights partly describe genre and era rather than quality.

🚀 Next steps

✅ Result
Saved 26 charts as PNG in /kaggle/working/figures and bundled them in /kaggle/working/figures.zip.