Build a Steam Popularity Classifier
Train a Python model to predict Steam game popularity from pre-release metadata.
Introduction
30 Second Summary
A game idea can sound promising long before anyone knows whether players will notice it. Price, platform support, and genre provide clues that are difficult to weigh by instinct alone.
In this project, you will build a Python machine learning notebook in Kaggle Notebooks that predicts whether a Steam game belongs to a low, medium, or high popularity tier. The finished workflow tests a balanced Random Forest on held-out Steam games.
What You'll Build
Run the finished notebook to turn a new indie game concept into a low, medium, or high prediction backed by visible evaluation charts.
By the end of this project, you'll have:
- Load the March 2025 Steam catalog using only the columns your model needs. See ownership ranges become three popularity tiers in a bar chart.
- Compare a majority-class baseline with a balanced Random Forest on the same held-out games. See the classification report expose the popularity tiers that the baseline misses.
- Enter a custom game concept to predict its popularity tier. Reload the saved steam_popularity_model.joblib bundle to reproduce the prediction.
- Secret Mission: add post-release review counts to expose how target leakage can inflate a model's score.
Are there any prerequisites?
You need a Kaggle account plus a browser. Basic Python familiarity with variables, functions, lists, and dictionaries is enough.
Before We Start
Before any hands-on work, commit to an honest pre-release classifier that predicts low, medium, or high Steam-game popularity from metadata available before launch. Excluding post-release reviews prevents target leakage because a new unreleased game has no reviews.
Set Up the Kaggle Notebook
Large datasets can stall a machine learning project before modeling starts. Local Windows package versions can also drift from the code you are trying to reproduce.
Kaggle Notebooks puts hosted compute beside attached datasets. This removes local installation work from the project.
In this step, you will create a Python notebook with the Steam Games Dataset 2025 attached. You will pin the required libraries before locating the cleaned CSV file.
In this step, get ready to:
- Create a Kaggle notebook with the Steam Games Dataset 2025 attached.
- Install the pinned libraries before importing the project APIs.
- Confirm that the notebook can locate the cleaned March 2025 dataset.
Create and attach the notebook
A notebook created from the dataset page receives that dataset as an attached input. This keeps the large cleaned file beside your code.
- Open the Steam Games Dataset 2025 page in your browser.
- Sign in to your Kaggle account if Kaggle asks you to authenticate.
- Click Code on the dataset page.
- Click Create a notebook to open a Python notebook with the dataset attached.
- Confirm that Steam Games Dataset 2025 appears in the Input pane.
That is your hosted workspace ready. The dataset is now mounted beside your notebook cells.
Pin and verify the libraries
Machine learning libraries can behave differently when their versions change. This project pins pandas 3.0.6, scikit-learn 1.9.1, Matplotlib 3.11.2, plus joblib 1.6.0.
- Select the first empty code cell in your notebook.
- Install the pinned dependencies by entering this command in the selected cell:
!pip install -q pandas==3.0.6 scikit-learn==1.9.1 matplotlib==3.11.2 joblib==1.6.0
Why pin these versions?
- The pip install command installs the four libraries into the current notebook session.
- Each == pin requests one exact package version.
- The -q flag reduces installation output so the notebook stays readable.
- Run the installation cell using the play control beside it.
- Wait for the cell to finish before selecting another cell.
The cell completes without an installation error. That setup hurdle is now cleared.
Did the installation stop?
- Compare every package name with the installation cell above.
- Check that every version uses two equals signs.
- Run the installation cell again if the connection was interrupted.
Ask for help with the exact output you received: Help me troubleshoot my Kaggle package installation.
The next cell imports every API used by the finished notebook. Its printed version map proves which library versions Python loaded.
- Select the next empty code cell beneath the installation cell.
- Import the project APIs by entering this code in the selected cell:
# Cell 1: imports and environment verification
from pathlib import Path
import joblib
import matplotlib
import pandas as pd
import sklearn
from joblib import dump, load
from sklearn.dummy import DummyClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
ConfusionMatrixDisplay,
balanced_accuracy_score,
classification_report,
)
from sklearn.model_selection import train_test_split
print(
{
"pandas": pd.__version__,
"scikit-learn": sklearn.__version__,
"matplotlib": matplotlib.__version__,
"joblib": joblib.__version__,
}
)
What does this code do?
- Path provides the filesystem tools used to search attached data.
- The scikit-learn imports prepare the later splitting, baseline, modeling, plus evaluation cells.
- dump plus load prepare the notebook to save the finished model bundle.
- The final print call exposes the four versions loaded by Python.
Before you run this cell, consider whether all four printed versions will match the pins.
- Run the import cell using the play control beside it.
✔️ I see all four pinned versions
Pandas prints 3.0.6. Scikit-learn prints 1.9.1.
Matplotlib prints 3.11.2. Joblib prints 1.6.0.
Your runtime now matches the project environment. Every later cell can use the verified APIs.
ⓧ I see different versions
- Return to the installation cell above.
- Run the installation cell again.
- Restart the Python process using the notebook session controls.
- Run the import cell again.
The version map should now show the four pinned versions.
ⓧ An import fails
- Return to the installation cell above.
- Compare its package names with the provided command.
- Run the corrected installation cell.
- Restart the Python process using the notebook session controls.
- Run the import cell again.
The import cell should complete before it prints the version map.
Locate the cleaned dataset
Kaggle mounts attached datasets beneath /kaggle/input. A recursive search finds games_march2025_cleaned.csv without depending on the mounted dataset folder name.
- Select the next empty code cell beneath the import cell.
- Locate the cleaned dataset by entering this code in the selected cell:
# Cell 2: locate the attached dataset
DATA_FILES = sorted(
Path("/kaggle/input").rglob("games_march2025_cleaned.csv")
)
if not DATA_FILES:
raise FileNotFoundError(
"Attach Steam Games Dataset 2025 before running this notebook."
)
DATA_PATH = DATA_FILES[0]
print(DATA_PATH)
What does this code do?
- DATA_FILES stores the sorted list of matching files found beneath the attached-data directory.
- if not DATA_FILES: stops the notebook with a clear message when the dataset is missing.
- DATA_PATH stores the first matching file for the loading step that follows.
- The final print call makes the selected path visible.
Before you run this cell, predict whether the recursive search will find the attached cleaned file.
- Run the dataset-location cell using the play control beside it.
You will see a path beneath /kaggle/input that ends in games_march2025_cleaned.csv. Your notebook can now reach the exact dataset required by the classifier.
Dataset path missing?
- Return to the Input pane.
- Confirm that Steam Games Dataset 2025 is listed there.
- Use Add Input to attach Steam Games Dataset 2025 if it is absent.
- Compare the searched filename with games_march2025_cleaned.csv.
Ask for help with the notebook state you can see: Help me find the attached Steam dataset in Kaggle.
✔️ Awesome, I've got everything!
Your two setup cells now match the project checkpoint.
ⓧ I'd like to double check the full code
# Cell 1: imports and environment verification
from pathlib import Path
import joblib
import matplotlib
import pandas as pd
import sklearn
from joblib import dump, load
from sklearn.dummy import DummyClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
ConfusionMatrixDisplay,
balanced_accuracy_score,
classification_report,
)
from sklearn.model_selection import train_test_split
print(
{
"pandas": pd.__version__,
"scikit-learn": sklearn.__version__,
"matplotlib": matplotlib.__version__,
"joblib": joblib.__version__,
}
)
# Cell 2: locate the attached dataset
DATA_FILES = sorted(
Path("/kaggle/input").rglob("games_march2025_cleaned.csv")
)
if not DATA_FILES:
raise FileNotFoundError(
"Attach Steam Games Dataset 2025 before running this notebook."
)
DATA_PATH = DATA_FILES[0]
print(DATA_PATH)
Your hosted environment is reproducible. Next, you will load the required columns before turning ownership ranges into the classifier's target.
Create Popularity Tiers
Your Kaggle Notebooks environment can now reach the cleaned Steam file. A multiclass classification workflow still needs one target label for every usable game.
Steam records ownership as ranges such as 0 - 20000. You will convert each range into a midpoint before grouping the games into low, medium, or high popularity tiers.
In this step, get ready to:
- Load only the eleven columns required by the classifier.
- Convert ownership ranges into project-defined popularity tiers.
- Display the class distribution in a bar chart.
Inspect the source data
The source file contains more information than this model needs. pandas can limit the load to the named columns through usecols.
- Add a new code cell directly below the cell that prints DATA_PATH.
- Load the required columns by pasting this code into the new cell:
RAW_COLUMNS = ["Name", "Estimated owners", "Required age", "Price", "Supported languages", "Windows", "Mac", "Linux", "Achievements", "Categories", "Genres"]
df = pd.read_csv(DATA_PATH, usecols=RAW_COLUMNS)
print("Shape:", df.shape)
print(df.dtypes)
print(df.isna().sum().sort_values(ascending=False))
df.head()
What does this code do?
- RAW_COLUMNS defines the eleven source fields used for the target or later model features.
- usecols=RAW_COLUMNS prevents unnecessary columns from being loaded into memory.
- df.dtypes shows how pandas interpreted each column.
- df.isna() reveals where values are missing.
- df.head() displays the first five rows for a quick structure check.
- Run the new code cell.
- Inspect the printed shape for a row count plus eleven columns.
- Inspect the data types for all eleven named columns.
- Inspect the missing-value counts for each column.
- Inspect the preview for the first five Steam games.
Good progress. Your notebook now holds only the columns needed for this workflow.
Having trouble loading the columns?
Confirm that DATA_PATH still points to games_march2025_cleaned.csv. Check every value in RAW_COLUMNS against the code block because the source column names must match exactly.
Help me diagnose the dataset-loading problem.
Turn ranges into tier labels
The Estimated owners column contains an interval for each game. Its midpoint gives the notebook one numeric ownership estimate to classify.
The tier thresholds are fixed teaching choices for this project. Consistent boundaries make future model comparisons repeatable.
- Add another code cell below the dataset preview cell.
- Create the midpoint labels plus the tier chart by pasting this code into the new cell:
def ownership_midpoint(value):
if pd.isna(value):
return None
bounds = str(value).replace(",", "").split(" - ")
if len(bounds) != 2:
return None
return (float(bounds[0]) + float(bounds[1])) / 2
df["owner_midpoint"] = df["Estimated owners"].map(ownership_midpoint)
df["popularity_tier"] = pd.cut(df["owner_midpoint"], bins=[-float("inf"), 50_000, 1_000_000, float("inf")], labels=["low", "medium", "high"], right=False)
model_df = df.dropna(subset=["popularity_tier"]).copy()
TIER_ORDER = ["low", "medium", "high"]
tier_ax = model_df["popularity_tier"].value_counts().reindex(TIER_ORDER).plot(kind="bar", title="Steam game popularity tiers")
tier_ax
How do the tiers work?
- ownership_midpoint() removes commas before splitting each ownership range into two bounds.
- pd.cut() assigns ownership below 50_000 to the low tier.
- pd.cut() assigns ownership from 50_000 to below 1_000_000 to the medium tier.
- pd.cut() assigns ownership of at least 1_000_000 to the high tier.
- right=False makes each lower boundary inclusive.
- model_df retains only rows with a usable target.
- TIER_ORDER keeps the chart ordered from low through high.
Run the tier check
The chart gives you the first visible view of class imbalance. Different bar heights show how ownership examples are distributed across the three labels.
Before you run the cell, which popularity tier do you expect to have the tallest bar?
- Run the target-creation cell.
- Check the horizontal axis for low, medium, and high.
- Check the chart title for Steam game popularity tiers.
You will see a bar chart containing all three popularity classes. The bar heights reveal the class distribution that the models must handle.
That is the target ready. Every retained game now has a low, medium, or high label for training.
Missing the popularity chart?
Confirm that the cell ends with tier_ax. Kaggle displays that chart object as the cell output.
If the cell stops earlier, compare the spacing in split(" - ") with the code block. Also confirm that Estimated owners appears in RAW_COLUMNS.
Help me fix the popularity-tier chart.
✔️ Awesome, I've got everything!
Your dataset load plus target cells are complete. Keep the chart visible as evidence that all three labels exist.
ⓧ I'd like to double check the full code
- Compare your notebook cells with this cumulative Python reference:
# Cell 1: imports and environment verification
from pathlib import Path
import joblib
import matplotlib
import pandas as pd
import sklearn
from joblib import dump, load
from sklearn.dummy import DummyClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
ConfusionMatrixDisplay,
balanced_accuracy_score,
classification_report,
)
from sklearn.model_selection import train_test_split
print(
{
"pandas": pd.__version__,
"scikit-learn": sklearn.__version__,
"matplotlib": matplotlib.__version__,
"joblib": joblib.__version__,
}
)
# Cell 2: locate the attached dataset
DATA_FILES = sorted(
Path("/kaggle/input").rglob("games_march2025_cleaned.csv")
)
if not DATA_FILES:
raise FileNotFoundError(
"Attach Steam Games Dataset 2025 before running this notebook."
)
DATA_PATH = DATA_FILES[0]
print(DATA_PATH)
RAW_COLUMNS = ["Name", "Estimated owners", "Required age", "Price", "Supported languages", "Windows", "Mac", "Linux", "Achievements", "Categories", "Genres"]
df = pd.read_csv(DATA_PATH, usecols=RAW_COLUMNS)
print("Shape:", df.shape)
print(df.dtypes)
print(df.isna().sum().sort_values(ascending=False))
df.head()
def ownership_midpoint(value):
if pd.isna(value):
return None
bounds = str(value).replace(",", "").split(" - ")
if len(bounds) != 2:
return None
return (float(bounds[0]) + float(bounds[1])) / 2
df["owner_midpoint"] = df["Estimated owners"].map(ownership_midpoint)
df["popularity_tier"] = pd.cut(df["owner_midpoint"], bins=[-float("inf"), 50_000, 1_000_000, float("inf")], labels=["low", "medium", "high"], right=False)
model_df = df.dropna(subset=["popularity_tier"]).copy()
TIER_ORDER = ["low", "medium", "high"]
tier_ax = model_df["popularity_tier"].value_counts().reindex(TIER_ORDER).plot(kind="bar", title="Steam game popularity tiers")
tier_ax
How to use this reference
The first two sections preserve your environment setup. The final two sections load the selected data before creating the three-class target.
Your popularity target is ready for modeling. Next, you will turn the remaining catalog metadata into numeric features before testing a deliberately weak baseline.
Engineer Features and Test the Baseline
Your notebook now turns Steam ownership ranges into three popularity tiers. The next question is whether catalog metadata can help predict those tiers.
Feature engineering converts raw catalog fields into consistent numeric inputs. A majority-class baseline then gives you a simple reference point for measuring class imbalance.
In this step, get ready to:
- Clean the numeric fields plus the platform flags.
- Create language counts plus selected genre and category indicators.
- Measure a majority-class baseline on a stratified test set.
Clean numeric and platform values
Raw catalog fields arrive in several formats. Consistent numeric values give every game the same feature schema.
- Add a new code cell below the popularity-tier chart cell.
- Define the platform normalizer by pasting this code:
# Cell 5: engineer numeric pre-release features
def to_binary(series):
normalized = series.astype(str).str.strip().str.lower()
return (
normalized
.map({"true": 1, "false": 0, "1": 1, "0": 0})
.fillna(0)
.astype(int)
)
What does this helper do?
- The normalized series uses lowercase text so equivalent platform values follow one format.
- The mapping converts true values to 1. It converts false values to 0.
- Missing or unfamiliar values become 0 so every platform column contains integers.
- Run the cell.
The cell finishes without an error. The to_binary helper is now available to normalize all three platform columns.
Does the helper cell fail?
Check the indentation inside to_binary. Each chained method must stay inside the return parentheses.
Help me fix the to_binary helper in my Steam popularity notebook.
- Add another code cell below the platform helper.
- Define the language counter by pasting this code:
def count_languages(value):
if pd.isna(value) or not str(value).strip():
return 0
return len(
[item for item in str(value).split(",") if item.strip()]
)
How does the language counter work?
- The missing-value check returns 0 when no supported languages are listed.
- The split separates comma-delimited language entries.
- The final length becomes one numeric feature for each game.
- Run the cell.
The cell finishes without an error. The count_languages helper can now turn each supported-language list into a count.
Does the language helper fail?
Check that pd still refers to the imported pandas library. Confirm that the list comprehension stays inside len().
Help me debug the count_languages helper in my notebook.
The helpers establish the conversion rules. The next cell applies those rules to model_df.
- Add a new code cell below the language helper.
- Clean the numeric values plus the platform fields by pasting this code:
for column in ["Price", "Required age", "Achievements"]:
model_df[column] = pd.to_numeric(
model_df[column], errors="coerce"
).fillna(0)
for column in ["Windows", "Mac", "Linux"]:
model_df[column] = to_binary(model_df[column])
model_df["language_count"] = model_df["Supported languages"].map(
count_languages
)
What does this cleaning cell do?
- Pandas converts price, age rating, and achievement counts into numeric values.
- Values that cannot be converted become missing values. The final fill replaces them with 0.
- The platform loop reuses to_binary for Windows, Mac, and Linux.
- The language mapping stores each supported-language count in language_count.
- Run the cleaning cell.
The cell finishes without an error. Your numeric, platform, and language features now use consistent values.
Does numeric cleaning fail?
Run both helper cells before this cell. A missing helper means the notebook cells ran out of order.
Help me debug the numeric and platform feature-cleaning cell.
Create honest catalog indicators
Genre and category columns hold several labels inside one text value. Selected binary indicators keep the model inputs understandable when you later build a custom game concept.
- Add a new code cell below the numeric cleaning cell.
- Define the selected genre and category indicators by pasting this code:
genre_text = model_df["Genres"].fillna("").astype(str)
category_text = model_df["Categories"].fillna("").astype(str)
GENRE_FEATURES = {
"genre_action": "Action",
"genre_adventure": "Adventure",
"genre_casual": "Casual",
"genre_indie": "Indie",
"genre_rpg": "RPG",
"genre_strategy": "Strategy",
}
CATEGORY_FEATURES = {
"category_single_player": "Single-player",
"category_multiplayer": "Multi-player",
"category_coop": "Co-op",
"category_controller": "Full controller support",
}
Why use selected indicators?
The dictionaries pair each model feature with the text token that activates it. This keeps the feature list narrow enough to inspect during evaluation.
Post-release reviews, ratings, playtime, recommendations, and concurrent-player counts stay outside FEATURE_COLUMNS. The finished model remains an honest pre-release predictor.
- Run the indicator-definition cell.
The cell finishes without an error. The genre and category dictionaries now define the indicators your model can use.
Does the indicator cell fail?
Check every comma inside GENRE_FEATURES and CATEGORY_FEATURES. Confirm that each dictionary ends with a closing brace.
Help me debug the genre and category feature dictionaries.
Each dictionary now describes what to search for. The next cell performs those searches and assembles the model inputs.
- Add a new code cell below the indicator definitions.
- Build the indicator columns plus the model matrices by pasting this code:
for feature, token in GENRE_FEATURES.items():
model_df[feature] = genre_text.str.contains(
token, case=False, regex=False, na=False
).astype(int)
for feature, token in CATEGORY_FEATURES.items():
model_df[feature] = category_text.str.contains(
token, case=False, regex=False, na=False
).astype(int)
FEATURE_COLUMNS = [
"Price",
"Required age",
"Achievements",
"Windows",
"Mac",
"Linux",
"language_count",
*GENRE_FEATURES.keys(),
*CATEGORY_FEATURES.keys(),
]
X = model_df[FEATURE_COLUMNS]
y = model_df["popularity_tier"].astype(str)
How are the model inputs assembled?
- Each text search produces 1 when the selected token appears. It produces 0 when the token is absent.
- FEATURE_COLUMNS fixes the exact input order for training and later inference.
- X contains the pre-release predictor values.
- y contains the low, medium, or high target for each row.
- Run the feature-assembly cell.
The cell finishes without an error. Your predictor matrix and target series now contain aligned rows.
Does feature assembly fail?
Run the indicator-definition cell before this one. Check that FEATURE_COLUMNS contains the same keys created by both dictionaries.
Help me debug the feature matrix assembly.
Split the data and test the baseline
A held-out test set measures predictions on rows that the model did not use for fitting. A stratified split preserves the class proportions across the training and test sets.
- Add a new code cell below the feature-assembly cell.
- Create the reproducible training and test sets by pasting this code:
# Cell 6: preserve class proportions in the held-out test set
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.20,
random_state=42,
stratify=y,
)
print(X_train.shape, X_test.shape)
What does the split preserve?
- Scikit-learn places 80 percent of the rows in the training set. It reserves 20 percent for testing.
- random_state=42 makes the split repeatable.
- stratify=y preserves the popularity-tier proportions in both sets.
- Run the split cell.
You will see two shapes in the output. The first contains about four times as many rows as the second.
Does the split cell fail?
Run the feature-assembly cell first so X and y exist. Confirm that train_test_split was imported in the setup cell.
Help me debug the stratified train-test split.
Before you run the baseline, which popularity tiers do you think it can recognize when its strategy always chooses the most common training class?
- Add a new code cell below the split cell.
- Fit and evaluate the majority-class baseline by pasting this code:
# Cell 7: experience the majority-class baseline limitation
dummy = DummyClassifier(strategy="most_frequent")
dummy.fit(X_train, y_train)
dummy_predictions = dummy.predict(X_test)
dummy_balanced_accuracy = balanced_accuracy_score(
y_test, dummy_predictions
)
print("Dummy balanced accuracy:", round(dummy_balanced_accuracy, 3))
print(
classification_report(
y_test,
dummy_predictions,
labels=TIER_ORDER,
zero_division=0,
)
)
What does this baseline prove?
- DummyClassifier uses the most frequent training class for every prediction.
- Balanced accuracy averages recall across the low, medium, and high tiers.
- The classification report exposes which tiers the majority-only strategy never recognizes.
- Run the baseline cell.
You will see the dummy balanced accuracy followed by the classification report. At least one non-majority tier has zero recall.
You have now caught class imbalance in a result you can point to. The intended shortfall gives your next model a meaningful result to beat.
Does the baseline cell fail?
Run the split cell before fitting dummy. Confirm that the setup cell imported DummyClassifier, balanced_accuracy_score, and classification_report.
Help me debug the majority-class baseline evaluation.
Use these tabs to confirm that your notebook contains every cell through the baseline evaluation.
✔️ Awesome, I've got everything!
Great. Your notebook now contains honest pre-release features plus a reproducible baseline result.
ⓧ I'd like to double check the full code
# Cell 1: imports and environment verification
from pathlib import Path
import joblib
import matplotlib
import pandas as pd
import sklearn
from joblib import dump, load
from sklearn.dummy import DummyClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
ConfusionMatrixDisplay,
balanced_accuracy_score,
classification_report,
)
from sklearn.model_selection import train_test_split
print(
{
"pandas": pd.__version__,
"scikit-learn": sklearn.__version__,
"matplotlib": matplotlib.__version__,
"joblib": joblib.__version__,
}
)
# Cell 2: locate the attached dataset
DATA_FILES = sorted(
Path("/kaggle/input").rglob("games_march2025_cleaned.csv")
)
if not DATA_FILES:
raise FileNotFoundError(
"Attach Steam Games Dataset 2025 before running this notebook."
)
DATA_PATH = DATA_FILES[0]
print(DATA_PATH)
# Cell 3: load only the columns used by the honest model
RAW_COLUMNS = [
"Name",
"Estimated owners",
"Required age",
"Price",
"Supported languages",
"Windows",
"Mac",
"Linux",
"Achievements",
"Categories",
"Genres",
]
df = pd.read_csv(DATA_PATH, usecols=RAW_COLUMNS)
print("Shape:", df.shape)
print(df.dtypes)
print(df.isna().sum().sort_values(ascending=False))
df.head()
# Cell 4: create the prediction target
def ownership_midpoint(value):
if pd.isna(value):
return None
bounds = str(value).replace(",", "").split(" - ")
if len(bounds) != 2:
return None
return (float(bounds[0]) + float(bounds[1])) / 2
df["owner_midpoint"] = df["Estimated owners"].map(ownership_midpoint)
df["popularity_tier"] = pd.cut(
df["owner_midpoint"],
bins=[-float("inf"), 50_000, 1_000_000, float("inf")],
labels=["low", "medium", "high"],
right=False,
)
model_df = df.dropna(subset=["popularity_tier"]).copy()
TIER_ORDER = ["low", "medium", "high"]
tier_ax = (
model_df["popularity_tier"]
.value_counts()
.reindex(TIER_ORDER)
.plot(kind="bar", title="Steam game popularity tiers")
)
tier_ax
# Cell 5: engineer numeric pre-release features
def to_binary(series):
normalized = series.astype(str).str.strip().str.lower()
return (
normalized
.map({"true": 1, "false": 0, "1": 1, "0": 0})
.fillna(0)
.astype(int)
)
def count_languages(value):
if pd.isna(value) or not str(value).strip():
return 0
return len(
[item for item in str(value).split(",") if item.strip()]
)
for column in ["Price", "Required age", "Achievements"]:
model_df[column] = pd.to_numeric(
model_df[column], errors="coerce"
).fillna(0)
for column in ["Windows", "Mac", "Linux"]:
model_df[column] = to_binary(model_df[column])
model_df["language_count"] = model_df["Supported languages"].map(
count_languages
)
genre_text = model_df["Genres"].fillna("").astype(str)
category_text = model_df["Categories"].fillna("").astype(str)
GENRE_FEATURES = {
"genre_action": "Action",
"genre_adventure": "Adventure",
"genre_casual": "Casual",
"genre_indie": "Indie",
"genre_rpg": "RPG",
"genre_strategy": "Strategy",
}
CATEGORY_FEATURES = {
"category_single_player": "Single-player",
"category_multiplayer": "Multi-player",
"category_coop": "Co-op",
"category_controller": "Full controller support",
}
for feature, token in GENRE_FEATURES.items():
model_df[feature] = genre_text.str.contains(
token, case=False, regex=False, na=False
).astype(int)
for feature, token in CATEGORY_FEATURES.items():
model_df[feature] = category_text.str.contains(
token, case=False, regex=False, na=False
).astype(int)
FEATURE_COLUMNS = [
"Price",
"Required age",
"Achievements",
"Windows",
"Mac",
"Linux",
"language_count",
*GENRE_FEATURES.keys(),
*CATEGORY_FEATURES.keys(),
]
X = model_df[FEATURE_COLUMNS]
y = model_df["popularity_tier"].astype(str)
# Cell 6: preserve class proportions in the held-out test set
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.20,
random_state=42,
stratify=y,
)
print(X_train.shape, X_test.shape)
# Cell 7: experience the majority-class baseline limitation
dummy = DummyClassifier(strategy="most_frequent")
dummy.fit(X_train, y_train)
dummy_predictions = dummy.predict(X_test)
dummy_balanced_accuracy = balanced_accuracy_score(
y_test, dummy_predictions
)
print("Dummy balanced accuracy:", round(dummy_balanced_accuracy, 3))
print(
classification_report(
y_test,
dummy_predictions,
labels=TIER_ORDER,
zero_division=0,
)
)
Your feature pipeline is ready. Next, you will train a balanced Random Forest that can learn from the metadata and compete with this baseline.
Train and Diagnose the Random Forest
Your majority-class baseline exposed class imbalance. The baseline ignored every metadata feature, so the less common popularity tiers received no useful predictions.
A Random Forest can learn nonlinear patterns from the game metadata. Balanced subsample weighting gives each popularity tier more influence during training.
In this step, get ready to:
- Train a class-balanced Random Forest on the existing training split.
- Evaluate predictions with balanced accuracy plus a class-level report.
- Diagnose the model with a normalized confusion matrix plus feature importances.
Train the balanced Random Forest
The baseline predicted only the most common tier. This model uses the same untouched split, which makes the comparison fair.
- Add a new code cell beneath the baseline report.
- Prepare the model training cell by pasting this code:
# Cell 8: train a class-balanced Random Forest
forest = RandomForestClassifier(
n_estimators=200,
min_samples_leaf=2,
class_weight="balanced_subsample",
random_state=42,
n_jobs=-1,
)
forest.fit(X_train, y_train)
forest_predictions = forest.predict(X_test)
forest_balanced_accuracy = balanced_accuracy_score(
y_test, forest_predictions
)
print("Forest balanced accuracy:", round(forest_balanced_accuracy, 3))
print(
classification_report(
y_test,
forest_predictions,
labels=TIER_ORDER,
zero_division=0,
)
)
What does this code do?
- The forest combines predictions from 200 decision trees.
- The minimum leaf size reduces branches built from tiny groups of games.
- Balanced subsample weighting recalculates class weights for each tree's training sample.
- The fixed random state makes repeated runs reproducible.
- The untouched test set measures how the forest handles games it did not train on.
Before you run this cell, do you expect every popularity tier to receive useful recall?
- Run the model training cell.
You will see a forest balanced-accuracy value. A class-level report follows with rows for the low, medium, and high tiers.
Missing a model or test variable?
- Confirm that the earlier feature-engineering cells created `X_train` plus `X_test`.
- Rerun the import cell if the classifier or metric names are unavailable.
- Rerun the split cell if your notebook session restarted.
Help me debug the Random Forest training cell.
Read the normalized confusion matrix
A confusion matrix shows which true tiers become which predicted tiers. Row normalization turns each cell into a share of its true class, so classes with different sizes remain comparable.
- Add a new code cell beneath the forest report.
- Prepare the normalized confusion matrix by pasting this code:
# Cell 9: diagnose class-level errors
ConfusionMatrixDisplay.from_predictions(
y_test,
forest_predictions,
labels=TIER_ORDER,
display_labels=TIER_ORDER,
normalize="true",
values_format=".2f",
)
How does the matrix work?
- The display compares the true test labels with the forest predictions.
- The tier order keeps low, medium, and high in a consistent sequence.
- True-class normalization makes every row total approximately one.
- Two-decimal formatting makes the class-level shares easier to compare.
- Run the confusion-matrix cell.
You will see a three-class matrix with low, medium, and high on both axes. The diagonal cells show the correctly classified share of each true tier.
Confusion matrix not displayed?
- Run the forest training cell again if `forest_predictions` is unavailable.
- Confirm that the display call remains the final expression in the cell.
- Rerun the setup imports if `ConfusionMatrixDisplay` is unavailable.
Help me display and interpret the normalized confusion matrix.
Inspect feature importance
The forest records an impurity-based feature importance for every training column. Pairing those values with `FEATURE_COLUMNS` reveals which inputs influenced the fitted trees most.
- Add a new code cell beneath the confusion matrix.
- Prepare the feature-importance chart by pasting this code:
# Cell 10: inspect impurity-based feature importances
importance_ax = (
pd.Series(forest.feature_importances_, index=FEATURE_COLUMNS)
.sort_values()
.plot(kind="barh", title="Random Forest feature importances")
)
importance_ax
What does this chart show?
- The series pairs every importance value with its matching feature name.
- Sorting places the least influential features at the beginning of the series.
- The horizontal bars make the feature names readable beside their relative importance.
Before you run the final cell, which pre-release feature do you expect to have the longest bar?
- Run the feature-importance cell.
You will see a horizontal chart titled `Random Forest feature importances`. Every entry from `FEATURE_COLUMNS` has a bar.
Feature chart missing or empty?
- Run the forest training cell again if `forest` has not been fitted.
- Confirm that `FEATURE_COLUMNS` matches the columns used to build `X`.
- Keep `importance_ax` as the final line so the notebook renders the chart.
Help me fix the feature-importance chart.
That is the key diagnostic win: your trained forest now comes with evidence you can inspect.
✔️ Awesome, I've got everything!
Great. Double-check that your three new cells have run successfully.
ⓧ I'd like to double check the full code
# Cell 1: imports and environment verification
from pathlib import Path
import joblib
import matplotlib
import pandas as pd
import sklearn
from joblib import dump, load
from sklearn.dummy import DummyClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
ConfusionMatrixDisplay,
balanced_accuracy_score,
classification_report,
)
from sklearn.model_selection import train_test_split
print(
{
"pandas": pd.__version__,
"scikit-learn": sklearn.__version__,
"matplotlib": matplotlib.__version__,
"joblib": joblib.__version__,
}
)
# Cell 2: locate the attached dataset
DATA_FILES = sorted(
Path("/kaggle/input").rglob("games_march2025_cleaned.csv")
)
if not DATA_FILES:
raise FileNotFoundError(
"Attach Steam Games Dataset 2025 before running this notebook."
)
DATA_PATH = DATA_FILES[0]
print(DATA_PATH)
# Cell 3: load only the columns used by the honest model
RAW_COLUMNS = [
"Name",
"Estimated owners",
"Required age",
"Price",
"Supported languages",
"Windows",
"Mac",
"Linux",
"Achievements",
"Categories",
"Genres",
]
df = pd.read_csv(DATA_PATH, usecols=RAW_COLUMNS)
print("Shape:", df.shape)
print(df.dtypes)
print(df.isna().sum().sort_values(ascending=False))
df.head()
# Cell 4: create the prediction target
def ownership_midpoint(value):
if pd.isna(value):
return None
bounds = str(value).replace(",", "").split(" - ")
if len(bounds) != 2:
return None
return (float(bounds[0]) + float(bounds[1])) / 2
df["owner_midpoint"] = df["Estimated owners"].map(ownership_midpoint)
df["popularity_tier"] = pd.cut(
df["owner_midpoint"],
bins=[-float("inf"), 50_000, 1_000_000, float("inf")],
labels=["low", "medium", "high"],
right=False,
)
model_df = df.dropna(subset=["popularity_tier"]).copy()
TIER_ORDER = ["low", "medium", "high"]
tier_ax = (
model_df["popularity_tier"]
.value_counts()
.reindex(TIER_ORDER)
.plot(kind="bar", title="Steam game popularity tiers")
)
tier_ax
# Cell 5: engineer numeric pre-release features
def to_binary(series):
normalized = series.astype(str).str.strip().str.lower()
return (
normalized
.map({"true": 1, "false": 0, "1": 1, "0": 0})
.fillna(0)
.astype(int)
)
def count_languages(value):
if pd.isna(value) or not str(value).strip():
return 0
return len(
[item for item in str(value).split(",") if item.strip()]
)
for column in ["Price", "Required age", "Achievements"]:
model_df[column] = pd.to_numeric(
model_df[column], errors="coerce"
).fillna(0)
for column in ["Windows", "Mac", "Linux"]:
model_df[column] = to_binary(model_df[column])
model_df["language_count"] = model_df["Supported languages"].map(
count_languages
)
genre_text = model_df["Genres"].fillna("").astype(str)
category_text = model_df["Categories"].fillna("").astype(str)
GENRE_FEATURES = {
"genre_action": "Action",
"genre_adventure": "Adventure",
"genre_casual": "Casual",
"genre_indie": "Indie",
"genre_rpg": "RPG",
"genre_strategy": "Strategy",
}
CATEGORY_FEATURES = {
"category_single_player": "Single-player",
"category_multiplayer": "Multi-player",
"category_coop": "Co-op",
"category_controller": "Full controller support",
}
for feature, token in GENRE_FEATURES.items():
model_df[feature] = genre_text.str.contains(
token, case=False, regex=False, na=False
).astype(int)
for feature, token in CATEGORY_FEATURES.items():
model_df[feature] = category_text.str.contains(
token, case=False, regex=False, na=False
).astype(int)
FEATURE_COLUMNS = [
"Price",
"Required age",
"Achievements",
"Windows",
"Mac",
"Linux",
"language_count",
*GENRE_FEATURES.keys(),
*CATEGORY_FEATURES.keys(),
]
X = model_df[FEATURE_COLUMNS]
y = model_df["popularity_tier"].astype(str)
# Cell 6: preserve class proportions in the held-out test set
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.20,
random_state=42,
stratify=y,
)
print(X_train.shape, X_test.shape)
# Cell 7: experience the majority-class baseline limitation
dummy = DummyClassifier(strategy="most_frequent")
dummy.fit(X_train, y_train)
dummy_predictions = dummy.predict(X_test)
dummy_balanced_accuracy = balanced_accuracy_score(
y_test, dummy_predictions
)
print("Dummy balanced accuracy:", round(dummy_balanced_accuracy, 3))
print(
classification_report(
y_test,
dummy_predictions,
labels=TIER_ORDER,
zero_division=0,
)
)
# Cell 8: train a class-balanced Random Forest
forest = RandomForestClassifier(
n_estimators=200,
min_samples_leaf=2,
class_weight="balanced_subsample",
random_state=42,
n_jobs=-1,
)
forest.fit(X_train, y_train)
forest_predictions = forest.predict(X_test)
forest_balanced_accuracy = balanced_accuracy_score(
y_test, forest_predictions
)
print("Forest balanced accuracy:", round(forest_balanced_accuracy, 3))
print(
classification_report(
y_test,
forest_predictions,
labels=TIER_ORDER,
zero_division=0,
)
)
# Cell 9: diagnose class-level errors
ConfusionMatrixDisplay.from_predictions(
y_test,
forest_predictions,
labels=TIER_ORDER,
display_labels=TIER_ORDER,
normalize="true",
values_format=".2f",
)
# Cell 10: inspect impurity-based feature importances
importance_ax = (
pd.Series(forest.feature_importances_, index=FEATURE_COLUMNS)
.sort_values()
.plot(kind="barh", title="Random Forest feature importances")
)
importance_ax
Your forest can now predict every popularity tier with inspectable evidence. Next, you will use it on a new game concept and preserve it as a reusable model bundle.
Predict and Save the Model
Your balanced Random Forest now has evaluation evidence from data it did not train on. The next goal is to use that fitted model with a game concept of your own.
Inference turns one new game description into a popularity prediction. Model persistence saves the fitted model so you can reuse it without training again.
In this step, get ready to:
- Convert a game concept into the model's feature schema.
- Predict the popularity tier of an indie action-adventure game.
- Save the trained model before confirming that its reloaded prediction matches.
Build a game feature row
The model expects every new game to use the same columns it saw during training. The feature function enforces that schema before inference begins.
- Add a new code cell below the feature-importance chart.
- Define the feature conversion function by entering this code in the new cell:
# Cell 11: build one inference row with the training schema
def make_game_features(
price,
required_age,
achievements,
windows,
mac,
linux,
supported_languages,
genres,
categories,
):
genre_value = str(genres).lower()
category_value = str(categories).lower()
row = {
"Price": float(price),
"Required age": int(required_age),
"Achievements": int(achievements),
"Windows": int(bool(windows)),
"Mac": int(bool(mac)),
"Linux": int(bool(linux)),
"language_count": count_languages(supported_languages),
}
for feature, token in GENRE_FEATURES.items():
row[feature] = int(token.lower() in genre_value)
for feature, token in CATEGORY_FEATURES.items():
row[feature] = int(token.lower() in category_value)
return pd.DataFrame([row], columns=FEATURE_COLUMNS)
What does this function do?
- The parameters capture the same pre-release details used by the honest model.
- The numeric conversions match the data types used during training.
- The genre and category loops recreate the selected binary indicators.
- The final DataFrame uses the exact order stored in FEATURE_COLUMNS.
- Run the cell containing make_game_features.
You will see an execution count beside the cell with no reported failure. The function is now available to later cells in the notebook.
Function cell not completing?
- Check that every line inside the function keeps its indentation.
- Confirm that count_languages still exists in an earlier cell.
- Confirm that GENRE_FEATURES and CATEGORY_FEATURES still exist in the notebook.
Help me debug the game feature function.
Predict a custom game concept
A custom prediction tests whether the training schema works beyond the original dataset rows. This concept describes an indie action-adventure game across price, platforms, languages, genres, and categories.
Before you run this cell, which popularity tier do you expect the forest to predict for this game?
- Add the custom concept below make_game_features by entering this code:
concept_features = make_game_features(
price=14.99,
required_age=12,
achievements=30,
windows=True,
mac=True,
linux=False,
supported_languages="English, Spanish, German",
genres="Action, Adventure, Indie",
categories="Single-player, Steam Achievements, Full controller support",
)
concept_prediction = forest.predict(concept_features)[0]
print("Custom concept prediction:", concept_prediction)
How does the prediction work?
- The function converts the concept into one row ordered by FEATURE_COLUMNS.
- The fitted forest evaluates that row using the patterns learned during training.
- The first prediction is stored in concept_prediction for the persistence check.
- Run the custom prediction cell.
You will see Custom concept prediction: followed by low, medium, or high. Your model can now score a game concept that was never a row in the training data.
Prediction cell not working?
- Run the earlier cell that defines make_game_features.
- Confirm that the fitted model is still named forest.
- Check the concept values for matching quotes and parentheses.
Help me fix the custom game prediction.
Save and reload the model
A saved model bundle needs both the fitted estimator and its expected feature order. Keeping those objects together protects future inference from a column-order mismatch.
Only load trusted model files
A model file can execute code when it loads. Reload only the artifact created by this notebook.
Before you run the final cell, do you expect the reloaded prediction to match the original?
- Add a new code cell below the custom prediction.
- Save the model bundle before reloading it by entering this code:
# Cell 12: save, reload, and verify the trusted artifact
MODEL_PATH = Path("/kaggle/working/steam_popularity_model.joblib")
model_bundle = {
"model": forest,
"feature_columns": FEATURE_COLUMNS,
}
dump(model_bundle, MODEL_PATH)
reloaded_bundle = load(MODEL_PATH)
reloaded_prediction = reloaded_bundle["model"].predict(
concept_features[reloaded_bundle["feature_columns"]]
)[0]
pd.DataFrame(
[
{
"concept": "Indie action adventure",
"original_prediction": concept_prediction,
"reloaded_prediction": reloaded_prediction,
"model_path": str(MODEL_PATH),
}
]
)
What does the final cell preserve?
- MODEL_PATH points to the notebook's saved output location.
- model_bundle keeps the fitted forest beside its feature schema.
- dump writes the trusted bundle to disk.
- load restores the saved bundle for reuse.
- The result table compares the original prediction with reloaded_prediction.
- Run the final cell.
You will see a one-row table with matching values under original_prediction and reloaded_prediction. The model_path column will show /kaggle/working/steam_popularity_model.joblib.
That match proves the saved bundle preserves the fitted classifier and its inference schema.
Predictions do not match?
- Confirm that model_bundle stores the existing forest.
- Confirm that the bundle stores FEATURE_COLUMNS under feature_columns.
- Rerun the custom concept cell before rerunning the persistence cell.
Help me compare the original and reloaded predictions.
Use the tabs below to confirm that your cumulative notebook code matches the finished pipeline.
✔️ Awesome, I've got everything!
Your model now predicts a custom concept from the training schema. It also survives a trusted save and reload cycle.
ⓧ I'd like to double check the full code
# Cell 1: imports and environment verification
from pathlib import Path
import joblib
import matplotlib
import pandas as pd
import sklearn
from joblib import dump, load
from sklearn.dummy import DummyClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
ConfusionMatrixDisplay,
balanced_accuracy_score,
classification_report,
)
from sklearn.model_selection import train_test_split
print(
{
"pandas": pd.__version__,
"scikit-learn": sklearn.__version__,
"matplotlib": matplotlib.__version__,
"joblib": joblib.__version__,
}
)
# Cell 2: locate the attached dataset
DATA_FILES = sorted(
Path("/kaggle/input").rglob("games_march2025_cleaned.csv")
)
if not DATA_FILES:
raise FileNotFoundError(
"Attach Steam Games Dataset 2025 before running this notebook."
)
DATA_PATH = DATA_FILES[0]
print(DATA_PATH)
# Cell 3: load only the columns used by the honest model
RAW_COLUMNS = [
"Name",
"Estimated owners",
"Required age",
"Price",
"Supported languages",
"Windows",
"Mac",
"Linux",
"Achievements",
"Categories",
"Genres",
]
df = pd.read_csv(DATA_PATH, usecols=RAW_COLUMNS)
print("Shape:", df.shape)
print(df.dtypes)
print(df.isna().sum().sort_values(ascending=False))
df.head()
# Cell 4: create the prediction target
def ownership_midpoint(value):
if pd.isna(value):
return None
bounds = str(value).replace(",", "").split(" - ")
if len(bounds) != 2:
return None
return (float(bounds[0]) + float(bounds[1])) / 2
df["owner_midpoint"] = df["Estimated owners"].map(ownership_midpoint)
df["popularity_tier"] = pd.cut(
df["owner_midpoint"],
bins=[-float("inf"), 50_000, 1_000_000, float("inf")],
labels=["low", "medium", "high"],
right=False,
)
model_df = df.dropna(subset=["popularity_tier"]).copy()
TIER_ORDER = ["low", "medium", "high"]
tier_ax = (
model_df["popularity_tier"]
.value_counts()
.reindex(TIER_ORDER)
.plot(kind="bar", title="Steam game popularity tiers")
)
tier_ax
# Cell 5: engineer numeric pre-release features
def to_binary(series):
normalized = series.astype(str).str.strip().str.lower()
return (
normalized
.map({"true": 1, "false": 0, "1": 1, "0": 0})
.fillna(0)
.astype(int)
)
def count_languages(value):
if pd.isna(value) or not str(value).strip():
return 0
return len(
[item for item in str(value).split(",") if item.strip()]
)
for column in ["Price", "Required age", "Achievements"]:
model_df[column] = pd.to_numeric(
model_df[column], errors="coerce"
).fillna(0)
for column in ["Windows", "Mac", "Linux"]:
model_df[column] = to_binary(model_df[column])
model_df["language_count"] = model_df["Supported languages"].map(
count_languages
)
genre_text = model_df["Genres"].fillna("").astype(str)
category_text = model_df["Categories"].fillna("").astype(str)
GENRE_FEATURES = {
"genre_action": "Action",
"genre_adventure": "Adventure",
"genre_casual": "Casual",
"genre_indie": "Indie",
"genre_rpg": "RPG",
"genre_strategy": "Strategy",
}
CATEGORY_FEATURES = {
"category_single_player": "Single-player",
"category_multiplayer": "Multi-player",
"category_coop": "Co-op",
"category_controller": "Full controller support",
}
for feature, token in GENRE_FEATURES.items():
model_df[feature] = genre_text.str.contains(
token, case=False, regex=False, na=False
).astype(int)
for feature, token in CATEGORY_FEATURES.items():
model_df[feature] = category_text.str.contains(
token, case=False, regex=False, na=False
).astype(int)
FEATURE_COLUMNS = [
"Price",
"Required age",
"Achievements",
"Windows",
"Mac",
"Linux",
"language_count",
*GENRE_FEATURES.keys(),
*CATEGORY_FEATURES.keys(),
]
X = model_df[FEATURE_COLUMNS]
y = model_df["popularity_tier"].astype(str)
# Cell 6: preserve class proportions in the held-out test set
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.20,
random_state=42,
stratify=y,
)
print(X_train.shape, X_test.shape)
# Cell 7: experience the majority-class baseline limitation
dummy = DummyClassifier(strategy="most_frequent")
dummy.fit(X_train, y_train)
dummy_predictions = dummy.predict(X_test)
dummy_balanced_accuracy = balanced_accuracy_score(
y_test, dummy_predictions
)
print("Dummy balanced accuracy:", round(dummy_balanced_accuracy, 3))
print(
classification_report(
y_test,
dummy_predictions,
labels=TIER_ORDER,
zero_division=0,
)
)
# Cell 8: train a class-balanced Random Forest
forest = RandomForestClassifier(
n_estimators=200,
min_samples_leaf=2,
class_weight="balanced_subsample",
random_state=42,
n_jobs=-1,
)
forest.fit(X_train, y_train)
forest_predictions = forest.predict(X_test)
forest_balanced_accuracy = balanced_accuracy_score(
y_test, forest_predictions
)
print("Forest balanced accuracy:", round(forest_balanced_accuracy, 3))
print(
classification_report(
y_test,
forest_predictions,
labels=TIER_ORDER,
zero_division=0,
)
)
# Cell 9: diagnose class-level errors
ConfusionMatrixDisplay.from_predictions(
y_test,
forest_predictions,
labels=TIER_ORDER,
display_labels=TIER_ORDER,
normalize="true",
values_format=".2f",
)
# Cell 10: inspect impurity-based feature importances
importance_ax = (
pd.Series(forest.feature_importances_, index=FEATURE_COLUMNS)
.sort_values()
.plot(kind="barh", title="Random Forest feature importances")
)
importance_ax
# Cell 11: build one inference row with the training schema
def make_game_features(
price,
required_age,
achievements,
windows,
mac,
linux,
supported_languages,
genres,
categories,
):
genre_value = str(genres).lower()
category_value = str(categories).lower()
row = {
"Price": float(price),
"Required age": int(required_age),
"Achievements": int(achievements),
"Windows": int(bool(windows)),
"Mac": int(bool(mac)),
"Linux": int(bool(linux)),
"language_count": count_languages(supported_languages),
}
for feature, token in GENRE_FEATURES.items():
row[feature] = int(token.lower() in genre_value)
for feature, token in CATEGORY_FEATURES.items():
row[feature] = int(token.lower() in category_value)
return pd.DataFrame([row], columns=FEATURE_COLUMNS)
concept_features = make_game_features(
price=14.99,
required_age=12,
achievements=30,
windows=True,
mac=True,
linux=False,
supported_languages="English, Spanish, German",
genres="Action, Adventure, Indie",
categories="Single-player, Steam Achievements, Full controller support",
)
concept_prediction = forest.predict(concept_features)[0]
print("Custom concept prediction:", concept_prediction)
# Cell 12: save, reload, and verify the trusted artifact
MODEL_PATH = Path("/kaggle/working/steam_popularity_model.joblib")
model_bundle = {
"model": forest,
"feature_columns": FEATURE_COLUMNS,
}
dump(model_bundle, MODEL_PATH)
reloaded_bundle = load(MODEL_PATH)
reloaded_prediction = reloaded_bundle["model"].predict(
concept_features[reloaded_bundle["feature_columns"]]
)[0]
pd.DataFrame(
[
{
"concept": "Indie action adventure",
"original_prediction": concept_prediction,
"reloaded_prediction": reloaded_prediction,
"model_path": str(MODEL_PATH),
}
]
)
How do the final cells fit together?
Cell 11 transforms one game concept into the exact schema used during training. Cell 12 preserves the forest with that schema before verifying the restored prediction.
You have turned an evaluated classifier into a reusable model artifact. The matching predictions show that your training schema survives the full save and reload cycle.
Secret mission
Catch Target Leakage in the Act
A model can look stronger when it learns from information created after launch. Add Steam review counts to the existing split to expose why that score cannot support a pre-release prediction.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your notebook available, pause its compute session, or remove its persistent artifacts. Kaggle Notebooks provides this project at no cost, so there are no ongoing charges.
Resources you used:
- One Kaggle Notebook containing the honest popularity pipeline plus the leakage experiment.
- One attachment to Steam Games Dataset 2025.
- One saved model bundle at /kaggle/working/steam_popularity_model.joblib.
- One Kaggle CPU interactive session.
Keep everything running
No action is needed if you are still testing game concepts or comparing the honest model with the leakage experiment.
- Select Save & Run All in the notebook editor to create a version containing your latest code plus its outputs.
- Keep the notebook page open until the version run finishes.
- Confirm the completed version includes the honest model results plus the leakage comparison table.
- Keep the notebook in your Kaggle account so its dataset attachment plus saved output remain available.
Your saved version keeps the work together for another modeling session.
Pause - I'll come back to this later
Stop the interactive session to free its CPU compute while keeping the saved notebook version available.
- Select Save & Run All in the notebook editor to create a current version.
- Wait for the version run to finish.
- Stop the interactive session from the notebook session controls.
- Confirm the notebook no longer shows an active CPU session.
Your saved code, outputs, dataset attachment, plus model bundle remain available when you return.
Delete - I don't want to use this again
Deletion is permanent, so pause here knowing it affects only this project notebook plus its saved artifacts. The public Steam Games Dataset 2025 remains available on Kaggle.
- Stop the interactive session from the notebook session controls.
- Return to the Kaggle page for this notebook.
- Use the notebook deletion option on that page to remove the Kaggle Notebook.
- Confirm the deletion when Kaggle asks.
- Check your notebook list to verify the project notebook no longer appears.
Deleting the notebook also removes its saved outputs, its dataset attachment, plus the persistent copy of /kaggle/working/steam_popularity_model.joblib.
Nice Work!
Nice Work!
That is a wrap! You built a complete Steam popularity classifier in Kaggle Notebooks that predicts the popularity tier of a new game concept.
What you learned:
- Converted Steam ownership ranges into a multiclass classification target spanning low through high popularity. Built an honest feature set from catalog metadata available before release.
- Used a majority-class baseline to expose class imbalance. Trained a class-balanced Random Forest on the fixed split. Measured balanced accuracy on untouched test rows.
- Read a normalized confusion matrix to find class-level errors. Used a feature-importance chart to explain the model's signals. Saved a reusable model bundle to /kaggle/working/steam_popularity_model.joblib. Reloaded the bundle to reproduce the custom game prediction.
- Secret Mission: Demonstrated target leakage by adding post-release review counts to a second model. Compared the honest score with the leaky score. Documented why the leaky result cannot support a pre-release prediction.
Ready to quiz yourself?