ash-coded-it's picture
Upload folder using huggingface_hub
1e3f942 verified
#!/usr/bin/env python3
import os
from datetime import datetime
from pathlib import Path
from typing import Optional, Tuple, Union
import gradio as gr
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
average_precision_score,
brier_score_loss,
roc_auc_score,
)
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
# Resolve repository root relative to this file
BASE_DIR = Path(__file__).resolve().parent.parent
# Output directory and preset dataset paths relative to repository root
SCORES_DIR = BASE_DIR / "scores"
# Preset example dataset paths
PRESET_FEATURES = BASE_DIR / "examples" / "synthetic_v2" / "leads_features.csv"
PRESET_OUTCOMES = BASE_DIR / "examples" / "synthetic_v2" / "outcomes.csv"
# Reuse same feature candidates and categorical sets as scripts/batch_scoring.py
FEATURE_CANDIDATES = [
"living_area_sqft",
"average_monthly_kwh",
"average_monthly_bill_usd",
"shading_factor",
"roof_suitability_score",
"seasonality_index",
"electric_panel_amperage",
"has_pool",
"is_remote_worker_household",
"tdsp",
"rate_structure",
"credit_score_range",
"household_income_bracket",
"preferred_financing_type",
"neighborhood_type",
]
CATEGORICAL = [
"tdsp",
"rate_structure",
"credit_score_range",
"household_income_bracket",
"preferred_financing_type",
"neighborhood_type",
]
def _safe_path(file_or_path: Optional[Union[str, gr.File]]) -> Optional[str]:
"""
Convert a gradio File object or string path to a usable string path.
"""
if file_or_path is None:
return None
if isinstance(file_or_path, str):
return file_or_path
# gradio File component returns a tempfile object-like with .name
if hasattr(file_or_path, "name"):
return file_or_path.name
# Some versions return a dict with 'name'
if isinstance(file_or_path, dict) and "name" in file_or_path:
return file_or_path["name"]
return None
def _validate_inputs(df_features: pd.DataFrame, df_outcomes: pd.DataFrame) -> None:
if "lead_id" not in df_features.columns:
raise ValueError("Features CSV must contain a 'lead_id' column.")
if "lead_id" not in df_outcomes.columns:
raise ValueError("Outcomes CSV must contain a 'lead_id' column.")
if "sold" not in df_outcomes.columns:
raise ValueError("Outcomes CSV must contain a 'sold' column (0/1).")
def _compute_metrics(y_true: np.ndarray, y_prob: np.ndarray) -> Tuple[Optional[float], Optional[float], Optional[float]]:
"""
Compute ROC AUC, PR AUC, and Brier score with graceful fallbacks.
"""
auc = None
pr_auc = None
brier = None
# Brier score is defined for binary labels even if only one class is present
try:
brier = float(brier_score_loss(y_true.astype(int), y_prob))
except Exception:
brier = None
# ROC AUC and PR AUC require both classes to be present in y_true
try:
if len(np.unique(y_true.astype(int))) >= 2:
auc = float(roc_auc_score(y_true.astype(int), y_prob))
else:
auc = None
except Exception:
auc = None
try:
if len(np.unique(y_true.astype(int))) >= 2:
pr_auc = float(average_precision_score(y_true.astype(int), y_prob))
else:
pr_auc = None
except Exception:
pr_auc = None
return auc, pr_auc, brier
def train_and_score(
mode: str,
features_file: Optional[Union[str, gr.File]],
outcomes_file: Optional[Union[str, gr.File]],
):
"""
mode: "Use example synthetic_v2" or "Upload CSVs"
Returns:
- metrics_markdown (str)
- preds_preview (pd.DataFrame)
- scored_preview (pd.DataFrame)
- predictions_file (str path)
- scored_file (str path)
"""
try:
if mode == "Use example synthetic_v2":
features_path = PRESET_FEATURES
outcomes_path = PRESET_OUTCOMES
if not features_path.exists() or not outcomes_path.exists():
raise FileNotFoundError(
f"Preset files not found. Expected:\n- {PRESET_FEATURES}\n- {PRESET_OUTCOMES}"
)
else:
f_path = _safe_path(features_file)
o_path = _safe_path(outcomes_file)
if not f_path or not o_path:
raise ValueError("Please upload BOTH Features CSV and Outcomes CSV.")
features_path = Path(f_path)
outcomes_path = Path(o_path)
if not features_path.exists():
raise FileNotFoundError(f"Features file not found: {features_path}")
if not outcomes_path.exists():
raise FileNotFoundError(f"Outcomes file not found: {outcomes_path}")
X = pd.read_csv(features_path)
y_df = pd.read_csv(outcomes_path)[["lead_id", "sold"]]
_validate_inputs(X, y_df)
df = X.merge(y_df, on="lead_id", how="inner")
# Select features present in this dataset
available = [c for c in FEATURE_CANDIDATES if c in df.columns]
if not available:
raise ValueError(
"No candidate features found in features CSV. "
f"Expected any of: {', '.join(FEATURE_CANDIDATES)}"
)
numeric = [c for c in available if c not in CATEGORICAL]
cat_cols = [c for c in available if c in CATEGORICAL]
preproc = ColumnTransformer(
transformers=[
("num", "passthrough", numeric),
("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols),
],
remainder="drop",
)
model = LogisticRegression(max_iter=5000)
pipe = Pipeline(steps=[("pre", preproc), ("clf", model)])
y = df["sold"].astype(int)
# Only stratify if both classes present
if len(np.unique(y)) >= 2:
train_df, test_df = train_test_split(
df, test_size=0.25, random_state=42, stratify=y
)
else:
train_df, test_df = train_test_split(
df, test_size=0.25, random_state=42, stratify=None
)
pipe.fit(train_df[available], train_df["sold"].astype(int))
test_probs = pipe.predict_proba(test_df[available])[:, 1]
auc, pr_auc, brier = _compute_metrics(test_df["sold"].values, test_probs)
# Score all rows
all_probs = pipe.predict_proba(df[available])[:, 1]
preds = df[["lead_id"]].copy()
preds["probability_to_buy"] = np.round(all_probs, 4)
# Persist outputs
SCORES_DIR.mkdir(parents=True, exist_ok=True)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
predictions_path = SCORES_DIR / f"predictions_{ts}.csv"
scored_path = SCORES_DIR / f"leads_features_scored_{ts}.csv"
preds.to_csv(predictions_path, index=False)
scored = X.merge(preds, on="lead_id", how="left")
scored.to_csv(scored_path, index=False)
# Prepare outputs
def fmt(val: Optional[float]) -> str:
return f"{val:.3f}" if val is not None else "N/A"
metrics_md = (
"### Evaluation Metrics (test split)\n"
f"- ROC AUC: {fmt(auc)}\n"
f"- PR AUC: {fmt(pr_auc)}\n"
f"- Brier Score: {fmt(brier)}\n\n"
f"Outputs were saved to:\n"
f"- {predictions_path}\n"
f"- {scored_path}\n"
)
preds_preview = preds.head(20)
scored_preview = scored.head(20)
return (
metrics_md,
preds_preview,
scored_preview,
str(predictions_path),
str(scored_path),
)
except Exception as e:
# On error, return message and empty placeholders
metrics_md = f"### Error\n{str(e)}"
return metrics_md, pd.DataFrame(), pd.DataFrame(), None, None
with gr.Blocks(title="SOLAI Scoring Dashboard") as demo:
gr.Markdown(
"""
# SOLAI Scoring Dashboard (Gradio)
Train a baseline Logistic Regression model on your solar lead dataset and generate probability_to_buy predictions.
- Default dataset: examples/synthetic_v2
- Outputs are always written to /Users/git/solai/scores and are also downloadable below.
""".strip()
)
with gr.Row():
mode = gr.Radio(
choices=["Use example synthetic_v2", "Upload CSVs"],
value="Use example synthetic_v2",
label="Data Source",
)
with gr.Row():
features_upload = gr.File(
label="Features CSV (for 'Upload CSVs' mode)",
file_types=[".csv"],
visible=False,
)
outcomes_upload = gr.File(
label="Outcomes CSV with columns [lead_id, sold] (for 'Upload CSVs' mode)",
file_types=[".csv"],
visible=False,
)
def toggle_uploads(selected_mode: str):
show = selected_mode == "Upload CSVs"
return [
gr.update(visible=show),
gr.update(visible=show),
]
mode.change(
toggle_uploads,
inputs=[mode],
outputs=[features_upload, outcomes_upload],
)
with gr.Row():
run_btn = gr.Button("Train + Score", variant="primary")
with gr.Row():
metrics_md = gr.Markdown()
with gr.Row():
preds_df = gr.Dataframe(label="predictions.csv (preview)", interactive=False)
with gr.Row():
scored_df = gr.Dataframe(label="leads_features_scored.csv (preview)", interactive=False)
with gr.Row():
pred_file = gr.File(label="Download predictions.csv")
scored_file = gr.File(label="Download leads_features_scored.csv")
run_btn.click(
fn=train_and_score,
inputs=[mode, features_upload, outcomes_upload],
outputs=[metrics_md, preds_df, scored_df, pred_file, scored_file],
)
if __name__ == "__main__":
demo.launch()