☰
2020-2022年成都市矢量数据跨年对比:坐标系与时间字段处理指南
2026/10/10 0:24:12 网站建设 项目流程

简介:这份2020—2022年成都市矢量数据资源面向GIS从业者、城市规划研究者、交通与水文分析人员及地图应用开发者,用于支撑空间分析、制图与决策支持。数据覆盖路网(含高速、国道、省道、县道及铁路地铁轻轨)、河流水系、建筑轮廓、省市县乡镇行政区划与POI兴趣点,可服务于交通规划、洪水预警、日照分析、商业选址与公共服务布局等场景。资源包共229个文件,以shp、shx、dbf、prj等Shapefile核心格式为主,辅以sbx、sbn空间索引、cpg编码说明及xlsx统计表、xml元数据,另含tif与tfw栅格文件,整体约95.58MB,目录按主题分模块组织,便于按需检索。目前已有1673人学习下载。读者可据此快速搭建成都市基础地理数据库,开展空间叠加、可达性评估与专题制图,为科研、规划与智慧城市系统开发提供可靠底图与属性支撑。

1. 拿到一份“2020-2022年成都市矢量数据”,先别急着往GIS里拖

如果你手里正好有一份标注为“2020-2022年成都市矢量数据”的文件包,第一反应大概率是双击、拖进QGIS、看图层能不能正常渲染。这个动作没错,但真正决定这份数据能不能用的,不是它能不能打开,而是三件事:坐标系对不对、时间字段有没有、行政边界版本是不是同一套。我见过太多人把三年数据叠在一起做变化检测,结果发现2020年用的是老版边界,2022年换了新版,同一个区县面积差了十几平方公里,后面所有统计全废。这份数据本质上是一套带时间维度的空间要素集合,常见内容包括行政区划、道路、水系、建设用地、POI等,格式多为Shapefile或GeoJSON。它适合做城市扩张分析、路网演变、用地变化监测这类需要跨年份对比的工作。下面我按实际处理流程,把坐标系、字段、合并、避坑和进阶验证拆开讲。

2. 坐标系与时间字段:决定这份数据能不能跨年对比的两个命门

2.1 为什么必须先查坐标系,而不是先看图形

矢量数据打开后图形位置“看起来对”,不代表坐标系定义正确。常见情况是:文件里.prj缺失或写的是WGS84地理坐标,但实际坐标值是投影后的米制单位。这种数据在QGIS里会显示在几内亚湾附近,但如果你手动改了显示CRS,它又能“正常”叠在底图上,于是很多人就这么用了。跨年对比时,只要有一年的坐标系定义和别人不一样,面积、长度、缓冲区分析全部失真。

成都市常用坐标系有两类:地理坐标系CGCS2000(EPSG:4490)和投影坐标系CGCS2000 3度带(EPSG:4544,中央经线105°E)。2020-2022年的政务数据里,两者都出现过。我的习惯是先用ogrinfo看元数据,再用Python批量检查。

# 查看单个Shapefile的坐标系和字段信息 ogrinfo -so -al chengdu_2020_boundary.shp # 批量查看目录下所有shp的CRS for f in *.shp; do echo "=== $f ===" ogrinfo -so -al "$f" | grep -E "Layer name|Geometry|Feature Count|EPSG" done

-so表示只输出摘要,-al表示所有图层。重点看输出里的EPSG编号和Geometry类型。如果EPSG显示为0或4326但坐标值是6位数以上,基本可以判定坐标系定义有问题。

import geopandas as gpd from pathlib import Path def check_crs_and_time(data_dir): """批量检查矢量数据的坐标系和时间字段""" results = [] for shp in Path(data_dir).glob("*.shp"): gdf = gpd.read_file(shp) # 检查坐标系 crs = gdf.crs # 检查可能的年份字段 year_cols = [c for c in gdf.columns if any(k in c.lower() for k in ['year', '年份', 'date', '时间'])] results.append({ 'file': shp.name, 'crs': str(crs), 'features': len(gdf), 'year_fields': year_cols, 'geom_type': gdf.geom_type.unique().tolist() }) return results for r in check_crs_and_time("./chengdu_vector"): print(r)

这段代码做三件事:读取每个Shapefile、提取CRS、扫描字段名里含年份或时间关键词的列。参数上,Path.glob("*.shp")只匹配Shapefile,如果你的数据是GeoJSON,改成*.geojson。year_cols的判断用了中英文关键词,因为2020-2022年的数据里两种命名都常见。

如果发现某一年没有年份字段,不要慌。常见做法是:文件名里通常带年份,可以在读取时用文件名回填一个source_year字段。但要注意,文件名年份和要素实际年份可能不一致,尤其是道路和水系这类更新周期长的数据。

2.2 时间字段的三种存在形式与统一策略

2020-2022年成都市矢量数据里,时间信息通常以三种形式出现:独立字段(如YEAR、年份)、日期字段(如UPDATE_DATE)、或者只存在于文件名。跨年合并前必须统一成同一列。

import pandas as pd def unify_year_field(gdf, filepath): """把不同形式的时间信息统一为 year 列""" fname = Path(filepath).stem # 优先级1:已有年份字段 for col in ['YEAR', 'year', '年份', 'YEAR_']: if col in gdf.columns: gdf['year'] = pd.to_numeric(gdf[col], errors='coerce').astype('Int64') return gdf # 优先级2:日期字段提取年份 for col in ['UPDATE_DATE', 'DATE', '更新时间']: if col in gdf.columns: gdf['year'] = pd.to_datetime(gdf[col], errors='coerce').dt.year return gdf # 优先级3:从文件名提取4位年份 import re match = re.search(r'(20\d{2})', fname) if match: gdf['year'] = int(match.group(1)) else: gdf['year'] = pd.NA return gdf

逻辑说明:优先用数据自带字段,其次从日期字段提取,最后才从文件名兜底。errors='coerce'把无法转换的值变成NaN,避免整列报错。Int64是pandas的可空整数类型,因为年份列可能有缺失。参数上,如果你的日期字段格式是“2020/1/1”这种,pd.to_datetime默认能解析;如果是“20200101”这种纯数字,需要加format='%Y%m%d'。

注意:从文件名提取年份时,如果文件名里同时出现“2020-2022”这种范围,正则(20\d{2})只会匹配到第一个年份。这种情况需要手动确认数据实际覆盖年份,不能盲目相信文件名。

3. 把三年数据合并成一套可分析图层:字段对齐与几何修复

3.1 字段对齐:为什么直接concat会翻车

三年数据的字段名和字段数量往往不一致。2020年可能叫NAME,2021年叫名称,2022年叫NAME_CN。直接pd.concat会生成三套列,后面按名称筛选时全是空值。我的做法是先做字段映射表,再合并。

import geopandas as gpd import pandas as pd from pathlib import Path # 字段映射:把各年份的字段名统一到标准名 FIELD_MAP = { 'NAME': 'name', '名称': 'name', 'NAME_CN': 'name', 'CODE': 'code', '编码': 'code', 'ADCODE': 'code', 'TYPE': 'type', '类型': 'type', 'CATEGORY': 'type' } def align_and_merge(data_dir, geom_type_filter=None): """对齐字段并合并多年矢量数据""" gdfs = [] for shp in sorted(Path(data_dir).glob("*.shp")): gdf = gpd.read_file(shp) # 统一年份 gdf = unify_year_field(gdf, shp) # 字段重命名 gdf = gdf.rename(columns={k: v for k, v in FIELD_MAP.items() if k in gdf.columns}) # 只保留标准字段+geometry+year keep = ['name', 'code', 'type', 'year', 'geometry'] gdf = gdf[[c for c in keep if c in gdf.columns]] # 几何类型过滤(比如只要面数据) if geom_type_filter: gdf = gdf[gdf.geom_type == geom_type_filter] gdfs.append(gdf) merged = pd.concat(gdfs, ignore_index=True) return gpd.GeoDataFrame(merged, crs=gdfs[0].crs)

关键参数:FIELD_MAP里的键是原始字段名,值是你希望统一成的标准名。实际数据里字段名可能更多,建议先跑一遍check_crs_and_time把所有字段列出来,再补全映射表。geom_type_filter用于过滤几何类型,比如做行政区划分析时只要Polygon,避免点线面混在一起。

合并后必须检查一件事:merged.crs是否和每个单年数据一致。如果某一年坐标系不同,pd.concat不会报错,但几何坐标会错位。正确做法是在合并前对每个gdf做to_crs统一。

# 合并前统一到EPSG:4544 target_crs = "EPSG:4544" for i, gdf in enumerate(gdfs): if gdf.crs != target_crs: gdfs[i] = gdf.to_crs(target_crs)

3.2 几何修复:自相交、空几何和重复要素

跨年数据合并后,几何问题会集中暴露。最常见的是自相交多边形(Invalid Geometry),在做空间叠加或面积统计时直接报错。另一个是空几何,通常来自数据导出时的截断。

from shapely.validation import make_valid def fix_geometries(gdf): """修复无效几何并删除空几何""" # 删除空几何 gdf = gdf[~gdf.geometry.is_empty & gdf.geometry.notna()].copy() # 修复无效几何 invalid_mask = ~gdf.geometry.is_valid if invalid_mask.any(): gdf.loc[invalid_mask, 'geometry'] = gdf.loc[invalid_mask, 'geometry'].apply(make_valid) # 删除重复要素(按name+year+geometry判断) gdf = gdf.drop_duplicates(subset=['name', 'year', 'geometry'], keep='first') return gdf

make_valid是Shapely 1.8+提供的修复函数,比传统的buffer(0)更稳定,不会改变几何形状。drop_duplicates的subset里包含geometry,是因为同名要素在不同年份可能几何不同,不能只按name去重。参数上,如果你的数据量很大(超过10万要素),apply(make_valid)会比较慢,可以先用gdf[invalid_mask]筛出无效的再修复。

注意:make_valid修复后的几何可能从Polygon变成MultiPolygon,如果后续分析要求单部件,需要再拆一次。这个坑我在处理某年建设用地数据时踩过,修复后面积统计对不上,排查半天才发现是几何类型变了。

4. 避坑与排查:这份数据最容易翻车的五个地方

4.1 现象:面积统计结果逐年跳变,差值远超预期

原因:不同年份的行政边界版本不一致。2020年可能用的是乡镇级边界,2022年换成了区县级,或者同一区县的边界被重新勘定过。直接按名称分组求和,会把不同尺度的数据混在一起。

解决:先做边界版本比对。用gdf.dissolve(by='name')把每年数据按名称融合,然后计算面积,看同一名称的面积是否一致。如果差异超过5%,说明边界版本变了。这时候要么统一到最新版边界,要么在分析时注明边界版本。

# 检查同名要素的面积一致性 for year in [2020, 2021, 2022]: sub = merged[merged['year'] == year].copy() sub['area'] = sub.geometry.area / 1e6 # 平方公里 area_by_name = sub.groupby('name')['area'].sum() print(f"--- {year} ---") print(area_by_name.head())

4.2 现象:空间连接后大量要素匹配不上

原因:坐标系不统一,或者几何有微小偏移。即使两个图层都标称EPSG:4544,如果一个是基于旧版控制点,一个是新版,叠加时会有米级偏移。道路和行政区划做空间连接时,边界附近的要素会漏匹配。

解决:先做坐标系一致性检查,再用buffer做容差匹配。比如判断POI是否在某个区内,可以给区边界加一个1米缓冲区再判断。

# 容差空间连接 buffered = districts.copy() buffered['geometry'] = buffered.geometry.buffer(1) # 1米容差 joined = gpd.sjoin(pois, buffered, how='left', predicate='within')

4.3 现象:合并后属性表出现大量空值

原因:字段映射不完整,或者某年数据缺少关键字段。比如2020年有type字段,2021年没有,合并后2021年的type全是NaN。

解决:在合并前打印每年的字段列表,做差集检查。对于缺失字段,如果该字段对分析必要,需要从其他来源补全;如果不必要,在合并时直接丢弃。

# 检查各年字段差异 all_cols = {} for shp in Path(data_dir).glob("*.shp"): gdf = gpd.read_file(shp) all_cols[shp.name] = set(gdf.columns) # 打印字段差异 for name, cols in all_cols.items(): print(f"{name}: {cols}")

4.4 现象:QGIS里显示正常,导出后坐标错乱

原因:QGIS的动态投影功能会让数据“看起来”在正确位置,但文件本身的坐标系定义没变。导出时如果选了错误的CRS,或者没勾选“另存为时重新投影”,就会出问题。

解决:导出时明确指定目标CRS,并在导出后用ogrinfo复查。不要依赖QGIS的显示效果判断坐标系。

4.5 现象:年份字段混入非年份值

原因:从文件名提取年份时,正则匹配到了其他数字。比如文件名“chengdu_2020_road_3rd.shp”,正则(20\d{2})会匹配到2020,但如果文件名是“chengdu_road_2020_v2.shp”,也可能匹配到2020,这没问题。真正的问题是文件名里同时有多个4位数,比如“2020_2022_merge.shp”,只会取到2020。

解决:提取后做范围校验,只保留1990-2030之间的值,并对范围文件名做人工确认。

def extract_year(fname): import re years = re.findall(r'(19\d{2}|20\d{2})', fname) if len(years) == 1: return int(years[0]) elif len(years) > 1: # 多个年份,返回范围或标记待确认 return f"{min(years)}-{max(years)}" return None

5. 进阶验证:用一套最小分析流程检验数据可用性

拿到合并后的数据,不要直接上复杂模型。先用一个最小分析流程验证数据质量:选一个区县,算三年建设用地面积变化,再和公开统计年鉴对一下量级。对不上,说明数据有问题;对得上,再往下做。

import geopandas as gpd import matplotlib.pyplot as plt # 假设merged是合并后的数据,含year和type字段 # 筛选建设用地(type里含"建设"或"城镇") construction = merged[merged['type'].str.contains('建设|城镇|用地', na=False)].copy() construction['area_km2'] = construction.geometry.area / 1e6 # 按年份和区县统计 stats = construction.groupby(['year', 'name'])['area_km2'].sum().reset_index() # 选一个区县看趋势 sample = stats[stats['name'] == stats['name'].iloc[0]] print(sample) # 可视化 fig, ax = plt.subplots(figsize=(8, 4)) for name, group in stats.groupby('name'): ax.plot(group['year'], group['area_km2'], marker='o', label=name) ax.set_xlabel('年份') ax.set_ylabel('建设用地面积(平方公里)') ax.legend() plt.tight_layout() plt.savefig('construction_trend.png', dpi=150)

这段代码做三件事:筛选建设用地、按年份和区县汇总面积、画趋势图。参数上,str.contains的正则根据你的type字段实际值调整,常见值有“建设用地”“城镇用地”“居民点”等。area / 1e6是把平方米转成平方公里,因为EPSG:4544的单位是米。

验证时重点看两点:一是趋势是否单调,建设用地面积通常逐年增加,如果某年突然下降,要么是数据缺失,要么是边界变了;二是量级是否合理,一个区县的建设用地面积一般在几十到几百平方公里,如果算出几千,大概率是坐标系单位错了(比如把度当米算)。

我自己的习惯是,每次拿到跨年矢量数据,先跑一遍这个最小流程,确认面积量级和趋势没问题,再投入时间做复杂分析。这个习惯帮我省过至少两次返工——有一次数据里混入了未投影的地理坐标,面积算出来只有实际的万分之一,趋势图直接是一条平线,一眼就能看出来。

注意:如果你的数据里没有type字段,可以用name字段做分组,或者直接统计所有要素的总面积。验证的核心是“量级合理、趋势可解释”,不一定要精确到具体类别。

最后说一个我踩过的坑:2020-2022年数据里,有些年份的行政区划名称用了简称,比如“高新”而不是“高新区”,“天府”而不是“天府新区”。合并时如果不做名称标准化,分组统计会多出好几个“区”。解决办法是建一个名称映射表,把简称统一成全称。这个表不用很复杂,手动列几十行就够了,但能避免后面所有分组分析出错。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询