Working with Geospatial Data in Python — PART 1
By: Ben Bogart
Tags: Geospatial, GIS, Yosemite, Matplotlib, Python

Let's be honest; geospatial data is scary. I don’t think there is any good reason for this other than that information on how to work with geospatial data in Python is not as easily accessible as information on working with other kinds of data.

This is the first article in a series focused on various must-know aspects of working with geospatial data in Python. Over the next several weeks, we will look at the main topics in working with geospatial data, including file formats, data types, coordinate systems, and tools to work with the data. I will try to keep these posts bite-sized. I hope that we will develop a comfort with the topic throughout the series, and our fear will fade away.

The first 2/3 of this post introduces concepts. In the last 1/3, we will get our hands dirty by pulling in two LiDAR rasters from opentopography.org and calculating a Canopy Height Model from a Digital Surface Model and a Digital Terrain Model.

I’ve organized the article is organized as follows:

Data Types Metadata Hands-on with raster data.

Data Types

There are two primary data types used in geospatial data: raster data and vector data. This is probably not surprising. If you have done any work on anything visual on a computer recently, you have probably interacted with both. Below we’ll take a brief look at each kind of data.

Raster Data

Raster Data is data in a grid. This is very similar to the data in a photo. In the case of satellite images, this data is like a photo. The difference between this geospatial raster data and photographic raster data is that geospatial data has a way of relating that data to specific locations on earth. It can also contain either float or integer data, unlike an image that only contains integer data.

Image by author

Raster data can also be categorical. For example, you might classify ground cover as low, medium, or high. In such a case, we map integer data to a category.

Image by author

Vector Data

Vector geospatial data is also very similar to its design counterpart that you might have worked with in Adobe Illustrator. Again, the difference is that geospatial vector data is mapped to specific places on earth.

Vector data comes in three types.

Image by author

Each type of vector data can represent different kinds of information.

A point can represent the location of something like a tree or a building.

can represent the location of something like a tree or a building. A line can represent a road or a river.

can represent a road or a river. A polygon can represent geographical units like the boundaries of a country, state, or other areas like national parks.

Raster Metadata

Geospatial rasters use metadata to communicate many vital aspects of the data to us. Below are a couple of the essential elements found in a raster file’s metadata.

Coordinate Reference Systems

I mentioned above that the essential characteristic of geospatial data is that it connects the data to a specific point on earth. The mechanism which makes this connection is the Coordinate Reference System or CRS.

There is one significant complication. The earth is a globe, and our data representation is two-dimensional. Like any paper map, our data is projected into two dimensions. Different CRS handle this projection differently. For example, some CRS are focused on only a specific area of the earth. Others are more general for projecting large portions of the earth's surface. There are many, many different CRS. Some are more common than others, but for our purposes, we need to know that two rasters with different CRS won’t line up when we visualize or make calculations from them. We will need to have a way to convert both rasters to the same CRS (more on this in a future article).

NoData value

Another critical piece of metadata is the NoData value. The grid format of a raster requires there to be data in every square. The NoData value tells us the number used by the raster to indicate that it has no value for that part of the grid. For integer and float rasters, common NoData data are -9999 and -3.4e+38 respectively, but any value can be used.

If you want to take a deeper dive into other metadata often found with geospatial raster data, this ArcGIS help page does an excellent job of running through the possibilities.

Working with Raster Data

We can all admit that we are by no means experts in geospatial data yet, but that shouldn’t stop us from digging in and seeing what we can accomplish.

For this example, I grabbed some LiDAR data from the Illilouette Creek Basin Lidar Survey, Yosemite Valley, CA 2018 dataset on opentopography.org. The data is open-source, and the interface is straightforward to use. If you prefer, I’ve uploaded the exact data I’m using the Github repository associated with this article.

The next thing we need is a tool that will let us read in our raster data. xarray is a python library that allows us to work with multi-dimensional labeled arrays in python. It's something like Pandas for multi-dimensional data. rioxarray is a package built on top of xarray that provides methods for reading and writing raster files with their metadata. To load in our data, we do the following.

import rioxarray terrain_file = 'data/rasters_CA18Thompson/output_be.tif'

terrain = rioxarray.open_rasterio(terrain_file) surface_file = 'data/rasters_CA18Thompson/output_hh.tif'

surface = rioxarray.open_rasterio(surface_file)

If we take a look at surface we can see some information about the data.

Image by author

We can see that this raster file has one band and that the data on both the x and y axes are float64 values. We can also see the _FillValue which is the same as the NoData value discussed above. To take a more direct look at the metadata, we can inspect individual properties of our xarray . The raster-specific data is in a sub-class called rio .

# Get no data value

surface.rio.nodata

1.70141e+38

# Show surface CRS

print(surface.rio.crs)

EPSG:6340

The above tells us that our NoData value is 1.70141e+38 , and our CRS is EPSG:6340. Go to https://epsg.io/6340 to learn more about this CRS. For our purposes, we only need to know that the CRS for the two files matches. Fortunately, it does.

# Show terrain CRS

print(terrain.rio.crs)

EPSG:6340

Let's take a look at what happens if we plot one of these rasters.

plt.figure(figsize=(10,8))

surface.plot()

Image by author

Hmm. That was less impressive than you hoped, wasn’t it? Looking at the color map on the right-hand side of the plot, we see that the max value is quite large and very similar to our NoData value. We forgot to replace our no data value with np.nan which is the pythonic way of representing missing values. The easiest way to do this is to reimport the file passing masked=True to the rasterio method.

# reload the surface raster masking nan values

surface = rioxarray.open_rasterio(surface_file, masked=True) # plot surface raster

plt.figure(figsize=(10,8))

surface.plot()

plt.title('Digital Surface Model')

Image by author

That's much better! It is clear now that the white area is outside the dataset and does not contain measurements. Let's take a look at the surface model raster.

# reload the terrain raster masking nan values

terrain = rioxarray.open_rasterio(terrain_file, masked=True) # plot terrain raster

plt.figure(figsize=(10,8))

terrain.plot()

plt.title('Digital Terrain Model')

Image by author

That looks very similar to the surface model.

Lastly, we are going to do a little raster math. We want to know the height of the canopy. In other words, we want to see the height of trees and ground cover relative to the ground. We have a surface model which gives us the contour of all the objects on the land and a terrain model which provides us with the contour of the land itself. To get a canopy height model, we subtract the terrain model from the surface model.

# calculate canopy height model

canopy = surface - terrain # plot surface raster

plt.figure(figsize=(10,8))

canopy.plot()

plt.title('Canopy Height Model')

There we have it. We can see this area mostly has low or no ground cover with some sparse trees up to 60 meters tall.

Conclusion

I hope the big takeaway from this article is that geospatial data is not that scary. It deals with one problem that we don’t usually have to deal with in flat datasets: projecting a 3-dimensional globe onto a 2-dimensional raster. We will learn about reprojecting and dealing with data from different sources in future articles in this series. Still, today we learned some basic terminology and worked with some basic geospatial python packages.

The is just the first article in this series. Follow me here on medium if you want to get notified when I post the next article in this series.

Resources