Chapter 6: Introduction to Spatial Data¶
Part II — Geographic Data Science
Learning Objectives¶
By the end of this chapter you will be able to:
- Distinguish vector and raster data models
- Understand coordinate reference systems (CRS)
- Load and inspect GeoJSON, Shapefile, and GeoTIFF files
# Standard imports — add chapter-specific imports below
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import geopandas as gpd
Part I never asked where anything was — world_data.csv had a country column, not a shape. Part II adds that missing dimension. Its main running example is the state of Maranhão, on Brazil's northern Atlantic coast — home to this book's own research group, and, not coincidentally, a state whose coastline of mangroves and shifting dunes is exactly the kind of place discrete spatial models get built for (Chapter 27 is one). A few chapters borrow other places when Maranhão cannot show what they need: the hills of Minas Gerais for elevation (Chapter 8), São Paulo for a well-documented development pattern (Chapter 12), and Amazonas for a land-cover grid (Chapter 16).
Vector vs Raster: Two Ways to Model Space¶
Every representation of geographic space reduces, in the end, to one of two models.
Vector data represents the world as discrete objects with exact geometry: a city is a point, a road is a line, a municipality's boundary is a polygon. Each object carries its own attributes — a municipality polygon might carry a name, a population, an area — in a structure that should already feel familiar from Part I: one row per object, one column per attribute, plus a special geometry column holding the shape itself. That structure is exactly what a GeoDataFrame is, as you'll see in a moment.
Raster data instead divides space into a regular grid of cells — pixels — and stores one or more values per cell: elevation, temperature, a land-cover class. There is no list of "objects"; there is only the grid itself and the value sitting in each cell. A satellite image is a raster. So, for that matter, is every digital photograph — a geographic raster just adds the information needed to place each pixel at a real-world coordinate.
Neither model is strictly better — each fits some phenomena more naturally than the other:
| Vector | Raster | |
|---|---|---|
| Natural fit for | Discrete objects — parcels, roads, administrative boundaries | Continuous fields — elevation, temperature, satellite imagery |
| Storage | A list of geometries + attribute table | A grid of values, uniform cell size |
| Precision | Exact, at the resolution the geometry was drawn | Limited by cell (pixel) size |
| Getting bigger | More geometries, each still exact | More cells, or coarser cells to keep the file size down |
This distinction is not a historical accident of GIS software — it runs all the way through this book. Chapter 7 works entirely in vector; Chapter 8 entirely in raster; Chapter 16 tackles the patterns for converting between them. And when Part IV turns to building spatial models, you'll find the same fork again: DisSModel's SpatialModel operates on a vector GeoDataFrame, its RasterModel on a NumPy array grid, and Chapter 22 introduces exactly this choice (Chapter 24 then measures it).
Geometry Types and WKT: The OGC Standard¶
"Vector data represents the world as discrete objects" is true, but it hides a real question: which objects, exactly? The Open Geospatial Consortium (OGC) answered this decades ago with Simple Features, a small, fixed hierarchy of geometry types that essentially every vector format and library in this book — Shapefile, GeoPackage, PostGIS, and shapely underneath geopandas itself — implements identically:
| Type | Built from | Example |
|---|---|---|
Point |
a single coordinate | a monitoring station |
LineString |
an ordered sequence of points | a road, a river |
Polygon |
one or more closed rings (an outer boundary, optional holes) | a municipality, a lake |
MultiPoint, MultiLineString, MultiPolygon |
several of the above, treated as one feature | an archipelago as one MultiPolygon |
GeometryCollection |
a mix of different types together | rarely needed, but always legal |
A Polygon's "one or more rings" detail is worth sitting with: the first ring is the outer boundary, and every ring after it is a hole cut out of the interior — Chapter 7's São Luís example, with its islands, is a MultiPolygon; a municipality with a hole for an enclave it doesn't administer would be a Polygon with two rings, not two separate shapes.
from shapely.geometry import Point, LineString, Polygon
from shapely import wkt
station = Point(-44.30, -2.53)
print(repr(station))
print(station.wkt)
<POINT (-44.3 -2.53)> POINT (-44.3 -2.53)
That POINT (-44.3 -2.53) text — printed automatically whenever a shapely geometry is shown — is not a Python-specific format. It's Well-Known Text (WKT), the exact OGC-standard serialization every geometry type above has: coordinates for a Point, a coordinate sequence for a LineString, ring(s) of coordinates for a Polygon. A GeoDataFrame's geometry column, under the hood, is nothing more than a column of shapely objects that all know how to read and write this format:
polygon_wkt = "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))"
recovered = wkt.loads(polygon_wkt)
print(type(recovered))
print(recovered.area)
<class 'shapely.geometry.polygon.Polygon'> 16.0
wkt.loads() going one direction, .wkt going the other — this round trip is exactly what happens invisibly every time geopandas.read_file() (Chapter 7) parses a Shapefile or GeoPackage, and it's why a WKT string is a perfectly valid, if verbose, way to hand a geometry to any spatial database or web API that speaks the OGC standard, PostGIS included.
Coordinate Reference Systems¶
A polygon's coordinates are meaningless without knowing what they're measured against. (-44.3, -2.5) could be degrees of longitude and latitude on a sphere, or it could be meters on a flat projected grid — and a Coordinate Reference System (CRS) is the piece of metadata that says which.
Two broad families matter here:
- Geographic CRS — coordinates are angles (degrees) measured on a model of the Earth's curved surface: longitude east-west, latitude north-south. WGS84 (EPSG:4326) is the global default, used by GPS; SIRGAS 2000 (EPSG:4674) is the geographic CRS Brazil's own agencies, including IBGE, publish data in. Distances and areas computed directly in degrees are not physically meaningful — a degree of longitude spans a very different ground distance at the Equator than near a pole.
- Projected CRS — the curved Earth has been mathematically flattened onto a plane, and coordinates are linear units — usually meters. UTM (Universal Transverse Mercator) divides the world into 60 numbered zones, each narrow enough that distortion inside it stays small; Maranhão sits mostly in UTM zone 23S (EPSG:31983 on the SIRGAS 2000 datum). Distances and areas computed in a projected CRS are physically meaningful, which is exactly why distances such as Chapter 16's distance-to-city driver are computed in one. For a whole country no single UTM zone will do; Brazil's national products often use an Albers equal-area projection instead, which keeps the area of every cell correct across the country. The Brazil Data Cube's version of it (EPSG:10857) is the projection of the national model grids in Chapters 28 and 31.
Every EPSG code names one specific, unambiguous CRS. GeoPandas exposes the current one on any GeoDataFrame as .crs, and reprojects with .to_crs():
# gdf.crs # inspect the current CRS
# gdf_projected = gdf.to_crs(epsg=31983) # reproject to UTM 23S (meters)
The rule of thumb this book follows from here on: store and exchange data in a geographic CRS (WGS84 or SIRGAS 2000), which most data portals publish in; reproject to a projected CRS the moment you need to measure a real distance or area — a UTM zone for a small region, an equal-area projection such as EPSG:10857 for areas over a large one — and to Web Mercator (EPSG:3857) when drawing over web basemap tiles, as Chapter 15 does.
Common Spatial Formats¶
Vector and raster data each have their own common file formats, and it's worth being able to recognize them on sight:
Vector
| Format | Extension(s) | Notes |
|---|---|---|
| Shapefile | .shp + .shx, .dbf, and usually .prj |
The oldest common format, still everywhere (IBGE publishes in it); a "single" shapefile is really a bundle of files that must stay together |
| GeoJSON | .geojson (or plain .json) |
Human-readable text, geometry and attributes together in one file; ubiquitous on the web |
| GeoPackage | .gpkg |
A single SQLite database file, can hold several layers at once; the modern, tidier alternative to a shapefile |
Raster
| Format | Extension(s) | Notes |
|---|---|---|
| GeoTIFF | .tif, .tiff |
The standard for a single raster grid — an ordinary TIFF image plus georeferencing metadata; Chapter 8 is built around it |
| NetCDF | .nc |
Built for stacks of grids sharing coordinates — typically a time series, or several variables on the same grid; Chapter 9's xarray chapter is built around it |
All of them, vector or raster, ultimately decompress to the same two ingredients Vector vs Raster already introduced: geometry (or a grid) plus attributes (or cell values).
Loading Spatial Data in Python¶
geopandas.read_file() is the single entry point for essentially every vector format above — Shapefile, GeoJSON, GeoPackage — and it works directly on a URL, exactly as pandas.read_csv() did for world_data.csv back in Chapter 3. IBGE, Brazil's official statistics agency, publishes municipal boundaries for every state at a stable URL; here is Maranhão's, the running example for the rest of Part II:
url = (
"https://geoftp.ibge.gov.br/organizacao_do_territorio/"
"malhas_territoriais/malhas_municipais/municipio_2025/"
"UFs/MA/MA_Municipios_2025.zip"
)
maranhao = gpd.read_file(url)
maranhao.head()
| CD_MUN | NM_MUN | CD_RGI | NM_RGI | CD_RGINT | NM_RGINT | CD_UF | NM_UF | SIGLA_UF | CD_REGIAO | NM_REGIAO | SIGLA_RG | CD_CONCURB | NM_CONCURB | AREA_KM2 | geometry | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2100402 | Altamira do Maranhão | 210010 | Bacabal | 2102 | Santa Inês - Bacabal | 21 | Maranhão | MA | 2 | Nordeste | NE | NaN | NaN | 524.374 | POLYGON ((-45.62607 -4.26241, -45.62499 -4.262... |
| 1 | 2108900 | Poção de Pedras | 210012 | Pedreiras | 2102 | Santa Inês - Bacabal | 21 | Maranhão | MA | 2 | Nordeste | NE | NaN | NaN | 990.660 | POLYGON ((-44.64375 -4.83662, -44.64388 -4.836... |
| 2 | 2100709 | Anajatuba | 210004 | Itapecuru Mirim | 2101 | São Luís | 21 | Maranhão | MA | 2 | Nordeste | NE | NaN | NaN | 940.489 | POLYGON ((-44.58949 -3.44717, -44.59039 -3.446... |
| 3 | 2108801 | Pirapemas | 210004 | Itapecuru Mirim | 2101 | São Luís | 21 | Maranhão | MA | 2 | Nordeste | NE | NaN | NaN | 688.761 | POLYGON ((-44.16229 -3.88911, -44.16266 -3.891... |
| 4 | 2100600 | Amarante do Maranhão | 210019 | Imperatriz | 2105 | Imperatriz | 21 | Maranhão | MA | 2 | Nordeste | NE | NaN | NaN | 7441.771 | POLYGON ((-46.32997 -5.55052, -46.32997 -5.551... |
Notice read_file() needed no format flag — it inspected the URL and the file inside the zip itself to work out that this was a shapefile. The result is a GeoDataFrame: everything from Chapter 3's DataFrame (.head(), .info(), boolean filtering, .groupby()) still applies, plus the geometry column and .crs:
print("CRS:", maranhao.crs)
print("Municipalities:", len(maranhao))
maranhao.plot(edgecolor="black", facecolor="none", figsize=(6, 6))
plt.title("Maranhão — 217 municipalities")
plt.show()
CRS: EPSG:4674 Municipalities: 217
That single .plot() call is doing more than it looks — it read every polygon's geometry, worked out a bounding box, and drew each municipality's boundary, all from three lines of code. Chapter 7 picks up exactly here and starts asking real questions of this same GeoDataFrame: which municipality is which, how to select just the state capital, and how to combine this boundary data with other layers.
Exercises¶
- Inspect before you trust. Load the Maranhão municipalities
GeoDataFrameabove and run.info()on it. How many columns does IBGE's shapefile carry beyondgeometry? What does theCD_MUNcolumn look like — and given Chapter 4's cleaning checklist, would you trust its dtype at a glance, or check it? - Reproject and compare. Reproject
maranhaoto UTM zone 23S (maranhao.to_crs(epsg=31983)) and plot it next to the original. The shape on screen barely changes — so what actually changed? (Hint: check.total_boundsbefore and after.) - Vector or raster? For each of the following, decide which data model fits more naturally, and why: (a) the boundary of a national park, (b) daily rainfall across a state, (c) the location of every fire station in a city, (d) satellite-derived forest cover for the Amazon.
- Find a format. Using the Common Spatial Formats table, name a format you'd choose to publish a single elevation grid for public download, and a different one you'd choose to share a table of 5,000 sensor locations with a colleague who doesn't use GIS software. Justify each choice in one sentence.
# Your code here
Summary¶
Key concepts introduced¶
- The vector/raster split: discrete objects with exact geometry versus a continuous grid of cell values — a distinction that runs through the rest of this book, including DisSModel's own
SpatialModel/RasterModelsplit in Part IV - Coordinate Reference Systems: geographic (degrees, e.g. WGS84 / SIRGAS 2000) versus projected (meters, e.g. UTM), and the rule of thumb — display in geographic, measure in projected
- The common file formats for each model: Shapefile, GeoJSON, and GeoPackage for vector; GeoTIFF and NetCDF for raster
geopandas.read_file()as the universal vector entry point, reading directly from a URL exactly aspandas.read_csv()did in Chapter 3
Maranhão's municipal boundaries, loaded here, carry through as the running example for the rest of Part II — Chapter 7 starts working with them directly.
Further Reading¶
- geopandas documentation, Introduction to GeoPandas: https://geopandas.org/en/stable/getting_started/introduction.html
- IBGE, Malhas Territoriais (the source of the Maranhão boundaries used above): https://www.ibge.gov.br/geociencias/organizacao-do-territorio/malhas-territoriais.html
- epsg.io — a searchable reference for any EPSG code, including 4326 (WGS84), 4674 (SIRGAS 2000), and 31983 (SIRGAS 2000 / UTM zone 23S): https://epsg.io/