# Import libraries
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt # To plot the final figure
import matplotlib.pyplot as mlines # To add legend to final figure
import geopandas as gpd # To work with shapefiles
import rioxarray as rioxr # To work with rasters
import contextily as ctx # To add a basemap
from pystac_client import Client # To access STAC catalogs
import planetary_computer # To sign items from the MPC STAC catalog
from IPython.display import Image # To nicely display images
# Set option to display all columns
'display.max_columns', None) pd.set_option(
Impact of Urbanization on Biodiversity
Course Website: EDS 220
About
Purpose: This project analyzes changes in the Biodiversity Intactness Index (BII) in Phoenix, Arizona (Maricopa County) between 2017 and 2020. The analysis aims to understand the impact of urban expansion on local biodiversity, as Phoenix has experienced significant urban development in recent decades.
Highlights:
Data Exploration: Accessing and extracting geospatial data through the
pystac_client
API,focusing on specific collections and spatial extents. Implementing raster data processing usingrioxarray
.Temporal BII Analysis: Analyzing changes in the Biodiversity Intactness Index (BII) across Phoenix from 2017 to 2020, with particular emphasis on areas maintaining high biodiversity values (BII ≥ 0.75). Quantifying percentage changes to measure the impact of urban development on local ecosystems.
Spatial Visualization: Developing comprehensive visualizations using multiple data layers to illustrate biodiversity changes. Combining vector and raster datasets to create informative maps that highlight areas of significant BII loss.
About the data:
This study utilizes two primary datasets to analyze biodiversity changes in Phoenix. The Biodiversity Intactness Index (BII) data is sourced from Microsoft Planetary Computer’s STAC catalog’s io-biodiversity
collection, specifically focusing on temporal changes between 2017 and 2020. The analysis area is defined by a bounding box with coordinates [-112.826843, 32.974108, -111.184387, 33.863574] encompassing the Phoenix metropolitan region. The study area boundaries are defined using the Phoenix subdivision shapefile, obtained from the 2020 TIGER/Line® Shapefiles published by the US Census Bureau, which provides detailed county subdivision boundaries for Arizona.
Source:
Microsoft Open Source, Matt McFarland, Rob Emanuele, Dan Morris, & Tom Augspurger. (2022). microsoft/PlanetaryComputer: October 2022 (2022.10.28). Zenodo. https://doi.org/10.5281/zenodo.7261897 Accessed: 2024-12-02
2020 TIGER/Line Shapefiles (machinereadable data files) / prepared by the U.S. Census Bureau, 2020 https://www.census.gov/cgi-bin/geo/shapefiles/index.php?year=2020&layergroup=County+Subdivisions Accessed: 2024-12-02
Set Up
We will use the following libraries and set-up through this analysis
Data Exploration
Before retriveing any data, update .gitignore with the entire data directory for best version control efficiency.
Download and import TIGER county subdivisons shapefile for Arizona in 2020.
Add a base map from the
contextily
package
# Load boundary data
= gpd.read_file(os.path.join('./data/','tl_2020_04_cousub.shp'))
arizona
arizona.head()
# Filter for Phoenix
= arizona[arizona['NAME'] == 'Phoenix']
phoenix
phoenix.head()
# Get general information about data
phoenix.plot() phoenix.crs
<Geographic 2D CRS: EPSG:4269>
Name: NAD83
Axis Info [ellipsoidal]:
- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)
Area of Use:
- name: North America - onshore and offshore: Canada - Alberta; British Columbia; Manitoba; New Brunswick; Newfoundland and Labrador; Northwest Territories; Nova Scotia; Nunavut; Ontario; Prince Edward Island; Quebec; Saskatchewan; Yukon. Puerto Rico. United States (USA) - Alabama; Alaska; Arizona; Arkansas; California; Colorado; Connecticut; Delaware; Florida; Georgia; Hawaii; Idaho; Illinois; Indiana; Iowa; Kansas; Kentucky; Louisiana; Maine; Maryland; Massachusetts; Michigan; Minnesota; Mississippi; Missouri; Montana; Nebraska; Nevada; New Hampshire; New Jersey; New Mexico; New York; North Carolina; North Dakota; Ohio; Oklahoma; Oregon; Pennsylvania; Rhode Island; South Carolina; South Dakota; Tennessee; Texas; Utah; Vermont; Virginia; Washington; West Virginia; Wisconsin; Wyoming. US Virgin Islands. British Virgin Islands.
- bounds: (167.65, 14.92, -40.73, 86.45)
Datum: North American Datum 1983
- Ellipsoid: GRS 1980
- Prime Meridian: Greenwich
Date Summary: The 2020 TIGER/Line Arizona County Subdivisions dataset provides detailed administrative boundary information for the 61 county subdivisions in Arizona. It is available as a GeoDataFrame containing multiple attributes including geographic identifiers (GEOID), names, legal/statistical area descriptions, and precise boundary geometries. The dataset uses the NAD83 (EPSG:4269) coordinate system and is updated annually as part of the U.S. Census Bureau’s TIGER/Line program.
# Plot Phoenix area together with a base map
# Initialize figure
= plt.subplots(figsize =(10, 10))
fig, ax
# Plot city boundary
=ax,
phoenix.boundary.plot(ax='blue')
color# Add base map
=ax,
ctx.add_basemap(ax# source=ctx.providers.CartoDB.Voyager,
=phoenix.crs)
crs
# Clean map
'Map of Phoenix')
ax.set_title('off')
ax.axis('NAME'])
ax.legend(phoenix[
plt.show()
# Save file as GeoJSON for reproducibility
'data_clean/phoenix-boundary-file', driver='GeoJSON') phoenix.to_file(
Now that we have Phoenix boundary mapped, we can move on to the Biodiversity Index dataset.
- Access BII data from MPC
Workflow: - Define bounding box and time range for search - Open MPC catalog - Search MPC catalog for io_biodiversity
collection with bounding box and time_range - Retrive search items - Select unique search item - Examine item.assets
keys and title - Display pre-rendered image - Save data
# Define study extent and time range
= [-112.826843, 32.974108, -111.184387, 33.863574]
bbox = "2017-01-01/2020-01-01"
time_range
# Access MPC catalog
= Client.open(
catalog "https://planetarycomputer.microsoft.com/api/stac/v1",
=planetary_computer.sign_inplace,
modifier
)
# Search for collection
= catalog.search(
search =['io-biodiversity'],
collections=bbox,
bbox=time_range)
datetime
search
# Retrieve items
= search.item_collection()
items
items# Determine number of items in search
print(f'There are {len(items)} items in the search.')
There are 4 items in the search.
=items[0]
item# Check assets in item
for key in item.assets.keys():
print(key, '--', item.assets[key].title)
data -- Biodiversity Intactness
tilejson -- TileJSON with default rendering
rendered_preview -- Rendered preview
# Plot rendered preview
=item.assets['rendered_preview'].href, width=500) Image(url

It seems that our biodiversity data is larger than phoenix. Let’s access the BII data and clip to study extent
Clip raster to geometry
# Explore data
print(type(items))
print(len(items))
print(items[0].assets['data'].href)
<class 'pystac.item_collection.ItemCollection'>
4
https://pcdata01euw.blob.core.windows.net/impact/bii-v1/bii_2020/bii_2020_34.74464974521749_-115.38597824385106_cog.tif?st=2024-12-08T00%3A17%3A42Z&se=2024-12-09T01%3A02%3A42Z&sp=rl&sv=2024-05-04&sr=c&skoid=9c8ff44a-6a2c-4dfb-b298-1c9212f64d9a&sktid=72f988bf-86f1-41af-91ab-2d7cd011db47&skt=2024-12-08T03%3A52%3A20Z&ske=2024-12-15T03%3A52%3A20Z&sks=b&skv=2024-05-04&sig=aPPNFKqsw8IBhVdRMBurGkpa2RhJ7mc6qWmptB3E37s%3D
There’s only 1 dimension for band so it’s efficient to drop it using the squeeze()
and .drop_vars()
# Select and process 2020 and 2017 data
for idx in [0, 3]:
# Load data
= rioxr.open_rasterio(items[idx].assets['data'].href)
bii # Drop band dimension
= bii.squeeze().drop_vars('band')
bii
# Match CRS and clip
= phoenix.to_crs(bii.rio.crs)
phoenix_match assert phoenix_match.crs == bii.rio.crs
# Clip the raster to phoenix geometry
= '2020' if idx == 0 else '2017'
year globals()[f'bii_{year}_phoenix'] = (bii.rio.clip_box(*phoenix_match.total_bounds)
.rio.clip(phoenix_match.geometry))
# Check clipped data
print(bii_2020_phoenix)
bii_2020_phoenix.plot()
<xarray.DataArray (y: 583, x: 990)> Size: 2MB
array([[nan, nan, nan, ..., nan, nan, nan],
[nan, nan, nan, ..., nan, nan, nan],
[nan, nan, nan, ..., nan, nan, nan],
...,
[nan, nan, nan, ..., nan, nan, nan],
[nan, nan, nan, ..., nan, nan, nan],
[nan, nan, nan, ..., nan, nan, nan]], dtype=float32)
Coordinates:
* x (x) float64 8kB -112.5 -112.5 -112.5 ... -111.6 -111.6 -111.6
* y (y) float64 5kB 33.81 33.81 33.81 33.81 ... 33.29 33.29 33.29
spatial_ref int64 8B 0
Attributes:
AREA_OR_POINT: Area
scale_factor: 1.0
add_offset: 0.0
Calculate percent area with BII >0.75
To calculate percent area, we need total pixel counts of phoenix and then pixel counts with BII >= 0.75.
# Count total Phoenix pixels
= bii_2020_phoenix.count().item()
total_counts print(f"There are {total_counts} pixels in Phoenix")
# Select BII >= 0.75
= (bii_2020_phoenix >= 0.75).astype(int)
over75_2020 = (bii_2017_phoenix >= 0.75).astype(int) over75_2017
There are 338699 pixels in Phoenix
# Count threshold pixels in 2020
= np.unique(over75_2020, return_counts=True)
value, counts assert len(value) == len(counts)
# Initialize dictionary with column's data
= {'code': value,
dic 'counts': counts}
# Create a data frame
= pd.DataFrame(dic)
pix_counts_20
pix_counts_20# Report findings
print(f"There are {pix_counts_20.iloc[1,1]} pixels with BII >= 0.75 in 2020")
There are 21986 pixels with BII >= 0.75 in 2020
While it’s easy to run the same code for 2017, it’s better to write a for
loop to maintain best coding practice.
# Count pixels with condition
= {'2020': over75_2020, '2017': over75_2017} # map years to their data
years
for year, data in years.items():
# Count pixels for True/False
= np.unique(data, return_counts=True)
value, counts assert len(value) == len(counts)
# Create dataframe
= {'code': value, 'counts': counts}
dic globals()[f'pix_counts_{year[-2:]}'] = pd.DataFrame(dic) # creates pix_counts_20 and pix_counts_17
# Save counts as variable
globals()[f'counts_75_{year}'] = counts[1]
# Report findings
print(f"There are {counts[1]} pixels with BII >= 0.75 in {year}")
There are 21986 pixels with BII >= 0.75 in 2020
There are 24133 pixels with BII >= 0.75 in 2017
# Plot the data to check extent
over75_2017.plot()
Now that we have pixel counts of both total cells in Phoenix and BII > 75% cells, we can calculate the percent area with a simple division.
# Write a "for loop" to run calculation for both years
= round((counts_75_2017/total_counts)*100, 2)
ratio_2017 ratio_2017
7.13
for year in years:
# Calculate ratio
globals()[f'ratio_{year}'] = (globals()[f'counts_75_{year}']/total_counts) * 100
# Print result
print(f"In {year}, {globals()[f'ratio_{year}']:.2f}% of pixels have BII >= 0.75")
In 2020, 6.49% of pixels have BII >= 0.75
In 2017, 7.13% of pixels have BII >= 0.75
# Obtain BII loss between 2017 to 2020
= over75_2017 - over75_2020
loss loss
<xarray.DataArray (y: 583, x: 990)> Size: 5MB array([[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]]) Coordinates: * x (x) float64 8kB -112.5 -112.5 -112.5 ... -111.6 -111.6 -111.6 * y (y) float64 5kB 33.81 33.81 33.81 33.81 ... 33.29 33.29 33.29 spatial_ref int64 8B 0
Value in loss
variable represents change of BII between the years. 1
indicates BII loss, and -1
indicates BII gain, 0
means no change.
# Select xarray cells with value of 1
= loss.where(loss==1)
loss
loss loss.plot()
Visualize biodiversity loss
# Define aspect ratio with height and width
= bii_2020_phoenix.rio.width/bii_2020_phoenix.rio.height
aspect
# Initialize figure
= plt.subplots(figsize=(10, 4*aspect))
fig, ax
# Plot the BII raster in 2020
=ax,
bii_2020_phoenix.plot(ax={
cbar_kwargs"orientation": "horizontal",
"label": "BII for 2020"}
)
# Plot the loss layer
=ax,
loss.plot(ax="inferno",
cmap=False)
add_colorbar
# Plot city boundary
=ax,
phoenix.boundary.plot(ax='blue')
color
# Set legend for change raster
= mlines.Line2D([], [], color = "red",
red_patch = "Biodiversity area loss",
label = 4)
linewidth
= [red_patch],
ax.legend(handles =(0, 0, 0.75, 0),
bbox_to_anchor= 8)
fontsize
# Set title
"Biodiversity Intactness Index (BII) \nPhoenix Subdivision")
ax.set_title(
# Turn axis off
"off")
ax.axis(
plt.show()
This figure shows the Biodiversity Intactness Index (BII) for the Phoenix Subdivision in 2020. The BII values are displayed using a color gradient scale ranging from 0.1 to 0.8. The area is outlined in blue, showing the subdivision boundaries. Quantitative analysis reveals a decline in high-biodiversity areas between 2017 and 2020. In 2017, 7.13% of the area had high biodiversity intactness (BII ≥ 0.75), which decreased to 6.49% by 2020. This decline is visually represented by the red areas marking “Biodiversity area loss,” which appear in two main locations: a larger area in the northeastern section of the subdivision and another smaller cluster in the southern portion.