Baseline drill

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.model_selection import TimeSeriesSplit

np.random.seed(42)

# =========================================================
# 0) Create a tiny CSV (so the drill is self-contained)
# =========================================================
n = 420
dates = pd.date_range("2024-01-01", periods=n, freq="B")
t = np.arange(n)

# Synthetic "price" with drift + seasonality + noise
price = 100 * np.exp(0.0005 * t + 0.02 * np.sin(2*np.pi*t/20) + 0.01*np.random.randn(n))
volume = 1_000 + 150*np.sin(2*np.pi*t/5) + 50*np.random.randn(n)

raw = pd.DataFrame({"date": dates, "price": price, "volume": volume})

# Inject common issues: missing values + duplicate timestamps + unsorted rows
raw.loc[50, "price"] = np.nan
raw.loc[120, "volume"] = np.nan
raw = pd.concat([raw, raw.iloc[[200]]], ignore_index=True)  # duplicate one row
raw = raw.sample(frac=1.0, random_state=1).reset_index(drop=True)  # shuffle rows

csv_path = "mini_drill_timeseries.csv"
raw.to_csv(csv_path, index=False)

# =========================================================
# 1) Read CSV
# =========================================================
df = pd.read_csv(csv_path, parse_dates=["date"])

# =========================================================
# 2) Clean
#    - sort time
#    - drop duplicate timestamps
#    - handle missing values (simple policy for drill)
# =========================================================
df = df.sort_values("date")
df = df.drop_duplicates(subset=["date"], keep="last")

# Simple missing policy:
# - forward fill price (common for prices, but must be documented)
# - fill volume with rolling median (robust-ish)
df["price"] = df["price"].ffill()
df["volume"] = df["volume"].fillna(df["volume"].rolling(10, min_periods=1).median())

# Invariants
assert df["date"].is_monotonic_increasing
assert df["date"].is_unique

# =========================================================
# 3) Features
#    Target: next-day log return r_{t+1}
# =========================================================
df["log_price"] = np.log(df["price"])
df["r1"] = df["log_price"].diff()                   # r_t
df["y"] = df["r1"].shift(-1)                         # y_t = r_{t+1}

# Lag features available at time t (causal)
df["lag1"] = df["r1"].shift(1)
df["lag2"] = df["r1"].shift(2)
df["vol_20"] = df["r1"].rolling(20).std()            # trailing vol proxy
df["dow"] = df["date"].dt.dayofweek                  # seasonality feature

feat_cols = ["lag1", "lag2", "vol_20", "dow"]

# Drop rows created by shifts/rolling
df_feat = df.dropna(subset=feat_cols + ["y"]).reset_index(drop=True)

# =========================================================
# 4) TimeSeriesSplit
# =========================================================
X = df_feat[feat_cols].values
y = df_feat["y"].values

tscv = TimeSeriesSplit(n_splits=5)

# =========================================================
# 5) Baseline model
#    Baseline: "predict next return equals last return"
#    yhat_t = r_t  -> since y_t is r_{t+1}, baseline uses current r_t (which is df_feat["r1"])
# =========================================================
def rmse(a, b):
    return float(np.sqrt(np.mean((a - b) ** 2)))

fold_rows = []
all_preds = np.full_like(y, np.nan, dtype=float)

for k, (train_idx, test_idx) in enumerate(tscv.split(X), start=1):
    # baseline prediction for test: yhat = r1 at time t
    yhat = df_feat.loc[test_idx, "r1"].values
    ytrue = y[test_idx]

    fold_rmse = rmse(ytrue, yhat)
    fold_rows.append({"fold": k, "n_test": len(test_idx), "rmse": fold_rmse})

    all_preds[test_idx] = yhat

fold_df = pd.DataFrame(fold_rows)

# =========================================================
# 6) Metric summary + quick plot
# =========================================================
print("Fold RMSE:")
print(fold_df.to_string(index=False))

print("\nAverage RMSE:", float(fold_df["rmse"].mean()))
print("Worst-fold RMSE:", float(fold_df["rmse"].max()))

# Plot predicted vs realised (subset)
plt.figure(figsize=(8, 3.8))
mask = ~np.isnan(all_preds)
plt.plot(df_feat.loc[mask, "date"].values, y[mask], label="Realised y = r_{t+1}")
plt.plot(df_feat.loc[mask, "date"].values, all_preds[mask], label="Baseline yhat = r_t")
plt.axhline(0, linestyle="--")
plt.title("TimeSeriesSplit baseline: next-day log return")
plt.xlabel("date")
plt.ylabel("log return")
plt.legend()
plt.tight_layout()
plt.show()

# =========================================================
# 7) Explain result (print-ready)
# =========================================================
explain = f"""
Explanation (what you should say):

- Target y_t is next-day log return r_(t+1).
- Baseline predicts yhat_t = r_t (persistence in returns).
- TimeSeriesSplit preserves time order, so no leakage from future data.
- RMSE is near the unconditional volatility of returns when predictability is weak.
- If folds vary a lot, that suggests regime changes or volatility shifts.
- This baseline is the minimum bar: any real model must beat it out-of-sample, fold by fold.
""".strip()

print("\n" + explain)

Extended two-baseline drill


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.model_selection import TimeSeriesSplit

np.random.seed(42)

# =========================================================
# 0) Create a tiny CSV (self-contained drill)
# =========================================================
n = 420
dates = pd.date_range("2024-01-01", periods=n, freq="B")
t = np.arange(n)

price = 100 * np.exp(0.0005 * t + 0.02 * np.sin(2*np.pi*t/20) + 0.01*np.random.randn(n))
volume = 1_000 + 150*np.sin(2*np.pi*t/5) + 50*np.random.randn(n)

raw = pd.DataFrame({"date": dates, "price": price, "volume": volume})

# Inject issues: missing values + duplicate timestamp + unsorted rows
raw.loc[50, "price"] = np.nan
raw.loc[120, "volume"] = np.nan
raw = pd.concat([raw, raw.iloc[[200]]], ignore_index=True)        # duplicate one row
raw = raw.sample(frac=1.0, random_state=1).reset_index(drop=True) # shuffle

csv_path = "mini_drill_timeseries.csv"
raw.to_csv(csv_path, index=False)

# =========================================================
# 1) Read CSV
# =========================================================
df = pd.read_csv(csv_path, parse_dates=["date"])

# =========================================================
# 2) Clean
# =========================================================
df = df.sort_values("date")
df = df.drop_duplicates(subset=["date"], keep="last")

# Missing policy (documented for drill)
df["price"] = df["price"].ffill()
df["volume"] = df["volume"].fillna(df["volume"].rolling(10, min_periods=1).median())

# Invariants
assert df["date"].is_monotonic_increasing
assert df["date"].is_unique

# =========================================================
# 3) Features and target
#    Target: next-day log return y_t = r_{t+1}
# =========================================================
df["log_price"] = np.log(df["price"])
df["r1"] = df["log_price"].diff()          # r_t
df["y"] = df["r1"].shift(-1)               # y_t = r_{t+1}

# Causal features at time t
df["lag1"] = df["r1"].shift(1)
df["lag2"] = df["r1"].shift(2)
df["vol_20"] = df["r1"].rolling(20).std()  # trailing volatility proxy
df["dow"] = df["date"].dt.dayofweek        # day-of-week

feat_cols = ["lag1", "lag2", "vol_20", "dow"]

df_feat = df.dropna(subset=feat_cols + ["y"]).reset_index(drop=True)

X = df_feat[feat_cols].values
y = df_feat["y"].values

# =========================================================
# 4) TimeSeriesSplit
# =========================================================
tscv = TimeSeriesSplit(n_splits=5)

# =========================================================
# 5) Two baselines
#    (A) Last value baseline: yhat_t = r_t
#    (B) Seasonal naive baseline (weekly): yhat_t = r_{t-5}
# =========================================================
def rmse(a, b):
    return float(np.sqrt(np.mean((a - b) ** 2)))

def mae(a, b):
    return float(np.mean(np.abs(a - b)))

fold_rows = []
pred_last = np.full_like(y, np.nan, dtype=float)
pred_seas = np.full_like(y, np.nan, dtype=float)

for k, (train_idx, test_idx) in enumerate(tscv.split(X), start=1):
    # Baseline A: predict next return equals current return
    yhat_last = df_feat.loc[test_idx, "r1"].values

    # Baseline B: seasonal naive using weekly lag (5 business days)
    # Need r_{t-5} available at time t => shift r1 by 5
    yhat_seas = df_feat.loc[test_idx, "r1"].shift(5).values

    # For early rows in fold, seasonal may be NaN; drop those for fair scoring
    ytrue = y[test_idx]

    m_last_rmse = rmse(ytrue, yhat_last)
    m_last_mae  = mae(ytrue, yhat_last)

    mask = ~np.isnan(yhat_seas)
    m_seas_rmse = rmse(ytrue[mask], yhat_seas[mask])
    m_seas_mae  = mae(ytrue[mask], yhat_seas[mask])

    fold_rows.append({
        "fold": k,
        "n_test": len(test_idx),
        "rmse_last": m_last_rmse,
        "mae_last": m_last_mae,
        "rmse_seasonal5": m_seas_rmse,
        "mae_seasonal5": m_seas_mae,
    })

    pred_last[test_idx] = yhat_last
    pred_seas[test_idx] = yhat_seas

fold_df = pd.DataFrame(fold_rows)

# =========================================================
# 6) One-slide summary table (print + optional plot)
# =========================================================
summary = pd.DataFrame({
    "baseline": ["Last value (yhat=r_t)", "Seasonal naive (yhat=r_{t-5})"],
    "RMSE_mean": [float(fold_df["rmse_last"].mean()), float(fold_df["rmse_seasonal5"].mean())],
    "RMSE_worst_fold": [float(fold_df["rmse_last"].max()), float(fold_df["rmse_seasonal5"].max())],
    "MAE_mean": [float(fold_df["mae_last"].mean()), float(fold_df["mae_seasonal5"].mean())],
})

print("=== Fold metrics (TimeSeriesSplit) ===")
print(fold_df.to_string(index=False))

print("\n=== One-slide summary ===")
print(summary.to_string(index=False))

# Plot: realised vs both baselines (subset)
mask = ~np.isnan(pred_last)
mask2 = mask & ~np.isnan(pred_seas)

plt.figure(figsize=(8, 3.8))
plt.plot(df_feat.loc[mask, "date"].values, y[mask], label="Realised y = r_{t+1}")
plt.plot(df_feat.loc[mask, "date"].values, pred_last[mask], label="Baseline A: yhat = r_t")
plt.plot(df_feat.loc[mask2, "date"].values, pred_seas[mask2], label="Baseline B: yhat = r_{t-5}")
plt.axhline(0, linestyle="--")
plt.title("Two baselines under TimeSeriesSplit (next-day log return)")
plt.xlabel("date")
plt.ylabel("log return")
plt.legend()
plt.tight_layout()
plt.show()

# =========================================================
# 7) Explain result (slide-ready bullets)
# =========================================================
explain = """
Slide-ready explanation:

- Target is next-day log return y_t = r_{t+1}.
- We use TimeSeriesSplit to preserve time order and avoid leakage.
- Baseline A (last value) tests short-memory persistence in returns.
- Baseline B (seasonal naive, lag 5) tests weekly structure.
- If Baseline B wins, the series contains weekly seasonality in returns.
- If both are similar, predictability is weak and RMSE mostly reflects volatility.
- Any real model must beat the stronger baseline out-of-sample, fold by fold, not only on average.
""".strip()

print("\n" + explain)