Python  

How to Animate a Heatmap of City Foot Traffic Using GPS Pings Using Python

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:

  1. Bin GPS pings into 5-minute intervals

  2. Render each frame as a heatmap

  3. 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}")
ewq

Running this code produces:

  • shibuya_foot_traffic.gif: A smooth animation showing traffic buildup from 2:00 PM to 4:00 PM

  • shibuya_total_heatmap.html: An interactive map of all pings

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.