Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Interactive Decision Tree Classifier for the Iris dataset using scikit-learn + Gradio. | |
| Features | |
| - Visualizes the trained decision tree (matplotlib.plot_tree). | |
| - Lets users tweak hyperparameters (criterion, max_depth, min_samples_split). | |
| - Shows accuracy and a full classification report. | |
| - Modular code organization for clarity and reuse. | |
| """ | |
| from __future__ import annotations | |
| import io | |
| from dataclasses import dataclass | |
| from typing import Optional, Tuple | |
| import numpy as np | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| from sklearn import datasets | |
| from sklearn.model_selection import train_test_split | |
| from sklearn.tree import DecisionTreeClassifier, plot_tree | |
| from sklearn.metrics import accuracy_score, classification_report | |
| # ----------------------------- | |
| # Data & Config | |
| # ----------------------------- | |
| class TrainConfig: | |
| criterion: str = "gini" # "gini", "entropy", or "log_loss" | |
| max_depth: Optional[int] = None # None means unlimited depth | |
| min_samples_split: int = 2 # integer >= 2 | |
| test_size: float = 0.25 # test split fraction | |
| random_state: int = 42 # reproducibility | |
| def load_iris() -> Tuple[pd.DataFrame, pd.Series, list[str]]: | |
| """Load the Iris dataset and return X (DataFrame), y (Series), and class names.""" | |
| iris = datasets.load_iris() | |
| X = pd.DataFrame(iris.data, columns=iris.feature_names) | |
| y = pd.Series(iris.target, name="target") | |
| class_names = iris.target_names.tolist() | |
| return X, y, class_names | |
| # ----------------------------- | |
| # Model Pipeline | |
| # ----------------------------- | |
| def build_model(cfg: TrainConfig) -> DecisionTreeClassifier: | |
| """Instantiate a DecisionTreeClassifier from a config.""" | |
| model = DecisionTreeClassifier( | |
| criterion=cfg.criterion, | |
| max_depth=cfg.max_depth, | |
| min_samples_split=cfg.min_samples_split, | |
| random_state=cfg.random_state, | |
| ) | |
| return model | |
| def train_and_evaluate( | |
| cfg: TrainConfig, | |
| ) -> Tuple[DecisionTreeClassifier, float, str, pd.DataFrame, pd.Series, list[str]]: | |
| """ | |
| Train a decision tree and compute accuracy + classification report. | |
| Returns: | |
| model, accuracy, report (str), X_test (DataFrame), y_test (Series), class_names (list) | |
| """ | |
| X, y, class_names = load_iris() | |
| X_train, X_test, y_train, y_test = train_test_split( | |
| X, y, test_size=cfg.test_size, random_state=cfg.random_state, stratify=y | |
| ) | |
| model = build_model(cfg) | |
| model.fit(X_train, y_train) | |
| y_pred = model.predict(X_test) | |
| acc = accuracy_score(y_test, y_pred) | |
| report = classification_report(y_test, y_pred, target_names=class_names) | |
| return model, acc, report, X_test, y_test, class_names | |
| # ----------------------------- | |
| # Visualization | |
| # ----------------------------- | |
| def render_tree_image( | |
| model: DecisionTreeClassifier, | |
| feature_names: list[str], | |
| class_names: list[str], | |
| dpi: int = 120, | |
| ) -> np.ndarray: | |
| """ | |
| Render a matplotlib plot_tree to a PNG image array suitable for display in Gradio. | |
| """ | |
| fig, ax = plt.subplots(figsize=(12, 8), dpi=dpi) | |
| plot_tree( | |
| model, | |
| feature_names=feature_names, | |
| class_names=class_names, | |
| filled=True, | |
| rounded=True, | |
| impurity=True, | |
| proportion=True, | |
| fontsize=8, | |
| ax=ax, | |
| ) | |
| buf = io.BytesIO() | |
| fig.tight_layout() | |
| fig.savefig(buf, format="png") | |
| plt.close(fig) | |
| buf.seek(0) | |
| # Convert buffer to numpy array for gr.Image | |
| import PIL.Image as Image | |
| img = Image.open(buf) | |
| return np.array(img) | |
| # ----------------------------- | |
| # Gradio App | |
| # ----------------------------- | |
| def inference( | |
| criterion: str, | |
| max_depth_enabled: bool, | |
| max_depth_val: int, | |
| min_samples_split: int, | |
| test_size: float, | |
| random_state: int, | |
| ) -> tuple[np.ndarray, str, str]: | |
| """ | |
| Gradio handler: trains, evaluates, and returns (tree_image, accuracy_text, classification_report). | |
| """ | |
| cfg = TrainConfig( | |
| criterion=criterion, | |
| max_depth=(max_depth_val if max_depth_enabled else None), | |
| min_samples_split=int(min_samples_split), | |
| test_size=float(test_size), | |
| random_state=int(random_state), | |
| ) | |
| model, acc, report, X_test, y_test, class_names = train_and_evaluate(cfg) | |
| tree_img = render_tree_image(model, feature_names=X_test.columns.tolist(), class_names=class_names) | |
| acc_text = f"Accuracy: {acc:.4f}\n\nTest Size: {cfg.test_size} | Random State: {cfg.random_state}\nParams -> criterion={cfg.criterion}, max_depth={cfg.max_depth}, min_samples_split={cfg.min_samples_split}" | |
| return tree_img, acc_text, report | |
| def build_interface(): | |
| import gradio as gr | |
| with gr.Blocks(title="Iris Decision Tree (scikit-learn)", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| """ | |
| # 🌸 Iris Decision Tree Classifier | |
| Tweak hyperparameters and see how the decision tree changes. View accuracy and a full classification report. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| criterion = gr.Dropdown( | |
| label="Criterion", | |
| choices=["gini", "entropy", "log_loss"], | |
| value="gini", | |
| info="Split quality metric" | |
| ) | |
| max_depth_enabled = gr.Checkbox( | |
| label="Limit max_depth?", | |
| value=False | |
| ) | |
| max_depth_val = gr.Slider( | |
| label="max_depth (if enabled)", | |
| minimum=1, | |
| maximum=10, | |
| step=1, | |
| value=3 | |
| ) | |
| min_samples_split = gr.Slider( | |
| label="min_samples_split", | |
| minimum=2, | |
| maximum=20, | |
| step=1, | |
| value=2 | |
| ) | |
| test_size = gr.Slider( | |
| label="test_size (fraction for test)", | |
| minimum=0.1, | |
| maximum=0.5, | |
| step=0.05, | |
| value=0.25 | |
| ) | |
| random_state = gr.Slider( | |
| label="random_state", | |
| minimum=0, | |
| maximum=9999, | |
| step=1, | |
| value=42 | |
| ) | |
| run_btn = gr.Button("Train & Evaluate", variant="primary") | |
| with gr.Column(scale=2): | |
| tree_img = gr.Image(label="Decision Tree", interactive=False) | |
| accuracy_box = gr.Textbox(label="Accuracy & Parameters", interactive=False, lines=4) | |
| report_box = gr.Textbox(label="Classification Report", interactive=False, lines=12) | |
| # Wire events | |
| inputs = [criterion, max_depth_enabled, max_depth_val, min_samples_split, test_size, random_state] | |
| outputs = [tree_img, accuracy_box, report_box] | |
| # Run once on load | |
| demo.load(fn=inference, inputs=inputs, outputs=outputs) | |
| # Run on button click | |
| run_btn.click(fn=inference, inputs=inputs, outputs=outputs) | |
| gr.Markdown( | |
| "Tip: Uncheck **Limit max_depth?** to let the tree grow fully (may overfit)." | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| demo = build_interface() | |
| demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False) | |