简介一份围绕USGS地震数据抓取与格式转换的Jupyter Notebook学习资料面向需要批量获取地震目录并开展初步分析的科研人员、地学数据分析学习者。资源提供从USGS地震事件API下载JSON数据、借助JSONView检查字段、再通过HW2.ipynb将JSON清洗为CSV的完整流程覆盖wget环境配置与数据预处理关键环节。压缩包共4个文件包含1个示例ipynb笔记本、1个原始JSON数据集、1个清洗后的CSV文件和1份README说明整体仅1.44MB结构精简便于直接对照运行。已有650人学习下载。通过本包可掌握地震数据自动下载、字段筛选与格式化的实用技巧适合希望快速搭建轻量级地震数据工作流、避免从零踩坑的入门到中级用户。1. USGS_earthquake一条 URL 拉回全球地震先搞懂它到底返回了什么很多人第一次接触 USGS_earthquake 数据接口以为 pandas 读个 CSV 就算会用了真正把请求发出去才发现拿回来的是一份嵌套很深的 GeoJSONproperties 和 geometry 各装一半信息时间戳还是毫秒整数。这个接口解决的事其实很直接用一条带参数的 HTTP 请求按时间、震级、地理位置圈选全球地震事件拿回结构化的 JSON 后自己解析、清洗、画图或做告警。适合做地震数据可视化、灾害评估、科研预处理或者只是想拿真实数据集练手的人。它最大的价值不是“免费数据源”这个标签而是字段规范、历史数据完整、更新及时能当生产级数据管道的地震输入层。本文按我实际搭建过的一条数据链路走一遍请求参数、解析清洗、可视化、增量维护和踩坑全程可复现。2. 用 Requests 拉取 USGS_earthquake从 URL 参数到 DataFrame 的最小路径2.1 先拆 GeoJSON 响应数据在 properties 和 geometry 两层里USGS_earthquake 的查询入口是 FDSN 事件服务协议返回格式支持 geojson、csv、xml 等我几乎只推荐 geojson原因有两条字段可比 csv 多不少而且经纬度和深度结构在 geometry 里解析路径固定不容易被表头顺序带偏。一个典型查询长这样import requests url https://earthquake.usgs.gov/fdsnws/event/1/query params { format: geojson, starttime: 2025-06-01, endtime: 2025-06-08, minmagnitude: 4.0, } resp requests.get(url, paramsparams, timeout30) data resp.json() print(data[metadata][count]) print(data[type]) print(data[features][0][properties].keys())这段代码先把时间段和震级下限塞进 params让 requests 帮你做 URL 编码避免手拼字符串踩空格和时区坑。返回的 data 是个标准 GeoJSON 结构外层 metadata 里有 count 和生成时间features 才是真正的地震事件数组。每个 feature 又分 properties 和 geometry 两层properties 里能拿到 time、mag、place、magType、tsunami 等字段geometry 里只有一个 coordinates 数组依次是经度、纬度、深度单位是千米不是米。第一次跑通后建议先打印一下 properties 的所有 key你会看到大量用不到但偶尔有用的字段比如 status、eventType、felt、cdi。实际做地图可视化我只需要 id、time、mag、place、depth_km、latitude、longitude 七列。特别注意一点metadata.count 是匹配总数features 是实际返回的事件数两者不一致时意味着响应被默认的行数上限截断了后面的增量脚本要按分页或分段查询去拿全量。2.2 最小解析脚本把 features 平铺成 DataFrame拿到 GeoJSON 后不能直接用 pandas 加载因为数据是嵌套的。我一般写一个解析函数把 features 数组逐条抽平存入 records 列表再转 DataFrame。这一步是整个链路的“翻译层”后面所有画图、建模、告警都依赖这一层输出。import pandas as pd def geojson_to_df(data): records [] for feat in data[features]: props feat[properties] coords feat[geometry][coordinates] records.append({ id: feat[id], time: props[time], mag: props[mag], place: props[place], longitude: coords[0], latitude: coords[1], depth_km: coords[2], }) return pd.DataFrame(records) df geojson_to_df(data) print(df.shape) print(df.head())这段代码有三个容易忽略的设计点。第一id 取自 feature 层而不是 properties 层同一个地震事件在 USGS 不同接口里这个 id 是稳定的后面做去重和增量更新都靠它。第二time 字段暂时保留毫秒时间戳不变这一列等到清洗阶段再用 pandas 转 UTC 时间过早转字符串反而会给后续操作找麻烦。第三坐标顺序是 longitude 在前、latitude 在后写错顺序的人十有八九会发现地图上的点落在了海里。解析层只做字段搬运不做类型转换这是刻意的。mag 可能是空值深度可能是负数表示地表以上或未校正统一交给清洗层处理解析层越无脑越不容易出错。2.3 请求参数怎么配时间窗口、震级下限和地理圈选USGS 查询参数看起来简单组合起来却很讲究。starttime 和 endtime 决定时间窗口minmagnitude 管震级下限maxlatitude 这些参数做地理圈选。我一组一组给参数并说清各自的行为。参数示例值作用注意点starttime / endtime2025-06-01 / 2025-06-08时间窗口区间默认按 UTC 解释不带时区会整体偏移minmagnitude4.0过滤震级下限想拿小震做序列分析时调到 2.5 以下maxmagnitude6.0过滤震级上限配合 minmagnitude 就能取区间minlatitude / maxlatitude-30 / 50纬度框选与经度框选配合成矩形区域minlongitude / maxlongitude100 / 140经度框选小心跨 180 度经线的情况orderbytime-asc结果排序建议按 time-asc 排方便增量更新limit20000单次返回条数上限超过上限要分页或缩短时间窗最常见的参数组合是“最近 7 天 震级大于 4”用来做宏观震情看板。若是要做历史地震目录入库存底我不建议一次拉几十年USGS 的默认行数不是给你做全量导出的。我一般按“年—月”分块请求每块控制在两万条以内配合断点记录失败了只重跑落后的一段不会整个任务推倒重来。3. 清洗 USGS_earthquake 数据的三道工序时间、坐标与缺失值3.1 时间列从毫秒时间戳到 UTC别让 Pandas 自动猜GeoJSON 里的 time 字段是自 1970-01-01 起的毫秒数这个设计通信协议里很常见但 pandas 不会主动把它读成时间。直接赋值给 DataFrame 后它只是 int64 列排序、按小时聚合、画时间轴全都不认。正确转换方式是这样的df[time] pd.to_datetime(df[time], unitms, utcTrue) df df.sort_values(time).reset_index(dropTrue) print(df[time].dtype) print(df[time].min(), df[time].max())to_datetime 的 unit 参数必须显式写成 ms默认单位是纳秒不指定的结果就是一组几千年的离谱日期。utcTrue 让时间带时区标记后续聚合按 UTC 的逻辑走不会命中本地时区偏移问题。排序之后最好 reset_index因为后面做 hour_bin 这种派生列时不连续的索引容易在 merge 和画图时报错。有人喜欢把 time 转成字符串再存数据库这也不是不行但会损失时间索引的能力。我习惯保留 datetime64[ns, UTC] 类型入库数据表里单独再存一列 UTC 字符串给调试时肉眼查用两列分工不同。3.2 坐标、深度与空值哪些字段需要显式类型转换坐标字段没有空值问题从来不会缺但它的坑在“假值”返回 0.0 的经纬度并不代表真实震中而是占位值。深度也可能为负通常发生在采用地表参考模型修正时Spherical 地球模型下偶尔出现离地表几千公里的错误深度这类记录在科学计算里要剔除但在通用可视化里保留也无伤大雅。震级字段 mag 才是真正会缺失的字段小地震常常没有震级很多数据库把它存成 null。df[mag] pd.to_numeric(df[mag], errorscoerce) df[depth_km] pd.to_numeric(df[depth_km], errorscoerce) df_plot df.dropna(subset[mag]).copy() df_plot[depth_bin] pd.cut( df_plot[depth_km], bins[-1, 0, 33, 70, 300, 800], labels[0km以内, 0-33km, 33-70km, 70-300km, 300km以上], )先转成 float再用 dropna 过滤比直接丢弃行更稳。mag 缺失的样本在做“某时段最大震级”这类统计时如果不先 dropna聚合结果是 NaN画图时那段曲线会断掉。深浅分桶是为了在地图上区分浅源、中源、深源地震浅源地震破坏性通常更强用颜色区分能直观看出板块边界附近的浅震聚集。分桶边界按实际业务定我给的这组来自常规地震分类适合通用场景。3.3 增量更新与去重维护一个本地地震目录拉历史数据和维护在线目录是两回事。在线数据每次全量拉回来直接覆盖历史研究则要累积。累积的前提是去重USGS 同一个事件在不同查询窗口里可能出现两次比如一次按天另一次按周交集部分的 id 相同。只按时间戳去重不够因为同一个事件可能有多个来源修正过的版本时间会变几毫秒。正确做法是按 id 去重。def merge_quake_records(old_df, new_df): combined pd.concat([old_df, new_df], ignore_indexTrue) combined combined.drop_duplicates(subsetid, keeplast) return combined.sort_values(time).reset_index(dropTrue) local_df merge_quake_records(local_df, df) print(local_df[id].duplicated().sum())这里 keeplast 的含义是新拉到的数据覆盖本地旧记录因为 USGS 会对历史事件做修订震级和位置都可能微调保留旧版本反而让分析前后口径不一致。合并后顺手检查一次 duplicated 数量为 0 才算成功。增量更新的频率要看用途实时告警通常几分钟一次数据研究一天一次足够拉太频只会给自己增加限流风险。4. 震中分布与震级趋势两张图讲清一次地震序列4.1 用 Folium 画震中分布圆的大小和颜色怎么映射地图可视化我优先选 Folium不需要处理投影参数缩放到交互级别很轻松导出 HTML 就能分享。震中分布图的关键不是把点画上去而是让点的大小和颜色承载信息半径表达震级颜色表达深度或烈度。下面这段代码是基础版import folium m folium.Map(location[20, 100], zoom_start3) for _, row in df_plot.iterrows(): folium.CircleMarker( location[row[latitude], row[longitude]], radius2 ** (row[mag] - 3), colorNone, fill_colorred if row[mag] 6 else orange, fill_opacity0.6, popupf{row[place]} M{row[mag]}, ).add_to(m) m.save(quake_map.html)radius 用了指数映射而不是线性映射因为震级本身是对数刻度每差一级能量相差约 32 倍线性映射会让小地震肉眼看不见。2 ** (mag - 3) 的意思是 4 级半径约 2 像素6 级约 8 像素8 级约 32 像素视觉覆盖面积和能量差异大致同阶。颜色按 6 级分界算经验阈值做全球分布可以做某个局部强震序列时要按实际震级分布调比如某次序列全是 3 到 4 级时阈值取 4 更合适。弹出框里放 place 和 mag 是最小可用配置。别放太多字段移动端弹窗会卡顿。数据量大时逐行 iterrows 画圆会慢我通常先按经纬度聚合到网格再画或者改用 MarkerCluster后者点多了会自动聚合成计数气泡。4.2 用 Plotly 画震级-时间气泡图从散点看出序列阶段震级-时间图是地震序列分析里最常用的一张图横轴时间纵轴震级气泡直径映射深度能一眼看出前震、主震、余震的节奏。不用 matplotlib 是因为 Plotly 有悬浮提示和范围缩放疑似异常点可以直接悬停查看是哪个事件。import plotly.express as px fig px.scatter( df_plot.sort_values(time), xtime, ymag, sizemag, colordepth_bin, hover_nameplace, hover_data[id], ) fig.update_traces(markerdict(sizemin3, sizeref0.5)) fig.show()size 和 x 都用了 mag 列图里纵轴读震级气泡大小也读震级视觉上重复但能强化“大事件”的感受。sizeref 是 Plotly 控制气泡绝对大小的缩放系数数值越小气泡越大。这里给出一个手动调法先把 sizeref 设成 1 跑一版满屏都是大圆再逐步往上加直到最大震级的圆不互相遮挡为止比直接改 size 列可控得多。color 接了 depth_bin 分类列颜色区分深浅源。如果你是做单一强震序列建议把颜色改成按时间连续渐变这样能清楚看到余震从主震位置向周边扩散的先后顺序。4.3 图与数据分离先存 GeoJSON 再画图避免重复请求初学者最容易犯的错是在画图脚本里直接发请求导致每调一次图就跑一次网络数据变化时图和行为不可复现。我一般把数据落成中间文件画图脚本只读文件不访问网络。df_plot.to_parquet(quakes_2025_w4.parquet, indexFalse)本地调试时再读回来保证数据和图一一对应这个习惯在数据管道里叫“数据与展示解耦”。Parquet 比 CSV 好在列类型不丢失time 还是 datetimemag 还是 float分桶列还是 category不用每次重读都做一遍类型修正。文件发给同事时对方不需要会调接口也能复现同一张图排查问题时这一条特别省心。5. USGS_earthquake 避坑清单时区、边界与重复记录的 5 个坑5.1 坑一时间参数不带时区查询范围整体偏移一截现象查“2025-06-01 到 2025-06-02”以为是一天返回的事件却包含了前一天或后一天的记录时间窗对不上。原因USGS 的时间参数默认按 UTC 解释如果你本地在东八区2025-06-01 00:00:00 本地时间其实是 2025-05-31 16:00:00 UTC直接传字符串就会把 5 月 31 日下午的半天数据也算进来。解决统一在请求侧把时间转成 UTC 再传给接口。用 Python 生成参数时这样处理from datetime import datetime, timezone, timedelta end_dt datetime(2025, 6, 7, tzinfotimezone.utc) start_dt end_dt - timedelta(days7) params[starttime] start_dt.strftime(%Y-%m-%dT%H:%M:%S) params[endtime] end_dt.strftime(%Y-%m-%dT%H:%M:%S)把时间计算全部改成带时区的 datetime 对象最后才格式化字符串就不会出现手动拼字符串导致的偏移。这是时区问题里最容易修的一条却也是翻车率最高的一条。5.2 坑二mag 和 depth 出现 null排序、绘图、建模三重翻车现象df.sort_values(mag) 报错画图时整行消失算 percentile 得到 NaN。原因USGS 对小地震和极浅源事件经常缺测properties.mag 字段直接是 nullpandas 排序时遇到缺失值会放到末尾或直接抛错具体表现取决于 pandas 版本。解决在清洗阶段统一用 pd.to_numeric 转 float再 dropna(subset[mag])。这里的顺序不能反先转换后丢弃因为 to_numeric 能把字符串形式的 null 正确转成 NaN顺序反了会留下字符串污染整列。5.3 坑三经纬度负号与 180 度跨线地图上多出诡异连线现象地图上出现一条横跨整个太平洋的斜线某些点的经度在 -180 和 179 之间跳变。原因有两类。第一类是经纬度写反把 latitude 当成 longitude 传给了地图第二类是数据源本身存在跨 180 经线的事件比如阿留申群岛附近经度从 179.9 跳到 -179.8连线时地图库会按最短路径连线结果就是横穿全球。解决画图前做一次经度一致性修正。跨线附近的点把负经度统一加 360 转成正度比如 -179.8 变成 180.2待画完再标回实际经度。如果只是画散点不画连线最省事的办法是关掉连线专心看点的分布不影响任何结论。5.4 坑四429 与超时幂等 GET 也要有指数退避现象循环逐月拉历史数据时前几次正常第五次左右开始抛 Timeout再往后直接 HTTP 429。原因USGS 对同一来源的短时高频请求有限流虽然公开接口不强制鉴权但连续大流量请求会触发保护机制。requests 默认没有重试一个超时异常就会让整段循环崩溃。解决写一个带指数退避的重试包装只对 GET 请求做重试因为 GET 是幂等的不会造成重复写入。import time def fetch_geojson(url, params, max_retries4): for attempt in range(max_retries): try: resp requests.get(url, paramsparams, timeout30) if resp.status_code 200: return resp.json() if resp.status_code in (429, 503): wait 2 ** attempt time.sleep(wait) continue resp.raise_for_status() except requests.Timeout: time.sleep(2 ** attempt) raise RuntimeError(ffailed after {max_retries} retries: {params})每次重试等待时间按 1、2、4、8 秒递增给限流窗口足够的恢复时间。如果重试四次仍然失败宁可让任务报错也不静默吞掉否则下游拿到的数据不完整更难排查。5.5 坑五同一个事件出现两次增量更新时不去重会双计现象用“近 7 天”和“近 30 天”两个任务拉数据合并后同一震级同一时间的事件在统计里出现了两次震级次数直接翻倍。原因不同时间窗口查询的结果集有交集同一事件以不同 id 前缀在两次响应里都出现USGS 的 id 是稳定的但只用时间位置去重则可能撞上同一区域同时段的两个独立事件。解决按 id 列去重这个 id 从 feature 层取解析时别漏掉。合并策略用 keeplast新数据里的修订版本覆盖旧数据做增量更新时这个做法能保证本地目录和 USGS 当前口径一致。6. 进阶把全球地震序列做成按小时回放的动画静态图能看出结果但看不出过程。把时间维度变成动画帧每小时推进一步能直观看到余震如何从主震位置向外扩散。做法不算复杂在 Plotly scatter 里加 animation_frame 参数就行。df_plot[hour_bin] df_plot[time].dt.floor(h) fig px.scatter( df_plot.sort_values(hour_bin), xlongitude, ylatitude, sizemag, colordepth_bin, animation_framehour_bin, range_x[-180, 180], range_y[-90, 90], hover_nameplace, ) fig.show()关键在三点。第一hour_bin 是 datetime 类型的分类值不是字符串dt.floor(h) 能把时间规整到整点防止同一小时内的事件分散到多帧。第二数据必须按 animation_frame 排序Plotly 的动画帧按出现顺序播放不排序就会有事件在帧间乱跳。第三range_x 和 range_y 要固定否则每帧自动缩放到当前数据范围看起来就像镜头在乱晃。如果要做 GIF 或视频用 plotly.io.write_image 逐帧导出再合成注意本地渲染会较慢帧数控制在 30 以内比较舒服。我此前给某内部看板做区域地震回放直接把窗口取成 24 小时、按分钟切帧结果动画一卡一卡换成按自然小时聚合后流畅多了。这个教训在于回放是给观察规律用的不是给监控实时刷新用的稳和清楚永远排在炫酷前面。希望帮到你。本文还有配套的精品资源点击获取