| """ |
| TerraMind - Feature Engineering utilities. |
| |
| Provides feature transformations for the yield predictor and |
| district intelligence modules. |
| """ |
| from __future__ import annotations |
|
|
| import numpy as np |
| import pandas as pd |
|
|
| from backend.core.logging_config import log |
|
|
|
|
| def engineer_yield_features(df: pd.DataFrame) -> pd.DataFrame: |
| """ |
| Given a combined yield DataFrame (State, District, Crop, Year_Num, Season, Area, Production, Yield), |
| compute historical lag/rolling features per (State, District, Crop, Season) group. |
| |
| Returns enriched DataFrame with additional columns: |
| - yield_lag1, yield_lag2, yield_lag3 |
| - yield_rolling3_mean, yield_rolling5_mean |
| - area_lag1 |
| - production_lag1 |
| - yield_trend_slope (linear slope over last 5 years) |
| """ |
| df = df.copy() |
| df.sort_values(["State", "District", "Crop", "Season", "Year_Num"], inplace=True) |
|
|
| grp = df.groupby(["State", "District", "Crop", "Season"], observed=True) |
|
|
| |
| df["yield_lag1"] = grp["Yield"].shift(1) |
| df["yield_lag2"] = grp["Yield"].shift(2) |
| df["yield_lag3"] = grp["Yield"].shift(3) |
|
|
| |
| df["yield_rolling3_mean"] = grp["Yield"].transform( |
| lambda s: s.shift(1).rolling(3, min_periods=1).mean() |
| ) |
| df["yield_rolling5_mean"] = grp["Yield"].transform( |
| lambda s: s.shift(1).rolling(5, min_periods=2).mean() |
| ) |
|
|
| |
| df["area_lag1"] = grp["Area"].shift(1) |
| df["production_lag1"] = grp["Production"].shift(1) |
|
|
| |
| def _slope(series): |
| vals = series.dropna().values[-5:] |
| if len(vals) < 3: |
| return np.nan |
| x = np.arange(len(vals), dtype=float) |
| slope = np.polyfit(x, vals, 1)[0] |
| return slope |
|
|
| df["yield_trend_slope"] = grp["Yield"].transform( |
| lambda s: s.expanding(min_periods=3).apply(_slope, raw=False) |
| ) |
|
|
| log.info("Engineered yield features - %d rows, %d cols", len(df), len(df.columns)) |
| return df |
|
|
|
|
| def compute_district_crop_stats(df: pd.DataFrame) -> pd.DataFrame: |
| """ |
| Precompute summary statistics per (State, District, Crop) for use |
| in the yield predictor and district intelligence. |
| |
| Returns DataFrame with one row per (State, District, Crop): |
| - mean_yield, std_yield, median_yield |
| - total_area, mean_area |
| - n_years |
| - first_year, last_year |
| """ |
| grp = df.groupby(["State", "District", "Crop"], observed=True) |
|
|
| stats = grp.agg( |
| mean_yield = ("Yield", "mean"), |
| std_yield = ("Yield", "std"), |
| median_yield = ("Yield", "median"), |
| total_area = ("Area", "sum"), |
| mean_area = ("Area", "mean"), |
| n_years = ("Year_Num", "nunique"), |
| first_year = ("Year_Num", "min"), |
| last_year = ("Year_Num", "max"), |
| ).reset_index() |
|
|
| stats["std_yield"] = stats["std_yield"].fillna(0) |
| return stats |
|
|
|
|
| def build_crop_frequency_prior(df: pd.DataFrame) -> pd.DataFrame: |
| """ |
| Build per-district crop frequency table. |
| |
| Returns DataFrame: State, District, Crop, area_share, frequency_rank |
| """ |
| |
| dist_total = df.groupby(["State", "District"], observed=True)["Area"].sum().reset_index() |
| dist_total.rename(columns={"Area": "district_total_area"}, inplace=True) |
|
|
| |
| crop_area = df.groupby(["State", "District", "Crop"], observed=True)["Area"].sum().reset_index() |
| crop_area.rename(columns={"Area": "crop_total_area"}, inplace=True) |
|
|
| merged = crop_area.merge(dist_total, on=["State", "District"], how="left") |
| merged["area_share"] = (merged["crop_total_area"] / merged["district_total_area"]).clip(0, 1) |
| merged["frequency_rank"] = merged.groupby(["State", "District"], observed=True)["crop_total_area"].rank( |
| ascending=False, method="min" |
| ) |
|
|
| return merged[["State", "District", "Crop", "area_share", "frequency_rank", "crop_total_area"]] |
|
|