Table of Contents
Introduction
Why Real-Time Foot Traffic Matters
Simulating Realistic GPS Ping Data
Building the Animated Heatmap
Complete Implementation with Live Visualization
Best Practices and Performance Tips
Conclusion
Introduction
In today’s data-driven world, understanding human movement patterns in urban environments is critical—for city planners optimizing public transport, retailers analyzing footfall, or emergency responders managing crowd safety. One powerful way to visualize this movement is through animated heatmaps built from GPS pings.
Unlike static maps, animated heatmaps reveal how foot traffic evolves over time—showing morning commutes, lunchtime rushes, or weekend strolls in real time. In this article, we’ll simulate realistic GPS data from a major city and build a smooth, interactive animated heatmap using Python—no external APIs required.
Why Real-Time Foot Traffic Matters
Consider Tokyo’s Shibuya Crossing—the world’s busiest pedestrian intersection. During peak hours, over 3,000 people cross every minute. If you’re a city engineer, you need to know when congestion spikes occur. If you run a café nearby, you want to predict lunch rushes. Real-time foot traffic heatmaps turn raw location data into actionable insights.
We’ll simulate this scenario: a 2-hour window of GPS pings from pedestrians near Shibuya Station, capturing the afternoon surge as commuters, shoppers, and tourists flood the area.
![PlantUML Diagram]()
Simulating Realistic GPS Ping Data
Real GPS data is noisy, clustered around landmarks, and time-stamped. We’ll generate synthetic but realistic pings:
Centered around Shibuya Station (lat: 35.6595
, lon: 139.7005
)
Higher density near Hachiko Statue and shopping streets
Pings arrive every 5–15 seconds per user
200 simulated pedestrians over 120 minutes
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
def generate_gps_pings(duration_minutes=120, num_people=200):
base_lat, base_lon = 35.6595, 139.7005
start_time = datetime(2024, 6, 15, 14, 0) # 2:00 PM
records = []
for _ in range(num_people):
# Each person appears for 10–60 minutes
active_minutes = np.random.randint(10, 61)
entry_time = start_time + timedelta(minutes=np.random.randint(0, duration_minutes - active_minutes))
# Move slightly around base location (within ~300m)
lat_offset = np.random.normal(0, 0.001) # ~111m per 0.001 deg
lon_offset = np.random.normal(0, 0.0012)
# Generate pings every 5–15 seconds
num_pings = int((active_minutes * 60) / np.random.randint(5, 16))
for i in range(num_pings):
time = entry_time + timedelta(seconds=i * np.random.randint(5, 16))
lat = base_lat + lat_offset + np.random.normal(0, 0.0003)
lon = base_lon + lon_offset + np.random.normal(0, 0.0003)
records.append((time, lat, lon))
df = pd.DataFrame(records, columns=['timestamp', 'lat', 'lon'])
df = df.sort_values('timestamp').reset_index(drop=True)
return df
Building the Animated Heatmap
We’ll use Folium for mapping and matplotlib with FuncAnimation for smooth time-based animation. The key is to:
Bin GPS pings into 5-minute intervals
Render each frame as a heatmap
Animate the sequence.
import folium
from folium.plugins import HeatMap
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import io
import base64
def create_animated_heatmap(df, output_file='shibuya_foot_traffic.gif'):
# Resample data into 5-minute bins
df['time_bin'] = df['timestamp'].dt.floor('5T')
time_bins = sorted(df['time_bin'].unique())
# Setup plot
fig, ax = plt.subplots(figsize=(10, 8))
ax.set_title('Shibuya Foot Traffic Heatmap (5-min intervals)', fontsize=14)
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
# Precompute heatmaps for each time bin
frames = []
for t in time_bins:
subset = df[df['time_bin'] == t]
if not subset.empty:
frames.append(subset[['lat', 'lon']].values)
else:
frames.append(np.array([]).reshape(0, 2))
# Animation function
def animate(i):
ax.clear()
ax.set_title(f'Shibuya Foot Traffic – {time_bins[i].strftime("%H:%M")}', fontsize=14)
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
data = frames[i]
if len(data) > 0:
ax.hexbin(data[:, 1], data[:, 0], gridsize=30, cmap='Reds', mincnt=1)
ax.set_xlim(139.695, 139.706)
ax.set_ylim(35.655, 35.664)
return ax,
anim = FuncAnimation(fig, animate, frames=len(frames), interval=800, blit=False)
anim.save(output_file, writer='pillow', fps=1.2)
plt.close(fig)
print(f"Animated heatmap saved as {output_file}")
Complete Implementation with Live Visualization
Putting it all together:
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
import folium
from folium.plugins import HeatMap
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import io
import base64
from PIL import Image
from IPython.display import Image as ColabImage, HTML, display
import os
def generate_gps_pings(duration_minutes=120, num_people=200):
"""Generates synthetic GPS pings for multiple people near Shibuya."""
base_lat, base_lon = 35.6595, 139.7005
start_time = datetime(2024, 6, 15, 14, 0) # 2:00 PM
records = []
person_id_counter = 1
for _ in range(num_people):
# Each person appears for 10–60 minutes
active_minutes = np.random.randint(10, 61)
# Entry time is within the total duration, allowing active_minutes to complete
entry_time = start_time + timedelta(minutes=np.random.randint(0, duration_minutes - active_minutes + 1))
# Base location of the person (within ~300m of Shibuya crossing)
lat_offset = np.random.normal(0, 0.001) # ~111m per 0.001 deg
lon_offset = np.random.normal(0, 0.0012)
# Ping interval for this person (5–15 seconds, constant for the person)
ping_interval_seconds = np.random.randint(5, 16)
# Generate pings
num_pings = int((active_minutes * 60) / ping_interval_seconds)
for i in range(num_pings):
# Use consistent time step
time = entry_time + timedelta(seconds=i * ping_interval_seconds)
# Position slightly randomized around the person's base location
lat = base_lat + lat_offset + np.random.normal(0, 0.0003)
lon = base_lon + lon_offset + np.random.normal(0, 0.0003)
records.append((time, lat, lon, person_id_counter))
person_id_counter += 1
df = pd.DataFrame(records, columns=['timestamp', 'lat', 'lon', 'person_id'])
df = df.sort_values('timestamp').reset_index(drop=True)
return df
def create_animated_heatmap(df, output_file='shibuya_foot_traffic.gif'):
"""Creates and saves an animated heatmap GIF."""
# Resample data into 5-minute bins (FIXED: Using '5min' instead of '5T')
df['time_bin'] = df['timestamp'].dt.floor('5min')
time_bins = sorted(df['time_bin'].unique())
# Setup plot
fig, ax = plt.subplots(figsize=(10, 8))
# Precompute heatmaps for each time bin and save frames to memory
frames_pil = []
for i, t in enumerate(time_bins):
subset = df[df['time_bin'] == t]
# Clear previous plot
ax.clear()
ax.set_title(f'Shibuya Foot Traffic – {t.strftime("%H:%M")}', fontsize=14)
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
data = subset[['lat', 'lon']].values
if len(data) > 0:
# Create a hexbin plot for the heatmap effect
ax.hexbin(data[:, 1], data[:, 0], gridsize=30, cmap='Reds', mincnt=1)
# Set consistent axis limits for a smooth animation
ax.set_xlim(139.695, 139.706)
ax.set_ylim(35.655, 35.664)
# Save the current figure to an in-memory buffer
buf = io.BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
# Open the image from the buffer using PIL and append it to the list
img = Image.open(buf)
frames_pil.append(img)
# Print progress (using '\r' to overwrite the line)
print(f"Processed frame {i+1}/{len(time_bins)}: {t.strftime('%H:%M')}", end='\r')
# Save the list of PIL images as an animated GIF
if frames_pil:
# Save the first frame and append the rest, setting duration (milliseconds)
frames_pil[0].save(
output_file,
save_all=True,
append_images=frames_pil[1:],
duration=800, # Frame duration in milliseconds (800ms = 0.8s)
loop=0 # 0 means infinite loop
)
print("\n" + "="*50)
print(f"Animated heatmap saved as {output_file}")
print("="*50)
else:
print("No data frames to animate.")
plt.close(fig) # Close the Matplotlib figure
# ==============================================================================
# Execution Block
# ==============================================================================
if __name__ == "__main__":
gif_file = 'shibuya_foot_traffic.gif'
html_file = 'shibuya_total_heatmap.html'
print(" Generating realistic GPS pings near Shibuya Station...")
gps_data = generate_gps_pings(duration_minutes=120, num_people=200)
print(f" Generated {len(gps_data)} GPS pings")
print("\n Creating animated heatmap...")
create_animated_heatmap(gps_data, gif_file)
# Optional: Create a static Folium map of total traffic
print("\n Creating static Folium map...")
m = folium.Map(location=[35.6595, 139.7005], zoom_start=16)
if not gps_data.empty:
HeatMap(gps_data[['lat', 'lon']].values, radius=12, blur=8).add_to(m)
m.save(html_file)
print(f" Static heatmap saved as '{html_file}'")
else:
print(" No GPS data to create static Folium map.")
print("\n" + "#" * 50)
print("DISPLAYING OUTPUTS IN GOOGLE COLAB")
print("#" * 50)
# --- 1. Display GIF ---
if os.path.exists(gif_file):
print("\n Animated Heatmap (GIF):")
display(ColabImage(filename=gif_file))
else:
print(f"File not found: {gif_file}")
# --- 2. Display Folium Map ---
if os.path.exists(html_file):
print("\n Static Folium Heatmap (Interactive Map):")
with open(html_file, 'r') as f:
html_content = f.read()
display(HTML(html_content))
else:
print(f"File not found: {html_file}")
![e]()
![w]()
![q]()
Running this code produces:
Pro Tip: For real deployments, replace simulated data with live Kafka streams or database queries—this pipeline scales seamlessly.
Best Practices and Performance Tips
Downsample wisely: For large datasets, aggregate pings into 5–10 minute windows to avoid clutter.
Use hexbin over scatter: plt.hexbin()
is faster and clearer for density visualization than thousands of points.
Clip coordinates: Restrict lat/lon ranges to your area of interest—prevents outliers from skewing the view.
Optimize GIF size: Use fps=1–2
and limit frames; 24+ frames can create huge files.
For web apps: Convert frames to base64 and embed in HTML, or use Plotly for interactive time sliders.
Conclusion
Animated heatmaps transform raw GPS pings into compelling stories of urban life. Whether you’re simulating Tokyo’s crowds or analyzing real data from smart city sensors, this technique reveals patterns invisible in static reports.
With just a few lines of Python, you can:
Simulate realistic human movement
Visualize temporal density changes
Export shareable animations or interactive maps
As cities grow smarter, the ability to animate and interpret spatial-temporal data becomes not just useful—but essential. Start small, iterate fast, and let the heatmap tell the story.
The city breathes. Now you can see it.