revai-api / app /services /training.py
Shaankar39's picture
Deploy RevAI API
3d1332e
Raw
History Blame Contribute Delete
4.7 kB
import base64
import pickle
import pandas as pd
import numpy as np
import json
from typing import List, Dict, Any
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score, roc_auc_score, precision_score, recall_score, confusion_matrix
import xgboost as xgb
def _is_id_column(series: pd.Series) -> bool:
if series.dtype == object:
n_unique = series.nunique()
n_rows = len(series)
if n_unique / n_rows > 0.8 or n_unique > 100:
return True
if series.str.match(r"^[A-Z]+-\d+$").all():
return True
return False
def _is_categorical(series: pd.Series) -> bool:
if series.dtype == object or series.dtype.name == "category":
n_unique = series.nunique()
if 2 <= n_unique <= 20:
return True
return False
def train_from_data(data: List[Dict[str, Any]], target_col: str, model_type: str = "churn") -> dict:
df = pd.DataFrame(data)
y = df[target_col]
X = df.drop(columns=[target_col])
skipped = []
encoders = {}
X_processed = pd.DataFrame(index=X.index)
for col in X.columns:
if _is_id_column(X[col]):
skipped.append(col)
continue
if _is_categorical(X[col]):
le = LabelEncoder()
values = X[col].astype(str).fillna("__missing__")
le.fit(values)
X_processed[col] = le.transform(values)
encoders[col] = {"classes_": le.classes_.tolist()}
elif pd.api.types.is_numeric_dtype(X[col]):
X_processed[col] = X[col].fillna(X[col].median() if not X[col].isna().all() else 0)
else:
skipped.append(col)
if len(X_processed.columns) == 0:
raise ValueError("No usable features found.")
if y.nunique() < 2:
raise ValueError(f"Target column needs both positive and negative examples.")
X_train, X_test, y_train, y_test = train_test_split(
X_processed, y, test_size=0.2, random_state=42, stratify=y
)
pos = int(y_train.sum()) if hasattr(y_train, 'sum') else int((y_train == 1).sum())
neg = len(y_train) - pos
scale_weight = neg / max(pos, 1)
model = xgb.XGBClassifier(
n_estimators=100, max_depth=5, learning_rate=0.08,
subsample=0.8, colsample_bytree=0.8,
scale_pos_weight=scale_weight,
random_state=42, eval_metric="logloss"
)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1]
model_b64 = base64.b64encode(pickle.dumps(model)).decode()
metrics = {
"accuracy": round(float(accuracy_score(y_test, y_pred)), 4),
"roc_auc": round(float(roc_auc_score(y_test, y_proba)), 4),
"precision": round(float(precision_score(y_test, y_pred, zero_division=0)), 4),
"recall": round(float(recall_score(y_test, y_pred, zero_division=0)), 4),
"confusion_matrix": confusion_matrix(y_test, y_pred).tolist(),
"feature_importance": dict(
zip(X_processed.columns.tolist(), [float(v) for v in model.feature_importances_])
),
}
f1_denom = metrics["precision"] + metrics["recall"]
metrics["f1"] = round(2 * metrics["precision"] * metrics["recall"] / max(f1_denom, 1e-9), 4)
summary = {
"n_rows": len(df),
"n_features": len(X_processed.columns),
"target_col": target_col,
"imbalance_ratio": round(neg / max(pos, 1), 1),
"skipped_columns": skipped,
}
return {
"model_binary": model_b64,
"feature_names": X_processed.columns.tolist(),
"encoders_json": json.dumps(encoders),
"metrics": metrics,
"metrics_json": json.dumps(metrics),
"summary": summary,
"summary_json": json.dumps(summary),
"n_features": len(X_processed.columns),
"n_rows": len(X),
}
def predict_with_model(model_b64: str, feature_names: list, encoders_json: str,
data: List[Dict[str, Any]]) -> list:
model = pickle.loads(base64.b64decode(model_b64))
encoders_data = json.loads(encoders_json)
df = pd.DataFrame(data)
for col, enc_info in encoders_data.items():
if col in df.columns:
classes = enc_info["classes_"]
mapping = {c: i for i, c in enumerate(classes)}
df[col] = df[col].astype(str).map(lambda x: mapping.get(x, -1))
for col in feature_names:
if col not in df.columns:
df[col] = 0
X = df[feature_names].fillna(0)
probs = model.predict_proba(X)[:, 1]
scores = [round(float(p * 100), 1) for p in probs]
return scores