logan-codes's picture
Rename model to joblib and track with LFS
6722f0a
"""
INFERENCE PIPELINE - Production ML Model Serving with Feature Consistency
=========================================================================
This module provides the core inference functionality for the Telco Churn prediction model.
It ensures that serving-time feature transformations exactly match training-time transformations,
which is CRITICAL for model accuracy in production.
Key Responsibilities:
1. Load MLflow-logged model and feature metadata from training
2. Apply identical feature transformations as used during training
3. Ensure correct feature ordering for model input
4. Convert model predictions to user-friendly output
CRITICAL PATTERN: Training/Serving Consistency
- Uses fixed BINARY_MAP for deterministic binary encoding
- Applies same one-hot encoding with drop_first=True
- Maintains exact feature column order from training
- Handles missing/new categorical values gracefully
Production Deployment:
- MODEL_DIR points to containerized model artifacts
- Feature schema loaded from training-time artifacts
- Optimized for single-row inference (real-time serving)
"""
import os
import json
import joblib
import pandas as pd
# === MODEL LOADING CONFIGURATION ===
# IMPORTANT: This path is set during Docker container build
# In development: uses local MLflow artifacts
# In production: uses model copied to container at build time
MODEL_DIR = "/app/model"
try:
# Load the trained model using joblib
model_path = os.path.join(os.path.dirname(__file__), "..", "..", "artifacts", "model.joblib")
model = joblib.load(model_path)
print(f"Model loaded successfully from {model_path}")
except Exception as e:
print(f"Failed to load model from {model_path}: {e}")
# Fallback for containerized environment
try:
model_path = os.path.join(MODEL_DIR, "model.joblib")
model = joblib.load(model_path)
print(f"Fallback: Loaded model from {model_path}")
except Exception as fallback_error:
raise Exception(f"Failed to load model: {e}. Fallback failed: {fallback_error}")
# === FEATURE SCHEMA LOADING ===
# CRITICAL: Load the exact feature column order used during training
# This ensures the model receives features in the expected order
try:
feature_file = os.path.join(os.path.dirname(__file__), "..", "..", "artifacts", "feature_columns.json")
with open(feature_file) as f:
FEATURE_COLS = json.load(f)
print(f"Loaded {len(FEATURE_COLS)} feature columns from training")
except Exception as e:
# Fallback for containerized environment
try:
feature_file = os.path.join(MODEL_DIR, "feature_columns.json")
with open(feature_file) as f:
FEATURE_COLS = json.load(f)
print(f"Fallback: Loaded {len(FEATURE_COLS)} feature columns")
except Exception as fallback_error:
raise Exception(f"Failed to load feature columns: {e}. Fallback failed: {fallback_error}")
# === FEATURE TRANSFORMATION CONSTANTS ===
# CRITICAL: These mappings must exactly match those used in training
# Any changes here will cause train/serve skew and degrade model performance
# Deterministic binary feature mappings (consistent with training)
BINARY_MAP = {
"gender": {"Female": 0, "Male": 1}, # Demographics
"Partner": {"No": 0, "Yes": 1}, # Has partner
"Dependents": {"No": 0, "Yes": 1}, # Has dependents
"PhoneService": {"No": 0, "Yes": 1}, # Phone service
"PaperlessBilling": {"No": 0, "Yes": 1}, # Billing preference
}
# Numeric columns that need type coercion
NUMERIC_COLS = ["tenure", "MonthlyCharges", "TotalCharges"]
def _serve_transform(df: pd.DataFrame) -> pd.DataFrame:
"""
Apply identical feature transformations as used during model training.
This function is CRITICAL for production ML - it ensures that features are
transformed exactly as they were during training to prevent train/serve skew.
Transformation Pipeline:
1. Clean column names and handle data types
2. Apply deterministic binary encoding (using BINARY_MAP)
3. One-hot encode remaining categorical features
4. Convert boolean columns to integers
5. Align features with training schema and order
Args:
df: Single-row DataFrame with raw customer data
Returns:
DataFrame with features transformed and ordered for model input
IMPORTANT: Any changes to this function must be reflected in training
feature engineering to maintain consistency.
"""
df = df.copy()
# Clean column names (remove any whitespace)
df.columns = df.columns.str.strip()
# === STEP 1: Numeric Type Coercion ===
# Ensure numeric columns are properly typed (handle string inputs)
for c in NUMERIC_COLS:
if c in df.columns:
# Convert to numeric, replacing invalid values with NaN
df[c] = pd.to_numeric(df[c], errors="coerce")
# Fill NaN with 0 (same as training preprocessing)
df[c] = df[c].fillna(0)
# === STEP 2: Binary Feature Encoding ===
# Apply deterministic mappings for binary features
# CRITICAL: Must use exact same mappings as training
for c, mapping in BINARY_MAP.items():
if c in df.columns:
df[c] = (
df[c]
.astype(str) # Convert to string
.str.strip() # Remove whitespace
.map(mapping) # Apply binary mapping
.astype("Int64") # Handle NaN values
.fillna(0) # Fill unknown values with 0
.astype(int) # Final integer conversion
)
# === STEP 3: One-Hot Encoding for Remaining Categorical Features ===
# Find remaining object/categorical columns (not in BINARY_MAP)
obj_cols = [c for c in df.select_dtypes(include=["object"]).columns]
if obj_cols:
# Apply one-hot encoding with drop_first=True (same as training)
# This prevents multicollinearity by dropping the first category
df = pd.get_dummies(df, columns=obj_cols, drop_first=True)
# === STEP 4: Boolean to Integer Conversion ===
# Convert any boolean columns to integers (XGBoost compatibility)
bool_cols = df.select_dtypes(include=["bool"]).columns
if len(bool_cols) > 0:
df[bool_cols] = df[bool_cols].astype(int)
# === STEP 5: Feature Alignment with Training Schema ===
# CRITICAL: Ensure features are in exact same order as training
# Missing features get filled with 0, extra features are dropped
df = df.reindex(columns=FEATURE_COLS, fill_value=0)
return df
def predict(input_dict: dict) -> str:
"""
Main prediction function for customer churn inference.
This function provides the complete inference pipeline from raw customer data
to business-friendly prediction output. It's called by both the FastAPI endpoint
and the Gradio interface to ensure consistent predictions.
Pipeline:
1. Convert input dictionary to DataFrame
2. Apply feature transformations (identical to training)
3. Generate model prediction using loaded XGBoost model
4. Convert prediction to user-friendly string
Args:
input_dict: Dictionary containing raw customer data with keys matching
the CustomerData schema (18 features total)
Returns:
Human-readable prediction string:
- "Likely to churn" for high-risk customers (model prediction = 1)
- "Not likely to churn" for low-risk customers (model prediction = 0)
Example:
>>> customer_data = {
... "gender": "Female", "tenure": 1, "Contract": "Month-to-month",
... "MonthlyCharges": 85.0, ... # other features
... }
>>> predict(customer_data)
"Likely to churn"
"""
# === STEP 1: Convert Input to DataFrame ===
# Create single-row DataFrame for pandas transformations
df = pd.DataFrame([input_dict])
# === STEP 2: Apply Feature Transformations ===
# Use the same transformation pipeline as training
df_enc = _serve_transform(df)
# === STEP 3: Generate Model Prediction ===
# Call the loaded MLflow model for inference
# The model returns predictions in various formats depending on the ML library
try:
preds = model.predict(df_enc)
# Normalize prediction output to consistent format
if hasattr(preds, "tolist"):
preds = preds.tolist() # Convert numpy array to list
# Extract single prediction value (for single-row input)
if isinstance(preds, (list, tuple)) and len(preds) == 1:
result = preds[0]
else:
result = preds
except Exception as e:
raise Exception(f"Model prediction failed: {e}")
# === STEP 4: Convert to Business-Friendly Output ===
# Convert binary prediction (0/1) to actionable business language
if result == 1:
return "Likely to churn" # High risk - needs intervention
else:
return "Not likely to churn" # Low risk - maintain normal service