Decision Tree (dt) Classifier
Data¶
We load training areas from
data/training_areas_ml.geojson.Spectral band values (Green, Red, NIR) are simulated from class-specific reflectance profiles.
The binary target is already encoded:
ml_label = 1(Corn/Maize) vsml_label = 0(Non-Corn).We normalize the features to [0, 1] and split into training and testing sets.
import json
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
OUTPUT_DIR = 'outputs'
os.makedirs(OUTPUT_DIR, exist_ok=True)
# Load training areas
with open('data/training_areas_ml.geojson') as f:
gj = json.load(f)
# Spectral profiles (Green, Red, NIR) per original land cover class
SPECTRAL_PROFILES = {
0: {'mean': [0.12, 0.08, 0.35], 'std': [0.02, 0.02, 0.04]}, # Rice Paddies
1: {'mean': [0.16, 0.07, 0.55], 'std': [0.03, 0.02, 0.05]}, # Corn / Maize
2: {'mean': [0.10, 0.05, 0.65], 'std': [0.02, 0.01, 0.05]}, # Other Vegetation / Trees
3: {'mean': [0.30, 0.35, 0.25], 'std': [0.04, 0.05, 0.04]}, # Built-up / Bare Land
4: {'mean': [0.08, 0.05, 0.04], 'std': [0.02, 0.01, 0.01]}, # Water / Fishponds
}
N_SAMPLES = 100
rng = np.random.default_rng(42)
records = []
for feat in gj['features']:
cl_id = feat['properties']['Cl_Id']
ml_label = feat['properties']['ml_label']
p = SPECTRAL_PROFILES[cl_id]
samples = rng.normal(loc=p['mean'], scale=p['std'], size=(N_SAMPLES, 3)).clip(0, 1)
for s in samples:
records.append({'Green': s[0], 'Red': s[1], 'NIR': s[2], 'Class': ml_label})
data = pd.DataFrame(records)
# Normalize the features (Green, Red, NIR)
scaler = MinMaxScaler()
data[['Green', 'Red', 'NIR']] = scaler.fit_transform(data[['Green', 'Red', 'NIR']])
# Split into training and test sets
X = data[['Green', 'Red', 'NIR']].values
y = data['Class'].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)
# Visualize the dataset in 2D (Green vs NIR)
plt.figure(figsize=(8, 6))
plt.scatter(X[:, 0], X[:, 2], c=y, cmap='coolwarm', edgecolors='k', alpha=0.7)
plt.xlabel('Green (Normalized)')
plt.ylabel('NIR (Normalized)')
plt.title('Corn/Maize vs Non-Corn Areas (Green vs NIR)')
plt.colorbar(label='Class (0 = Non-Corn, 1 = Corn/Maize)')
plt.savefig(os.path.join(OUTPUT_DIR, 'dt_scatter_green_nir.png'), dpi=150, bbox_inches='tight')
plt.show()
print(data.head())Decision boundaries¶
Next, we are going to define a function to plot the decision boundary.
# Define function
def plot_decision_boundary(model, X, y, title):
"""
Plots the decision boundary for a 2D dataset.
"""
x_min, x_max = X[:, 0].min() - 0.1, X[:, 0].max() + 0.1
y_min, y_max = X[:, 1].min() - 0.1, X[:, 1].max() + 0.1
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),
np.linspace(y_min, y_max, 200))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# Plot into the *current* axes
plt.contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm')
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='coolwarm', edgecolors='k')
plt.xlabel('Green')
plt.ylabel('NIR')
plt.title(title)
plt.colorbar(label='Class (0 = Non-Corn, 1 = Corn/Maize)')# Import the decision tree classifier
from sklearn.tree import DecisionTreeClassifier, plot_tree
# Train two Decision Trees with different criteria
tree_gini = DecisionTreeClassifier(criterion='gini', max_depth=3, random_state=42)
tree_entropy = DecisionTreeClassifier(criterion='entropy', max_depth=3, random_state=42)
tree_gini.fit(X_train, y_train)
tree_entropy.fit(X_train, y_train)
# Fix 'Red' dimension at its median to visualize the boundary in 2D
fixed_red = np.median(X_train[:, 1]) # 1 => the 'Red' column
def plot_tree_boundary_2D(clf, title, subplot_index):
"""
Plot the 2D decision boundary (Green vs. NIR) by fixing Red dimension.
"""
steps = 200
green_vals = np.linspace(0, 1, steps)
nir_vals = np.linspace(0, 1, steps)
g_grid, n_grid = np.meshgrid(green_vals, nir_vals)
g_flat = g_grid.ravel()
n_flat = n_grid.ravel()
# Rebuild the 3D array but Red is fixed
X_2D = np.column_stack([g_flat,
np.full_like(g_flat, fixed_red),
n_flat])
y_pred = clf.predict(X_2D).reshape(g_grid.shape)
plt.subplot(1, 2, subplot_index)
plt.contourf(g_grid, n_grid, y_pred, alpha=0.3, cmap=plt.cm.coolwarm)
plt.scatter(
X_train[y_train==0, 0], X_train[y_train==0, 2],
c='blue', edgecolors='k', label='Non-Corn'
)
plt.scatter(
X_train[y_train==1, 0], X_train[y_train==1, 2],
c='red', edgecolors='k', label='Corn/Maize'
)
plt.title(title)
plt.xlabel('Green (Normalized)')
plt.ylabel('NIR (Normalized)')
plt.xlim(0, 1)
plt.ylim(0, 1)
plt.legend(loc='lower right')
plt.grid(True)
plt.figure(figsize=(12, 5))
plot_tree_boundary_2D(tree_gini, 'Decision Tree (Gini)', 1)
plot_tree_boundary_2D(tree_entropy, 'Decision Tree (Entropy)', 2)
plt.tight_layout()
plt.savefig(os.path.join(OUTPUT_DIR, 'dt_boundaries.png'), dpi=150, bbox_inches='tight')
plt.show()
acc_gini = tree_gini.score(X_test, y_test)
acc_entropy = tree_entropy.score(X_test, y_test)
print(f'Gini Tree Accuracy : {acc_gini*100:.2f}%')
print(f'Entropy Tree Accuracy: {acc_entropy*100:.2f}%')Finally, we plot the decision trees (Gini and Entropy).
# Plot the Gini Tree
plt.figure(figsize=(16, 8))
plot_tree(
tree_gini,
feature_names=['Green', 'Red', 'NIR'],
class_names=['Non-Corn', 'Corn/Maize'],
filled=True,
rounded=True,
proportion=False,
fontsize=10
)
plt.title('Decision Tree (Gini)')
plt.savefig(os.path.join(OUTPUT_DIR, 'dt_gini_tree.png'), dpi=150, bbox_inches='tight')
plt.show()
# Plot the Entropy Tree
plt.figure(figsize=(16, 8))
plot_tree(
tree_entropy,
feature_names=['Green', 'Red', 'NIR'],
class_names=['Non-Corn', 'Corn/Maize'],
filled=True,
rounded=True,
proportion=False,
fontsize=10
)
plt.title('Decision Tree (Entropy)')
plt.savefig(os.path.join(OUTPUT_DIR, 'dt_entropy_tree.png'), dpi=150, bbox_inches='tight')
plt.show()
acc_gini = tree_gini.score(X_test, y_test)
acc_entropy = tree_entropy.score(X_test, y_test)
print(f'Gini Tree Accuracy : {acc_gini*100:.2f}%')
print(f'Entropy Tree Accuracy: {acc_entropy*100:.2f}%')👩🏽💻 TEST IT OUT
How about if we want to check for 5 classes? Try it out with training_areas.geojson.