AaTekle
Deploy Agentic Machine Learning app
6a0b176
Raw
History Blame Contribute Delete
29 kB
# postponed evaluation of annotations for forward-compatible type hints
from __future__ import annotations
# JSON serialization utilities used for compact dataset metadata output
import json
# math helpers for safely normalizing numeric values
import math
# timing for tracking model training duration
import time
# dataclass helpers used to define the machine learning context container
from dataclasses import dataclass, field
# filesystem path handling for loading datasets from disk
from pathlib import Path
# Imports the generic Any type used throughout flexible result dictionaries
from typing import Any
# Imports NumPy for numeric type handling, array operations, and metric calculations
import numpy as np
# Imports pandas for tabular dataset loading, inspection, cleaning, and transformation
import pandas as pd
# Imports scikit-learn preprocessing, estimator, metric, splitting, and pipeline components
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression, Ridge, SGDClassifier, SGDRegressor
from sklearn.metrics import (
accuracy_score,
f1_score,
mean_absolute_error,
mean_squared_error,
precision_score,
r2_score,
recall_score,
roc_auc_score,
)
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.svm import LinearSVC
# imports project-wide training limits and the shared reproducibility seed
from config import MAX_TRAIN_ROWS, RANDOM_STATE
# defines the supported classification algorithms exposed by the app
CLASSIFICATION_ALGORITHMS = [
"Logistic Regression",
"Random Forest Classifier",
"SGD Classifier",
"Linear SVM Classifier",
]
# defines the supported regression algorithms exposed by the app
REGRESSION_ALGORITHMS = [
"Ridge Regression",
"Random Forest Regressor",
"SGD Regressor",
]
# combines automatic selection with all supported classification and regression algorithms
ALL_ALGORITHMS = ["Auto"] + CLASSIFICATION_ALGORITHMS + REGRESSION_ALGORITHMS
# converts NumPy and pandas scalar values into JSON-friendly native Python values
def _python_scalar(value: Any) -> Any:
if isinstance(value, (np.integer,)):
return int(value)
if isinstance(value, (np.floating,)):
if math.isnan(float(value)):
return None
return float(value)
if isinstance(value, (np.bool_,)):
return bool(value)
if pd.isna(value):
return None
return value
# collects a small set of unique non-null example values from a pandas Series
def _safe_examples(series: pd.Series, limit: int = 3) -> list[Any]:
values = series.dropna().head(25).tolist()
result: list[Any] = []
for value in values:
value = _python_scalar(value)
if value not in result:
result.append(value)
if len(result) >= limit:
break
return result
# maps pandas dtypes to simplified logical data types used in dataset profiles
def _logical_type(series: pd.Series) -> str:
if pd.api.types.is_bool_dtype(series):
return "boolean"
if pd.api.types.is_integer_dtype(series):
return "integer"
if pd.api.types.is_float_dtype(series):
return "float"
if pd.api.types.is_datetime64_any_dtype(series):
return "datetime"
return "string"
# Copies the dataset and replaces infinite numeric values with missing values
def _clean_frame(df: pd.DataFrame) -> pd.DataFrame:
cleaned = df.copy()
numeric = cleaned.select_dtypes(include=[np.number]).columns
if len(numeric):
cleaned.loc[:, numeric] = cleaned.loc[:, numeric].replace([np.inf, -np.inf], np.nan)
return cleaned
# Loads a dataset from disk using the reader appropriate for its file extension
def _read_dataset(path: Path) -> pd.DataFrame:
suffix = path.suffix.lower()
if suffix == ".csv":
return pd.read_csv(path)
if suffix == ".parquet":
return pd.read_parquet(path)
if suffix == ".json":
return pd.read_json(path)
if suffix == ".jsonl":
return pd.read_json(path, lines=True)
if suffix in {".xlsx", ".xls"}:
return pd.read_excel(path)
raise ValueError(f"Unsupported dataset type: {suffix}")
# Resolves the requested algorithm or selects the default algorithm for the problem type
def _algorithm_for(problem_type: str, algorithm: str) -> str:
if algorithm and algorithm != "Auto":
return algorithm
return "Logistic Regression" if problem_type == "classification" else "Ridge Regression"
# Constructs the configured scikit-learn estimator for classification or regression
def _build_estimator(problem_type: str, algorithm: str):
algorithm = _algorithm_for(problem_type, algorithm)
# Handles estimator construction for supported classification algorithms
if problem_type == "classification":
if algorithm == "Logistic Regression":
return LogisticRegression(max_iter=1200, class_weight="balanced"), algorithm
if algorithm == "Random Forest Classifier":
return RandomForestClassifier(
n_estimators=240,
max_depth=None,
min_samples_leaf=2,
n_jobs=-1,
class_weight="balanced_subsample",
random_state=RANDOM_STATE,
), algorithm
if algorithm == "SGD Classifier":
return SGDClassifier(
loss="log_loss",
alpha=1e-4,
max_iter=1500,
class_weight="balanced",
random_state=RANDOM_STATE,
), algorithm
if algorithm == "Linear SVM Classifier":
return LinearSVC(class_weight="balanced", random_state=RANDOM_STATE), algorithm
raise ValueError(f"{algorithm} is not a classification algorithm.")
# Handles estimator construction for supported regression algorithms
if algorithm == "Ridge Regression":
return Ridge(alpha=1.0, solver="lsqr"), algorithm
if algorithm == "Random Forest Regressor":
return RandomForestRegressor(
n_estimators=240,
min_samples_leaf=2,
n_jobs=-1,
random_state=RANDOM_STATE,
), algorithm
if algorithm == "SGD Regressor":
return SGDRegressor(
loss="squared_error",
penalty="l2",
alpha=1e-4,
max_iter=1500,
random_state=RANDOM_STATE,
), algorithm
raise ValueError(f"{algorithm} is not a regression algorithm.")
# builds preprocessing and estimator steps into a complete scikit-learn pipeline
def _build_pipeline(X: pd.DataFrame, problem_type: str, algorithm: str) -> tuple[Pipeline, str, list[str], list[str]]:
# separates numeric feature columns from categorical feature columns
numeric_columns = X.select_dtypes(include=[np.number]).columns.tolist()
categorical_columns = [c for c in X.columns if c not in numeric_columns]
# builds numeric preprocessing with median imputation and optional feature scaling
numeric_steps: list[tuple[str, Any]] = [("imputer", SimpleImputer(strategy="median"))]
if algorithm not in {"Random Forest Classifier", "Random Forest Regressor"}:
numeric_steps.append(("scaler", StandardScaler()))
numeric_pipeline = Pipeline(numeric_steps)
# builds categorical preprocessing with frequent-value imputation and one-hot encoding
categorical_pipeline = Pipeline(
steps=[
("imputer", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore", min_frequency=2)),
]
)
# Combines numeric and categorical preprocessing into a column-aware transformer
preprocessor = ColumnTransformer(
transformers=[
("num", numeric_pipeline, numeric_columns),
("cat", categorical_pipeline, categorical_columns),
],
remainder="drop",
)
# Instantiates the requested estimator and attaches it after preprocessing
estimator, resolved_algorithm = _build_estimator(problem_type, algorithm)
pipeline = Pipeline([("preprocess", preprocessor), ("model", estimator)])
return pipeline, resolved_algorithm, numeric_columns, categorical_columns
# Extracts the strongest model feature importances or coefficient magnitudes when available
def _feature_importance(pipeline: Pipeline, limit: int = 25) -> list[dict[str, Any]]:
try:
# Retrieves transformed feature names and the fitted estimator from the pipeline
feature_names = pipeline.named_steps["preprocess"].get_feature_names_out()
model = pipeline.named_steps["model"]
# Supports both tree-based feature importances and linear-model coefficients
if hasattr(model, "feature_importances_"):
values = np.asarray(model.feature_importances_)
elif hasattr(model, "coef_"):
coef = np.asarray(model.coef_)
values = np.mean(np.abs(coef), axis=0) if coef.ndim > 1 else np.abs(coef)
else:
return []
# Sorts features by descending importance and limits the returned result size
order = np.argsort(values)[::-1][:limit]
rows = []
for idx in order:
name = str(feature_names[idx]).replace("num__", "").replace("cat__", "")
rows.append({"feature": name, "importance": round(float(values[idx]), 6)})
return rows
except Exception:
return []
# Stores the active dataset, source metadata, and most recent trained model state
@dataclass
class MLContext:
dataframe: pd.DataFrame
source_name: str
last_run: dict[str, Any] | None = None
last_pipeline: Any = field(default=None, repr=False)
# Creates an ML context by loading, optionally truncating, and cleaning a dataset file
@classmethod
def from_path(cls, path: str | Path, max_rows: int = 100000) -> "MLContext":
path = Path(path)
frame = _read_dataset(path)
if max_rows and len(frame) > max_rows:
frame = frame.head(max_rows).copy()
return cls(_clean_frame(frame), path.name)
# Builds a reusable dataset profile containing dimensions, memory usage, schema, and examples
def profile(self) -> dict[str, Any]:
df = self.dataframe
rows = len(df)
schema = []
# Profiles each column individually to capture type, null rate, cardinality, and examples
for name in df.columns:
series = df[name]
schema.append(
{
"name": str(name),
"logical_type": _logical_type(series),
"pandas_dtype": str(series.dtype),
"null_pct": round(float(series.isna().mean() * 100), 2),
"unique_count": int(series.nunique(dropna=True)),
"examples": _safe_examples(series),
}
)
# Returns the assembled dataset-level and column-level profiling information
return {
"source": self.source_name,
"rows": int(rows),
"columns": int(df.shape[1]),
"memory_mb": round(float(df.memory_usage(deep=True).sum() / 1024 / 1024), 3),
"duplicate_rows": int(df.duplicated().sum()),
"column_names": [str(c) for c in df.columns],
"column_schema": schema,
}
# Ranks plausible target columns by low cardinality before falling back to column order
def target_candidates(self) -> list[str]:
if self.dataframe.empty:
return []
columns = [str(c) for c in self.dataframe.columns]
low_cardinality = []
n = max(len(self.dataframe), 1)
for column in columns:
unique = self.dataframe[column].nunique(dropna=True)
if 2 <= unique <= min(50, max(10, int(n * 0.05))):
low_cardinality.append(column)
ordered = []
for column in low_cardinality + list(reversed(columns)):
if column not in ordered:
ordered.append(column)
return ordered
# Resolves whether the selected target should be treated as classification or regression
def infer_problem_type(self, target: str, requested: str = "Auto") -> str:
if target not in self.dataframe.columns:
raise ValueError(f"Target column `{target}` does not exist.")
if requested and requested.lower() in {"classification", "regression"}:
return requested.lower()
# Infers classification from categorical-like targets or sufficiently low target cardinality
y = self.dataframe[target]
unique = y.nunique(dropna=True)
if (
pd.api.types.is_object_dtype(y)
or pd.api.types.is_bool_dtype(y)
or isinstance(y.dtype, pd.CategoricalDtype)
or unique <= min(30, max(10, int(len(y) * 0.02)))
):
return "classification"
return "regression"
# Summarizes the target and recommends algorithms appropriate for the resolved problem type
def modeling_recommendation(self, target: str, requested_problem_type: str = "Auto") -> dict[str, Any]:
problem_type = self.infer_problem_type(target, requested_problem_type)
y = self.dataframe[target]
result = {
"target": target,
"problem_type": problem_type,
"target_nulls": int(y.isna().sum()),
"target_unique": int(y.nunique(dropna=True)),
"rows_available": int(len(self.dataframe)),
"recommended_algorithms": CLASSIFICATION_ALGORITHMS if problem_type == "classification" else REGRESSION_ALGORITHMS,
}
# Adds class distribution and imbalance diagnostics for classification targets
if problem_type == "classification":
counts = y.value_counts(dropna=True).head(15)
result["class_distribution"] = {str(k): int(v) for k, v in counts.items()}
if len(counts) > 1:
result["class_imbalance_ratio"] = round(float(counts.max() / max(counts.min(), 1)), 3)
return result
# Trains and evaluates one candidate model using a deterministic holdout workflow
def train_candidate(
self,
target: str,
algorithm: str = "Auto",
requested_problem_type: str = "Auto",
test_size: float = 0.2,
max_rows: int = MAX_TRAIN_ROWS,
) -> dict[str, Any]:
# Validates that the requested target column exists before preparing training data
if target not in self.dataframe.columns:
raise ValueError(f"Target column `{target}` does not exist.")
# Removes rows with missing targets and enforces a minimum usable sample size
frame = self.dataframe.dropna(subset=[target]).copy()
if len(frame) < 20:
raise ValueError("At least 20 rows with a non-null target are required.")
# Downsamples oversized datasets to the configured training-row limit reproducibly
if max_rows and len(frame) > max_rows:
frame = frame.sample(n=max_rows, random_state=RANDOM_STATE)
# Separates the target from feature columns after resolving the supervised learning problem type
problem_type = self.infer_problem_type(target, requested_problem_type)
X = frame.drop(columns=[target])
y = frame[target]
# Validates that the feature matrix and target are usable for the selected problem type
if X.shape[1] == 0:
raise ValueError("The dataset needs at least one feature column besides the target.")
if problem_type == "classification" and y.nunique(dropna=True) < 2:
raise ValueError("Classification requires at least two target classes.")
# Converts regression targets to numeric values and removes rows that cannot be converted
if problem_type == "regression" and not pd.api.types.is_numeric_dtype(y):
y = pd.to_numeric(y, errors="coerce")
valid = y.notna()
X, y = X.loc[valid], y.loc[valid]
if len(y) < 20:
raise ValueError("Regression target could not be converted to enough numeric values.")
# Validates that the selected algorithm belongs to the resolved problem family
resolved_algorithm = _algorithm_for(problem_type, algorithm)
if problem_type == "classification" and resolved_algorithm not in CLASSIFICATION_ALGORITHMS:
raise ValueError(f"Select a classification algorithm for target `{target}`.")
if problem_type == "regression" and resolved_algorithm not in REGRESSION_ALGORITHMS:
raise ValueError(f"Select a regression algorithm for target `{target}`.")
# Enables stratified splitting when classification classes have enough examples
stratify = None
if problem_type == "classification":
counts = y.value_counts()
if len(counts) > 1 and counts.min() >= 2:
stratify = y
# Splits the data into reproducible training and holdout evaluation partitions
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=float(test_size),
random_state=RANDOM_STATE,
stratify=stratify,
)
# Builds preprocessing and modeling steps using only the training feature schema
pipeline, resolved_algorithm, numeric_columns, categorical_columns = _build_pipeline(
X_train, problem_type, resolved_algorithm
)
# Fits the pipeline while measuring training time and then generates holdout predictions
start = time.perf_counter()
pipeline.fit(X_train, y_train)
fit_seconds = time.perf_counter() - start
predictions = pipeline.predict(X_test)
# Calculates task-appropriate evaluation metrics from holdout predictions
metrics: dict[str, float] = {}
# Computes weighted classification metrics and binary ROC AUC when probabilities are available
if problem_type == "classification":
metrics = {
"accuracy": float(accuracy_score(y_test, predictions)),
"precision_weighted": float(precision_score(y_test, predictions, average="weighted", zero_division=0)),
"recall_weighted": float(recall_score(y_test, predictions, average="weighted", zero_division=0)),
"f1_weighted": float(f1_score(y_test, predictions, average="weighted", zero_division=0)),
}
if y.nunique() == 2 and hasattr(pipeline, "predict_proba"):
try:
probabilities = pipeline.predict_proba(X_test)[:, 1]
classes = list(pipeline.named_steps["model"].classes_)
positive = classes[1]
binary_y = (y_test == positive).astype(int)
metrics["roc_auc"] = float(roc_auc_score(binary_y, probabilities))
except Exception:
pass
else:
# Computes regression error metrics and coefficient of determination
rmse = float(np.sqrt(mean_squared_error(y_test, predictions)))
metrics = {
"mae": float(mean_absolute_error(y_test, predictions)),
"rmse": rmse,
"r2": float(r2_score(y_test, predictions)),
}
# Rounds metrics for stable display and derives model feature importance information
metrics = {k: round(float(v), 6) for k, v in metrics.items()}
importance = _feature_importance(pipeline)
# Packages training metadata, feature groups, timings, metrics, and importances into one result
result = {
"status": "trained",
"source": self.source_name,
"target": target,
"problem_type": problem_type,
"algorithm": resolved_algorithm,
"rows_used": int(len(frame)),
"train_rows": int(len(X_train)),
"test_rows": int(len(X_test)),
"feature_columns": int(X.shape[1]),
"numeric_features": numeric_columns,
"categorical_features": categorical_columns,
"fit_seconds": round(float(fit_seconds), 3),
"metrics": metrics,
"feature_importance": importance,
}
# Stores the fitted pipeline and its result on the context for later reuse or export
self.last_pipeline = pipeline
self.last_run = result
return result
# Benchmarks a compact set of supported algorithms for the selected target
def compare_algorithms(
self,
target: str,
requested_problem_type: str = "Auto",
test_size: float = 0.2,
max_rows: int = MAX_TRAIN_ROWS,
) -> dict[str, Any]:
# Selects the comparison candidates based on the inferred machine learning problem type
problem_type = self.infer_problem_type(target, requested_problem_type)
algorithms = CLASSIFICATION_ALGORITHMS[:3] if problem_type == "classification" else REGRESSION_ALGORITHMS
runs = []
# Trains each candidate independently and captures either its score or its error
for algorithm in algorithms:
try:
result = self.train_candidate(target, algorithm, problem_type, test_size, max_rows)
score_name = "roc_auc" if "roc_auc" in result["metrics"] else ("f1_weighted" if problem_type == "classification" else "r2")
score = result["metrics"].get(score_name)
runs.append(
{
"algorithm": algorithm,
"problem_type": problem_type,
"primary_metric": score_name,
"score": score,
"fit_seconds": result["fit_seconds"],
**result["metrics"],
}
)
except Exception as exc:
runs.append({"algorithm": algorithm, "problem_type": problem_type, "error": str(exc)})
# Chooses the highest-scoring successful baseline result when at least one run completed
valid = [row for row in runs if row.get("score") is not None]
if valid:
valid.sort(key=lambda row: float(row["score"]), reverse=True)
best = valid[0]["algorithm"]
else:
best = None
# Returns the comparison summary together with every individual baseline run
return {"problem_type": problem_type, "target": target, "best_algorithm": best, "results": runs}
# Generates standalone reproducible scikit-learn pipeline source code for the current setup
def generate_pipeline_code(
self,
target: str,
algorithm: str = "Auto",
requested_problem_type: str = "Auto",
test_size: float = 0.2,
) -> str:
# Resolves the final problem type and algorithm before assembling generated code fragments
problem_type = self.infer_problem_type(target, requested_problem_type)
algorithm = _algorithm_for(problem_type, algorithm)
# Initializes algorithm-specific import, estimator, and metric code fragments
estimator_import = ""
estimator_code = ""
metric_code = ""
# Selects generated estimator and evaluation code for each supported algorithm
if algorithm == "Logistic Regression":
estimator_import = "from sklearn.linear_model import LogisticRegression"
estimator_code = 'LogisticRegression(max_iter=1200, class_weight="balanced")'
metric_code = '''print("accuracy:", accuracy_score(y_test, pred))\nprint("f1_weighted:", f1_score(y_test, pred, average="weighted"))'''
elif algorithm == "Random Forest Classifier":
estimator_import = "from sklearn.ensemble import RandomForestClassifier"
estimator_code = 'RandomForestClassifier(n_estimators=240, min_samples_leaf=2, class_weight="balanced_subsample", n_jobs=-1, random_state=42)'
metric_code = '''print("accuracy:", accuracy_score(y_test, pred))\nprint("f1_weighted:", f1_score(y_test, pred, average="weighted"))'''
elif algorithm == "SGD Classifier":
estimator_import = "from sklearn.linear_model import SGDClassifier"
estimator_code = 'SGDClassifier(loss="log_loss", class_weight="balanced", max_iter=1500, random_state=42)'
metric_code = '''print("accuracy:", accuracy_score(y_test, pred))\nprint("f1_weighted:", f1_score(y_test, pred, average="weighted"))'''
elif algorithm == "Linear SVM Classifier":
estimator_import = "from sklearn.svm import LinearSVC"
estimator_code = 'LinearSVC(class_weight="balanced", random_state=42)'
metric_code = '''print("accuracy:", accuracy_score(y_test, pred))\nprint("f1_weighted:", f1_score(y_test, pred, average="weighted"))'''
elif algorithm == "Ridge Regression":
estimator_import = "from sklearn.linear_model import Ridge"
estimator_code = 'Ridge(alpha=1.0, solver="lsqr")'
metric_code = '''rmse = mean_squared_error(y_test, pred) ** 0.5\nprint("mae:", mean_absolute_error(y_test, pred))\nprint("rmse:", rmse)\nprint("r2:", r2_score(y_test, pred))'''
elif algorithm == "Random Forest Regressor":
estimator_import = "from sklearn.ensemble import RandomForestRegressor"
estimator_code = 'RandomForestRegressor(n_estimators=240, min_samples_leaf=2, n_jobs=-1, random_state=42)'
metric_code = '''rmse = mean_squared_error(y_test, pred) ** 0.5\nprint("mae:", mean_absolute_error(y_test, pred))\nprint("rmse:", rmse)\nprint("r2:", r2_score(y_test, pred))'''
elif algorithm == "SGD Regressor":
estimator_import = "from sklearn.linear_model import SGDRegressor"
estimator_code = 'SGDRegressor(max_iter=1500, random_state=42)'
metric_code = '''rmse = mean_squared_error(y_test, pred) ** 0.5\nprint("mae:", mean_absolute_error(y_test, pred))\nprint("rmse:", rmse)\nprint("r2:", r2_score(y_test, pred))'''
else:
raise ValueError(f"Unsupported algorithm: {algorithm}")
# Selects the metric imports required by the resolved supervised learning problem type
metric_import = (
"from sklearn.metrics import accuracy_score, f1_score"
if problem_type == "classification"
else "from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score"
)
# Generates stratification setup code for classification while disabling it for regression
stratify_setup = (
'stratify_target = y if y.value_counts().min() >= 2 else None'
if problem_type == "classification"
else 'stratify_target = None'
)
# Returns the assembled standalone training script without executing the generated source
return f'''import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
{estimator_import}
{metric_import}
# Replace with your production data source.
df = pd.read_csv("dataset.csv")
TARGET = {target!r}
# Drop rows where the supervised-learning target is missing.
df = df.dropna(subset=[TARGET]).copy()
X = df.drop(columns=[TARGET])
y = df[TARGET]
numeric_features = X.select_dtypes(include="number").columns.tolist()
categorical_features = [c for c in X.columns if c not in numeric_features]
numeric_pipeline = Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
])
categorical_pipeline = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore", min_frequency=2)),
])
preprocessor = ColumnTransformer([
("num", numeric_pipeline, numeric_features),
("cat", categorical_pipeline, categorical_features),
])
model = {estimator_code}
pipeline = Pipeline([
("preprocess", preprocessor),
("model", model),
])
{stratify_setup}
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size={float(test_size):.2f},
random_state=42,
stratify=stratify_target,
)
pipeline.fit(X_train, y_train)
pred = pipeline.predict(X_test)
{metric_code}
'''
# Produces a reduced dataset profile suitable for compact tool or agent responses
def compact_profile(self) -> dict[str, Any]:
profile = self.profile()
return {
"source": profile["source"],
"rows": profile["rows"],
"columns": profile["columns"],
"duplicate_rows": profile["duplicate_rows"],
"schema": profile["column_schema"],
}
# Serializes the compact dataset profile as formatted JSON text
def to_json(self) -> str:
return json.dumps(self.compact_profile(), indent=2, default=str)