Objective. Students perform at least one spatial join or geographic aggregation on their city data and produce a choropleth or point-density map that visually communicates a geographic pattern relevant to their research question.
This lesson, its worked examples and its practice questions has not yet been verified by an industry-certified professional or by a certification or approval body. It is a study draft and may contain errors. Do not use it as instructions for work on a job site or on live equipment. Always follow your instructor, your supervising professional, and current official references and regulations.
VoTechWorks program approval and accreditation are still in progress. Credentials are issued only by the certifying bodies named in each program, after their own exams.
Spatial analysis adds a geographic dimension to data science: instead of asking only what happened, you ask where it happened and whether location explains a pattern. City and urbanicity data—inspections, permits, 311 calls, demographic counts—almost always carry an address or coordinate that can be mapped, joined, and aggregated by geography. This lab brings those skills together in your capstone project.
Two neighborhoods can look identical in a table of averages but tell very different stories on a map. A restaurant inspection failure rate of 12 % means one thing city-wide and something very different when you see it concentrated in three census tracts near an industrial corridor. Choropleth maps and density maps surface that spatial structure instantly. Policymakers, planners, and community groups read maps fluently, so a well-made map is often the most persuasive deliverable in an urban data project.
Every spatial dataset lives in a coordinate reference system—a mathematical model that relates coordinates on a curved Earth to a flat map or to a 3-D globe. If two layers use different CRS values, a spatial join or overlay will silently produce wrong results or raise a geometry mismatch error. Before any spatial operation, confirm that all layers share the same CRS.
Two common families matter for US city work:
In GeoPandas, check and reproject with:
```python
print(gdf.crs) # check current CRS
gdf = gdf.to_crs(epsg=26918) # reproject to UTM zone 18N, for example
```
Document your CRS choice and the reason for any reprojection in your project notebook.
A spatial join attaches the attributes of one layer to the features of another based on their geometric relationship (intersects, within, contains, nearest). The most common urban-data pattern joins point records (inspections, incidents) to polygon features (census tracts, zip codes) so you can count or aggregate events by area.
```python
import geopandas as gpd
tracts = gpd.read_file('census_tracts.shp').to_crs(epsg=26918)
points = gpd.read_file('inspections.geojson').to_crs(epsg=26918)
joined = gpd.sjoin(points, tracts, how='left', predicate='within')
counts = joined.groupby('GEOID')['inspection_id'].count().reset_index()
counts.columns = ['GEOID', 'inspection_count']
tracts = tracts.merge(counts, on='GEOID', how='left')
tracts['rate_per_1000'] = (tracts['inspection_count'] / tracts['POP']) * 1000
```
Always verify that the join produced a sensible row count and check for null geometry rows before proceeding.
A buffer creates a zone of a specified distance around each feature. Buffers are useful for proximity questions: How many food vendors are within 500 meters of a school? Apply buffers in a projected CRS so the distance unit is meters or feet, not degrees.
```python
schools_proj = schools.to_crs(epsg=26918)
school_buffers = schools_proj.copy()
school_buffers['geometry'] = schools_proj.geometry.buffer(500) # 500 m
vendors_near = gpd.sjoin(vendors.to_crs(epsg=26918), school_buffers,
how='inner', predicate='within')
```
A choropleth map shades polygons by a numeric variable. Best practices:
1. Normalize your variable. Map rates or densities (per capita, per square kilometer), not raw counts, unless polygon sizes are roughly equal.
2. Choose a classification scheme appropriate to your data distribution: equal intervals, quantiles, or natural breaks (Jenks).
3. Select a color palette intentionally. Sequential palettes (light to dark single hue) suit one-directional data. Diverging palettes (two hues meeting at a neutral midpoint) suit data centered on a meaningful reference value such as the city average.
4. Test for color-blind accessibility. Avoid red-green combinations. Tools like ColorBrewer 2.0 label palettes as colorblind-safe.
5. Include all required map elements: title, legend with value ranges and units, north arrow or orientation note, data source credit, and projection note.
In GeoPandas and Matplotlib:
```python
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
tracts.plot(column='rate_per_1000', cmap='RdBu_r', scheme='quantiles',
legend=True, ax=ax, missing_kwds={'color': 'lightgrey'})
ax.set_title('Inspection Rate per 1,000 Residents by Census Tract')
ax.set_axis_off()
plt.tight_layout()
plt.savefig('choropleth_map.png', dpi=150)
```
Spatial work introduces specific data-quality risks that function as the "safety" layer of GIS practice:
Before submitting, post your map to the class board and use the five-item checklist (title, legend, north arrow or orientation note, data source credit, color accessibility) to evaluate two classmates' maps, and incorporate their written feedback on yours. Peer critique catches omissions that you stop seeing after hours of your own work. Revision based on feedback is part of the grade.
Answer each question, then check it. Nothing is recorded; this is your own practice.
A free tech program — complimentary, as our commitment to get America working. Help us build VoTechWorks. Tell us what works, what doesn’t, and what you want to learn.