"""Feature engineering for the preparation-time model. Highlights: - ``kitchen_load`` is derived from order timestamps: number of other orders that fell into a 30-minute rolling window before each order, per city. The raw dataset has no explicit "kitchen load" feature; we approximate it through order density, which is the most direct proxy for "how busy was the kitchen when the order came in". - Time-of-day buckets, weekday/weekend, distance. The output dataframe is what ``train.py`` consumes. """ from __future__ import annotations import sys from math import asin, cos, radians, sin, sqrt from pathlib import Path import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from src.config import PROCESSED_DIR # noqa: E402 CLEAN_CSV = PROCESSED_DIR / "food_delivery_clean.csv" FEATURES_CSV = PROCESSED_DIR / "food_delivery_features.csv" TARGET = "Time_taken(min)" NUMERIC_FEATURES = [ "Delivery_person_Age", "Delivery_person_Ratings", "Vehicle_condition", "multiple_deliveries", "distance_km", "distance_missing", "kitchen_load_30min", "hour_of_day", "day_of_week", "is_peak_hour", "is_weekend", ] CATEGORICAL_FEATURES = [ "Weatherconditions", "Road_traffic_density", "Type_of_order", "Type_of_vehicle", "Festival", "City", ] def haversine_km(lat1: pd.Series, lon1: pd.Series, lat2: pd.Series, lon2: pd.Series) -> pd.Series: r = 6371.0 lat1r = np.radians(lat1) lat2r = np.radians(lat2) dlat = np.radians(lat2 - lat1) dlon = np.radians(lon2 - lon1) a = np.sin(dlat / 2) ** 2 + np.cos(lat1r) * np.cos(lat2r) * np.sin(dlon / 2) ** 2 return 2 * r * np.arcsin(np.sqrt(a)) def add_distance(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() if { "Restaurant_latitude", "Restaurant_longitude", "Delivery_location_latitude", "Delivery_location_longitude", }.issubset(df.columns): df["distance_km"] = haversine_km( df["Restaurant_latitude"], df["Restaurant_longitude"], df["Delivery_location_latitude"], df["Delivery_location_longitude"], ) # cap absurd distances (data noise) df.loc[df["distance_km"] > 50, "distance_km"] = np.nan else: df["distance_km"] = np.nan return df def add_time_features(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() df["order_datetime"] = pd.to_datetime(df["order_datetime"], errors="coerce") df["hour_of_day"] = df["order_datetime"].dt.hour df["day_of_week"] = df["order_datetime"].dt.dayofweek df["is_weekend"] = df["day_of_week"].isin([5, 6]).astype(int) df["is_peak_hour"] = df["hour_of_day"].isin([12, 13, 19, 20, 21]).astype(int) return df def add_kitchen_load(df: pd.DataFrame, window_minutes: int = 30) -> pd.DataFrame: """Count of other orders within the previous ``window_minutes`` per city. The City column is a noisy categorical proxy for "restaurant location". We fall back to a single global grouping if the column is missing. """ df = df.copy() df = df.sort_values("order_datetime") group_col = "City" if "City" in df.columns else None def _per_group(g: pd.DataFrame) -> pd.DataFrame: g = g.set_index("order_datetime").sort_index() rolling = g.assign(_one=1)["_one"].rolling(f"{window_minutes}min").sum() - 1 g["kitchen_load_30min"] = rolling.values return g.reset_index() if group_col is not None and df[group_col].notna().any(): df = df.groupby(group_col, group_keys=False).apply(_per_group) else: df = _per_group(df) df["kitchen_load_30min"] = df["kitchen_load_30min"].clip(lower=0).fillna(0) return df def build_features(df: pd.DataFrame) -> pd.DataFrame: df = add_distance(df) # Explicit missing-distance flag so the model can learn the missing-data # pattern instead of relying on a silent median imputation. Captures both # missing coordinates and distances capped as noise (> 50 km). df["distance_missing"] = df["distance_km"].isna().astype(int) df = add_time_features(df) df = add_kitchen_load(df) keep = [TARGET] + NUMERIC_FEATURES + [c for c in CATEGORICAL_FEATURES if c in df.columns] keep = [c for c in keep if c in df.columns] out = df[keep].copy() # Drop rows with missing target out = out.dropna(subset=[TARGET]) return out def main() -> None: if not CLEAN_CSV.exists(): raise FileNotFoundError( f"{CLEAN_CSV} missing. Run 'python -m src.ml.prepare_data' first." ) df = pd.read_csv(CLEAN_CSV) feats = build_features(df) FEATURES_CSV.parent.mkdir(parents=True, exist_ok=True) feats.to_csv(FEATURES_CSV, index=False) print(f"[features] wrote {FEATURES_CSV} ({len(feats):,} rows, {feats.shape[1]} cols)") if __name__ == "__main__": main()