实验目标

本次实验50分钟,完成实验后你将掌握:

  1. 复用课前建好的总工程Rpractice,理解R项目文件管理规范
  2. 区分一手数据、二手数据来源,理解科研数据采集的基本思路
  3. 掌握Tidy Data(整洁数据)的三条核心原则,理解其在R编程中的优势
  4. 完整实操:脏数据导入 → 数据诊断 → 列名标准化、日期清洗、分类变量处理
  5. 识别异常值、统计缺失值,掌握基础缺失值处理思路
  6. 导出清洗完成的数据集,制作科研所需的数据字典,保证分析可复现

预备知识

  1. ✅前置实验:已经创建好总工程 practice,熟悉RStudio项目基本操作
  2. 已经完成前置R基础实验:变量、向量、数据框、因子、管道符%>%基础
  3. 本实验需要拓展包,提前在Rpractice项目内执行安装
# 安装实验需要的全部R包;eval=FALSE代表这份代码不会自动运行
# tidyverse:包含dplyr、ggplot2等,用于数据处理和绘图
# lubridate:专门处理日期时间
# readxl:读取Excel文件
# writexl:写入Excel文件
# knitr:制作美观表格
install.packages(c("tidyverse","lubridate","readxl","writexl","knitr")) #使用国内镜像,下载更快
  1. 实验使用模拟亚太自然灾害数据集,代码可以直接生成脏数据,不需要额外下载文件。

Rpractice总工程说明

【重要】我们第1次实验已经建立总工程 Rpractice,后续所有章节实验全部复用这个工程,不需要重复新建Project。

Rpractice固定目录结构:

  • code/:存放每一章R脚本(本章R脚本保存到这里)
  • data/:存放所有章节原始数据、清洗后数据文件
  • output/:存放所有章节导出的图表、报告

✅ 课堂操作步骤:

  1. 打开Rpractice工程:找到文件夹内 Rpractice.Rproj,双击打开RStudio
  2. File → New File → R Script,新建本文档,保存到 code/ 文件夹,命名chapter3_data_process.R
  3. 直接使用相对路径读写文件:data/xxx.xlsx,图片自动输出到output/

1 数据的获取

数据分为一手数据(直接获取)和二手数据(复用已有公开数据)。

1.1 一手数据获取方式

一手数据:研究者为当前研究专门收集的数据。

  1. 问卷调查:纸质/线上问卷,设计问卷、科学抽样、预调查测试信效度;线上线下混合模式。
  2. 实验方法:实验室实验(高内部效度)、田野实验(高外部效度);随机分配、对照组、盲法。
  3. 观察方法:参与式观察、非参与式观察,注意观察者偏差。

注意:一手数据拿到之后,原始问卷导出的数据,几乎都是“脏数据”,必须经过数据清洗。

1.2 二手数据获取方式

二手数据:他人已经收集完成,用于二次开发分析。

  • 政府统计:统计年鉴、普查公开数据
  • 学术共享数据库:CFPS、CHFS等
  • 开放政务平台、商业报告、机器生成大数据

⚠️二手数据注意点:原始收集目的不一定匹配你的研究,需要校验适用性、准确性、时效性、合规伦理。

1.3 什么是整洁数据 Tidy Data(核心概念,重点)

Tidy Data三条基本原则

  1. ✅每个变量独占一列
  2. ✅每条观测独占一行
  3. ✅每种观测单元构成独立表格

从编程角度理解:为什么我们一定要用Tidy Data?

很多同学会问:只要数据能放进R里,脏数据不也能分析吗? 从R代码编写的角度,整洁数据可以大幅降低写代码的难度,减少bug:

  1. 向量化运算天然适配:R本身是向量语言。一列就是一个向量。整洁数据每一列代表一个变量,我们可以直接对整列做运算(取平均、筛选、转换),不需要写循环、不需要在单元格之间来回跳转。脏数据格式混乱,经常需要写大量循环、判断才能提取信息。
  2. tidyverse函数全部默认接收整洁数据:dplyr、ggplot2这类最常用的包,设计逻辑就是“一列=变量”。只要数据是整洁格式,分组、汇总、绘图一行管道代码就能完成。如果数据是宽表、合并单元格、多变量挤在一列,代码会变得非常冗长。
  3. 代码可读性更强,方便调试、复用:整洁数据结构统一。你写的数据清洗代码,可以迁移到别的数据集;出报错的时候,可以快速定位是哪一列、哪一行出错。杂乱数据的代码只能一次性使用,换一份数据就要全部重写。
  4. 绘图函数输入简单:ggplot要求x、y、分组变量各自单独一列。Tidy Data可以直接映射;脏数据往往需要先拆分、重塑,才能绘图。
  5. 降低人为错误:脏数据经常出现一个单元格多个信息,很容易在读入、运算时产生隐藏错误。整洁数据把信息拆分干净,减少隐性bug。

2 实操:灾害脏数据集生成

我们先运行代码,生成一份存在大量问题的原始脏数据,模拟现实上报的灾害记录,存在中英文混杂、日期格式混乱、异常值、缺失值、列名带空格中文。

# 加载需要的包
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.6
## ✔ forcats   1.0.1     ✔ stringr   1.6.0
## ✔ ggplot2   4.0.1     ✔ tibble    3.3.0
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.2.0     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)
library(writexl)

# set.seed():设定随机种子,保证每次运行生成一模一样的数据,方便复现结果
set.seed(2030)

# 生成2030全年日期字符串
datedata <- seq(ymd("2030-01-01"), ymd("2030-12-31"), by = "day") %>% as.character()

# tibble:tidy版本的数据框,比原生data.frame打印更友好,适合查看数据
disaster_messy_data <- tibble(
  `事件ID` = c(paste0("DIS-2030-", 1001:1021)),       # 灾害事件编号
  `Event Date` = c(                                   # 事件日期,多种格式混杂,脏数据典型特征
    "2030-01-17", "19/03/2030", "20300707", "2030-08-07", "2030/06/18",
    sample(datedata, 16, replace = FALSE)
  ),
  `Disaster Type` = c("Flood", "flooding", "洪水", "Earthquake", "地震", "TYPHOON",
                      "洪水", "earthquake", "地震", "Flood", "flood", "飓风",
                      "Typhoon", "洪水", "Cyclone", "typhoon", "飓风",
                      "干旱", "Earthquake", "earthquake", NA), # 灾害类型中英文混用,存在缺失值NA
  `Casualties` = c(                                   # 伤亡人数,包含异常值99999
    91, 28, 3, 699, 6, 77, 6, 20, 24, 4, 69, 12, 6, 99999, 8, 3, 0, 126, 27, 2, 5
  ),
  `Economic Loss (million USD)` = c(5800, 740, 20, 15000, 255, 12000, 430, 2530,
                                    1650, 250, 4120, 245, 800, NA, 32.2, 41, 970, 1250, 1300, 82, -100), # 经济损失,存在负数和NA
  `Coastal Area` = sample(c("Y", "N", NA), 21, replace = TRUE, prob = c(0.4, 0.5, 0.1)), # 是否沿海,随机生成缺失值
  `Severity Level` = c("severe", "moderate", "mild", "severe", "moderate", "severe",
                       "moderate", "severe", "severe", "mild", "severe", "moderate",
                       "moderate", "mild", "mild", "mild", "moderate", "severe",
                       "moderate", "mild", "mild") # 灾害严重等级
)

# 将脏数据保存到Rpractice工程下的data文件夹
write_xlsx(disaster_messy_data, "data/disaster_messy_data.xlsx")

随堂练习:运行完上面代码,打开Rpractice里的data文件夹,可以看到disaster_messy_data.xlsx原始脏数据文件。

3 完整数据清洗实操流程

3.1 导入原始数据,数据初步诊断

步骤:读入数据 → glimpse()看结构 → summary()看统计、缺失、异常

# 加载读取Excel的包
library(readxl)

# 导入原始脏数据,使用相对路径(Rpractice项目自动定位根目录,换电脑只需要移动整个Rpractice文件夹)
disaster_raw <- read_excel("data/disaster_messy_data.xlsx")

# glimpse():快速查看数据结构,每一列的类型、前几条数据,tidyverse推荐查看方式
glimpse(disaster_raw)
## Rows: 21
## Columns: 7
## $ 事件ID                        <chr> "DIS-2030-1001", "DIS-2030-1002", "DIS-2…
## $ `Event Date`                  <chr> "2030-01-17", "19/03/2030", "20300707", …
## $ `Disaster Type`               <chr> "Flood", "flooding", "洪水", "Earthquake",…
## $ Casualties                    <dbl> 91, 28, 3, 699, 6, 77, 6, 20, 24, 4, 69,…
## $ `Economic Loss (million USD)` <dbl> 5800.0, 740.0, 20.0, 15000.0, 255.0, 120…
## $ `Coastal Area`                <chr> "N", "Y", "Y", "N", "Y", "Y", "Y", NA, "…
## $ `Severity Level`              <chr> "severe", "moderate", "mild", "severe", …
# summary():基础统计摘要,数值列会输出均值最值,同时统计NA数量
summary(disaster_raw)
##     事件ID           Event Date        Disaster Type        Casualties   
##  Length:21          Length:21          Length:21          Min.   :    0  
##  Class :character   Class :character   Class :character   1st Qu.:    5  
##  Mode  :character   Mode  :character   Mode  :character   Median :   12  
##                                                           Mean   : 4820  
##                                                           3rd Qu.:   69  
##                                                           Max.   :99999  
##                                                                          
##  Economic Loss (million USD) Coastal Area       Severity Level    
##  Min.   : -100.0             Length:21          Length:21         
##  1st Qu.:  204.2             Class :character   Class :character  
##  Median :  770.0             Mode  :character   Mode  :character  
##  Mean   : 2370.8                                                  
##  3rd Qu.: 1870.0                                                  
##  Max.   :15000.0                                                  
##  NA's   :1

观察问题清单

  1. 列名混杂中文、空格、括号;
  2. 日期列格式不统一;
  3. 灾害类型中英文混杂;
  4. casualties存在99999极端异常值;economic_loss存在负数;
  5. 多处NA缺失值。

3.2 第一步:标准化列名(snake_case命名)

原则:列名全部小写,空格替换下划线,去掉中文、括号、特殊符号。

  • rename():手动修改个别特殊列
  • rename_with():批量统一处理全部列
# %>% 管道符:把左边对象作为函数第一个参数传入,代码从左往右读,更符合阅读习惯
disaster_clean <- disaster_raw %>%
  # 手动重命名指定列
  rename(
    event_id = `事件ID`,
    economic_loss = `Economic Loss (million USD)`
  ) %>%
  # 批量对所有列名做处理:转小写,空格替换下划线
  rename_with(~ str_to_lower(str_replace_all(., " ", "_")))

# 查看修改后的列名和数据结构
glimpse(disaster_clean)
## Rows: 21
## Columns: 7
## $ event_id       <chr> "DIS-2030-1001", "DIS-2030-1002", "DIS-2030-1003", "DIS…
## $ event_date     <chr> "2030-01-17", "19/03/2030", "20300707", "2030-08-07", "…
## $ disaster_type  <chr> "Flood", "flooding", "洪水", "Earthquake", "地震", "TYPHOON…
## $ casualties     <dbl> 91, 28, 3, 699, 6, 77, 6, 20, 24, 4, 69, 12, 6, 99999, …
## $ economic_loss  <dbl> 5800.0, 740.0, 20.0, 15000.0, 255.0, 12000.0, 430.0, 25…
## $ coastal_area   <chr> "N", "Y", "Y", "N", "Y", "Y", "Y", NA, "N", "N", NA, "N…
## $ severity_level <chr> "severe", "moderate", "mild", "severe", "moderate", "se…

3.3 第二步:统一日期格式

原始数据存在2030-01-17、19/03/2030、20300707多种格式,使用lubridate::parse_date_time()多格式解析。

disaster_clean <- disaster_clean %>%
  mutate(
    # mutate:新增/修改列,Tidy Data中,我们只修改整列,不修改单个单元格
    event_date = coalesce(
      parse_date_time(event_date, orders = c("ymd", "dmy", "mdy"))
    ),
    event_date = format(event_date, "%Y-%m-%d") # 统一转换成 yyyy-mm-dd 文本格式
  )
# 查看修改结果
glimpse(disaster_clean)
## Rows: 21
## Columns: 7
## $ event_id       <chr> "DIS-2030-1001", "DIS-2030-1002", "DIS-2030-1003", "DIS…
## $ event_date     <chr> "2030-01-17", "2030-03-19", "2030-07-07", "2030-08-07",…
## $ disaster_type  <chr> "Flood", "flooding", "洪水", "Earthquake", "地震", "TYPHOON…
## $ casualties     <dbl> 91, 28, 3, 699, 6, 77, 6, 20, 24, 4, 69, 12, 6, 99999, …
## $ economic_loss  <dbl> 5800.0, 740.0, 20.0, 15000.0, 255.0, 12000.0, 430.0, 25…
## $ coastal_area   <chr> "N", "Y", "Y", "N", "Y", "Y", "Y", NA, "N", "N", NA, "N…
## $ severity_level <chr> "severe", "moderate", "mild", "severe", "moderate", "se…

3.4 第三步:分类变量标准化(灾害类型、有序因子)

中英文混杂,使用case_when()做匹配替换,转换因子;严重程度转为有序因子ordered()。

disaster_clean <- disaster_clean %>%
  mutate(
    # case_when:多条件判断,依次匹配条件,适合批量文本替换
    disaster_type = case_when(
      str_detect(disaster_type, "Flood|洪水|flooding") ~ "flood",
      str_detect(disaster_type, "Earthquake|地震") ~ "earthquake",
      str_detect(disaster_type, "cyclone|Cyclone|飓风|TYPHOON|Typhoon") ~ "typhoon",
      str_detect(disaster_type, "wildfire|野火") ~ "wild fire",
      str_detect(disaster_type, "干旱") ~ "drought",
      TRUE ~ tolower(disaster_type) # 其余全部转为小写
    ) %>% factor(), # factor:因子,分类变量,用于分组统计绘图
    # ordered:有序因子,有大小顺序:mild < moderate < severe
    severity_level = ordered(severity_level, levels = c("mild", "moderate", "severe"))
  )
glimpse(disaster_clean)
## Rows: 21
## Columns: 7
## $ event_id       <chr> "DIS-2030-1001", "DIS-2030-1002", "DIS-2030-1003", "DIS…
## $ event_date     <chr> "2030-01-17", "2030-03-19", "2030-07-07", "2030-08-07",…
## $ disaster_type  <fct> flood, flood, flood, earthquake, earthquake, typhoon, f…
## $ casualties     <dbl> 91, 28, 3, 699, 6, 77, 6, 20, 24, 4, 69, 12, 6, 99999, …
## $ economic_loss  <dbl> 5800.0, 740.0, 20.0, 15000.0, 255.0, 12000.0, 430.0, 25…
## $ coastal_area   <chr> "N", "Y", "Y", "N", "Y", "Y", "Y", NA, "N", "N", NA, "N…
## $ severity_level <ord> severe, moderate, mild, severe, moderate, severe, moder…

3.5 第四步:数值异常值处理(业务逻辑规则)

业务常识:

  1. 伤亡人数不可能>10000,99999属于录入错误,改为NA
  2. 经济损失不可能为负数,负数改为NA
disaster_clean <- disaster_clean %>%
  mutate(
    # replace(向量, 条件, 替换值):批量修改满足条件的元素,R向量化操作,不用循环
    casualties = replace(casualties, casualties > 1e4, NA),
    economic_loss = replace(economic_loss, economic_loss < 0, NA)
  )
glimpse(disaster_clean)
## Rows: 21
## Columns: 7
## $ event_id       <chr> "DIS-2030-1001", "DIS-2030-1002", "DIS-2030-1003", "DIS…
## $ event_date     <chr> "2030-01-17", "2030-03-19", "2030-07-07", "2030-08-07",…
## $ disaster_type  <fct> flood, flood, flood, earthquake, earthquake, typhoon, f…
## $ casualties     <dbl> 91, 28, 3, 699, 6, 77, 6, 20, 24, 4, 69, 12, 6, NA, 8, …
## $ economic_loss  <dbl> 5800.0, 740.0, 20.0, 15000.0, 255.0, 12000.0, 430.0, 25…
## $ coastal_area   <chr> "N", "Y", "Y", "N", "Y", "Y", "Y", NA, "N", "N", NA, "N…
## $ severity_level <ord> severe, moderate, mild, severe, moderate, severe, moder…

3.6 第五步:缺失值统计

计算每一列缺失率,快速评估数据质量。

library(knitr)
# map_dbl:对每一列运行函数,返回数值向量,计算每一列缺失比例
na_info <- map_dbl(disaster_clean, ~mean(is.na(.))) %>%
  tibble(variable = names(.), na_rate = .)

# kable:输出美观表格
kable(na_info, caption = "各变量缺失率", format = "simple")
各变量缺失率
variable na_rate
event_id 0.0000000
event_date 0.0000000
disaster_type 0.0476190
casualties 0.0476190
economic_loss 0.0952381
coastal_area 0.1428571
severity_level 0.0000000

真实项目中,缺失值处理分删除、填充等,本实验演示统计缺失率。 本示例代码演示人为补全缺失(仅教学演示!真实科研必须溯源核实原始记录,不能盲目填充)

# which(is.na()):找到缺失值所在行号
disaster_clean$disaster_type[which(is.na(disaster_clean$disaster_type))] <-c("flood")
disaster_clean$casualties[which(is.na(disaster_clean$casualties))] <-c(9)
disaster_clean$economic_loss[which(is.na(disaster_clean$economic_loss))] <-c(500,100)
disaster_clean$coastal_area[which(is.na(disaster_clean$coastal_area))] <-c("Y")

3.7 导出清洗完成的数据

保存到Rpractice工程data/文件夹,原始脏数据不动,新生成清洗版本。

# 将清洗完毕整洁数据写入Excel
write_xlsx(disaster_clean, "data/disaster_data_cleaned.xlsx")

3.8 拓展:制作数据字典(科研报告必备)

记录每一个变量:变量名、中文标签、数据类型、取值范围、来源、完整性。

disaster_clean_note <- tibble(
  变量名 = names(disaster_clean),
  中文标签 = c("事件ID", "事件日期", "灾害类型", "伤亡人数", "经济损失(百万美元)",
               "是否沿海地区", "灾害严重程度"),
  数据类型 = c("字符型", "字符型", "因子型", "数值型", "数值型",
               "字符型", "有序因子型"),
  取值范围 = c("DIS-2030-XXXX", "2030-01-01至2030-12-31",
               "flood/earthquake/typhoon/drought", "0-10000",
               ">=0", "Y/N", "mild/moderate/severe"),
  数据来源 = c("系统生成", "事件报告", "分类系统", "伤亡统计", "经济损失评估",
               "地理信息系统", "灾害评估"),
  完整性 = paste0(round(100 - sapply(disaster_clean, function(x) sum(is.na(x))/length(x)*100), 1), "%")
)

kable(disaster_clean_note,
      caption = "清洗后灾害数据的数据字典",
      align = c("l", "l", "c", "l", "l", "c"),
      format = "simple")
清洗后灾害数据的数据字典
变量名 中文标签 数据类型 取值范围 数据来源 完整性
event_id 事件ID 字符型 DIS-2030-XXXX 系统生成 100%
event_date 事件日期 字符型 2030-01-01至2030-12-31 事件报告 100%
disaster_type 灾害类型 因子型 flood/earthquake/typhoon/drought 分类系统 100%
casualties 伤亡人数 数值型 0-10000 伤亡统计 100%
economic_loss 经济损失(百万美元) 数值型 >=0 经济损失评估 100%
coastal_area 是否沿海地区 字符型 Y/N 地理信息系统 100%
severity_level 灾害严重程度 有序因子型 mild/moderate/severe 灾害评估 100%

随堂练习:

  1. 检查字典,确认每个变量含义;
  2. 思考:如果真实科研,缺失值补全这一步能不能直接写代码填充?为什么?

实验总结

  1. 使用总工程 Rpractice 管理全部代码、数据文件,不重复新建项目,便于多次实验连续开展。
  2. 数据分为一手、二手数据,二手数据要评估适用性。
  3. Tidy Data 三大原则:一变量一列,一观测一行,一类观测一张表。适配 R 向量化与 tidyverse。
  4. 数据清洗完整流程:导入→诊断→标准化列名→日期清洗→分类变量因子化→异常值修正→缺失值统计→导出清洗后数据。
  5. 科研规范:原始数据永远保留不修改;清洗后的数据集单独另存;撰写数据字典,保证研究可复现。

实验课后思考

  1. 在 Rpractice 的 code 文件夹新建 R,完整复现本次实验全部代码,确认 data 文件夹生成两个文件:disaster_messy_data.xlsx、disaster_data_cleaned.xlsx。
  2. 统计 disaster_clean 中,不同灾害类型(disaster_type)的事件数量。
  3. 思考题:
    • 1)为什么原始脏数据不能直接拿来做统计分析?
    • 2)为什么科研中不建议随意填充缺失值?
    • 3)Tidy Data 的三条核心原则是什么?