Skip to main content 首页 创作者 xjtulyc awesome-rosetta-skills mobility-trajectories
mobility-trajectories Use this Skill for urban mobility analysis: GPS trajectory processing, stop detection, OD matrix construction, and mobility entropy metrics.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/xjtulyc/awesome-rosetta-skills --skill mobility-trajectories命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... 同仓库更多 Skills Astronomical data analysis with astropy and astroquery — FITS I/O, WCS transforms, catalog cross-matching, aperture photometry, and CMB power spectra.
Seismological data analysis with ObsPy — FDSN waveform download, response removal, phase picking, moment tensor inversion, and seismicity mapping.
Download and analyze oceanographic data from Copernicus Marine Service and Argo floats using copernicusmarine, gsw, and xarray.
name mobility-trajectories description Use this Skill for urban mobility analysis: GPS trajectory processing, stop detection, OD matrix construction, and mobility entropy metrics.
tags ["urban-science","mobility","gps","trajectories","transportation"] version 1.0.0 authors [{"name":"Rosetta Skills Contributors","github":"@xjtulyc"}] license MIT platforms ["claude-code","codex","gemini-cli","cursor"] dependencies {"python":["pandas>=2.0","numpy>=1.24","scipy>=1.11","scikit-learn>=1.3","matplotlib>=3.7","geopandas>=0.14"]} last_updated 2026-03-17 status stable
Urban Mobility Trajectory Analysis
One-line summary : Process GPS mobility traces to detect activity stops, build origin-destination matrices, measure individual mobility entropy, and model urban travel demand patterns.
When to Use This Skill
When processing GPS/CDR traces to identify stops and trips
When building origin-destination (OD) matrices from mobility data
When measuring individual mobility entropy and radius of gyration
When detecting home/work locations from trajectory data
When analyzing commuting patterns and modal split
When computing flow maps and desire lines
Trigger keywords : GPS trajectories, mobility data, origin-destination matrix, stop detection, radius of gyration, mobility entropy, CDR data, travel behavior, commuting, flow map, home detection, individual mobility, urban mobility, transportation
Background & Key Concepts
Radius of Gyration
$$
r_g = \sqrt{\frac{1}{N}\sum_{i=1}^N (r_i - r_{cm})^2}
$$
where $r_{cm}$ is the center of mass of all visited locations. Measures spatial extent of individual mobility.
Mobility Entropy (Diversity of Locations)
$$
S = -\sum_{i} p_i \ln p_i
$$
where $p_i$ = fraction of time spent at location $i$. High S = diverse, low S = routine/concentrated.
Stop Detection
A stop = sequence of GPS points where speed < threshold AND dwell time > minimum duration. Between stops = trips.
Environment Setup
Install Dependencies
pip install pandas>=2.0 numpy>=1.24 scipy>=1.11 scikit-learn>=1.3 \
matplotlib>=3.7 geopandas>=0.14
Verify Installation
import pandas as pd
import numpy as np
from scipy.spatial.distance import cdist
def haversine (lat1, lon1, lat2, lon2 ):
R = 6371
lat1, lon1, lat2, lon2 = map (np.radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1; dlon = lon2 - lon1
a = np.sin(dlat/ )** + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/ )**
* R * np.arcsin(np.sqrt(a))
dist = haversine( , - , , - )
( )
2
2
2
2
return
2
40.7128
74.0060
40.7589
73.9851
print
f"Distance: {dist:.3 f} km (expected ~5.7 km)"
Core Workflow
Step 1: GPS Trajectory Simulation and Stop Detection import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
def haversine (lat1, lon1, lat2, lon2 ):
"""Vectorized haversine distance in km."""
R = 6371.0
lat1, lon1, lat2, lon2 = (np.radians(x) for x in [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1; dlon = lon2 - lon1
a = np.sin(dlat/2 )**2 + np.cos(lat1)*np.cos(lat2)*np.sin(dlon/2 )**2
return 2 * R * np.arcsin(np.sqrt(np.clip(a, 0 , 1 )))
np.random.seed(42 )
city_center = (40.7128 , -74.0060 )
def generate_individual_trace (uid, n_days=7 , city_lat=40.7128 , city_lon=-74.0060 ):
"""
Generate synthetic GPS trace for one individual.
Activity pattern: home(night) → commute → work(day) → commute → home(evening)
"""
records = []
t = datetime(2023 , 3 , 6 , 7 , 0 , 0 )
home_lat = city_lat + np.random.uniform(-0.05 , 0.05 )
home_lon = city_lon + np.random.uniform(-0.05 , 0.05 )
work_lat = city_lat + np.random.uniform(-0.03 , 0.03 )
work_lon = city_lon + np.random.uniform(-0.03 , 0.03 )
pois = [(city_lat + np.random.uniform(-0.04 , 0.04 ),
city_lon + np.random.uniform(-0.04 , 0.04 )) for _ in range (5 )]
for day in range (n_days):
for h in range (0 , 7 ):
for m in range (0 , 60 , 10 ):
records.append({'uid' : uid, 'ts' : t,
'lat' : home_lat + np.random.normal(0 , 0.0005 ),
'lon' : home_lon + np.random.normal(0 , 0.0005 )})
t += timedelta(minutes=10 )
for step in range (6 ):
frac = step / 5
records.append({'uid' : uid, 'ts' : t,
'lat' : home_lat + frac*(work_lat-home_lat) + np.random.normal(0 , 0.001 ),
'lon' : home_lon + frac*(work_lon-home_lon) + np.random.normal(0 , 0.001 )})
t += timedelta(minutes=5 )
if day < 5 :
for h in range (48 ):
records.append({'uid' : uid, 'ts' : t,
'lat' : work_lat + np.random.normal(0 , 0.0005 ),
'lon' : work_lon + np.random.normal(0 , 0.0005 )})
t += timedelta(minutes=10 )
else :
poi = pois[np.random.randint(len (pois))]
for h in range (48 ):
records.append({'uid' : uid, 'ts' : t,
'lat' : poi[0 ] + np.random.normal(0 , 0.001 ),
'lon' : poi[1 ] + np.random.normal(0 , 0.001 )})
t += timedelta(minutes=10 )
for step in range (6 ):
frac = step / 5
records.append({'uid' : uid, 'ts' : t,
'lat' : work_lat + frac*(home_lat-work_lat) + np.random.normal(0 , 0.001 ),
'lon' : work_lon + frac*(home_lon-work_lon) + np.random.normal(0 , 0.001 )})
t += timedelta(minutes=5 )
for h in range (24 ):
records.append({'uid' : uid, 'ts' : t,
'lat' : home_lat + np.random.normal(0 , 0.0005 ),
'lon' : home_lon + np.random.normal(0 , 0.0005 )})
t += timedelta(minutes=10 )
return records
all_records = []
for uid in range (10 ):
all_records.extend(generate_individual_trace(uid))
df_gps = pd.DataFrame(all_records)
df_gps['ts' ] = pd.to_datetime(df_gps['ts' ])
df_gps = df_gps.sort_values(['uid' , 'ts' ]).reset_index(drop=True )
print (f"GPS dataset: {len (df_gps):,} records, {df_gps['uid' ].nunique()} individuals" )
def detect_stops (trace, speed_threshold_kmh=1.5 , min_duration_min=15 , radius_m=50 ):
"""
Detect activity stops from GPS trace.
Parameters
----------
trace : DataFrame — sorted by 'ts', with 'lat', 'lon' columns
speed_threshold_kmh : float — max speed to be considered stationary
min_duration_min : float — minimum stop duration
Returns
-------
DataFrame with stop records: lat, lon, arrive_ts, leave_ts, duration_min
"""
trace = trace.copy().reset_index(drop=True )
speeds = [0.0 ]
for i in range (1 , len (trace)):
dt = (trace.loc[i,'ts' ] - trace.loc[i-1 ,'ts' ]).total_seconds() / 3600
d = haversine(trace.loc[i-1 ,'lat' ], trace.loc[i-1 ,'lon' ],
trace.loc[i,'lat' ], trace.loc[i,'lon' ])
speeds.append(d/dt if dt > 0 else 0 )
trace['speed_kmh' ] = speeds
trace['stationary' ] = trace['speed_kmh' ] < speed_threshold_kmh
stops = []
in_stop = False
stop_pts = []
for _, row in trace.iterrows():
if row['stationary' ]:
stop_pts.append(row)
in_stop = True
else :
if in_stop and len (stop_pts) > 0 :
arrive = stop_pts[0 ]['ts' ]
leave = stop_pts[-1 ]['ts' ]
dur_min = (leave - arrive).total_seconds() / 60
if dur_min >= min_duration_min:
stops.append({
'lat' : np.mean([p['lat' ] for p in stop_pts]),
'lon' : np.mean([p['lon' ] for p in stop_pts]),
'arrive_ts' : arrive,
'leave_ts' : leave,
'duration_min' : dur_min,
'n_points' : len (stop_pts),
})
stop_pts = []
in_stop = False
return pd.DataFrame(stops)
stops_all = []
for uid, group in df_gps.groupby('uid' ):
group_sorted = group.sort_values('ts' )
stops = detect_stops(group_sorted)
if len (stops) > 0 :
stops['uid' ] = uid
stops_all.append(stops)
df_stops = pd.concat(stops_all, ignore_index=True )
print (f"\nDetected {len (df_stops)} stops across {df_stops['uid' ].nunique()} individuals" )
print (f"Mean stop duration: {df_stops['duration_min' ].mean():.1 f} min" )
print (f"Median stops per person: {df_stops.groupby('uid' ).size().median():.0 f} " )
Step 2: Origin-Destination Matrix import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import linkage, dendrogram, fcluster
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
stop_coords = df_stops[['lat' , 'lon' ]].values
scaler = StandardScaler()
coords_scaled = scaler.fit_transform(stop_coords)
n_zones = 8
kmeans = KMeans(n_clusters=n_zones, random_state=42 , n_init=10 )
df_stops['zone' ] = kmeans.fit_predict(coords_scaled)
zone_centers = scaler.inverse_transform(kmeans.cluster_centers_)
zone_centers_df = pd.DataFrame(zone_centers, columns=['lat' , 'lon' ])
zone_names = [f"Zone {i+1 } " for i in range (n_zones)]
def build_od_matrix (stops_df, n_zones ):
"""
Build OD matrix from consecutive stops (same individual).
Each trip = from departure zone to arrival zone.
"""
od = np.zeros((n_zones, n_zones), dtype=int )
for uid, group in stops_df.groupby('uid' ):
group_sorted = group.sort_values('arrive_ts' ).reset_index(drop=True )
for i in range (len (group_sorted) - 1 ):
origin = int (group_sorted.loc[i, 'zone' ])
dest = int (group_sorted.loc[i+1 , 'zone' ])
if origin != dest:
od[origin, dest] += 1
return od
od_matrix = build_od_matrix(df_stops, n_zones)
od_df = pd.DataFrame(od_matrix, index=zone_names, columns=zone_names)
print ("\nOrigin-Destination Matrix (trips):" )
print (od_df)
fig, axes = plt.subplots(1 , 2 , figsize=(14 , 6 ))
im = axes[0 ].imshow(od_matrix, cmap='YlOrRd' , aspect='auto' )
plt.colorbar(im, ax=axes[0 ], label='Number of trips' )
axes[0 ].set_xticks(range (n_zones)); axes[0 ].set_xticklabels(zone_names, rotation=30 , ha='right' , fontsize=8 )
axes[0 ].set_yticks(range (n_zones)); axes[0 ].set_yticklabels(zone_names, fontsize=8 )
axes[0 ].set_xlabel("Destination" ); axes[0 ].set_ylabel("Origin" )
axes[0 ].set_title("Origin-Destination Matrix" )
for i in range (n_zones):
for j in range (n_zones):
if od_matrix[i,j] > 0 :
axes[0 ].text(j, i, str (od_matrix[i,j]), ha='center' , va='center' ,
fontsize=8 , color='white' if od_matrix[i,j] > od_matrix.max ()/2 else 'black' )
axes[1 ].scatter(zone_centers_df['lon' ], zone_centers_df['lat' ],
c='red' , s=200 , zorder=5 , edgecolors='black' , linewidths=0.7 )
for i, name in enumerate (zone_names):
axes[1 ].annotate(name, (zone_centers_df.loc[i,'lon' ], zone_centers_df.loc[i,'lat' ]),
fontsize=8 , xytext=(5 ,5 ), textcoords='offset points' )
max_flow = od_matrix.max ()
for i in range (n_zones):
for j in range (i+1 , n_zones):
flow = od_matrix[i,j] + od_matrix[j,i]
if flow > 0 :
x = [zone_centers_df.loc[i,'lon' ], zone_centers_df.loc[j,'lon' ]]
y = [zone_centers_df.loc[i,'lat' ], zone_centers_df.loc[j,'lat' ]]
lw = flow / max_flow * 5 + 0.5
axes[1 ].plot(x, y, 'b-' , linewidth=lw, alpha=0.5 )
axes[1 ].set_xlabel("Longitude" ); axes[1 ].set_ylabel("Latitude" )
axes[1 ].set_title("Mobility Flow Map (Desire Lines)" )
axes[1 ].grid(True , alpha=0.3 )
plt.tight_layout()
plt.savefig("od_matrix.png" , dpi=150 )
plt.show()
Step 3: Mobility Metrics (Entropy, Radius of Gyration) import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
def radius_of_gyration (stops_df, uid ):
"""Radius of gyration for one individual (in km)."""
user_stops = stops_df[stops_df['uid' ] == uid][['lat' ,'lon' ,'duration_min' ]]
if len (user_stops) == 0 :
return np.nan
weights = user_stops['duration_min' ].values
weights = weights / weights.sum ()
lat_cm = np.average(user_stops['lat' ], weights=weights)
lon_cm = np.average(user_stops['lon' ], weights=weights)
dists = haversine(user_stops['lat' ].values, user_stops['lon' ].values,
np.full(len (user_stops), lat_cm),
np.full(len (user_stops), lon_cm))
return np.sqrt(np.average(dists**2 , weights=weights))
def mobility_entropy (stops_df, uid, zone_col='zone' ):
"""Shannon entropy over zone visit time fractions."""
user_stops = stops_df[stops_df['uid' ] == uid]
if len (user_stops) == 0 :
return np.nan
zone_time = user_stops.groupby(zone_col)['duration_min' ].sum ()
p = zone_time / zone_time.sum ()
p = p[p > 0 ]
return -np.sum (p * np.log(p))
def detect_home (stops_df, uid, night_hours=(22 , 6 ) ):
"""Detect home location: most frequent stop during night hours."""
user_stops = stops_df[stops_df['uid' ] == uid].copy()
hour = user_stops['arrive_ts' ].dt.hour
night_mask = (hour >= night_hours[0 ]) | (hour < night_hours[1 ])
night_stops = user_stops[night_mask]
if len (night_stops) == 0 :
return np.nan, np.nan
home_zone = night_stops.groupby('zone' )['duration_min' ].sum ().idxmax()
home_loc = night_stops[night_stops['zone' ] == home_zone][['lat' ,'lon' ]].mean()
return home_loc['lat' ], home_loc['lon' ]
mobility_metrics = []
for uid in df_stops['uid' ].unique():
rg = radius_of_gyration(df_stops, uid)
S = mobility_entropy(df_stops, uid)
h_lat, h_lon = detect_home(df_stops, uid)
n_stops_per_day = df_stops[df_stops['uid' ]==uid]['arrive_ts' ].apply(lambda x: x.date()).nunique()
mobility_metrics.append({
'uid' : uid,
'n_stops' : len (df_stops[df_stops['uid' ]==uid]),
'radius_of_gyration_km' : rg,
'mobility_entropy' : S,
'n_zones_visited' : df_stops[df_stops['uid' ]==uid]['zone' ].nunique(),
'home_lat' : h_lat, 'home_lon' : h_lon,
})
df_mobility = pd.DataFrame(mobility_metrics)
print ("Individual Mobility Metrics:" )
print (df_mobility[['uid' ,'n_stops' ,'radius_of_gyration_km' ,'mobility_entropy' ,'n_zones_visited' ]].round (3 ).to_string(index=False ))
fig, axes = plt.subplots(2 , 2 , figsize=(12 , 9 ))
axes[0 ][0 ].hist(df_mobility['radius_of_gyration_km' ].dropna(), bins=8 ,
color='steelblue' , edgecolor='black' , linewidth=0.7 )
axes[0 ][0 ].set_xlabel("Radius of Gyration (km)" ); axes[0 ][0 ].set_ylabel("Count" )
axes[0 ][0 ].set_title("Radius of Gyration Distribution" ); axes[0 ][0 ].grid(axis='y' , alpha=0.3 )
axes[0 ][1 ].hist(df_mobility['mobility_entropy' ].dropna(), bins=8 ,
color='coral' , edgecolor='black' , linewidth=0.7 )
axes[0 ][1 ].set_xlabel("Mobility Entropy (nats)" ); axes[0 ][1 ].set_ylabel("Count" )
axes[0 ][1 ].set_title("Mobility Entropy Distribution" ); axes[0 ][1 ].grid(axis='y' , alpha=0.3 )
axes[1 ][0 ].scatter(df_mobility['radius_of_gyration_km' ],
df_mobility['mobility_entropy' ],
c='purple' , s=80 , edgecolors='black' , linewidths=0.5 )
for _, row in df_mobility.iterrows():
axes[1 ][0 ].annotate(f"U{row['uid' ]} " , (row['radius_of_gyration_km' ], row['mobility_entropy' ]),
fontsize=8 , xytext=(3 ,3 ), textcoords='offset points' )
axes[1 ][0 ].set_xlabel("Radius of Gyration (km)" ); axes[1 ][0 ].set_ylabel("Mobility Entropy" )
axes[1 ][0 ].set_title("Mobility Extent vs. Diversity" ); axes[1 ][0 ].grid(True , alpha=0.3 )
zone_visit_matrix = df_stops.groupby(['uid' ,'zone' ])['duration_min' ].sum ().unstack(fill_value=0 )
im = axes[1 ][1 ].imshow(zone_visit_matrix.values, cmap='YlOrRd' , aspect='auto' )
plt.colorbar(im, ax=axes[1 ][1 ], label='Total time (min)' )
axes[1 ][1 ].set_xticks(range (n_zones)); axes[1 ][1 ].set_xticklabels(zone_names, rotation=30 , ha='right' , fontsize=8 )
axes[1 ][1 ].set_yticks(range (len (zone_visit_matrix))); axes[1 ][1 ].set_yticklabels([f"U{uid} " for uid in zone_visit_matrix.index], fontsize=8 )
axes[1 ][1 ].set_xlabel("Zone" ); axes[1 ][1 ].set_ylabel("Individual" )
axes[1 ][1 ].set_title("Time Spent per Zone per Individual" )
plt.tight_layout()
plt.savefig("mobility_metrics.png" , dpi=150 )
plt.show()
Advanced Usage
Commuting Pattern Analysis import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
df_stops['hour_of_day' ] = df_stops['arrive_ts' ].dt.hour
trip_hours = []
for uid, group in df_stops.groupby('uid' ):
g = group.sort_values('arrive_ts' )
for i in range (len (g)-1 ):
o_zone = g.iloc[i]['zone' ]
d_zone = g.iloc[i+1 ]['zone' ]
if o_zone != d_zone:
trip_hours.append(g.iloc[i]['leave_ts' ].hour if 'leave_ts' in g.columns else g.iloc[i]['arrive_ts' ].hour)
fig, ax = plt.subplots(figsize=(10 , 4 ))
ax.hist(trip_hours, bins=24 , range =(0 ,24 ), color='steelblue' , edgecolor='black' , linewidth=0.5 )
ax.set_xlabel("Hour of day" ); ax.set_ylabel("Number of trips" )
ax.set_title("Trip Departure Distribution by Hour" ); ax.grid(axis='y' , alpha=0.3 )
ax.set_xticks(range (0 , 25 , 2 )); ax.axvspan(7 , 9 , alpha=0.1 , color='red' , label='AM peak' )
ax.axvspan(17 , 19 , alpha=0.1 , color='orange' , label='PM peak' )
ax.legend(); plt.tight_layout(); plt.savefig("trip_hours.png" , dpi=150 ); plt.show()
Troubleshooting
Stop detection misses short stops
stops = detect_stops(trace, min_duration_min=5 )
GPS traces with gaps (network outage)
df_gps['time_gap' ] = df_gps.groupby('uid' )['ts' ].diff().dt.total_seconds() / 60
df_gps = df_gps[df_gps['time_gap' ] < 60 ]
Haversine distance overflow for antipodal points
return 2 * R * np.arcsin(np.sqrt(np.clip(a, 0 , 1 )))
Version Compatibility Package Tested versions Notes pandas 2.0, 2.1 dt.hour, groupby stablescikit-learn 1.3, 1.4 KMeans API stablegeopandas 0.14 For spatial joining with zones
External Resources
Official Documentation
Key Papers
González, M.C., Hidalgo, C.A. & Barabási, A.-L. (2008). Understanding individual human mobility patterns . Nature.
Song, C. et al. (2010). Limits of predictability in human mobility . Science.
Examples
Example 1: Daily Mobility Profile import numpy as np
import matplotlib.pyplot as plt
for uid in range (3 ):
user_trace = df_gps[df_gps['uid' ] == uid].copy()
user_trace['hour' ] = user_trace['ts' ].dt.hour
home_lat = user_trace.groupby('uid' ).apply(lambda x: x.iloc[0 ]['lat' ]).values[0 ] if len (user_trace) > 0 else 40.71
home_lon = user_trace.groupby('uid' ).apply(lambda x: x.iloc[0 ]['lon' ]).values[0 ] if len (user_trace) > 0 else -74.01
print ("Daily profile analysis requires full trace data — see Step 1 output." )
Example 2: Mobility Predictability (Maximum Entropy) import numpy as np
n_zones_visited = df_mobility['n_zones_visited' ].values
max_entropy = np.log(n_zones_visited)
actual_entropy = df_mobility['mobility_entropy' ].values
predictability = 1 - actual_entropy / (max_entropy + 1e-8 )
print ("Individual mobility predictability (1 = perfectly predictable):" )
for uid, pred in zip (df_mobility['uid' ], predictability):
print (f" User {uid} : {pred:.3 f} " )
print (f"\nMean predictability: {predictability.mean():.3 f} " )
Last updated: 2026-03-17 | Maintainer: @xjtulyc
Issues: GitHub Issues