简介CrashData 是一份面向数据分析与航空安全研究者的 R 语言数据采集脚本资源用于从 planecrashinfo.com 逐年抓取飞机失事的历史记录解决公开事故数据分散、难以批量获取的问题。作者以略显取巧但可用的方式将网页内容整理为规整的 data.frame方便后续做趋势分析、可视化或统计建模适合具备 R 基础、关注航空事故数据的中级学习者。压缩包共 2 个文件包含 1 个 R 脚本与 1 个 md 说明文档整体仅 1KB轻量易读脚本负责抓取与结构化说明文档交代用法与背景。目前已有 1422 人学习下载。读者可直接获得可复用的数据抓取逻辑、逐年提取思路以及开箱即用的数据框输出省去自行解析网页的时间也能在此基础上扩展字段、清洗缺失值或接入自己的分析流程是入门网络数据采集与航空事故研究的实用起点。1. 从公开事故记录到可分析数据集CrashData 到底解决什么问题做航空安全分析、事故统计建模或者时间序列研究的人大概率都遇到过同一个尴尬网上能查到的坠机信息不少但真要拉一份结构化的历史数据下来要么字段残缺要么格式混乱要么得手动一页页抄。CrashData 这个资源包瞄准的就是这个痛点——它把 planecrashinfo.com 上公开的飞机失事历史记录整理成一份可以直接用 R 加载、清洗、分析的本地数据集。换句话说它不教你造轮子而是把轮子拆好、标好尺寸递到你手上。这份资源适合三类人一是做航空安全或事故频率建模的研究者需要一份可复现的底层数据二是学 R 语言数据清洗的练习者想拿真实脏数据练手三是做可视化或统计报告的人想快速拿到一份带时间、地点、机型、伤亡人数的结构化表。它不涉及任何敏感信息全部来自公开事故记录重点在于“把散落的网页表格变成能跑代码的数据框”。下面我从数据获取、字段解析、清洗逻辑到常见翻车点一步步拆开讲。2. 数据获取与原始结构从网页表格到本地文件的完整链路2.1 为什么选 planecrashinfo.com 作为数据源公开的航空事故记录来源不少但 planecrashinfo.com 的特点是时间跨度长、字段相对固定、页面结构简单。它按年份组织事故列表每条记录通常包含日期、地点、机型、注册号、运营商、死亡人数、事故描述等。对于用 R 做分析的人来说这种“一个年份一个页面、表格行列规整”的结构比那些需要登录、分页、动态加载的站点友好得多。常见做法是先用rvest把每个年份页面的表格抓下来再合并成一个大表。但这里有个前提你得先确认目标页面的 HTML 结构是否稳定。我一般会先抓一年看看列数和列名再决定是直接读table还是逐行解析tr。CrashData 资源包里已经包含了整理好的原始抓取结果和对应的 R 脚本省去了从零写爬虫的环节。如果你要自己复现建议先检查页面是否有反爬限制以及年份覆盖范围是否满足你的研究区间。2.2 用 R 读取和合并多年度数据假设你已经把各年份的页面保存为本地 HTML 文件或者直接用read_html()在线读取下面这段代码展示了如何把多个年份的表格合并成一个数据框。注意年份范围要根据你的实际需求调整不要盲目全量抓取。library(rvest) library(dplyr) library(purrr) # 定义要抓取的年份范围建议先小范围测试 years - 2000:2020 # 用一个函数封装单年抓取逻辑 fetch_year - function(year) { url - paste0(http://www.planecrashinfo.com/, year, /, year, .htm) page - read_html(url) # 抓取页面中的表格通常第一个表格就是事故列表 tbl - page %% html_node(table) %% html_table(fill TRUE) # 加上年份列方便后续合并 tbl$year - year return(tbl) } # 批量抓取用 possibly 避免单年失败导致整体中断 safe_fetch - possibly(fetch_year, otherwise NULL) all_data - map(years, safe_fetch) %% compact() %% bind_rows()这段代码的核心逻辑是“分年抓取、统一加年份列、最后按行合并”。possibly()的作用是容错——某一年页面结构变了或者网络超时不会让整个循环崩掉而是返回 NULL 被compact()过滤掉。参数方面html_table(fill TRUE)里的fill很关键因为原始表格可能存在空单元格不加这个参数会报错或者列数对不齐。年份范围建议先跑 3 到 5 年确认字段一致后再扩大。2.3 字段映射与类型转换的注意事项抓下来的原始表格通常是字符型列名也可能带空格或特殊符号。直接拿来分析会很难受所以需要做一轮重命名和类型转换。常见做法是把日期列转成Date类型把死亡人数转成数值型把地点和运营商保留为字符型。这里有个坑原始数据里的日期格式可能是“Jan 1, 2000”这种直接as.Date()会返回 NA需要指定格式或者用lubridate的mdy()。library(lubridate) clean_data - all_data %% rename( date_raw Date, location Location, operator Operator, aircraft Aircraft, fatalities Fatalities ) %% mutate( # 日期解析注意原始格式可能是 Jan 1, 2000 date mdy(date_raw), # 死亡人数可能包含 123 (123) 这种格式需要提取数字 fatalities_num as.numeric(str_extract(fatalities, \\d)), # 年份从日期中提取用于分组统计 year year(date) ) %% filter(!is.na(date)) # 去掉日期解析失败的行这段清洗逻辑的重点是“先重命名、再解析、最后过滤”。mdy()能自动识别月日年顺序比as.Date()省心。str_extract()用来从混合字符串里抠出第一个数字因为原始死亡人数字段有时会写成“123 (123)”表示总死亡和机上死亡。过滤掉日期解析失败的行是必要的否则后续按时间聚合会出错。如果你发现某一年大量日期解析失败大概率是页面格式变了需要单独检查那一年的原始 HTML。3. 数据清洗与特征工程把脏表变成可建模的数据框3.1 处理缺失值与重复记录的策略公开事故记录里缺失值很常见尤其是早期年份的运营商、机型字段。直接删行会损失样本全部填充又可能引入偏差。我一般会分字段处理日期和死亡人数这种关键字段缺失就删地点和运营商这种描述性字段缺失就保留 NA分析时再按需过滤。重复记录方面同一年份页面里偶尔会出现同一事故被列两次的情况通常是因为表格跨页或者抓取时重复读取。可以用日期加地点加机型做联合去重。dedup_data - clean_data %% # 关键字段缺失直接剔除 filter(!is.na(date), !is.na(fatalities_num)) %% # 按日期、地点、机型联合去重保留第一条 distinct(date, location, aircraft, .keep_all TRUE)distinct()的参数.keep_all TRUE表示保留所有列只按指定列去重。这里选日期、地点、机型三个字段是因为它们组合起来基本能唯一标识一次事故。如果你发现去重后样本量骤降可能是某个字段的格式不统一比如地点里多了空格或者大小写不一致需要先做标准化。3.2 构造时间维度特征年份、十年期与事故频率做时间序列分析时原始日期粒度太细通常需要聚合到年或十年。用lubridate可以很方便地提取年份、月份、十年期。事故频率则可以用count()按年份统计再和总航班量或乘客数做归一化——不过后者需要额外数据源CrashData 本身不包含。这里先展示如何构造年份和十年期字段以及如何计算每年的每起事故平均死亡人数。feature_data - dedup_data %% mutate( year year(date), decade floor(year / 10) * 10, month month(date, label TRUE) ) %% group_by(year) %% mutate( accidents_per_year n(), avg_fatalities mean(fatalities_num, na.rm TRUE) ) %% ungroup()floor(year / 10) * 10是构造十年期的常用技巧比如 1987 年归到 1980 年代。group_by()加mutate()可以在不丢失原始行的情况下添加聚合特征比summarise()更适合后续做逐行分析。na.rm TRUE在计算均值时排除缺失值避免整列变 NA。如果你要做事故频率的年度趋势图直接用count(feature_data, year)更简洁。3.3 地点与运营商的文本标准化地点字段经常包含国家、城市、机场名混在一起的情况运营商字段可能有缩写和全称并存。如果要做分组统计不标准化会导致同一个实体被拆成多个类别。常见做法是用stringr做大小写统一、去空格、替换常见缩写。比如把“U.S.”和“United States”统一成“USA”把“Intl”和“International”统一。这一步没有万能规则需要根据你的分析目标决定粒度。library(stringr) normalized_data - feature_data %% mutate( location_clean location %% str_to_upper() %% str_replace_all(U\\.S\\., USA) %% str_replace_all(UNITED STATES, USA) %% str_trim(), operator_clean operator %% str_to_upper() %% str_replace_all(AIRLINES, AIR) %% str_trim() )这段代码展示了文本标准化的基本套路先统一大写再替换常见变体最后去首尾空格。str_replace_all()支持正则所以“U\.S\.”里的反斜杠是转义点号。注意不要过度标准化比如把“AIR”和“AIRWAYS”合并可能会丢失信息。我一般会先跑一遍table()看看有哪些高频值再决定替换规则。4. 避坑与排查抓取和清洗中最容易翻车的五个点4.1 页面结构变动导致表格列错位现象某一年抓下来的数据框列数和列名跟其他年份对不上合并时报错或者出现大量 NA。原因planecrashinfo.com 的页面模板并非完全统一早期年份和近年份的表格列数可能不同或者某些年份的表格嵌套了额外标签。解决不要假设所有年份结构一致。先写一个检查函数输出每年的列数和列名对比后再决定是统一重命名还是分组合并。如果差异太大可以考虑按年份分别保存分析时再按需合并。4.2 日期格式解析失败导致大量 NA现象mdy()或as.Date()之后date列出现大量 NA过滤后样本量骤减。原因原始日期字段可能包含“Jan 1, 2000”“1 January 2000”“2000-01-01”等多种格式单一解析函数无法覆盖。解决先用unique()看看日期字段有哪些模式再写一个分支解析函数。常见做法是用parse_date_time()指定多个候选格式或者用case_when()按模式分别处理。解析失败的行不要急着删先导出看看是不是格式特殊。4.3 死亡人数字段混入非数字字符现象as.numeric()转换后产生大量 NA并伴随警告“NAs introduced by coercion”。原因死亡人数字段可能包含“123 (123)”“约 100”“unknown”等非纯数字内容。解决先用str_extract()提取第一个连续数字再转数值。对于“unknown”这类可以统一替换为 NA 并在分析时排除。不要直接用as.numeric()硬转否则会丢失信息且难以排查。4.4 重复抓取导致同一条事故出现多次现象合并后的数据框里同一天同一地点的事故出现两到三次。原因可能是页面本身重复列出也可能是抓取时循环逻辑有误比如年份范围重叠或者 URL 拼接错误。解决用distinct()按关键字段去重同时检查年份循环是否有多余的重复。建议在抓取阶段就打印每年的记录数异常高的年份重点排查。4.5 在线抓取被限流或超时现象批量抓取时部分年份返回空表或报错重试后又正常。原因目标站点对频繁请求有限制或者本地网络不稳定。解决在循环里加Sys.sleep()控制请求频率一般 1 到 2 秒比较安全。用possibly()或tryCatch()包裹单次抓取失败时记录年份并跳过最后统一重试失败年份。不要一次性抓几十年分批次跑更稳妥。5. 进阶用法用 R 做事故趋势可视化与交互式查询5.1 快速绘制年度事故数与死亡人数趋势图清洗完的数据可以直接用ggplot2出图。下面这段代码展示了如何把年度事故数和平均死亡人数画在同一张图上双轴虽然不推荐但用于快速探索很方便。更常见的做法是分面或者用两条独立的时间序列。library(ggplot2) yearly_summary - normalized_data %% group_by(year) %% summarise( accidents n(), total_fatalities sum(fatalities_num, na.rm TRUE), avg_fatalities mean(fatalities_num, na.rm TRUE) ) ggplot(yearly_summary, aes(x year)) geom_line(aes(y accidents, color 事故数), size 1) geom_line(aes(y avg_fatalities, color 平均死亡人数), size 1) scale_color_manual(values c(事故数 steelblue, 平均死亡人数 firebrick)) labs( title 年度事故数与平均死亡人数趋势, x 年份, y 数值, color 指标 ) theme_minimal()这段代码先用group_by()和summarise()生成年度汇总表再用ggplot2画两条线。scale_color_manual()用来指定颜色映射避免默认配色难以区分。注意size参数在较新版本的ggplot2里被linewidth替代如果报错就换成linewidth。这张图适合快速看趋势如果要发表或做报告建议分开画或者用分面。5.2 用 DT 包做交互式事故查询表如果你想把数据分享给不写代码的同事可以用DT包生成一个可搜索、可排序的 HTML 表格。下面这段代码展示了如何把清洗后的数据渲染成交互表并限制默认显示行数。library(DT) datatable( normalized_data %% select(date, location_clean, operator_clean, aircraft, fatalities_num) %% arrange(desc(date)), filter top, options list( pageLength 15, autoWidth TRUE, scrollX TRUE ), caption 飞机失事历史记录清洗后 )filter top会在表格顶部加一排筛选框方便按地点或运营商过滤。pageLength 15控制默认每页显示行数避免一次性渲染太多行导致卡顿。scrollX TRUE在列数较多时启用横向滚动。这个表可以直接嵌入 R Markdown 文档导出 HTML 后发给别人用浏览器打开。5.3 一个我踩过的坑不要用原始数据直接做回归早期我图省事直接把抓下来的原始表扔进lm()做死亡人数和年份的回归结果系数完全不可解释。后来发现两个问题一是死亡人数字段里混了非数字被强制转换后产生大量 NA实际参与回归的样本只剩一小部分二是不同年份的事故记录完整度不一样早期年份漏记严重直接回归等于用有偏样本拟合。从那以后我每次拿到这类历史数据都强制走一遍“字段类型检查 → 缺失模式可视化 → 样本完整性评估”三步确认数据质量后再建模。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取