# scripts/prepare_moirai_data.py import pandas as pd from pathlib import Path from sklearn.model_selection import train_test_split from sklearn.preprocessing import RobustScaler import joblib RAW_PATH = Path("data/processed/mandi_feature_engineered.csv") OUT_PATH = Path("data/processed/mandi_moirai_scaled.parquet") SCALER_DIR = Path("models") SCALER_DIR.mkdir(parents=True, exist_ok=True) DYN_SCALER_PATH = SCALER_DIR / "moirai_dyn_scaler.pkl" TARGET_SCALER_PATH = SCALER_DIR / "moirai_target_scaler.pkl" def main(): print(f"Loading engineered data from {RAW_PATH}...") df = pd.read_csv(RAW_PATH) # ensure datetime df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) # ---------- choose dynamic numeric cols (exclude IDs, series_id, target ---------- dynamic_num_cols = [ # core prices & arrivals "Arrivals", "MinPrice", "MaxPrice", # weather "temp_avg", "temp_max", "temp_min", "rainfall", "humidity", "solar_radiation", "wind_speed", # time encodings (optional but fine to scale) "day", "month", "year", "day_of_week", "week_of_year", "day_of_year", "is_weekend", "sin1", "cos1", "sin2", "cos2", # price lags & stats "modal_lag_1", "modal_lag_3", "modal_lag_7", "modal_lag_14", "modal_lag_30", "rolling_mean_3", "rolling_std_3", "rolling_mean_7", "rolling_std_7", "rolling_mean_14", "rolling_std_14", "rolling_mean_30", "rolling_std_30", "price_range", "price_spread", "norm_spread", "volatility_7", "volatility_30", "zscore_7", "momentum_7", "momentum_14", # arrivals features "arrivals_lag_1", "arrivals_lag_3", "arrivals_lag_7", "arrivals_lag_14", "arrivals_avg_7", "arrivals_avg_30", "arrival_change_7", # climate aggregates "temp_range", "rain_intensity", "temp_avg_30", "rain_avg_30", "temp_anomaly", "rain_anomaly", # geo encodings "lat_sin", "lat_cos", "lon_sin", "lon_cos", ] # sanity: keep only those that actually exist dynamic_num_cols = [c for c in dynamic_num_cols if c in df.columns] print(f"Using {len(dynamic_num_cols)} dynamic numeric cols.") # ---------- time-based split: train / val / test ---------- # 1) first split: train vs temp (80/20) train_df, temp_df = train_test_split(df, test_size=0.2, shuffle=False) # 2) split temp into val/test (50/50 -> 10% + 10%) val_df, test_df = train_test_split(temp_df, test_size=0.5, shuffle=False) print(f"Train: {train_df.shape}, Val: {val_df.shape}, Test: {test_df.shape}") # ---------- scale dynamic numeric features ---------- dyn_scaler = RobustScaler() dyn_scaler.fit(train_df[dynamic_num_cols]) train_df.loc[:, dynamic_num_cols] = dyn_scaler.transform(train_df[dynamic_num_cols]) val_df.loc[:, dynamic_num_cols] = dyn_scaler.transform(val_df[dynamic_num_cols]) test_df.loc[:, dynamic_num_cols] = dyn_scaler.transform(test_df[dynamic_num_cols]) # ---------- scale target (ModalPrice) into 'y' ---------- target_scaler = RobustScaler() train_df["y"] = target_scaler.fit_transform(train_df[["ModalPrice"]]) val_df["y"] = target_scaler.transform(val_df[["ModalPrice"]]) test_df["y"] = target_scaler.transform(test_df[["ModalPrice"]]) # ---------- recombine in original time order ---------- scaled_df = pd.concat([train_df, val_df, test_df], axis=0) scaled_df = scaled_df.sort_values("date").reset_index(drop=True) OUT_PATH.parent.mkdir(parents=True, exist_ok=True) scaled_df.to_parquet(OUT_PATH, index=False) print(f"Saved scaled data to {OUT_PATH} with shape {scaled_df.shape}") # save scalers joblib.dump(dyn_scaler, DYN_SCALER_PATH) joblib.dump(target_scaler, TARGET_SCALER_PATH) print(f"Saved dyn_scaler to {DYN_SCALER_PATH}") print(f"Saved target_scaler to {TARGET_SCALER_PATH}") if __name__ == "__main__": main()