news 2026/10/13 13:37:08

USGS地震数据抓取与可视化:从GeoJSON到Plotly的完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
USGS地震数据抓取与可视化:从GeoJSON到Plotly的完整实战

简介:USGS地震数据获取与预处理工具包,面向地理信息或数据科学初学者,解决从USGS平台批量下载地震JSON并将其转换为结构化CSV进行后续分析的问题。压缩包共4个文件,包括Jupyter Notebook转换脚本HW2.ipynb、精简后的地震明细CSV、原始JSON数据以及README说明文档,整体体积仅1.44MB,便于快速部署本地环境。目前已有650人学习下载,适合需要掌握公开地震数据接口调用、JSON解析与表格化整理的入门者。通过该包可完整了解使用wget命令行抓取USGS数据、借助JSONView检查返回字段,以及利用Notebook执行数据清洗与降维存储的流程;配套的CSV文件还可用作后续可视化或统计建模的直接输入,节省从零检索API文档和调试格式的时间。

1. 全球地震目录五分钟一更新:USGS_earthquake 到底是干什么用的

如果只盯着标题里的 USGS 三个字母,很容易以为这是一个研究板块运动的学术项目;实际上,USGS_earthquake 更像是一个长期可用的地震数据总线。美国地质调查局的公开地震接口每 5 分钟同步一次全球台网捕捉到的震动事件,任何人在不申请密钥的情况下,就能拿到近实时目录、历史事件、震级修正记录和基础波形元数据。这个"项目"真正解决的问题,不是帮你预测地震,而是把散落在多个专用接口里的地震事件,变成你本地数据管道里一行行结构化的记录。

很多人第一次拉数据时会反直觉地发现两件事:第一,USGS 目录里大量事件是负震级或 0 级以下的微震,直接画图会得到一片密密麻麻的噪点;第二,每条震例的时间字段实际上是一个 UTC 毫秒时间戳,附带的时区偏移量常常为空,新手用本地时间直接解析,整张时间轴都会错位。USGS_earthquake 的价值就在这些细节里:选对接口、理解字段语义、做对过滤和可视化。这篇文章按我自己的落地路径来写:先讲数据源选型,再给可复现的抓取和绘图脚本,最后列出反复踩过的坑,以及一个用古登堡-里克特定律校准数据质量的方法。适合的读者不是地质学专家,而是要做监测大屏、灾害数据归档、地理分析或者单纯想用真实公开数据练手的数据工程师。

2. 先分清楚数据源:GeoJSON Feed、FDSN 事件服务与 CSV 出口怎么选

USGS 对外提供了不止一个地震数据入口,新手最容易犯的错误是拿到一个 URL 就开始写代码,跑通之后就把它固化到生产环境里,从不质疑这个接口到底覆盖什么范围、稳不稳定。我这边的做法是,先做一次数据源选型,把接口的实时性、回溯范围、字段完整度列成表,再按业务场景决定用哪个。常见的选择有三个:实时 GeoJSON Feed、FDSN 事件服务、以及带 format=csv 的快捷导出口。三者都能返回震例列表,但覆盖范围和使用限制差异非常大。

2.1 三个常用端点的实时性与覆盖范围

GeoJSON Feed 是大多数人最先接触的入口,地址在 earthquake.usgs.gov/earthquakes/feed/v1.0/summary 下,按时间窗口预生成了 all_hour、all_day、all_week、all_month 几个汇总文件,格式固定为 GeoJSON。它的定位是给网页和轻量应用做实时展示,数据来自自动定位系统,通常在震动发生后几分钟内就会出现在 Feed 里,更新频率大概每 5 分钟一次。好处是无需参数、无需分页、一个 URL 直接拉全量;坏处是它只保留最近 30 天左右的数据,并且所有事件都没有经过人工复核,震级、位置随时可能被后续更新覆盖。

FDSN 事件服务则是另一套更正式的查询接口,路径在 /fdsnws/event/1/query,遵循国际数字地震台网联合会的通用规范,支持通过 starttime、minmagnitude、经纬度范围等参数做针对性查询,也能回溯到几十年前的历史目录。研究级场景里,USGS 最终发布的事件目录以它为准。这个接口返回的震例里,每条事件还可能附带后续修订出来的多版本震级记录,适合做震级的时序分析。它的代价是语法更复杂,且对单次查询返回的记录数有限制,不能像 Feed 一样一把梭拉全量。

第三个 CSV 快捷出口本质上就是 FDSN 事件服务的一种输出格式,URL 上指定 format=csv 就能直接拿到逗号分隔文本。它的优势是省去 JSON 解析,用 Excel 或 pandas 读取都很顺手;缺点是字段会少掉一部分,比如缺少详细的事件详情链接和多种震级信息,做深度分析时会被迫回头再查单个事件。我平时只把它当作快速探查工具,比如先确认某个时间窗内到底有多少条记录,真正入库时还是会切回 GeoJSON。

三个入口的适合场景可以这样概括:实时监测页面用 Feed,归档和回溯用 FDSN 事件服务,临时看数据用 CSV 出口。选型时还有一个容易忽略的细节,Feed 里的 all_month 虽然能拿到 30 天数据,但它的数据源和 FDSN query 并不完全同步,具体表现是同一个事件在同一时刻查出的震级可能差 0.1 到 0.3,这个问题后面避坑章节会专门讲。

2.2 过滤器参数:时间、震级、经纬度框的边界条件

如果说数据源选型决定你能不能拿到数据,参数过滤就决定你拿到的数据是不是你要的那批。FDSN 事件服务最常用的参数我在下表里列一下,方便写请求时对照。需要注意,这些参数全部是 HTTP query string,用 requests 时放进 params 字典即可,别自己拼 URL。

参数名作用常见取值示例
starttime / endtime限定事件发生时间范围,ISO 8601 格式2024-01-01T00:00:00
minmagnitude / maxmagnitude按目录首选震级过滤minmagnitude=5.5
minlatitude / maxlatitude纬度范围,南纬为负-45 到 45
minlongitude / maxlongitude经度范围,西经为负-180 到 180
mindepth / maxdepth震源深度范围,单位千米0 到 100
eventtype事件类型,可选 earthquake 或 otherearthquake
orderby结果排序time、time-asc、magnitude
limit / offset分页控制,单页上限约 20000limit=10000
format输出格式geojson、csv、kml

这里有三个边界条件需要特别说明。第一是时间范围,FDSN 接口能回溯的数据可以到上世纪七十年代,但超过三十年以上的早期目录震级类型以体波震级和面波震级为主,跟现在的矩震级不可直接混用,我在做长周期趋势分析时会刻意分时间段建模。第二是震级过滤的语义,minmagnitude 过滤的是"目录首选震级",USGS 每次只选择一个震级作为展示代表,可能是 Mww、Ml、Mb 中的任何一种,如果你想专门筛矩震级,需要在事件详情里再过滤一次。第三是经纬度框不支持跨日期变更线环绕地球,比如把西经 175 度和东经 175 度放在同一个查询里,接口会认为经度范围超过 180 度而报错或返回空结果,正确做法是拆成两个查询再合并结果。

深度参数同样是常见误区,mindepth 和 maxdepth 的单位是千米,不是米。全球事件里震源深度可以到几百千米,浅源地震集中在 0 到 30 千米;如果你想把浅源地震单独排查出来,直接 minlatitude 和 maxlatitude 双框往往更稳定。最后提醒一句,limit 参数默认值对 FDSN 来说通常是 10000 左右,单次超过 20000 条记录容易触发服务端错误,拉取历史数据时我习惯按周切片而不是按月一次拉完。

2.3 返回字段的解析约定:毫秒时间戳、首选震级和坐标顺序

确认了接口和过滤条件,下一步是理解响应里的字段语义。GeoJSON 格式的响应整体是一个符合 GeoJSON 规范的对象,核心数据在 features 数组下的每个元素里,每个元素代表一条地震事件。properties 对象里存放震级、地点描述和时间信息,geometry 对象存放经纬度坐标和深度,这样的嵌套结构让不少只处理过二维表的数据工程师第一次上手时很懵。

最容易看走眼的字段是 time,它不是一个字符串,而是从 1970-01-01T00:00:00Z 开始计数的毫秒数,所有时间戳都是 UTC 时刻,本身不带时区。properties.tz 提供的是该事件当地时区相对 UTC 的分钟偏移量,但这个字段在某些震例里是 null,所以解析时不能依赖它做时间轴换算。我的处理习惯是统一先把毫秒转成 pandas 的 UTC datetime,只在最终展示时按项目需要的时区做转换,中间分析流程一概使用 UTC,这样能避免一半以上的时间错乱问题。

另一个关键字段是 properties.mag,注意它是"首选震级"。USGS 会根据数据质量自动选择一个它认为最可靠的震级类型放到这里,同一事件在不同时刻访问这个字段,值可能因为人工修订而改变。如果你要研究震级稳定性或者做余震序列分析,更可靠的做法是取 properties.detail 指向的单事件详情接口,那里面列出了所有台网计算出的震级类型和对应数值。此外 geometry.coordinates 的顺序是经度在前、纬度在后、深度在最后,跟常见地图 API 的 lat/lon 顺序相反,解析时务必写成 lon, lat, depth,否则所有震中会变成海里的异常点。

字段语义这块我吃过一次亏,在一张时间序列图上有 30% 的事件时间点对不上,排查到大半夜才发现是 time 字段被当成了本地时间直接取小时,而不是先转换到 UTC。建议拿到数据后的第一件事不是画图,而是先打印前三条记录的原始字段做人工确认,这一步能省下后面无数排错时间。

3. 本地跑通最小抓取脚本:从实时 Feed 到存量回填的两种写法

选型做完,接下来就是把数据真正抓到本地。整个抓取链路可以分为三层:第一层用 requests 访问公开接口拿到 JSON;第二层把嵌套的 GeoJSON 解析成扁平化的 DataFrame;第三层处理历史数据回填时的分页和时间切片。很多教程只演示最小请求,导致读者在真实场景下遇到数据量变大、请求超时、内存暴涨时完全不知道怎么扩展。这一章按我实际使用的顺序来写,从最小可用脚本开始,一步一步加到能反复执行的批量抓取任务。

3.1 最小请求脚本:请求头、超时与返回状态判断

最小请求不需要任何第三方库,直接用标准库 urllib 也能做,但我普遍推荐 requests,因为它处理 params 编码和 JSON 解析更省心。先拉最近 24 小时全球目录,代码如下。这个脚本可以作为所有后续代码的骨架,我一开始做某次全球事件可视化 Demo 时就是从这个版本出发的。

import requests # 最近24小时全球地震目录,GeoJSON格式 url = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson" headers = { "User-Agent": "Seismo-Data-Lab/0.1 (contact: data@example.com)", "Accept": "application/json", } resp = requests.get(url, headers=headers, timeout=15) print("status_code:", resp.status_code) print("content_type:", resp.headers.get("Content-Type")) data = resp.json() features = data.get("features", []) print("event count:", len(features)) if features: props = features[0]["properties"] print("sample properties:", props)

请求头里的 User-Agent 在这里是必须养成的习惯。USGS 对没有 User-Agent 的匿名请求一视同仁,但你带上一个能标明用途的标识,出问题排查时更容易在服务日志里定位自己的请求。timeout 我设置成 15 秒,因为全球目录文件不算大,但偶尔会遇到网络波动,15 秒已经能覆盖正常响应。status_code 只检查等不等于 200 还不够,我通常还会看一眼 Content-Type 是不是 application/json,有时候服务端会返回一个 HTML 错误页但状态码仍然是 200,直接解析 JSON 就会抛异常。

第一次跑这个脚本时如果看到 event count 是空或者只有几十条,不用怀疑,这是正常的。所有震例的 properties.mag 分布从负数到 7 以上都有,全部画出来就是一大片噪点,后续过滤要靠 minmagnitude 参数或者本地做条件筛选。另外,这里的 features 数组顺序并不保证按时间排列,需要按 properties.time 重新排序,不要假设索引就是时间顺序。

3.2 把嵌套 GeoJSON 解析成扁平 DataFrame

拿到原始 JSON 之后,下一步是把它整理成能进 pandas 的表格结构。GeoJSON 的嵌套层级深,直接用 pandas.json_normalize 可以省不少代码,但我在生产环境里更倾向于手写一个简单的提取函数,原因是字段重命名和空值处理更可控,而且代码可读性更好。以下是解析函数。

import pandas as pd def parse_usgs_geojson(data): rows = [] for f in data.get("features", []): props = f.get("properties", {}) g = f.get("geometry", {}) # GeoJSON坐标顺序为经度、纬度、深度 coords = g.get("coordinates", [None, None, None]) coords = (coords + [None] * 3)[:3] # 防止坐标字段缺失 rows.append({ "event_id": f.get("id"), "time_epoch_ms": props.get("time"), "tz_offset_min": props.get("tz"), "mag": props.get("mag"), "place": props.get("place"), "detail_url": props.get("detail"), "lon": coords[0], "lat": coords[1], "depth_km": coords[2], }) df = pd.DataFrame(rows) # 毫秒时间戳统一转成UTC datetime if not df.empty: df["time_utc"] = pd.to_datetime(df["time_epoch_ms"], unit="ms", utc=True) return df # 接前面接口返回的 data df = parse_usgs_geojson(data) print(df[["event_id", "time_utc", "mag", "place", "lat", "lon", "depth_km"]].head(5))

这段代码里处理了两个边界问题。第一个是 geometry 字段可能缺失,某些特殊事件类型在目录中并不附带坐标,我通过坐标列表填充 None 来保证列数对齐。第二个是 time_epoch_ms 转换为 time_utc 时显式指定 unit='ms',pandas 默认是纳秒,如果把毫秒时间戳直接传给 pd.to_datetime 而不带 unit,结果会变成 1970 年附近的巨大时间值,这是抓数据后最常见的低级错误之一。

解析完成后一定要做一次数据质量检查,我一般会先看 df.info() 和 df.describe()。一个正常的 24 小时全球目录应该有几千到上万条记录,lat 范围在 -90 到 90,lon 范围在 -180 到 180。如果发现 lat 出现 100 多这种越界值,说明坐标解析时顺序写反了,立即回头改代码,不要带着脏数据往下走。tz_offset_min字段经常是整列的 None,这符合预期,不要因为它空就删掉整列,在图谱上地震所在地理位置的时区分布仍可能用得到。

3.3 翻历史目录:FDSN 事件服务的分页与时间切片回填

实时 Feed 只能拿最近 30 天,做历史分析时必须切换到 FDSN 事件服务。这个接口支持 starttime、endtime 参数,可以精确到秒,也支持 limit 和 offset 做结果分页。限流方面虽然没有明确写死每分钟请求次数,但官方对单次请求的返回量有上限,体感上单次超过 2 万条记录容易超时或报错,所以我更倾向于按时间切片,让每次请求落在几千到一万条左右。

import requests import time base = "https://earthquake.usgs.gov/fdsnws/event/1/query" headers = {"User-Agent": "Seismo-Data-Lab/0.1"} # 复用前面的定义 def fetch_by_date_range(starttime, endtime, minmagnitude=4.5): all_features = [] offset = 1 page_size = 10000 while True: params = { "format": "geojson", "starttime": starttime, "endtime": endtime, "minmagnitude": minmagnitude, "orderby": "time", "limit": page_size, "offset": offset, } resp = requests.get(base, params=params, headers=headers, timeout=30) resp.raise_for_status() payload = resp.json() feats = payload.get("features", []) all_features.extend(feats) # 返回记录数小于单页上限,说明没有下一页了 if len(feats) < page_size: break offset += 1 time.sleep(0.5) # 简单限速,避免连续请求打满服务 return all_features # 示例:取2024年1月全球4.5级以上事件 features = fetch_by_date_range("2024-01-01T00:00:00", "2024-01-31T23:59:59") print("total events:", len(features))

这个函数有几个值得细说的点。其一,orderby 建议固定为 time,这样分页的 offset 是基于时间顺序的游标,不至于在翻页时因排序不稳定导致重复或遗漏。其二,limit 直接设成 10000,是为了让单页数据量适中,既不会频繁翻页,也不会因为太大而触发服务端限制。其三,while True 加 len(feats) < page_size 的退出条件,当最后一页不足上限时自然停止,逻辑直观且不会出现把空页当结果的情况。

时间切片还有个优化技巧,把一个月拆成四个周区间分别请求,能减少单次请求的数据量,同时避免因为临时网络问题丢掉整个月的任务。每个切片请求完成后把 features 合并,再统一传给前面的 parse_usgs_geojson 函数生成 DataFrame。这个模式也适合做成定时任务:每天跑一次前一天的时间窗,把新增震例追加到自己的本地数据库里,形成稳定的增量归档管道。

4. 把震中从表格变成地图:Plotly 散点地图的两个关键映射

抓下来的数据最终要让人能一眼看懂。地震数据最直观的可视化形式就是震中分布图,用气泡大小表示震级、颜色表示深度,既能看到全球地震带轮廓,也能突出目标事件。可视化实现我一般直接用 Plotly Express,因为它交互性好、能导出 HTML,对新手最友好,而且散点地图 API 的设计和 pandas 接得很顺。但如果闭眼就画,很容易遇到负震级导致气泡大小报错、跨日期变更线时地图中心错乱之类的坑。这一章只讲两个关键映射:怎么把震级正确映射成气泡大小,怎么让地图自动居中到数据范围。

4.1 用震级映射气泡大小、用深度映射颜色的实现

先做数据清洗再画图。直接从 DataFrame 画 scatter_geo 时,如果 mag 列里有负值,Plotly 会报错,因为气泡 size 属性不允许负数。所有负震级事件在地图上都应该被视为最小可见点,不能直接删掉,否则全球小震分布信息就丢了。我的处理方式是做一次归一化,把最小震级映射为 1,这样所有点都成为合法正数。

import plotly.express as px # 去除无法定位的行,这是画图前最基础的清洗 plot_df = df.dropna(subset=["lat", "lon", "mag"]).copy() # 把震级归一化到正数区间,避免Plotly气泡尺寸报错 min_mag = plot_df["mag"].min() plot_df["bubble_size"] = (plot_df["mag"] - min_mag) + 1.5 plot_df["bubble_size"] = plot_df["bubble_size"] * 2 # 放大整体气泡 fig = px.scatter_geo( plot_df, lat="lat", lon="lon", size="bubble_size", color="depth_km", hover_name="place", color_continuous_scale="Viridis", projection="natural earth", title="全球震中分布(颜色=深度,大小=震级)", ) fig.show()

这段代码里,bubble_size 是显式构造的列,用来绕过 Plotly 对 marker.size 必须为正数的限制。乘数 2 只是视觉上的放大系数,实际项目中我会根据数据点的密集程度调整,比如只画 5 级以上时就调成 3 或 4,让气泡更有辨识度。color 用 depth_km 而不是震级,是因为震级已经由气泡大小表达了,颜色再放震级会造成信息冗余;深度则能体现不同地震带的发震特征,比如环太平洋深源震和浅源震在图上的颜色差异非常明显。

需要注意,px.scatter_geo 对数据量有一定的性能瓶颈,两三千个点交互还流畅,但如果拉了一整个月的全球小震数据,几万点一起渲染浏览器会很卡。我的实践是把可视化和数据筛选分离,画图前先在 DataFrame 上做一次条件过滤,比如只保留 mag >= 3 的事件,或者按自己关注的区域做经纬度切片。这样图上的信息密度反而更高,比全量渲染更加可读。

4.2 跨日期变更线时让地图自动居中的技巧

地图自动居中是个容易被忽略的问题。Plotly 的默认地图中心是 0 度经线附近,当数据集中在西半球或跨越大西洋时,默认视图会显得很偏。手动指定 center 也不是不行,但如果数据分布动态变化,每次手动改经纬度就是在找死路。Plotly 提供了一个 fitbounds 参数,可以让地图自动匹配当前数据点范围,效果类似点图自适应。

fig.update_geos( fitbounds="locations", projection_type="natural earth", showcountries=True, showcoastlines=True, ) # 如果数据量小,可以关闭地图背景只留数据点 fig.update_layout(margin={"r": 0, "t": 40, "l": 0, "b": 0}) fig.show()

fitbounds 的取值是 "locations",它会根据 scattergeo trace 里的经纬度范围自动计算中心点,这比手动指定 center 要省心得多。但是有一个坑,当数据跨越日期变更线时,比如同时包含 175°E 和 -175°W 的事件,Plotly 会认为经度差接近 350 度,缩放到整个世界地图,整个图就变得很小。处理方式是在抓取时就把跨线区域的事件经度统一转换到 [0, 360] 区间,或者直接按两半分别画两张子图。我的习惯是优先拆成子图,因为转换经度会让图上位置和原始数据对不上,反而引起歧义。

出图之前还有一道工序,把生成的可视化导出成 HTML,方便浏览器里查看和分享。Plotly 导出 HTML 是fig.write_html("global_map.html"),这个文件是自包含的,不需要再装环境,适合把结果发给同事看。如果要嵌入到自己的 Web 服务里,也可以换成fig.to_json()传给前端渲染,但那是另一个话题了,这里不展开。

5. USGS_earthquake 实战避坑:五个翻车现场与排查清单

我在初期处理 USGS 数据时踩过不少坑,有些坑当时花了一整天才排查清楚。这些坑几乎没有例外,全集中在对字段语义的理解、对不同接口特性的把握和对数据分布的错误预期上。这一章把它们单独拿出来,按现象、原因、解决的顺序写清楚。你可以把这个章节当成一份排查清单,每遇到一次异常就往这里对号入座。

5.1 时间轴整体偏移几个小时,时区字段还忽有忽无

现象:画出的震中分布图没问题,但按时间序列统计每小时事件数时,峰值时刻和官方页面完全不对齐,整个曲线平移了几个小时。我曾在一次数据看板调试里发现所有事件时间都慢了八小时,整整一个下午的时间都在质疑自己的统计代码。

原因:USGS 返回的 properties.time 是 UTC 毫秒时间戳,本身是绝对时刻。我在解析后直接调用了本地时间的小时属性,相当于把 UTC 时刻当成了东八区去读,所以整条时间轴全都偏移。properties.tz 字段只在部分事件中有值,不能指望用它来统一校准。

解决:所有内部处理统一用 UTC,转换成df["time_utc"] = pd.to_datetime(df["time_epoch_ms"], unit="ms", utc=True);只有在图表展示的最终环节,才把 UTC 转成目标时区,转换时用 pandas 的dt.tz_convert,不要用字符串替换之类的土办法。这样无论项目部署在哪台机器上,结果都不会因为系统时区不同而再次漂移。

5.2 GeoJSON Feed 和 FDSN 查同一事件,震级对不上

现象:实时监控页面上显示某事件震级 5.3,第二天用 FDSN 事件服务查同一个 event id,震级变成了 5.6,位置也有几百米的偏移,团队以为数据管道出了问题。

原因:这是两类接口的定位差异。Feed 来自自动处理系统,速度快但精度有限;FDSN 目录里包含的是经过人工复核和重新定位的正式结果,震级修正很常见。自动定位的震级波动 0.3 到 0.5 并不奇怪,尤其对远震和大震。

解决:明确接口分工,实时展示层允许用 Feed,但所有进入历史库的数据必须以 FDSN 查询为准。入库时如果按 event id 做增量更新,要设计成"更新覆盖"语义,而不是"不存在才插入"。这样当同一事件出现修订后的震级时,库里保存的是最新可靠值,而不是第一次抓到的临时值。不要试图在 Feed 和 FDSN 之间做数据对齐,两边本来就是两种口径。

5.3 minmagnitude=0 拉了一个月,数据量大到本地直接卡死

现象:请求参数只写了 starttime 和 endtime,没加震级过滤,返回的 features 数量超过十几万,解析成 DataFrame 后内存占用飙升,后续画图完全卡住,浏览器直接白屏。

原因:USGS 目录里包含大量零级以下的小震和微震,全球每天可以达到几千条。不加过滤时,一个月的数据量轻松达到几十万条,远超地图可视化的合理承载范围。这不是接口的 bug,而是对数据规模缺乏预估。

解决:先跑一次带 format=csv 的快速查询确认总量,再决定时间切片粒度。比如先查 2024-01-01 到 2024-01-07 的记录数,如果一周就超过两万条,就把窗口切成按天拉取。可视化前至少先做 minmagnitude 过滤,我通常是从 2.5 或 3.0 起步,再根据分析目的逐步下调。高速下载时空闲时间可以加一句time.sleep(0.3),既不惹恼服务器,也给自己留调控余地。

5.4 震中全部落在海里,还在赤道两边对称

现象:地图上散点几乎全部分布在太平洋和赤道附近,大量点落在海中呈一条直线,陆地上的明显地震带却基本没有点。第一反应是 USGS 数据有误,但网页上明明显示得很正常。

原因:这是 GeoJSON 坐标系顺序被写成 lat, lon 的结果。GeoJSON 规范明确规定 coordinates 是 [经度, 纬度, 深度],但很多用习惯了地图 API 的人在解析时习惯性先填 latitude 再填 longitude,一交换,所有点都被投影到了错误的位置。赤道两侧对称是因为纬度被填进了经度槽,经度值超过 90 的部分被强制包裹后出现了镜像效果。

解决:在解析函数里固定坐标映射顺序,并加一道断言防线。解析完成后检查df["lat"].between(-90, 90)和df["lon"].between(-180, 180)的通过率,如果发现越界值直接抛异常终止流程。这样即使未来换了人维护代码、改了字段名,也能在最早阶段发现坐标错位,而不是等到地图上出现诡异图形才回头查。

5.5 URL 参数没编码,带空格的请求返回 400

现象:手写 URL 拼 request 时用了 starttime=2024-01-01 00:00:00,中间带空格,接口直接返回 HTTP 400,日志里只写了 Bad Request,没有任何更具体的提示。查了一圈文档也没找到原因。

原因:HTTP GET 请求的 query string 里空格必须编码为%20或+。很多教程里用 requests.get(url, params=params) 传入字典,requests 会自动处理编码,于是没人注意到这个问题。自己改成字符串拼接后,站点的合法性检查就会卡在未编码的空格和冒号上。

解决:统一用 requests 的 params 字典参数,不手拼 URL。如果确实需要生成带签名的 URL,用 urllib.parse.urlencode 对参数做编码处理,再把编码后的 query string 拼到基础地址后面。这个坑不大,但会让人在排错时白白浪费不少时间,放在这里提醒一句。

6. 进阶:用古登堡-里克特定律拟合 b 值,反向检查数据完整性

抓下来的数据质量到底怎么样,不能光靠肉眼扫地图,也值得用一个简单的定量手段来验证。地震学里的古登堡-里克特定律描述震级和频次的关系:log10N = a - bM,其中 N 是大于等于震级 M 的事件数,b 值正常范围大约在 0.8 到 1.2。b 值异常偏大,通常意味着小震级事件在目录里大量缺失,数据截止不完整。利用这个关系做一次线性回归,就能找到自己数据集的完整性下限 Mc,也能验证接口是否把该收的事件都收进来了。

import numpy as np mags = plot_df["mag"].dropna().values # 震级分桶,统计每档事件数量,再计算累积频次 bin_edges = np.arange(np.floor(mags.min()), np.ceil(mags.max()), 0.1) counts, _ = np.histogram(mags, bins=bin_edges) counts = counts[::-1] # 从大到小累积 cum_counts = np.cumsum(counts) # 取中值作为该震级档的代表 bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2 logN = np.log10(cum_counts) # 只拟合大于完整性下限Mc的数据段,这里以3.0为例 mask = bin_centers >= 3.0 coeff = np.polyfit(bin_centers[mask], logN[mask], 1) b_value = -coeff[0] print(f"b值: {b_value:.2f}")

这段代码的核心是最小二乘拟合,np.polyfit 返回的斜率取负号就是 b 值。拟合前要把所有低震级段剔除,否则低震级缺失带来的曲线弯曲会拉高斜率,b 值失真到 1.5 甚至更高。完整性下限 Mc 的判断方法很简单,把拟合直线和实际累积曲线画在同一张图上,曲线开始明显偏离直线的那个震级就是 Mc。低于 Mc 的数据在统计上不可靠,做趋势分析时应丢弃。我自己的习惯是,每个区域数据集都算一次 b 值,如果某个月的 b 值突然跳到 1.4 以上,就会先检查抓取任务是不是断了,再怀疑数据源本身的问题。

这一步做下来,USGS_earthquake 从拉数到验证的完整链路就通了。项目日后真正起作用的不是那条地图,而是你建立的这套数据处理习惯:先确认接口语义,再写防御性解析,最后用科学的统计方法做自查。我从一开始直接拿全量震级拟合,跑出过 1.8 的荒唐 b 值,后来才意识到低震级段的漏报全都被带进了回归里。这个错犯过一次之后,我再也不会跳过 Mc 判断直接套公式。希望这篇笔记里的思路和脚本能帮你少走这段弯路,也希望你在自己的数据管道里早日跑出稳定的 b 值。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 13:36:24

MySQL触发器实战指南:从语法到避坑,一篇讲透

“MySQL 进阶三板斧”这个系列&#xff0c;之前聊过不少优化和锁相关的东西&#xff0c;这一篇换个口味&#xff0c;聊聊很多朋友既熟悉又陌生的“触发器 (Trigger)”。写这一篇的起因&#xff0c;是有次在项目里排查一个订单库存异常的问题&#xff0c;查到最后发现不是业务代…

作者头像 李华
网站建设 2026/10/13 13:36:18

数据湖架构从原理到落地:存储、表格式与实战避坑指南

前阵子参加一场数据架构评审会&#xff0c;会上又吵起了数据仓库和数据湖的边界问题。有人说数据湖就是把文件丢到对象存储里&#xff0c;有人说数据湖已经过时了&#xff0c;湖仓一体才是未来。老实讲&#xff0c;数据湖架构这个概念被讲烂了&#xff0c;真正把它落地跑稳的团…

作者头像 李华
网站建设 2026/10/13 13:35:37

Claude Haiku 5.5 成本降75%:轻量模型迁移与混合部署实战

1. 这次更新到底改了什么&#xff1a;从成本结构说起 Claude Haiku 5.5 发布的消息里&#xff0c;最抓眼球的数字就是“运行成本较 4.5 降低约 75%”。很多人第一反应是“降价了”&#xff0c;但如果你真在业务里跑过模型调用&#xff0c;就会知道成本下降从来不是单一维度的事…

作者头像 李华
网站建设 2026/10/13 13:34:21

从源码到可执行文件:一文搞懂编译链接全流程

1. 从一行代码到可执行文件&#xff0c;中间到底发生了什么如果你刚开始学编程&#xff0c;大概率有过这样的疑惑&#xff1a;明明在编辑器里敲了几行代码&#xff0c;保存、运行&#xff0c;程序就跑起来了。但中间那个“保存”到“运行”之间&#xff0c;编译器到底背着我们干…

作者头像 李华
网站建设 2026/10/13 13:33:44

网页游戏板块背后的技术跃迁:WebGPU、Service Worker与实时交互

1. 从“主页多了一块区域”说起&#xff1a;这不是UI改版&#xff0c;而是平台基因的悄然迁移 我第一次注意到那个变化&#xff0c;是在一个普通周三下午刷完三支短视频后——首页左上角突然多出一个带游戏手柄图标的独立板块&#xff0c;标题是“网页游戏”&#xff0c;底下排…

作者头像 李华
网站建设 2026/10/13 13:31:56

微信PC版数据库解密工具:从密钥字节数组到拖拽打包的完整实现

简介&#xff1a;一份面向开发者和数据恢复需求的微信PC版加密数据库解密工具&#xff0c;基于.NET框架实现。工具通过自定义密钥字节数组完成解密&#xff0c;支持将加密数据库文件拖拽到可执行程序上快速触发&#xff0c;处理完成后自动归档为Decrypte.zip&#xff0c;适配聊…

作者头像 李华