dynamic-pricing-engine / app /feature_engineering.py
AishwaryaNJ's picture
Add comprehensive tests for pricing engine and API endpoints
1e11bce
Raw
History Blame Contribute Delete
4.04 kB
from __future__ import annotations
from pathlib import Path
from typing import Iterable
import numpy as np
import pandas as pd
NUMERIC_FEATURES = [
"hour_of_day",
"day_of_week",
"is_weekend",
"is_festival",
"inventory_level",
"inventory_days_cover",
"competitor_price",
"click_through_rate",
"conversion_rate",
"units_sold_last_5m",
"units_sold_last_1h",
"base_cost",
"current_price",
"demand_index",
"inventory_pressure",
"competitor_gap",
]
CATEGORICAL_FEATURES = ["category", "brand", "customer_segment"]
TARGET_COLUMN = "optimal_price"
KAGGLE_RETAIL_NUMERIC_FEATURES = [
"qty",
"freight_price",
"product_name_lenght",
"product_description_lenght",
"product_photos_qty",
"product_weight_g",
"product_score",
"customers",
"weekday",
"weekend",
"holiday",
"volume",
"comp_1",
"ps1",
"fp1",
"comp_2",
"ps2",
"fp2",
"comp_3",
"ps3",
"fp3",
"lag_price",
"month",
"year",
]
KAGGLE_RETAIL_CATEGORICAL_FEATURES = ["product_id", "product_category_name"]
KAGGLE_RETAIL_TARGET_COLUMN = "unit_price"
def add_derived_features(frame: pd.DataFrame) -> pd.DataFrame:
enriched = frame.copy()
enriched["demand_index"] = (
enriched["units_sold_last_1h"] * 0.55
+ enriched["units_sold_last_5m"] * 0.35
+ enriched["conversion_rate"] * 100 * 0.10
)
enriched["inventory_pressure"] = np.where(
enriched["inventory_level"] <= 20,
1.25,
np.where(enriched["inventory_level"] <= 60, 1.05, 0.92),
)
enriched["competitor_gap"] = (
enriched["current_price"] - enriched["competitor_price"]
) / enriched["competitor_price"].clip(lower=1.0)
return enriched
def load_training_data(path: Path) -> pd.DataFrame:
frame = pd.read_csv(path)
return add_derived_features(frame)
def load_kaggle_retail_training_data(path: Path) -> pd.DataFrame:
frame = pd.read_csv(path)
if "month_year" in frame.columns:
parsed_month_year = pd.to_datetime(frame["month_year"], dayfirst=True, errors="coerce")
if "month" not in frame.columns:
frame["month"] = parsed_month_year.dt.month
if "year" not in frame.columns:
frame["year"] = parsed_month_year.dt.year
numeric_defaults = [
"comp_1",
"ps1",
"fp1",
"comp_2",
"ps2",
"fp2",
"comp_3",
"ps3",
"fp3",
"lag_price",
]
for column in numeric_defaults:
if column not in frame.columns:
frame[column] = np.nan
if "volume" not in frame.columns:
frame["volume"] = (
frame.get("product_name_lenght", 0).fillna(0)
* frame.get("product_description_lenght", 0).fillna(0)
* frame.get("product_photos_qty", 0).fillna(0).clip(lower=1)
)
if "weekday" not in frame.columns:
frame["weekday"] = 0
if "weekend" not in frame.columns:
frame["weekend"] = 0
if "holiday" not in frame.columns:
frame["holiday"] = 0
ensure_columns(
frame,
KAGGLE_RETAIL_NUMERIC_FEATURES
+ KAGGLE_RETAIL_CATEGORICAL_FEATURES
+ [KAGGLE_RETAIL_TARGET_COLUMN],
)
return frame
def split_xy(
frame: pd.DataFrame,
numeric_features: list[str] | None = None,
categorical_features: list[str] | None = None,
target_column: str = TARGET_COLUMN,
) -> tuple[pd.DataFrame, pd.Series]:
selected_numeric = numeric_features or NUMERIC_FEATURES
selected_categorical = categorical_features or CATEGORICAL_FEATURES
features = frame[selected_numeric + selected_categorical].copy()
target = frame[target_column].copy()
return features, target
def ensure_columns(frame: pd.DataFrame, required_columns: Iterable[str]) -> None:
missing = [column for column in required_columns if column not in frame.columns]
if missing:
raise ValueError(f"Missing required columns: {missing}")