脏数据构造、识别与清洗

本练习用 R 语言在 MASS::Melanoma 数据集上,先人为制造常见脏数据,再逐步识别与清洗。每一小段代码都尽量短,方便你“复制 → 运行 → 核对输出”,做出一条可复用的数据清洗流水线。
建议时长
40–60 分钟(含截图/整理提交内容)
学习目标
会数缺失、会找异常、会去重、会统一编码,并能解释你选择的清洗策略
工具
R 或 RStudio;若你用 RStudio,推荐在 Console 里逐段运行

1. Melanoma 与脏数据构造

1.1 准备环境与载入数据

说明:本练习用 MASS 包的 Melanoma(黑色素瘤随访相关变量)。包安装只需一次;每次新开 R 会话需要重新 library(MASS)
Step 1 / 设置镜像(只需一次,可跳过)
options(repos = c(CRAN = "https://mirrors.tuna.tsinghua.edu.cn/CRAN/"))
Step 2 / 安装并加载包(安装只需一次)
install.packages("MASS")      # 未安装时运行;已安装可跳过
library(MASS)                 # 每次新会话都要运行
Step 3 / 载入数据并先“看一眼”
data(Melanoma)
head(Melanoma, 6)             # 前 6 行
str(Melanoma)                 # 变量类型
summary(Melanoma)             # 分布概况
检查点:你应能看到数据是一个表格(data.frame),包含多列变量(如 agethicknesssex 等)。

1.2 逐步制造四类脏数据

  • 缺失:让部分 agethickness 变为 NA
  • 异常:放入不合理年龄/厚度(如 -1、200、999)
  • 重复:追加几行完全重复记录
  • 不一致sex 混用数值(0/1)与文字(男/女)
Step 1 / 复制一份“可随便改”的数据
Melanoma_dirty = as.data.frame(Melanoma)  # 保留原始数据不被污染
Step 2 / 固定随机种子(保证全班结果一致)
set.seed(123)
n = nrow(Melanoma_dirty)
Step 3 / 制造缺失(age 缺 8 个,thickness 缺 6 个)
Melanoma_dirty$age[sample(n, 8)] = NA
Melanoma_dirty$thickness[sample(n, 6)] = NA
Step 4 / 制造异常(放入明显不合理值)
Melanoma_dirty$age[3] = -1        # 不可能的年龄
Melanoma_dirty$age[10] = 200      # 过大年龄(可按业务规则判为异常)
Melanoma_dirty$thickness[5] = 999 # 明显离谱的厚度
Step 5 / 制造重复(追加几条整行重复记录)
dup_rows = Melanoma_dirty[c(1, 1, 15, 15, 15), ]  # 选中若干行当作“重复”
Melanoma_dirty = rbind(Melanoma_dirty, dup_rows)  # 追加到表格末尾
Step 6 / 制造不一致:sex 混用“男/女”和“0/1”
Melanoma_dirty$sex = as.character(Melanoma_dirty$sex)  # 先转成字符,方便混用
Melanoma_dirty$sex[Melanoma_dirty$sex == "0"] = "女"
Melanoma_dirty$sex[Melanoma_dirty$sex == "1"] = "男"

# 让少数记录“反向”变回 0/1(模拟数据录入不一致)
Melanoma_dirty$sex[sample(which(Melanoma_dirty$sex == "男"), 3)] = "1"
Melanoma_dirty$sex[sample(which(Melanoma_dirty$sex == "女"), 2)] = "0"

1.3 快速核对(检查点)

Step 7 / 用最少的输出做核对
head(Melanoma_dirty, 10)
cat("总行数:", nrow(Melanoma_dirty),
    " | 去重后行数:", nrow(unique(Melanoma_dirty)), "\n")
cat("age NA 数:", sum(is.na(Melanoma_dirty$age)),
    " | thickness NA 数:", sum(is.na(Melanoma_dirty$thickness)), "\n")
table(Melanoma_dirty$sex, useNA = "ifany")
检查点:你应看到(1)总行数比原始更多(因为追加了重复行);(2)agethickness 都出现 NA;(3)sex 同时出现“男/女”和“0/1”。

2. 质量问题识别

2.1 缺失:计数与模式

Step 1 / 逐列统计缺失个数与比例
na_count = colSums(is.na(Melanoma_dirty))
na_pct = na_count / nrow(Melanoma_dirty) * 100
data.frame(缺失个数 = na_count, 缺失比例 = round(na_pct, 2))
Step 2 / 只列出“确实有缺失”的列(更清爽)
na_count[na_count > 0]
Step 3 / 看缺失模式:age 和 thickness 是否常一起缺?
table(
  age_缺失 = is.na(Melanoma_dirty$age),
  thickness_缺失 = is.na(Melanoma_dirty$thickness)
)
提示:本练习不要求判断 MCAR/MAR/MNAR,只要你能读懂“是否同时缺失”的列联表即可。

2.2 异常:业务规则 / 3σ / IQR

Step 4 / 先用 summary 快速扫一遍极值
summary(Melanoma_dirty$age)
summary(Melanoma_dirty$thickness)
Step 5 / 用 3σ 找 age 的极端值(先排除 NA)
age_ok = Melanoma_dirty$age[!is.na(Melanoma_dirty$age)]
mu = mean(age_ok)
s = sd(age_ok)
idx_age_3sigma = which(Melanoma_dirty$age < (mu - 3 * s) | Melanoma_dirty$age > (mu + 3 * s))
idx_age_3sigma
Step 6 / 用 IQR 找 thickness 的离群值(箱线图规则)
th = Melanoma_dirty$thickness
q1 = quantile(th, 0.25, na.rm = TRUE)
q3 = quantile(th, 0.75, na.rm = TRUE)
iqr = q3 - q1
idx_th_iqr = which(th < (q1 - 1.5 * iqr) | th > (q3 + 1.5 * iqr))
idx_th_iqr
Step 7 / 可视化:箱线图直观看离群点
boxplot(Melanoma_dirty$age, main = "age 箱线图")
boxplot(Melanoma_dirty$thickness, main = "thickness 箱线图")
注意:3σ/IQR 只是一种“统计规则”。医学数据通常还需要结合业务规则(如年龄 0–120)判断哪些才算真正“错误/无效”。

2.3 重复与编码混用

Step 8 / 重复:先用“总行数 vs unique 行数”快速判断
n_total = nrow(Melanoma_dirty)
n_unique = nrow(unique(Melanoma_dirty))
cat("总行数:", n_total,
    " | 去重后:", n_unique,
    " | 重复行数:", n_total - n_unique, "\n")
Step 9 / 编码混用:用 table 看 sex、status 的取值
table(Melanoma_dirty$sex, useNA = "ifany")
table(Melanoma_dirty$status, useNA = "ifany")
Step 10 / 业务规则示例:找不合理年龄(0–120 之外)
bad_age = (Melanoma_dirty$age < 0) | (Melanoma_dirty$age > 120)
bad_age[is.na(bad_age)] = FALSE
which(bad_age)
可选:用 dplyr 按“业务主键”查重复组合(如果你装了 dplyr)
如果你尚未安装:先运行 install.packages("dplyr")
library(dplyr)
dup_key = Melanoma_dirty %>%
  count(time, age, thickness, sex) %>%   # 这里把这 4 列当作“同一条记录”的关键字段
  filter(n > 1)
dup_key

3. 清洗:缺失与异常

3.1 缺失:删除 vs 填充

怎么选? 删除(na.omit)简单但丢样本;填充能保留样本但会改变分布。本练习两种都演示,你提交时说明自己主要采用哪一种即可。
Step 1 / 方案 A:直接删除含 NA 的行
clean_missing_drop = na.omit(Melanoma_dirty)
nrow(clean_missing_drop)
Step 2 / 方案 B:对 age、thickness 用中位数填充
clean_missing = as.data.frame(Melanoma_dirty)
clean_missing$age[is.na(clean_missing$age)] =
  median(clean_missing$age, na.rm = TRUE)
clean_missing$thickness[is.na(clean_missing$thickness)] =
  median(clean_missing$thickness, na.rm = TRUE)
Step 3 / 检查填充结果:NA 是否清零?
cat("填充后 age NA 数:", sum(is.na(clean_missing$age)),
    " | thickness NA 数:", sum(is.na(clean_missing$thickness)), "\n")

3.2 异常:规则化 + IQR 标记

策略:先用业务规则把“明显无效”的值标为 NA(例如年龄不在 0–120);再用 IQR 把厚度离群点也标为 NA(便于后续统一处理)。
Step 4 / 从“已处理缺失”的数据开始(使用 clean_missing)
d = as.data.frame(clean_missing)
Step 5 / 业务规则:年龄 0–120 之外记为 NA
d$age[d$age < 0 | d$age > 120] = NA
Step 6 / 业务规则:厚度过大记为 NA(示例阈值 50,可按课程要求调整)
d$thickness[d$thickness > 50] = NA
Step 7 / IQR:把箱线图规则外的 thickness 再标为 NA
th = d$thickness[!is.na(d$thickness)]
q1 = quantile(th, 0.25)
q3 = quantile(th, 0.75)
iqr = q3 - q1
out_iqr = (d$thickness < (q1 - 1.5 * iqr)) | (d$thickness > (q3 + 1.5 * iqr))
d$thickness[out_iqr & !is.na(d$thickness)] = NA
Step 8 / 得到 clean_outlier,并画图看处理后分布
clean_outlier = d
boxplot(
  clean_outlier$age, clean_outlier$thickness,
  names = c("age", "thickness"),
  main = "异常值处理后的分布"
)

4. 去重与编码统一

目标:本节结束时你应得到对象 clean_std(已去重 + 已统一编码)。下一练习 数据集成与变量转换 将在此基础上继续。

4.1 去重(定义“同一条记录”)

关键点:去重之前必须先定义业务主键。这里为了练习,临时用 timeagethicknesssex 作为“同一条记录”的关键字段(仅为示例)。
推荐:用 dplyr 去重(更直观)
若提示找不到包:先运行 install.packages("dplyr")
library(dplyr)
d = as.data.frame(clean_outlier)
clean_dup = d %>%
  arrange(time, age, thickness, sex) %>%               # 先排序,保证“保留第一条”可复现
  distinct(time, age, thickness, sex, .keep_all = TRUE)
cat("去重前行数:", nrow(d), " | 去重后:", nrow(clean_dup), "\n")
备选:不用 dplyr 的去重方式(完全基于 base R)
d = as.data.frame(clean_outlier)
key = paste(d$time, d$age, d$thickness, d$sex, sep = "||")
keep = !duplicated(key)        # 保留第一次出现
clean_dup = d[keep, ]
cat("去重前行数:", nrow(d), " | 去重后:", nrow(clean_dup), "\n")

4.2 统一 sex 编码

Step 1 / 在 clean_dup 的基础上统一 sex(输出应只剩“男/女/NA”)
d = as.data.frame(clean_dup)

# 把多种写法统一成 “男/女”;无法识别的写 NA(完全 base R,不依赖 dplyr)
sx = as.character(d$sex)
sx[sx %in% c("1", "男", "M", "Male")] = "男"
sx[sx %in% c("0", "女", "F", "Female")] = "女"
sx[!(sx %in% c("男", "女"))] = NA
d$sex = sx

clean_std = d
table(clean_std$sex, useNA = "ifany")
检查点:运行最后一行的 table 后,sex 的取值应比清洗前更“统一”(不再同时出现 0/1 和 男/女)。

5. 实验内容

A. 数据与脏数据构造
  • 原始数据概览:提交一次 str(Melanoma)summary(Melanoma) 的关键输出(截图或文本)。并用一句话说明:结构化 / 非结构化?为什么?
  • 脏数据核对:构造 Melanoma_dirty 后提交以下数值:sum(is.na(Melanoma_dirty$age))sum(is.na(Melanoma_dirty$thickness))nrow(Melanoma_dirty)nrow(unique(Melanoma_dirty))
B. 识别与清洗
  • 缺失统计:提交“各列缺失个数汇总表”(或仅缺失 > 0 的列),并提交 agethickness 缺失的二维列联表。
  • 编码统一:提交 table(Melanoma_dirty$sex, useNA = "ifany")table(clean_std$sex, useNA = "ifany");用两句话说明“统一前后差异”。
  • 缺失处理策略:说明你主要采用删除还是中位数填充(或两种都试),并给出处理前后 agethickness 的 NA 个数对比。
  • 异常/去重效果:提交去重前后行数;若做了异常值规则或 IQR 处理,请写清阈值或规则依据,并提交处理前后任一变量的 summary() 对比之一。
C. 概念简述
  • 5V:任选两个维度(Volume / Variety / Value / Velocity / Veracity)解释其含义。
  • 结合实例:任选一种脏数据类型(缺失/异常/重复/不一致),各给一句“医学场景中的例子”。