Spaces:
Sleeping
Sleeping
| 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 | |