Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Geospatial Python

Review of Python concepts

Data types and data structures

Control flow

Classes and objects

Exercise

  1. Define a class called Image that accepts a metadata dictionary in its constructor and stores it as an instance attribute.

  2. Using the metadata variable defined earlier (satellite: "Sentinel-2", date: "2025-01-15", cloud_cover: 15), create an Image instance called img1.

  3. Create a second Image instance called img2 with the following metadata: satellite: "Sentinel-2", date: "2025-01-16", cloud_cover: 32.

  4. Store both instances in a list called images.

  5. Loop through images and print the metadata of each.

Expected output:

{'satellite': 'Sentinel-2', 'date': '2025-01-15', 'cloud_cover': 15}
{'satellite': 'Sentinel-2', 'date': '2025-01-16', 'cloud_cover': 32}

Numpy

NumPy (Numerical Python) is the core library for numerical computing in Python. It provides powerful tools for working with arrays, mathematical operations, and large datasets efficiently. In this cheat sheet, NumPy serves as the foundation of the Python scientific ecosystem, meaning many other libraries (like Pandas, SciPy, and machine learning tools) rely on it for fast computations.

📖 RTFM

  1. When in doubt, refer to the NumPy Documentation.

  2. The NumPy Quickstart Tutorial is a great place to start.

  3. Other free resources you can use include:

Array

Array operations

Array manipulation

GeoPandas

GeoPandas is an extension of Pandas designed for working with geospatial data. It allows you to handle geographic information (such as points, lines, and polygons) in a tabular format, just like a regular DataFrame—but with added spatial capabilities. In this cheat sheet, GeoPandas is introduced as the main tool for vector-based geospatial analysis in Python, combining data manipulation with mapping and spatial operations.

📖 RTFM

  1. When in doubt, refer to the GeoPandas Documentation.

  2. The GeoPandas User Guide covers core concepts in depth.

  3. Other free resources you can use include:

GeoDataFrames

The core data structure in GeoPandas are:

  • the geopandas.GeoDataFrame, a subclass of pandas.DataFrame, that can store geometry columns and perform spatial operations, and

  • the geopandas.GeoSeries, a subclass of pandas.Series, handles the geometries.

Therefore, your GeoDataFrame is a combination of pandas.Series, with traditional data (numerical, boolean, text etc.), and geopandas.GeoSeries, with geometries (points, polygons etc.).

You can have as many columns with geometries as you wish; there’s no limit typical for desktop GIS software.

Reading/Writing Vector Data

# Read files
gdf = gpd.read_file("data.shp")
gdf = gpd.read_file("data.geojson")
gdf = gpd.read_file("data.gpkg")

# Write files
gdf.to_file("output.shp")
gdf.to_file("output.geojson", driver="GeoJSON")
gdf.to_file("output.gpkg", driver="GPKG")

👨‍💻 TRY IT OUT

Try it with our test data or your own dataset!

CRS handling

GeoPandas is able to handle CRS.

Let’s try to use it to convert our NCR municities data to PRS 92 Zone 3.

GeoDataFrame Operations

# Inspect data
gdf.head()              # First 5 rows
gdf.info()              # Column info
gdf.describe()          # Statistics
gdf.crs                 # Coordinate Reference System
gdf.geometry            # Geometry column
gdf.total_bounds        # Bounding box [minx, miny, maxx, maxy]

# Filtering
metro_manila = gdf[gdf["region"] == "NCR"]
large_areas = gdf[gdf.area > 1000000]

# Sorting
gdf.sort_values("population", ascending=False)

Attribute joins

⚠️ IMPORTANT

  1. Column or index level names to join on must be found in both DataFrames (i.e. same name).

  2. If both key columns contain rows where the key is a null value, those rows will be matched against each other. This is different from usual SQL join behaviour and can lead to unexpected results.

🤔 SOMETHING TO THINK ABOUT

Notice that we have two municity fields in the joined output. Why is that?

Let’s remove the extra field and rename the other one.

🗐 Always use copies when doing edits to your dataframes

It’s always a good idea to make a copy of your dataframes and apply changes to them instead of working with the original dataframe directly. You can do this with:

df_copy = df.copy()

You can also use the del function.

Spatial Operations

# Coordinate Reference System
gdf.to_crs("EPSG:4326")          # Reproject to WGS84
gdf.to_crs("EPSG:32651")         # Reproject to UTM Zone 51N

# Geometric properties
gdf.area                          # Area
gdf.length                        # Perimeter/length
gdf.centroid                      # Centroids
gdf.bounds                        # Bounding boxes

# Spatial relationships
gdf1.intersects(gdf2)            # Intersection check
gdf1.contains(point)             # Containment check
gdf1.within(polygon)             # Within check

# Spatial joins
gpd.sjoin(points, polygons, how="inner", predicate="within")

# Overlay operations
gpd.overlay(gdf1, gdf2, how="intersection")
gpd.overlay(gdf1, gdf2, how="union")
gpd.overlay(gdf1, gdf2, how="difference")

⚠️ CRS IS IMPORTANT

Try this using the GDF w/ geographic CRS (WGS84). What happens?

Spatial Joins

GeoPandas has support for spatial joins (https://geopandas.org/en/stable/gallery/spatial_joins.html)

Visualizations

# Basic plot
gdf.plot()

# Styled plot
gdf.plot(column="population",
         cmap="YlOrRd",
         legend=True,
         figsize=(10, 8))

# Multiple layers
ax = gdf1.plot(color="blue", alpha=0.5)
gdf2.plot(ax=ax, color="red", alpha=0.5)

Rasterio

Rasterio is a Python library used for working with raster data—gridded datasets such as satellite imagery, elevation models, and remote sensing data. In this cheat sheet, Rasterio is presented as the go-to tool for raster-based geospatial analysis, allowing you to read, write, and manipulate raster datasets efficiently in Python.

📖 RTFM

  1. When in doubt, refer to the Rasterio Documentation.

  2. The Rasterio Quickstart covers the most common read/write operations.

  3. Other free resources you can use include:

Reading raster data

import rasterio

# Open raster
with rasterio.open("image.tif") as src:
    # Metadata
    print(src.crs)          # CRS
    print(src.bounds)       # Bounding box
    print(src.shape)        # (height, width)
    print(src.count)        # Number of bands
    print(src.transform)    # Affine transform

    # Read data
    band1 = src.read(1)     # Read band 1
    all_bands = src.read()  # Read all bands

    # Windowed read
    window = rasterio.windows.Window(0, 0, 512, 512)
    subset = src.read(1, window=window)
{'driver': 'GTiff', 'dtype': 'int16', 'nodata': -32768.0, 'width': 781, 'height': 1575, 'count': 1, 'crs': CRS.from_wkt('PROJCS["WGS 84 / UTM zone 51N",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",123],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32651"]]'), 'transform': Affine(30.200644046094723, 0.0, 275364.4216, 0.0, -30.475696571428507, 1635171.5473)}
(1575, 781)
1
('int16',)
BoundingBox(left=275364.4216, bottom=1587172.3252, right=298951.1246, top=1635171.5473)

Writing raster data

import rasterio

# Write single band
with rasterio.open(
    "output.tif",
    "w",
    driver="GTiff",
    height=data.shape[0],
    width=data.shape[1],
    count=1,
    dtype=data.dtype,
    crs="EPSG:32651",
    transform=transform
) as dst:
    dst.write(data, 1)

# Write multiple bands
with rasterio.open("output.tif", "w", ...) as dst:
    for i, band in enumerate(bands, start=1):
        dst.write(band, i)

Raster operations

# Reproject
from rasterio.warp import reproject, Resampling

reproject(
    source=src_array,
    destination=dst_array,
    src_transform=src.transform,
    src_crs=src.crs,
    dst_transform=dst_transform,
    dst_crs="EPSG:4326",
    resampling=Resampling.bilinear
)

# Masking
from rasterio.mask import mask

with rasterio.open("image.tif") as src:
    clipped, transform = mask(src, shapes, crop=True)

# Calculate indices
with rasterio.open("sentinel2.tif") as src:
    red = src.read(4).astype(float)
    nir = src.read(8).astype(float)
    ndvi = (nir - red) / (nir + red)

Visualization

from rasterio.plot import show

# Single band
with rasterio.open("image.tif") as src:
    show(src, cmap="gray")

# RGB composite
with rasterio.open("image.tif") as src:
    show((src, [4, 3, 2]))  # True color (R, G, B)
<Figure size 640x480 with 1 Axes>
<Axes: >
<Figure size 640x480 with 1 Axes>
<Figure size 640x480 with 1 Axes>
<Figure size 640x480 with 1 Axes>
<Axes: title={'center': 'B'}>
<Figure size 2100x700 with 3 Axes>

Raster algebra: NDVI

Max NDVI (Healthiest vegetation): 0.96
Min NDVI (Water or Built-up): -0.95
<Figure size 1000x800 with 2 Axes>

Clipping

To speed up analysis, we often “clip” or “crop” the image using a vector polygon (e.g., province boundary or a farm parcel) so we only analyze relevant pixels.

We will use shapely to draw a quick custom Area of Interest (AOI) box right in the middle of our image, and use rasterio.mask to cut it out.

Original shape: (1439, 1288)
Clipped shape: (351, 352)
<Figure size 600x600 with 1 Axes>