☰
水质断面坐标数据处理:从清洗到坐标系转换的GIS实操指南
2026/10/3 18:34:26 网站建设 项目流程

简介:天津市地表水水质国控断面坐标数据收录全市23个国控断面的空间信息,面向环境科学、GIS应用与水资源管理研究人群,可用于水质监测点分布制图、污染来源追溯和长期变化趋势评估。压缩包共8个文件,包含shp图形文件、dbf属性表、prj投影信息、cpg编码文件以及sbn/sbx空间索引,完整支持ArcGIS、QGIS等主流GIS平台直接加载,整包仅5KB,轻量便捷。每个断面详细记录所属省市、流域、河流、断面名称及精确经纬度,数据可直观呈现点位分布规律,并能与水文、污染源等图层叠加分析,为区域水环境质量评估、断面优化布设及高校科研教学提供规范底图。国控断面由国家统一设定,数据口径权威、可比性强,便于开展跨区域横向对比。已有614人学习,适合需要快速获取天津市水质监测点位空间信息并开展空间分析的环境从业者与研究者。

1. 23个水质断面坐标:看着只是一张表,实际是一份需要认真对待的基础数据

“天津市地表水水质国控断面坐标数据 共23个水质断面”听起来像一份很小的Excel表——23行,两列经纬度,体积不到几KB。但从事水环境监测、环评报告或者污染溯源项目的人都知道,坐标数据是所有后续工作的地基:断面点位要上图、要匹配月度监测值、要按上下游顺序参与水质评价,任何一个小数点错位都会让结论全部返工。这23个坐标点不是“一次性参考文件”,而是一份需要长期维护、被多方复用的基础数据集。这篇笔记就围绕它讲清楚三件事:数据到手后如何清洗和空间化、坐标系如何统一、以及落地使用时的坑和自检方法。适合刚拿到坐标要出图做报告的工程师,也适合要把这批断面坐标接入数据平台的分析师。

2. 把国控断面坐标变成可复用数据资产:字段清洗与空间化

2.1 一张原始坐标表背后到底该有哪些字段

拿到一份坐标数据,第一步不是急着画图,而是先看表头。常见的原始坐标表长这样:断面名称、经度、纬度,凑合能看,但真要跟监测数据关联就会出问题。比如“海河三岔口”这种名称在不同年份的表格里可能写成“三岔口”“海河-三岔口”,单靠名称匹配很快就会翻车。我一般会在清洗阶段就补齐以下字段,形成一份标准的断面基础表:

字段名说明示例
section_code断面代码,唯一主键120001
section_name断面名称海河三岔口
river_name所在河流/湖库海河
longitude经度,十进制度117.200123
latitude纬度,十进制度39.120456
district所在行政区红桥区
control_level控制级别,用于区分国控/市控国控
monitor_freq监测频次每月

断面代码一定要保留。国控断面每年都会由国家监测网统一编号,用代码做主键,后续和监测值表做关联时才不会受名称写法变化影响。control_level这个字段很多人会忽略,但同一个坐标点在平台上可能同时被标记为“考核断面”“参考断面”,用途不同,保留它能让后续筛选省很多事。

2.2 用Python做一次干净的读取和清洗

拿到原始表以后,我一般会用脚本做一次彻底清洗,而不是直接在Excel里手工改。手工改的问题在于操作不可回溯,23个点少一个都不容易发现,更别说改了哪个字段。下面这份脚本覆盖了读取、列名标准化、空值检查和坐标范围校验,是一个可以直接抄的起点。

import pandas as pd # 读取原始Excel,dtype指定为str,避免断面代码被读成数字 df = pd.read_excel("raw_sections.xlsx", dtype=str) # 列名标准化,统一成英文小写字段名 df.columns = [c.strip().lower() for c in df.columns] df = df.rename(columns={ "断面编号": "section_code", "断面名称": "section_name", "河流": "river_name", "经度": "longitude", "纬度": "latitude", "所在区": "district", }) # 只保留关键字段,避免无关列干扰 keep_cols = ["section_code", "section_name", "river_name", "longitude", "latitude", "district"] df = df[keep_cols] # 将经纬度转为数值,非法值会变成NaN for col in ["longitude", "latitude"]: df[col] = pd.to_numeric(df[col], errors="coerce") # 坐标范围校验:天津市经度约116.7-118.1,纬度约38.5-40.3 df = df[df["longitude"].between(116.0, 119.0)] df = df[df["latitude"].between(38.0, 41.0)] # 断面代码去重,重复代码说明原始表有冗余 duplicated = df[df["section_code"].duplicated(keep=False)] if not duplicated.empty: print("发现重复断面代码:") print(duplicated) # 检查缺失值 null_report = df.isnull().sum() print("缺失值统计:") print(null_report) # 输出清洗后的CSV,编码utf-8-sig方便Excel打开 df.to_csv("sections_clean.csv", index=False, encoding="utf-8-sig")

这段脚本有一个关键设计:读取时用dtype=str先把所有列当文本读入,之后经纬度再手动转成数值。这样做的原因是断面代码如果直接按数值读,像“012001”这类编号会被截断成12001,等后面跟监测数据匹配时才发现对不上。经纬度用pd.to_numeric配合errors="coerce",非法值变成NaN后统一暴露出来,而不是让Excel悄悄把字符型坐标留在表里。坐标范围校验用的是天津市的经纬度粗边界,如果数据点落在范围外,大概率是经纬度写反了,或者混入了其他城市的点位。

在实际项目里,这段清洗脚本输出的是sections_clean.csv,它是一份“表结构干净、但还没有空间属性”的数据。下一步把经纬度转成真正的空间对象,才能做叠加分析、缓冲区、上下游排序这些后续操作。

2.3 空间化输出:GeoJSON和SHP怎么选

坐标表清洗完,接下来要回答一个实际选型问题:交付给下游的坐标文件用什么格式。常见做法是同时输出GeoJSON和Shapefile(SHP)两个版本,GeoJSON给Web系统和Python生态用,SHP给ArcGIS/QGIS这类桌面GIS用。需要提醒的是,SHP不是一个文件而是多个文件共同组成,至少包含.shp、.shx、.dbf三个基础文件,拷贝时少拿一个都会导致图层打开失败。

import geopandas as gpd from shapely.geometry import Point # 读回清洗后的CSV df = pd.read_csv("sections_clean.csv", encoding="utf-8-sig") # 由经纬度构造点几何对象 geometry = [Point(x, y) for x, y in zip(df["longitude"], df["latitude"])] # 创建GeoDataFrame,显式声明坐标系为WGS84经纬度坐标系 gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") # 输出GeoJSON,适合Web端和快速查看 gdf.to_file("sections.geojson", driver="GeoJSON") # 输出Shapefile,注意编码设置,避免中文属性乱码 gdf.to_file("sections_shp/sections.shp", driver="ESRI Shapefile", encoding="utf-8")

创建GeoDataFrame时crs="EPSG:4326"是一个必须做对的操作。EPSG:4326代表WGS84经纬度坐标系,也是绝大多数GPS设备输出的坐标框架。这里声明的是“原始坐标所在的空间参考系统”,不是“目标坐标系”——很多新手在这里误以为写了4326就完成了转换,其实它只是告诉软件这批经纬度是哪个框架下的。真正的坐标系转换在下文单独讲。

3. 坐标系是第一个坑:CGCS2000、WGS84与地图底图叠加的偏移处理

3.1 为什么23个点会在底图上整体漂移

几乎每个刚把断面坐标做成图层的人,都会遇到同一个诡异现象:点位单独看没问题,属性表也对,但叠加到在线卫星底图上,23个点整体往某个方向偏移了几十米甚至几百米。这不是数据错了,而是坐标系不一致。

要理清这个问题,得先分清三种坐标框架。WGS84是全球GPS定位用的地心坐标系,绝大多数便携式GPS输出的是它。CGCS2000是中国建立的国家大地坐标系,近十几年的测绘成果和国土业务数据大量采用它。第三个是Web地图底图使用的墨卡托投影坐标(EPSG:3857),谷歌、OpenStreetMap这类在线底图默认用它。前两者之间的差异通常很小,在我国范围内也就是米级;但经纬度数据和底图的投影坐标混用时,坐标会被当作投影平面坐标来解释,点位自然整体漂移。此外,国内商业地图平台还有自己的加密偏移算法,叠加这类底图时还要在地理坐标和加密坐标之间换算,具体数值不是恒定公式,这个后文会讲到。

判断坐标系是否匹配,有一个非常简单的肉眼测试:把点位图层叠加到在线底图上,看断面点是否落在河道水面上。国控断面绝大多数设置在河流、湖库上,点位不在水里而在岸上几十米,八成是坐标系问题。

3.2 用pyproj做坐标系转换的完整步骤

如果确认坐标文件写的是CGCS2000,但业务底图是WGS84,就需要做一次明确的坐标转换。Python生态里pyproj是标准工具,它封装了PROJ库,转换精度足够工程使用。

import pandas as pd from pyproj import Transformer # CGCS2000经纬度 -> WGS84经纬度 trans = Transformer.from_crs("EPSG:4490", "EPSG:4326", always_xy=True) df = pd.read_csv("sections_clean.csv", encoding="utf-8-sig") # 逐行转换,得到目标坐标系下的经纬度 lon_list, lat_list = [], [] for lon, lat in zip(df["longitude"], df["latitude"]): new_lon, new_lat = trans.transform(lon, lat) lon_list.append(new_lon) lat_list.append(new_lat) df["lon_wgs84"] = lon_list df["lat_wgs84"] = lat_list # 对比转换前后的差值,帮助判断原始坐标系 df["delta_lon"] = df["lon_wgs84"] - df["longitude"] df["delta_lat"] = df["lat_wgs84"] - df["latitude"] print(df[["section_name", "longitude", "lon_wgs84", "delta_lon", "delta_lat"]].head(10))

这里的always_xy=True很关键。pyproj的默认行为在某些版本中会按(纬度, 经度)顺序返回结果,加上always_xy=True强制统一成(经度, 纬度),避免坐标轴顺序不同导致点位横竖互换。EPSG:4490是CGCS2000的经纬度坐标系,EPSG:4326是WGS84经纬度坐标系,两者转换后差值会体现在小数后多位。如果你的数据本身声明是WGS84,但叠加后依然偏移,可能需要检查的是数据源是否实际上用了CGCS2000却标注为WGS84——这种情况我遇到过不止一次,所以在转换后一定要打印差值并和已知点位对比。

转换完成后,新生成的lon_wgs84和lat_wgs84字段才是真正能和其他WGS84数据一起用的经纬度。把这个结果另存为一个新文件,不要覆盖原始数据,因为后续如果发现原始表其实没有坐标系标注错误,原始数据还能补救。

3.3 叠加底图验证:用QGIS快速核对23个点位

坐标转换做没做对,最终要靠叠加底图验证。我常用的流程分三步:先在QGIS中加载在线底图,再加载转换后的GeoJSON图层,最后逐个点核对是否落在河道或湖泊上。这个流程适合23个点的核对量,不需要写代码。

具体操作参数:新建工程后,在浏览器面板里添加在线瓦片底图,比较稳定的选择是OpenStreetMap标准底图;然后把sections.geojson以矢量图层方式拖入工程;图层右键打开属性表,打开标注,用“断面名称”字段作为标签显示;最后以底图为参照,观察点位是否紧贴河道中心线或湖库边界。如果有点位明显悬空,单独选中它,对照属性表中的经纬度,在底图上用“测量工具”定位该坐标,确认是转换问题还是原始坐标错乱。

检查项一致时的表现不一致时的表现
底图投影方式点位与河道吻合所有点位整体偏移
坐标轴顺序点位方向正确、位置合理点位经纬度互换
单点坐标错误多数点正常、个别点离群离群点落入陆地区域

第4章会专门讲排查顺序,这里先把一个经验提出来:不要同时调整“坐标系”和“点位位置”,不要因为点位偏了就直接在图上拖动修改,23个点手工拖动过之后原始坐标就废了。

4. 坐标数据使用中的5个常见坑:现象、原因与排查顺序

4.1 点位在底图上整体偏移,错在坐标系

现象:23个点全部串在一起朝同一方向偏移,偏移量恒定,大致是几十米到几百米不等。

原因:最常见的是原始坐标系标注错误。很多野外采集人员把GPS设备里的坐标导出后,设备默认输出是WGS84,但数据表头却写成“CGCS2000坐标”;反过来,从国土部门拷出的数据是CGCS2000,表头却沿用旧模板写成“WGS84”。第二种常见原因是把经纬度直接当作Web墨卡托坐标显示在底图上。

解决:先拿一个已知位置的断面点核对,确定表头标注与实际坐标之间的偏差方向;再用pyproj按第3章的转换脚本转一次,看差值是否收敛。核心原则是只信任“已知点验证”,不要相信表头,因为它可能是复制旧文件后忘了改。

4.2 经纬度是度分秒还是小数度,肉眼难辨

现象:点位看起来有规律,但全部落在同一条直线上,或者坐标数值小数点后位数异常,像是“117°12’35”这种写法被直接当作小数度使用。

原因:Excel单元格里存的是“117.123456”这类小数度,但其实原始记录写的是“117度12分34.56秒”,被某个版本的软件自动转成了小数,或者根本没有转换。

解决:面对一份来历不明的坐标表,先用一个已知点的坐标做反算。比如天津海河某断面的实际位置大约在东经117.2、北纬39.1附近,如果表里出现“117123456”这种数字,大概率是度分秒被拼接成了字符串。处理方式是把度分秒先拆开重算成小数度,公式是度 + 分/60 + 秒/3600。不要试图用Excel的“分列”一步搞定,因为秒字段可能带小数点,手工处理最容易出错的地方就是这里。

4.3 断面上游下游顺序颠倒,水质评价直接翻车

现象:断面点位都画对了,但按“河流从上游到下游”排列时,顺序跟实际水流方向相反,导致污染变化趋势图看起来莫名其妙。国控断面评价有时要按干流和支流分级,顺序颠倒会直接得出错误结论。

原因:坐标表只给经纬度,没有给“相对河流位置的顺序号”。很多使用者直接用断面代码排序,但代码编号是行政区划或监测管理逻辑,跟水流方向没有必然关系。

解决:把断面图层叠加到水系图上,沿着河流中心线从上游往下游人工过一遍,给每个断面加一个stream_order字段。字段不要按1、2、3连续编号,而是用“河流名_S_01”“河流名_S_02”这类带河流前缀的编号,避免不同河流之间排序混乱。这个工作23个点大约需要二十分钟,但能保证后续水质趋势分析的基础逻辑不出问题。

4.4 断面数量对不上,多半是手工拷贝惹的祸

现象:标题写着23个断面,清洗完却只有22个,或者多出1个重复点。反复核对后发现Excel里某一行被隐藏了。

原因:原始数据从网页或报告导出时,经常混入不可见行;另一些情况是数据管理人手滑,复制粘贴时漏了一行却没注意到;还有可能是同一个断面在不同年份被录入两次,但经纬度略有不同。

解决:清洗脚本里务必加duplicated检查,并且要用断面代码做主键去重,不要用名称。如果发现只有一个重复,不要随手删除,先从原始表里找出两次录入的差异。有些监测项目对同一断面有“表层采样点”和“垂向采样点”之分,代码相同但坐标不同,这时需要和业务方确认是否保留两条记录,而不是直接删掉。

4.5 同一断面坐标前后版本不一致,要留变更记录

现象:去年拿到的坐标和今年拿到的不一致,偏移不大,但足以让叠加分析出差错。

原因:国控断面点位根据实际监测条件会有微调;此外,GPS设备精度差异、坐标系转换参数不同,都会让同一断面在不同批次数据里出现微小差别。

解决:建立一份“断面坐标变更记录表”,至少记录断面代码、旧坐标、新坐标、变更原因、变更日期、变更人。每次拿到新版本,先和旧版本做差,超过预设阈值时触发人工核实。阈值我一般设5米,因为厘米级差异往往是转换精度,米级以上通常意味着点位真实发生了变化。养成这个习惯后,就不会出现“别人拿新数据、你拿旧数据”导致的结论不一致。

5. 从坐标到业务:断面水质评价图与共享数据包制作

5.1 按水质类别渲染断面的简单思路

坐标数据的直接消费方是各类图件。最基础的需求是“把监测值挂到断面上,按水质类别着色”。这里给出一个不依赖GIS软件、用pandas和matplotlib就能完成的方案,适合快速生成报告插图。

import pandas as pd import matplotlib.pyplot as plt from matplotlib.patches import Patch # 读取带有监测结果和坐标的表 df = pd.read_csv("sections_with_result.csv", encoding="utf-8-sig") # 按水质类别映射颜色:I/II类蓝色,III类绿色,IV类黄色,V类橙色,劣V类红色 color_map = { "I类": "#1E90FF", "II类": "#1E90FF", "III类": "#2E8B57", "IV类": "#FFD700", "V类": "#FF8C00", "劣V类": "#DC143C", } df["color"] = df["water_quality"].map(color_map) fig, ax = plt.subplots(figsize=(10, 12)) # 绘制断面点,颜色代表水质类别,s调节点大小 ax.scatter(df["longitude"], df["latitude"], c=df["color"], s=120, alpha=0.85) # 给每个点标注断面名称,注意偏移量避免文字压点 for _, row in df.iterrows(): ax.annotate(row["section_name"], xy=(row["longitude"], row["latitude"]), xytext=(4, 4), textcoords="offset points", fontsize=9) # 图例和网格,网格线让经纬度参考更清晰 ax.set_xlabel("经度") ax.set_ylabel("纬度") ax.grid(True, linestyle="--", linewidth=0.5) legend_items = [Patch(facecolor=color_map[k], label=k) for k in color_map] ax.legend(handles=legend_items, loc="upper right") plt.title("2024年天津市国控断面水质类别分布") plt.tight_layout() plt.savefig("sections_water_quality.png", dpi=200)

graph代码里water_quality字段需要提前从监测结果表匹配到断面代码上。matplotlib默认的坐标轴纵横比会让图形在纬度跨度大的时候看起来不协调,如果点位分布南北方向距离较大,可以用ax.set_aspect(1/cos(39.1*pi/180))调整,这里39.1是天津的纬度参考值。现实中水质类别图一般还要叠加河流底图,避免点位悬空,但这一步在QGIS里更合适,静态脚本只适合快速出草图。

5.2 用逆地理编码核对断面地名

坐标数据有个隐蔽问题:经纬度正确,但属性表里的断面名称和真实地理位置对不上。比如“团泊洼水库”的坐标点上,叠加上去后落在了附近某条排干渠上。这类错误在人工录入坐标时时有发生,仅靠肉眼很难发现。

一个可靠的做法是用空间连接做逆地理编码,拿坐标去匹配行政区划或流域分区面图层,再和属性表里的地名比对。

import geopandas as gpd # 读取断面点和行政区划面图层 sections = gpd.read_file("sections.geojson") districts = gpd.read_file("tianjin_districts.geojson") # 确保两个图层坐标系一致,不一致时先转换 if sections.crs != districts.crs: districts = districts.to_crs(sections.crs) # 空间连接:给每个断面附加所在行政区名称 joined = gpd.sjoin(sections, districts, how="left", predicate="within") # 对比原始字段和空间匹配结果 joined["district_match"] = joined["district"] == joined["district_name"] mismatch = joined[joined["district_match"] == False] if not mismatch.empty: print("存在行政区不匹配的断面:") print(mismatch[["section_name", "district", "district_name"]]) else: print("全部断面行政区归属与坐标一致")

空间连接时的predicate="within"表示断面点必须完全落在区划面内部。天津有些断面临近区县边界,可能出现“原始记录属于A区、空间计算落到B区”的情况,这不一定是坐标错,而是断面恰好设在边界河道上。遇到这种mismatch不要直接改数据,人工确认断面实际归属后再更新一张标准的区县对照表。

5.3 交付给同事的数据包:一份README写清楚什么

数据文件本身容易复制,难复制的是它的背景信息。我见过太多次“拿到坐标文件,不知道坐标系、不知道字段含义、不知道更新频率”的情况。一个合格的断面坐标数据包应该包含以下文件:

文件名内容作用
sections_clean.csv清洗后的坐标与属性通用表格,Excel可直接打开
sections.geojson带坐标系的矢量数据供QGIS和Web系统使用
sections_shp/Shapefile三件套供ArcGIS等桌面GIS使用
coordinate_readme.md坐标系、字段说明、处理记录防止数据被误用
change_log.csv坐标变更历史追踪点位调整

README里最核心的是坐标系说明和字段字典。坐标系部分至少写清楚“本文件采用EPSG:4326(WGS84)经纬度坐标,由CGCS2000原始数据经pyproj转换而来,转换时间截至于X年X月X日”,字段字典则把每个字段的中文名、类型、取值说明列一遍。很多人觉得写README浪费时间,但这份文件会让下游使用者少问十次重复问题,是真正值得投入的时间。

6. 把断面坐标用扎实:一个自检脚本与长期习惯

坐标数据是一类“用的时候才发现有问题”的数据。为了不让自己在DDL前夕才发现坐标系错了,我后来养成了一个习惯:每次拿到或更新断面坐标,先跑一遍自检脚本,把基础校验自动化。

#!/bin/bash CSV="sections_clean.csv" echo "=== 1. 检查文件是否存在 ===" test -f "$CSV" || { echo "缺少文件 $CSV"; exit 1; } echo "=== 2. 检查记录行数 ===" COUNT=$(wc -l < "$CSV") echo "数据行数:$COUNT(含表头)" echo "=== 3. 检查经度范围 ===" awk -F',' 'NR>1 && ($4 < 116 || $4 > 119) {print "经度越界: "$0}' "$CSV" echo "=== 4. 检查纬度范围 ===" awk -F',' 'NR>1 && ($5 < 38 || $5 > 41) {print "纬度越界: "$0}' "$CSV" echo "=== 5. 检查缺失值 ===" awk -F',' 'NR>1 && ($4 == "" || $5 == "") {print "经纬度缺失: "$0}' "$CSV" echo "自检完成"

这段脚本假设CSV的列顺序是断面名称、断面代码、河流、经度、纬度等,第4列和第5列是经纬度。实际使用时字段顺序不同,要按自己清洗后的列结构调整。把这段脚本放到数据文件同目录下,每次更新完数据就跑一遍,20秒内能排除大部分低级错误。

这套流程走下来的核心收获是:把坐标数据当成“需要持续维护的工程资产”,而不是“一次性附件”。坐标系、断面代码、上下游顺序、变更记录,每一环都要有据可查。我早期做水环境分析时吃过一次亏——用一份没标注坐标系的表格直接上图,点位整体偏移了约百米却没发现,等报告提交后才被下游单位指出来,那次的返工教训至今记得。从那以后,拿到任何坐标数据都先做坐标框架验证,再谈业务分析,这个顺序再也没有乱过。希望帮到你。

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

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

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

立即咨询