Introduction to GeoPanadas#

A considerable proportion of the data generated every day is inherently spatial. From Earth Observation data and GPS data to data included in all kinds of maps, spatial data –also known sometimes as geospatial data or geographic information– are data for which a specific location is associated with each record.

There are many tools suited for geospatial data science. In this notebook, we will focus on how to use the GeoPandas library to work with geospatial data. We will learn how to read, write, and manipulate geospatial data using GeoPandas. We will also learn how to visualize geospatial data using GeoPandas, as well as some basic geospatial operations such as spatial joins.

Basics of GeoPandas#

GeoPandas, as its name suggests, is an extension of the pandas library that helps you work with geospatial data.

The main data structures in geopandas are GeoSeries and GeoDataFrame which extend the capabilities of Series and DataFrames from pandas.

The main difference between geodataframes and pandas dataframes is that a geodataframe should contain one column for geometries. By default, the name of this column is 'geometry', which contains the geometries (points, lines, polygons, multipolygons etc.).

image from Geopandas website

Install Geopandas:#

To avoid complexities, GeoPandas recommends installing the library using the conda package manager.

conda install geopandas

Reading spatial data#

To read spatial data, GeoPandas comes with the geopandas.read_file() function. This function can automatically read most of the occurring vector-based spatial data.

Some of the most common vector data formats are:

  • Shapefile. As the industry standard, shapefiles are the most common vector data format.

  • GeoJSON. It’s a newer format for geospatial data released in 2016. Unlike shapefiles, GeoJSON is a single file, making it easier to work with.

Reading from a shapefile:#

import geopandas as gpd

gdf = gpd.read_file('path/to/shapefile.shp')


Reading from a GeoJSON file:#

import geopandas as gpd

gdf = gpd.read_file('path/to/geojson.geojson')


As an example, let’s load the boundaries for the City of Calgary which you can have access to it in the City of Calgary Open Data Portal.

We have the data as shape file (.shp), so we only need to call the data:

# import GeoPandas
import geopandas as gpd


# reading the shapefile
boundary = gpd.read_file('City_Boundary.shp')

boundary = boundary[['CITY', 'geometry']]
boundary
CITY geometry
0 CALGARY POLYGON ((-114.01328 51.21243, -114.01327 51.2...
# display the type of the dataframe
type(boundary)
geopandas.geodataframe.GeoDataFrame
# display the first rows of the dataframe
boundary.head()
CITY geometry
0 CALGARY POLYGON ((-114.01328 51.21243, -114.01327 51.2...
boundary.info() 
<class 'geopandas.geodataframe.GeoDataFrame'>
RangeIndex: 1 entries, 0 to 0
Data columns (total 2 columns):
 #   Column    Non-Null Count  Dtype   
---  ------    --------------  -----   
 0   CITY      1 non-null      object  
 1   geometry  1 non-null      geometry
dtypes: geometry(1), object(1)
memory usage: 148.0+ bytes

As we can see, the geometry column has Dtype of “geometry” which is a special data type in GeoPandas. It is used to store the spatial data. The geometry column contains the polygon data for each community district. The polygon data is used to draw the boundaries of the community districts on the map.

Reading from a CSV file:#

When reading from a CSV file, you need to create a GeoDataFrame from a DataFrame by specifying the geometry column. Depending on your data, if you have a latitude and longitude column, you can create a geometry column from them using the gpd.points_from_xy function. If you have a WKT column, you can create a geometry column from it using the gpd.GeoDataFrame function. If you have line or polygon geometries, you can create a geometry column from them using the gpd.GeoDataFrame function.

For example, if you read a point CSV file with columns lon and lat and you want to create a GeoDataFrame from it, the geometry column can be created from lon and lat coordinates using the gpd.points_from_xy function.

import geopandas as gpd

gdf = gpd.read_file('path/to/csv.csv')
gdf = gpd.GeoDataFrame(gdf, geometry=gpd.points_from_xy(gdf.lon, gdf.lat))

Note

Take a look at GeoPandas documentation for more examples: https://geopandas.org/en/latest/gallery/create_geopandas_from_pandas.html

Now, let’s imagine that we have a tex or csv file that includes points with latitude and longitude column. In this case, after loading the data, we have to define the geometry column from the latitude and longitude columns.

In this example, we are going to load the dataset of a list of COVID-19 and flu vaccination locations in Canada.

(Link to the dataset.)

import geopandas as gpd

# reading the csv file
vaccination_location = gpd.read_file('Vaccination_Locations.csv')

Let’s look at the data:

# display the top 3 rows of the dataframe
vaccination_location.head(3)
Address1 Address2 City Phone VaccineType latitude longitude Take Home Test Kits Antiviral Confirmed Title
0 104-375 ST. ALBERT TR ST ALBERT 780-458-8000 ADULT XBB 53.64320444 -113.6226777 YES NO LONDON DRUGS #48
1 601-401 COOPERS BLVD SW AIRDRIE 587-254-5810 ADULT XBB 51.25705871 -114.0210533 YES YES SAVE-ON-FOODS PHARMACY #6603 - AIRDRIE WEST
2 110-10 PELICAN PL SYLVAN LAKE 403-864-5192 ADULT XBB 52.29941313 -114.0959723 YES YES PELICAN PLACE PHARMACY

As we can see, the geometry column contains None values. This is because the geometry column is empty and we need to fill it with the correct geometry values. We can do this by using the latitude and longitude columns to create a Point geometry. We can use the following code to create a Point geometry from the latitude and longitude columns and fill the geometry column with the correct geometry values.

# create a geometry column
vaccination_location['geometry'] = gpd.points_from_xy(vaccination_location['longitude'], vaccination_location['latitude'])

# display the top 3 rows of the dataframe
vaccination_location.head(3)
Address1 Address2 City Phone VaccineType latitude longitude Take Home Test Kits Antiviral Confirmed Title geometry
0 104-375 ST. ALBERT TR ST ALBERT 780-458-8000 ADULT XBB 53.64320444 -113.6226777 YES NO LONDON DRUGS #48 POINT (-113.62268 53.6432)
1 601-401 COOPERS BLVD SW AIRDRIE 587-254-5810 ADULT XBB 51.25705871 -114.0210533 YES YES SAVE-ON-FOODS PHARMACY #6603 - AIRDRIE WEST POINT (-114.02105 51.25706)
2 110-10 PELICAN PL SYLVAN LAKE 403-864-5192 ADULT XBB 52.29941313 -114.0959723 YES YES PELICAN PLACE PHARMACY POINT (-114.09597 52.29941)

As we can see, the vaccination_location dataframe now has a geometry column that contains the Point objects. We can now convert the dataframe into a GeoDataFrame.

# convert the dataframe to a GeoDataFrame
vaccination_location = gpd.GeoDataFrame(vaccination_location, geometry='geometry')

# display the info of the geodataframe
vaccination_location.info()
<class 'geopandas.geodataframe.GeoDataFrame'>
RangeIndex: 188 entries, 0 to 187
Data columns (total 11 columns):
 #   Column               Non-Null Count  Dtype   
---  ------               --------------  -----   
 0   Address1             188 non-null    object  
 1   Address2             188 non-null    object  
 2   City                 188 non-null    object  
 3   Phone                188 non-null    object  
 4   VaccineType          188 non-null    object  
 5   latitude             188 non-null    object  
 6   longitude            188 non-null    object  
 7   Take Home Test Kits  188 non-null    object  
 8   Antiviral Confirmed  188 non-null    object  
 9   Title                188 non-null    object  
 10  geometry             188 non-null    geometry
dtypes: geometry(1), object(10)
memory usage: 16.3+ KB

Exploring GeoDataFrames#

WE saw that the geometry column can contain any type of vector data, such as points, lines, and polygons.

Another important feature of GeoDataFrames is that every geometry comes with associated CRS information. This CRS information tells GeoPandas where the coordinates are located on Earth. This information is key for spatial analysis. For example, if you need to combine two spatial datasets, you need to make sure they are expressed in the same CRS. Otherwise, you won’t get the result you expected.

There are two main categories of CRS:

  • Geographic coordinates. They define a global position in degrees of latitude and longitude relative to the equator and the prime meridian. With this system, we can easily specify any location on earth. It is used widely, for example, in GPS. The most popular CRS is EPSG:4326, also called WGS84.

  • Projected coordinates. While Earth is round, we usually represent it on a two-dimension map. Projected coordinates express locations in X and Y dimensions, thereby allowing us to work with a length unit, such as meters, instead of degrees, which makes the analysis more convenient and effective. However, moving from the three-dimensional Earth to a two-dimensional map will inevitably result in distortions. That’s why there are different approaches to creating projected coordinates. For example, many countries have adopted a standard projected CRS for their particular geography.

Hint

There’s much more about CRS, but it’s out of the scope of this tutorial. To find out additional information about projection and CRS you can take a look at Geopandas documentation.

In GeoPandas, the CRS information is stored in the crs attribute:

# get the crs of the boundary geodataframe
boundary.crs
<Geographic 2D CRS: EPSG:4326>
Name: WGS 84
Axis Info [ellipsoidal]:
- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)
Area of Use:
- name: World.
- bounds: (-180.0, -90.0, 180.0, 90.0)
Datum: World Geodetic System 1984 ensemble
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich

As we can see, the crs of the boundary geodataframe is ‘Geographic 2D CRS: EPSG:4326’.

Now let’s see the crs of the vaccination_location geodataframe.

# get the crs of the vaccination_location geodataframe
vaccination_location.crs

We can see that the vaccination_location geodataframe does not have a crs. This is because the csv file does not contain the crs information. We can set the crs of the vaccination_location geodataframe to the crs of the boundary geodataframe using the to_crs method. In this way, we can ensure that both the geodataframes have the same crs so that we can perform spatial operations such spatial join, overlay, etc.

# set the crs of the vaccination_location geodataframe to the crs of the boundary geodataframe
vaccination_location.crs = boundary.crs

#display the crs of the vaccination_location geodataframe
vaccination_location.crs
<Geographic 2D CRS: EPSG:4326>
Name: WGS 84
Axis Info [ellipsoidal]:
- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)
Area of Use:
- name: World.
- bounds: (-180.0, -90.0, 180.0, 90.0)
Datum: World Geodetic System 1984 ensemble
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich

Now both dataframes have the same crs, we can proceed to the next steps.

Plotting the geometries#

It is always a good idea to plot the geometries to see if they are correctly placed on the map. We can use the plot() method to plot the geometries of the geodataframes.

# plot the boundary geodataframe
boundary.plot()
<Axes: >
../../_images/2b9a8365d5ee15cf1dbdaf290f6e5a1c9622f5dacf3b49ad88dbded65ee3be22.png
# plot the vaccination_location geodataframe
vaccination_location.plot()
<Axes: >
../../_images/86bdfde968760b1b5d05c189c8aa405d65c413735f19e228119a56c27d7b5967.png

From previous turtoials, you are familiar with folium. Now, lets use folium to plot the boundary and vaccination_location geodataframes

boundary
CITY geometry
0 CALGARY POLYGON ((-114.01328 51.21243, -114.01327 51.2...
import folium

# create a map object with Calgary latitude and longitude as the center
m = folium.Map(location=[51.0447, -114.0719], zoom_start=10)

# plot the boundary geodataframe
folium.GeoJson(boundary).add_to(m)

# plot the vaccination_location geodataframe
for idx, row in vaccination_location.iterrows():
    folium.Marker([row['latitude'], row['longitude']]).add_to(m)
    
# display the map
m
Make this Notebook Trusted to load map: File -> Trust Notebook

Spatial Relationships with GeoPandas#

One of the key aspects of geospatial data is how they relate to each other in space. GeoPandas leverages the power of pandas and shapely packages to perform all kinds of spatial relationships between spatial datasets. In this section, we will cover some of the most common operations.

Spatial Joins#

Spatial joins allow the merging of two GeoDataFrames based on their spatial relationships.

In GeoPandas, the spatial join operation is available as the sjoin() function. The first argument we specify is the GeoDataFrame to which we want to add information, and the second argument is the GeoDataFrame that contains the information we want to add. Then, we have to specify the type of join. Finally, the parameter “predicate” tells GeoPandas which spatial relationship we want to use to match both datasets. Some of the most common relationships “intersects”, “contains”, and “within”.

Note

You can refer to https://geopandas.org/en/stable/gallery/spatial_joins.html for more information about spatial join.

For example, let’s say that we want to find the vaccination locations that are within the city boundary. We can use the sjoin method to perform a spatial join between the two geodataframes. The sjoin method will return a new geodataframe with the vaccination locations that are within the city boundary.

# perform a spatial join between the boundary and vaccination_location geodataframes
vaccination_location_within_boundary = gpd.sjoin(vaccination_location, boundary, predicate='within')
vaccination_location_within_boundary
Address1 Address2 City Phone VaccineType latitude longitude Take Home Test Kits Antiviral Confirmed Title geometry index_right CITY
5 113-5271 MEMORIAL DR SE CALGARY 403-235-0081 51.05308993 -113.9562593 YES NO UNIVERSAL PHARMACY POINT (-113.95626 51.05309) 0 CALGARY
8 11-3735 RUNDLEHORN DR NE CALGARY 403-798-9280 ADULT XBB 51.07151283 -113.97887 YES YES RUNDLE PHARMACY POINT (-113.97887 51.07151) 0 CALGARY
15 850 SADDLETOWNE CIR NE CALGARY 403-293-1670 51.12375399 -113.9447021 YES NO FRESHCO PHARMACY #4291 POINT (-113.9447 51.12375) 0 CALGARY
16 3130-11 ROYAL VISTA DR NW CALGARY 403-800-6646 ADULT XBB 51.1500556 -114.2064574 NO NO CALGARY DRUG MART #03 POINT (-114.20646 51.15006) 0 CALGARY
25 400-1000 HAMPTONS DR NW CALGARY 403-299-6715 ADULT XBB 51.14347029 -114.1295243 YES YES CALGARY CO-OP PHARMACY #17 POINT (-114.12952 51.14347) 0 CALGARY
27 771 NORTH MOUNT DR NW CALGARY 403-454-5244 ADULT XBB 51.08260367 -114.0959373 YES NO MELL PHARMACY POINT (-114.09594 51.0826) 0 CALGARY
30 114-8180 MACLEOD TR SE CALGARY 403-253-7383 ADULT XBB 50.98199571 -114.0703651 YES YES MEDICINE SHOPPE PHARMACY 111 POINT (-114.07037 50.982) 0 CALGARY
32 108-5401 TEMPLE DR NE CALGARY 403-460-0303 ADULT XBB 51.0851268 -113.9569255 YES NO TEMPLE MEDICAL CLINIC PHARMACY POINT (-113.95693 51.08513) 0 CALGARY
35 32-4604 37 ST SW CALGARY 825-210-9716 51.01325442 -114.1399128 YES NO RUTLAND COMPOUNDING PHARMACY POINT (-114.13991 51.01325) 0 CALGARY
36 4-7930 BOWNESS RD NW CALGARY 403-288-4041 ADULT XBB 51.08913208 -114.2011653 YES YES MEDICINE SHOPPE PHARMACY 199 POINT (-114.20117 51.08913) 0 CALGARY
37 356-100 ANDERSON RD SE CALGARY 403-271-7877 50.95353734 -114.0667463 NO YES SHOPPERS DRUG MART #316 POINT (-114.06675 50.95354) 0 CALGARY
41 860 13 ST SE CALGARY 587-480-0178 ADULT XBB 51.04042942 -114.0309414 YES YES SAGE PLUS CLINICAL PHARMACY POINT (-114.03094 51.04043) 0 CALGARY
47 1711 4 ST SW CALGARY 403-228-5067 ADULT XBB 51.0372509 -114.0717656 YES YES RIDEAU PHARMACY POINT (-114.07177 51.03725) 0 CALGARY
48 11-600 CROWFOOT CRES NW CALGARY 403-455-9939 ADULT XBB 51.12979307 -114.2039427 YES YES MEDICINE SHOPPE PHARMACY 260 POINT (-114.20394 51.12979) 0 CALGARY
51 2079 146 AVE SE CALGARY 403-278-1565 ADULT XBB 50.9198697 -114.0148617 YES YES DRUG SHOPPE POINT (-114.01486 50.91987) 0 CALGARY
58 240-520 3 AVE SW CALGARY 403-454-4412 51.0507308 -114.0688522 YES YES MEDIMAX PHARMACY POINT (-114.06885 51.05073) 0 CALGARY
65 8855 MACLEOD TR SW CALGARY 403-640-7617 ADULT XBB 50.97449255 -114.0719797 YES NO SAVE-ON-FOODS PHARMACY #6634 POINT (-114.07198 50.97449) 0 CALGARY
66 1219 11 AVE SW CALGARY 587-392-5857 ADULT XBB 51.04319727 -114.0900798 YES NO PHARMASAVE JO'S BELTLINE PHARMACY POINT (-114.09008 51.0432) 0 CALGARY
73 7-2111 CENTRE ST NW CALGARY 403-276-7076 ADULT XBB 51.07131481 -114.0629897 YES NO EXALAN PHARMACY POINT (-114.06299 51.07131) 0 CALGARY
81 159 MAHOGANY CENTRE SE CALGARY 403-299-5808 ADULT XBB 50.89697308 -113.9348758 YES YES CURIS RX PHARMACY MAHOGANY POINT (-113.93488 50.89697) 0 CALGARY
83 7048-2060 SYMONS VALLEY PKWY NW CALGARY 403-266-1450 ADULT XBB 51.17595078 -114.10752 YES NO FRESHCO PHARMACY #4311 POINT (-114.10752 51.17595) 0 CALGARY
90 4122 BRENTWOOD RD NW CALGARY 403-299-4308 51.08844285 -114.1308792 YES YES CALGARY CO-OP PHARMACY #4 POINT (-114.13088 51.08844) 0 CALGARY
97 109-5809 MACLEOD TR SW CALGARY 403-460-3825 ADULT XBB 51.00187606 -114.0724615 YES NO HEALTH WAY PHARMACY POINT (-114.07246 51.00188) 0 CALGARY
102 78-4307 130 AVE SE CALGARY 403-475-8612 ADULT XBB 50.93059254 -113.9730717 NO YES SOUTH TRAIL PHARMACY POINT (-113.97307 50.93059) 0 CALGARY
106 100-2675 36 ST NE CALGARY 403-291-0076 ADULT XBB 51.07748805 -113.9825869 NO YES MEDICINE SHOPPE PHARMACY 171 POINT (-113.98259 51.07749) 0 CALGARY
110 114-10325 BONAVENTURE DR SE CALGARY 403-271-8160 50.96051322 -114.0697877 YES YES NULIFE PHARMACY POINT (-114.06979 50.96051) 0 CALGARY
111 103A-3009 14 ST SW CALGARY 403-764-3080 ADULT XBB 51.02730152 -114.0950098 YES YES PHARM DRUGSTORE POINT (-114.09501 51.0273) 0 CALGARY
114 4700 130 AVE SE CALGARY 403-257-6530 ADULT XBB 50.93356613 -113.9668006 NO YES LOBLAW PHARMACY #1546 POINT (-113.9668 50.93357) 0 CALGARY
122 101-83 DEERPOINT RD SE CALGARY 587-554-5588 ADULT XBB 50.9268828 -114.0289032 NO YES DEER VALLEY PHARMACY POINT (-114.0289 50.92688) 0 CALGARY
125 200 STEWART GREEN SW CALGARY 403-246-0336 ADULT XBB 51.01460372 -114.1670197 NO NO SAFEWAY PHARMACY #8852 POINT (-114.16702 51.0146) 0 CALGARY
129 100-250 SHAWVILLE BLVD SE CALGARY 403-299-4439 ADULT XBB 50.90319486 -114.0637289 YES YES CALGARY CO-OP PHARMACY #14 POINT (-114.06373 50.90319) 0 CALGARY
131 9-15 MILLRISE BLVD SW CALGARY 587-329-5858 ADULT XBB 50.9205757 -114.076119 NO YES MILLRISE PHARMACY POINT (-114.07612 50.92058) 0 CALGARY
141 128-6707 ELBOW DR SW CALGARY 403-259-6893 ADULT XBB 50.99368257 -114.0840543 YES NO MAYFAIR PHARMACY POINT (-114.08405 50.99368) 0 CALGARY
145 7-1603 62 AVE SE CALGARY 587-391-9878 ADULT XBB 50.99746705 -114.020351 YES NO OGDEN PHARMACY POINT (-114.02035 50.99747) 0 CALGARY
149 9-728 NORTHMOUNT DR NW CALGARY 403-289-9181 ADULT XBB 51.08230306 -114.09281 YES YES CAMBRIAN PHARMACY POINT (-114.09281 51.0823) 0 CALGARY
152 104-305 10 ST NW CALGARY 825-540-1500 ADULT XBB 51.05451586 -114.0862431 YES YES HEATHERS PHARMACY POINT (-114.08624 51.05452) 0 CALGARY
156 4940 RICHMOND RD SW CALGARY 403-299-4487 ADULT XBB 51.01911946 -114.1589891 NO YES CALGARY CO-OP PHARMACY #6 POINT (-114.15899 51.01912) 0 CALGARY
158 110-8500 BLACKFOOT TR SE CALGARY 403-253-5700 ADULT XBB 50.97661519 -114.046636 YES YES MEADOWS MILE PHARMACY POINT (-114.04664 50.97662) 0 CALGARY
161 105-4411 16 AVE NW CALGARY 403-247-9502 ADULT XBB 51.06805148 -114.1588588 YES NO MEDICINE SHOPPE PHARMACY 178 POINT (-114.15886 51.06805) 0 CALGARY
162 125-8888 COUNTRY HILLS BLVD NW CALGARY 403-239-7455 51.15178225 -114.2131886 YES NO SOBEYS PHARMACY #1129 POINT (-114.21319 51.15178) 0 CALGARY
164 2790 32 AVE NE CALGARY 403-453-2443 ADULT XBB 51.08199601 -113.9952041 YES YES UNITED PHARMACY POINT (-113.9952 51.082) 0 CALGARY
169 700-2220 68 ST NE CALGARY 403-299-2612 ADULT XBB 51.0684625 -113.9336307 YES YES CALGARY CO-OP PHARMACY #15 POINT (-113.93363 51.06846) 0 CALGARY
173 6213 CENTRE ST NW CALGARY 403-274-2444 ADULT XBB 51.10788492 -114.0633693 YES YES SANDSTONE PHARMACIES SIMONS VALLEY POINT (-114.06337 51.10788) 0 CALGARY
178 1010 1 AVE NE CALGARY 403-269-6440 ADULT XBB 51.05342892 -114.0401932 NO NO BRIDGEDALE PHARMACY POINT (-114.04019 51.05343) 0 CALGARY
184 4501 BRISEBOIS DR NW CALGARY 403-282-1208 ADULT XBB 51.09340467 -114.127905 YES YES PHARMASAVE ON NORTHMOUNT POINT (-114.1279 51.0934) 0 CALGARY
185 100-5005 DALHOUSIE DR NW CALGARY 403-202-0444 ADULT XBB 51.10549355 -114.1612934 YES YES SAFEWAY PHARMACY #8844 POINT (-114.16129 51.10549) 0 CALGARY
186 2303 4 ST SW CALGARY 403-229-3458 ADULT XBB 51.03231993 -114.0718985 YES YES MEDICINE SHOPPE PHARMACY 194 POINT (-114.0719 51.03232) 0 CALGARY
187 28 PANATELLA BLVD NW CALGARY 403-460-6707 ADULT XBB 51.16547718 -114.0712342 NO YES PANORAMA HILLS PHARMACY POINT (-114.07123 51.16548) 0 CALGARY
# plot the new vaccination_location_within_boundary geodataframe and the boundary geodataframe with folium

# create a map object with Calgary latitude and longitude as the center
m = folium.Map(location=[51.0447, -114.0719], zoom_start=10)

# plot the boundary geodataframe
folium.GeoJson(boundary).add_to(m)

# plot the vaccination_location_within_boundary geodataframe
for idx, row in vaccination_location_within_boundary.iterrows():
    folium.Marker([row['latitude'], row['longitude']]).add_to(m)
    
# display the map
m
Make this Notebook Trusted to load map: File -> Trust Notebook

As we can see, the vaccination_location_within_boundary geodataframe contains the vaccination locations that are within the boundary of Calgary. We can now use this geodataframe to find the vaccination locations within Calgary. the spatial join operation is a very useful operation in geospatial analysis. It can be used to find the points that are within a polygon, the polygons that intersect with another polygon, and many other spatial operations.

Summary#

I hope you enjoyed this GeoPandas tutorial. Spatial analysis is one of the coolest domains in data science, providing endless opportunities to enrich your analysis with location-based information. There are many tools for spatial analysis, but if you’re already familiar with Python, GeoPandas is a great place to get started.