Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animating Yearly Data from Pandas in GeoPandas with Matplotlib FuncAnimation

Using this dataset of % change by state, I have merged it with a cartographic boundary map of US states from the Census department: https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_state_500k.zip

df.head()

Year        2017    2018    2019    2020    2021    2022    2023
State                           
Alabama     0.00    0.00     0.00   0.00    0.00    0.00    0.00
Arizona     0.24    0.00     0.03  -0.15    0.56   -0.36    0.21
Arkansas    0.35   -0.06    -0.03   0.03   -0.00   -0.13   -0.02
California  0.13    0.07    -0.03   0.04    0.21   -0.10    0.03
Colorado    0.81   -0.18    -0.01  -0.05    0.10   -0.03   -0.51

figures from column (year) 2017 shown on map

I would like to cycle through the columns (years) in a FuncAnimation after the boundaries have been plotted, and I am not quite sure how to go about it. The lifecycle of a plot in official reference manual cites relevant examples, but all deal with built-in figures, and not shape files.

Here is a related answer that seems exactly like what I'm missing, but deals with only (x, y) line graph: How to keep shifting the X axis and show the more recent data using matplotlib.animation in Python?

How do I extrapolate column outside of calling shape.plot()?

code:

shape = gpd.read_file(shapefile)
years = dfc.columns  # dfc = % change df
tspan = len(dfc.columns)


""" merge map with dataframe on state name column """ 
shape = pd.merge(
    left=shape,
    right=dfc,
    left_on='NAME',
    right_on='State',
    how='right'
)
""" init pyplot 'OO method' """
fig, ax = plt.subplots(figsize=(10, 5))

""" draw shape boundary """
ax = shape.boundary.plot(
    ax=ax,
    edgecolor='black', 
    linewidth=0.3, 
    )

""" plot shape """
ax = shape.plot(
    ax=ax,
    column=year, # what I need access to
    legend=True, cmap='RdBu_r', 
    legend_kwds={'shrink': 0.3, 'orientation': 'horizontal', 'format': '%.0f'})

""" cycle through columns -- not operable yet """ 
def animate(year):
    ax.clear()
    ax.shape.column(year)

animation = FuncAnimation(states, animate, frames=(dfc.columns[0], dfc.columns[tspan] + 1, 1), repeat=True, interval=1000)

I really haven't found anything online dealing with these cartographic boundary maps specifically

I have tried the most obvious things I could think of:
Putting the entire shape.plot() method into animate()

I tried a for loop cycling the years, which resulted in 7 distinct maps. Each iteration lost the attributes I set in shape.boundary.plot()

Edit:

Since I've converted the original procedural example into the OO format, I am starting to have new questions about what might be done.

If ax = shape.plot(ax=ax), is there some kind of getter/setter, for previously defined attributes? e.g. ax.set_attr = column=year (will scour manual immediately after I finish this)

Is there a way to define the map's boundary lines, shown here with shape.plot() and shape.boundary.plot(), using the fig, instead of ax (ax = shape.plot())?

Barring that, could we have shape.plot() and shape.boundary.plot() persist to the first subplot axs[0] and have columns of data shown using subsequent overlapping subplots axs[n == year]?

Any iterative process I've seen so far has lost the boundary attributes, so that's been a big sticking point for me.

like image 725
AveryFreeman Avatar asked Oct 13 '25 01:10

AveryFreeman


1 Answers

In the following animation, only states in data are plotted since how='right' is used for pd.merge.

Tested in python v3.12.3, geopandas v0.14.4, matplotlib v3.8.4.

import geopandas as gpd
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, PillowWriter

# Sample data
data = {
    'State': ['Alabama', 'Arizona', 'Arkansas', 'California', 'Colorado'],
    '2017': [0.00, 0.24, 0.35, 0.13, 0.81],
    '2018': [0.00, 0.00, -0.06, 0.07, -0.18],
    '2019': [0.00, 0.03, -0.03, -0.03, -0.01],
    '2020': [0.00, -0.15, 0.03, 0.04, -0.05],
    '2021': [0.00, 0.56, -0.00, 0.21, 0.10],
    '2022': [0.00, -0.36, -0.13, -0.10, -0.03],
    '2023': [0.00, 0.21, -0.02, 0.03, -0.51],
}
df = pd.DataFrame(data)

# Load the shapefile
shape = gpd.read_file('cb_2018_us_state_500k.shp')

# Merge the shape data with the dataframe
shape = pd.merge(
    left=shape,
    right=df,
    left_on='NAME',
    right_on='State',
    how='right'
)

# Initialize the plot
fig, ax = plt.subplots(figsize=(10, 5))

# Set fixed axis limits
xlim = (shape.total_bounds[0], shape.total_bounds[2])
ylim = (shape.total_bounds[1], shape.total_bounds[3])
ax.set_xlim(xlim)
ax.set_ylim(ylim)

# Plot initial boundaries
boundary = shape.boundary.plot(ax=ax, edgecolor='black', linewidth=0.3)

# Initialize the colorbar variable with a fixed normalization
norm = plt.Normalize(vmin=df.iloc[:, 1:].min().min(), vmax=df.iloc[:, 1:].max().max())
sm = plt.cm.ScalarMappable(cmap='RdBu_r', norm=norm)
sm.set_array([])  # Only needed for adding the colorbar
colorbar = fig.colorbar(sm, ax=ax, orientation='horizontal', shrink=0.5, format='%.2f')

# Function to update the plot for each year
def animate(year):
    ax.clear()

    # Set the fixed axis limits
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)

    # Plot initial boundaries
    boundary = shape.boundary.plot(ax=ax, edgecolor='black', linewidth=0.3)
    
    # Plot the data for the current year
    shape.plot(
        ax=ax, column=year, legend=False, cmap='RdBu_r', norm=norm
    )

    # Add year annotation at the top
    ax.annotate(f'Year: {year}', xy=(0.5, 1.05), xycoords='axes fraction', fontsize=12, ha='center')

# Create the animation
years = df.columns[1:]  # Skip the 'State' column
animation = FuncAnimation(fig, animate, frames=years, repeat=False, interval=1000)

# Save the animation as a GIF
writer = PillowWriter(fps=1)
animation.save('us_states_animation.gif', writer=writer)

# Show the plot
plt.show()

enter image description here

Note: Segmentation of the colorbar is an artifact of the .gif format and is not present when running the animation.


Save the file as a .mp4, which doesn't display segmentation in the colorbar. Download FFmped from FFmpeg download page, extract the archive, and add the bin folder path to the Path variable in 'System Variables'.

from matplotlib.animation import FuncAnimation, FFMpegWriter
import matplotlib as mpl

# Set the path to the ffmpeg executable
mpl.rcParams['animation.ffmpeg_path'] = r'C:\FFmpeg\bin\ffmpeg.exe'  # Replace this with the correct path to your ffmpeg executable

...

# Save the animation as an MP4
writer = FFMpegWriter(fps=1, metadata=dict(artist='Me'), bitrate=1800)
animation.save('us_states_animation.mp4', writer=writer)

# Show the plot
plt.show()
like image 125
Trenton McKinney Avatar answered Oct 14 '25 17:10

Trenton McKinney