Back to Projects
Time Series Project

Store Sales Time Series Forecasting

Download public Store Sales CSVs, build time-based features, create validation by date, train a baseline model, and produce a forecast submission.

Final deliverable: A time-series forecasting notebook with a date-based validation split and submission.csv file.

Dataset: Store Sales public CSV mirror

The notebook downloads a public mirror of the Ecuador store sales files. The task is to forecast unit sales for store/family/date combinations.

  • train.csv: date, store_nbr, family, sales, onpromotion
  • test.csv: future date/store/family rows requiring predictions
  • stores.csv, oil.csv, holidays_events.csv, transactions.csv: optional external/context tables
Step 1

Download Store Sales Data

Download the public Store Sales CSV files.

Your task

Run the public download cell and inspect the CSV files.

Try writing this cell
# Run this cell in Colab, Kaggle, or a local notebook.
# It downloads public dataset files directly, so you do not need Kaggle credentials.

from pathlib import Path
from urllib.request import urlretrieve
import zipfile

DATA_DIR = Path("data/store-sales-time-series-forecasting")
DATA_DIR.mkdir(parents=True, exist_ok=True)

def find_file(name):
    possible_roots = [DATA_DIR, Path("/kaggle/input")]
    for root in possible_roots:
        if root.exists():
            matches = list(root.rglob(name))
            if matches:
                return matches[0]
    raise FileNotFoundError(f"Could not find {name}. Run the dataset download cell first.")

files = {
    "train.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/train.csv",
    "test.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/test.csv",
    "sample_submission.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/sample_submission.csv",
    "stores.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/stores.csv",
    "oil.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/oil.csv",
    "holidays_events.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/holidays_events.csv",
    "transactions.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/transactions.csv"
}

for filename, url in files.items():
    target = DATA_DIR / filename
    if target.exists() and target.stat().st_size > 0:
        print(f"already exists: {target}")
        continue
    print(f"downloading {filename} ...")
    urlretrieve(url, target)
    print(f"saved: {target}")

for zip_path in DATA_DIR.glob("*.zip"):
    extract_dir = DATA_DIR / zip_path.stem
    extract_dir.mkdir(exist_ok=True)
    if any(extract_dir.iterdir()):
        print(f"already extracted: {extract_dir}")
        continue
    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(extract_dir)
    print(f"extracted: {zip_path} -> {extract_dir}")

print("Files:")
all_files = [path for path in sorted(DATA_DIR.rglob("*")) if path.is_file()]
for path in all_files[:40]:
    print(path)
if len(all_files) > 40:
    print(f"... {len(all_files) - 40} more files not shown")
Expected output
A file list containing train.csv, test.csv, stores.csv, oil.csv, holidays_events.csv, and transactions.csv.
Show reference solution
# Run this cell in Colab, Kaggle, or a local notebook.
# It downloads public dataset files directly, so you do not need Kaggle credentials.

from pathlib import Path
from urllib.request import urlretrieve
import zipfile

DATA_DIR = Path("data/store-sales-time-series-forecasting")
DATA_DIR.mkdir(parents=True, exist_ok=True)

def find_file(name):
    possible_roots = [DATA_DIR, Path("/kaggle/input")]
    for root in possible_roots:
        if root.exists():
            matches = list(root.rglob(name))
            if matches:
                return matches[0]
    raise FileNotFoundError(f"Could not find {name}. Run the dataset download cell first.")

files = {
    "train.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/train.csv",
    "test.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/test.csv",
    "sample_submission.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/sample_submission.csv",
    "stores.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/stores.csv",
    "oil.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/oil.csv",
    "holidays_events.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/holidays_events.csv",
    "transactions.csv": "https://huggingface.co/datasets/t4tiana/store-sales-time-series-forecasting/resolve/main/transactions.csv"
}

for filename, url in files.items():
    target = DATA_DIR / filename
    if target.exists() and target.stat().st_size > 0:
        print(f"already exists: {target}")
        continue
    print(f"downloading {filename} ...")
    urlretrieve(url, target)
    print(f"saved: {target}")

for zip_path in DATA_DIR.glob("*.zip"):
    extract_dir = DATA_DIR / zip_path.stem
    extract_dir.mkdir(exist_ok=True)
    if any(extract_dir.iterdir()):
        print(f"already extracted: {extract_dir}")
        continue
    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(extract_dir)
    print(f"extracted: {zip_path} -> {extract_dir}")

print("Files:")
all_files = [path for path in sorted(DATA_DIR.rglob("*")) if path.is_file()]
for path in all_files[:40]:
    print(path)
if len(all_files) > 40:
    print(f"... {len(all_files) - 40} more files not shown")
Why this step matters

Time-series projects often have multiple related tables. Listing files first helps learners see what is available before modeling.

Common mistake
Only loading train.csv and ignoring useful calendar/store/context files too early.
Step 2

Load and Inspect the Time Series

Read train.csv with parsed dates and inspect date range, stores, families, and sales distribution.

Your task

Load train.csv, parse date, and print basic dataset shape and ranges.

Try writing this cell
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Find train.csv and load it with parsed dates

# Inspect shape, date range, stores, families
Expected output
Shape, date range, number of stores/families, and sales summary.
Show reference solution
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

train_path = find_file("train.csv")
df = pd.read_csv(train_path, parse_dates=["date"])

print(df.shape)
print(df["date"].min(), df["date"].max())
print("stores:", df["store_nbr"].nunique())
print("families:", df["family"].nunique())
display(df.head())
display(df["sales"].describe())
Why this step matters

Forecasting starts with understanding the time axis. Random train/test splits are usually wrong for time series.

Common mistake
Using train_test_split randomly, which leaks future patterns into training.
Step 3

Plot Aggregate Sales Over Time

See trend, seasonality, and unusual spikes at the daily aggregate level.

Your task

Group sales by date and plot total sales.

Try writing this cell
# Aggregate sales by date

# Plot daily total sales
Expected output
A line chart of total daily sales.
Show reference solution
daily_sales = df.groupby("date", as_index=False)["sales"].sum()

plt.figure(figsize=(12, 4))
plt.plot(daily_sales["date"], daily_sales["sales"])
plt.title("Total Daily Sales")
plt.xlabel("Date")
plt.ylabel("Sales")
plt.show()
Why this step matters

A simple aggregate plot reveals seasonality, holidays, missing periods, and trend changes before feature engineering.

Common mistake
Looking only at row-level head() and never plotting the series.
Step 4

Create Date Features

Add calendar features that a tree model can use.

Your task

Create day_of_week, month, year, day_of_month, weekofyear, and weekend features for train and test.

Try writing this cell
# Define a function add_date_features(data)

# Apply it to train data
Expected output
New calendar feature columns added to the training data.
Show reference solution
def add_date_features(data):
    data = data.copy()
    data["day_of_week"] = data["date"].dt.dayofweek
    data["month"] = data["date"].dt.month
    data["year"] = data["date"].dt.year
    data["day_of_month"] = data["date"].dt.day
    data["weekofyear"] = data["date"].dt.isocalendar().week.astype(int)
    data["is_weekend"] = data["day_of_week"].isin([5, 6]).astype(int)
    return data

df_features = add_date_features(df)
display(df_features[["date", "day_of_week", "month", "weekofyear", "is_weekend"]].head())
Why this step matters

Tree models do not understand dates directly. Calendar features expose weekly and yearly patterns.

Common mistake
Converting date to an arbitrary integer and expecting the model to learn calendar structure automatically.
Step 5

Create a Date-Based Validation Split

Train on earlier dates and validate on the most recent period.

Your task

Use the last 30 training days as validation and all earlier rows as training.

Try writing this cell
# Find cutoff date

# Split train_part and val_part by date
Expected output
Training dates before validation dates, with no future rows in training.
Show reference solution
cutoff = df_features["date"].max() - pd.Timedelta(days=30)

train_part = df_features[df_features["date"] <= cutoff].copy()
val_part = df_features[df_features["date"] > cutoff].copy()

print(train_part["date"].min(), train_part["date"].max())
print(val_part["date"].min(), val_part["date"].max())
print(train_part.shape, val_part.shape)
Why this step matters

Time-series validation must mimic the future. The model should not train on rows after the validation period.

Common mistake
Randomly splitting rows and getting an overly optimistic validation score.
Step 6

Train a Baseline Forecasting Model

Train a model using store, family, promotion, and date features.

Your task

Use ordinal encoding, store/family aggregate features, log-target training, and a fast gradient-boosting baseline. Evaluate RMSLE on a recent validation window.

Try writing this cell
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.metrics import mean_squared_log_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OrdinalEncoder

# Build features

# Add store/family historical aggregate features without using validation rows

# Train on log1p(sales) and evaluate RMSLE
Expected output
A training-row count and validation RMSLE value.
Show reference solution
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.metrics import mean_squared_log_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OrdinalEncoder

def add_group_features(base, source):
    base = base.copy()
    keys = ["store_nbr", "family"]
    source = source.copy()

    sf_mean = source.groupby(keys)["sales"].mean().rename("sf_mean").reset_index()
    sf_dow_mean = source.groupby(keys + ["day_of_week"])["sales"].mean().rename("sf_dow_mean").reset_index()
    sf_month_mean = source.groupby(keys + ["month"])["sales"].mean().rename("sf_month_mean").reset_index()

    base = base.merge(sf_mean, on=keys, how="left")
    base = base.merge(sf_dow_mean, on=keys + ["day_of_week"], how="left")
    base = base.merge(sf_month_mean, on=keys + ["month"], how="left")

    global_mean = float(source["sales"].mean())
    for col in ["sf_mean", "sf_dow_mean", "sf_month_mean"]:
        base[col] = base[col].fillna(global_mean)
    return base

features = [
    "store_nbr", "family", "onpromotion", "day_of_week", "month", "year",
    "day_of_month", "weekofyear", "is_weekend", "sf_mean", "sf_dow_mean", "sf_month_mean",
]
categorical = ["family"]
numeric = [col for col in features if col not in categorical]

preprocess = ColumnTransformer([
    ("cat", OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1), categorical),
    ("num", "passthrough", numeric),
])

recent_cutoff = train_part["date"].max() - pd.Timedelta(days=365)
recent_train = train_part[train_part["date"] >= recent_cutoff].copy()
recent_train_model = add_group_features(recent_train, recent_train)
val_model = add_group_features(val_part, recent_train)
print("training rows used:", recent_train.shape[0])

model = Pipeline([
    ("preprocess", preprocess),
    ("regressor", HistGradientBoostingRegressor(
        max_iter=250,
        learning_rate=0.06,
        l2_regularization=0.05,
        random_state=42,
    )),
])

model.fit(recent_train_model[features], np.log1p(recent_train_model["sales"]))
val_preds = np.expm1(model.predict(val_model[features])).clip(0)

rmsle = np.sqrt(mean_squared_log_error(val_part["sales"], val_preds))
print("RMSLE:", rmsle)
Why this step matters

The aggregate features tell the model what each store/family usually sells overall, on that weekday, and in that month. Training on log sales matches RMSLE better than raw sales.

Common mistake
Computing historical averages from train plus validation together, which leaks future validation information.
Step 7

Create Forecast Submission

Train on all training data, predict test rows, and save id/sales submission.

Your task

Load test.csv, add date features, predict sales, clip negatives, and write submission.csv.

Try writing this cell
# Load test.csv

# Add date features

# Fit on full train and predict test

# Save submission.csv
Expected output
submission.csv with id and sales columns.
Show reference solution
test_df = pd.read_csv(find_file("test.csv"), parse_dates=["date"])
test_features = add_date_features(test_df)

recent_full_cutoff = df_features["date"].max() - pd.Timedelta(days=365)
recent_full = df_features[df_features["date"] >= recent_full_cutoff].copy()
recent_full_model = add_group_features(recent_full, recent_full)
test_model = add_group_features(test_features, recent_full)

model.fit(recent_full_model[features], np.log1p(recent_full_model["sales"]))
test_preds = np.expm1(model.predict(test_model[features])).clip(0)

submission = pd.DataFrame({
    "id": test_df["id"],
    "sales": test_preds,
})
submission.to_csv("submission.csv", index=False)
display(submission.head())
Why this step matters

The final project artifact is a reproducible forecast file. The same feature pipeline must work on train, validation, and test.

Common mistake
Creating date features on train but forgetting to apply them to test.