TerraMind / backend /utils /feature_engineering.py
n4ksworks's picture
Deploy: TerraMind agriculture intelligence platform
f38691a
Raw
History Blame Contribute Delete
4.02 kB
"""
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)
# Lag features
df["yield_lag1"] = grp["Yield"].shift(1)
df["yield_lag2"] = grp["Yield"].shift(2)
df["yield_lag3"] = grp["Yield"].shift(3)
# Rolling means
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()
)
# Area / production lag
df["area_lag1"] = grp["Area"].shift(1)
df["production_lag1"] = grp["Production"].shift(1)
# Linear trend slope over last 5 observations
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
"""
# Total area per district
dist_total = df.groupby(["State", "District"], observed=True)["Area"].sum().reset_index()
dist_total.rename(columns={"Area": "district_total_area"}, inplace=True)
# Crop area per district
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"]]