abersbail's picture
Add predictive ML workbench Space
ca86eea verified
Raw
History Blame Contribute Delete
14.5 kB
import os
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.base import clone
from sklearn.cluster import AgglomerativeClustering, KMeans
from sklearn.compose import ColumnTransformer
from sklearn.decomposition import PCA
from sklearn.ensemble import GradientBoostingClassifier, GradientBoostingRegressor, RandomForestClassifier, RandomForestRegressor
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression, Ridge
from sklearn.metrics import (
accuracy_score,
confusion_matrix,
f1_score,
mean_absolute_error,
mean_squared_error,
r2_score,
silhouette_score,
)
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
os.environ.setdefault("LOKY_MAX_CPU_COUNT", "2")
class PredictiveMLWorkbenchService:
def run(self, csv_path, workflow, target_column, test_size, cv_folds, max_clusters):
if not csv_path:
return "", "", "", None, "Upload a CSV file first."
try:
df = pd.read_csv(csv_path)
except Exception as exc:
return "", "", "", None, f"Could not read CSV: {type(exc).__name__}: {exc}"
if df.empty:
return "", "", "", None, "Dataset is empty."
try:
if workflow == "Classification":
return self._run_classification(df, target_column, test_size, cv_folds)
if workflow == "Regression":
return self._run_regression(df, target_column, test_size, cv_folds)
if workflow == "Clustering":
return self._run_clustering(df, target_column, max_clusters)
return self._run_dimensionality_reduction(df, target_column)
except Exception as exc:
return "", "", "", None, f"Workflow failed: {type(exc).__name__}: {exc}"
def _run_classification(self, df, target_column, test_size, cv_folds):
x, y = self._supervised_split(df, target_column)
preprocessor = self._build_preprocessor(x)
candidates = [
(
"LogisticRegression",
LogisticRegression(max_iter=600),
{"model__C": [0.5, 1.0, 2.0]},
),
(
"RandomForestClassifier",
RandomForestClassifier(random_state=42),
{"model__n_estimators": [120, 220], "model__max_depth": [None, 8]},
),
(
"GradientBoostingClassifier",
GradientBoostingClassifier(random_state=42),
{"model__n_estimators": [80, 140], "model__learning_rate": [0.05, 0.1]},
),
]
x_train, x_test, y_train, y_test = train_test_split(
x,
y,
test_size=test_size,
random_state=42,
stratify=y if y.nunique() > 1 else None,
)
best_name, best_search = self._select_model(
candidates=candidates,
preprocessor=preprocessor,
x_train=x_train,
y_train=y_train,
cv_folds=cv_folds,
scoring="f1_macro",
)
preds = best_search.best_estimator_.predict(x_test)
acc = accuracy_score(y_test, preds)
macro_f1 = f1_score(y_test, preds, average="macro")
metrics = "\n".join(
[
f"Accuracy: {acc:.4f}",
f"Macro F1: {macro_f1:.4f}",
f"CV Best Score: {best_search.best_score_:.4f}",
f"Train Rows: {len(x_train)}",
f"Test Rows: {len(x_test)}",
f"Classes: {y.nunique()}",
f"Best Params: {best_search.best_params_}",
]
)
fig = self._plot_confusion_matrix(y_test, preds)
preview = x.head(8).to_string(index=False)
status = "Completed end-to-end classification workflow with preprocessing, model selection, and evaluation."
return best_name, metrics, preview, fig, status
def _run_regression(self, df, target_column, test_size, cv_folds):
x, y = self._supervised_split(df, target_column)
if not pd.api.types.is_numeric_dtype(y):
raise ValueError("Regression target column must be numeric.")
preprocessor = self._build_preprocessor(x)
candidates = [
(
"Ridge",
Ridge(),
{"model__alpha": [0.5, 1.0, 2.0, 5.0]},
),
(
"RandomForestRegressor",
RandomForestRegressor(random_state=42),
{"model__n_estimators": [120, 220], "model__max_depth": [None, 8]},
),
(
"GradientBoostingRegressor",
GradientBoostingRegressor(random_state=42),
{"model__n_estimators": [80, 140], "model__learning_rate": [0.05, 0.1]},
),
]
x_train, x_test, y_train, y_test = train_test_split(
x,
y,
test_size=test_size,
random_state=42,
)
best_name, best_search = self._select_model(
candidates=candidates,
preprocessor=preprocessor,
x_train=x_train,
y_train=y_train,
cv_folds=cv_folds,
scoring="r2",
)
preds = best_search.best_estimator_.predict(x_test)
r2 = r2_score(y_test, preds)
mae = mean_absolute_error(y_test, preds)
rmse = float(np.sqrt(mean_squared_error(y_test, preds)))
metrics = "\n".join(
[
f"R2: {r2:.4f}",
f"MAE: {mae:.4f}",
f"RMSE: {rmse:.4f}",
f"CV Best Score: {best_search.best_score_:.4f}",
f"Train Rows: {len(x_train)}",
f"Test Rows: {len(x_test)}",
f"Best Params: {best_search.best_params_}",
]
)
fig = self._plot_regression_scatter(y_test, preds)
preview = x.head(8).to_string(index=False)
status = "Completed end-to-end regression workflow with preprocessing, model selection, and evaluation."
return best_name, metrics, preview, fig, status
def _run_clustering(self, df, target_column, max_clusters):
x = df.copy()
if target_column and target_column in x.columns:
x = x.drop(columns=[target_column])
preprocessor = self._build_preprocessor(x)
transformed = preprocessor.fit_transform(x)
transformed = np.asarray(transformed)
sample = transformed
if transformed.shape[0] > 1200:
sample = transformed[:1200]
best = None
best_labels = None
for n_clusters in range(2, max_clusters + 1):
for name, estimator in [
("KMeans", KMeans(n_clusters=n_clusters, random_state=42, n_init=10)),
("AgglomerativeClustering", AgglomerativeClustering(n_clusters=n_clusters)),
]:
labels = estimator.fit_predict(sample)
if len(np.unique(labels)) < 2:
continue
score = silhouette_score(sample, labels)
if best is None or score > best["score"]:
best = {"name": name, "clusters": n_clusters, "score": score}
best_labels = labels
if best is None:
raise ValueError("Could not produce a valid clustering result.")
reduced = PCA(n_components=2, random_state=42).fit_transform(sample)
metrics = "\n".join(
[
f"Algorithm: {best['name']}",
f"Clusters: {best['clusters']}",
f"Silhouette Score: {best['score']:.4f}",
f"Rows Used: {sample.shape[0]}",
f"Features After Preprocessing: {sample.shape[1]}",
]
)
fig = self._plot_cluster_scatter(reduced, best_labels, title=f"{best['name']} clustering")
preview = x.head(8).to_string(index=False)
status = "Completed clustering workflow with preprocessing, model selection across algorithms, and evaluation."
return best["name"], metrics, preview, fig, status
def _run_dimensionality_reduction(self, df, target_column):
x = df.copy()
labels = None
if target_column and target_column in x.columns:
labels = x[target_column].astype(str)
x = x.drop(columns=[target_column])
preprocessor = self._build_preprocessor(x)
transformed = preprocessor.fit_transform(x)
transformed = np.asarray(transformed)
n_components = 2 if transformed.shape[1] >= 2 else 1
pca = PCA(n_components=n_components, random_state=42)
reduced = pca.fit_transform(transformed)
explained = pca.explained_variance_ratio_
metrics = "\n".join(
[
f"Method: PCA",
f"Components: {n_components}",
f"Explained Variance: {', '.join(f'{v:.4f}' for v in explained)}",
f"Cumulative Variance: {explained.sum():.4f}",
f"Rows: {transformed.shape[0]}",
f"Features After Preprocessing: {transformed.shape[1]}",
]
)
fig = self._plot_pca_scatter(reduced, labels)
preview_df = pd.DataFrame(reduced, columns=[f"PC{i+1}" for i in range(n_components)])
preview = preview_df.head(8).to_string(index=False)
status = "Completed dimensionality reduction workflow with preprocessing and PCA evaluation."
return "PCA", metrics, preview, fig, status
def _build_preprocessor(self, x):
numeric_cols = x.select_dtypes(include=["number"]).columns.tolist()
categorical_cols = [col for col in x.columns if col not in numeric_cols]
numeric_pipeline = Pipeline(
steps=[
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
]
)
categorical_pipeline = Pipeline(
steps=[
("imputer", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
]
)
return ColumnTransformer(
transformers=[
("num", numeric_pipeline, numeric_cols),
("cat", categorical_pipeline, categorical_cols),
],
remainder="drop",
)
def _supervised_split(self, df, target_column):
if not target_column:
raise ValueError("Target column is required for supervised workflows.")
if target_column not in df.columns:
raise ValueError(f"Target column `{target_column}` was not found.")
x = df.drop(columns=[target_column])
y = df[target_column]
if x.shape[1] == 0:
raise ValueError("Dataset needs at least one feature column.")
return x, y
def _select_model(self, candidates, preprocessor, x_train, y_train, cv_folds, scoring):
best_name = None
best_search = None
for name, estimator, param_grid in candidates:
pipeline = Pipeline(
steps=[
("preprocessor", clone(preprocessor)),
("model", estimator),
]
)
search = GridSearchCV(
estimator=pipeline,
param_grid=param_grid,
cv=cv_folds,
scoring=scoring,
n_jobs=1,
)
search.fit(x_train, y_train)
if best_search is None or search.best_score_ > best_search.best_score_:
best_name = name
best_search = search
return best_name, best_search
def _plot_confusion_matrix(self, y_true, y_pred):
fig, ax = plt.subplots(figsize=(5.5, 4.5))
labels = np.unique(np.concatenate([np.asarray(y_true), np.asarray(y_pred)]))
matrix = confusion_matrix(y_true, y_pred, labels=labels)
im = ax.imshow(matrix, cmap="Blues")
ax.set_title("Confusion Matrix")
ax.set_xlabel("Predicted")
ax.set_ylabel("True")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels, rotation=45, ha="right")
ax.set_yticks(range(len(labels)))
ax.set_yticklabels(labels)
for i in range(matrix.shape[0]):
for j in range(matrix.shape[1]):
ax.text(j, i, str(matrix[i, j]), ha="center", va="center", color="black")
fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
fig.tight_layout()
return fig
def _plot_regression_scatter(self, y_true, y_pred):
fig, ax = plt.subplots(figsize=(5.5, 4.5))
ax.scatter(y_true, y_pred, alpha=0.75)
min_val = min(np.min(y_true), np.min(y_pred))
max_val = max(np.max(y_true), np.max(y_pred))
ax.plot([min_val, max_val], [min_val, max_val], linestyle="--", color="red")
ax.set_title("Actual vs Predicted")
ax.set_xlabel("Actual")
ax.set_ylabel("Predicted")
fig.tight_layout()
return fig
def _plot_cluster_scatter(self, reduced, labels, title):
fig, ax = plt.subplots(figsize=(5.5, 4.5))
scatter = ax.scatter(reduced[:, 0], reduced[:, 1], c=labels, cmap="tab10", alpha=0.85)
ax.set_title(title)
ax.set_xlabel("Component 1")
ax.set_ylabel("Component 2")
fig.colorbar(scatter, ax=ax, fraction=0.046, pad=0.04)
fig.tight_layout()
return fig
def _plot_pca_scatter(self, reduced, labels):
fig, ax = plt.subplots(figsize=(5.5, 4.5))
if reduced.shape[1] == 1:
ax.scatter(reduced[:, 0], np.zeros_like(reduced[:, 0]), alpha=0.75)
ax.set_ylabel("Zero Axis")
else:
if labels is not None:
unique_labels = labels.astype(str)
for label in sorted(unique_labels.unique())[:8]:
mask = unique_labels == label
ax.scatter(reduced[mask, 0], reduced[mask, 1], alpha=0.75, label=label)
ax.legend(loc="best", fontsize=8)
else:
ax.scatter(reduced[:, 0], reduced[:, 1], alpha=0.75)
ax.set_ylabel("PC2")
ax.set_title("PCA Projection")
ax.set_xlabel("PC1")
fig.tight_layout()
return fig