Build a Sales Forecasting Model
Build a demand forecasting ML pipeline for a chocolate company.
Introduction
30 Second Summary
Every chocolate company faces the same problem: Valentine's Day, Easter, Halloween, and Christmas create massive sales spikes that are hard to plan for. Order too little inventory and you miss out on revenue. Order too much and you're stuck with melting stock.
In this project, you will build a demand forecasting model that predicts chocolate sales 90 days into the future. You'll generate realistic sales data, watch a basic model fail to capture seasonal patterns, then engineer smart features that make your predictions dramatically better.
What You'll Build
You run a single command and a forecast chart appears showing predicted daily chocolate sales for the next three months, with seasonal peaks clearly anticipated before they happen.
By the end of this project, you'll have:
- A 90-day demand forecast you can generate by running a single script, saved as both a visual plot and a CSV file that a real business could hand to their supply chain team.
- A clear view of feature importance showing exactly which signals drive the predictions, from holiday proximity curves to lag patterns and cyclical month encoding.
- A modular pipeline architecture where you can swap the feature module for a different domain (churn prediction, inventory optimization) without touching the model code.
- Secret Mission: Extend your pipeline to forecast three separate chocolate product lines, each with its own seasonal profile.
Are there any prerequisites?
You need basic Python familiarity (variables, functions, loops). No machine learning experience is required. This project teaches you ML concepts from scratch.
Before We Start
You're about to build a machine learning model that predicts chocolate sales 90 days into the future. Before writing any prediction code, you need a clean workspace with the right tools.
A virtual environment keeps your ML libraries isolated from your system packages. This step creates that isolation, installs everything, and confirms it all works together.
In this step, get ready to:
- Create a project directory with folders for data and output.
- Install ML libraries in an isolated virtual environment.
- Verify all imports work together.
Create the project structure
A well-organized directory structure makes your project reusable. You'll separate raw data from outputs so the pipeline stays clean as you iterate.
- Open your terminal.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder with subdirectories and move into it by running these commands:
mkdir -p chocolate-forecast/{data,outputs}
cd chocolate-forecast
- Confirm the folders exist by running:
ls
You should see data and outputs listed in the output.
Don't see the folders?
Make sure you ran the mkdir -p command from the ~/Desktop directory. The -p flag creates parent directories as needed.
help me debug why my mkdir command didn't create the expected folders
- Create and activate a virtual environment by running these commands:
python -m venv venv
source venv/bin/activate
Your terminal prompt now shows (venv) at the beginning. This confirms you're inside the virtual environment.
What is a virtual environment?
A virtual environment is an isolated Python installation inside your project folder. Packages you install here won't interfere with packages in other projects or your system Python.
The venv folder contains its own pip and its own library storage. Every command you run while (venv) is active uses this isolated copy.
Don't see (venv) in your prompt?
Make sure you're running source venv/bin/activate from inside the chocolate-forecast folder (where the venv directory lives).
help me activate my Python virtual environment
Install dependencies
Your forecasting model needs three key libraries: scikit-learn for the machine learning model, matplotlib for visualization, and NumPy for numerical operations.
- Create a file called requirements.txt in your chocolate-forecast folder by running this command:
cat > requirements.txt << 'EOF'
scikit-learn==1.9.0
matplotlib==3.11.0
pandas
numpy
EOF
What does this command do?
The cat > ... << 'EOF' pattern writes everything between the two EOF markers into the file. It's a quick way to create a small file directly from the terminal.
Pinning scikit-learn==1.9.0 and matplotlib==3.11.0 to exact versions ensures your results are reproducible. pandas and numpy are left unpinned since they're already on your system at compatible versions.
- Confirm the file was created by running:
cat requirements.txt
You should see all four package names printed.
✔️ Awesome, I've got everything!
Great. Make sure you see all four lines in the output before moving on.
ⓧ I'd like to double check the full code
Your requirements.txt should contain exactly this:
scikit-learn==1.9.0
matplotlib==3.11.0
pandas
numpy
- Install all the dependencies by running:
pip install -r requirements.txt
You'll see pip downloading and installing each package. This takes around 30 seconds depending on your internet connection.
Seeing permission errors or install failures?
Make sure your prompt shows (venv) before running pip. If it doesn't, re-activate with source venv/bin/activate and try again.
help me fix pip install errors in my virtual environment
Verify the installation
Before you run the verification, what do you expect to see if all three libraries installed correctly?
- Check that all libraries import successfully by running:
python -c "import sklearn, pandas, matplotlib; print(f'scikit-learn {sklearn.__version__}'); print(f'pandas {pandas.__version__}'); print(f'matplotlib {matplotlib.__version__}'); print('All imports OK')"
You should see each library print its version number, followed by All imports OK at the end.
What should I see?
The output prints three version lines (scikit-learn 1.9.0, your pandas version, and matplotlib 3.11.0) followed by the confirmation message. If any library failed to install, Python raises an ImportError instead.
Seeing an ImportError?
Check that your prompt still shows (venv). If you opened a new terminal tab, the virtual environment won't be active there. Re-activate it and re-run the check.
If one specific library failed, re-install it directly with pip install scikit-learn==1.9.0 (replacing the package name as needed) and check the error output for clues.
help me fix ImportError when verifying my ML libraries
- Open VS Code in your project folder by running:
code .
VS Code opens with your chocolate-forecast folder in the sidebar. You should see the data and outputs folders, plus requirements.txt and the venv directory listed in the Explorer panel.
Your workspace is ready. Next up, you'll generate three years of realistic chocolate sales data and spot the seasonal patterns hiding inside it.
Generate Sales Data and Spot the Pattern
Your environment is ready with all the libraries installed. Now you need data to train a model on.
Before you can predict anything, you need realistic data that mimics what a real chocolate company sees. Generating synthetic data yourself means you know the ground truth (the exact seasonal patterns you baked in) and can verify later whether your model actually learned them.
In this step, get ready to:
- Generate 3 years of synthetic chocolate sales data with seasonal spikes.
- Visualize the data to identify the seasonal peaks your model needs to capture.
Create the data generator
The data generator creates 3 years of daily sales figures with realistic seasonal spikes baked in. Each chocolate holiday (Valentine's Day, Easter, Halloween, Christmas) gets its own Gaussian peak, mimicking how real demand surges and fades around key dates.
- Create a new file called generate_data.py in your chocolate-forecast folder.
- Add the imports, function signature, and seasonal peak definitions by pasting this code:
import numpy as np
import pandas as pd
from pathlib import Path
def generate_chocolate_sales(start_date='2021-01-01', periods=1095, seed=42):
"""Generate 3 years of daily chocolate sales data."""
np.random.seed(seed)
dates = pd.date_range(start=start_date, periods=periods, freq='D')
# Base sales (units per day) with slight upward trend
base = 200 + np.arange(periods) * 0.05
# Seasonal patterns - chocolate peaks
day_of_year = dates.dayofyear
# Valentine's Day peak (Feb 14, approx day 45)
valentines = 150 * np.exp(-0.5 * ((day_of_year - 45) / 7) ** 2)
# Easter peak (approx day 100)
easter = 120 * np.exp(-0.5 * ((day_of_year - 100) / 10) ** 2)
# Halloween peak (Oct 31, approx day 304)
halloween = 130 * np.exp(-0.5 * ((day_of_year - 304) / 8) ** 2)
# Christmas peak (Dec 25, approx day 359)
christmas = 200 * np.exp(-0.5 * ((day_of_year - 359) / 12) ** 2)
What does this code do?
Each holiday is modeled as a Gaussian (bell curve) centered on the holiday's approximate day-of-year. The number before np.exp controls the spike height (how many extra units sell), and the divisor controls the width (how many days the rush lasts).
Valentine's Day spikes sharply over about a week. Christmas has a wider spread over almost a month of holiday shopping.
- Add the remaining logic directly below the Christmas line to complete the function and the main execution block:
# Weekly pattern (weekends slightly higher)
weekly = 20 * (dates.dayofweek >= 5).astype(float)
# Random noise
noise = np.random.normal(0, 25, periods)
# Combine all components
sales = base + valentines + easter + halloween + christmas + weekly + noise
sales = np.maximum(sales, 50) # Floor at 50 units
df = pd.DataFrame({
'date': dates,
'sales': np.round(sales).astype(int)
})
return df
if __name__ == '__main__':
Path('data').mkdir(exist_ok=True)
df = generate_chocolate_sales()
df.to_csv('data/chocolate_sales.csv', index=False)
print(f"Generated {len(df)} days of sales data")
print(f"Date range: {df['date'].min()} to {df['date'].max()}")
print(f"Sales range: {df['sales'].min()} to {df['sales'].max()}")
print(f"Saved to data/chocolate_sales.csv")
What does this code do?
- Weekend bumps add 20 extra units on Saturdays and Sundays.
- Random noise keeps the data from looking too perfectly smooth.
- All the components (base + holidays + weekends + noise) are summed together, with a floor of 50 units so sales never go negative.
- The if __name__ block runs the generator and saves the result to data/chocolate_sales.csv.
- Save generate_data.py.
- Run the data generator to create your CSV by running this command:
python generate_data.py
You should see output confirming 1095 days of data were generated, a date range from 2021-01-01 to 2023-12-31, and the file saved to data/chocolate_sales.csv.
Seeing a ModuleNotFoundError?
Make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt. If not, run source venv/bin/activate again.
Also confirm you are in the chocolate-forecast directory where your venv folder lives.
Help me debug a ModuleNotFoundError when running generate_data.py
✔️ Awesome, I've got everything!
Great. Double check you have saved generate_data.py and confirmed the CSV output in your terminal.
ⓧ I'd like to double check the full code
Your generate_data.py should contain these sections in order:
- Imports (numpy, pandas, pathlib).
- Function generate_chocolate_sales() with seed, dates, base trend, and four seasonal peaks (valentines, easter, halloween, christmas).
- Weekly pattern, random noise, component combination with floor at 50, DataFrame creation, and return.
- The if __name__ == '__main__' block that creates the data/ directory, calls the function, saves the CSV, and prints stats.
Scroll up and confirm each section matches what you pasted.
Create the exploration script
Raw CSV data is hard to reason about. A quick visualization turns 1095 rows of numbers into a clear picture of what your model needs to learn.
- Create a new file called explore.py in your chocolate-forecast folder.
- Add the imports, function start, and daily time series plot by pasting this code:
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
def explore_sales():
"""Load and visualize chocolate sales data."""
df = pd.read_csv('data/chocolate_sales.csv', parse_dates=['date'])
Path('outputs').mkdir(exist_ok=True)
# Daily sales over time
plt.figure(figsize=(14, 5))
plt.plot(df['date'], df['sales'], linewidth=0.5, alpha=0.7)
plt.title('Daily Chocolate Sales (3 Years)')
plt.xlabel('Date')
plt.ylabel('Units Sold')
plt.tight_layout()
plt.savefig('outputs/daily_sales.png', dpi=100)
plt.show()
What does this code do?
This loads the CSV you just generated and plots the raw daily sales as a line chart. The thin line (linewidth=0.5) lets you see the overall shape without the day-to-day noise dominating the picture.
The plot is saved to outputs/daily_sales.png so you have a persistent copy.
- Add the monthly bar chart and statistics output directly below the daily plot code:
# Monthly average sales
df['month'] = df['date'].dt.month
monthly_avg = df.groupby('month')['sales'].mean()
plt.figure(figsize=(10, 5))
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
plt.bar(months, monthly_avg.values, color='chocolate')
plt.title('Average Monthly Chocolate Sales')
plt.xlabel('Month')
plt.ylabel('Average Units Sold')
plt.tight_layout()
plt.savefig('outputs/monthly_pattern.png', dpi=100)
plt.show()
print("Plots saved to outputs/")
print(f"\nSales Statistics:")
print(f" Mean daily sales: {df['sales'].mean():.0f} units")
print(f" Peak month: {months[monthly_avg.idxmax() - 1]} ({monthly_avg.max():.0f} avg units)")
print(f" Lowest month: {months[monthly_avg.idxmin() - 1]} ({monthly_avg.min():.0f} avg units)")
if __name__ == '__main__':
explore_sales()
What does this code do?
- Groups sales by month and averages them across all 3 years, smoothing out the noise to reveal the seasonal shape.
- Plots a bar chart with the 'chocolate' color theme. This is the chart where seasonal peaks become unmistakable.
- Prints summary statistics so you can see the peak and lowest months numerically.
- Save explore.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved explore.py before moving on.
ⓧ I'd like to double check the full code
Your explore.py should contain these sections in order:
- Imports (pandas, matplotlib.pyplot, pathlib).
- Function explore_sales() starting with CSV load and the daily time series plot.
- Monthly average bar chart with the months list, plt.bar() call, and print statements for statistics.
- The if __name__ == '__main__' block calling explore_sales().
Scroll up and confirm each section matches what you pasted.
Run both scripts and spot the seasonal pattern
With both scripts saved, you can generate the data and visualize it in one go. The monthly bar chart is the key output. It should make the seasonal peaks obvious at a glance.
- Before you run this, think about which months you expect to see the highest chocolate sales and why.
- Generate the data and create both plots by running this command:
python generate_data.py && python explore.py
Two plot windows will appear one after the other. Close each plot window to let the script continue to the next one.
After all plots close, check the terminal output. You should see sales statistics including the peak and lowest months.
- Confirm the bar chart in outputs/monthly_pattern.png clearly shows peaks in February, April, October, and December.
Why those four months?
February captures the Valentine's Day rush. April catches Easter. October is Halloween. December is Christmas. These are the patterns your model must learn to be useful for demand planning.
A model that misses these spikes would leave a chocolate company under-stocked when demand is highest.
Not seeing any plots?
If the script appears to hang, the plot window may have opened behind VS Code. Check your taskbar for a Python or matplotlib window.
If you see a FileNotFoundError on data/chocolate_sales.csv, make sure generate_data.py ran successfully first. The && operator only runs explore.py if generate_data.py succeeds.
Help me debug why my explore.py plots are not appearing
You now have realistic chocolate sales data with clear seasonal patterns. Next up, you will build a model to predict these sales and watch it struggle without the right features.
Build a Naive Model and Watch It Fail
Your monthly bar chart from the last step clearly showed those seasonal spikes. Now the question is: can a model learn to predict them?
In this step, you will train a simple model with only basic features and test whether it can capture the seasonal patterns in your chocolate sales data.
In this step, get ready to:
- Create a naive training script with minimal features and a temporal train/test split.
- Run the model and evaluate its predictions against actual sales.
Create the naive training script
You will give the model only two pieces of information: which month it is and how many days have passed since the start of the dataset. These are the simplest possible features.
- Create a new file called train_naive.py in your chocolate-forecast folder.
- Add the imports, data loading, feature creation, and temporal split by pasting this code:
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, r2_score
import matplotlib.pyplot as plt
from pathlib import Path
def train_naive_model():
"""Train a naive model with minimal features to show the limitation."""
df = pd.read_csv('data/chocolate_sales.csv', parse_dates=['date'])
# Minimal features: just the numeric date and month
df['days_since_start'] = (df['date'] - df['date'].min()).dt.days
df['month'] = df['date'].dt.month
features = ['days_since_start', 'month']
X = df[features]
y = df['sales']
# Temporal split: train on first 83%, test on last 17% (~6 months)
split_idx = int(len(df) * 0.83)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]
dates_test = df['date'][split_idx:]
# Train simple linear regression
model = LinearRegression()
model.fit(X_train, y_train)
predictions = model.predict(X_test)
What does this code do?
- days_since_start converts dates into plain numbers (0, 1, 2...) so the model can see an overall trend.
- month gives the model the month number (1-12) as its only seasonal signal.
- The temporal split trains on the first 83% of days and tests on the last 17%. This respects time order, unlike a random split that would leak future data into training.
- LinearRegression draws a straight line through the data. It cannot curve to match sharp holiday spikes.
Why a temporal split instead of random?
Sales data has order. If you randomly shuffled days into training and testing sets, the model could peek at December 2023 sales while predicting November 2023. A temporal split forces the model to only learn from the past, just like a real forecast.
- Add the evaluation, plotting, and main block below the code you just pasted:
# Evaluate
mae = mean_absolute_error(y_test, predictions)
r2 = r2_score(y_test, predictions)
print("=== Naive Model Results ===")
print(f"Features used: {features}")
print(f"Mean Absolute Error: {mae:.1f} units")
print(f"R2 Score: {r2:.3f}")
print(f"\nThe model is off by ~{mae:.0f} units on average!")
print("It cannot capture the seasonal spikes.")
# Plot predictions vs actual
Path('outputs').mkdir(exist_ok=True)
plt.figure(figsize=(14, 5))
plt.plot(dates_test.values, y_test.values, label='Actual', linewidth=0.8)
plt.plot(dates_test.values, predictions, label='Predicted (Naive)',
linewidth=0.8, color='red', linestyle='--')
plt.title(f'Naive Model: Predictions vs Actual (MAE={mae:.0f}, R2={r2:.2f})')
plt.xlabel('Date')
plt.ylabel('Units Sold')
plt.legend()
plt.tight_layout()
plt.savefig('outputs/naive_predictions.png', dpi=100)
plt.show()
print("\nPlot saved to outputs/naive_predictions.png")
print("\nNotice how the model misses Valentine's Day, Halloween, and Christmas peaks!")
if __name__ == '__main__':
train_naive_model()
What does this code do?
- mean_absolute_error (MAE) measures how many units off the predictions are on average. Lower is better.
- r2_score (R2) measures how well the model explains the variance in sales. 1.0 is perfect, 0.0 means no better than guessing the average.
- The plot overlays actual sales (blue) with predictions (red dashed) so you can visually see where the model fails.
- Save train_naive.py.
- Confirm the file exists by running:
ls train_naive.py
You should see train_naive.py listed in the output.
File not showing up?
- Make sure you are in the chocolate-forecast directory. Run pwd to check.
- Confirm you saved the file in VS Code (the dot on the tab disappears once saved).
Why can't I find my train_naive.py file?
✔️ Awesome, I've got everything!
Great. Make sure you have saved train_naive.py before moving on.
ⓧ I'd like to double check the full code
Your file has two sections that you built above. Scroll up and confirm both are present in this order:
- Chunk 1: Imports, the train_naive_model() function definition, feature creation (days_since_start and month), temporal split, and LinearRegression training through model.predict(X_test).
- Chunk 2: Evaluation metrics, print statements, the matplotlib plot saving to outputs/naive_predictions.png, and the if __name__ == '__main__' block at the bottom.
Run the model and observe the failure
Your model has been trained on two features. Before you run it, think about what you expect to happen.
Do you think the model will track those Valentine's Day and Christmas spikes you saw in the bar chart? Or will it draw something flatter?
- Run the naive model by executing this command:
python train_naive.py
You will see an MAE of 60+ units and an R2 below 0.5. The model is off by more than 60 units on average.
Open outputs/naive_predictions.png. The red dashed prediction line is nearly flat while the blue actual sales line spikes dramatically around each holiday. The model completely missed the seasonal pattern.
Why did the model fail?
Linear regression with only days_since_start and month can draw a trend line and shift slightly by month. But it has no way to know that February 14th is special, or that sales ramp up for two weeks before Christmas.
This is exactly the gap that feature engineering solves. You need to give the model the right information in the right form: holiday proximity, cyclical patterns, and recent sales history.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is activated. Your terminal prompt should show (venv) at the beginning.
- If it is not showing, run source venv/bin/activate and try again.
- If the error mentions sklearn specifically, re-run pip install -r requirements.txt to reinstall dependencies.
Why is my train_naive.py throwing an import error?
This failure is intentional and it proves something important. A model is only as good as the features you give it. In the next step, you will engineer smart features that give the model the seasonal awareness it needs to actually track those holiday spikes.
Engineer Features That Capture Seasonality
Your naive model missed every holiday spike because it had no way to "know" that Valentine's Day is approaching. It only had two features: a date counter and a month number.
In this step, you will build a dedicated features module that gives the model the right information in the right form. Holiday proximity curves, cyclical month encoding, and lag features from recent sales history turn a clueless model into one that anticipates seasonal demand.
In this step, get ready to:
- Create a reusable features.py module with four feature engineering functions.
- Create train_model.py to train a Random Forest model on the engineered features.
- Compare the improved model's accuracy against the naive model's failure.
Create the features module
Feature engineering is where you translate domain knowledge into numbers a model can learn from. You will create a separate module so these functions are reusable across different models or prediction tasks.
- Create a new file called features.py in your chocolate-forecast folder.
- Add the imports and the first feature function by pasting this code:
import pandas as pd
import numpy as np
def create_date_features(df):
"""Extract date-based features from the date column."""
df = df.copy()
df['day_of_week'] = df['date'].dt.dayofweek
df['day_of_year'] = df['date'].dt.dayofyear
df['month'] = df['date'].dt.month
df['quarter'] = df['date'].dt.quarter
df['week_of_year'] = df['date'].dt.isocalendar().week.astype(int)
df['is_weekend'] = (df['date'].dt.dayofweek >= 5).astype(int)
return df
What does this code do?
create_date_features() extracts calendar attributes from each date: day of week, quarter, week number, and a weekend flag. These capture weekly and quarterly sales patterns the model can learn from.
- Below create_date_features, add the seasonal features function:
def create_seasonal_features(df):
"""Create features for chocolate-specific seasonal peaks."""
df = df.copy()
day_of_year = df['date'].dt.dayofyear
# Proximity to key chocolate holidays (Gaussian curves)
df['valentines_proximity'] = np.exp(-0.5 * ((day_of_year - 45) / 7) ** 2)
df['easter_proximity'] = np.exp(-0.5 * ((day_of_year - 100) / 10) ** 2)
df['halloween_proximity'] = np.exp(-0.5 * ((day_of_year - 304) / 8) ** 2)
df['christmas_proximity'] = np.exp(-0.5 * ((day_of_year - 359) / 12) ** 2)
# Cyclical encoding of month (captures circular nature of seasons)
df['month_sin'] = np.sin(2 * np.pi * df['date'].dt.month / 12)
df['month_cos'] = np.cos(2 * np.pi * df['date'].dt.month / 12)
return df
What does this code do?
create_seasonal_features() is the key insight. Each Gaussian curve peaks at 1.0 on the holiday and smoothly decays to 0 as you move away. This tells the model "Valentine's Day is 3 days away" as a continuous signal, not just "it's February."
The sin/cos encoding captures the circular nature of months so December and January are close together, not 11 apart.
- Below create_seasonal_features, add the lag features and the pipeline orchestrator function:
def create_lag_features(df, target_col='sales', lags=None):
"""Create lag and rolling window features for time series."""
df = df.copy()
if lags is None:
lags = [7, 14, 28]
for lag in lags:
df[f'sales_lag_{lag}'] = df[target_col].shift(lag)
# Rolling averages (shifted by 1 to avoid data leakage)
df['sales_rolling_7'] = df[target_col].shift(1).rolling(window=7).mean()
df['sales_rolling_30'] = df[target_col].shift(1).rolling(window=30).mean()
return df
def prepare_features(df, target_col='sales', include_lags=True):
"""Full feature engineering pipeline. Reusable entry point."""
df = create_date_features(df)
df = create_seasonal_features(df)
if include_lags:
df = create_lag_features(df, target_col=target_col)
# Drop rows with NaN from lag features
df = df.dropna()
# Define feature columns (exclude date and target)
exclude_cols = ['date', target_col]
feature_cols = [col for col in df.columns if col not in exclude_cols]
return df, feature_cols
What does this code do?
create_lag_features() gives the model memory. The 7-day, 14-day, and 28-day lags let it see recent sales history. Rolling averages smooth out daily noise so the model can detect trends. The shift(1) prevents data leakage by ensuring you never peek at today's sales when predicting today.
prepare_features() chains all three functions together into a single reusable entry point. This is the function you swap out when adapting the pipeline for a different prediction domain.
- Save features.py.
- Verify the module loads correctly by running this command:
python -c "from features import prepare_features; print('Features module OK')"
- You should see Features module OK printed with no errors.
Seeing an ImportError?
- Make sure features.py is saved in the same chocolate-forecast directory where you are running the command.
- Check for typos in the function name prepare_features at the bottom of the file.
- Confirm your virtual environment is still activated (you should see (venv) in your terminal prompt).
help me debug my features.py ImportError
Create the improved training script
Now you will build a training script that imports your features module and trains a Random Forest model on the full set of engineered features.
- Create a new file called train_model.py in your chocolate-forecast folder.
- Add the imports, data loading, feature engineering, and temporal split by pasting this code:
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, r2_score
import matplotlib.pyplot as plt
from pathlib import Path
from features import prepare_features
def train_improved_model():
"""Train model with engineered features and compare to naive."""
df = pd.read_csv('data/chocolate_sales.csv', parse_dates=['date'])
# Apply feature engineering
df_featured, feature_cols = prepare_features(df)
X = df_featured[feature_cols]
y = df_featured['sales']
dates = df_featured['date']
# Temporal split
split_idx = int(len(df_featured) * 0.83)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]
dates_test = dates[split_idx:]
What does this code do?
The from features import prepare_features line pulls in your entire feature engineering pipeline from the module you just built.
The temporal split (83% train, 17% test) respects time ordering. Random splits would leak future data into training, which inflates accuracy and hides real-world performance.
- Below the temporal split, add the model training, evaluation, and feature importance logic:
# Train Random Forest
model = RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1)
model.fit(X_train, y_train)
predictions = model.predict(X_test)
# Evaluate
mae = mean_absolute_error(y_test, predictions)
r2 = r2_score(y_test, predictions)
print("=== Improved Model Results ===")
print(f"Features used: {len(feature_cols)} engineered features")
print(f"Mean Absolute Error: {mae:.1f} units")
print(f"R2 Score: {r2:.3f}")
print(f"\nCompare to naive model (~60+ MAE, ~0.3 R2)")
print(f"Improvement: captures seasonal spikes!")
# Feature importance
importance = pd.Series(model.feature_importances_, index=feature_cols)
top_features = importance.nlargest(10)
print(f"\nTop 10 Most Important Features:")
for feat, imp in top_features.items():
print(f" {feat}: {imp:.3f}")
What does this code do?
RandomForestRegressor trains 100 decision trees, each on a random subset of data and features. Their combined predictions average out individual tree errors. The n_jobs=-1 parameter uses all available CPU cores for parallel training.
The feature_importances_ attribute tells you which features the model relied on most. This is the payoff of modular feature engineering: you can see exactly which signals drive predictions.
- Below the feature importance loop, add the visualization code and the main block:
# Plot predictions vs actual
Path('outputs').mkdir(exist_ok=True)
plt.figure(figsize=(14, 5))
plt.plot(dates_test.values, y_test.values, label='Actual', linewidth=0.8)
plt.plot(dates_test.values, predictions, label='Predicted (Random Forest)',
linewidth=0.8, color='green', linestyle='--')
plt.title(f'Improved Model: Predictions vs Actual (MAE={mae:.0f}, R2={r2:.2f})')
plt.xlabel('Date')
plt.ylabel('Units Sold')
plt.legend()
plt.tight_layout()
plt.savefig('outputs/improved_predictions.png', dpi=100)
plt.show()
# Feature importance plot
plt.figure(figsize=(10, 6))
top_features.sort_values().plot(kind='barh', color='chocolate')
plt.title('Top 10 Feature Importances')
plt.xlabel('Importance')
plt.tight_layout()
plt.savefig('outputs/feature_importance.png', dpi=100)
plt.show()
print("\nPlots saved to outputs/")
if __name__ == '__main__':
train_improved_model()
What does this code do?
The first plot overlays the Random Forest predictions (green) against actual sales so you can visually confirm it tracks the seasonal spikes.
The second plot is a horizontal bar chart of the top 10 features ranked by importance. This reveals which engineered signals the model found most useful for capturing demand patterns.
- Save train_model.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved both features.py and train_model.py.
ⓧ I'd like to double check the full code
Scroll up and confirm each chunk is present in order:
- features.py: imports and create_date_features() at the top, then create_seasonal_features(), then create_lag_features(), then prepare_features() at the bottom.
- train_model.py: imports and function definition with temporal split at the top, then training and evaluation logic, then the two plot sections and the if __name__ block at the bottom.
Run and compare to the naive model
Before you run this, think about what you expect to happen. The naive model had an MAE of 60+ units and could not track holiday peaks. Do you think the engineered features will cut that error in half? By more?
- Run the improved training script:
python train_model.py
You should see the MAE drop to below 20 units and the R2 score rise above 0.90. That is a dramatic improvement from the naive model's ~60 MAE and ~0.3 R2.
What do these metrics mean?
MAE (Mean Absolute Error) tells you the average prediction miss in units. Below 20 means the model is typically off by fewer than 20 chocolate units per day.
R2 above 0.90 means the model explains over 90% of the variation in daily sales. The feature importance chart reveals which signals matter most. Holiday proximity and lag features should dominate the top spots.
- Open outputs/improved_predictions.png. The green prediction line now tracks the seasonal spikes closely, unlike the flat red line from the naive model.
- Open outputs/feature_importance.png. This horizontal bar chart shows the top 10 features ranked by how much the model relied on them.
Seeing an ImportError or unexpected results?
- If you see ModuleNotFoundError: No module named 'features', make sure features.py is in the same folder as train_model.py.
- If the MAE is much higher than expected, check that prepare_features() in features.py calls all three sub-functions (create_date_features, create_seasonal_features, create_lag_features).
- If you see a plot but the data looks wrong, confirm data/chocolate_sales.csv still exists from Step 2.
help me debug my train_model.py results
Your feature engineering module transformed a clueless model into one that captures seasonal demand patterns. Next up, you will turn this trained model into a 90-day business forecast that a chocolate company could actually use for inventory planning.
Build the 90-Day Forecast Pipeline
Your improved model can evaluate past data with impressive accuracy. But a model that only scores historical data is a science experiment.
A model that generates future predictions is a business tool. In this step, you will turn your trained model into a 90-day demand forecast that a chocolate company could actually use to plan inventory and staffing.
In this step, get ready to:
- Create a forecast pipeline that trains on all available data and predicts 90 future days.
- Generate a combined history and forecast visualization plus a CSV of daily predictions.
Create the forecast script
Forecasting future values with lag features requires an iterative approach. Each day's prediction feeds back as a lag feature for the next day's prediction, building the forecast one step at a time.
- Create a new file called forecast_pipeline.py in your chocolate-forecast folder.
- Add the imports and the first part of the build_forecast_pipeline() function, which loads data, engineers features, and trains the model on all available data:
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, r2_score
import matplotlib.pyplot as plt
from pathlib import Path
from features import prepare_features
def build_forecast_pipeline():
"""Build a reusable forecasting pipeline and generate future predictions."""
# Load and prepare data
df = pd.read_csv('data/chocolate_sales.csv', parse_dates=['date'])
df_featured, feature_cols = prepare_features(df)
X = df_featured[feature_cols]
y = df_featured['sales']
# Train on all available data for final forecast
model = RandomForestRegressor(n_estimators=200, random_state=42, n_jobs=-1)
model.fit(X, y)
What does this code do?
Unlike train_model.py which held back 17% for testing, this script trains on ALL the data. When you are generating real future predictions, you want the model to learn from every available data point.
The n_estimators=200 uses 200 trees (up from 100 in the evaluation script) for extra robustness since this is the production forecast.
- Directly below the code you just added, paste the iterative forecasting logic that predicts 90 future days one at a time:
# Generate future dates (next 90 days)
last_date = df['date'].max()
future_dates = pd.date_range(
start=last_date + pd.Timedelta(days=1), periods=90, freq='D'
)
# Build future dataframe
future_df = pd.DataFrame({'date': future_dates, 'sales': np.nan})
full_df = pd.concat([df, future_df], ignore_index=True)
# Iterative prediction (each prediction feeds back as lag for the next)
for i in range(len(df), len(full_df)):
temp_df = full_df.iloc[:i + 1].copy()
temp_df, temp_features = prepare_features(temp_df, include_lags=True)
if len(temp_df) == 0:
continue
# Predict the last row
last_row = temp_df[temp_features].iloc[[-1]]
prediction = model.predict(last_row)[0]
full_df.loc[full_df.index[i], 'sales'] = max(round(prediction), 50)
# Extract forecast
forecast = full_df.iloc[len(df):].copy()
forecast['sales'] = forecast['sales'].astype(int)
Why predict one day at a time?
Your model uses lag features (sales from 7, 14, and 28 days ago). For future dates, those lag values do not exist yet. The iterative loop solves this by predicting day 1, writing that prediction into the dataframe, then using it as the lag input for day 2, and so on.
This is the standard approach for multi-step time series forecasting with lag features. It is slower than predicting all 90 days at once, but much more accurate.
- Below the forecast extraction code, add the visualization that plots recent history alongside the 90-day forecast:
# Visualize: last 6 months of history + 90-day forecast
Path('outputs').mkdir(exist_ok=True)
plt.figure(figsize=(14, 6))
recent = df.tail(180)
plt.plot(recent['date'], recent['sales'], label='Historical Sales',
linewidth=0.8, color='steelblue')
plt.plot(forecast['date'], forecast['sales'], label='90-Day Forecast',
linewidth=1.5, color='darkorange', linestyle='--')
plt.axvline(x=last_date, color='gray', linestyle=':', alpha=0.7,
label='Forecast Start')
plt.title('Chocolate Sales: 90-Day Demand Forecast')
plt.xlabel('Date')
plt.ylabel('Units Sold')
plt.legend()
plt.tight_layout()
plt.savefig('outputs/forecast_90day.png', dpi=100)
plt.show()
What does this visualization show?
The plot shows the last 180 days of real sales data in blue alongside your 90-day forecast in orange. A vertical gray line marks where history ends and the forecast begins. This makes it easy to visually confirm the forecast follows realistic seasonal patterns.
- Finally, add the CSV export, summary printout, and the main block that makes the script runnable:
# Save forecast to CSV
forecast[['date', 'sales']].to_csv('outputs/forecast_results.csv', index=False)
print("=== 90-Day Forecast Generated ===")
print(f"Forecast period: {forecast['date'].min().date()} to "
f"{forecast['date'].max().date()}")
print(f"Predicted avg daily sales: {forecast['sales'].mean():.0f} units")
print(f"Predicted peak day: {forecast.loc[forecast['sales'].idxmax(), 'date'].date()} "
f"({forecast['sales'].max()} units)")
print(f"\nForecast saved to outputs/forecast_results.csv")
print(f"Plot saved to outputs/forecast_90day.png")
print(f"\n--- Pipeline Architecture ---")
print(f"This pipeline is designed to be reusable:")
print(f" 1. Swap generate_data.py with real data loading")
print(f" 2. Modify features.py for your specific domain")
print(f" 3. Change the model in this script (try GradientBoosting, XGBoost)")
print(f" 4. Adjust forecast horizon (currently 90 days)")
if __name__ == '__main__':
build_forecast_pipeline()
What does this code do?
The CSV export saves the 90-day forecast as a file a business team could open in Excel or import into a planning tool. The print statements give you a quick summary in the terminal: the forecast date range, average predicted sales, and the single highest-demand day.
The architecture notes at the end remind you that this pipeline is modular. You can swap the data source, change the feature module, or try a different model without restructuring the whole script.
- Save forecast_pipeline.py.
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before running it in the next substep.
ⓧ I'd like to double check the full code
Scroll up through the four code chunks you pasted and confirm they appear in this order inside your build_forecast_pipeline() function:
- Imports and function definition with data loading and model training.
- Future date generation and the iterative prediction loop.
- Matplotlib visualization block saving to outputs/forecast_90day.png.
- CSV export, print summary, and the if __name__ == '__main__': block at the bottom.
Run and interpret the forecast
Your forecast script is ready. Before you run it, think about what you expect to see: should the next 90 days show a seasonal spike, or flat demand? It depends on which calendar period falls in the forecast window.
- Run the forecast pipeline by executing this command in your terminal:
python forecast_pipeline.py
The script takes a moment to run because it predicts each of the 90 days iteratively. When it finishes, your terminal displays the forecast period, average predicted daily sales, and the peak predicted day.
- Open outputs/forecast_90day.png to see the forecast plot. You should see the last 6 months of historical sales in blue, a vertical gray line marking the forecast boundary, and the 90-day forecast in dashed orange.
- Confirm that outputs/forecast_results.csv exists by running:
ls outputs/forecast_results.csv
You should see the file listed. This CSV contains 90 rows with columns date and sales, one row per forecasted day.
Why is this a business tool now?
The CSV output is something a real supply chain team could import into their planning system. If the forecast predicts a spike in 45 days, the company can order extra cocoa, schedule extra shifts, and stock retail shelves ahead of the rush.
Script taking too long or erroring out?
- If you see an ImportError for features, make sure your terminal is inside the chocolate-forecast folder where features.py lives.
- If you see a FileNotFoundError for the CSV, confirm you ran python generate_data.py in a previous step and that data/chocolate_sales.csv exists.
- The script can take 30-60 seconds because it calls prepare_features() 90 times in the loop. This is expected.
Why isn't my forecast pipeline running?
You now have a complete, end-to-end demand forecasting system. It generates data, engineers features, trains a model, and produces a 90-day business-ready forecast. Next up, you will take on a secret mission to extend this pipeline to multiple product lines.
Secret mission
Multi-Product Forecasting
A real chocolate company does not sell just one product. Can you extend your pipeline to forecast demand for three distinct product lines, each with its own seasonal personality? Prove your modular architecture actually scales.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The chocolate-forecast project folder on your Desktop (includes Python scripts, virtual environment, data, and output files).
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment further with the forecasting pipeline.
- Your project files, virtual environment, and output plots remain in ~/Desktop/chocolate-forecast.
- The virtual environment only uses disk space. It does not run any background processes.
- You can reactivate it anytime with source venv/bin/activate from inside the project folder.
Pause - I'll come back to this later
Deactivate the virtual environment to free up your terminal session. Your files stay on disk for when you return.
- If your virtual environment is still active, deactivate it by running:
deactivate
- Your terminal prompt returns to normal (the (venv) prefix disappears).
- When you come back, navigate to ~/Desktop/chocolate-forecast and run source venv/bin/activate to pick up where you left off.
Delete - I don't want to use this again
Remove all project files, the virtual environment, and generated outputs. This cannot be undone.
- If your virtual environment is still active, deactivate it first by running:
deactivate
- Delete the entire project folder by running:
rm -rf ~/Desktop/chocolate-forecast
- Confirm the folder is gone by running:
ls ~/Desktop/chocolate-forecast
- You should see a message indicating the directory does not exist. This confirms complete removal.
Nice Work!
Nice Work!
You just built a complete demand forecasting system for a chocolate company from scratch. From generating realistic seasonal data to engineering features that dramatically improved predictions, you now have a production-style ML pipeline you can adapt to any tabular prediction task.
You've learned how to:
- Build a modular ML pipeline with separate data generation, feature engineering, and model training modules designed to scale to other prediction domains.
- Experience firsthand why feature engineering matters by watching a naive model fail (MAE 60+) and then engineering seasonal proximity curves, cyclical encoding, and lag features that dropped the error dramatically (MAE below 20).
- Generate a business-ready 90-day demand forecast using iterative prediction with a Random Forest model, producing both a visualization and a CSV that a real company could act on.
- Secret Mission: Extended the pipeline to forecast multiple product lines independently, proving the modular architecture scales across products with distinct seasonal profiles.
Ready to quiz yourself?