2026/10/13 18:37:11

山东省SHP数据实战:从坐标系确认到县级交通可达性打分

山东省SHP数据实战:从坐标系确认到县级交通可达性打分 简介这份资源面向GIS从业者、城市规划与交通研究人员及地理信息相关专业学生提供2022年7月山东省的行政区划与交通网络矢量数据可用于空间查询、叠加分析、缓冲区分析及专题制图等场景。压缩包共33个文件约37.59MB以shp、dbf、prj、shx、xml等格式为主其中shp存储省、市、县行政边界及道路、铁路的几何形状dbf保存名称、等级、里程等属性信息prj定义坐标系统以保障定位准确shx与xml则分别承担索引和元数据说明。数据覆盖省级、市级、县级三级行政单元并配套公路网与铁路网图层可直接在ArcGIS、QGIS等软件中加载使用。目前已有2018人学习下载适合用于交通规划、灾害响应评估、环境研究及商业选址等分析帮助读者快速构建山东区域的空间数据基础减少数据采集与整理成本。1. 拿到一份 2022.07 的山东省 SHP 数据先别急着往 GIS 里拖如果你手上刚拿到一份标注为「2022.07」的山东省行政区划 SHP 文件里面同时包含省、市、县三级边界外加道路网和公路网两个线图层第一反应大概率是直接拖进 QGIS 或 ArcGIS 看效果。我理解这种冲动但这份数据真正值钱的地方不在「能打开」而在于它把行政边界和交通网络放在了同一套坐标体系下——这意味着你可以做行政区划统计、路网密度分析、可达性计算甚至给县级单元做交通便利度打分。问题在于很多人在第一步就翻车坐标系没确认、字段编码没检查、层级关系没理顺后面所有分析都是空中楼阁。这篇文章面向的是需要拿这份数据做实际分析的人——不管你是做区域规划、物流选址还是单纯想跑一遍路网密度计算下面这套流程都能直接抄。2. 先搞清楚这份 SHP 里到底装了什么图层结构与字段拆解2.1 省、市、县三级边界是三个独立图层还是混在一起拿到一份行政区划 SHP第一件事不是打开属性表而是确认它的组织方式。常见做法有两种一种是把省、市、县分别存成三个独立的.shp文件比如province.shp、city.shp、county.shp另一种是全部塞进一个图层靠一个level字段区分层级。这两种方式对后续处理的影响完全不同。如果是三个独立文件好处是结构清晰做空间连接时不容易搞混层级坏处是跨层级统计时要手动关联。如果是单图层加level字段好处是一次加载就能看到全貌坏处是筛选时容易漏条件——比如你只想取县级边界结果忘了加level3把省市边界也算进去了面积统计直接翻倍。我一般会先用ogrinfo看一眼图层清单和字段结构再决定怎么处理。GDAL 自带的这个命令比打开桌面软件快得多尤其适合在服务器上做批量检查。# 查看 SHP 文件的图层信息和字段结构 ogrinfo -so -al shandong_admin_2022.shp # 如果省、市、县是分开的文件逐个查看 ogrinfo -so -al province.shp ogrinfo -so -al city.shp ogrinfo -so -al county.shp-so表示只输出摘要信息不打印每个要素的几何-al表示对所有图层生效。输出里重点看三样东西Geometry类型是 Polygon 还是 MultiPolygon、Feature Count要素数量是否符合预期比如山东省县级单元大概在 130 多个、Extent范围是否落在山东境内。如果县级图层要素数明显偏少可能是数据只覆盖了部分地市后面做全省统计就会出问题。2.2 道路网和公路网的区别别把两个线图层当成一回事标题里同时出现了「道路网」和「公路网」这两个词在日常语境下经常混用但在 GIS 数据里通常是两个不同的图层。公路网一般指国道、省道、县道这类有行政等级的道路属性表里会有road_class或grade字段道路网的范围更宽可能包含城市道路、乡村道路甚至内部道路属性字段更杂。这个区别直接影响你怎么用。如果你要做的是「县级单元公路通达度」那应该用公路网图层按road_class筛选出国道和省道再和县级边界做空间连接。如果你要做的是「路网密度」那道路网图层更合适因为它覆盖更全但要注意剔除重复路段和断头路。import geopandas as gpd # 分别加载两个线图层先看字段 road_net gpd.read_file(road_network.shp) highway_net gpd.read_file(highway_network.shp) print(道路网字段:, road_net.columns.tolist()) print(公路网字段:, highway_net.columns.tolist()) print(道路网要素数:, len(road_net)) print(公路网要素数:, len(highway_net)) # 查看公路等级分布确认字段名和取值 if road_class in highway_net.columns: print(highway_net[road_class].value_counts()) elif grade in highway_net.columns: print(highway_net[grade].value_counts())这段代码的关键不是加载数据而是确认字段名。不同来源的 SHP 数据道路等级字段可能叫road_class、grade、type、level取值可能是「国道/省道/县道」也可能是「G/S/X」这样的编码。不先确认就写筛选条件后面必然报错。要素数也要看一眼如果公路网只有几百条那大概率只覆盖了主要干道不能拿来算全域路网密度。2.3 坐标系确认没有这一步后面所有距离计算都是错的坐标系是 SHP 数据最容易埋雷的地方。常见情况有三种数据自带.prj文件且坐标系正确数据没有.prj文件但实际是某种坐标系数据有.prj文件但坐标系标错了。第三种最坑因为软件会按错误的坐标系去投影算出来的距离和面积完全不对。山东所在的经度范围大约在东经 114° 到 122° 之间纬度在北纬 34° 到 38° 之间。如果数据是地理坐标系坐标值应该是小数形式的经纬度如果是投影坐标系坐标值通常是六位或七位的大数。你可以先用geopandas读一下边界范围心里有个数。import geopandas as gpd county gpd.read_file(county.shp) # 查看当前坐标系 print(当前 CRS:, county.crs) # 查看范围 bounds county.total_bounds print(范围:, bounds) # 如果 CRS 为空根据范围判断原始坐标系 if county.crs is None: if bounds[0] 100 and bounds[0] 130: print(坐标值像经纬度可能是 CGCS2000 或 WGS84 地理坐标系) else: print(坐标值像投影坐标需要进一步确认带号)如果crs为空但坐标值像经纬度常见做法是先按EPSG:4490CGCS2000 地理坐标系赋值再转到投影坐标系做距离计算。山东常用的投影坐标系是EPSG:4547CGCS2000 3 度带 CM 117E或EPSG:4526附近具体用哪个取决于数据覆盖范围。这一步不能猜猜错了后面路网长度能差出几十公里。3. 把三级边界和路网接起来空间连接与统计的完整流程3.1 用县级边界裁剪路网得到每个县的路网长度拿到干净的图层之后最常做的分析就是「每个县有多少公里公路」。这个操作在 GIS 里叫空间连接加长度汇总核心逻辑是先把路网按县级边界做裁剪或相交再按县名分组求和。这里有个细节容易翻车如果一条公路横跨两个县直接做空间连接会把整条路的长度算给两个县导致总里程虚高。正确做法是先做intersection把跨县路段打断再算每段的长度。import geopandas as gpd # 加载县级边界和公路网 county gpd.read_file(county.shp) highway gpd.read_file(highway_network.shp) # 统一坐标系确保两者一致 if county.crs ! highway.crs: highway highway.to_crs(county.crs) # 投影到适合山东的坐标系方便算长度 county_proj county.to_crs(EPSG:4547) highway_proj highway.to_crs(EPSG:4547) # 用县级边界打断公路 highway_clip gpd.overlay(highway_proj, county_proj, howintersection) # 计算每段长度单位米 highway_clip[length_m] highway_clip.geometry.length # 按县名汇总 result highway_clip.groupby(county_name)[length_m].sum().reset_index() result[length_km] result[length_m] / 1000 print(result.sort_values(length_km, ascendingFalse).head(10))gpd.overlay的howintersection会把每条公路按县级边界切碎落在每个县内的段单独成行。geometry.length在投影坐标系下返回的是米除以 1000 得到公里。分组字段county_name要换成你数据里实际的县名字段可能是NAME、县名、adcode之类。如果结果里出现某个县的路网长度为 0先检查是不是县名匹配不上或者该县边界和路网确实没有交集。3.2 按道路等级分层统计国道、省道、县道各占多少只算总里程还不够很多分析需要按道路等级拆分。比如做交通优势度评价时国道和省道的权重不一样。这时候就要在汇总前先按等级字段筛选或分组。# 假设公路等级字段是 road_class取值有 国道、省道、县道 highway_clip[road_class] highway_clip[road_class].fillna(未知) # 按县和道路等级分组汇总 result_by_class highway_clip.groupby([county_name, road_class])[length_m].sum().reset_index() result_by_class[length_km] result_by_class[length_m] / 1000 # 透视成宽表方便对比 pivot result_by_class.pivot_table( indexcounty_name, columnsroad_class, valueslength_km, fill_value0 ) print(pivot.head())fillna(未知)是为了防止空值导致分组丢失。pivot_table把长表转成宽表每个县一行国道、省道、县道各一列。如果某个等级在所有县都是 0那说明筛选条件写错了或者数据里根本没有这个等级的路。3.3 路网密度计算面积口径和长度口径要统一路网密度等于路网长度除以区域面积。听起来简单但有两个坑一是面积单位二是面积口径。面积要用投影坐标系下的面积不能用经纬度算口径要统一要么都用县级行政面积要么都用建设用地面积不能混。# 计算每个县的面积单位平方公里 county_proj[area_km2] county_proj.geometry.area / 1e6 # 合并路网长度和面积 density result.merge( county_proj[[county_name, area_km2]], oncounty_name, howleft ) # 计算路网密度单位 km/km2 density[road_density] density[length_km] / density[area_km2] print(density[[county_name, length_km, area_km2, road_density]].sort_values(road_density, ascendingFalse).head(10))geometry.area在投影坐标系下返回平方米除以1e6得到平方公里。如果area_km2出现异常大或异常小的值检查是不是有县的几何是空的或者坐标系没转对。路网密度超过 3 km/km² 的县通常是城区或者地形平坦的农业县低于 0.5 的多半是山区。4. 避坑指南这份数据最容易出问题的五个地方4.1 县级边界有重叠或缝隙导致路网被重复计算现象两个相邻县的路网长度加起来比实际总长度多出一截或者某个县的路网长度明显偏高。原因县级边界图层存在重叠区域或者边界之间有缝隙。重叠区域在overlay时会把同一条路算两次缝隙则会导致路网被切断后落在空白区统计时丢失。解决先做拓扑检查。用geopandas的unary_union把县级边界合并成一个整体再看有没有自相交或缝隙。如果重叠不严重可以在overlay之前先对县级边界做dissolve再重新拆分如果缝隙明显需要用buffer(0)修复几何。# 检查县级边界是否有重叠 county_union county_proj.unary_union county_dissolved county_proj.dissolve() # 对比合并前后的面积 print(原始面积和:, county_proj.geometry.area.sum()) print(合并后面积:, county_dissolved.geometry.area.sum()) # 如果差异超过 1%说明有重叠 if abs(county_proj.geometry.area.sum() - county_dissolved.geometry.area.sum()) / county_dissolved.geometry.area.sum() 0.01: print(存在明显重叠需要修复)4.2 道路网图层包含重复路段长度统计虚高现象路网总长度比官方公布的公路里程多出很多或者同一段路在属性表里出现多次。原因道路网数据在采集时可能把双向车道分别存成两条线或者同一条路在不同图层里重复录入。如果不做去重长度直接翻倍。解决先按几何去重再按属性去重。几何去重用drop_duplicates(subsetgeometry)属性去重看有没有唯一的道路编号字段。如果没有编号可以按起终点坐标和道路名称组合去重。# 按几何去重 highway_dedup highway_proj.drop_duplicates(subsetgeometry) # 如果有效果对比去重前后的要素数和总长度 print(去重前要素数:, len(highway_proj), 总长度:, highway_proj.geometry.length.sum() / 1000) print(去重后要素数:, len(highway_dedup), 总长度:, highway_dedup.geometry.length.sum() / 1000)4.3 县名字段有空格或编码问题分组时匹配不上现象分组汇总后某些县的路网长度为 0或者县名出现乱码。原因SHP 文件的属性表对中文支持不稳定县名字段可能带有前后空格、不可见字符或者编码不是 UTF-8。解决分组前先做字符串清洗去掉空格和特殊字符。如果乱码严重用ogrinfo确认编码必要时用 GDAL 重新导出为 UTF-8。# 清洗县名字段 county_proj[county_name] county_proj[county_name].str.strip() highway_clip[county_name] highway_clip[county_name].str.strip() # 检查是否有空值或异常字符 print(county_proj[county_name].unique()[:20])4.4 投影坐标系选错路网长度偏差超过 10%现象算出来的路网长度和官方数据对不上偏差不大不小刚好在 10% 到 20% 之间。原因用了地理坐标系直接算长度或者投影带号选错了。地理坐标系下geometry.length返回的是度数不是米带号选错会导致投影变形偏大。解决山东全境建议用EPSG:4547或EPSG:4526前者是 3 度带 CM 117E后者是 CM 120E。如果不确定可以分别用两个坐标系算一遍对比差异。差异小于 1% 说明选哪个都行差异大说明数据覆盖范围偏向某一侧。4.5 道路网和公路网坐标系不一致叠加后错位现象把道路网和公路网同时加载到地图上发现两条线明显错开或者路网跑到行政区划外面去了。原因两个图层的坐标系不同一个是地理坐标系一个是投影坐标系或者投影带号不同。解决加载后先检查crs不一致就统一转到同一个坐标系。注意转换顺序先转成地理坐标系再转目标投影坐标系不要直接从一个投影坐标系转到另一个投影坐标系。# 统一坐标系 target_crs EPSG:4547 road_net road_net.to_crs(target_crs) highway_net highway_net.to_crs(target_crs) county county.to_crs(target_crs) # 检查范围是否一致 print(道路网范围:, road_net.total_bounds) print(公路网范围:, highway_net.total_bounds) print(县级边界范围:, county.total_bounds)5. 进阶用法用这份数据做县级交通可达性打分5.1 构建评价指标路网密度、干线占比、邻接关系前面做的路网密度只是最基础的指标。如果你想让这份数据发挥更大价值可以做一个县级交通可达性综合打分。我一般会选三个维度路网密度反映覆盖程度、干线占比反映道路等级结构、邻接关系反映与周边县的连通性。路网密度和干线占比可以直接从前面算的结果里取。邻接关系需要额外做一步用县级边界做空间自相关找出每个县的相邻县再统计相邻县之间的公路连接数。# 计算干线占比 pivot[total_km] pivot.sum(axis1) pivot[arterial_ratio] (pivot.get(国道, 0) pivot.get(省道, 0)) / pivot[total_km] # 构建邻接关系 county_proj[neighbors] None for idx, row in county_proj.iterrows(): neighbors county_proj[county_proj.geometry.touches(row.geometry)][county_name].tolist() county_proj.at[idx, neighbors] neighbors # 统计邻接县数量 county_proj[neighbor_count] county_proj[neighbors].apply(len)geometry.touches判断两个几何是否相邻但不重叠。neighbor_count越大说明该县与周边县的物理连接越多交通区位越好。这个指标在山区县和平原县之间差异会很明显。5.2 归一化与加权把三个指标合成一个分数三个指标量纲不同不能直接相加。先做 min-max 归一化再按权重合成。权重怎么定取决于你的分析目的如果关注覆盖公平性路网密度权重高一些如果关注干线连通干线占比权重高一些。from sklearn.preprocessing import MinMaxScaler # 准备数据 score_df density[[county_name, road_density]].merge( pivot[[arterial_ratio]], left_indexTrue, right_indexTrue, howleft ).merge( county_proj[[county_name, neighbor_count]], oncounty_name, howleft ) # 归一化 scaler MinMaxScaler() score_df[[density_norm, arterial_norm, neighbor_norm]] scaler.fit_transform( score_df[[road_density, arterial_ratio, neighbor_count]] ) # 加权求和权重按需调整 score_df[accessibility_score] ( score_df[density_norm] * 0.4 score_df[arterial_norm] * 0.35 score_df[neighbor_norm] * 0.25 ) print(score_df.sort_values(accessibility_score, ascendingFalse).head(10))权重0.4 / 0.35 / 0.25是我做区域交通评价时常用的经验值你可以根据实际需求调整。归一化用MinMaxScaler把每个指标压到 0 到 1 之间避免量纲差异导致某个指标主导结果。5.3 结果验证用已知事实做交叉检查打分结果出来后别急着下结论。先做一轮交叉检查得分最高的县是不是你印象中交通发达的县得分最低的是不是山区或边缘县如果结果和常识偏差太大回头检查数据或权重。我一般会挑三到五个县手动核对它们的路网密度和干线数量。比如某个县得分很高但路网密度一般那可能是邻接关系指标拉高了分数需要确认邻接关系计算是否正确。这种手动核对看起来笨但能帮你发现数据里的隐藏问题比盲目相信模型输出靠谱得多。# 挑几个县手动核对 check_counties [某县A, 某县B, 某县C] print(score_df[score_df[county_name].isin(check_counties)]) # 对比原始路网长度和面积 print(density[density[county_name].isin(check_counties)])如果核对发现某个县的路网长度明显异常回到第 4 章排查重叠、重复和坐标系问题。数据质量永远比模型复杂度重要这句话在我做过的每一个空间分析项目里都被验证过。希望帮到你。本文还有配套的精品资源点击获取