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 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/xjtulyc/awesome-rosetta-skills --skill mobility-trajectories명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... 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