Comparing Machine Learning Classifiers for Crop Mapping in the Philippines
This notebook compares six machine learning classifiers — Minimum Distance, K-Nearest Neighbors (KNN), Support Vector Machines (SVM), Decision Trees (CART), Random Forest, and Gradient Tree Boosting — for crop mapping in the Philippines using Google Earth Engine (GEE) and Python.
The exercise uses Sentinel-2 satellite imagery and training areas digitized over a Philippine study area to map five crop / land cover classes:
| Class ID | Class Name |
|---|---|
| 0 | Rice Paddies |
| 1 | Corn / Maize |
| 2 | Other Vegetation / Trees |
| 3 | Built-up / Bare Land |
| 4 | Water / Fishponds |
Requirements¶
An active Google Earth Engine account
Authenticated Earth Engine Python API (instructions here)
The geemap Python package for visualization
Default Study Area¶
The default study area is Pangasinan, a province in Central Luzon known for rice, corn, and fishpond production — covering all five target classes. You can change this in the Configuration step below.
Step 1: Initialize and Authenticate Earth Engine¶
Import the Earth Engine API and authenticate your session before using any GEE functions.
# Import the Earth Engine API
import eeInitialize the Earth Engine API with your GEE project ID. Replace 'your-ee-project-id' with your actual project ID. On first run, you will be prompted to authenticate via your Google account.
# Trigger the authentication flow.
ee.Authenticate()
# Initialize the library with your GEE project.
ee.Initialize(project='your-gee-project-id-here') # Change to your EE projectStep 2: Import Libraries¶
Import additional libraries needed for visualization.
# Import the necessary libraries
import geemap
import osStep 3: Configuration — Edit Before Running¶
Edit the settings in the cell below before running the rest of the notebook.
Option A — Local Files (USE_LOCAL_FILES = True) ✅ Recommended¶
Place your boundary and training area files in a data/ folder next to this notebook:
ml-gee/
├── ml-gee-ph.ipynb
└── data/
├── boundary.geojson ← Study area polygon
└── training_areas.geojson ← Training polygons with field 'Cl_Id'Supported formats: .geojson or .shp (must be in WGS84 / EPSG:4326).
Your training areas file must have an integer field named Cl_Id with values:
| Value | Class |
|---|---|
| 0 | Rice Paddies |
| 1 | Corn / Maize |
| 2 | Other Vegetation / Trees |
| 3 | Built-up / Bare Land |
| 4 | Water / Fishponds |
Option B — GEE Administrative Boundary (USE_LOCAL_FILES = False)¶
Uses the FAO GAUL Level 1 (Province) dataset from GEE for the boundary. You must also provide a GEE asset path for your training areas.
# ============================================================
# CONFIGURATION — Edit these settings before running
# ============================================================
# Set to True to load boundary and training areas from local files.
# Set to False to use a GEE administrative boundary instead.
USE_LOCAL_FILES = True
# --- LOCAL FILE PATHS (used when USE_LOCAL_FILES = True) ---
# Place your files in a 'data/' subfolder next to this notebook.
# Supported formats: .geojson or .shp | Coordinate system: WGS84 (EPSG:4326)
BOUNDARY_FILE = 'data/boundary.geojson'
TRAINING_FILE = 'data/training_areas.geojson'
# --- GEE SETTINGS (used when USE_LOCAL_FILES = False) ---
# Province name as it appears in the FAO GAUL dataset (e.g. 'Pangasinan', 'Iloilo')
GEE_PROVINCE = 'Pangasinan'
# Your uploaded GEE training areas asset path
GEE_TRAINING_ASSET = 'users/YOUR_USERNAME/training_areas_ph'
# --- IMAGERY SETTINGS ---
START_DATE = '2024-01-01' # Dry season: rice is actively growing Jan–Apr in Central Luzon
END_DATE = '2024-04-30'
CLOUD_PCT = 20 # Maximum cloud cover percentageStep 4: Load Study Area Boundary and Training Areas¶
Load the study area boundary and training areas using the option configured above.
# Load boundary
if USE_LOCAL_FILES:
if BOUNDARY_FILE.endswith('.geojson'):
boundary = geemap.geojson_to_ee(BOUNDARY_FILE)
else:
boundary = geemap.shp_to_ee(BOUNDARY_FILE)
print('Boundary loaded from local file:', BOUNDARY_FILE)
else:
provinces = ee.FeatureCollection('FAO/GAUL/2015/level1')
boundary = provinces.filter(
ee.Filter.And(
ee.Filter.eq('ADM0_NAME', 'Philippines'),
ee.Filter.eq('ADM1_NAME', GEE_PROVINCE)
)
)
print(f'Boundary loaded from GEE FAO GAUL: {GEE_PROVINCE}, Philippines')
# Load training areas
if USE_LOCAL_FILES:
if TRAINING_FILE.endswith('.geojson'):
training_areas = geemap.geojson_to_ee(TRAINING_FILE)
else:
training_areas = geemap.shp_to_ee(TRAINING_FILE)
print('Training areas loaded from local file:', TRAINING_FILE)
else:
training_areas = ee.FeatureCollection(GEE_TRAINING_ASSET)
print(f'Training areas loaded from GEE asset: {GEE_TRAINING_ASSET}')Step 5: Prepare Sentinel-2 Imagery¶
Preprocess Sentinel-2 Surface Reflectance imagery for the study area. Cloud and cirrus pixels are removed using the QA60 band. Images within the configured date range are filtered by cloud cover percentage and a median composite is generated and clipped to the boundary.
The default date range covers the dry season (January–April) in Central Luzon — a period with lower cloud cover and active dry-season rice cultivation in Pangasinan.
# Function for cloud masking using the QA60 band
def mask_s2clouds(image):
qa = image.select('QA60')
cloudBitMask = ee.Number(2).pow(10).int()
cirrusBitMask = ee.Number(2).pow(11).int()
mask = qa.bitwiseAnd(cloudBitMask).eq(0).And(qa.bitwiseAnd(cirrusBitMask).eq(0))
return image.updateMask(mask).divide(10000)
# Load and filter Sentinel-2 Surface Reflectance imagery
s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED') \
.filterDate(START_DATE, END_DATE) \
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', CLOUD_PCT)) \
.map(mask_s2clouds) \
.select(['B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B11', 'B12'])
# Create a median composite clipped to the study area
composite = s2.median().clip(boundary)
# Print Sentinel-2 composite info
print('Sentinel-2 Composite:', composite.getInfo())
# Display the composite as a false-color image (SWIR / NIR / Green)
map1 = geemap.Map()
map1.addLayer(composite, {'bands': ['B11', 'B8', 'B3'], 'min': 0, 'max': 0.3}, 'Sentinel-2 Composite')
map1.centerObject(boundary, 10)
map1.addLayerControl()
map1Step 6: Prepare Training Data¶
The training areas are rasterized using the Cl_Id class field. The composite image is sampled at those rasterized locations using stratified random sampling to ensure proportional representation of each crop class.
| Class ID | Class Name | Color |
|---|---|---|
| 0 | Rice Paddies | Yellow |
| 1 | Corn / Maize | Light Green |
| 2 | Other Vegetation / Trees | Dark Green |
| 3 | Built-up / Bare Land | Grey |
| 4 | Water / Fishponds | Blue |
The sampled dataset is split 70% for training and 30% for validation.
# Band list for input features
bands = ee.List(['B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B11', 'B12'])
input_features = composite.clip(boundary)
print('Input features:', input_features.getInfo())
# Rasterize training areas using the 'Cl_Id' field
# Class IDs: 0=Rice, 1=Corn/Maize, 2=Other Vegetation, 3=Built-up/Bare, 4=Water/Fishponds
training_rasterized = training_areas.reduceToImage(
properties=['Cl_Id'],
reducer=ee.Reducer.first()
).toInt().remap([0, 1, 2, 3, 4], [0, 1, 2, 3, 4])
# Add the class band to the input features
input_features = input_features.addBands(training_rasterized.toInt().rename('class'))
# Stratified random sampling — ensures all classes are represented
training_dataset = input_features.stratifiedSample(
numPoints=5000,
classBand='class',
region=boundary,
scale=20
)
# Add a random column for reproducible train/test split
training_dataset = training_dataset.randomColumn('random', 50)
# Split into training (70%) and validation (30%)
split = 0.7
Sample_training = training_dataset.filter(ee.Filter.lt('random', split))
Sample_test = training_dataset.filter(ee.Filter.gte('random', split))
print('Number of training pixels:', Sample_training.size().getInfo())
print('Number of validation pixels:', Sample_test.size().getInfo())Step 7: Define and Train Machine Learning Classifiers¶
Six classifiers are initialized and a helper function train_classifier is defined to train any of them on the specified dataset and bands:
Minimum Distance — assigns each pixel to the nearest class centroid in feature space
K-Nearest Neighbors (KNN) — classifies based on the k=5 most similar training samples
Support Vector Machine (SVM) — RBF kernel with gamma=0.5; finds optimal class boundaries
Decision Tree (CART) — builds a single tree of if/then rules from training data
Random Forest — ensemble of 100 decision trees; robust to noise and overfitting
Gradient Tree Boosting (GTB) — 50 trees built sequentially; often highest accuracy
# Helper function to train any classifier
def train_classifier(classifier, training_data):
return classifier.train(
features=training_data,
classProperty='class',
inputProperties=bands
)
# Initialize classifiers
md_classifier = ee.Classifier.minimumDistance()
knn_classifier = ee.Classifier.smileKNN(k=5)
rbf_svc_classifier = ee.Classifier.libsvm(kernelType='RBF', gamma=0.5)
dt_classifier = ee.Classifier.smileCart()
rf_classifier = ee.Classifier.smileRandomForest(100)
gtb_classifier = ee.Classifier.smileGradientTreeBoost(
numberOfTrees=50,
shrinkage=0.005,
samplingRate=0.7,
maxNodes=None,
loss='LeastAbsoluteDeviation',
seed=0
)Step 8: Define the Classification Function¶
The train_run_classification function selects, trains, and applies a chosen classifier to the input features, then clips the result to the boundary.
Crop class color palette:
| Color | Class |
|---|---|
🟡 Yellow #F5D33A | Rice Paddies |
🟢 Light Green #90EE90 | Corn / Maize |
🌲 Dark Green #228B22 | Other Vegetation / Trees |
⬜ Grey #808080 | Built-up / Bare Land |
🔵 Blue #1E90FF | Water / Fishponds |
# Crop class color palette
# Order matches class IDs: 0=Rice, 1=Corn, 2=Other Veg, 3=Built-up/Bare, 4=Water/Fishponds
PALETTE = ['#F5D33A', '#90EE90', '#228B22', '#808080', '#1E90FF']
def train_run_classification(selected_classifier, sample_training, bands, boundary):
trained_classifier = None
classified_map = None
viz = {'min': 0, 'max': 4, 'palette': PALETTE}
if selected_classifier == 'Minimum distance':
md_classifier = ee.Classifier.minimumDistance()
trained_classifier = train_classifier(md_classifier, sample_training)
classified_map = input_features.select(bands).classify(trained_classifier).clip(boundary)
elif selected_classifier == 'KNN':
knn_classifier = ee.Classifier.smileKNN(k=5)
trained_classifier = train_classifier(knn_classifier, sample_training)
classified_map = input_features.select(bands).classify(trained_classifier).clip(boundary)
elif selected_classifier == 'SVM':
rbf_svc_classifier = ee.Classifier.libsvm(kernelType='RBF', gamma=0.5)
trained_classifier = train_classifier(rbf_svc_classifier, sample_training)
classified_map = input_features.select(bands).classify(trained_classifier).clip(boundary)
elif selected_classifier == 'CART':
dt_classifier = ee.Classifier.smileCart()
trained_classifier = train_classifier(dt_classifier, sample_training)
classified_map = input_features.select(bands).classify(trained_classifier).clip(boundary)
elif selected_classifier == 'Random forest':
rf_classifier = ee.Classifier.smileRandomForest(100)
trained_classifier = train_classifier(rf_classifier, sample_training)
classified_map = input_features.select(bands).classify(trained_classifier).clip(boundary)
elif selected_classifier == 'Gradient tree boost':
gtb_classifier = ee.Classifier.smileGradientTreeBoost(
numberOfTrees=50, shrinkage=0.005, samplingRate=0.7,
maxNodes=None, loss='LeastAbsoluteDeviation', seed=0)
trained_classifier = train_classifier(gtb_classifier, sample_training)
classified_map = input_features.select(bands).classify(trained_classifier).clip(boundary)
else:
print('Invalid classifier selected.')
return trained_classifier, classified_mapStep 9: Run All Classifiers¶
Train all six classifiers and generate classified crop maps.
# Train classifiers and generate classified crop maps
trainedMD, md_classified_map = train_run_classification('Minimum distance', Sample_training, bands, boundary)
trainedKNN, knn_classified_map = train_run_classification('KNN', Sample_training, bands, boundary)
trainedRBFSVC, rbf_svc_classified_map = train_run_classification('SVM', Sample_training, bands, boundary)
trainedDT, dt_classified_map = train_run_classification('CART', Sample_training, bands, boundary)
trainedRF, rf_classified_map = train_run_classification('Random forest', Sample_training, bands, boundary)
trainedGTB, gtb_classified_map = train_run_classification('Gradient tree boost', Sample_training, bands, boundary)Step 10: Display Crop Maps¶
Each classified crop map is displayed with a legend showing the five crop / land cover classes.
# Legend definition for Philippine crop classes
def add_legend(map_instance):
legend_dict = {
'Rice Paddies': (245, 211, 58), # yellow
'Corn / Maize': (144, 238, 144), # light green
'Other Vegetation': ( 34, 139, 34), # dark green
'Built-up / Bare Land': (128, 128, 128), # grey
'Water / Fishponds': ( 30, 144, 255), # blue
}
map_instance.add_legend(legend_title='Crop / Land Cover Classes', legend_dict=legend_dict)
def display_map_with_legend(classified_map, title):
map_instance = geemap.Map()
viz = {'min': 0, 'max': 4, 'palette': PALETTE}
map_instance.centerObject(boundary, 10)
map_instance.addLayer(classified_map, viz, title)
map_instance.addLayerControl()
add_legend(map_instance)
return map_instance
# Create map objects for all six classifiers
map_md = display_map_with_legend(md_classified_map, 'Minimum Distance Crop Map')
map_knn = display_map_with_legend(knn_classified_map, 'KNN Crop Map')
map_rbf_svc = display_map_with_legend(rbf_svc_classified_map, 'SVM (RBF) Crop Map')
map_dt = display_map_with_legend(dt_classified_map, 'Decision Tree Crop Map')
map_rf = display_map_with_legend(rf_classified_map, 'Random Forest Crop Map')
map_gtb = display_map_with_legend(gtb_classified_map, 'Gradient Tree Boost Crop Map')Minimum Distance — Crop Map¶
Display the crop map derived from the Minimum Distance classifier.
map_mdK-Nearest Neighbors (KNN) — Crop Map¶
Display the crop map derived from the KNN classifier.
map_knnSupport Vector Machine (SVM) — Crop Map¶
Display the crop map derived from the SVM (RBF kernel) classifier.
map_rbf_svcDecision Tree (CART) — Crop Map¶
Display the crop map derived from the Decision Tree (CART) classifier.
map_dtRandom Forest — Crop Map¶
Display the crop map derived from the Random Forest classifier.
map_rfGradient Tree Boosting — Crop Map¶
Display the crop map derived from the Gradient Tree Boosting classifier.
map_gtbStep 11: Classification Accuracy Assessment¶
Evaluate the accuracy of each classifier using the 30% validation set held back during the train/test split.
The compute_metrics function applies the trained classifier to the validation data and computes:
Error Matrix — shows predicted vs. actual class counts
User Accuracy (Precision per class) — how reliable is the map for each class?
Producer Accuracy (Recall per class) — how well is each class detected?
Overall Accuracy — percentage of correctly classified pixels
# Function to compute accuracy metrics
def compute_metrics(classifier, validation_data, algorithm_name):
validation_data_classified = validation_data.classify(classifier)
error_matrix = validation_data_classified.errorMatrix('class', 'classification')
print(f'Algorithm: {algorithm_name}')
print('Validation Error Matrix:\n', error_matrix.getInfo())
print('User Accuracy: ', error_matrix.consumersAccuracy().getInfo())
print('Producer Accuracy: ', error_matrix.producersAccuracy().getInfo())
print('Overall Accuracy: ', error_matrix.accuracy().getInfo())
print('-----------------------------------------------')
# Compute metrics for each classifier
compute_metrics(trainedMD, Sample_test, 'Minimum Distance')
compute_metrics(trainedKNN, Sample_test, 'KNN')
compute_metrics(trainedRBFSVC, Sample_test, 'RBF SVM')
compute_metrics(trainedDT, Sample_test, 'Decision Tree (CART)')
compute_metrics(trainedRF, Sample_test, 'Random Forest')
compute_metrics(trainedGTB, Sample_test, 'Gradient Tree Boosting')