Build an Inventory Recommender
Forecast product demand and create inventory recommendations with Python.
Introduction
30 Second Summary
Too little stock loses sales. Too much stock traps cash in products that wait for a buyer.
In this project, you will build a notebook-style Python workflow that turns a Kaggle inventory dataset into daily demand forecasts for an adaptable Tanzanian retail scenario. You will evaluate a machine learning model on later records before converting its predictions into stock recommendations.
What You'll Build
Your completed workflow produces a ranked recommendation file that a shop manager can open in Excel to see which products need attention first.
By the end of this project, you'll have:
- A visible dataset check that shows the expected retail fields plus the dates covered by the file.
- A chronological validation that compares a constant-demand baseline with a trained model using mean absolute error.
- A retailer-facing recommendation report saved as inventory_recommendations.csv that ranks products as ORDER NOW, MONITOR, or HOLD.
- Secret Mission: Add a per-product spending limit that caps each suggested order while revealing any remaining shortage.
Are there any prerequisites?
Basic Python syntax is enough. A Kaggle account plus Visual Studio Code with Jupyter support gives you everything else you need.
Before We Start
This checkpoint gives your inventory recommender a real stock problem before any hands-on work begins. Every result stays framed as part of a learning prototype.
A small grocery shop may lose sales when everyday products run out. A clothing shop may tie up cash when slow-moving stock fills its shelves.
Set Up the Dataset and Python Environment
You have chosen the Tanzanian retailer your prototype supports. The next challenge is giving that prototype a reproducible source of historical inventory data.
This step places the exact Kaggle file inside a pinned Python environment. Visual Studio Code then uses the same interpreter to run a Jupyter-style cell.
In this step, get ready to:
- Save the retail dataset at data/inventory_demand_forecasting.csv.
- Verify Python 3.11 or newer.
- Install the pinned packages from requirements.txt.
- Run the first cell in inventory_recommender.py.
Download the dataset and build the folder structure
The model needs one predictable file location. A fixed path also prevents later cells from loading the wrong download.
- Visit the Inventory Demand Forecasting dataset page.
- Use the dataset page's download control to download its data.
- Use your browser's downloaded-file control to show the download in File Explorer.
- Extract the download with Windows' built-in archive tool if it arrives as an archive.
- Select your Desktop in File Explorer.
- Create an inventory-recommender folder on your Desktop.
- Create a data folder inside inventory-recommender.
- Move inventory_demand_forecasting.csv into the data folder.
You should now see the dataset at data/inventory_demand_forecasting.csv inside inventory-recommender.
Keep the filename unchanged. The next step uses this exact relative path when it reads the CSV.
Can't find the CSV?
Check the extracted download for inventory_demand_forecasting.csv. Make sure Windows has not placed an extra archive folder between data and the CSV.
Ask for help with the folder structure:
Verify Python and install the pinned packages
A requirements file records the package versions that the project expects. This project uses pandas 3.0.6 for tabular data and scikit-learn 1.9.1 for machine learning.
- Press the Windows key to open Windows search.
- Type Visual Studio Code and press Enter.
- Use Visual Studio Code's folder-opening option to select Desktop\inventory-recommender.
- Select the new-file control in the Explorer sidebar.
- Name the file requirements.txt.
- Add the pinned dependencies by pasting this content:
pandas==3.0.6
scikit-learn==1.9.1
What does this file do?
- The first line pins pandas to the version used by this project.
- The second line pins scikit-learn to its matching project version.
- Exact pins give every learner the same package APIs.
- Save requirements.txt.
- Confirm the Explorer sidebar lists requirements.txt beside the data folder.
Your project now carries its package requirements with the dataset.
Requirements file missing?
Check that requirements.txt is directly inside inventory-recommender. Make sure its name does not end with an extra text-file extension.
Ask for help checking the file:
✔️ Awesome, I've got everything!
Your saved requirements.txt contains both pinned dependencies.
ⓧ I'd like to double check the full code
pandas==3.0.6
scikit-learn==1.9.1
Both pinned packages require Python 3.11 or newer. Checking the interpreter first prevents an incompatible package installation.
- Open Visual Studio Code's integrated terminal panel.
- Choose PowerShell as the terminal profile.
- Check your Python version by running this command:
py --version
What does this command check?
The Windows Python launcher reports the interpreter version it selects. Use the result to choose the matching path below.
✔️ I see version 3.11 or higher
Your Python version supports both pinned packages. Keep this PowerShell terminal open for the installation.
ⓧ I see an older version
Your current interpreter is below the packages' >=3.11 requirement. Install a supported release before continuing.
- Visit the official Python releases for Windows page.
- Download a Windows installer for Python 3.11 or newer.
- Run the downloaded installer.
- Keep the Python launcher enabled during installation.
- Return to the Visual Studio Code terminal panel after installation.
- Repeat the version check shown above in a fresh PowerShell session.
You can continue when the command reports Python 3.11 or newer.
ⓧ Command not found
Windows cannot find the Python launcher yet. Installing Python with its launcher makes the required py command available.
- Visit the official Python releases for Windows page.
- Download a Windows installer for Python 3.11 or newer.
- Run the downloaded installer.
- Keep the Python launcher enabled during installation.
- Return to the Visual Studio Code terminal panel after installation.
- Repeat the version check shown above in a fresh PowerShell session.
You can continue when the command reports Python 3.11 or newer.
- Install the project dependencies by running this command:
py -m pip install -r requirements.txt
What does this command do?
- The py launcher uses the verified Python interpreter.
- The -m pip portion runs that interpreter's package installer.
- The -r option reads both pins from requirements.txt.
The command finishes without an installation error. Your selected Python installation now has the two required packages.
Package installation failed?
Confirm the PowerShell terminal is inside inventory-recommender. The installer needs to find requirements.txt in that folder.
If the failure mentions compatibility, repeat the version check. A Python version below 3.11 cannot install these pins.
Ask for help with the installation error:
Select the interpreter and run the first cell
A # %% delimiter turns a Python file section into a runnable notebook-style cell. Running a small import cell proves that Visual Studio Code is using the environment you just prepared.
- Select the new-file control in the Explorer sidebar.
- Name the file inventory_recommender.py.
- Create the environment check by pasting this cell:
# %%
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
print("Environment ready.")
What does this cell do?
- The # %% delimiter defines the cell boundary.
- The pandas as pd import makes the tabular data library available as pd.
- ColumnTransformer and OneHotEncoder provide the categorical preprocessing tools used later.
- RandomForestRegressor provides the demand model used later.
- Pipeline connects preprocessing with the model.
- mean_absolute_error provides the evaluation metric used later.
- The final print gives you a visible environment check.
- Save inventory_recommender.py.
- Open the Command Palette from Visual Studio Code's top menu.
- Choose Python: Select Interpreter.
- Select the interpreter whose version matched your PowerShell check.
Before you run the cell, do you expect every import to load successfully?
- Click Run Cell above the # %% delimiter.
You will see Environment ready. in the Python Interactive window. That output confirms the selected interpreter can import every pinned dependency.
Cell import failed?
Use Python: Select Interpreter again if the Interactive window is using a different interpreter. Select the environment where you installed requirements.txt.
Check each import against the full file below if Python reports a missing module or import name.
Ask for help with the cell error:
✔️ Awesome, I've got everything!
Your saved inventory_recommender.py contains the complete environment check cell.
ⓧ I'd like to double check the full code
# %%
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
print("Environment ready.")
Your dataset and Python environment are ready for the first data check. Next, you will load the CSV and confirm its retail fields before trusting it with a model.
Prepare the Demand Dataset
Your Python environment already runs the first cell in inventory_recommender.py. The Kaggle file is waiting at data/inventory_demand_forecasting.csv.
A demand forecast needs an explicit source path. It also needs a known schema.
Chronological validation depends on records being ordered by date. Calendar fields then give later models usable weekday and monthly signals.
In this step, get ready to:
- Define the source file path.
- Validate the seven required retail fields.
- Prepare chronologically ordered records with calendar signals.
Build the data-loading cell
The pandas library loads the retailer's CSV into a DataFrame. The required-column check stops the cell when the file does not match the expected retail schema.
- In inventory_recommender.py, place your cursor after print("Environment ready.").
- Create a new Python cell by pasting the code below:
# %%
DATA_PATH = "data/inventory_demand_forecasting.csv"
REQUIRED_COLUMNS = [
"date",
"product_id",
"category",
"price",
"promotion",
"stock",
"demand",
]
data = pd.read_csv(DATA_PATH, parse_dates=["date"])
missing_columns = sorted(set(REQUIRED_COLUMNS) - set(data.columns))
if missing_columns:
raise ValueError(f"Missing columns: {missing_columns}")
data = data.sort_values(by="date", ignore_index=True)
data["day_of_week"] = data["date"].dt.dayofweek
data["month"] = data["date"].dt.month
print(data[REQUIRED_COLUMNS].head().to_string(index=False))
print(f"Rows: {len(data):,}")
print(
"Date range:",
data["date"].min().date(),
"to",
data["date"].max().date(),
)
What does this code do?
- DATA_PATH keeps the dataset location in one reusable value.
- REQUIRED_COLUMNS acts as the schema contract for the workflow.
- parse_dates converts date into values that pandas can order chronologically.
- missing_columns identifies required fields that are absent from the CSV.
- Sorting by date protects the order needed for time-aware evaluation.
- day_of_week captures the position of each record within a week.
- month captures the calendar month for each record.
- The final print statements expose the schema preview, row count, and observed date range.
Before you run the cell, do you expect the saved file to satisfy every required field?
- Save inventory_recommender.py.
- Click Run Cell above the new # %% cell.
You'll see a compact table whose header matches REQUIRED_COLUMNS. Sample values appear beneath the header.
The row count appears below the preview. The final line displays the observed date range.
No dataset preview?
- Confirm that inventory_demand_forecasting.csv is inside the data folder.
- Check that DATA_PATH matches data/inventory_demand_forecasting.csv exactly.
- Compare the CSV headers with REQUIRED_COLUMNS if the output mentions Missing columns:.
Help me diagnose the data-loading cell.
✔️ Awesome, I've got everything!
Your file now matches the prepared dataset workflow.
ⓧ I'd like to double check the full code
# %%
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
print("Environment ready.")
# %%
DATA_PATH = "data/inventory_demand_forecasting.csv"
REQUIRED_COLUMNS = [
"date",
"product_id",
"category",
"price",
"promotion",
"stock",
"demand",
]
data = pd.read_csv(DATA_PATH, parse_dates=["date"])
missing_columns = sorted(set(REQUIRED_COLUMNS) - set(data.columns))
if missing_columns:
raise ValueError(f"Missing columns: {missing_columns}")
data = data.sort_values(by="date", ignore_index=True)
data["day_of_week"] = data["date"].dt.dayofweek
data["month"] = data["date"].dt.month
print(data[REQUIRED_COLUMNS].head().to_string(index=False))
print(f"Rows: {len(data):,}")
print(
"Date range:",
data["date"].min().date(),
"to",
data["date"].max().date(),
)
How is this file organised?
This reference combines the environment cell with the complete data-loading cell. The second cell ends with the three outputs used for verification.
Confirm the prepared dataset
The preview proves that the expected schema loaded. The date range proves that the records are ready for a chronological split.
Before the final run, do you expect the date range to follow the same order as the prepared rows?
- Click Run Cell above the data-loading cell.
The cell prints a compact preview first. The row count appears next.
The final line begins with Date range:. It shows the earliest date before the latest date.
- Confirm that the preview header matches REQUIRED_COLUMNS.
- Check that sample values appear beneath every header.
- Check that the row count appears below the preview.
- Check that the date range moves from the earliest record to the latest record.
You have the data foundation in place for chronological evaluation.
Your demand data now has a checked schema. Next, you'll measure a constant-demand baseline on later records.
Evaluate the Demand Baseline
Your retail records now load in chronological order. The next forecasting approach needs a fair reference score before its added complexity means anything.
A baseline model uses one training-period average across every later row. Evaluating that average on held-out demand gives you a benchmark for the next model.
In this step, get ready to:
- Reserve the earliest 80 percent of rows for training.
- Keep the latest 20 percent of rows outside the training period.
- Measure the constant-demand baseline with mean absolute error.
Protect the later test period
Chronological validation keeps later records outside the training period. This prevents future demand from influencing the benchmark being evaluated.
- Switch back to inventory_recommender.py in Visual Studio Code.
- Find the line data = data.sort_values(by="date", ignore_index=True) in the data-loading cell.
- Confirm the sort line appears before the day_of_week column is created.
Why does time order matter?
A shuffled evaluation can place later retail records inside the training period. That can produce an overly optimistic forecasting score.
This chronological holdout gives the earliest 80 percent of rows to train. The latest 20 percent stays in test for evaluation.
Measure the constant baseline
Mean absolute error measures the average size of the gaps between predictions and actual demand. A lower value represents smaller prediction errors.
- Add a new # %% cell beneath the data-loading cell by pasting this code:
# %%
SPLIT_FRACTION = 0.80
split_index = int(len(data) * SPLIT_FRACTION)
train = data.iloc[:split_index].copy()
test = data.iloc[split_index:].copy()
baseline_value = train["demand"].mean()
baseline_predictions = [baseline_value for _ in range(len(test))]
baseline_mae = mean_absolute_error(test["demand"], baseline_predictions)
print(f"Baseline MAE: {baseline_mae:.2f}")
What does this code do?
- The SPLIT_FRACTION value places 80 percent of the ordered rows in the training period.
- The first iloc slice copies the earlier rows into train.
- The second iloc slice copies the later rows into test.
- The baseline_value stores the mean demand from the training period.
- The baseline_predictions list repeats that fixed value for every held-out row.
- The baseline_mae value measures the prediction error against later demand.
- Save inventory_recommender.py.
Before you run the cell, do you expect its one fixed prediction to react when the product or promotion changes?
- Select Run Cell above the new baseline cell.
You will see Baseline MAE: followed by a number with two decimal places.
You now have an honest benchmark. Its score comes entirely from demand in the later held-out records.
Missing the baseline score?
Run the data-loading cell first if the baseline cell cannot find data. Confirm that the baseline code sits beneath its own # %% delimiter.
Help me debug the missing baseline score.
Every row in test received the same baseline_value. That constant forecast is the intended limitation you needed to expose.
What does the baseline ignore?
- Product identity does not change the prediction.
- Category does not change the prediction.
- Price does not change the prediction.
- Promotion does not change the prediction.
- Current stock does not change the prediction.
- Calendar timing does not change the prediction.
- The baseline MAE still provides a useful comparison score.
✔️ Awesome, I've got everything!
Your saved file now contains the chronological split and the constant-demand baseline.
ⓧ I'd like to double check the full code
# %%
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
print("Environment ready.")
# %%
DATA_PATH = "data/inventory_demand_forecasting.csv"
REQUIRED_COLUMNS = [
"date",
"product_id",
"category",
"price",
"promotion",
"stock",
"demand",
]
data = pd.read_csv(DATA_PATH, parse_dates=["date"])
missing_columns = sorted(set(REQUIRED_COLUMNS) - set(data.columns))
if missing_columns:
raise ValueError(f"Missing columns: {missing_columns}")
data = data.sort_values(by="date", ignore_index=True)
data["day_of_week"] = data["date"].dt.dayofweek
data["month"] = data["date"].dt.month
print(data[REQUIRED_COLUMNS].head().to_string(index=False))
print(f"Rows: {len(data):,}")
print(
"Date range:",
data["date"].min().date(),
"to",
data["date"].max().date(),
)
# %%
SPLIT_FRACTION = 0.80
split_index = int(len(data) * SPLIT_FRACTION)
train = data.iloc[:split_index].copy()
test = data.iloc[split_index:].copy()
baseline_value = train["demand"].mean()
baseline_predictions = [baseline_value for _ in range(len(test))]
baseline_mae = mean_absolute_error(test["demand"], baseline_predictions)
print(f"Baseline MAE: {baseline_mae:.2f}")
Your chronological baseline is now measured against later demand. Next, you will test whether a machine learning model can use retail conditions to produce a stronger held-out result.
Train the Demand Model
Your chronological validation split now shows how far one constant prediction misses the later demand records. That fixed value cannot adapt when retail conditions change.
A random forest regression model can learn from each row's retail conditions. A scikit-learn Pipeline keeps preprocessing attached to the estimator throughout the workflow.
In this step, get ready to:
- Prepare categorical and numeric model features.
- Build a trained regression pipeline.
- Compare the held-out model MAE with the baseline MAE.
Choose the model inputs
Product labels need a numeric representation before the model can use them. One-hot encoding creates that representation while the measured retail values stay numeric.
- Add the feature groups below the baseline cell in inventory_recommender.py by copying this code:
# %%
CATEGORICAL_FEATURES = ["product_id", "category"]
NUMERIC_FEATURES = [
"price",
"promotion",
"stock",
"day_of_week",
"month",
]
FEATURES = CATEGORICAL_FEATURES + NUMERIC_FEATURES
What Do These Groups Control?
- CATEGORICAL_FEATURES isolates the labels that need encoding.
- NUMERIC_FEATURES names the quantities that can pass into the model as numbers.
- FEATURES gives training and prediction the same input order.
- Save inventory_recommender.py.
- Run the feature cell by selecting Run Cell above it.
The cell completes without an exception. Python now holds the three model feature lists.
Feature Cell Not Completing?
Check that every feature name has matching quotation marks. Confirm that each list has its closing bracket.
Help me check the feature lists in my demand model.
- Add the preprocessor below FEATURES by copying this code:
preprocessor = ColumnTransformer(
transformers=[
(
"categorical",
OneHotEncoder(handle_unknown="ignore"),
CATEGORICAL_FEATURES,
)
],
remainder="passthrough",
)
What Does Preprocessing Do?
- OneHotEncoder(handle_unknown="ignore") converts product and category labels into numeric indicators.
- The unknown-category setting lets the workflow process a label that appears only in the later records.
- The remaining numeric columns pass through unchanged.
- Save inventory_recommender.py.
- Run the updated feature cell by selecting Run Cell.
The cell completes without an exception. The preprocessor is now ready for the regression pipeline.
Preprocessor Cell Failing?
Check the indentation inside the transformer tuple. Confirm that CATEGORICAL_FEATURES matches the feature list you created above.
Help me debug the preprocessing block.
Build the preprocessing and regression pipeline
The pipeline applies the same transformations whenever the model fits or predicts. This keeps raw retail fields aligned with the trained regressor.
- Add the model pipeline below preprocessor by copying this code:
model = Pipeline(
steps=[
("preprocess", preprocessor),
(
"regressor",
RandomForestRegressor(
n_estimators=150,
min_samples_leaf=3,
random_state=42,
n_jobs=-1,
),
),
]
)
How Does the Pipeline Fit Together?
- Pipeline fixes the order of preprocessing and regression.
- RandomForestRegressor learns demand patterns from the prepared features.
- The regressor settings control model complexity. They also keep repeated runs comparable.
- Save inventory_recommender.py.
- Run the updated model cell by selecting Run Cell.
The cell completes without an exception. Python now holds a pipeline named model.
Pipeline Not Building?
Check that each pipeline step is inside the steps list. Confirm that the regressor's parentheses close before the pipeline closes.
Help me find the pipeline structure problem.
Train and compare the model
Training stays on the earlier records from train. The later records in test remain unseen until prediction.
- Add the fitting and evaluation logic below model by copying this code:
model.fit(train[FEATURES], train["demand"])
predictions = model.predict(test[FEATURES])
test["predicted_demand"] = [max(0, value) for value in predictions]
model_mae = mean_absolute_error(test["demand"], test["predicted_demand"])
print(f"Baseline MAE: {baseline_mae:.2f}")
print(f"Model MAE: {model_mae:.2f}")
What Does the Evaluation Prove?
- Fitting uses only train. The held-out rows stay outside model training.
- predicted_demand keeps every forecast at zero or higher.
- model_mae measures the model against demand from the later records.
- Save inventory_recommender.py.
Before you run the completed cell, which method do you expect to have the smaller held-out MAE?
- Run the completed model cell by selecting Run Cell.
You'll see Baseline MAE: followed by a numeric value. The next line shows Model MAE: with the model's result.
How Should I Read the Comparison?
A smaller mean absolute error (MAE) means the predictions missed the actual demand by less on average. Compare the values directly because model complexity alone does not guarantee better forecasts.
Model Cell Not Finishing?
Confirm that you ran the earlier data and baseline cells during this session. Check that train and test still contain the chronological split.
Help me debug the model training cell.
✔️ Awesome, I've got everything!
Your saved file now contains the chronological baseline plus the trained demand model.
ⓧ I'd like to double check the full code
- Compare your inventory_recommender.py file with this cumulative reference:
# %%
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
print("Environment ready.")
# %%
DATA_PATH = "data/inventory_demand_forecasting.csv"
REQUIRED_COLUMNS = [
"date",
"product_id",
"category",
"price",
"promotion",
"stock",
"demand",
]
data = pd.read_csv(DATA_PATH, parse_dates=["date"])
missing_columns = sorted(set(REQUIRED_COLUMNS) - set(data.columns))
if missing_columns:
raise ValueError(f"Missing columns: {missing_columns}")
data = data.sort_values(by="date", ignore_index=True)
data["day_of_week"] = data["date"].dt.dayofweek
data["month"] = data["date"].dt.month
print(data[REQUIRED_COLUMNS].head().to_string(index=False))
print(f"Rows: {len(data):,}")
print(
"Date range:",
data["date"].min().date(),
"to",
data["date"].max().date(),
)
# %%
SPLIT_FRACTION = 0.80
split_index = int(len(data) * SPLIT_FRACTION)
train = data.iloc[:split_index].copy()
test = data.iloc[split_index:].copy()
baseline_value = train["demand"].mean()
baseline_predictions = [baseline_value for _ in range(len(test))]
baseline_mae = mean_absolute_error(test["demand"], baseline_predictions)
print(f"Baseline MAE: {baseline_mae:.2f}")
# %%
CATEGORICAL_FEATURES = ["product_id", "category"]
NUMERIC_FEATURES = [
"price",
"promotion",
"stock",
"day_of_week",
"month",
]
FEATURES = CATEGORICAL_FEATURES + NUMERIC_FEATURES
preprocessor = ColumnTransformer(
transformers=[
(
"categorical",
OneHotEncoder(handle_unknown="ignore"),
CATEGORICAL_FEATURES,
)
],
remainder="passthrough",
)
model = Pipeline(
steps=[
("preprocess", preprocessor),
(
"regressor",
RandomForestRegressor(
n_estimators=150,
min_samples_leaf=3,
random_state=42,
n_jobs=-1,
),
),
]
)
model.fit(train[FEATURES], train["demand"])
predictions = model.predict(test[FEATURES])
test["predicted_demand"] = [max(0, value) for value in predictions]
model_mae = mean_absolute_error(test["demand"], test["predicted_demand"])
print(f"Baseline MAE: {baseline_mae:.2f}")
print(f"Model MAE: {model_mae:.2f}")
That comparison is now running on later records that the model did not train on. You have evidence for judging whether the retail features improved the forecast.
Your model now produces held-out demand predictions from retail conditions. Next, you'll turn those forecasts into reorder quantities and clear stock actions.
Create Stock Recommendations
Your chronological model now predicts daily demand for the held-out records. Its accuracy remains visible beside the baseline.
A demand forecast becomes useful when it supports a purchasing decision. This step converts each prediction into a stock target before producing a ranked CSV report for retailer review.
In this step, get ready to:
- Convert daily demand predictions into seven-day stock targets.
- Assign a purchasing action to every held-out record.
- Export a ranked stock recommendation report.
Translate forecasts into stock quantities
The model predicts demand for one day. A seven-day planning window converts that prediction into expected need.
A 20 percent buffer creates a higher target stock level. This gives each row a clear inventory threshold.
Treat These as Scenario Settings
The seven-day window is a learning-scenario assumption. A real retailer would replace it with a locally validated planning period.
The 20 percent buffer is also a scenario assumption. Its real value should reflect delivery schedules plus acceptable stockout risk.
- In inventory_recommender.py, add a new # %% cell below the model cell.
- Set the planning window before calculating expected need plus target stock by entering this code:
# %%
PLANNING_DAYS = 7
BUFFER_FRACTION = 0.20
test["expected_need"] = [
max(0, round(value * PLANNING_DAYS))
for value in test["predicted_demand"]
]
test["target_stock"] = [
max(0, round(need * (1 + BUFFER_FRACTION)))
for need in test["expected_need"]
]
What Does This Code Do?
- The PLANNING_DAYS value scales each daily prediction across the seven-day planning window.
- The expected_need column rounds each scaled prediction into a non-negative unit count.
- The target_stock column applies the 20 percent buffer before rounding the result.
- Save inventory_recommender.py.
- Run the new cell by clicking Run Cell above it.
- Inspect the test DataFrame in the Python Interactive window.
You will see expected_need beside target_stock. Every target stock value will be at least as large as its expected need value.
Missing the New Columns?
Confirm that you ran the model cell before the recommendation cell. The test DataFrame must already contain predicted_demand.
Keep both calculations inside the same new cell. Help me fix missing recommendation columns in inventory_recommender.py.
Assign purchasing actions
Current stock determines the urgency of each recommendation. Each row receives one action based on expected need plus buffered target stock.
- Place your cursor below the target_stock calculation in the new cell.
- Calculate each stock gap before assigning its purchasing action by entering this code:
test["reorder_quantity"] = [
max(target - stock, 0)
for target, stock in zip(test["target_stock"], test["stock"])
]
test["action"] = [
"ORDER NOW"
if stock < need
else "MONITOR"
if stock < target
else "HOLD"
for stock, need, target in zip(
test["stock"],
test["expected_need"],
test["target_stock"],
)
]
What Does This Code Do?
- The reorder_quantity calculation measures the gap between target stock plus current stock.
- The max() call prevents well-stocked rows from receiving a negative order quantity.
- The action calculation compares current stock with both inventory thresholds.
How Do the Labels Differ?
A row receives ORDER NOW when current stock cannot cover expected need. A row receives MONITOR when stock covers expected need but remains below target stock.
A row receives HOLD when current stock meets the buffered target. Every label can be traced to the same two thresholds.
- Save inventory_recommender.py.
- Run the updated cell by clicking Run Cell.
- Inspect the updated test DataFrame in the Python Interactive window.
You will see non-negative values in reorder_quantity. The action column will contain ORDER NOW, MONITOR, or HOLD.
Seeing Incorrect Actions?
Check the order of the conditions inside test["action"]. The comparison with expected need must come before the comparison with target stock.
Confirm that reorder_quantity subtracts stock from target. Help me debug the action labels in my stock recommendation cell.
Rank and export the recommendation report
A retailer needs the largest stock gaps at the top of the report. The final calculation selects the review columns before sorting every held-out row by reorder quantity.
- Place your cursor below the action calculation in the recommendation cell.
- Build the ranked report before exporting every recommendation by entering this code:
RECOMMENDATION_COLUMNS = [
"date",
"product_id",
"category",
"price",
"promotion",
"stock",
"predicted_demand",
"expected_need",
"target_stock",
"reorder_quantity",
"action",
]
recommendations = test[RECOMMENDATION_COLUMNS].sort_values(
by="reorder_quantity",
ascending=False,
ignore_index=True,
)
print(recommendations.head(10).to_string(index=False))
recommendations.to_csv("inventory_recommendations.csv", index=False)
print("Saved inventory_recommendations.csv")
What Does This Code Do?
- The RECOMMENDATION_COLUMNS list keeps the forecast inputs plus decision outputs together.
- The sort_values() call places the largest reorder quantities first.
- The first print() call displays the ten highest-priority rows.
- The to_csv() call exports every recommendation without a DataFrame index column.
- Save inventory_recommender.py.
Before you run the completed cell, which products do you expect to rise to the top of the report?
- Run the completed recommendation cell by clicking Run Cell.
You will see ten rows ranked by descending reorder_quantity. Their action values will include ORDER NOW, MONITOR, or HOLD.
The last output line says Saved inventory_recommendations.csv. This confirms that the complete held-out recommendation table was exported.
- Confirm that inventory_recommendations.csv appears in the Visual Studio Code file sidebar.
You will see inventory_recommendations.csv inside the inventory-recommender folder. The file contains all held-out rows in descending reorder quantity order.
Report Not Saved?
Confirm that you ran the complete recommendation cell from its # %% delimiter. The export lines must remain below the code that creates recommendations.
Check the Visual Studio Code file sidebar after the cell finishes. Help me find why inventory_recommendations.csv was not created.
- Compare your saved inventory_recommender.py file with the complete reference in the second tab.
✔️ Awesome, I've got everything!
Your saved file now contains the complete demand forecasting plus stock recommendation workflow.
ⓧ I'd like to double check the full code
# %%
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
print("Environment ready.")
# %%
DATA_PATH = "data/inventory_demand_forecasting.csv"
REQUIRED_COLUMNS = [
"date",
"product_id",
"category",
"price",
"promotion",
"stock",
"demand",
]
data = pd.read_csv(DATA_PATH, parse_dates=["date"])
missing_columns = sorted(set(REQUIRED_COLUMNS) - set(data.columns))
if missing_columns:
raise ValueError(f"Missing columns: {missing_columns}")
data = data.sort_values(by="date", ignore_index=True)
data["day_of_week"] = data["date"].dt.dayofweek
data["month"] = data["date"].dt.month
print(data[REQUIRED_COLUMNS].head().to_string(index=False))
print(f"Rows: {len(data):,}")
print(
"Date range:",
data["date"].min().date(),
"to",
data["date"].max().date(),
)
# %%
SPLIT_FRACTION = 0.80
split_index = int(len(data) * SPLIT_FRACTION)
train = data.iloc[:split_index].copy()
test = data.iloc[split_index:].copy()
baseline_value = train["demand"].mean()
baseline_predictions = [baseline_value for _ in range(len(test))]
baseline_mae = mean_absolute_error(test["demand"], baseline_predictions)
print(f"Baseline MAE: {baseline_mae:.2f}")
# %%
CATEGORICAL_FEATURES = ["product_id", "category"]
NUMERIC_FEATURES = [
"price",
"promotion",
"stock",
"day_of_week",
"month",
]
FEATURES = CATEGORICAL_FEATURES + NUMERIC_FEATURES
preprocessor = ColumnTransformer(
transformers=[
(
"categorical",
OneHotEncoder(handle_unknown="ignore"),
CATEGORICAL_FEATURES,
)
],
remainder="passthrough",
)
model = Pipeline(
steps=[
("preprocess", preprocessor),
(
"regressor",
RandomForestRegressor(
n_estimators=150,
min_samples_leaf=3,
random_state=42,
n_jobs=-1,
),
),
]
)
model.fit(train[FEATURES], train["demand"])
predictions = model.predict(test[FEATURES])
test["predicted_demand"] = [max(0, value) for value in predictions]
model_mae = mean_absolute_error(test["demand"], test["predicted_demand"])
print(f"Baseline MAE: {baseline_mae:.2f}")
print(f"Model MAE: {model_mae:.2f}")
# %%
PLANNING_DAYS = 7
BUFFER_FRACTION = 0.20
test["expected_need"] = [
max(0, round(value * PLANNING_DAYS))
for value in test["predicted_demand"]
]
test["target_stock"] = [
max(0, round(need * (1 + BUFFER_FRACTION)))
for need in test["expected_need"]
]
test["reorder_quantity"] = [
max(target - stock, 0)
for target, stock in zip(test["target_stock"], test["stock"])
]
test["action"] = [
"ORDER NOW"
if stock < need
else "MONITOR"
if stock < target
else "HOLD"
for stock, need, target in zip(
test["stock"],
test["expected_need"],
test["target_stock"],
)
]
RECOMMENDATION_COLUMNS = [
"date",
"product_id",
"category",
"price",
"promotion",
"stock",
"predicted_demand",
"expected_need",
"target_stock",
"reorder_quantity",
"action",
]
recommendations = test[RECOMMENDATION_COLUMNS].sort_values(
by="reorder_quantity",
ascending=False,
ignore_index=True,
)
print(recommendations.head(10).to_string(index=False))
recommendations.to_csv("inventory_recommendations.csv", index=False)
print("Saved inventory_recommendations.csv")
You have turned held-out demand forecasts into a ranked purchasing report. A retailer can now review the highest-priority products before opening the exported file.
Secret mission
Add a Per-Product Spending Limit
Your reorder table assumes every suggested unit can be purchased. You will cap each proposed order by what the scenario can afford. The second report exposes every remaining shortage.
Clean Up Your Resources
Clean Up Your Resources
Your model and reports live entirely in a local project folder with no ongoing cost. Choose whether to keep the folder, pause your work, or delete it.
Resources you used:
- The local project folder currently open in Visual Studio Code.
Keep everything running
No action is needed. Choose this option if you want to keep testing the model or reviewing its reports.
- Keep the project folder in its current location.
- Return to inventory_recommender.py when you want to regenerate inventory_recommendations.csv.
- Return to budget_scenario.py when you want to test another value for MAX_SPEND_PER_PRODUCT.
Pause - I'll come back to this later
Pause your work by ending the local editor session. Your scripts and CSV reports remain ready for your next session.
- Save any open files in the project folder.
- Stop any cell that is still running in the Python Interactive window.
- Close Visual Studio Code.
Delete - I don't want to use this again
Deleting the project folder is permanent. Your installed packages remain available for other projects.
- Return to the Visual Studio Code window from earlier.
- Right-click the top-level project folder in the Explorer sidebar.
- Choose the option that reveals the folder in your operating system's file manager.
macOS
Emptying Trash permanently removes the folder. Your Mac may ask you to confirm this deletion.
- Close Visual Studio Code.
- Select the revealed project folder in Finder.
- Press Cmd+Delete to move the folder to Trash.
- Open Trash from the Dock.
- Click Empty.
- Confirm the permanent deletion if your Mac prompts you.
- Return to the Finder window that showed the project folder.
That clears the full local project from your Mac. You should no longer see its folder in Finder or Trash.
Windows
A permanent deletion prompt protects the folder from an accidental click. Confirm it only after checking that you selected the correct project folder.
- Close Visual Studio Code.
- Select the revealed project folder in File Explorer.
- Press Shift+Delete to request permanent deletion.
- Confirm the permanent deletion when Windows prompts you.
- Check the File Explorer window that showed the project folder.
That clears the full local project from your Windows computer. You should no longer see its folder in File Explorer.
Nice Work!
Nice Work!
You did it! You built a notebook-style demand forecasting workflow that turns historical retail records into practical stock decisions.
You've learned how to:
- Build a notebook-style Python workflow around a Kaggle retail inventory dataset.
- Protect model evaluation with chronological validation. Compare a constant-demand baseline model with a trained regression model using mean absolute error.
- Convert predicted demand into stock actions labeled ORDER NOW, MONITOR, or HOLD. Export the complete retailer-facing table to inventory_recommendations.csv.
- Secret Mission: Apply a per-product spending limit with MAX_SPEND_PER_PRODUCT. Expose cash-limited demand in budget_limited_recommendations.csv through remaining_shortage.
Ready to quiz yourself?