☰
全国POI数据2024处理实战:清洗、去重与坐标转换
2026/10/3 4:42:33 网站建设 项目流程

简介:全国POI兴趣点数据(2024年)是一份基于OSM地图获取的矢量数据集,覆盖全国及各省份,坐标系为GCS_WGS_1984,包含Fclass、Code、Name等属性,可支持POI分类统计、城市分析、商业选址与地图制图等常见GIS场景。资源包共178个文件,主体为35个省市级shp及其配套shx、dbf、cpg、prj文件,分别存储几何、属性、编码与投影信息,另含全国汇总的xml、sbn、sbx,压缩后24.91MB。数据按省份拆分,也提供全国范围数据,使用者可直接按行政区划读取,或将字段中的118种POI类别用于筛选、统计与可视化。目前已有624人学习下载,适合GIS、城乡规划、交通出行等研究者和学生作为基础底图,免去自行爬取与清洗OSM数据的麻烦。

1. 全国POI兴趣点数据2024年:它不是一份普通表格

全国POI兴趣点数据2024年这份数据集,在圈内常被叫做兴趣点库或点位底图。它的核心价值很直接:当你要做商业选址、LBS应用、城市网格分析或竞品点位对比时,不需要再自己爬地图接口反查坐标,直接把一份全国范围、带经纬度与多级分类的表格作为底图,省掉的是几周的数据采集和清洗时间。这份资源适合三类人:做GIS开发的人拿它建检索服务,做商业分析的人拿它做密度统计,做城市规划的人拿它验证空间分布。数据本身是静态的,但它能不能用、怎么用,完全取决于你拿到手之后有没有做对三件事:先查坐标系,再按城市做覆盖度统计,最后跑一遍去重。这三件事没做,轻则热力图失真,重则整份数据的分析结论都要推翻。

2. 字段与分类体系:先搞懂POI数据集的规格再动手

2.1 核心字段:七个属性先对上

全国POI数据集的字段命名各家不一样,但核心属性就那几样。我拿到手先做的不是列清单,而是把列名统一成一套自己的schema。下表是我在实战里固定使用的字段对照,后面所有脚本都依赖这套命名。

统一字段原始常见写法含义典型取值
name名称/店名/poi名称点位名称杭州东站
address地址/详细地址地址文本杭州市江干区…
province/city/district省/市/区、省市区行政区划三级归属浙江省/杭州市/上城区
longitude/latitudelng/lat、lon/lat、经度/纬度点位坐标120.212, 30.249
category分类/行业分类/行业多级分类编码餐饮-中式-杭帮菜
tel电话/联系方式联系电话0571-88…
update_time更新时间/数据时间点位更新时间2024-03-12

这份对照表里最容易踩坑的是经纬度列名。有的数据源用lng,有的用lon,还有的直接用中文字段,直接拿原始列名跑脚本会报KeyError。我一般会在读入数据之后立刻做一个rename,把所有可能的写法收拢到longitude和latitude两个固定字段上,顺便把全角空格、BOM头清掉。

import pandas as pd df = pd.read_csv("poi_2024.csv", encoding="utf-8-sig") # encoding 用 utf-8-sig,防止 CSV 带 BOM 头导致第一列列名出现乱码 col_map = { "名称": "name", "店名": "name", "poi名称": "name", "经度": "longitude", "lng": "longitude", "lon": "longitude", "纬度": "latitude", "lat": "latitude", "省": "province", "省份": "province", "市": "city", "城市": "city", "区": "district", "区县": "district", "分类": "category", "行业分类": "category", "电话": "tel", "联系方式": "tel", } df = df.rename(columns=col_map) need = ["name", "longitude", "latitude", "province", "city", "district", "category"] print(df[need].head()) print("字段缺失率:\n", df[need].isna().mean().round(4))

这段代码有两个关键点。utf-8-sig编码是为了处理带BOM的CSV,否则第一列列名会变成\ufeffname,后续字段对拍永远对不上。col_map里我收拢了所有可能的同义列名,统一成固定字段,做完这一步再看缺失率。注意缺失率不是越低越好,tel这类字段在POI数据里常年缺失率都在20%以上,这是正常的;但name、longitude、latitude、city这四个字段如果缺失率超过3%,这份数据就需要回头查生产方的下发逻辑。

除此之外还有一个字段容易被忽略:update_time。全国数据往往是多期合并出来的,有的行更新时间是2023年,有的是2024年,有的是时间戳格式,有的是ISO字符串,还有的带时区偏移。我一般会新加一列update_time_norm统一转成ISO 8601格式,原始列保留不动,后面做数据时效性分析时只信归一化后的列。原始列保留是一种工作习惯:数据处理过程里原始字段永远留底,方便任何时候回溯。

2.2 分类体系:多级分类对齐是分析的前提

分类字段是POI数据集里信息密度最高的字段,也是被用错最多的字段。国内流通的POI数据分类体系主要有两类:一类是互联网地图厂商的分类法,一级分类通常有餐饮、购物、娱乐、交通、医疗、教育、生活服务、金融、住宿等十几个大类,二级和三级在下面继续细分;另一类是参照国民经济行业分类做的口径,字段值看起来更像标准代码。拿到一份新数据时,我不会先假设它属于哪种分类法,而是先用value_counts看一级分类的实际取值。

# 先看一级分类到底有哪些取值,再决定怎么归并 df["cat1"] = df["category"].str.split("-").str[0] top = df["cat1"].value_counts().head(30) print(top)

如果打印结果里同时出现“美食”“餐饮服务”“餐厅”三个名字,说明这份数据的生产方合并过来源,分类口径没有统一。这种时候就要做一张映射表,把这些同义分类收拢成分析用的一级分类。举例:

原始分类名分析用一级分类归并条件
美食/餐厅/餐饮服务餐饮名称含关键词即可
购物中心/商场/百货购物名称含关键词即可
地铁站/公交站/火车站交通设施名称含关键词或分类编码前缀匹配

映射表的维护有一个原则:宁可映射得粗,不要映射得细。分析用一级分类设在10到15个左右足够支撑大多数选址和密度分析,强行保留全部三级分类反而会让统计结果碎成一片。我常用的做法是保留统一的分类层级字段,原始分类字段保留不覆盖,分析时只用统一字段。

三级分类在实战里有一个很常见的坑:空值率偏高。小城市的数据生产方往往只维护到二级分类,三级分类空值率可能超过50%。做行业分析前必须先统计空值率,超过10%就统一用二级分类兜底,否则统计结果会出现大片「未分类」。

2.3 覆盖范围与量级:先看行政区划清单

数据集叫全国,不代表每个省、市、区县都完整。覆盖度问题不去查,后面所有按城市维度的统计都会失真。我拿到数据做汇总性检查的步骤是这样的。

第一步,从数据里提取省、市、区县三级的唯一组合,统计每个组合下的POI数量。第二步,找一份标准的行政区划代码表做对拍,国内常用历年发布的省/市/县代码,也可以直接拿维护好的区划清单,只要字段里有省市区名称和对应的行政代码就行。第三步看差异:标准表里有但数据里完全没有的区县,就是要重点补查的盲区。

# 按省市两级统计数量,倒序看分布 summary = df.groupby(["province", "city"]).size().reset_index(name="cnt") summary = summary.sort_values("cnt", ascending=False) print(summary.head(30)) print("城市数量:", summary["city"].nunique())

看这个结果时,我的节奏是先扫头部再扫尾部。头部城市如果出现某个地级市的数量比省会还多,先别高兴,很可能那里有重复采集或口径外的数据;尾部城市如果出现零覆盖,就要结合标准区划清单确认是不是真实存在的地级市。正常的数据分布曲线应该是头部长尾平滑,某个城市突然跳高或跳低都是需要回去查的信号。

这里还涉及一个有实用价值的字段:adcode,也就是行政区划代码。很多POI数据集会在点位里带上adcode字段,如果没有,可以用省市区名称去匹配区划清单回填。回填后的adcode可以作为城市连接键,把POI数据和其他行业数据、人口数据、边界数据join在一起。我在做城市分析时几乎天天用这个字段,没有adcode,关联外部数据的成本会高很多。

3. 质量校验三道关:坐标合法性、覆盖度与去重

3.1 坐标合法性校验:范围过滤是第一道闸门

拿到POI数据集,第一道闸门永远是坐标合法性过滤。中国陆地范围的经纬度平常区间大约在经度73到135、纬度18到54之间,这只是个粗范围,但作为数据清洗的边界条件已经足够。超过这个范围的坐标点,几乎可以断定是脏数据。

我见过最离谱的情况是坐标写成(0,0)。很多数据生产方的兜底逻辑是把缺失坐标填成0,如果不做0值过滤,整批点会被画到地图左下角,热力图直接废掉。还有一种是坐标单位被当成度分秒解析,出来的经度变成120.302515这种看起来很像经纬度的数,但实际上是错的。范围过滤虽然不能识别所有这类问题,但能先把明显越界的点清出去。

import pandas as pd df = pd.read_csv("poi_2024_unified.csv") def coord_filter(df): lon_ok = (df["longitude"] > 73.0) & (df["longitude"] < 135.0) lat_ok = (df["latitude"] > 18.0) & (df["latitude"] < 54.0) not_zero = (df["longitude"] != 0.0) & (df["latitude"] != 0.0) return df[lon_ok & lat_ok & not_zero].copy() df_clean = coord_filter(df) print("过滤前:", len(df), "过滤后:", len(df_clean)) print("过滤比例:", round((len(df) - len(df_clean)) / len(df), 4))

过滤比例是一个非常重要的质量信号。正常的全国POI数据,坐标异常比例应该在千分之一以下。如果过滤比例到了百分之一以上,说明数据生产的坐标字段本身就不可靠,这时候要做的不只是过滤,而是回头确认数据源坐标系是否统一。第三个过滤条件很多人会漏写:有些数据把经纬度留成字符串"nan"或者"NULL",pandas读进来后变成NaN,NaN和0在比较运算里的表现不一样,所以我会在过滤前先做一次to_numeric转换。

df["longitude"] = pd.to_numeric(df["longitude"], errors="coerce") df["latitude"] = pd.to_numeric(df["latitude"], errors="coerce") df = df.dropna(subset=["longitude", "latitude"])

加上这行之后,所有非数字的经纬度都会变成NaN,再配合dropna,坐标字段才真正算清洗干净。这一步做完再看过滤比例,得到的结果才可信。

3.2 覆盖度评估:按城市与区县看数量分布

坐标过滤完,第二关是覆盖度评估。这一步的目的不是做精细的测绘,而是用一个比较粗的统计量判断这份数据集「哪些城市能信、哪些城市不能信」。

做法很简单:按province和city分组,统计每个城市的POI数量,从高到低排序,然后看分布曲线。正常的数据应该是头部城市数量大、地级市数量递减、尾部城市数量少但不断档。如果出现某个地级市数量异常高,基本可以判定是重复采集或者分类口径异常;如果省会数量排名远低于地级市,说明这个城市的数据来源有问题。

覆盖度评估还有一个进阶校验:行政区划字段和坐标的一致性。点位文字上写着「北京市朝阳区」,但坐标落在河北省境内,这种错位在POI数据里很常见,原因是生产时行政区划字段来自文本解析,坐标来自GPS定位,两者来自不同的处理链路。简单做法是用省市县边界数据做点在多边形内的判断,拿边界的GeoJSON和点位坐标做空间关联,判断每个点是否落在自己声称的行政区内。国内有现成的省市县边界包,从公开的GeoAtlas渠道下载GeoJSON格式边界,导入到GeoPandas里做sjoin即可。

import geopandas as gpd # 加载边界数据,字段里要包含 adcode 或省市名称 boundary = gpd.read_file("city_boundary.geojson") gdf = gpd.GeoDataFrame( df_clean, geometry=gpd.points_from_xy(df_clean["longitude"], df_clean["latitude"]), crs="EPSG:4326", ) joined = gpd.sjoin(gdf, boundary, how="left", predicate="within") mismatch = joined[joined["city"] != joined["city_name"]] print("行政归属不一致的比例:", round(len(mismatch) / len(joined), 4))

sjoin的predicate参数有三个常见选项:within表示点完全落在多边形内,intersects表示边界相交,contains需要反过来写。做点归属判断用within就够了。这个校验输出很有用:不一致比例高的城市,我会在后续分析里单独打标记,或者直接用坐标重新反查行政区划并覆盖原始字段。行政区划字段是所有后续join操作的基础,它错了后面全错,这个环节值得花时间排查。

3.3 去重策略:名称归一化与Geohash网格聚合

第三关是去重。POI数据的重复没有想象的那么直白,「一模一样完全重复」只是最少见的一种。实际生产环境里更多是这三种形态:同一点位被多期数据采集重复写入,字段完全相同只是主键不同;同一品牌连锁店的多个分店,名字完全相同但坐标不同,距离可能在几百米到几十公里;同一栋楼里多个商户,坐标几乎重合但名称完全不同。

针对这三种形态,我一般分三层去重。第一层是全字段精确去重,拿name、longitude、latitude、category四个字段做完全匹配,重复的只保留一条。第二层是Geohash网格聚合去重,把坐标编码成网格字符串,同一个网格里名称归一化后相同的点位只保留一条。第三层是业务规则去重,综合名称、地址、电话三个字段,配合评分保留信息最全的那条。

import geohash2 import pandas as pd def norm_name(s): return str(s).replace(" ", "").replace(" ", "").lower() # 名称归一化:去掉空格和全角空格,统一小写 df["name_norm"] = df["name"].map(norm_name) # Geohash 编码:precision=6 时网格约 1.2km x 0.6km df["gh6"] = df.apply( lambda r: geohash2.encode(r["latitude"], r["longitude"], precision=6), axis=1, ) # 先做全字段精确去重 df = df.drop_duplicates(subset=["name_norm", "longitude", "latitude", "category"]) # 再做网格 + 名称去重,保留电话非空且更新时间最新的那条 df["sort_key"] = df["tel"].notna().astype(int) * 100 + pd.to_datetime( df["update_time_norm"], errors="coerce" ).astype("int64") // 10**9 df = df.sort_values("sort_key", ascending=False) df = df.drop_duplicates(subset=["gh6", "name_norm"]) print("去重后点位数量:", len(df))

Geohash的精度选择是个关键参数。precision=6的网格在纬度35度附近大约对应1.2km×0.6km,适合做全国级别的同名点位聚合;precision=7对应约150m级别的网格,适合识别同一建筑内的多个POI。做全国数据时我通常先用6位跑一遍,把跨网格的同名连锁店保留下来——同一个名字出现在相隔几十公里的两个网格里,大概率是不同分店,不能删。

提示:全国POI数据集的重复率通常在5%到15%之间,如果去重比例超过20%,说明生产方合并了多个来源但没做清洗,这份数据的前期处理成本会比预期高很多。

去重不是越狠越好,过度去重会把同一商圈里真正的多商户误删。我在实际项目里会把去重后的数据再抽样一轮人工核对,抽样比例不必高,千分之一就够,目的是建立对去重规则的信心。这套三层去重跑完,数据集的点位数会明显下降,但剩下每一条都是能放心用的。

4. 坐标转换实战:GCJ-02偏移修正与全量批处理

4.1 坐标系判断:先取样本点验证偏移方向

坐标系判断是整个POI数据处理里最容易翻车的环节,甚至可以说是门玄学。为什么这么说?因为从数据文件本身经常看不出坐标系是哪一种,字段名不会标注,文档也不一定写,只能靠样本点去验证。

国内流通的POI数据坐标系主要三种,行为差异很典型。WGS-84是GPS原始坐标,坐标值基本符合测绘学里的标准经纬度定义;GCJ-02在国内地图厂商的底图上通行,和WGS-84的差值通常在几十米到几百米,方向不是固定的,而是随地理位置变化;BD-09是百度体系专用,它在GCJ-02之上再做了一次非线性偏移。字段名区分不了这三种坐标系,得靠采样对拍。

我的做法是:随机取50个样本点,把它们叠加在国内地图底图上,和道路位置做对比。如果点位大致落在道路中心线上,大概率是WGS-84;如果点位平行偏出道路几十米,而且偏移方向在同一座城市内基本一致,大概率是GCJ-02;如果点位直接偏到街区外的建筑群里,要考虑BD-09的可能。这个判断我用一张表来归纳:

坐标系典型使用方与道路的位置关系叠加国内地图的表现
WGS-84GPS、国际数据包贴合道路中心线偏移小于20米
GCJ-02高德、腾讯等整体偏移数十米平行偏移几十米
BD-09百度生态偏移更大且非线性视觉上偏离更明显

实际判断不只看一个城市的样本,我会选三个不同纬度的城市各取20个样本,因为在不同经纬度下GCJ-02的偏移方向和大小都不一样,多看几个城市能避免误判。这里有个重要的选择依据:如果数据文件的使用场景是国际地图产品,WGS-84是首选;如果是要叠加国内地图厂商的底图或在国内LBS系统里使用,最稳的选择是统一转成GCJ-02。

4.2 坐标转换:WGS-84与GCJ-02互转的公开算法

坐标系定下来之后,转换算法是有公开方案的,不需要自己搞逆向工程。WGS-84与GCJ-02之间的互转是目前国内开发者用得最广的一组算法,核心是GCJ-02相对于WGS-84的偏移量计算。拿到WGS-84坐标要转成GCJ-02,先把偏移量算出来再加上去;要从GCJ-02转回WGS-84,一般用迭代逼近,因为GCJ-02到WGS-84没有严格的解析逆函数。

import math pi = 3.1415926535897932384626 a = 6378245.0 # 椭球长半轴,单位米 ee = 0.00669342162296594323 # 椭球偏心率平方 def _transform_lat(lng, lat): ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat ret += 0.1 * lng * lat + 0.2 * math.sqrt(abs(lng)) ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * math.sin(2.0 * lng * pi)) * 2.0 / 3.0 ret += (20.0 * math.sin(lat * pi) + 40.0 * math.sin(lat / 3.0 * pi)) * 2.0 / 3.0 ret += (160.0 * math.sin(lat / 12.0 * pi) + 320.0 * math.sin(lat * pi / 30.0)) * 2.0 / 3.0 return ret def _transform_lng(lng, lat): ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng ret += 0.1 * lng * lat + 0.1 * math.sqrt(abs(lng)) ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * math.sin(2.0 * lng * pi)) * 2.0 / 3.0 ret += (20.0 * math.sin(lng * pi) + 40.0 * math.sin(lng / 3.0 * pi)) * 2.0 / 3.0 ret += (150.0 * math.sin(lng / 12.0 * pi) + 300.0 * math.sin(lng / 30.0 * pi)) * 2.0 / 3.0 return ret def wgs84_to_gcj02(lng, lat): d_lat = _transform_lat(lng - 105.0, lat - 35.0) d_lng = _transform_lng(lng - 105.0, lat - 35.0) rad_lat = lat / 180.0 * pi magic = math.sin(rad_lat) magic = 1 - ee * magic * magic sqrt_magic = math.sqrt(magic) d_lat = (d_lat * 180.0) / ((a * (1 - ee)) / (magic * sqrt_magic) * pi) d_lng = (d_lng * 180.0) / (a / sqrt_magic * math.cos(rad_lat) * pi) return lng + d_lng, lat + d_lat def gcj02_to_wgs84(lng, lat): # 迭代逼近:先算出正转换的偏移量,再用差值反向修正 _lng, _lat = lng, lat for _ in range(10): g_lng, g_lat = wgs84_to_gcj02(_lng, _lat) _lng -= g_lng - lng _lat -= g_lat - lat return _lng, _lat

这段代码里需要重点解释两个参数。a=6378245.0是克拉索夫斯基椭球的长半轴,ee是偏心率平方,这两个值来自国内地图坐标体系常用的椭球参数,直接固定使用即可。_transform_lat和_transform_lng里的常数项是GCJ-02偏移量拟合公式的系数,它们是公开算法约定俗成的组成部分,整个公式的输入是(lng - 105.0, lat - 35.0),105和35是算法的基准点经度和纬度,意思是偏移量以这个中心基准来拟合。

迭代逼近的道理很简单:先假设一个坐标,用正转换算出它在GCJ-02下的位置,再用和真实目标位置的差值去修正假设值,重复10轮后误差会收敛到米级。注意这段代码没有包含范围判断,国内常见算法会在开头判断坐标是否在范围外、不在范围内直接返回原坐标,我在批量处理时会在前一章的范围过滤阶段就把越界点清理掉,所以这里没有保留这个判断。

4.3 全量批处理:分块转换与异常标记

坐标转换是纯计算操作,全国数据量级动辄千万条,直接pandas apply也能跑,但内存占用和耗时都不划算。我的常规做法是分块读入、分块转换、分块写出,每块10万行起,内存占用稳定可控。

import pandas as pd chunk_size = 100_000 src_path = "poi_2024_cleaned.csv" out_path = "poi_2024_gcj02.csv" first_chunk = True for chunk in pd.read_csv(src_path, chunksize=chunk_size): chunk["longitude"] = pd.to_numeric(chunk["longitude"], errors="coerce") chunk["latitude"] = pd.to_numeric(chunk["latitude"], errors="coerce") chunk = chunk.dropna(subset=["longitude", "latitude"]) # 逐行转换,数据量大时可改为 numpy 向量化实现 lng_list, lat_list = [], [] for _, row in chunk.iterrows(): lng, lat = wgs84_to_gcj02(row["longitude"], row["latitude"]) lng_list.append(lng) lat_list.append(lat) chunk["longitude"] = lng_list chunk["latitude"] = lat_list chunk.to_csv(out_path, mode="a", header=first_chunk, index=False) first_chunk = False print("已处理行数:", chunk.shape[0])

分块大小的选择要看机器内存。16G内存的机器处理10万行一块没问题,字段多时可以降到5万行。iterrows在这种规模下确实不算快,但胜在逻辑直观,生产环境里我会先把坐标列转成numpy数组,用向量化方式一次算完,速度能提升一个数量级。不管用哪种方式,转换后都要做一次抽样验证,把转换前后的坐标差值打印出来看看是否符合预期量级。

整个批处理流程里我坚持一个原则:原始坐标列不覆盖,新增一列gcj_lng和gcj_lat。这样如果后续发现坐标系判断错了,还能从原始列重新转,不用回去找原始数据。这是整个流程的后悔药,强烈建议保留。

5. 避坑指南:POI使用中最常见的五个翻车现场

5.1 坐标类故障:越界点与二次偏移

翻车现场一,坐标出现明显越界的点。

现象:地图上所有点位挤到一个角落,或者坐标值出现(0,0)、经度大于180、纬度大于90这类物理上不可能的值。

原因:数据生产环节的兜底逻辑把缺失坐标写成(0,0),或者坐标列里混入了度分秒字符串,读入后类型转换出错。还有一种情况是坐标单位被当成投影坐标值使用,投影坐标直接当经纬度存进表里,数值看起来就是几百万的规模。

解决:范围过滤和数值类型强制转换缺一不可。先用pd.to_numeric把坐标列转成数值,再按经度73到135、纬度18到54的范围过滤,最后单独扔掉0值。这套逻辑在第三章节已经给出代码,我每次处理新数据都会强制跑一遍。

翻车现场二,坐标转换方向搞反,越转越偏。

现象:因为怀疑数据是GCJ-02,做了一次转WGS-84的操作,结果叠加路网后偏移量从原来的50米扩大到300米。

原因:原始数据本身就是GCJ-02,却把它当成WGS-84做了一次正转换,等于在GCJ-02之上又叠加了一层偏移。这类错误最隐蔽的地方在于,结果看起来只是「偏了」,不会报错,也没有异常值,很难被发现。

解决:转换之前一定先做4.1节的样本坐标对拍,确认原始坐标系再动手。另一个办法是算转换前后的位移量:GCJ-02到WGS-84的转换位移一般在几十米到几百米,如果单点位移超过1公里,基本可以断定转换方向错了。

5.2 数据质量类故障:重复与分类空值

翻车现场三,重复数据导致热点分析出现假热点。

现象:某个商圈的POI数量比实际多出30%,画出的热力图在商圈中心形成一个亮得发白的假热点。

原因:全国数据是多个来源合并的,同一个点位在A来源和B来源里各存了一份,坐标相近但字段不完全相同,简单的全字段去重无法覆盖。多期数据叠加也会造成同样的问题。

解决:分两层去重。先做「名称归一化+坐标+分类」的精确去重,再做Geohash 6位网格内的同名聚合去重。网格大小直接影响去重强度,如果发现删少了,把precision降到5;如果发现误删了同商圈不同商户,升到7。这个调节过程是纯经验活,没有固定最优值,需要根据数据特点反复试。

翻车现场四,三级分类空值率高得离谱,统计口径直接错。

现象:按三级分类统计时,某些城市的全部记录都落在「未分类」里,统计结果没法用。

原因:分类字段由人工标注,小城市数据往往只维护到二级分类,三级分类在生产环节就是空的。

解决:统计空值率后分层兜底。空值率低于10%,直接丢弃未分类样本;空值率在10%到50%,统一用二级分类替代;空值率超过50%,就应该仔细看看是不是整批数据的分类体系都不一样。这种问题不是清洗能解决的,可能需要重新映射整个分类字段。

5.3 业务使用类故障:行政区划字段与坐标错位

翻车现场五,城市字段和坐标归属不一致。

现象:统计北京市POI数量时,里面混入了不少坐标实际落在河北省廊坊境内的点位。

原因:行政区划字段来自地址文本解析,坐标来自GPS定位,两条处理链路在数据生产方就没有对齐。地址里写着北京,实际使用者可能已经搬到了环京区域。

解决:以坐标为基准重新做归属判断。用3.2节的geopandas sjoin,把点位和省市县边界做空间关联,坐标落在哪个区县就以哪个区县为准重写行政区划字段。做城市粒度统计之前,这个步骤是必须做的。我有一次就是因为没做归属校验,给客户的选址报告里把廊坊的几个商圈全算进了北京,最后只能连夜重跑数据。

6. 从CSV到PostGIS:入库与密度网格验证一条线

6.1 PostGIS入库与空间索引

数据清洗和坐标转换做完,POI数据集已经可以用了,但CSV文件在全量查询时不太方便。我会在最后一步把它导入PostGIS,建空间索引,把后续的检索和统计全部放到数据库里做。这也是为什么清洗阶段要保证schema统一——入库建表时字段一一对应,省得来回改。

CREATE TABLE poi_2024 ( id bigserial PRIMARY KEY, name text, province text, city text, district text, category text, longitude numeric(10,6), latitude numeric(10,6), geom geometry(Point, 4326) ); COPY poi_2024(name, province, city, district, category, longitude, latitude) FROM '/path/poi_2024_gcj02.csv' WITH (FORMAT csv, HEADER true); UPDATE poi_2024 SET geom = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326); CREATE INDEX idx_poi_geom ON poi_2024 USING GIST(geom);

这里有一个空间参考的细节要说清楚:GCJ-02坐标在PostGIS里不存在对应的SRID,所以入库时仍然以4326存储,业务使用时必须记住这张表的坐标标准是GCJ-02,叠加国内底图时不需要额外偏移,但一旦和其他WGS-84数据做空间运算,要先做转换。PostGIS本身不感知这个差异,这个标记责任落在开发者身上。

6.2 密度网格验证:用网格统计检验数据质量

入库之后做一次网格密度统计,既是质量验证也是后续分析的起点。把全国点位按0.005度的网格聚合,统计每个网格的点数,排名靠前的网格应该集中在一线、新一线城市的主城区,如果出现偏远地区高密度网格,多半又是哪道关没把住。

SELECT ST_X(ST_SnapToGrid(geom, 0.005)) AS grid_lng, ST_Y(ST_SnapToGrid(geom, 0.005)) AS grid_lat, count(*) AS cnt FROM poi_2024 GROUP BY ST_SnapToGrid(geom, 0.005) ORDER BY cnt DESC LIMIT 50;

0.005度约等于500米,这是密度分析里常用的网格尺度。前50个网格的热度排序如果在直觉上说得通,这份POI数据集就可以进入业务分析流程了。我在实际项目里会在这一步之后把前几个城市的网格统计结果导出成CSV,交给出图同事直接画热力底图。

最后说一个我的习惯。以前拿POI数据上来就画热力图,结果一个商圈因为重复数据被渲染成了一个巨型火球,后来查出是去重环节没跑透。从那以后,我每次拿到POI数据集,都强制先跑一遍「范围过滤、覆盖度统计、去重、坐标系确认」这四步,再谈分析和建模。这套流程的顺序我从来没有变过,变一次,就会付出重跑全量数据的代价。希望帮到你。

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

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

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

立即咨询