Lesson 8.2.1: Converting model predictions into disease probability maps
Lesson 8.2.1: Converting model predictions into disease probability maps
Learning Objectives
- Understand how to transform raw model outputs into interpretable probability values.
- Apply statistical techniques such as softmax and logistic regression to calibrate predictions.
- Generate raster probability maps from model predictions using GIS tools.
- Critically evaluate the accuracy and spatial reliability of disease probability maps.
Key Terms
| Softmax | A normalisation function that converts vector outputs into probability distributions summing to one. |
|---|---|
| Logistic Regression | A statistical model that estimates the probability of a binary outcome based on predictor variables. |
| Raster | A grid-based spatial data format where each cell holds a value, often used for maps. |
| Calibration | The process of adjusting model predictions so that predicted probabilities reflect observed frequencies. |
| Spatial Autocorrelation | A measure of the degree to which similar values cluster together in space. |
flowchart TD
A[Input Image] --> B[Deep Learning Model]
B --> C[Raw logits]
C --> D[Softmax → Probability map]
D --> E[Optional Post‑processing]
E --> F[Thresholding → Binary disease map]
F --> G[Output Disease Probability Map]
Concept: From Scores to Probabilities
In computer vision for medical imaging, a deep learning model often outputs a score for each voxel or pixel in a scan. That score is usually a real‑valued number that reflects how strongly the model believes that the location belongs to a particular class, such as tumour tissue versus normal brain. However, for clinical reporting and downstream decision‑support, we rarely want a raw score; we need a probability that a voxel truly belongs to the disease class. Converting a score to a probability is a small but crucial mathematical step that bridges the gap between the model’s internal representation and a clinically interpretable metric.
Why Probabilities Matter
Probabilities have several desirable properties:
- Interpretability: A 0.8 probability is immediately understood as an 80 % chance that the voxel is diseased.
- Calibration: Probabilities can be calibrated against ground truth data so that, for example, all voxels whose model outputs a probability of 0.7 actually contain disease in 70 % of cases.
- Decision‑making: Thresholds can be chosen to optimise sensitivity or specificity, or to balance them via ROC analysis.
- Aggregation: Probabilities can be summed or averaged over patches, slices, or the entire volume to produce region‑level or patient‑level metrics.
Raw scores, on the other hand, are often unbounded or symmetric around zero and do not directly convey certainty. For instance, the model might output a score of 2.5 for a lesion and 0.5 for normal tissue; without a mapping to probabilities, it is unclear what those numbers mean in absolute terms.
Common Score‑to‑Probability Transformations
Two transformations dominate the literature: sigmoid and softmax. The choice depends on the number of classes and the structure of the network’s final layer.
Sigmoid for Binary or One‑vs‑Rest Problems
A sigmoid function maps any real‑valued input to the open interval (0, 1). It is defined as:
def sigmoid(x):
return 1 / (1 + math.exp(-x))
Applied voxel‑wise to a score s, the sigmoid yields a probability p = sigmoid(s). The function has an S‑shaped curve that asymptotically approaches 0 and 1, ensuring that extreme scores translate into probabilities close to the limits.
Softmax for Multi‑Class Problems
When the model outputs a vector of scores [s1, s2, …, sk] for \(k\) classes, the softmax function normalises them into a probability distribution:
import math
def softmax(scores):
exp_scores = [math.exp(s) for s in scores]
sum_exp = sum(exp_scores)
return [e / sum_exp for e in exp_scores]
Each output probability sums to 1 across all classes, making it straightforward to interpret which class is most likely.
Temperature Scaling
Sometimes a model is overconfident; its probabilities are too close to 0 or 1. Temperature scaling introduces a scalar T > 0 to soften the distribution:
def softmax_temperature(scores, T=1.0):
exp_scores = [math.exp(s / T) for s in scores]
sum_exp = sum(exp_scores)
return [e / sum_exp for e in exp_scores]
A higher temperature (>1) flattens the distribution, while a lower temperature (<1) sharpens it. Temperature is typically tuned on a validation set to improve calibration.
Implementing the Transformation in Practice
Below is a concise example that demonstrates how to convert a 3‑D numpy array of logits (raw scores) into a probability map using the sigmoid function. The code is written in a way that is directly usable in a typical PyTorch or TensorFlow inference pipeline.
import numpy as np
import math
def sigmoid(x):
"Return the logistic sigmoid of x."
return 1.0 / (1.0 + math.exp(-x))
def logits_to_probability_map(logits):
"
Convert a 3-D array of logits to a probability map.
Parameters
----------
logits : np.ndarray
3‑D array of shape (Z, Y, X) containing raw model scores.
Returns
-------
probs : np.ndarray
3‑D array of shape (Z, Y, X) with values in [0, 1].
"
# Vectorise the sigmoid to operate on the whole array efficiently
vectorised_sigmoid = np.vectorize(sigmoid)
probs = vectorised_sigmoid(logits)
return probs
Example usage
logits = np.random.randn(64, 128, 128) # random scores for a 64x128x128 volume
prob_map = logits_to_probability_map(logits)
print(prob_map.shape, prob_map.min(), prob_map.max())
Line‑by‑line explanation:
import numpy as npandimport mathbring in the numerical libraries required for array operations and the exponential function.- The
sigmoidfunction implements the standard logistic function. It takes a single scalarxand returns a value between 0 and 1. logits_to_probability_mapis a helper that accepts a 3‑D numpy array of logits. The docstring explains the purpose, parameters, and return type for clarity.- Inside the function,
np.vectorize(sigmoid)creates a vectorised version of the sigmoid that can be applied element‑wise across the entire array without explicit loops. - The vectorised sigmoid is then applied to
logits, producingprobs, a probability map with the same shape. - The example at the bottom generates a random 3‑D volume, converts it, and prints its shape and extremal values to confirm that all probabilities lie between 0 and 1.
Performance Considerations
In production, you would typically rely on the deep learning framework’s built‑in sigmoid (e.g. torch.sigmoid or tf.nn.sigmoid) to utilise GPU acceleration. The numpy example is primarily for educational purposes and can be easily swapped out:
import torch
def logits_to_probs_torch(logits_tensor):
return torch.sigmoid(logits_tensor)
Using the framework’s native function ensures that the operation is differentiable and can be part of a larger computational graph if required for further optimisation.
Calibration: Aligning Predicted Probabilities with Reality
Even with a mathematically correct transformation, the model’s output probabilities may not be well calibrated. Calibration curves plot the empirical accuracy against predicted probabilities, revealing systematic over‑ or under‑confidence. Common calibration techniques include:
- Platt Scaling: Fit a logistic regression model to map logits to probabilities on a validation set.
import numpy as np def vectorised_sigmoid(logits): ""Return a probability map using NumPy broadcasting."" return 1.0 / (1.0 + np.exp(-logits)) Example usage
logits = np.random.randn(32, 64, 64) probs = vectorised_sigmoid(logits) print(probs.shape, probs.min(), probs.max())Approach Speed (approx.) Memory Footprint Ease of Use Python loop + math.exp Slow (ms per voxel) Low (single scalar) High (simple code) np.vectorize + math.exp Moderate (depends on vectorisation overhead) Moderate (creates temporary array) Medium (requires helper) NumPy broadcasting (np.exp) Fast (vectorised C ops) High (stores full array of exponentials) High (concise, no helper) Framework sigmoid (torch/TensorFlow) Very fast (GPU‑accelerated) High (GPU memory) High (built‑in) Activity: Reflect on Probability MappingAfter learning how to convert model scores into disease probability maps, think about the practical implications of this process. Consider how changes in the threshold affect clinical decision-making, the importance of calibration, and potential pitfalls when interpreting probability maps. Write a brief reflection (2-4 sentences) addressing these points and how you would apply them in a real-world scenario.
Implementation/Code: Building a Probability Raster
In this section we step through the complete workflow that transforms raw model outputs—normally a list or array of class‑probabilities—into a tidy raster that can be visualised in any GIS platform. The process is split into three logical blocks:
- Preparing the reference grid and spatial metadata
- Reshaping the model output into a 2‑D array that matches the grid
- Writing the probability layer to disk with correct georeferencing information
All of the code below is written in Python 3.10+, using the most common geospatial libraries:
rasteriofor raster I/O,numpyfor array manipulation, andgeopandasto handle the reference vector that defines the study area. The example assumes a binary classifier that outputs a single probability value per pixel for the presence of a disease.1. Setting up the reference grid
Before we can write a raster we must know its spatial extent, resolution, and coordinate reference system (CRS). Usually this information comes from an existing reference raster or from a vector layer that bounds the study area. In the example we read a vector shapefile that contains the bounding polygon of the target region.
import geopandas as gpd import rasterio from rasterio.transform import from_origin import numpy as npLoad the vector with the study area boundary
boundary = gpd.read_file('data/study_area.shp')Ensure the boundary is in a projected CRS that preserves area (e.g. EPSG:3857)
boundary = boundary.to_crs(epsg=3857)Calculate the bounding box of the boundary
minx, miny, maxx, maxy = boundary.total_boundsDefine raster resolution (e.g. 30 metres per pixel)
pixel_size = 30Compute the number of rows and columns
width = int(np.ceil((maxx - minx) / pixel_size)) height = int(np.ceil((maxy - miny) / pixel_size))Build an affine transform that maps pixel coordinates to map units
transform = from_origin(minx, maxy, pixel_size, pixel_size)Line‑by‑line explanation:
- Import the required libraries.
- Read the shapefile containing the study area into a
GeoDataFrame. - Re‑project the geometry to EPSG:3857 (Web Mercator) which is a common projected CRS that preserves distances and is suitable for rasterisation.
- Extract the minimum and maximum coordinates that bound the geometry.
- Set the desired pixel size.
- Calculate how many columns (
width) and rows (height) are needed to cover the bounding box at the chosen resolution.np.ceilensures we round up so no area is left out. - Create an affine transform that tells
rasteriohow to convert from pixel indices to map coordinates.
At this stage we have the skeleton of the raster: its shape, resolution, and CRS.
2. Reshaping the model output
The model typically returns a flat array of probabilities, one per pixel in the reference grid, ordered row‑major. We need to reshape this into a 2‑D
numpyarray that matches the raster dimensions. In practice the model might output a 3‑D array (batch, height, width) or a flat list; the code below handles the most common case.# Mock model output: a flat list of probabilitiesIn practice this would come from your model inference pipeline
flat_probs = np.random.uniform(low=0.0, high=1.0, size=width * height)Reshape into a 2‑D array (height rows, width columns)
prob_array = flat_probs.reshape((height, width))Explanation:
- Generate a random array to simulate model output. Replace this with the real probabilities from your classifier.
- Use
reshapeto turn the flat vector into a 2‑D matrix. The first dimension isheight(rows), the second iswidth(columns).numpywill fill the array in row‑major order, which aligns with howrasteriowrites data.
In many cases you might need to mask out areas outside the study boundary.
rasterio.features.rasterisecan create a mask from the vector geometry. Below we show how to apply such a mask, setting pixels outside the polygon tonp.nanso they are ignored in visualisations.from rasterio.features import rasterizeCreate a mask array where inside the boundary is 1, outside is 0
mask = rasterize( [(geom, 1) for geom in boundary.geometry], out_shape=(height, width), transform=transform, fill=0, dtype=np.uint8 )Apply the mask: set probabilities outside the boundary to NaN
prob_array = np.where(mask == 1, prob_array, np.nan)Now
prob_arraycontains the disease probability for every pixel inside the study area, withNaNvalues indicating no data.3. Writing the probability raster to disk
With the data array and spatial metadata ready, we can write the raster.
rasteriosupports various data types; for probability values a 32‑bit floating point is appropriate. We also embed a descriptive name and a colour map reference to aid interpretation in GIS viewers.# Define output file path output_path = 'output/disease_probability.tif'Build the metadata dictionary
meta = { 'driver': 'GTiff', 'height': height, 'width': width, 'count': 1, # single band 'dtype': 'float32', 'crs': 'EPSG:3857', 'transform': transform, 'nodata': np.nan, 'compress': 'lzw', 'description': 'Probability of disease presence' }Write the raster
with rasterio.open(output_path, 'w', **meta) as dst: dst.write(prob_array.astype('float32'), 1)Line‑by‑line:
- Specify where the resulting GeoTIFF will be stored.
- Construct a dictionary of metadata:
driverspecifies the file format.- Dimension and band count.
- Data type
float32. - CRS matches the reference grid.
- Affine transform from earlier.
nodataset tonp.nanso that GIS software recognises missing values.- Compression to reduce file size.
- A human‑readable description.
- Open the file for writing, passing the metadata.
- Write the probability array to the first (and only) band, converting to
float32to match the declared dtype.
After running the script, the file
disease_probability.tifcan be loaded into QGIS, ArcGIS, or any other GIS application. When displayed, the probability values can be colour‑graded using a suitable colour ramp (e.g. green for low risk, red for high risk). TheNaNcells outside the boundary will appear transparent, ensuring a clean visualisation.Common pitfalls and best practices
Issue Solution Mismatch between model pixel order and raster order Verify that the reshape dimensions match the model’s inherent ordering. If the model outputs column‑major, transpose the array before writing. Incorrect CRS leading to mis‑registered rasters Double‑check that the CRS of the boundary matches the CRS used in the raster transform. Use boundary.to_crs()if necessary.Large rasters exceeding memory limits Process in blocks: read a chunk of the model output, write it to the raster in a loop. rasteriosupports block‑wise write operations.Loss of precision when converting to float32For higher precision, use float64but be aware of increased file size.Extending the workflow
Once the basic probability raster is in place, several enhancements are common in production pipelines:
- Thresholding – Convert probabilities to binary presence/absence maps by applying a threshold (e.g. 0.5). This can be done in
numpywithprob_array > 0.5and then writing auint8raster. - Uncertainty quantification – If the model produces a distribution of predictions (e.g. via Monte Carlo dropout), write separate raster layers for mean, variance, or confidence intervals.
- Multi‑class probability rasters – For multi‑class problems, reshape the output to
(n_classes, height, width)and write each class as a separate band. Include a colour table that maps each band to a distinct colour. - Geoprocessing automation – Wrap the entire process in a Python function or a Snakemake rule, enabling reproducible runs across multiple study areas.
By following the steps above you will reliably convert any model’s raw probability outputs into a GIS‑ready raster that can be analysed, visualised, and shared with stakeholders. The key lies in aligning spatial metadata, handling missing data correctly, and preserving numerical precision throughout the pipeline.
Alternative approach: clipping with
rasterio.maskInstead of building a mask array manually, you can clip the probability raster as soon as it is written, using
rasterio.mask.mask. This is handy when the model output is already in raster form or when you want to keep the workflow tidy.from rasterio.mask import maskAssuming `prob_array` has been written to disk already
with rasterio.open(output_path, 'r+') as src: out_image, out_transform = mask(src, boundary.geometry, crop=True, nodata=np.nan) # out_image is a 3‑D array (bands, rows, cols); keep the first band src.update_tags(1, description='Clipped probability') src.write(out_image[0].astype('float32'), 1)Clipping in this way ensures the final file only contains data within the study boundary, potentially reducing file size.
Approach Pros Cons Manual mask with rasteriseFull control over mask shape; works before writing. Requires extra memory for the mask array; extra code. Clipping with rasterio.maskSimpler code; removes external memory usage. Needs the raster to exist on disk first; can be slower for large rasters. Vector‑to‑Raster conversion with rasterio.features.rasteriseand direct writeOne‑step process; no intermediate files. Mask must be created in memory; not suitable for extremely large extents. Activity: Converting Model Predictions into Probability Maps-
Which function is used to convert raw model outputs into probability values for each class?
- softmax
- relu
- sigmoid
- tanh
-
After applying the activation function, what is the next step to create a raster file?
- Normalise the probabilities to 0-255
- Reshape the array into a 3‑D tensor
- Write the array to a GeoTIFF using rasterio
- Apply a random walk algorithm
-
Which of the following is NOT a required step when building a probability raster?
- Reproject the raster to match the study area CRS
- Apply a Gaussian filter to smooth the predictions
- Clip the raster to the study area extent
- Convert the probabilities to binary presence/absence
-
What does the term “softmax” refer to in the context of model output processing?
- A scaling technique to normalise pixel values
- A loss function used during training
- A normalisation that converts logits into a probability distribution across classes
- A post‑processing step that thresholds predictions
3.1 Alternative Implementation: Random Forest Classifier
Below is a compact Python script that replaces the logistic regression with a random forest classifier from
scikit-learn. This approach captures non‑linear relationships and interactions between covariates, which can be important for complex disease systems.import geopandas as gpd import rasterio import numpy as np import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score from rasterio.transform import from_origin1. Load covariate rasters
rasters = { 'temp': rasterio.open('temp.tif'), 'rain': rasterio.open('rain.tif'), 'elev': rasterio.open('elev.tif'), 'land': rasterio.open('land.tif'), 'popd': rasterio.open('popd.tif') }2. Read hotspot points
hotspots = gpd.read_file('hotspots.shp') hotspots = hotspots.to_crs(rasters['temp'].crs)3. Extract raster values at hotspot locations
def extract_values(gdf, raster, name): coords = [(x, y) for x, y in zip(gdf.geometry.x, gdf.geometry.y)] values = [val[0] for val in raster.sample(coords)] gdf[name] = values for key in rasters: extract_values(hotspots, rasters[key], key)4. Prepare design matrix and response
X = hotspots[['temp', 'rain', 'elev', 'land', 'popd']].values y = hotspots['label'].values5. Fit Random Forest
rf = RandomForestClassifier( n_estimators=200, max_depth=10, random_state=42, n_jobs=-1 ) rf.fit(X, y)6. Model evaluation
y_pred_proba = rf.predict_proba(X)[:, 1] print(f'Cross‑validated AUC: {roc_auc_score(y, y_pred_proba):.3f}')7. Predict across the study area
meta = rasters['temp'].meta rows, cols = meta['height'], meta['width'] transform = meta['transform']Stack rasters into a 2‑D array (rows*cols, covariates)
stack = np.vstack([ rasters['temp'].read(1).ravel(), rasters['rain'].read(1).ravel(), rasters['elev'].read(1).ravel(), rasters['land'].read(1).ravel(), rasters['popd'].read(1).ravel() ]).T mask = np.any(np.isnan(stack), axis=1) stack_clean = stack[~mask]Predict probabilities
probs = rf.predict_proba(stack_clean)[:, 1]Re‑inject masked values
full_probs = np.full((rows * cols), np.nan) full_probs[~mask] = probs prob_raster = full_probs.reshape((rows, cols))8. Write probability raster
with rasterio.open( 'malaria_prob_rf.tif', 'w', driver='GTiff', height=rows, width=cols, count=1, dtype=rasterio.float32, crs=meta['crs'], transform=transform, ) as dst: dst.write(prob_raster.astype(rasterio.float32), 1)This script demonstrates how to swap the modelling backbone while keeping the spatial workflow unchanged. The resulting GeoTIFF,
malaria_prob_rf.tif, can be compared directly with the logistic‑regression output to assess model performance and spatial bias.Approach Model Type Key Strengths Typical Use‑Case Logistic Regression Linear in log‑odds Interpretability, fast training, clear coefficient signs Baseline risk mapping, when relationships are approximately linear Random Forest Ensemble of decision trees Captures non‑linearities, robust to over‑fitting, handles missing values Complex ecological landscapes, when interactions are suspected Bayesian Hierarchical Probabilistic, spatially explicit Quantifies uncertainty, incorporates prior knowledge, handles spatial autocorrelation When detailed uncertainty quantification is required Activity: Interpreting Your Malaria Risk MapYou have just used the model to generate a probability map of malaria risk across the region. Now imagine you are a public health officer tasked with allocating limited resources. Analyse the map: identify the three zones with the highest predicted risk, justify why they stand out, and outline a brief action plan for each zone (e.g., targeted spraying, community education, or bed net distribution). Use the insights from the last section to support your decisions.
Pitfalls: Common Errors and How to Avoid Them
When converting the raw outputs of a predictive model into spatial probability maps, a range of subtle yet consequential errors can creep in. These mistakes often arise from a mismatch between the statistical assumptions of the model, the preprocessing of the data, and the visualisation pipeline. Below we dissect the most frequent pitfalls, analyse why they occur, and present concrete strategies to eliminate them.
1. Ignoring Class Imbalance During Training
In many disease mapping studies, the proportion of infected individuals is far lower than that of healthy ones. If the model is trained on such imbalanced data without adjusting for it, the output probabilities will be systematically biased toward the majority class. As a result, the resulting map will under‑estimate risk in genuinely high‑prevalence areas.
- Solution: Apply class‑weighting or resampling techniques during model fitting. Most scikit‑learn estimators accept a
class_weightargument; in neural networks, use a weighted loss function. - Verification: Examine the calibration curve of the model on a held‑out set; a perfectly calibrated model will produce a diagonal line in the plot.
2. Mis‑scaled Input Features
When raster layers are fed into the model, each band often carries a different numerical range (e.g. NDVI in [−1, 1] versus temperature in [−50, 50]). Without normalising these inputs, the optimiser will give undue weight to features with larger absolute values, distorting the probability estimates.
- Solution: Standardise all covariates to zero mean and unit variance, or scale them to a common range such as [0, 1]. Keep the scaler fitted on the training set and apply the same transformation to the prediction tiles.
- Tip: Store the scaler parameters in a JSON file so they can be re‑used during inference.
3. Overlooking Spatial Autocorrelation
Most machine‑learning algorithms assume independence between samples. In spatial epidemiology, this assumption is rarely true; nearby locations often exhibit similar disease risk due to unmeasured environmental drivers. Ignoring this can lead to overconfident probability estimates.
- Solution: Incorporate a spatial random effect (e.g. via a Gaussian Process or Conditional Autoregressive model) or use a mixed‑effects framework. Alternatively, add a spatial lag of the predictors to the feature set.
- Check: Compute the Moran’s I statistic on the residuals; a significant value indicates remaining spatial structure.
4. Inaccurate Raster Alignment and Projection
Probability maps are only meaningful if they align perfectly with the reference geographic coordinate system. A small mis‑registration between the model output and the target grid can produce misleading hotspots.
- Solution: Use geospatial libraries (e.g. rasterio) to reproject all rasters to the same CRS before feeding them into the model. Validate the alignment by overlaying a vector boundary.
- Validation step: Run a quick overlay test:
rasterio.plot.show((raster1, raster2))and confirm that the visual alignment matches.
5. Over‑Thresholding Probabilities
Converting continuous probabilities to binary maps by applying a hard threshold (e.g. 0.5) without considering the prevalence or cost of false positives/negatives can produce artefacts. A single pixel may be misclassified simply because the threshold was arbitrary.
- Solution: Use a cost‑sensitive threshold derived from the ROC curve or from a decision‑analytic framework such as Youden’s J statistic. Alternatively, retain the continuous probability field and let subsequent analyses (e.g. risk zoning) decide thresholds.
- Example threshold optimisation:
optimal_threshold = roc_curve(y_true, y_pred)[1][np.argmax(roc_curve(y_true, y_pred)[2] - roc_curve(y_true, y_pred)[1])]
6. Neglecting Uncertainty Propagation
Most practitioners present a single deterministic probability map, masking the inherent uncertainty of the model. This can be misleading, especially in policy contexts where risk thresholds are critical.
- Solution: Quantify predictive uncertainty using techniques such as Monte‑Carlo dropout, ensemble methods, or Bayesian neural networks. Visualise the standard deviation or credible intervals as an overlay or separate thematic layer.
- Practical tip: Store the full distribution of predictions for each pixel in a NetCDF file for downstream analysis.
7. Failure to Re‑Calibrate Post‑Training
Even a well‑trained model may produce poorly calibrated probabilities due to changes in data distribution between training and deployment. This is particularly common when the model is applied to a new region.
- Solution: Employ post‑hoc calibration methods such as Platt scaling or isotonic regression on a calibration set drawn from the target area. Libraries like scikit‑learn provide
CalibratedClassifierCVfor this purpose. - Check calibration: Plot a reliability diagram; the closer the points lie to the diagonal, the better the calibration.
8. Over‑Fitting to a Limited Dataset
High‑dimensional models with too many predictors relative to the number of observations tend to over‑fit, producing probability maps that look convincing but are not generalisable.
- Solution: Apply dimensionality reduction (e.g. PCA) or feature selection based on mutual information. Use cross‑validation to monitor generalisation performance.
- Rule of thumb: For logistic regression, keep the number of predictors below one‑third of the number of positive cases.
9. Poor Handling of Missing Data
Spatial datasets often contain gaps due to cloud cover or sensor failure. If missing values are not properly imputed, the model may produce NaNs or biased probabilities in those areas.
- Solution: Use spatial interpolation (e.g. kriging) or machine‑learning imputers (e.g. KNNImputer) that respect spatial structure. Always flag imputed areas in the final map.
- Implementation hint: In rasterio, use
rasterio.fillnodatawith a suitable method before feeding the data into the model.
10. Assuming Homogeneous Model Performance Across Space
It is tempting to apply a single probability map across the entire study area, assuming the model performs equally well everywhere. In reality, model error can vary spatially due to unmeasured covariates or differing data quality.
- Solution: Partition the study area into sub‑regions based on environmental strata or administrative boundaries, train local models, or at least evaluate spatially varying performance metrics.
- Evaluation: Compute the confusion matrix for each sub‑region and visualise the spatial distribution of sensitivity and specificity.
Practical Implementation Example
Below is a concise Python snippet demonstrating how to convert raw model logits into calibrated probabilities, apply a cost‑sensitive threshold, and save the result as a GeoTIFF. The code uses scikit‑learn for calibration and rasterio for geospatial handling.
import numpy as np import rasterio from rasterio.transform import from_origin from sklearn.calibration import CalibratedClassifierCV from sklearn.linear_model import LogisticRegressionLoad pre‑processed raster patches (features) and corresponding labels
X = np.load('features.npy') # shape: (n_samples, n_features) y = np.load('labels.npy') # shape: (n_samples,)Train a base classifier
base_clf = LogisticRegression(max_iter=1000) base_clf.fit(X, y)Calibrate the probabilities using Platt scaling
calibrated_clf = CalibratedClassifierCV(base_clf, method='sigmoid', cv=5) calibrated_clf.fit(X, y)Predict probabilities on a new raster block
X_new = np.load('new_features.npy') # same feature engineering as training probabilities = calibrated_clf.predict_proba(X_new)[:, 1]Determine optimal threshold via Youden's J
from sklearn.metrics import roc_curve fpr, tpr, thresholds = roc_curve(y, calibrated_clf.predict_proba(X)[:, 1]) youden_index = tpr - fpr optimal_thr = thresholds[np.argmax(youden_index)]Apply threshold to obtain binary map
binary_map = (probabilities >= optimal_thr).astype(np.uint8)Save as GeoTIFF
transform = from_origin(-180, 90, 0.01, 0.01) # example geotransform with rasterio.open( 'probability_map.tif', 'w', driver='GTiff', height=binary_map.shape[0], width=binary_map.shape[1], count=1, dtype=binary_map.dtype, crs='+proj=longlat', transform=transform, ) as dst: dst.write(binary_map, 1)Line‑by‑line explanation
import numpy as np– Load NumPy for numerical operations.import rasterio– Rasterio handles reading and writing GeoTIFFs.from rasterio.transform import from_origin– Create an affine transform for georeferencing.from sklearn.calibration import CalibratedClassifierCV– Wrapper for probability calibration.from sklearn.linear_model import LogisticRegression– Base classifier.- Load training features and labels from pre‑saved NumPy arrays.
- Instantiate and fit the logistic regression model.
- Wrap the base classifier in
CalibratedClassifierCVwith Platt scaling (method='sigmoid') and 5‑fold cross‑validation. - Fit the calibrated model on the same data.
- Predict raw probabilities on a new raster block.
- Compute the ROC curve on training data to obtain false positive/true positive rates.
- Calculate Youden's J statistic (
tpr - fpr) to identify the threshold that maximises sensitivity + specificity. - Determine the optimal threshold from the ROC array.
- Generate a binary map by thresholding the probabilities.
- Define the geotransform (here a placeholder of 0.01 degree resolution).
- Write the binary map to a GeoTIFF with appropriate metadata (CRS, dimensions, datatype).
By following the guidelines above, you minimise the risk of producing misleading disease probability maps. The combination of rigorous preprocessing, model calibration, spatially aware validation, and transparent uncertainty quantification ensures that the final visualisations are both scientifically robust and policy‑relevant.
Additional Example: Bayesian Neural Network for Uncertainty Estimation
Below is a short snippet that demonstrates how to construct a simple Bayesian neural network in Pyro, predict probabilistic outcomes, and visualise the mean and standard deviation of the predictive distribution over a raster field.
import torch import pyro import pyro.distributions as dist from pyro.infer import Predictive import rasterio import numpy as npDefine a Bayesian neural network with one hidden layer
class BNN(torch.nn.Module): def __init__(self, input_dim, hidden_dim=32): super().__init__() self.fc1 = torch.nn.Linear(input_dim, hidden_dim) self.fc2 = torch.nn.Linear(hidden_dim, 1) def forward(self, x): h = torch.relu(self.fc1(x)) return torch.sigmoid(self.fc2(h))Wrap the network in a Pyro model
def model(x, y=None): weight1 = pyro.sample('weight1', dist.Normal(0., 1.).expand([input_dim, hidden_dim]).to_event(2)) bias1 = pyro.sample('bias1', dist.Normal(0., 1.).expand([hidden_dim]).to_event(1)) weight2 = pyro.sample('weight2', dist.Normal(0., 1.).expand([hidden_dim, 1]).to_event(2)) bias2 = pyro.sample('bias2', dist.Normal(0., 1.).expand([1]).to_event(1)) def net(x): h = torch.relu(torch.matmul(x, weight1) + bias1) return torch.sigmoid(torch.matmul(h, weight2) + bias2) with pyro.plate('data', x.shape[0]): obs = pyro.sample('obs', dist.Bernoulli(net(x)), obs=y) return obsAssume features have been pre‑processed and loaded
X = torch.tensor(np.load('features.npy'), dtype=torch.float32) input_dim = X.shape[1]Perform posterior predictive sampling
predictive = Predictive(model, num_samples=200) samples = predictive(X)Compute mean and std dev of predictions
mean_pred = samples['obs'].float().mean(0).numpy() std_pred = samples['obs'].float().std(0).numpy()Save mean and std dev maps as separate GeoTIFFs
with rasterio.open('bnn_mean.tif', 'w', driver='GTiff', height=mean_pred.shape[0], width=mean_pred.shape[1], count=1, dtype='float32', crs='+proj=longlat', transform=from_origin(-180, 90, 0.01, 0.01)) as dst: dst.write(mean_pred, 1) with rasterio.open('bnn_std.tif', 'w', driver='GTiff', height=std_pred.shape[0], width=std_pred.shape[1], count=1, dtype='float32', crs='+proj=longlat', transform=from_origin(-180, 90, 0.01, 0.01)) as dst: dst.write(std_pred, 1)Approach Comparison Table
Approach Typical Use Case Pros Cons Logistic Regression Baseline, interpretable risk mapping Fast, clear coefficients, easy calibration Sensitive to class imbalance, limited non‑linear capture Random Forest / Gradient Boosting High‑dimensional, non‑linear relationships Robust to over‑fitting, handles missing data Harder to calibrate, less interpretable Convolutional Neural Network (CNN) Large raster patches, spatial patterns Captures local texture, end‑to‑end learning Requires large data, computationally intensive Bayesian Neural Network (Pyro) Uncertainty‑aware risk mapping Provides predictive variance, principled Bayesian inference Slow inference, complex model specification Gaussian Process Regression Smooth spatial variation, small datasets Explicit spatial correlation, uncertainty estimates Scales poorly with many points, kernel choice sensitive Activity: Discuss Common Pitfalls in Probability MappingReflect on the pitfalls covered in the "Pitfalls: Common Errors and How to Avoid Them" section. Identify two specific errors that can arise when converting model predictions into disease probability maps, explain why each error occurs, and propose a practical solution to mitigate it. Share your insights in the discussion forum and respond to at least one peer’s post to deepen your understanding.
Key Takeaways
- Understand how to transform raw model outputs into interpretable probability values.
- Apply statistical techniques such as softmax and logistic regression to calibrate predictions.
- Generate raster probability maps from model predictions using GIS tools.
- Critically evaluate the accuracy and spatial reliability of disease probability maps.
Further Reading
- World Health Organisation (2018) — Global Atlas of Disease Prevalence. A comprehensive guide to mapping disease burden using model outputs. This atlas demonstrates best practices for converting statistical predictions into spatial probability surfaces.
- Waller, L.E. & Gotway, C.A. (2004) — Applied Spatial Statistics for Public Health Data. The book offers detailed methodology for spatial interpolation and risk mapping, including case studies that transform model predictions into probability maps.
- Wang, H., et al. (2019) — Spatial Epidemiology: Methods and Applications. This peer‑reviewed article reviews modern techniques for converting predictive models into risk surfaces, emphasising validation and uncertainty quantification.
- Open Geospatial Consortium (OGC) (2021) — Web Coverage Service (WCS) Specification. Provides the technical standard for delivering continuous probability maps from model servers, essential for interoperable disease probability mapping.
- Jiang, Y., & Smith, R. (2022) — Bayesian Spatial Models in Epidemiology. The chapter discusses hierarchical modelling and the generation of probability maps, with practical examples in R and Python.
- Centres for Disease Control and Prevention (CDC) (2020) — Spatial Epidemiology and Risk Mapping. Official documentation outlining CDC’s approach to creating and interpreting disease probability maps from predictive models.


Comments are closed.