Spaces:
Sleeping
Sleeping
| import numpy as np | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| from sklearn.datasets import load_iris | |
| from sklearn.model_selection import train_test_split | |
| from sklearn.tree import DecisionTreeClassifier | |
| from sklearn.metrics import ( | |
| accuracy_score, | |
| precision_score, | |
| recall_score, | |
| f1_score, | |
| confusion_matrix, | |
| classification_report | |
| ) | |
| import gradio as gr | |
| # -------- Data -------- | |
| iris = load_iris() | |
| X_full = pd.DataFrame(iris.data, columns=iris.feature_names) | |
| y_full = pd.Series(iris.target, name="target") | |
| class_names = iris.target_names | |
| FEATURE_CHOICES = list(X_full.columns) | |
| # -------- Plot helpers -------- | |
| def plot_confusion_matrix(cm, class_names): | |
| fig, ax = plt.subplots(figsize=(5, 4), dpi=120) | |
| im = ax.imshow(cm, interpolation="nearest") | |
| ax.figure.colorbar(im, ax=ax) | |
| ax.set( | |
| xticks=np.arange(cm.shape[1]), | |
| yticks=np.arange(cm.shape[0]), | |
| xticklabels=class_names, | |
| yticklabels=class_names, | |
| ylabel="True label", | |
| xlabel="Predicted label", | |
| title="Confusion Matrix", | |
| ) | |
| # Show counts on cells | |
| thresh = cm.max() / 2.0 | |
| for i in range(cm.shape[0]): | |
| for j in range(cm.shape[1]): | |
| ax.text( | |
| j, i, format(cm[i, j], "d"), | |
| ha="center", va="center", | |
| color="white" if cm[i, j] > thresh else "black" | |
| ) | |
| fig.tight_layout() | |
| return fig | |
| def plot_feature_importances(model, feature_names): | |
| importances = model.feature_importances_ | |
| order = np.argsort(importances)[::-1] | |
| fig, ax = plt.subplots(figsize=(6, 4), dpi=120) | |
| ax.bar(range(len(importances)), importances[order]) | |
| ax.set_xticks(range(len(importances))) | |
| ax.set_xticklabels([feature_names[i] for i in order], rotation=30, ha="right") | |
| ax.set_ylabel("Importance") | |
| ax.set_title("Feature Importances") | |
| fig.tight_layout() | |
| return fig | |
| def plot_decision_regions_2d(model, X, y, feature_x_name, feature_y_name, class_names): | |
| # X: dataframe with exactly two columns (selected features) | |
| # Create a mesh | |
| x_min, x_max = X.iloc[:, 0].min() - 0.5, X.iloc[:, 0].max() + 0.5 | |
| y_min, y_max = X.iloc[:, 1].min() - 0.5, X.iloc[:, 1].max() + 0.5 | |
| xx, yy = np.meshgrid( | |
| np.linspace(x_min, x_max, 300), | |
| np.linspace(y_min, y_max, 300) | |
| ) | |
| grid = np.c_[xx.ravel(), yy.ravel()] | |
| Z = model.predict(grid).reshape(xx.shape) | |
| fig, ax = plt.subplots(figsize=(6, 5), dpi=120) | |
| ax.contourf(xx, yy, Z, alpha=0.2) | |
| # Scatter original points | |
| for idx, cname in enumerate(class_names): | |
| mask = (y == idx) | |
| ax.scatter( | |
| X.loc[mask, feature_x_name], | |
| X.loc[mask, feature_y_name], | |
| label=cname, s=24 | |
| ) | |
| ax.set_xlabel(feature_x_name) | |
| ax.set_ylabel(feature_y_name) | |
| ax.set_title("Decision Regions (2 features)") | |
| ax.legend(loc="upper right", fontsize=8) | |
| fig.tight_layout() | |
| return fig | |
| # -------- Core training + evaluation -------- | |
| def run_decision_tree( | |
| test_size, | |
| random_state, | |
| criterion, | |
| splitter, | |
| unlimited_depth, | |
| max_depth, | |
| min_samples_split, | |
| min_samples_leaf, | |
| max_features, | |
| class_weight_mode, | |
| feature_x_name, | |
| feature_y_name | |
| ): | |
| # Map UI values to sklearn-friendly options | |
| md = None if unlimited_depth else int(max_depth) | |
| if max_features == "None": | |
| mf = None | |
| elif max_features == "auto/sqrt": | |
| mf = "sqrt" | |
| elif max_features == "log2": | |
| mf = "log2" | |
| else: | |
| mf = None | |
| cw = None if class_weight_mode == "None" else "balanced" | |
| # Train / Test split | |
| X_train, X_test, y_train, y_test = train_test_split( | |
| X_full, y_full, | |
| test_size=float(test_size), | |
| random_state=int(random_state), | |
| stratify=y_full | |
| ) | |
| # Model | |
| clf = DecisionTreeClassifier( | |
| criterion=criterion, | |
| splitter=splitter, | |
| max_depth=md, | |
| min_samples_split=int(min_samples_split), | |
| min_samples_leaf=int(min_samples_leaf), | |
| max_features=mf, | |
| class_weight=cw, | |
| random_state=int(random_state) | |
| ) | |
| clf.fit(X_train, y_train) | |
| # Predictions & metrics | |
| y_pred = clf.predict(X_test) | |
| acc = accuracy_score(y_test, y_pred) | |
| prec = precision_score(y_test, y_pred, average="macro", zero_division=0) | |
| rec = recall_score(y_test, y_pred, average="macro", zero_division=0) | |
| f1 = f1_score(y_test, y_pred, average="macro", zero_division=0) | |
| report = classification_report(y_test, y_pred, target_names=class_names, zero_division=0) | |
| # Confusion matrix | |
| cm = confusion_matrix(y_test, y_pred) | |
| cm_fig = plot_confusion_matrix(cm, class_names) | |
| # Feature importances | |
| fi_fig = plot_feature_importances(clf, X_full.columns) | |
| # Decision boundary for 2 chosen features | |
| # Refit a new tree **on those two features only** to plot clear regions (same hyperparams) | |
| if feature_x_name == feature_y_name: | |
| # If same feature accidentally chosen, pick a safe default distinct pair | |
| feature_x_name, feature_y_name = FEATURE_CHOICES[0], FEATURE_CHOICES[1] | |
| two_feat_cols = [feature_x_name, feature_y_name] | |
| X2_train = X_train[two_feat_cols] | |
| X2_test = X_test[two_feat_cols] | |
| clf2 = DecisionTreeClassifier( | |
| criterion=criterion, | |
| splitter=splitter, | |
| max_depth=md, | |
| min_samples_split=int(min_samples_split), | |
| min_samples_leaf=int(min_samples_leaf), | |
| max_features=None, # force 2D features for plotting | |
| class_weight=cw, | |
| random_state=int(random_state) | |
| ) | |
| clf2.fit(X2_train, y_train) | |
| boundary_fig = plot_decision_regions_2d( | |
| clf2, | |
| pd.concat([X2_train, X2_test], axis=0), | |
| pd.concat([y_train, y_test], axis=0).values, | |
| feature_x_name, feature_y_name, class_names | |
| ) | |
| # Make a small metrics dataframe for display | |
| metrics_df = pd.DataFrame( | |
| [{"accuracy": acc, "precision_macro": prec, "recall_macro": rec, "f1_macro": f1}] | |
| ) | |
| # Classification report as preformatted text | |
| report_text = f"```\n{report}\n```" | |
| return ( | |
| metrics_df, | |
| report_text, | |
| cm_fig, | |
| fi_fig, | |
| boundary_fig | |
| ) | |
| # -------- Gradio UI -------- | |
| with gr.Blocks(title="Iris Decision Tree Explorer") as demo: | |
| gr.Markdown( | |
| """ | |
| # 🌸 Iris Decision Tree Explorer | |
| Train a Decision Tree on the classic Iris dataset. Tweak hyperparameters on the left, then inspect metrics and plots on the right. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Hyperparameters") | |
| criterion = gr.Dropdown( | |
| label="criterion", | |
| choices=["gini", "entropy", "log_loss"], | |
| value="gini" | |
| ) | |
| splitter = gr.Dropdown( | |
| label="splitter", | |
| choices=["best", "random"], | |
| value="best" | |
| ) | |
| unlimited_depth = gr.Checkbox( | |
| label="Unlimited depth (ignore max_depth)", | |
| value=True | |
| ) | |
| max_depth = gr.Slider( | |
| label="max_depth (used only if Unlimited depth = False)", | |
| minimum=1, maximum=30, step=1, value=5 | |
| ) | |
| min_samples_split = gr.Slider( | |
| label="min_samples_split", | |
| minimum=2, maximum=20, step=1, value=2 | |
| ) | |
| min_samples_leaf = gr.Slider( | |
| label="min_samples_leaf", | |
| minimum=1, maximum=20, step=1, value=1 | |
| ) | |
| max_features = gr.Dropdown( | |
| label="max_features", | |
| choices=["None", "auto/sqrt", "log2"], | |
| value="None" | |
| ) | |
| class_weight_mode = gr.Dropdown( | |
| label="class_weight", | |
| choices=["None", "balanced"], | |
| value="None" | |
| ) | |
| gr.Markdown("### Data & Reproducibility") | |
| test_size = gr.Slider( | |
| label="test_size", | |
| minimum=0.1, maximum=0.5, step=0.05, value=0.2 | |
| ) | |
| random_state = gr.Number( | |
| label="random_state", | |
| value=42, precision=0 | |
| ) | |
| gr.Markdown("### Decision Region (2D) Features") | |
| feature_x_name = gr.Dropdown( | |
| label="X-axis feature", | |
| choices=FEATURE_CHOICES, | |
| value=FEATURE_CHOICES[0] | |
| ) | |
| feature_y_name = gr.Dropdown( | |
| label="Y-axis feature", | |
| choices=FEATURE_CHOICES, | |
| value=FEATURE_CHOICES[1] | |
| ) | |
| run_btn = gr.Button("Train & Evaluate", variant="primary") | |
| with gr.Column(scale=2): | |
| gr.Markdown("### Results") | |
| metrics_df = gr.Dataframe( | |
| label="Metrics (test set)", | |
| interactive=False, | |
| headers=["accuracy", "precision_macro", "recall_macro", "f1_macro"] | |
| ) | |
| report_md = gr.Markdown(label="Classification Report") | |
| with gr.Row(): | |
| cm_plot = gr.Plot(label="Confusion Matrix") | |
| fi_plot = gr.Plot(label="Feature Importances") | |
| boundary_plot = gr.Plot(label="Decision Regions (2 features)") | |
| run_btn.click( | |
| fn=run_decision_tree, | |
| inputs=[ | |
| test_size, random_state, | |
| criterion, splitter, unlimited_depth, max_depth, | |
| min_samples_split, min_samples_leaf, max_features, class_weight_mode, | |
| feature_x_name, feature_y_name | |
| ], | |
| outputs=[metrics_df, report_md, cm_plot, fi_plot, boundary_plot] | |
| ) | |
| if __name__ == "__main__": | |
| # You can set share=True if you want a public link when running locally | |
| demo.launch() | |