🎯 What You'll Learn
- Implement four advanced spatial behavioral science projects
- Calculate velocity between GPS points using the Haversine formula
- Create color-coded kinetic maps (red = friction, green = flow)
- Use DBSCAN clustering to automatically detect conflict zones
- Build a modular SpatialIntelligenceEngine class for enterprise-grade analysis
📋 Before You Begin
- Completed the Desire Path Analysis article
- Understanding of pandas, Folium, and EXIF metadata extraction
- Familiarity with basic clustering algorithms
Table of Contents
Four High-Impact Projects
Building on the Desire Path concept, here are four projects using similar logic for different architectural problems.
1. The "Ghost Plaza" Audit (Underutilization Mapping)
Focus: Dead zones where people don't go.
Data: Areas with zero photo activity over time.
Utility: Identifies "Poche Space" for tactical urbanism (pop-up cafes, art galleries).
2. Viewshed & "Instagrammability" Indexing
Focus: Camera orientation as aesthetic value.
Data: Use GPSImgDirection to map "Visual Magnets."
Utility: Ensure new construction doesn't block valued views.
3. Kinetic Energy & "Friction" Mapping
Focus: How people move, not just where.
Data: Velocity (Δd/Δt) between consecutive points.
Utility: Identify circulation conflicts in narrow hallways.
4. Semantic Environment Mapping
Focus: Image content + spatial location.
Data: Use CLIP to tag images ("Greenery," "Crowded," "Damaged").
Utility: Create sentiment maps for maintenance and wayfinding.
| Project | Focus | Primary Metric | Outcome |
|---|---|---|---|
| Desire Path | Efficiency | Path Deviation | Pave new shortcuts |
| Ghost Plaza | Waste | Activity Absence | Repurpose dead space |
| Viewshed | Aesthetics | Camera Heading | Protect visual landmarks |
| Kinetic Mapping | Comfort | Velocity/Stay-time | Resolve bottlenecks |
Velocity & Friction Mapping
Calculate velocity using the Haversine formula for spherical coordinates.
import math
def haversine(coord1, coord2):
R = 6371000 # Radius of earth in meters
lat1, lon1 = math.radians(coord1[0]), math.radians(coord1[1])
lat2, lon2 = math.radians(coord2[0]), math.radians(coord2[1])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
return R * c
def calculate_kinetics(df):
df = df.sort_values('time').reset_index(drop=True)
df['velocity_kmh'] = 0.0
for i in range(1, len(df)):
dist = haversine((df.iloc[i-1]['lat'], df.iloc[i-1]['lon']),
(df.iloc[i]['lat'], df.iloc[i]['lon']))
time_diff = (df.iloc[i]['time'] - df.iloc[i-1]['time']).total_seconds() / 3600
if time_diff > 0:
df.at[i, 'velocity_kmh'] = (dist / 1000) / time_diff
return df
High Velocity (>5 km/h): Transit zones (walking/running) Low Velocity (<1 km/h): "Sticky" zones (lingering, talking)
Visualizing Friction vs. Flow
def create_kinetic_map(df, output="kinetic_analysis.html"):
m = folium.Map(location=[df['lat'].mean(), df['lon'].mean()], zoom_start=18)
for i in range(1, len(df)):
color = "red" if df.iloc[i]['velocity_kmh'] < 1.5 else "green"
line_coords = [
[df.iloc[i-1]['lat'], df.iloc[i-1]['lon']],
[df.iloc[i]['lat'], df.iloc[i]['lon']]
]
folium.PolyLine(
line_coords,
color=color,
weight=5,
opacity=0.8,
tooltip=f"Speed: {df.iloc[i]['velocity_kmh']:.2f} km/h"
).add_to(m)
m.save(output)
Automated Conflict Detection
Use DBSCAN clustering to automatically identify problem zones.
from sklearn.cluster import DBSCAN
import numpy as np
def identify_conflict_zones(df, velocity_threshold=1.5):
friction_points = df[df['velocity_kmh'] < velocity_threshold].copy()
if friction_points.empty:
return []
coords = friction_points[['lat', 'lon']].values
clustering = DBSCAN(eps=0.0001, min_samples=3).fit(coords)
friction_points['cluster'] = clustering.labels_
report = []
for cluster_id in set(clustering.labels_):
if cluster_id == -1: continue
cluster_data = friction_points[friction_points['cluster'] == cluster_id]
center_lat = cluster_data['lat'].mean()
center_lon = cluster_data['lon'].mean()
occurrence_count = len(cluster_data)
report.append({
'zone_id': cluster_id,
'lat': center_lat,
'lon': center_lon,
'intensity': occurrence_count,
'recommendation': "Add seating/shade" if occurrence_count > 10 else "Widening required"
})
return sorted(report, key=lambda x: x['intensity'], reverse=True)
Generating the Architect's Report
def generate_architect_summary(report_data, filename="architect_report.md"):
with open(filename, 'w') as f:
f.write("# Campus Spatial Audit: Conflict Zones Report\n")
f.write(f"Generated on: {datetime.now().strftime('%Y-%m-%d')}\n\n")
f.write("| Zone ID | Location | Intensity | Suggested Action |\n")
f.write("| :--- | :--- | :--- | :--- |\n")
for zone in report_data:
f.write(f"| {zone['zone_id']} | {zone['lat']:.5f}, {zone['lon']:.5f} | {zone['intensity']} | {zone['recommendation']} |\n")
print(f"Summary report generated: {filename}")
# Campus Spatial Audit: Conflict Zones Report Generated on: 2026-03-17 | Zone ID | Location | Intensity | Suggested Action | | :--- | :--- | :--- | :--- | | 0 | 19.12345, 72.54321 | 15 | Add seating/shade |
SpatialIntelligenceEngine Class
A modular, enterprise-grade class that encapsulates the entire ETL and analysis pipeline.
import os
import exifread
import pandas as pd
import folium
import geopandas as gpd
from datetime import datetime
from sklearn.cluster import DBSCAN
from folium.plugins import HeatMap, AntPath, TimestampedGeoJson
class SpatialIntelligenceEngine:
def __init__(self, source_dir):
self.source_dir = source_dir
self.df = pd.DataFrame()
self.conflict_report = []
def run_pipeline(self, official_map_path=None):
print("--- Starting Spatial Audit ---")
self._extract_metadata()
self._process_kinetics()
self._detect_conflicts()
self.generate_map(official_map_path)
print("--- Audit Complete ---")
def _extract_metadata(self):
pass # Internal logic from previous steps
def _process_kinetics(self):
pass # Haversine + velocity calculation
def _detect_conflicts(self):
pass # DBSCAN clustering logic
def generate_map(self, official_map_path=None, output="final_audit.html"):
m = folium.Map(location=[self.df['lat'].mean(), self.df['lon'].mean()], zoom_start=18)
if official_map_path and os.path.exists(official_map_path):
official_gdf = gpd.read_file(official_map_path)
folium.GeoJson(
official_gdf,
name="Official Pathways",
style_function=lambda x: {'color': 'black', 'weight': 2, 'dashArray': '5, 5'}
).add_to(m)
HeatMap(self.df[['lat', 'lon']].values.tolist(), name="Human Density").add_to(m)
fg_kinetic = folium.FeatureGroup(name="Kinetic Flow").add_to(m)
folium.LayerControl().add_to(m)
m.save(output)
Blueprint Gap Analysis
Overlay a Shapefile (official campus map) on human-generated data to calculate the "Blueprint Gap."
Final Utility Summary
- Objectivity: Replace "I think" with "I know" using data.
- Agility: Run audits every semester to track construction impact.
- Resilience: Reveal the "invisible city" — shortcuts not on any map.
Key Takeaways
- Four projects extend Desire Path Analysis: Ghost Plaza, Viewshed, Kinetic Mapping, Semantic Tagging
- Haversine formula calculates accurate distance between GPS coordinates
- DBSCAN clustering automates conflict zone detection
- SpatialIntelligenceEngine class provides modular, reusable code
- Blueprint Gap analysis compares human behavior against official maps