"""Derived ratios computed from raw application_train inputs. Mirrors the 5 ratio features added in feature_engineering/orchestrator.py :: app_train_clean(). Re-applied at inference time after JSON inputs override the stored row, so the ratios always reflect the user-provided values. """ from __future__ import annotations import numpy as np import pandas as pd def apply_derived_ratios(df: pd.DataFrame) -> pd.DataFrame: """Compute the 5 ratio features in-place and return the DataFrame. Division by zero or NaN propagates as NaN — LightGBM handles it natively using the same routing it learned during training. """ out = df.copy() with np.errstate(divide="ignore", invalid="ignore"): out["DAYS_EMPLOYED_PERC"] = _safe_divide(out["DAYS_EMPLOYED"], out["DAYS_BIRTH"]) out["INCOME_CREDIT_PERC"] = _safe_divide( out["AMT_INCOME_TOTAL"], out["AMT_CREDIT"] ) out["INCOME_PER_PERSON"] = _safe_divide( out["AMT_INCOME_TOTAL"], out["CNT_FAM_MEMBERS"] ) out["ANNUITY_INCOME_PERC"] = _safe_divide( out["AMT_ANNUITY"], out["AMT_INCOME_TOTAL"] ) out["PAYMENT_RATE"] = _safe_divide(out["AMT_ANNUITY"], out["AMT_CREDIT"]) return out def _safe_divide(num: pd.Series, denom: pd.Series) -> pd.Series: """Element-wise division returning NaN when denominator is 0 or NaN.""" result = num / denom return result.replace([np.inf, -np.inf], np.nan)