🎯 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)
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
⚖️ 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
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
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
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).
'/kaggle/working/figures/fig01_missing_values.png'
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)
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
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 |
'/kaggle/working/figures/fig_storage_memory.png'
📦 3.2 Which file format?
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 |
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 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.
"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
'/kaggle/working/figures/fig02_releases_and_reviews_by_year.png'
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.
'/kaggle/working/figures/fig03_owners_price_reviews.png'
🏔️ 4.2 How concentrated is attention?
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.
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?
"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.
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.
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 |
'/kaggle/working/figures/fig06_age_bias_of_success_label.png'
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.
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.
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.
'/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.
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.
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.
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 |
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 |
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
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.
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.'/kaggle/working/figures/fig14_feature_group_ablation.png'
'/kaggle/working/figures/fig15_permutation_importance.png'
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.📌 8. Findings and limits
A reader who skips everything else should still leave with the main numbers and the caveats that come with them.
'/kaggle/working/figures/fig17_key_numbers.png'
📊 What the data says (for hit, 120,629 games)
- 🏔️ Success is rare and concentrated. The label covers 5.9% of games; the top 1% of games hold 80% of all reviews.
- ⚖️ Reach and quality are different things. Only 26% of commercial hits are also well liked, which is why
hitis much rarer than either part. - 🎯 Strongest single indicators: Achievements (AUC 0.73), n_categories (AUC 0.72), n_languages (AUC 0.72).
- 🏷️ Genre matters: RPG has the highest lift (1.5x), Violent the lowest (0.1x).
- 🤖 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.
- 🧩 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
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 |
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 |
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.
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.
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:
- 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. - 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).
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
'/kaggle/working/figures/fig21_developer_track_record.png'
TF-IDF matrix: 50,646 training games x 20,000 words and word pairs
'/kaggle/working/figures/fig22_store_text_words.png'
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 |
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.
"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 |
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
- Match developers more loosely (lowercase, strip "Inc.", "Ltd.") so a studio spelled two ways counts once.
- Add a point-in-time version of history features using review counts at the earlier game's own launch, if a dated review snapshot becomes available.
- Try sentence embeddings of
About the gameinstead of TF-IDF, and compare with the text gain measured here.
Saved 26 charts as PNG in
/kaggle/working/figures and bundled them in /kaggle/working/figures.zip.