def tree_playground(max_depth=5, min_samples_split=2, min_samples_leaf=1,
dataset='moons', noise=0.25, n_samples=300):
"""
Complete interactive playground for decision trees.
Students can adjust all parameters and see results immediately.
"""
# Generate dataset
if dataset == 'moons':
X, y = make_moons(n_samples=n_samples, noise=noise, random_state=42)
elif dataset == 'circles':
from sklearn.datasets import make_circles
X, y = make_circles(n_samples=n_samples, noise=noise, factor=0.5, random_state=42)
else: # xor
np.random.seed(42)
n_per = n_samples // 4
X = np.vstack([
np.random.randn(n_per, 2) * noise + [0, 0],
np.random.randn(n_per, 2) * noise + [1, 1],
np.random.randn(n_per, 2) * noise + [0, 1],
np.random.randn(n_per, 2) * noise + [1, 0]
])
y = np.array([0]*n_per*2 + [1]*n_per*2)
# Split and train
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
clf = DecisionTreeClassifier(
max_depth=max_depth,
min_samples_split=min_samples_split,
min_samples_leaf=min_samples_leaf,
random_state=42
)
clf.fit(X_train, y_train)
train_acc = clf.score(X_train, y_train)
test_acc = clf.score(X_test, y_test)
gap = train_acc - test_acc
# Create mesh
h = 0.02
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# Create figure
fig = plt.figure(figsize=(16, 6))
# Left: Decision boundary
ax1 = plt.subplot(1, 2, 1)
ax1.contourf(xx, yy, Z, alpha=0.4, cmap='RdYlBu')
ax1.scatter(X_train[y_train==0, 0], X_train[y_train==0, 1],
c='blue', s=50, edgecolors='black', alpha=0.7, label='Class 0 (train)')
ax1.scatter(X_train[y_train==1, 0], X_train[y_train==1, 1],
c='red', s=50, edgecolors='black', alpha=0.7, label='Class 1 (train)')
ax1.scatter(X_test[y_test==0, 0], X_test[y_test==0, 1],
c='blue', s=120, marker='s', edgecolors='black', linewidth=2, alpha=0.9, label='Class 0 (test)')
ax1.scatter(X_test[y_test==1, 0], X_test[y_test==1, 1],
c='red', s=120, marker='s', edgecolors='black', linewidth=2, alpha=0.9, label='Class 1 (test)')
# Status color
if gap < 0.05:
status = "Good Balance โ
"
color = 'green'
elif gap < 0.15:
status = "Slight Overfitting โ ๏ธ"
color = 'orange'
else:
status = "Severe Overfitting โ"
color = 'red'
ax1.set_xlabel('Feature 1', fontsize=12)
ax1.set_ylabel('Feature 2', fontsize=12)
ax1.set_title(f'Decision Boundary\nTrain: {train_acc:.3f} | Test: {test_acc:.3f} | Gap: {gap:.3f}\n{status}',
fontsize=12, fontweight='bold', color=color)
ax1.legend(loc='best', fontsize=9)
ax1.grid(True, alpha=0.3)
# Right: Tree structure (simplified)
ax2 = plt.subplot(1, 2, 2)
# Get tree statistics
n_nodes = clf.tree_.node_count
n_leaves = clf.get_n_leaves()
actual_depth = clf.get_depth()
# Display tree info as text
info_text = f"""
๐ณ TREE STATISTICS
{'='*40}
Structure:
โข Total Nodes: {n_nodes}
โข Leaf Nodes: {n_leaves}
โข Actual Depth: {actual_depth}
Hyperparameters:
โข max_depth: {max_depth}
โข min_samples_split: {min_samples_split}
โข min_samples_leaf: {min_samples_leaf}
Performance:
โข Training Accuracy: {train_acc:.4f}
โข Test Accuracy: {test_acc:.4f}
โข Train-Test Gap: {gap:.4f}
Dataset:
โข Type: {dataset.upper()}
โข Training Samples: {len(X_train)}
โข Test Samples: {len(X_test)}
โข Noise Level: {noise:.2f}
Status: {status}
"""
ax2.text(0.1, 0.5, info_text, transform=ax2.transAxes,
fontsize=11, verticalalignment='center',
fontfamily='monospace',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
ax2.axis('off')
plt.tight_layout()
plt.show()
# Recommendations
print("\n" + "="*80)
print("๐ก RECOMMENDATIONS")
print("="*80)
if gap > 0.15:
print("\nโ ๏ธ High overfitting detected! Try these:")
print(f" 1. Decrease max_depth (current: {max_depth}) โ try {max(1, max_depth-2)}")
print(f" 2. Increase min_samples_split (current: {min_samples_split}) โ try {min_samples_split + 10}")
print(f" 3. Increase min_samples_leaf (current: {min_samples_leaf}) โ try {min_samples_leaf + 5}")
elif gap < 0.02 and train_acc < 0.85:
print("\n๐ Possible underfitting! Try these:")
print(f" 1. Increase max_depth (current: {max_depth}) โ try {max_depth + 2}")
print(f" 2. Decrease min_samples_split (current: {min_samples_split}) โ try {max(2, min_samples_split - 5)}")
print(f" 3. Decrease min_samples_leaf (current: {min_samples_leaf}) โ try {max(1, min_samples_leaf - 2)}")
else:
print("\nโ
Model looks good! Current hyperparameters are reasonable.")
print(" You could still fine-tune further if needed.")
print("\n๐ญ General Tips:")
print(" โข Start with shallow trees (depth 3-5) and increase if needed")
print(" โข Watch the train-test gap more than absolute accuracy")
print(" โข Use cross-validation for more reliable estimates")
print(" โข More data helps reduce overfitting")
print("="*80)
# Create interactive widget with all controls
print("\n๐ฎ INTERACTIVE DECISION TREE PLAYGROUND")
print("Use the sliders below to explore how different hyperparameters affect the tree!\n")
interact(tree_playground,
max_depth=IntSlider(value=5, min=1, max=15, step=1,
description='Max Depth:',
style={'description_width': '150px'}),
min_samples_split=IntSlider(value=2, min=2, max=50, step=2,
description='Min Samples Split:',
style={'description_width': '150px'}),
min_samples_leaf=IntSlider(value=1, min=1, max=20, step=1,
description='Min Samples Leaf:',
style={'description_width': '150px'}),
dataset=Dropdown(options=['moons', 'circles', 'xor'], value='moons',
description='Dataset:',
style={'description_width': '150px'}),
noise=FloatSlider(value=0.25, min=0.1, max=0.5, step=0.05,
description='Noise Level:',
style={'description_width': '150px'}),
n_samples=IntSlider(value=300, min=100, max=500, step=50,
description='Sample Size:',
style={'description_width': '150px'}));