数据集成与变量转换

本练习承接上一页的清洗结果 clean_std,演示多表对齐合并(数据集成)与常见的变量转换/特征构造(如标准化、归一化、对数变换、分箱、PCA)。 每一步都尽量拆成短代码块,方便你逐段复制运行。
建议时长
30–45 分钟
学习目标
会按主键对齐合并两张表;会做 4 类常用变量转换;能产出可用于建模的特征矩阵或 PCA 结果
前置
上一练习的输出对象 clean_std(若新开会话,请按第 0 节重建)
提醒:如果你刚做完 02 脏数据清洗 且 R 环境里还在,通常可以直接从第 1 节开始。

0. 得到 clean_std(新会话)

0.1 如果已有 clean_std:直接跳过

Step 0 / 检查对象是否存在
exists("clean_std")
if (exists("clean_std")) head(clean_std, 3)
检查点:如果输出为 TRUE,且能看到 clean_std 的前几行,就可以直接去第 1 节。

0.2 新会话:按步骤重建 clean_std

重要:第 0 节只是在你“新开 R 会话”时让你能继续做第 1 节。若你想完全理解清洗流程,请回到上一练习按步骤运行:02 脏数据构造、识别与清洗
Step 1 / 设置镜像(可选)
options(repos = c(CRAN = "https://mirrors.tuna.tsinghua.edu.cn/CRAN/"))
Step 2 / 安装并加载 MASS(必须)
if (!requireNamespace("MASS", quietly = TRUE)) install.packages("MASS")
library(MASS)
Step 3 / 载入数据并复制一份
data(Melanoma)
Melanoma_dirty = as.data.frame(Melanoma)
Step 4 / 制造脏数据(与上一练习一致,保证可复现)
set.seed(123)
n = nrow(Melanoma_dirty)

# 缺失
Melanoma_dirty$age[sample(n, 8)] = NA
Melanoma_dirty$thickness[sample(n, 6)] = NA

# 异常
Melanoma_dirty$age[3] = -1
Melanoma_dirty$age[10] = 200
Melanoma_dirty$thickness[5] = 999

# 重复
dup_rows = Melanoma_dirty[c(1, 1, 15, 15, 15), ]
Melanoma_dirty = rbind(Melanoma_dirty, dup_rows)

# 不一致: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"] = "男"
Melanoma_dirty$sex[sample(which(Melanoma_dirty$sex == "男"), 3)] = "1"
Melanoma_dirty$sex[sample(which(Melanoma_dirty$sex == "女"), 2)] = "0"
Step 5 / 缺失:用中位数填充(便于后续演示)
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 6 / 异常:业务规则 + IQR 标记为 NA
d = as.data.frame(clean_missing)
d$age[d$age < 0 | d$age > 120] = NA
d$thickness[d$thickness > 50] = 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
clean_outlier = d
推荐:用 dplyr 去重(如果你装了 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)
备选:不用 dplyr 的去重(base R)
d = as.data.frame(clean_outlier)
key = paste(d$time, d$age, d$thickness, d$sex, sep = "||")
clean_dup = d[!duplicated(key), ]
Step 7 / 统一 sex 编码(base R,无依赖)
d = as.data.frame(clean_dup)
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")
检查点:你应得到对象 clean_std,并且 sex 的取值应主要是“男/女”(可能有少量 NA)。

1. 集成与变量转换

本节你会做三件事:先把一张表拆成两张“模拟多源数据”,再按主键对齐合并;然后做常见变量转换;最后演示特征构造与 PCA(探索性降维)。

1.1 集成:拆表与合并(对齐主键)

Step 1 / 生成主键 id(这里用行号模拟)
d = as.data.frame(clean_std)
d$id = seq_len(nrow(d))
head(d[, c("id", "time", "age", "sex")], 3)
Step 2 / 拆成两张表:基础信息 vs 结局相关
tbl_base = d[, c("id", "time", "age", "sex")]
tbl_outcome = d[, c("id", "time", "thickness", "ulcer", "status")]
Step 3 / 按主键合并(左连接效果)
merged = merge(tbl_base, tbl_outcome, by = "id", all.x = TRUE, suffixes = c(".x", ".y"))

# 只保留一份 time(示例:保留 time.x)
merged$time.y = NULL
names(merged)[names(merged) == "time.x"] = "time"

head(merged, 3)
检查点merged 应比单张表“列更多”(包含 base 与 outcome 两边的字段),行数与 tbl_base 一致。

1.2 变量转换:标准化 / 归一化 / log / 分箱

Step 4 / Z-score 标准化(均值 0、方差 1)
d = as.data.frame(clean_std)
d$age_z = as.numeric(scale(d$age))
d$thickness_z = as.numeric(scale(d$thickness))
head(d[, c("age", "age_z", "thickness", "thickness_z")], 3)
Step 5 / Min-Max 归一化(映射到 0–1)
d$age_norm = (d$age - min(d$age, na.rm = TRUE)) /
  (max(d$age, na.rm = TRUE) - min(d$age, na.rm = TRUE))

# thickness 可能含 NA 或极端值;分母加一个很小常数防止除 0
d$thickness_norm = (d$thickness - min(d$thickness, na.rm = TRUE)) /
  (max(d$thickness, na.rm = TRUE) - min(d$thickness, na.rm = TRUE) + 1e-8)
head(d[, c("age", "age_norm", "thickness", "thickness_norm")], 3)
Step 6 / 对数变换(适合右偏分布)
d$log_time = log1p(d$time)
d$log_thickness = log1p(d$thickness)
summary(d[, c("time", "log_time", "thickness", "log_thickness")])
Step 7 / 分箱:把连续变量变成类别
d$age_grp = cut(
  d$age,
  breaks = c(0, 40, 60, 100),
  labels = c("<40", "40-60", ">60"),
  include.lowest = TRUE
)
d$thickness_cat = cut(
  d$thickness,
  breaks = c(0, 1, 4, 10),
  labels = c("薄", "中", "厚"),
  include.lowest = TRUE
)
table(d$age_grp, useNA = "ifany")
table(d$thickness_cat, useNA = "ifany")
Step 8 / 保存变换结果(后续提交用)
transformed = d
head(transformed[, c("age", "age_z", "age_norm", "age_grp", "log_thickness", "thickness_cat")], 3)

1.3 特征构造与 PCA(探索性降维)

Step 9 / 特征构造:交互项 + 指示变量
d = as.data.frame(clean_std)
d$age_thick = d$age * d$thickness
d$is_older = (d$age > 60) + 0
head(d[, c("age", "thickness", "age_thick", "is_older")], 3)
Step 10 / PCA:选数值列 → 标准化 → prcomp
num_cols = c("time", "age", "thickness", "ulcer", "year")
X = scale(d[, num_cols], center = TRUE, scale = TRUE)
X = X[complete.cases(X), ]   # PCA 需要完整数值行
pc = prcomp(X)
Step 11 / 看主成分解释度,并取前两维
summary(pc)
feat_pc = pc$x[, 1:2]
colnames(feat_pc) = c("PC1", "PC2")
head(feat_pc, 3)
检查点summary(pc) 会显示每个主成分解释的方差比例;feat_pc 是每条样本在 PC1/PC2 上的坐标。

2. 实验内容

提交建议:尽量提交“短而关键”的输出(例如 head 的前 3 行、table 的频数、summary 的关键行),并配上你的 1–2 句解释。
A. 集成与转换
  • 集成结果:提交 head(merged, 3) 输出(或截图)。
  • 变换解释:从 transformed 中任选 1 列变换(如 age_zage_grp),写一句话解释它在后续分析/建模中可能的作用。
B. PCA(探索性降维)
  • PCA 输出:提交 summary(pc)head(feat_pc, 3) 任一项输出。
  • 一句话说明:用一句话说明 PCA 在本练习中用于做什么(例如探索性降维、特征压缩等)。