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.

Making spatial projects understandable, not just runnable

Open In Colab

In the previous chapters, you learned how to write cleaner Python, organize code beyond one giant notebook, make workflows more robust, and record software environments. These are all essential technical parts of reproducibility. But a project can still fail as a scientific communication artifact even if all of those pieces are perfectly in place.

Why? Because reproducibility depends on more than just file availability. It fundamentally depends on interpretability.

A repository may contain the code, the data, and the environment file, yet still leave the reader asking basic questions:

Documentation answers those questions.

Often viewed as bureaucratic busywork left for the very end of a project, documentation is actually a core component of the scientific workflow.

In this chapter, you will learn the hierarchy of documentation in a spatial data science project. We will move from the macro level down to the micro level, covering how to:

Interactive Explorer: Documentation Hierarchy.
Select documentation layers such as README, notebook Markdown, inline comments, docstrings, and figure captions to compare weak and strong examples and see which reader question each layer answers. For improved visibility of the explorer, follow this link.


1. README files that actually help

The README file is the front door to your project. It is the first thing anyone sees when they open your repository on GitHub, GitLab, or a shared folder. In a well-organized project, it answers the most important questions before the reader even opens a notebook.

A weak README creates uncertainty. A strong README creates orientation.

A weak README

A weak README assumes the reader already knows what the project is about. It forces the user to dig into the code to understand the goal.

# SDS210 final project

This notebook analyzes spatial data. Run `workflow.ipynb`. Data is in the data folder.

This tells the reader almost nothing. It does not explain the scientific topic, the study area, the data sources, the outputs, or the required software environment.

A stronger README

A strong README acts as a standalone guide. It explains the scientific objective, the required data, and the computational environment needed to run the code.

A strong geospatial README:

# Urban Heat in Zurich

This project analyzes long-term urban heat patterns in Zurich using Landsat-derived land surface temperature and vegetation indicators.

## Objective
The goal of this project is to extract a 10-year time series of summer surface temperatures to identify neighborhoods with the most intense urban heat island effects.

## Data Sources
* **Landsat 8 LST:** Annual summer summaries (30m resolution).
* **City Boundaries:** Vector neighborhood layers provided by the City of Zurich.
* *Note: Due to file size constraints, raw `.tif` files are not tracked in this repository. Download them via this Earth Engine script (insert link) and place them in `data/raw/`.*

## Reproducing the Environment
This project requires a specific spatial software stack. To recreate the environment:
1. Ensure you have Conda installed.
2. Run: `conda env create -f environment.yml`
3. Activate: `conda activate zurich-heat-env`

## Usage
Execute the Jupyter Notebook `01_urban_heat_analysis.ipynb` from top to bottom. Trend maps will be exported to `outputs/maps/`.

This version removes the guesswork. It immediately gives the reader a clear mental model of the project and explicitly ties into the environment management practices you learned in the previous chapter.

What a useful README should answer

A practical student project README should usually cover:

That does not require pages of text. Clarity matters much more than length.

Why a geospatial framing helps

Geospatial projects benefit especially from a good README because spatial workflows involve unique dependencies. Your README should explain how these pieces fit together, such as:


2. Explain inputs, outputs & assumptions

A reproducible project explains what files it includes, where they came from, and why they matter.

This is especially important in geospatial analysis, where the exact same Python script can produce different results depending on the study area, the raster resolution, or the coordinate reference system. You should document these details at both the project level, in your README, and the code level, in your scripts.

Documenting inputs and outputs

A reader should not have to guess what is inside your data/ or outputs/ folders. You should provide a brief “data dictionary” in your README or at the top of your main notebook.

For inputs, a reader needs to know:

Input data:
- `stations.gpkg`: climate monitoring stations in Switzerland (raw)
- `dem_2m.tif`: digital elevation model at 2 m resolution (swissALTI3D)
- `cantons.gpkg`: administrative boundaries used for context mapping

For outputs, a reader needs to know:

Outputs:
- `station_elevation.csv`: extracted elevation values at station points
- `elevation_map.png`: map of station points colored by extracted elevation
- `stations_with_dem.gpkg`: station layer enriched with elevation values

Documenting macro-assumptions

Some of the most important documentation is not about files at all; it is about analytical choices. If an analytical threshold or projection choice shapes the final map, it deserves explicit documentation in your Markdown text.

These macro-assumptions often include:

If you document these clearly in your README or notebook introduction, collaborators immediately understand the boundaries of your science.

Documenting micro-assumptions: inline comments

While Markdown explains the broad strokes of the workflow, inline code comments (#) exist to explain specific, non-obvious programmatic decisions down at the code level.

The golden rule of code comments is: Explain why, not what. Python syntax already tells the reader what is happening. Comments should be reserved for explaining the scientific assumptions or the hidden logic behind the numbers, the magic numbers we discussed in the Defensive Coding chapter.

Bad comment: explains the syntax

# Buffer the points by 500
stations_buffered = stations.buffer(500)

The reader already knows it is buffering by 500. What they do not know is why 500 was chosen instead of 100 or 1000.

Good comment: explains the assumption

# Buffer by 500m to account for maximum GPS drift in complex alpine terrain
stations_buffered = stations.buffer(500)

Good comment: explains a data quirk

# Drop rows where elevation is -9999 (swisstopo's NoData value for this specific dataset)
clean_dem = raw_dem.where(raw_dem != -9999)

Use comments sparingly but strategically. If you find yourself writing a massive paragraph of comments to explain a complex block of code, it is often a sign that the code itself is too confusing and should be simplified or broken down into smaller, well-named functions.


3. Docstrings and type hints

Documentation exists at different levels of a project. As we have seen, your README explains the overall project, your notebook Markdown explains the workflow, and your inline comments explain specific line-by-line assumptions.

But what happens when you write custom, reusable functions, like the ones we built in the Defensive Coding chapter? For reusable code, Python provides two specific tools: type hints and docstrings.

Type hints add lightweight clarity

Modern Python allows you to use type hints directly in your function definitions. Type hints act as built-in, self-documenting code. Even without reading the full implementation, a collaborator instantly knows what the function expects and what it gives back.

import geopandas as gpd

# The colon (:) defines the input type. The arrow (->) defines the output type.
def clip_to_bbox(gdf: gpd.GeoDataFrame, bbox: dict) -> gpd.GeoDataFrame:
    return gdf.cx[bbox["xmin"]:bbox["xmax"], bbox["ymin"]:bbox["ymax"]]

Docstrings explain behavior

While type hints explain the data types, a docstring explains the behavior.

A docstring is a multi-line string, enclosed in """, placed immediately inside the function definition. It is the standard Python way to specify what the function does, what the parameters mean, and what assumptions or errors might occur.

When you combine type hints with a clear docstring, your functions become easy for collaborators to use safely.

A fully documented geospatial function:

def filter_high_elevation(gdf: gpd.GeoDataFrame, threshold_m: float) -> gpd.GeoDataFrame:
    """
    Filters a spatial dataset to retain only features above a certain elevation.
    
    Assumes the input GeoDataFrame contains a column named 'elevation_m'.
    
    Parameters
    ----------
    gdf : geopandas.GeoDataFrame
        The input spatial dataset containing point or polygon features.
    threshold_m : float
        The minimum elevation in meters. Features below this value are removed.
        
    Returns
    -------
    geopandas.GeoDataFrame
        A new GeoDataFrame containing only the high-elevation features.
        
    Raises
    ------
    KeyError
        If the required 'elevation_m' column is missing.
    """
    # Defensive check
    if "elevation_m" not in gdf.columns:
        raise KeyError("Input GeoDataFrame must contain an 'elevation_m' column.")
        
    return gdf[gdf["elevation_m"] >= threshold_m].copy()

The payoff: built-in documentation

Why go through the trouble of formatting your docstring so formally? Because Python and modern code editors, like VS Code or JupyterLab, read these strings automatically.

If a collaborator imports your filter_high_elevation function in a different notebook, they can simply type help(filter_high_elevation), or hover their mouse over the function name in their editor, and your docstring will appear. They never have to guess what data types to provide, what column names are required, or what the function will return.

Concept Check: Which Layer Gets the Explanation?

You are filtering buildings smaller than 100 m² because the source dataset includes many sheds and digitizing artifacts. Where should this assumption be documented?

A. Only in the README, because it is a project-level decision.

B. Near the filtering line as a why-comment, and also in the README or notebook if it shapes the final interpretation.

C. Only inside a docstring, because every explanation belongs in reusable functions.


4. Captions, figure titles, & provenance

Documentation is not only about code. It also applies to your scientific outputs.

The final step of most spatial workflows is communication via maps or plots. However, a map can be visually impressive and still communicate very little if the title is vague, the units are unclear, the data source is hidden, or the processing steps are never mentioned. In reproducible science, visual outputs must be self-explanatory.

Weak and strong figure titles

A strong title tells the reader exactly what variable is shown, where it is shown, and over what time period it was summarized.

Weak title:

Map

Strong title:

Mean annual snow cover fraction in Switzerland, 2001–2024

Captions should add meaning

A caption should not simply restate the title. It should explain what the figure shows, how to interpret it, and what macro-assumptions were made to produce it.

Weak caption:

This figure shows the results.

Strong caption:

Stations are colored by elevation values extracted from the DEM after reprojection to EPSG:2056. Higher elevation stations cluster in the Alpine regions.

Now the caption adds both provenance, such as reprojection and extraction, and interpretation, such as clustering.

Provenance notes in code

For scientific outputs, it is crucial to note the source dataset and important parameters. If you add these details directly into your plotting code, every generated image carries its own context. If the map is saved as a .png and embedded in a report or a presentation, the scientific context travels with it.

When plotting in a notebook, ensure your figures contain descriptive titles, axis labels with units, and a small text addition noting where the data originated. For example, are the coordinates in degrees or meters?

import matplotlib.pyplot as plt
import geopandas as gpd

# fig, ax = plt.subplots(figsize=(8, 6))
# rhone_rivers_gdf.plot(ax=ax)

# 1. Add a descriptive title
# ax.set_title("Major River Networks in the Rhone Catchment", fontsize=14)

# 2. Add axis labels with clear units/CRS
# ax.set_xlabel("Easting (CH1903+ / LV95)")
# ax.set_ylabel("Northing (CH1903+ / LV95)")

# 3. Add a provenance note to the bottom of the figure
# fig.text(0.15, 0.02, "Data: swisstopo TLM3D (2023)", fontsize=9, style='italic')

# plt.show()

5. Telling the workflow story in notebooks

Jupyter Notebooks are powerful because they allow you to interleave code with text. However, a common mistake is treating a notebook like a pure Python script, using only code cells and ignoring Markdown.

A notebook is not just a place where code runs; it is a narrative document. The reader should be able to follow the scientific question, the data origins, the analysis steps, and the interpretation of the outputs just by scrolling. A notebook that begins immediately with imports and data loading forces the reader to reverse-engineer the purpose of the analysis.

Start with context

Before the first code block, use Markdown to establish the geographical context and overall objective. Explain what the notebook does, why the analysis matters, what data are used, and what the reader should expect by the end.

# Extracting Station Elevations

This notebook extracts elevation values from a 2m DEM at climate monitoring station locations in Switzerland. The goal is to compare station elevation patterns across regions and produce a cleaned, enriched output layer for the final temperature trend analysis.

That single paragraph makes the notebook much more approachable.

Use Markdown to frame steps

Before executing a code block, use Markdown to outline the methodology and introduce the analytical role of the step.

Poor narrative: pure Python script style

# Loading data and clipping it
import geopandas as gpd
study_area = gpd.read_file("catchment.gpkg")
rivers = gpd.read_file("rivers.shp")
clipped_rivers = rivers.clip(study_area)

Strong narrative: notebook style

(In a Markdown cell)

Isolating the River Network

To analyze hydrological connectivity, we first need to restrict the national river network to our specific study area, the Rhone catchment. We will load both vector layers and perform a spatial clip.

(In a Code cell)

import geopandas as gpd

catchment_gdf = gpd.read_file("../data/raw/rhone_catchment.gpkg")
national_rivers_gdf = gpd.read_file("../data/raw/swiss_rivers.shp")

rhone_rivers_gdf = national_rivers_gdf.clip(catchment_gdf)

This helps the notebook read like a guided scientific workflow rather than a raw execution log.

Explain results after they appear

Documentation does not stop when the code finishes running. After you generate a figure, map, or summary table, add a brief Markdown cell to interpret the result.

**Interpretation:** As shown in the map above, the highest elevation stations are densely concentrated in southern and eastern Switzerland. This matches the expected topographic pattern of the Alps and confirms that the raster point-extraction worked correctly.

That small paragraph turns the notebook from a technical transcript into a communicative, scientific document.


6. Weak vs. strong documentation

It is useful to see the difference between weak and strong documentation side by side. The difference is not about making things look pretty; it is about making them scientifically interpretable.

1. The project entry: README

Weak project entry:

# DEM analysis

This notebook uses some spatial data and creates outputs.

This tells the reader nothing about the science, the data, or the requirements.

Strong project entry:

# Station Elevation Analysis in Switzerland

This project extracts elevation values from a 2m DEM at station locations across Switzerland. The workflow loads point and raster data, aligns them in a common CRS, performs raster extraction, and exports a cleaned point layer with elevation attributes.

## Input data
- `stations.gpkg`: point layer of monitoring stations
- `dem_2m.tif`: digital elevation model (swissALTI3D)

## Outputs
- `stations_with_elevation.gpkg`
- `station_elevation_map.png`

## Key assumptions
- Raster extraction is performed after reprojection to EPSG:2056
- Output values are reported in meters above sea level

2. Reusable functions: docstrings and type hints

Weak function documentation:

def extract_values(points, raster):
    # Does some extraction
    return points

This may run, but the user has to guess the expected data types and what the function actually modifies.

Strong function documentation:

import geopandas as gpd
from pathlib import Path

def extract_raster_values(points_gdf: gpd.GeoDataFrame, raster_path: Path) -> gpd.GeoDataFrame:
    """
    Extract raster values at point locations.

    Parameters
    ----------
    points_gdf : geopandas.GeoDataFrame
        Point layer in the exact same CRS as the raster.
    raster_path : pathlib.Path
        Path to the raster file.

    Returns
    -------
    geopandas.GeoDataFrame
        A copy of the input point layer with a new column containing extracted raster values.
    """
    # Extraction logic here...
    return points_gdf

3. Scientific outputs: figures and maps

Weak figure communication:

Title: Results
Caption: This shows the final map.

Strong figure communication:

Title: Elevation of monitoring stations in Switzerland (2024)
Caption: Station points are colored by DEM-derived elevation in meters after raster extraction in EPSG:2056. Higher values correctly cluster in the Alpine south and east.
Data Source: swisstopo (2024).

The takeaway

In all three cases, the code underneath might be identical. But the strong examples treat the reader as a collaborator, while the weak examples treat the reader as a mind-reader. Good documentation is the bridge between code that merely runs and science that can be trusted.


7. Exercise

Below are poorly documented components from a hypothetical spatial analysis project. They technically work, but they force the reader to guess their purpose, inputs, and assumptions.

Task

Improve the project’s documentation at both the macro and micro levels.

Part 1: The macro level: README and outputs

  1. Rewrite the weak README text so that it clearly explains the objective, data, and outputs.

  2. Suggest a strong figure title and caption to replace the weak ones.

Part 2: The micro level: functions and comments

  1. Add type hints to the parameters and the return value of the clean_and_mask function.

  2. Write a standard docstring explaining what the function does, its parameters, and its return value.

  3. Replace the weak code comments, which only explain the what, with strong comments that explain the why: the spatial assumptions.

The weak documentation

Weak README:

# SDS210 project

This project analyzes station data and raster data.

Weak Figure Communication:

Title: Results
Caption: This shows the final map.

Weak Helper Function:

import xarray as xr

# function to fix raster
def clean_and_mask(raster_data, mask_layer, threshold):
    # check threshold
    if threshold < 0:
        raise ValueError("Invalid threshold")
        
    # mask it
    cleaned = raster_data.where(mask_layer > 0)
    
    # apply threshold
    final = cleaned.where(cleaned > threshold)
    
    return final

Your workspace

Draft your improved README, figure caption, and rewritten function below.

import xarray as xr

# Rewrite the clean_and_mask function here

8. Summary

Documentation is not an optional extra added after the real analysis is done; it is a core component of the scientific workflow. A reproducible project must be not only runnable but also understandable.

Key Takeaways:

Reproducibility depends on more than just code availability and environment files. It fundamentally depends on interpretability.