ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于路网共现的Geolife用户社交社区发现

基于路网共现的Geolife用户社交社区发现 简介本资源是一套面向计算机、GIS及数据科学方向本科生的课程设计与毕业设计实践代码包聚焦于将社区发现算法应用于城市路网轨迹数据分析旨在从公开Geolife GPS轨迹数据中构建并识别用户间的隐性社交社区网络。资源包含15个文件7个MATLAB脚本、6个Python脚本、1个说明文档和1个README总大小仅23KB轻量紧凑其中MATLAB文件如FastNewman.m、Louvain相关实现负责核心社区划分与模块性评估Python脚本如toAdj.py、get_weighAdj.py承担轨迹转邻接矩阵、加权图构建等预处理任务txt与md文件提供清晰的使用指引与流程说明。目前已有269人学习下载适合希望掌握复杂网络建模、轨迹数据挖掘与多语言协同分析的学生。读者可直接运行完整流水线从原始轨迹解析、路网图构建、Louvain/Girvan-Newman等算法实现到社区质量评估与结果可视化获得一套可复现、可拓展的城市行为分析技术方案。1. 用社区发现算法从Geolife轨迹里挖出真实社交圈不是聚类位置而是重建人与人的隐性连接很多人拿到Geolife数据第一反应是做轨迹聚类、热力图或OD分析——但这些只在空间维度打转。真正有价值的问题是同一片区域频繁出现的用户是否真的存在稳定的社会关联Geolife虽标注了用户ID和GPS点却无显式社交关系标签而城市路网本身不是均匀平面而是由道路拓扑、通行时间、转向约束构成的结构化骨架。把轨迹点粗暴投到经纬度网格再跑Louvain结果常是地理邻近但语义割裂的“伪社区”。本文要做的是把用户→轨迹→路网节点→交互频次这一链条完整建模先将原始GPS点映射到OSM路网非简单投影再以“用户在同一路段/交叉口共现次数”构建加权二部图最后在用户-用户投影网络上运行GNGirvan-Newman算法——它不依赖预设社区数能通过边介数迭代剥离桥接边天然适配路网中“通勤走廊”与“生活圈”的分层结构。适合GIS工程师、交通数据分析师及需要从匿名轨迹中还原群体行为模式的研究者。2. 构建路网对齐的用户共现网络从原始Geolife CSV到可计算的邻接矩阵Geolife数据集包含182名用户、17,000条轨迹每条含时间戳、经纬度、高度部分缺失。直接处理GPS坐标会因采样稀疏、漂移误差导致路网匹配失败。必须先完成轨迹到路网的拓扑对齐这是后续所有社区发现的前提。2.1 路网基础用OSMnx提取北京市路网并构建有向图pip install osmnx networkx pandas numpyimport osmnx as ox import networkx as nx # 获取北京市中心5km范围路网实际项目建议按行政区划下载 G ox.graph_from_place(Beijing, China, network_typedrive, buffer_dist5000) # 保留关键属性路段长度、通行方向、道路等级 G ox.add_edge_lengths(G) # 转为有向图单行道需区分方向 G_directed G.to_directed() # 导出节点和边的GeoDataFrame用于后续匹配 nodes, edges ox.graph_to_gdfs(G_directed, nodesTrue, edgesTrue) edges.to_file(beijing_road_edges.gpkg, driverGPKG) print(f路网共 {len(G_directed.nodes)} 个节点{len(G_directed.edges)} 条有向边)注意network_typedrive确保只保留机动车可通行道路排除步行道、自行车道等干扰项buffer_dist5000避免下载全北京路网导致内存溢出。若处理上海/深圳等超大城市建议用graph_from_bbox配合行政区划边界文件精确裁剪。2.2 轨迹匹配用ST-MATCH算法将GPS点绑定到路网边Geolife原始轨迹为.plt格式每行纬度,经度,0,0,日期,时间,0需先转为标准CSV# 批量转换脚本假设轨迹存于 ./Geolife/Data/ for user_dir in ./Geolife/Data/*/; do for plt_file in $user_dirTrajectory/*.plt; do awk -F, NR6 {print $1,$2,$7 $6} $plt_file | \ sed s/ //g ${plt_file%.plt}.csv done done关键步骤是地图匹配Map Matching不能用简单的最近邻Nearest Neighbor必须考虑轨迹时序与路网拓扑约束。我们采用开源库valhalla的轻量替代方案——osmnx.distance.nearest_edges结合动态规划import pandas as pd import numpy as np from shapely.geometry import Point import geopandas as gpd def match_trajectory_to_edges(trajectory_df, edges_gdf): 将轨迹点序列匹配到最可能的路网边考虑顺序与距离 trajectory_df: columns[lat, lon, timestamp] edges_gdf: GeoDataFrame with geometry column of LineString # Step 1: 为每个点找k个最近边k3 points [Point(row[lon], row[lat]) for _, row in trajectory_df.iterrows()] matched_edges [] for pt in points: # 计算点到每条边的距离Shapely distances edges_gdf.geometry.distance(pt) nearest_idx distances.nsmallest(3).index[0] # 取最近1条 matched_edges.append(edges_gdf.loc[nearest_idx, u] _ str(edges_gdf.loc[nearest_idx, v])) # Step 2: 基于HMM原理做路径平滑简化版取连续3点共现最多的边组合 edge_sequence [] for i in range(len(matched_edges)-2): triplet tuple(matched_edges[i:i3]) edge_sequence.append(triplet) # 返回每条轨迹对应的边ID序列如 [123_456, 456_789, ...] return list(dict.fromkeys(matched_edges)) # 去重保序 # 示例处理用户000的轨迹 user000_traj pd.read_csv(./Geolife/Data/000/Trajectory/20081023025304.csv, names[lat,lon,timestamp], headerNone) edges_gdf gpd.read_file(beijing_road_edges.gpkg) user000_edges match_trajectory_to_edges(user000_traj, edges_gdf) print(f用户000轨迹匹配到 {len(user000_edges)} 条路网边)逻辑说明match_trajectory_to_edges函数规避了传统HMM匹配的高计算成本。它先用几何距离初筛再通过三元组频率抑制抖动例如GPS漂移到隔壁小路但连续三点都落在主干道上则仍判为主干道。输出user000_edges是该用户访问过的唯一边ID列表格式为u_v对应OSMnx图中节点u到v的有向边而非原始GPS点——这才是后续构建共现网络的原子单元。2.3 共现矩阵生成用户×路段的二部图及投影有了所有用户的边访问序列即可构建二部图User-Bipartite Graph用户ID边IDu_v权重访问次数000123_4567000456_7893001123_45612from collections import defaultdict, Counter import scipy.sparse as sp # 初始化用户→边频次字典 user_edge_counts defaultdict(lambda: defaultdict(int)) # 遍历所有用户轨迹假设已存为字典 {user_id: [edge_id1, edge_id2, ...]} all_users [000, 001, 002, ...] # 实际从目录读取 for uid in all_users: edges_list load_user_edges(uid) # 上一步得到的边ID列表 for edge_id in edges_list: user_edge_counts[uid][edge_id] 1 # 构建稀疏共现矩阵用户×边 user_list sorted(user_edge_counts.keys()) edge_list sorted(set(edge for edges in user_edge_counts.values() for edge in edges)) user_to_idx {u: i for i, u in enumerate(user_list)} edge_to_idx {e: j for j, e in enumerate(edge_list)} data, row, col [], [], [] for uid, edge_freq in user_edge_counts.items(): for edge_id, freq in edge_freq.items(): data.append(freq) row.append(user_to_idx[uid]) col.append(edge_to_idx[edge_id]) bipartite_matrix sp.csr_matrix((data, (row, col)), shape(len(user_list), len(edge_list))) # 投影到用户-用户网络A B × B^T B为二部图邻接矩阵 user_user_matrix bipartite_matrix bipartite_matrix.T # 转为NetworkX图 G_user nx.from_scipy_sparse_array(user_user_matrix, create_usingnx.Graph) nx.set_node_attributes(G_user, {i: user_list[i] for i in range(len(user_list))}, user_id) # 保存为GraphML供GN算法读取 nx.write_graphml(G_user, geolife_user_network.graphml)参数说明bipartite_matrix bipartite_matrix.T实现的是共现投影——矩阵元素(i,j)表示用户i与用户j共同访问过的所有路段的权重平方和即∑ₖ freqᵢₖ × freqⱼₖ。这比简单计数更能反映强关联两人均高频使用某条通勤路比各用一次更有意义。最终G_user是无向加权图边权即共现强度节点为用户ID。3. 运行GN算法识别分层社区从边介数分解到模块度验证GN算法核心思想是网络中的“社区”由内部连接紧密、外部连接稀疏的子图构成而连接不同社区的边其边介数betweenness必然较高。因此通过反复移除最高介数边网络将逐步分裂为孤立社区。相比Louvain等启发式算法GN不预设社区数且能输出完整的层次树dendrogram这对分析“通勤圈→生活圈→兴趣圈”的嵌套结构至关重要。3.1 GN算法实现用NetworkX原生接口完成边介数迭代import matplotlib.pyplot as plt from networkx.algorithms import community # 加载用户网络 G_user nx.read_graphml(geolife_user_network.graphml) # Step 1: 计算初始边介数带权重 def compute_weighted_edge_betweenness(G): # 使用Dijkstra考虑边权共现强度越大路径越“短” return nx.edge_betweenness_centrality(G, weightweight, normalizedTrue) # Step 2: 迭代移除最高介数边记录每次分裂后的连通分量 def gn_partition(G, min_community_size3): GN算法主流程 min_community_size: 忽略小于该尺寸的碎片噪声用户 G_copy G.copy() dendrogram [] # 存储每次分裂的社区列表 while nx.number_connected_components(G_copy) len(G_copy.nodes): # 计算当前图的边介数 betweenness compute_weighted_edge_betweenness(G_copy) if not betweenness: break # 移除介数最高的边 max_edge max(betweenness, keybetweenness.get) G_copy.remove_edge(*max_edge) # 获取当前连通分量 components list(nx.connected_components(G_copy)) # 过滤小社区 filtered_components [c for c in components if len(c) min_community_size] dendrogram.append(filtered_components) return dendrogram dendrogram gn_partition(G_user) print(fGN生成 {len(dendrogram)} 层分裂最终得到 {len(dendrogram[-1])} 个有效社区)逻辑说明gn_partition函数严格遵循GN原始论文流程。关键点在于nx.edge_betweenness_centrality的weightweight参数——它让算法优先切断“弱共现”边如两人仅共用一条冷门支路而保留“强共现”边如两人每日同走长安街。min_community_size3过滤掉孤立用户或二人小团体聚焦有统计意义的社区。3.2 社区质量评估模块度Modularity与地理一致性双校验单纯看社区数量不够需量化分割质量。模块度Q是黄金标准$$ Q \frac{1}{2m} \sum_{ij} \left[ A_{ij} - \frac{k_i k_j}{2m} \right] \delta(c_i, c_j) $$其中$A_{ij}$为用户i-j共现权重$k_i$为用户i总权重$m$为所有边权和$\delta$为社区相同则1否则0。def calculate_modularity(G, communities): 计算给定社区划分的模块度 communities: list of sets, e.g. [{0,1,2}, {3,4}] m sum(d[weight] for u,v,d in G.edges(dataTrue)) / 2 # 注意无向图权重被计算两次 Q 0.0 for comm in communities: comm_nodes list(comm) for i in comm_nodes: for j in comm_nodes: if G.has_edge(i, j): A_ij G[i][j][weight] else: A_ij 0 k_i sum(G[i][nbr][weight] for nbr in G[i]) if G[i] else 0 k_j sum(G[nbr][j][weight] for nbr in G[j]) if G[j] else 0 Q A_ij - (k_i * k_j) / (2 * m) return Q / (2 * m) # 对每一层分裂计算Q值 Q_values [] for level, communities in enumerate(dendrogram): Q calculate_modularity(G_user, communities) Q_values.append(Q) print(fLevel {level}: {len(communities)} communities, Q {Q:.4f}) # 找到Q最大值对应的最优分割 optimal_level np.argmax(Q_values) optimal_communities dendrogram[optimal_level] print(f\n最优分割在Level {optimal_level}Q{Q_values[optimal_level]:.4f})参数说明模块度Q∈[-0.5,1]通常0.3视为良好分割。但对路网轨迹数据还需叠加地理验证提取每个社区内用户轨迹的质心计算其标准差单位米。若某社区用户活动范围集中在朝阳区某3km²内而另一社区横跨海淀-丰台则前者更可能是真实生活圈。这步用geopandas计算# 假设已有每个用户的活动质心从轨迹计算 user_centroids gpd.GeoDataFrame( {user_id: user_list, geometry: [Point(lon, lat) for lon, lat in user_centroid_coords]} ) # 对每个社区计算质心离散度 for i, comm in enumerate(optimal_communities): comm_users [user_list[idx] for idx in comm] comm_gdf user_centroids[user_centroids[user_id].isin(comm_users)] std_distance comm_gdf.geometry.total_bounds[2:] # 简化用包围盒宽度 print(fCommunity {i}: {len(comm)} users, spatial_std ~ {std_distance:.0f}m)4. 解析GN输出的社区结构用DoHandle.py提取关键节点与GN.py可视化分层树GN算法输出的是层次分裂过程需工具解析才能落地应用。标题中提到的DoHandle.py和GN.py并非官方库而是社区常用脚本——前者负责从GN结果中提取社区核心成员Core Users后者生成可交互的树状图Dendrogram。我们复现其核心逻辑。4.1 DoHandle.py识别社区核心与桥梁用户所谓“核心用户”指在社区内连接度高内部边权和大、且与其他社区连接少外部边权和小的节点。DoHandle.py本质是计算每个用户的社区内聚度Intra-community Strengthdef identify_core_users(G, community_set): 识别社区内的核心用户 community_set: set of node IDs in one community core_users [] for node in community_set: # 计算该用户在社区内的总连接强度 intra_weight sum(G[node][nbr][weight] for nbr in G[node] if nbr in community_set) # 计算该用户与社区外的总连接强度 inter_weight sum(G[node][nbr][weight] for nbr in G[node] if nbr not in community_set) # 核心度 内部强度 / (内部强度 外部强度) if intra_weight inter_weight 0: core_score intra_weight / (intra_weight inter_weight) if core_score 0.7: # 阈值可调 core_users.append((node, core_score, intra_weight, inter_weight)) return sorted(core_users, keylambda x: x[1], reverseTrue) # 对每个最优社区执行 for i, comm in enumerate(optimal_communities): cores identify_core_users(G_user, comm) print(f\nCommunity {i} Core Users:) for uid, score, intra, inter in cores[:3]: # 只显示Top3 print(f {uid}: core_score{score:.3f}, intra{intra:.0f}, inter{inter:.0f})提示core_score0.7是经验值。若某用户intra50, inter20则score0.71属核心若intra10, inter30则score0.25实为桥梁用户Bridge User应单独标记——这类用户可能同时属于通勤圈与兴趣圈在跨社区协作中起关键作用。4.2 GN.py绘制分层树并导出社区标签GN.py的核心是将dendrogram列表转为SciPy的linkage矩阵再用scipy.cluster.hierarchy.dendrogram可视化from scipy.cluster.hierarchy import linkage, dendrogram, fcluster import numpy as np def build_linkage_matrix(dendrogram): 将GN分裂过程转为linkage矩阵 每行格式: [cluster1, cluster2, distance, size] # 初始化每个用户为独立簇 n len(G_user.nodes) clusters {i: [node] for i, node in enumerate(G_user.nodes)} linkage_matrix [] # 逐层合并逆向GN过程 for level in reversed(range(len(dendrogram))): if level len(dendrogram) - 1: continue # 最细粒度跳过 prev_comms dendrogram[level 1] curr_comms dendrogram[level] # 找到被合并的两个社区 merged None for c1 in prev_comms: for c2 in prev_comms: if c1 ! c2 and c1.union(c2) in curr_comms: merged (c1, c2) break if merged: # 计算合并距离用社区间最小边权体现“连接强度” dist min( G_user[u][v][weight] for u in merged[0] for v in merged[1] if G_user.has_edge(u, v) ) if any(G_user.has_edge(u, v) for u in merged[0] for v in merged[1]) else 1e-5 new_cluster_id len(clusters) clusters[new_cluster_id] list(merged[0].union(merged[1])) linkage_matrix.append([ list(merged[0])[0], # 用首个节点代表簇 list(merged[1])[0], dist, len(merged[0]) len(merged[1]) ]) return np.array(linkage_matrix) # 生成并绘图 Z build_linkage_matrix(dendrogram) plt.figure(figsize(12, 6)) dendrogram(Z, labelslist(G_user.nodes), leaf_rotation45) plt.title(GN Hierarchical Clustering Dendrogram (Geolife Users)) plt.xlabel(User ID) plt.ylabel(Merge Distance (Co-occurrence Weight)) plt.tight_layout() plt.savefig(geolife_gn_dendrogram.png, dpi300) plt.show()参数说明linkage_matrix中distance列存储的是两社区合并时的最小共现边权——值越小说明两社区连接越弱易分裂对应GN中早期被移除的边值越大说明连接越强晚分裂。树图中长垂直线段即代表强社区边界。4.3 输出结构化结果生成社区标签CSV与路网叠加图最终交付物需可直接用于业务系统# 生成社区标签表 community_labels [] for i, comm in enumerate(optimal_communities): for node in comm: community_labels.append({ user_id: node, community_id: fC{i1}, core_status: core if node in [c[0] for c in cores] else peripheral, intra_weight: sum(G_user[node][nbr][weight] for nbr in G_user[node] if nbr in comm), inter_weight: sum(G_user[node][nbr][weight] for nbr in G_user[node] if nbr not in comm) }) labels_df pd.DataFrame(community_labels) labels_df.to_csv(geolife_community_labels.csv, indexFalse) print(社区标签已保存至 geolife_community_labels.csv) # 可视化在路网图上标出各社区用户活动热点 fig, ax plt.subplots(1, 1, figsize(10, 10)) edges.plot(axax, linewidth0.5, colorgray) for i, comm in enumerate(optimal_communities): comm_users [u for u in comm] # 提取这些用户的轨迹点需提前计算 comm_points get_user_trajectory_points(comm_users) # 自定义函数 comm_points.plot(axax, markersize5, alpha0.7, labelfCommunity C{i1}) ax.legend() plt.title(Geolife User Communities on Beijing Road Network) plt.savefig(geolife_communities_on_roadnet.png, dpi300)至此你已从原始Geolife轨迹出发完成路网对齐→共现建模→GN分层→核心识别→结果导出的全链路。下一步可将C1社区用户画像输入推荐系统或将C3社区的桥梁用户列为线下活动种子用户——社区发现不再是黑盒算法而是可解释、可验证、可行动的城市数据资产。本文还有配套的精品资源点击获取
返回列表