aegis-fraud-detector / backend /predictor.py
WizardCoder2007's picture
Fresh deploy with clean history
31b612e
Raw
History Blame Contribute Delete
6.12 kB
import os
import joblib
import pandas as pd
import numpy as np
# Path to model artifacts
ARTIFACTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "model_artifacts"))
class FraudPredictor:
def __init__(self):
self.artifacts_dir = ARTIFACTS_DIR
self.models = {}
self.scaler = None
self.selector = None
self.train_median = None
self.ohe_columns = None
self.cat_cols = None
self.constant_cols = None
self.heavy_missing_cols = None
self.load_artifacts()
def load_artifacts(self):
print(f"Loading model artifacts from {self.artifacts_dir}...")
try:
self.cat_cols = joblib.load(os.path.join(self.artifacts_dir, 'cat_cols.pkl'))
self.constant_cols = joblib.load(os.path.join(self.artifacts_dir, 'constant_cols.pkl'))
self.heavy_missing_cols = joblib.load(os.path.join(self.artifacts_dir, 'heavy_missing_cols.pkl'))
self.ohe_columns = joblib.load(os.path.join(self.artifacts_dir, 'model_features.pkl'))
self.train_median = joblib.load(os.path.join(self.artifacts_dir, 'train_median.pkl'))
self.scaler = joblib.load(os.path.join(self.artifacts_dir, 'robust_scaler.pkl'))
self.selector = joblib.load(os.path.join(self.artifacts_dir, 'variance_selector.pkl'))
# Load models on demand or pre-load them
self.models['voting'] = joblib.load(os.path.join(self.artifacts_dir, 'voting_classifier_model.pkl'))
self.models['xgboost'] = joblib.load(os.path.join(self.artifacts_dir, 'xgboost_model.pkl'))
self.models['random_forest'] = joblib.load(os.path.join(self.artifacts_dir, 'random_forest_model.pkl'))
print("Successfully loaded all artifacts and models.")
except Exception as e:
print(f"Error loading model artifacts: {e}")
raise e
def predict_dataframe(self, df: pd.DataFrame, model_name: str = 'voting', threshold: float = 0.30) -> pd.DataFrame:
"""
Processes a whole DataFrame and returns prediction results (probability, flag, risk_level).
Uses a highly optimized vectorized approach.
"""
if model_name not in self.models:
model_name = 'voting'
model = self.models[model_name]
processed_df = df.copy()
# 1. Convert dtypes for consistency
for col_dtype in processed_df.select_dtypes(include='float64').columns:
processed_df[col_dtype] = processed_df[col_dtype].astype("float32")
# 2. Drop constant and heavy missing columns from training
cols_to_drop = [col for col in self.constant_cols if col in processed_df.columns] + \
[col for col in self.heavy_missing_cols if col in processed_df.columns]
if cols_to_drop:
processed_df.drop(columns=cols_to_drop, inplace=True, errors='ignore')
# 3. Feature Engineering (Date-Time)
if 'F3888' in processed_df.columns:
# Parse datetime safely
dates = pd.to_datetime(processed_df['F3888'], errors='coerce')
# Combine all new time features in a single concat to avoid fragmentation
time_df = pd.DataFrame({
'transaction_month': dates.dt.month,
'year': dates.dt.year,
'day': dates.dt.day,
'day_of_week': dates.dt.dayofweek,
'transaction_quarter': dates.dt.quarter,
'week_of_year': dates.dt.isocalendar().week.astype(float),
'day_of_year': dates.dt.dayofyear
}, index=processed_df.index)
time_df['is_weekend'] = ((time_df['day_of_week'] == 5) | (time_df['day_of_week'] == 6)).astype(int)
processed_df = pd.concat([processed_df, time_df], axis=1)
processed_df.drop(columns=['F3888'], inplace=True, errors='ignore')
# 4. Drop indices & target
cols_to_drop_pre_ohe = ['Unnamed: 0', 'F3923']
processed_df.drop(columns=[col for col in cols_to_drop_pre_ohe if col in processed_df.columns], inplace=True, errors='ignore')
# 5. One-Hot Encoding for categorical columns
cols_to_ohe_exist = [col for col in self.cat_cols if col in processed_df.columns]
if cols_to_ohe_exist:
processed_df = pd.get_dummies(processed_df, columns=cols_to_ohe_exist, drop_first=True)
# 6. Align columns with training OHE columns
processed_df = processed_df.reindex(columns=self.ohe_columns, fill_value=0)
# 7. Impute missing values
imputed_data = processed_df.fillna(self.train_median)
# 8. Scale
scaled_data = self.scaler.transform(imputed_data)
# 9. Feature Selection
final_processed_data = self.selector.transform(scaled_data)
# 10. Predict probabilities
probs = model.predict_proba(final_processed_data)[:, 1]
# 11. Create results dataframe
results = pd.DataFrame({
'probability': np.round(probs, 4),
'is_suspicious': probs >= threshold
})
# Risk levels
results['risk_level'] = np.where(
probs > 0.7, 'High',
np.where(probs >= threshold, 'Medium', 'Low')
)
return results
def predict_transaction(self, data_row: dict, model_name: str = 'voting', threshold: float = 0.30) -> dict:
"""
Accepts a single raw row (as dict) and returns risk assessment.
"""
df = pd.DataFrame([data_row])
results_df = self.predict_dataframe(df, model_name, threshold)
row_res = results_df.iloc[0]
return {
'probability': float(row_res['probability']),
'is_suspicious': bool(row_res['is_suspicious']),
'risk_level': str(row_res['risk_level'])
}
# Global predictor instance
predictor = None
def get_predictor():
global predictor
if predictor is None:
predictor = FraudPredictor()
return predictor