| """Build the 768-column feature row consumed by the model. |
| |
| The flow: |
| 1. Take the raw JSON inputs and apply app_train transforms (factorize + |
| one-hot using the training categories) and the 5 derived ratios. |
| 2. Look up the auxiliary aggregates either from the parquet feature store |
| (Case 1: known SK_ID_CURR) or from the no-history template (Case 2). |
| 3. Concatenate horizontally and reindex to feature_names so the column |
| order matches what the model was trained on. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| from dataclasses import dataclass |
| from pathlib import Path |
| from typing import Any |
|
|
| import numpy as np |
| import pandas as pd |
|
|
| from api.inputs_transform import ( |
| load_binary_mappings, |
| load_categories, |
| transform_app_train_inputs, |
| ) |
| from api.ratios import apply_derived_ratios |
|
|
|
|
| @dataclass(frozen=True) |
| class InferenceArtefacts: |
| """Read-only container for everything needed to assemble a feature row.""" |
|
|
| feature_names: list[str] |
| known_categories: dict[str, list[str]] |
| binary_mappings: dict[str, dict[str, int]] |
| no_history_template: dict[str, float | int | None] |
| feature_store: pd.DataFrame |
|
|
| @classmethod |
| def load( |
| cls, |
| feature_names_path: Path, |
| categories_path: Path, |
| binary_mappings_path: Path, |
| no_history_template_path: Path, |
| feature_store_path: Path, |
| ) -> "InferenceArtefacts": |
| feature_names = json.loads(feature_names_path.read_text()) |
| known_categories = load_categories(categories_path) |
| binary_mappings = load_binary_mappings(binary_mappings_path) |
| |
| |
| raw_template = json.loads(no_history_template_path.read_text()) |
| no_history_template = { |
| k: (np.nan if v is None else v) for k, v in raw_template.items() |
| } |
|
|
| feature_store = pd.read_parquet(feature_store_path) |
| if feature_store.index.name != "SK_ID_CURR": |
| |
| if "SK_ID_CURR" in feature_store.columns: |
| feature_store = feature_store.set_index("SK_ID_CURR") |
|
|
| return cls( |
| feature_names=feature_names, |
| known_categories=known_categories, |
| binary_mappings=binary_mappings, |
| no_history_template=no_history_template, |
| feature_store=feature_store, |
| ) |
|
|
|
|
| def assemble( |
| raw_inputs: dict[str, Any], |
| sk_id_curr: int, |
| artefacts: InferenceArtefacts, |
| ) -> tuple[pd.DataFrame, bool]: |
| """Build a 1Γ768 DataFrame ready for model.predict(). |
| |
| Returns (features, client_known). |
| """ |
| |
| app_part = transform_app_train_inputs( |
| raw_inputs, |
| known_categories=artefacts.known_categories, |
| binary_mappings=artefacts.binary_mappings, |
| ) |
| app_part = apply_derived_ratios(app_part) |
|
|
| |
| if sk_id_curr in artefacts.feature_store.index: |
| agg_part = artefacts.feature_store.loc[[sk_id_curr]].reset_index(drop=True) |
| client_known = True |
| else: |
| agg_part = pd.DataFrame([artefacts.no_history_template]) |
| client_known = False |
|
|
| |
| combined = pd.concat( |
| [app_part.reset_index(drop=True), agg_part.reset_index(drop=True)], |
| axis=1, |
| ) |
|
|
| |
| |
| combined = combined.loc[:, ~combined.columns.duplicated()] |
|
|
| aligned = combined.reindex(columns=artefacts.feature_names) |
| aligned = aligned.replace([np.inf, -np.inf], np.nan) |
|
|
| return aligned, client_known |
|
|