Skill Tracer
Skills › Scientific Agent Skills › geomaster › code-examples.md

code-examples.md

154 lines · skills/geomaster/references/code-examples.md at 5f2bc03

Open on GitHub
1# Small geospatial code recipes
2
3Reviewed 2026-10-01. These focus on contracts that caused silent errors in earlier
4examples. See [review.md](review.md) for tested versions. Data-dependent fragments
5are illustrative; variables such as `gdf`, `polygon` and `zones` must be supplied.
6The collection does not claim hundreds of executed examples.
7

Small geospatial code recipes

Reviewed 2026-10-01. These focus on contracts that caused silent errors in earlier examples. See review.md for tested versions. Data-dependent fragments are illustrative; variables such as gdf, polygon and zones must be supplied. The collection does not claim hundreds of executed examples.

Raw lines 1 to 7
8## Coordinates, vector data and maps
9
10```python
11import geopandas as gpd
12from pyproj import Geod
14gdf = gpd.read_file('data.gpkg', layer='features')
15if gdf.crs is None:
16 raise ValueError('Source CRS is missing')
17metric = gdf.to_crs(gdf.estimate_utm_crs())
18metric['area_m2'] = metric.geometry.area
19buffer_geometry = metric.buffer(1000)
20# Keep a single active geometry when exporting to formats that require it.
21buffer_gdf = metric.set_geometry(buffer_geometry)
22buffer_gdf.to_file('buffers.gpkg', driver='GPKG')
24# Geographic azimuth/distance: Geod returns forward azimuth, back azimuth, metres.
25azimuth, back_azimuth, metres = Geod(ellps='WGS84').inv(-122.4, 37.7, -122.3, 37.8)
26```
28`geopy.distance.geodesic` returns distance but has no `initial_bearing` property.
29Geopy coordinates are latitude,longitude; GeoJSON/PyProj with `always_xy` use longitude,latitude.
30For a geographic BallTree use radians and `metric='haversine'`, then convert angular
31distance using an explicit Earth-radius convention. Projected Euclidean k-NN needs
32appropriate coordinate units and scaling.
34Folium uses `Map(location=[latitude, longitude])` and `GeoJson(gdf.to_crs(4326))`.
35Contextily uses `add_basemap(ax, crs=gdf.crs)` and a permitted tile provider. Pass
36explicit data/position accessors to Pydeck; an unconfigured layer does not establish
37that coordinates are displayed correctly. All web maps require attribution and
38compliance with provider access/caching rules.

Coordinates, vector data and maps

import geopandas as gpd
from pyproj import Geod

gdf = gpd.read_file('data.gpkg', layer='features')
if gdf.crs is None:
    raise ValueError('Source CRS is missing')
metric = gdf.to_crs(gdf.estimate_utm_crs())
metric['area_m2'] = metric.geometry.area
buffer_geometry = metric.buffer(1000)
# Keep a single active geometry when exporting to formats that require it.
buffer_gdf = metric.set_geometry(buffer_geometry)
buffer_gdf.to_file('buffers.gpkg', driver='GPKG')

# Geographic azimuth/distance: Geod returns forward azimuth, back azimuth, metres.
azimuth, back_azimuth, metres = Geod(ellps='WGS84').inv(-122.4, 37.7, -122.3, 37.8)

geopy.distance.geodesic returns distance but has no initial_bearing property. Geopy coordinates are latitude,longitude; GeoJSON/PyProj with always_xy use longitude,latitude. For a geographic BallTree use radians and metric='haversine', then convert angular distance using an explicit Earth-radius convention. Projected Euclidean k-NN needs appropriate coordinate units and scaling.

Folium uses Map(location=[latitude, longitude]) and GeoJson(gdf.to_crs(4326)). Contextily uses add_basemap(ax, crs=gdf.crs) and a permitted tile provider. Pass explicit data/position accessors to Pydeck; an unconfigured layer does not establish that coordinates are displayed correctly. All web maps require attribution and compliance with provider access/caching rules.

Raw lines 8 to 39
40## Raster I/O and masks
42```python
43import rasterio
44from rasterio.mask import mask
46with rasterio.open('raster.tif') as src:
47 # polygon must already be in src.crs
48 clipped, transform = mask(src, [polygon], crop=True, filled=False)
49 profile = src.profile.copy()
50profile.update(height=clipped.shape[1], width=clipped.shape[2], transform=transform)
51with rasterio.open('clipped.tif', 'w', **profile) as dst:
52 dst.write(clipped, masked=True)
53```
55Do not read from a closed `src`. For resampling, update the affine alongside shape:
57```python
58from rasterio.enums import Resampling
59from affine import Affine
60with rasterio.open('raster.tif') as src:
61 rows, cols = src.height * 2, src.width * 2
62 data = src.read(out_shape=(src.count, rows, cols), masked=True,
63 resampling=Resampling.bilinear)
64 transform = src.transform * Affine.scale(src.width / cols, src.height / rows)
65```
67Bilinear is suitable only for continuous quantities; categories need nearest/mode.
68Doubling pixel count does not double actual resolving power. Reprojection needs both
69source and destination CRS/transform plus a nodata/resampling policy.

Raster I/O and masks

import rasterio
from rasterio.mask import mask

with rasterio.open('raster.tif') as src:
    # polygon must already be in src.crs
    clipped, transform = mask(src, [polygon], crop=True, filled=False)
    profile = src.profile.copy()
profile.update(height=clipped.shape[1], width=clipped.shape[2], transform=transform)
with rasterio.open('clipped.tif', 'w', **profile) as dst:
    dst.write(clipped, masked=True)

Do not read from a closed src. For resampling, update the affine alongside shape:

from rasterio.enums import Resampling
from affine import Affine
with rasterio.open('raster.tif') as src:
    rows, cols = src.height * 2, src.width * 2
    data = src.read(out_shape=(src.count, rows, cols), masked=True,
                    resampling=Resampling.bilinear)
    transform = src.transform * Affine.scale(src.width / cols, src.height / rows)

Bilinear is suitable only for continuous quantities; categories need nearest/mode. Doubling pixel count does not double actual resolving power. Reprojection needs both source and destination CRS/transform plus a nodata/resampling policy.

Raw lines 40 to 70
71## Spectral indices, classification and terrain
73The bundled `raster_workflows.py` contains the maintained implementations; import it
74from the skill's scripts directory. See the main skill for examples. It provides:
76- `normalized_difference(a,b)`: floating-point subtraction, mask and zero-denominator handling;
77- `spectral_indices(...)`: physical-reflectance NDVI/EVI/SAVI/NDWI/MNDWI/NBR/NDBI;
78- `terrain_metrics(dem,transform,crs)`: physical gradients, downslope aspect, flat-cell policy;
79- `write_ndvi(...)`: explicit bands/calibration, preserved raster georeferencing/nodata;
80- `classify_imagery(...)`: known CRS, raster-aligned labels, valid pixels and uint16 class output.
82Raster classification requires the raster's actual affine transform when rasterizing
83training polygons; a default identity transform can yield wrong or empty training
84samples. A Random Forest fitted to pixels has not measured spatial transfer accuracy.
85Use blocked validation. Do not label a flat DEM threshold as flood-risk modeling.

Spectral indices, classification and terrain

The bundled raster_workflows.py contains the maintained implementations; import it from the skill's scripts directory. See the main skill for examples. It provides:

  • normalized_difference(a,b): floating-point subtraction, mask and zero-denominator handling;
  • spectral_indices(...): physical-reflectance NDVI/EVI/SAVI/NDWI/MNDWI/NBR/NDBI;
  • terrain_metrics(dem,transform,crs): physical gradients, downslope aspect, flat-cell policy;
  • write_ndvi(...): explicit bands/calibration, preserved raster georeferencing/nodata;
  • classify_imagery(...): known CRS, raster-aligned labels, valid pixels and uint16 class output.

Raster classification requires the raster's actual affine transform when rasterizing training polygons; a default identity transform can yield wrong or empty training samples. A Random Forest fitted to pixels has not measured spatial transfer accuracy. Use blocked validation. Do not label a flat DEM threshold as flood-risk modeling.

Raw lines 71 to 86
87## Interpolation and spatial statistics
89```python
90import numpy as np
91from scipy.interpolate import griddata, NearestNDInterpolator, RBFInterpolator
92from scipy.stats import gaussian_kde
94# points: (n,2) projected coordinates; values: (n,)
95linear = griddata(points, values, (xi, yi), method='linear')
96nearest = NearestNDInterpolator(points, values)
97rbf = RBFInterpolator(points, values, kernel='thin_plate_spline')
99# KDE expects dimensions x observations, evaluation dimensions x locations.
100grid_x, grid_y = np.mgrid[xmin:xmax:100j, ymin:ymax:100j]
101kde = gaussian_kde(points.T)
102density = kde(np.vstack([grid_x.ravel(), grid_y.ravel()])).reshape(grid_x.shape)
103```
105`griddata(method='linear')` is linear triangulation, not IDW.
106`NearestNDInterpolator` is nearest neighbour, not natural neighbour.
107KDE requires a defined bandwidth and sufficient nondegenerate observations; compare
108boundary correction and sampling effort before interpreting density as risk.
109For Moran's I, Geary's C and Gi*, align observation order with a chosen spatial weight
110matrix. Kriging needs a defensible variogram and spatial holdouts; see
111[specialized topics](specialized-topics.md).

Interpolation and spatial statistics

import numpy as np
from scipy.interpolate import griddata, NearestNDInterpolator, RBFInterpolator
from scipy.stats import gaussian_kde

# points: (n,2) projected coordinates; values: (n,)
linear = griddata(points, values, (xi, yi), method='linear')
nearest = NearestNDInterpolator(points, values)
rbf = RBFInterpolator(points, values, kernel='thin_plate_spline')

# KDE expects dimensions x observations, evaluation dimensions x locations.
grid_x, grid_y = np.mgrid[xmin:xmax:100j, ymin:ymax:100j]
kde = gaussian_kde(points.T)
density = kde(np.vstack([grid_x.ravel(), grid_y.ravel()])).reshape(grid_x.shape)

griddata(method='linear') is linear triangulation, not IDW. NearestNDInterpolator is nearest neighbour, not natural neighbour. KDE requires a defined bandwidth and sufficient nondegenerate observations; compare boundary correction and sampling effort before interpreting density as risk. For Moran's I, Geary's C and Gi*, align observation order with a chosen spatial weight matrix. Kriging needs a defensible variogram and spatial holdouts; see specialized topics.

Raw lines 87 to 112
113## Image watershed versus catchment delineation
115```python
116from scipy.ndimage import label
117from skimage.segmentation import watershed
118markers, marker_count = label(local_minima)
119segments = watershed(elevation, markers=markers, mask=valid_mask)
120```
122This is image segmentation. It does not implement hydrological catchment routing,
123pour-point snapping or contributing area. `scipy.ndimage.watershed` is not this API.
124For hydrology use the domain workflow and documented direction conventions.

Image watershed versus catchment delineation

from scipy.ndimage import label
from skimage.segmentation import watershed
markers, marker_count = label(local_minima)
segments = watershed(elevation, markers=markers, mask=valid_mask)

This is image segmentation. It does not implement hydrological catchment routing, pour-point snapping or contributing area. scipy.ndimage.watershed is not this API. For hydrology use the domain workflow and documented direction conventions.

Raw lines 113 to 125
126## Tiles and network helpers
128```python
129import mercantile
130# Geographic bbox west,south,east,north; zoom is a separate argument.
131tiles = list(mercantile.tiles(-122.41, 37.70, -122.40, 37.71, zooms=12))
132```
134Enumerating tiles does not render/reproject raster pixels. A renderer must produce
135each tile's correct Web Mercator bounds/resolution and nodata/alpha behavior. Public
136OSM tiles are not a bulk/offline export service.
138Use `ox.routing.shortest_path` or NetworkX shortest paths on an OSMnx graph, with
139explicit weights/units and no-route handling. Metric reachability is not an edge-count
140radius. See [advanced GIS](advanced-gis.md).

Tiles and network helpers

import mercantile
# Geographic bbox west,south,east,north; zoom is a separate argument.
tiles = list(mercantile.tiles(-122.41, 37.70, -122.40, 37.71, zooms=12))

Enumerating tiles does not render/reproject raster pixels. A renderer must produce each tile's correct Web Mercator bounds/resolution and nodata/alpha behavior. Public OSM tiles are not a bulk/offline export service.

Use ox.routing.shortest_path or NetworkX shortest paths on an OSMnx graph, with explicit weights/units and no-route handling. Metric reachability is not an edge-count radius. See advanced GIS.

Raw lines 126 to 141
142## Other languages
144The maintained R, Julia, JavaScript, C++, Java, Go and Rust fragments are in
145[programming-languages.md](programming-languages.md). In Turf,
146`interpolate(points,cellSize,{property:'value',units:'kilometers'})` expects a
147FeatureCollection of points with values, not a LineString.
149Sources: [PyProj Geod](https://pyproj4.github.io/pyproj/stable/api/geod.html),
150[Rasterio resampling](https://rasterio.readthedocs.io/en/stable/topics/resampling.html),
151[SciPy interpolation](https://docs.scipy.org/doc/scipy/reference/interpolate.html),
152[Scikit-image watershed](https://scikit-image.org/docs/stable/api/skimage.segmentation.html#skimage.segmentation.watershed),
153[Mercantile](https://mercantile.readthedocs.io/en/latest/api/mercantile.html),
154[Turf interpolation](https://turfjs.org/docs/api/interpolate).

Other languages

The maintained R, Julia, JavaScript, C++, Java, Go and Rust fragments are in programming-languages.md. In Turf, interpolate(points,cellSize,{property:'value',units:'kilometers'}) expects a FeatureCollection of points with values, not a LineString.

Sources: PyProj Geod, Rasterio resampling, SciPy interpolation, Scikit-image watershed, Mercantile, Turf interpolation.

Raw lines 142 to 154