在Rstudio中安装本节课所有需要的包,运行以下指令:
install.packages(c(
"dplyr", "tidyr", "data.table",
"ggplot2", "patchwork", "RColorBrewer", "factoextra",
"cluster", "mclust", "dbscan", "Rtsne", "umap",
"arules", "arulesViz", "arulesSequences",
"caret", "randomForest", "xgboost", "iml", "DALEX", "pdp", "lime",
"forecast", "prophet", "fpp2",
"tm", "SnowballC", "wordcloud", "topicmodels", "tidytext", "jiebaR",
"recommenderlab",
"isotree", "dbscan",
"purrr", "broom", "knitr", "mlbench"
))
| 章节 | 主题 | 核心内容 |
|---|---|---|
| 第九章 | 无监督学习 | 聚类方法、降维、异常检测 |
| 第十章 | 关联规则与模式挖掘 | Apriori、FP-Growth、频繁项集 |
| 第十一章 | 特征选择与降维 | 过滤法、包装法、嵌入法 |
| 第十二章 | 模型解释与可解释性 | SHAP、LIME、PDP、公平性 |
| 第十三章 | 时间序列机器学习 | 特征工程、Prophet、交叉验证 |
| 第十四章 | 文本挖掘基础 | TF-IDF、主题模型、文本分类 |
| 第十五章 | 推荐系统基础 | 协同过滤、矩阵分解、评估指标 |
聚类(Clustering) 是一种无监督学习方法,目标是将相似的样本归为同一组(簇),使组内样本相似度高、组间样本相似度低。
与分类的区别:
| 对比维度 | 分类 | 聚类 |
|---|---|---|
| 学习类型 | 监督学习 | 无监督学习 |
| 标签 | 有标签 | 无标签 |
| 目标 | 预测已知类别 | 发现数据结构 |
| 评估 | 准确率等 | 轮廓系数等 |
| 方法类型 | 代表算法 | 核心思想 | 适用场景 |
|---|---|---|---|
| 划分方法 | K-Means | 最小化簇内距离 | 球形簇、规模适中 |
| 层次方法 | 凝聚式聚类 | 自底向上合并 | 层次结构、小数据 |
| 密度方法 | DBSCAN | 密度相连区域 | 任意形状、有噪声 |
| 模型方法 | GMM | 概率分布拟合 | 概率聚类、软分配 |
| 图方法 | 谱聚类 | 图分割 | 非凸形状、复杂结构 |
核心意义:聚类帮助发现数据的内在结构,是数据探索和模式发现的基础工具。
举例:患者分型研究
为什么这种数据适合用聚类?
在医学研究中,往往存在多种疾病亚型,但临床诊断标准可能无法完全区分。例如,糖尿病患者可能有不同的代谢特征组合,传统分类方法可能遗漏某些亚型。聚类分析可以基于患者的多项指标(血糖、胰岛素、BMI、血脂等)自动发现潜在的患者亚群。
研究目的是什么?
可能得到什么样的结果?
聚类可能发现3-4个患者亚群:代谢正常型、肥胖相关型、自身免疫型等。每个亚群可能有不同的治疗响应和预后。
需要注意什么?
聚类结果需要结合临床知识进行解释和验证。不同的聚类方法和参数可能得到不同的结果,需要进行稳定性分析。
K-Means 是最常用的聚类算法,通过迭代优化将数据划分为K个簇:
算法步骤: 1. 随机初始化K个聚类中心 2. 将每个样本分配到最近的聚类中心 3. 重新计算每个簇的中心(均值) 4. 重复步骤2-3直到收敛
目标函数:
\[J = \sum_{k=1}^{K} \sum_{x_i \in C_k} \|x_i - \mu_k\|^2\]
其中\(\mu_k\)是簇\(C_k\)的中心。
# K-Means聚类示例
set.seed(42)
# 生成模拟数据:三个患者群体
n_per_group <- 50
# 创建三种不同特征的患者群体
patient_data <- data.frame(
# 特征1:炎症指标
inflammation = c(
rnorm(n_per_group, mean = 5, sd = 1), # 低炎症组
rnorm(n_per_group, mean = 10, sd = 1.5), # 中炎症组
rnorm(n_per_group, mean = 15, sd = 2) # 高炎症组
),
# 特征2:代谢指标
metabolism = c(
rnorm(n_per_group, mean = 3, sd = 0.8),
rnorm(n_per_group, mean = 6, sd = 1),
rnorm(n_per_group, mean = 9, sd = 1.2)
),
# 真实标签(仅用于验证,聚类时不用)
true_group = rep(c("低风险", "中风险", "高风险"), each = n_per_group)
)
# 执行K-Means聚类(假设不知道真实分组)
kmeans_result <- kmeans(patient_data[, c("inflammation", "metabolism")],
centers = 3, nstart = 25)
# 将聚类结果添加到数据框
patient_data$cluster <- as.factor(kmeans_result$cluster)
# 查看聚类中心
print("聚类中心:")
## [1] "聚类中心:"
print(kmeans_result$centers)
## inflammation metabolism
## 1 14.936596 9.109263
## 2 4.975035 3.027561
## 3 10.374221 6.118794
# 查看簇大小
print("各簇样本数:")
## [1] "各簇样本数:"
print(kmeans_result$size)
## [1] 46 51 53
# 可视化聚类结果
p1 <- ggplot2::ggplot(patient_data, ggplot2::aes(x = inflammation, y = metabolism)) +
ggplot2::geom_point(ggplot2::aes(color = cluster), size = 3, alpha = 0.7) +
ggplot2::geom_point(data = as.data.frame(kmeans_result$centers),
ggplot2::aes(x = inflammation, y = metabolism),
shape = 4, size = 5, stroke = 2, color = "black") +
ggplot2::labs(
title = "K-Means聚类结果",
subtitle = "X标记为聚类中心",
x = "炎症指标", y = "代谢指标", color = "聚类"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
# 对比真实分组
p2 <- ggplot2::ggplot(patient_data, ggplot2::aes(x = inflammation, y = metabolism)) +
ggplot2::geom_point(ggplot2::aes(color = true_group), size = 3, alpha = 0.7) +
ggplot2::labs(
title = "真实分组",
x = "炎症指标", y = "代谢指标", color = "真实组别"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
p1 + p2 + patchwork::plot_annotation(
title = "K-Means聚类 vs 真实分组对比"
)
肘部法则(Elbow Method):绘制不同K值对应的簇内平方和(WSS),选择拐点作为最佳K值。
# 肘部法则:计算不同K值的WSS
wss_values <- sapply(1:10, function(k) {
kmeans(patient_data[, c("inflammation", "metabolism")],
centers = k, nstart = 25)$tot.withinss
})
# 创建数据框用于可视化
elbow_df <- data.frame(
k = 1:10,
wss = wss_values
)
# 绘制肘部图
ggplot2::ggplot(elbow_df, ggplot2::aes(x = k, y = wss)) +
ggplot2::geom_line(color = "steelblue", linewidth = 1) +
ggplot2::geom_point(color = "steelblue", size = 3) +
ggplot2::geom_vline(xintercept = 3, linetype = "dashed", color = "red") +
ggplot2::annotate("text", x = 3.3, y = max(wss_values) * 0.9,
label = "肘部点 K=3", color = "red") +
ggplot2::labs(
title = "肘部法则选择K值",
subtitle = "WSS下降速度明显放缓的点为最佳K值",
x = "聚类数 K", y = "簇内平方和(WSS)"
) +
ggplot2::theme_minimal()
轮廓系数(Silhouette Coefficient) 衡量样本与同簇样本的相似度 vs 与其他簇样本的相似度:
\[s(i) = \frac{b(i) - a(i)}{\max(a(i), b(i))}\]
其中: - \(a(i)\):样本\(i\)与同簇其他样本的平均距离 - \(b(i)\):样本\(i\)与最近其他簇样本的平均距离
取值范围:\(-1 \leq s(i) \leq 1\),越接近1表示聚类效果越好。
# 计算轮廓系数
silhouette_result <- cluster::silhouette(
kmeans_result$cluster,
dist(patient_data[, c("inflammation", "metabolism")])
)
# 使用factoextra包可视化轮廓系数
factoextra::fviz_silhouette(silhouette_result) +
ggplot2::labs(
title = "轮廓系数图",
subtitle = "正值表示聚类合理,负值表示可能分错簇"
) +
ggplot2::theme_minimal()
## cluster size ave.sil.width
## 1 1 46 0.53
## 2 2 51 0.72
## 3 3 53 0.56
# 计算平均轮廓系数
avg_silhouette <- mean(silhouette_result[, 3])
print(paste("平均轮廓系数:", round(avg_silhouette, 3)))
## [1] "平均轮廓系数: 0.606"
K-Means对初始中心敏感,常用改进方法:
| 方法 | 说明 | R实现 |
|---|---|---|
| 随机初始化 | 随机选择K个样本作为初始中心 | kmeans()默认 |
| K-Means++ | 选择距离已选中心较远的点 | kmeans(nstart > 1) |
| 多次运行 | 运行多次选择最优结果 | nstart参数 |
# 使用nstart参数多次运行,选择最优结果
kmeans_multi <- kmeans(patient_data[, c("inflammation", "metabolism")],
centers = 3, nstart = 50)
print(paste("最优结果的WSS:", round(kmeans_multi$tot.withinss, 2)))
## [1] "最优结果的WSS: 434.39"
核心意义:K-Means是最常用的聚类方法,简单高效,适合大规模数据的初步探索。
举例:医院患者分型
为什么这种数据适合用K-Means?
医院积累了大量患者的检验指标数据,如血常规、生化指标等。这些数据通常是连续型数值,且患者群体可能存在自然的分组(如不同疾病类型、不同严重程度)。K-Means可以快速发现这些潜在的患者亚群。
研究目的是什么?
可能得到什么样的结果?
K-Means可能将患者分为3-5个亚群,每个亚群有特征性的检验指标组合。医生可以据此制定针对性的诊疗方案。
需要注意什么?
K-Means假设簇是球形的,对于非球形或大小差异大的簇效果不好。需要预先指定K值,建议结合肘部法则和轮廓系数确定。对异常值敏感,建议先进行异常值处理。
层次聚类(Hierarchical Clustering) 通过构建聚类的层次结构(树状图)来组织数据,不需要预先指定聚类数。
两种主要方法:
| 方法 | 方向 | 过程 |
|---|---|---|
| 凝聚式(Agglomerative) | 自底向上 | 每个样本开始为一个簇,逐步合并 |
| 分裂式(Divisive) | 自顶向下 | 所有样本开始为一个簇,逐步分裂 |
常用链接方法:
| 链接方法 | 簇间距离定义 | 特点 |
|---|---|---|
| 单链接 | 最近点距离 | 发现链状结构 |
| 全链接 | 最远点距离 | 发现紧凑簇 |
| 平均链接 | 平均距离 | 平衡的方法 |
| Ward方法 | 簇内方差增量 | 发现球形簇 |
# 层次聚类示例
set.seed(42)
# 生成模拟数据
n_samples <- 30
hclust_data <- data.frame(
feature1 = c(rnorm(n_samples/3, 2, 0.5),
rnorm(n_samples/3, 5, 0.5),
rnorm(n_samples/3, 8, 0.5)),
feature2 = c(rnorm(n_samples/3, 2, 0.5),
rnorm(n_samples/3, 5, 0.5),
rnorm(n_samples/3, 2, 0.5))
)
# 计算距离矩阵
dist_matrix <- dist(hclust_data)
# 执行层次聚类(使用Ward方法)
hclust_result <- hclust(dist_matrix, method = "ward.D2")
# 绘制树状图
plot(hclust_result, main = "层次聚类树状图",
xlab = "样本", ylab = "距离", sub = "")
# 添加矩形框标记聚类
rect.hclust(hclust_result, k = 3, border = "red")
# 使用factoextra包绘制更美观的树状图
factoextra::fviz_dend(hclust_result, k = 3,
cex = 0.8,
k_colors = c("#2E9FDF", "#00AFBB", "#E7B800"),
rect = TRUE,
rect_border = c("#2E9FDF", "#00AFBB", "#E7B800"),
rect_fill = TRUE,
lower_rect = -0.5) +
ggplot2::labs(
title = "层次聚类树状图",
subtitle = "不同颜色表示不同聚类"
)
# 切割树状图,获取3个聚类
clusters <- cutree(hclust_result, k = 3)
# 将聚类结果添加到数据框
hclust_data$cluster <- as.factor(clusters)
# 可视化聚类结果
ggplot2::ggplot(hclust_data, ggplot2::aes(x = feature1, y = feature2)) +
ggplot2::geom_point(ggplot2::aes(color = cluster), size = 4, alpha = 0.7) +
ggplot2::labs(
title = "层次聚类结果",
subtitle = "通过切割树状图获得3个聚类",
x = "特征1", y = "特征2", color = "聚类"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
DBSCAN(Density-Based Spatial Clustering of Applications with Noise) 是一种基于密度的聚类算法,能够发现任意形状的簇,并识别噪声点。
核心概念:
| 概念 | 定义 |
|---|---|
| 核心点 | ε邻域内至少有MinPts个点的点 |
| 边界点 | 不是核心点,但在某核心点的ε邻域内 |
| 噪声点 | 既不是核心点也不是边界点 |
参数: - ε(eps):邻域半径 - MinPts:形成核心点的最小邻居数
# DBSCAN聚类示例
set.seed(42)
# 生成非球形数据:两个同心圆
n_per_circle <- 100
# 外圆
theta_outer <- runif(n_per_circle, 0, 2 * pi)
outer_circle <- data.frame(
x = 5 * cos(theta_outer) + rnorm(n_per_circle, 0, 0.2),
y = 5 * sin(theta_outer) + rnorm(n_per_circle, 0, 2),
true_cluster = "外圆"
)
# 内圆
theta_inner <- runif(n_per_circle, 0, 2 * pi)
inner_circle <- data.frame(
x = 2 * cos(theta_inner) + rnorm(n_per_circle, 0, 0.2),
y = 2 * sin(theta_inner) + rnorm(n_per_circle, 0, 0.2),
true_cluster = "内圆"
)
# 合并数据
dbscan_data <- rbind(outer_circle, inner_circle)
# 执行DBSCAN聚类
dbscan_result <- dbscan::dbscan(dbscan_data[, c("x", "y")],
eps = 0.5, minPts = 5)
# 查看聚类结果
print("DBSCAN聚类结果:")
## [1] "DBSCAN聚类结果:"
print(table(dbscan_result$cluster))
##
## 0 1 2 3 4
## 88 8 20 60 24
# 0表示噪声点
print(paste("噪声点数量:", sum(dbscan_result$cluster == 0)))
## [1] "噪声点数量: 88"
# 将聚类结果添加到数据框
dbscan_data$cluster <- as.factor(dbscan_result$cluster)
# 可视化
ggplot2::ggplot(dbscan_data, ggplot2::aes(x = x, y = y)) +
ggplot2::geom_point(ggplot2::aes(color = cluster), size = 2, alpha = 0.7) +
ggplot2::scale_color_manual(
values = c("0" = "gray50", "1" = "steelblue", "2" = "salmon"),
labels = c("0" = "噪声", "1" = "簇1", "2" = "簇2")
) +
ggplot2::labs(
title = "DBSCAN聚类结果",
subtitle = "能够发现任意形状的簇,灰色点为噪声",
x = "X", y = "Y", color = "聚类"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
# K-Means对同一数据的聚类结果
kmeans_for_compare <- kmeans(dbscan_data[, c("x", "y")], centers = 2, nstart = 25)
dbscan_data$kmeans_cluster <- as.factor(kmeans_for_compare$cluster)
# 对比可视化
p_dbscan <- ggplot2::ggplot(dbscan_data, ggplot2::aes(x = x, y = y)) +
ggplot2::geom_point(ggplot2::aes(color = cluster), size = 2, alpha = 0.7) +
ggplot2::labs(title = "DBSCAN", x = "", y = "", color = "聚类") +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "none")
p_kmeans <- ggplot2::ggplot(dbscan_data, ggplot2::aes(x = x, y = y)) +
ggplot2::geom_point(ggplot2::aes(color = kmeans_cluster), size = 2, alpha = 0.7) +
ggplot2::labs(title = "K-Means", x = "", y = "", color = "聚类") +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "none")
p_dbscan + p_kmeans + patchwork::plot_annotation(
title = "DBSCAN vs K-Means:非球形数据聚类对比",
subtitle = "DBSCAN能正确识别同心圆结构,K-Means无法处理"
)
# 使用k-距离图选择eps参数
# 计算每个点到其第k近邻的距离
k <- 5 # minPts - 1
knn_dist <- dbscan::kNNdist(dbscan_data[, c("x", "y")], k = k)
# kNNdist当k为单个值时返回向量,直接使用
# 绘制k-距离图(排序后)
knn_dist_sorted <- sort(knn_dist, decreasing = FALSE)
plot(knn_dist_sorted, type = "l",
main = "K-距离图(用于选择eps)",
xlab = "样本(按距离排序)", ylab = paste0(k, "-NN距离"))
abline(h = 0.5, col = "red", lty = 2)
text(length(knn_dist_sorted) * 0.8, 0.6, "eps = 0.5", col = "red")
核心意义:DBSCAN能够发现任意形状的簇,并自动识别噪声点,适合复杂数据结构。
举例:医学影像异常区域检测
为什么这种数据适合用DBSCAN?
医学影像中的病变区域往往形状不规则,且正常组织和异常组织的密度分布不同。DBSCAN可以基于像素特征的密度分布,发现任意形状的异常区域,同时识别噪声点(如伪影)。
研究目的是什么?
可能得到什么样的结果?
DBSCAN可能识别出多个密度较高的区域(疑似病变),同时标记出孤立的噪声点。医生可以重点检查这些区域。
需要注意什么?
参数eps和minPts的选择对结果影响很大。对于密度不均匀的数据,可能需要使用改进算法如HDBSCAN。
高斯混合模型(Gaussian Mixture Model, GMM) 假设数据由多个高斯分布混合生成:
\[p(x) = \sum_{k=1}^{K} \pi_k \mathcal{N}(x | \mu_k, \Sigma_k)\]
其中: - \(\pi_k\):第k个高斯分量的混合系数(权重) - \(\mu_k\):第k个高斯分量的均值 - \(\Sigma_k\):第k个高斯分量的协方差矩阵
EM算法: 1. E步:计算每个样本属于各高斯分量的概率(后验概率) 2. M步:根据后验概率更新参数(均值、协方差、权重) 3. 重复直到收敛
# GMM聚类示例
set.seed(42)
# 生成模拟数据:两个重叠的高斯分布
n_per_cluster <- 100
gmm_data <- data.frame(
x = c(rnorm(n_per_cluster, mean = 0, sd = 1),
rnorm(n_per_cluster, mean = 3, sd = 1.5)),
y = c(rnorm(n_per_cluster, mean = 0, sd = 1),
rnorm(n_per_cluster, mean = 2, sd = 1))
)
# 使用mclust包进行GMM聚类
gmm_result <- mclust::Mclust(gmm_data, G = 2) # 指定2个分量
# 查看模型摘要
print(gmm_result)
## 'Mclust' model object: (EVI,2)
##
## Available components:
## [1] "call" "data" "modelName" "n"
## [5] "d" "G" "BIC" "loglik"
## [9] "df" "bic" "icl" "hypvol"
## [13] "parameters" "z" "classification" "uncertainty"
# 使用factoextra包可视化
factoextra::fviz_cluster(gmm_result, data = gmm_data,
ellipse.type = "confidence",
ellipse.level = 0.95,
geom = "point",
palette = "jco") +
ggplot2::labs(
title = "高斯混合模型聚类",
subtitle = "椭圆表示95%置信区域"
) +
ggplot2::theme_minimal()
# GMM提供软聚类(概率分配)
# 查看每个样本属于各簇的概率
prob_df <- as.data.frame(gmm_result$z)
colnames(prob_df) <- c("P(簇1)", "P(簇2)")
prob_df$sample <- 1:nrow(prob_df)
prob_df$assigned_cluster <- gmm_result$classification
# 查看前10个样本的概率分配
head(prob_df, 10)
## P(簇1) P(簇2) sample assigned_cluster
## 1 0.9999325 6.753952e-05 1 1
## 2 0.9944366 5.563433e-03 2 1
## 3 0.8401161 1.598839e-01 3 1
## 4 0.3338216 6.661784e-01 4 2
## 5 0.9998823 1.177108e-04 5 1
## 6 0.9998853 1.146762e-04 6 1
## 7 0.9918100 8.190009e-03 7 1
## 8 0.9998381 1.619277e-04 8 1
## 9 0.9728473 2.715268e-02 9 1
## 10 0.9973525 2.647517e-03 10 1
# 可视化概率分布
prob_df_melt <- tidyr::pivot_longer(prob_df,
cols = c("P(簇1)", "P(簇2)"),
names_to = "Cluster",
values_to = "Probability")
ggplot2::ggplot(prob_df_melt, ggplot2::aes(x = sample, y = Probability, fill = Cluster)) +
ggplot2::geom_bar(stat = "identity", position = "stack", width = 1) +
ggplot2::labs(
title = "GMM软聚类:样本属于各簇的概率",
subtitle = "颜色深浅表示概率大小",
x = "样本", y = "概率"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
核心意义:GMM提供概率性的聚类结果,适合需要不确定性估计的场景。
举例:疾病风险分层
为什么这种数据适合用GMM?
疾病风险往往是连续的,患者可能同时具有多种风险特征。GMM的软聚类可以给出患者属于各风险组的概率,更符合医学实际。
研究目的是什么?
可能得到什么样的结果?
GMM可能发现患者以80%概率属于高风险组,20%概率属于中风险组,而非简单的”高风险”标签。
需要注意什么?
GMM假设数据服从高斯分布,对于非高斯数据效果可能不好。需要指定分量数量,可以使用BIC等准则自动选择。
谱聚类(Spectral Clustering) 基于图论,将聚类问题转化为图分割问题:
算法步骤: 1. 构建相似度图(如k近邻图) 2. 计算图的拉普拉斯矩阵 3. 计算拉普拉斯矩阵的前k个特征向量 4. 对特征向量进行K-Means聚类
适用场景:非凸形状的簇、复杂结构数据
# 谱聚类示例
set.seed(42)
# 生成双月形数据
moon_data <- mlbench::mlbench.circle(n = 200, d = 2)
moon_df <- data.frame(
x = moon_data$x[, 1],
y = moon_data$x[, 2],
true_class = moon_data$classes
)
# 使用kernlab包的谱聚类
library(kernlab)
spectral_result <- kernlab::specc(as.matrix(moon_df[, c("x", "y")]),
centers = 2)
# 添加聚类结果
moon_df$spectral_cluster <- as.factor(spectral_result@.Data)
# 可视化
ggplot2::ggplot(moon_df, ggplot2::aes(x = x, y = y)) +
ggplot2::geom_point(ggplot2::aes(color = spectral_cluster), size = 2, alpha = 0.7) +
ggplot2::labs(
title = "谱聚类结果",
subtitle = "能够正确识别非凸形状的簇",
x = "X", y = "Y", color = "聚类"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
不需要真实标签,仅基于数据本身评估聚类质量。
| 指标 | 公式/原理 | 取值范围 | 最佳值 |
|---|---|---|---|
| 轮廓系数 | \(\frac{b-a}{\max(a,b)}\) | [-1, 1] | 接近1 |
| Davies-Bouldin指数 | 簇内散度/簇间距离 | [0, ∞) | 接近0 |
| Calinski-Harabasz指数 | 簇间方差/簇内方差 | [0, ∞) | 越大越好 |
# 使用factoextra包计算多种评估指标
# 准备数据
set.seed(42)
eval_data <- data.frame(
x = c(rnorm(50, 0, 1), rnorm(50, 4, 1), rnorm(50, 8, 1)),
y = c(rnorm(50, 0, 1), rnorm(50, 4, 1), rnorm(50, 0, 1))
)
# 执行K-Means
km_eval <- kmeans(eval_data, centers = 3, nstart = 25)
# 轮廓系数
silhouette_eval <- cluster::silhouette(km_eval$cluster, dist(eval_data))
avg_sil <- mean(silhouette_eval[, 3])
# Davies-Bouldin指数
db_index <- clusterSim::index.DB(eval_data, km_eval$cluster)$DB
# Calinski-Harabasz指数
ch_index <- fpc::calinhara(eval_data, km_eval$cluster)
# 汇总结果
eval_results <- data.frame(
指标 = c("轮廓系数", "Davies-Bouldin指数", "Calinski-Harabasz指数"),
值 = c(avg_sil, db_index, ch_index),
最佳值 = c("接近1", "接近0", "越大越好")
)
print(eval_results)
## 指标 值 最佳值
## 1 轮廓系数 0.6778823 接近1
## 2 Davies-Bouldin指数 0.4907522 接近0
## 3 Calinski-Harabasz指数 532.0862203 越大越好
需要真实标签,评估聚类结果与真实分类的一致性。
| 指标 | 说明 | 取值范围 |
|---|---|---|
| 兰德指数(Rand Index) | 样本对分类一致性 | [0, 1] |
| 调整兰德指数(ARI) | 校正随机情况的RI | [-1, 1] |
| 互信息(MI) | 聚类与标签的互信息 | [0, ∞) |
| 归一化互信息(NMI) | 归一化的MI | [0, 1] |
# 假设有真实标签
set.seed(42)
true_labels <- rep(c(1, 2, 3), each = 50)
# 计算调整兰德指数
ari <- mclust::adjustedRandIndex(km_eval$cluster, true_labels)
print(paste("调整兰德指数:", round(ari, 3)))
## [1] "调整兰德指数: 1"
# 计算归一化互信息
nmi <- aricode::NMI(km_eval$cluster, true_labels)
print(paste("归一化互信息:", round(nmi, 3)))
## [1] "归一化互信息: 1"
主成分分析(PCA) 是一种线性降维方法,通过正交变换将高维数据投影到低维空间,保留尽可能多的方差。
目标:找到数据方差最大的方向(主成分)
\[\max_w \text{Var}(Xw) = \max_w w^T \Sigma w\]
其中\(\Sigma\)是协方差矩阵。
# PCA示例:基因表达数据降维
set.seed(42)
# 生成模拟基因表达数据(100个样本,50个基因)
n_samples <- 100
n_genes <- 50
# 创建具有分组结构的基因表达数据
gene_expr <- matrix(rnorm(n_samples * n_genes), nrow = n_samples, ncol = n_genes)
# 添加分组效应
gene_expr[1:50, 1:10] <- gene_expr[1:50, 1:10] + 2 # 第一组样本的前10个基因高表达
gene_expr[51:100, 11:20] <- gene_expr[51:100, 11:20] + 2 # 第二组样本的中间基因高表达
# 转换为数据框
gene_df <- as.data.frame(gene_expr)
colnames(gene_df) <- paste0("Gene_", 1:n_genes)
# 执行PCA
pca_result <- prcomp(gene_df, scale. = TRUE, center = TRUE)
# 查看主成分的方差贡献
var_explained <- pca_result$sdev^2 / sum(pca_result$sdev^2)
var_df <- data.frame(
PC = paste0("PC", 1:10),
Variance = var_explained[1:10] * 100,
Cumulative = cumsum(var_explained[1:10]) * 100
)
print("前10个主成分的方差贡献:")
## [1] "前10个主成分的方差贡献:"
print(head(var_df, 10))
## PC Variance Cumulative
## 1 PC1 21.864839 21.86484
## 2 PC2 4.922180 26.78702
## 3 PC3 4.356587 31.14361
## 4 PC4 4.213363 35.35697
## 5 PC5 3.772029 39.12900
## 6 PC6 3.487178 42.61618
## 7 PC7 3.416547 46.03272
## 8 PC8 3.143081 49.17580
## 9 PC9 3.090488 52.26629
## 10 PC10 2.841843 55.10813
# 碎石图:方差贡献
p1 <- ggplot2::ggplot(var_df, ggplot2::aes(x = PC, y = Variance)) +
ggplot2::geom_bar(stat = "identity", fill = "steelblue") +
ggplot2::geom_line(ggplot2::aes(group = 1), color = "red") +
ggplot2::labs(
title = "碎石图:各主成分方差贡献",
x = "主成分", y = "方差贡献(%)"
) +
ggplot2::theme_minimal()
# 累积方差贡献
p2 <- ggplot2::ggplot(var_df, ggplot2::aes(x = PC, y = Cumulative)) +
ggplot2::geom_line(color = "steelblue", linewidth = 1) +
ggplot2::geom_point(color = "steelblue", size = 2) +
ggplot2::geom_hline(yintercept = 80, linetype = "dashed", color = "red") +
ggplot2::labs(
title = "累积方差贡献",
x = "主成分", y = "累积方差贡献(%)"
) +
ggplot2::theme_minimal()
p1 + p2 + patchwork::plot_annotation(
title = "PCA方差分析"
)
# 添加分组信息
gene_df$group <- factor(rep(c("Group_A", "Group_B"), each = 50))
# 创建PCA得分数据框
pca_scores <- as.data.frame(pca_result$x[, 1:2])
pca_scores$group <- gene_df$group
# 绘制PCA得分图
ggplot2::ggplot(pca_scores, ggplot2::aes(x = PC1, y = PC2)) +
ggplot2::geom_point(ggplot2::aes(color = group), size = 3, alpha = 0.7) +
ggplot2::labs(
title = "PCA得分图:样本在前两个主成分上的分布",
subtitle = paste0("PC1解释", round(var_explained[1] * 100, 1),
"%方差,PC2解释", round(var_explained[2] * 100, 1), "%方差"),
x = paste0("PC1 (", round(var_explained[1] * 100, 1), "%)"),
y = paste0("PC2 (", round(var_explained[2] * 100, 1), "%)"),
color = "分组"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
核心意义:PCA是最常用的降维方法,能够减少数据维度、去除噪声、可视化高维数据。
举例:基因表达谱可视化
为什么这种数据适合用PCA?
基因表达数据通常有数千个基因(高维),但样本数相对较少。PCA可以将高维数据降维到2-3维,便于可视化和发现样本分组模式。
研究目的是什么?
可能得到什么样的结果?
PCA可能揭示样本在PC1-PC2平面上的自然分组,不同疾病类型的患者可能形成明显的簇。
需要注意什么?
PCA是线性方法,对于非线性结构可能效果不好。主成分的解释性可能较差,需要结合载荷矩阵分析。
t-SNE(t-Distributed Stochastic Neighbor Embedding) 是一种非线性降维方法,特别适合高维数据的可视化。
核心思想: 1. 在高维空间计算样本间的相似度(高斯分布) 2. 在低维空间计算样本间的相似度(t分布) 3. 最小化两个相似度分布的KL散度
# t-SNE示例
set.seed(42)
# 使用Rtsne包进行t-SNE降维
library(Rtsne)
# 对基因表达数据进行t-SNE
tsne_result <- Rtsne::Rtsne(gene_df[, 1:n_genes],
dims = 2,
perplexity = 30,
verbose = FALSE)
# 创建结果数据框
tsne_df <- as.data.frame(tsne_result$Y)
colnames(tsne_df) <- c("tSNE1", "tSNE2")
tsne_df$group <- gene_df$group
# 可视化t-SNE结果
ggplot2::ggplot(tsne_df, ggplot2::aes(x = tSNE1, y = tSNE2)) +
ggplot2::geom_point(ggplot2::aes(color = group), size = 3, alpha = 0.7) +
ggplot2::labs(
title = "t-SNE降维可视化",
subtitle = "非线性降维,能够保持局部结构",
x = "t-SNE维度1", y = "t-SNE维度2", color = "分组"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
UMAP(Uniform Manifold Approximation and Projection) 是另一种流行的非线性降维方法,比t-SNE更快,且能更好地保持全局结构。
# UMAP示例
library(umap)
# 执行UMAP降维
umap_result <- umap::umap(gene_df[, 1:n_genes])
# 创建结果数据框
umap_df <- as.data.frame(umap_result$layout)
colnames(umap_df) <- c("UMAP1", "UMAP2")
umap_df$group <- gene_df$group
# 可视化UMAP结果
ggplot2::ggplot(umap_df, ggplot2::aes(x = UMAP1, y = UMAP2)) +
ggplot2::geom_point(ggplot2::aes(color = group), size = 3, alpha = 0.7) +
ggplot2::labs(
title = "UMAP降维可视化",
subtitle = "比t-SNE更快,保持全局结构更好",
x = "UMAP维度1", y = "UMAP维度2", color = "分组"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
# 三种方法的对比可视化
p_pca <- ggplot2::ggplot(pca_scores, ggplot2::aes(x = PC1, y = PC2)) +
ggplot2::geom_point(ggplot2::aes(color = group), size = 2, alpha = 0.7) +
ggplot2::labs(title = "PCA", x = "", y = "") +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "none")
p_tsne <- ggplot2::ggplot(tsne_df, ggplot2::aes(x = tSNE1, y = tSNE2)) +
ggplot2::geom_point(ggplot2::aes(color = group), size = 2, alpha = 0.7) +
ggplot2::labs(title = "t-SNE", x = "", y = "") +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "none")
p_umap <- ggplot2::ggplot(umap_df, ggplot2::aes(x = UMAP1, y = UMAP2)) +
ggplot2::geom_point(ggplot2::aes(color = group), size = 2, alpha = 0.7) +
ggplot2::labs(title = "UMAP", x = "", y = "") +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "none")
p_pca + p_tsne + p_umap + patchwork::plot_annotation(
title = "降维方法对比:PCA vs t-SNE vs UMAP"
)
核心意义:非线性降维方法能够揭示高维数据的复杂结构,是数据探索和可视化的有力工具。
举例:单细胞RNA测序数据分析
为什么这种数据适合用t-SNE/UMAP?
单细胞RNA测序数据通常有数千个基因,但每个细胞只表达其中一部分。数据具有高度非线性的结构,传统线性方法难以捕捉。t-SNE和UMAP能够揭示细胞亚群和分化轨迹。
研究目的是什么?
可能得到什么样的结果?
t-SNE/UMAP图可能显示多个明显的细胞簇,对应不同的细胞类型或状态。
需要注意什么?
t-SNE和UMAP的结果受参数影响较大,不同运行可能得到不同结果。距离和簇大小在图中可能不反映真实情况,需要结合其他方法验证。
自编码器(Autoencoder) 是一种神经网络架构,用于学习数据的有效压缩表示:
结构: - 编码器:将输入压缩到低维潜在空间 - 解码器:从潜在表示重建输入
目标:最小化重建误差 \(\|x - \hat{x}\|^2\)
输入层 → 编码层 → 潜在层(低维) → 解码层 → 输出层
原理:异常点更容易被孤立(需要更少的分割即可分离)
# 孤立森林示例
set.seed(42)
# 生成正常数据和异常数据
normal_data <- data.frame(
feature1 = rnorm(200, mean = 0, sd = 1),
feature2 = rnorm(200, mean = 0, sd = 1),
type = "正常"
)
# 添加异常点
anomaly_data <- data.frame(
feature1 = c(rnorm(10, mean = 5, sd = 0.5), rnorm(5, mean = -4, sd = 0.5)),
feature2 = c(rnorm(10, mean = 5, sd = 0.5), rnorm(5, mean = -4, sd = 0.5)),
type = "异常"
)
# 合并数据
all_data <- rbind(normal_data, anomaly_data)
# 使用isotree包实现孤立森林
library(isotree)
iso_forest <- isotree::isolation.forest(all_data[, c("feature1", "feature2")])
# 预测异常分数
all_data$anomaly_score <- predict(iso_forest, all_data[, c("feature1", "feature2")])
# 可视化异常分数
ggplot2::ggplot(all_data, ggplot2::aes(x = feature1, y = feature2)) +
ggplot2::geom_point(ggplot2::aes(color = anomaly_score), size = 2) +
ggplot2::scale_color_gradient(low = "blue", high = "red") +
ggplot2::labs(
title = "孤立森林异常检测",
subtitle = "红色点表示高异常分数",
x = "特征1", y = "特征2", color = "异常分数"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
原理:比较样本的局部密度与邻居的局部密度
# LOF异常检测
library(dbscan)
# 计算LOF分数
lof_scores <- dbscan::lof(all_data[, c("feature1", "feature2")], k = 5)
# 添加LOF分数到数据框
all_data$lof_score <- lof_scores
# 可视化LOF分数
ggplot2::ggplot(all_data, ggplot2::aes(x = feature1, y = feature2)) +
ggplot2::geom_point(ggplot2::aes(color = lof_score), size = 2) +
ggplot2::scale_color_gradient(low = "blue", high = "red") +
ggplot2::labs(
title = "LOF异常检测",
subtitle = "LOF > 1表示密度低于邻居(可能是异常)",
x = "特征1", y = "特征2", color = "LOF分数"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
原理:学习正常数据的边界,边界外的点为异常
# 单类SVM示例
library(e1071)
# 训练单类SVM(只使用正常数据)
ocsvm_model <- e1071::svm(
x = as.matrix(normal_data[, c("feature1", "feature2")]),
type = "one-classification",
nu = 0.05, # 预期异常比例
kernel = "radial"
)
# 预测
all_data$ocsvm_pred <- predict(ocsvm_model, all_data[, c("feature1", "feature2")])
# 可视化
ggplot2::ggplot(all_data, ggplot2::aes(x = feature1, y = feature2)) +
ggplot2::geom_point(ggplot2::aes(color = ocsvm_pred), size = 2) +
ggplot2::scale_color_manual(values = c("FALSE" = "red", "TRUE" = "blue"),
labels = c("FALSE" = "异常", "TRUE" = "正常")) +
ggplot2::labs(
title = "单类SVM异常检测",
x = "特征1", y = "特征2", color = "预测结果"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
关联规则(Association Rules) 发现数据项之间的有趣关联关系,最著名的例子是”啤酒与尿布”的购物篮分析。
基本概念:
| 概念 | 定义 | 说明 |
|---|---|---|
| 项集 | 项的集合 | {A, B, C} |
| 支持度 | 项集出现的频率 | P(A∩B) |
| 置信度 | 条件概率 | P(B |
| 提升度 | 规则的有效性 | P(B |
支持度(Support):
\[\text{Support}(A \rightarrow B) = \frac{|A \cap B|}{|D|}\]
表示A和B同时出现的频率。
置信度(Confidence):
\[\text{Confidence}(A \rightarrow B) = \frac{\text{Support}(A \cup B)}{\text{Support}(A)}\]
表示在A出现的条件下B出现的概率。
提升度(Lift):
\[\text{Lift}(A \rightarrow B) = \frac{\text{Confidence}(A \rightarrow B)}{\text{Support}(B)}\]
# 关联规则示例:医学共病分析
# 创建模拟的疾病共病数据
set.seed(42)
# 假设有1000个患者的诊断记录
n_patients <- 1000
# 创建疾病诊断矩阵(患者 × 疾病)
diseases <- c("高血压", "糖尿病", "冠心病", "高血脂", "肥胖", "肾病")
# 生成模拟数据:某些疾病倾向于同时出现
diagnosis_data <- matrix(0, nrow = n_patients, ncol = length(diseases))
colnames(diagnosis_data) <- diseases
# 模拟疾病关联
for (i in 1:n_patients) {
# 基础概率
diagnosis_data[i, "高血压"] <- rbinom(1, 1, 0.3)
diagnosis_data[i, "糖尿病"] <- rbinom(1, 1, 0.2)
# 高血压患者更容易有冠心病
if (diagnosis_data[i, "高血压"] == 1) {
diagnosis_data[i, "冠心病"] <- rbinom(1, 1, 0.4)
} else {
diagnosis_data[i, "冠心病"] <- rbinom(1, 1, 0.1)
}
# 糖尿病患者更容易有肾病
if (diagnosis_data[i, "糖尿病"] == 1) {
diagnosis_data[i, "肾病"] <- rbinom(1, 1, 0.3)
} else {
diagnosis_data[i, "肾病"] <- rbinom(1, 1, 0.05)
}
# 其他疾病
diagnosis_data[i, "高血脂"] <- rbinom(1, 1, 0.25)
diagnosis_data[i, "肥胖"] <- rbinom(1, 1, 0.2)
}
# 转换为数据框
diagnosis_df <- as.data.frame(diagnosis_data)
# 查看数据结构
head(diagnosis_df)
## 高血压 糖尿病 冠心病 高血脂 肥胖 肾病
## 1 1 1 0 0 0 1
## 2 1 0 1 0 0 0
## 3 1 0 0 1 0 0
## 4 0 0 1 1 1 0
## 5 0 0 0 0 1 0
## 6 1 1 0 0 1 0
# 计算各疾病的患病率(支持度)
disease_support <- colMeans(diagnosis_df)
print("各疾病患病率:")
## [1] "各疾病患病率:"
print(round(disease_support, 3))
## 高血压 糖尿病 冠心病 高血脂 肥胖 肾病
## 0.301 0.227 0.183 0.256 0.214 0.122
Apriori算法 是最经典的关联规则挖掘算法,基于频繁项集的先验性质:
核心思想:如果某个项集是频繁的,那么它的所有子集也是频繁的。
算法步骤: 1. 扫描数据库,找出频繁1-项集(L1) 2. 由L1生成候选2-项集,找出频繁2-项集(L2) 3. 重复直到无法生成更大的频繁项集 4. 由频繁项集生成关联规则
# 使用arules包进行关联规则挖掘
library(arules)
# 将数据转换为transactions格式
# 首先将数据转换为适合arules的格式
# 创建交易数据:每个患者的疾病列表
transactions_list <- apply(diagnosis_df, 1, function(x) {
diseases[x == 1]
})
# 转换为transactions对象
trans <- as(transactions_list, "transactions")
# 查看事务摘要
summary(trans)
## transactions as itemMatrix in sparse format with
## 1000 rows (elements/itemsets/transactions) and
## 6 columns (items) and a density of 0.2171667
##
## most frequent items:
## 高血压 高血脂 糖尿病 肥胖 冠心病 (Other)
## 301 256 227 214 183 122
##
## element (itemset/transaction) length distribution:
## sizes
## 0 1 2 3 4 5
## 258 366 232 107 33 4
##
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.000 0.000 1.000 1.303 2.000 5.000
##
## includes extended item information - examples:
## labels
## 1 肥胖
## 2 高血压
## 3 高血脂
# 执行Apriori算法
rules <- arules::apriori(trans,
parameter = list(
support = 0.05, # 最小支持度
confidence = 0.5, # 最小置信度
minlen = 2, # 规则最小长度
maxlen = 4 # 规则最大长度
))
## Apriori
##
## Parameter specification:
## confidence minval smax arem aval originalSupport maxtime support minlen
## 0.5 0.1 1 none FALSE TRUE 5 0.05 2
## maxlen target ext
## 4 rules TRUE
##
## Algorithmic control:
## filter tree heap memopt load sort verbose
## 0.1 TRUE TRUE FALSE TRUE 2 TRUE
##
## Absolute minimum support count: 50
##
## set item appearances ...[0 item(s)] done [0.00s].
## set transactions ...[6 item(s), 1000 transaction(s)] done [0.00s].
## sorting and recoding items ... [6 item(s)] done [0.00s].
## creating transaction tree ... done [0.00s].
## checking subsets of size 1 2 3 done [0.00s].
## writing ... [2 rule(s)] done [0.00s].
## creating S4 object ... done [0.00s].
# 查看规则摘要
summary(rules)
## set of 2 rules
##
## rule length distribution (lhs + rhs):sizes
## 2
## 2
##
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 2 2 2 2 2 2
##
## summary of quality measures:
## support confidence coverage lift
## Min. :0.0810 Min. :0.5847 Min. :0.1220 Min. :1.943
## 1st Qu.:0.0875 1st Qu.:0.6045 1st Qu.:0.1373 1st Qu.:2.188
## Median :0.0940 Median :0.6243 Median :0.1525 Median :2.434
## Mean :0.0940 Mean :0.6243 Mean :0.1525 Mean :2.434
## 3rd Qu.:0.1005 3rd Qu.:0.6441 3rd Qu.:0.1678 3rd Qu.:2.679
## Max. :0.1070 Max. :0.6639 Max. :0.1830 Max. :2.925
## count
## Min. : 81.0
## 1st Qu.: 87.5
## Median : 94.0
## Mean : 94.0
## 3rd Qu.:100.5
## Max. :107.0
##
## mining info:
## data ntransactions support confidence
## trans 1000 0.05 0.5
## call
## arules::apriori(data = trans, parameter = list(support = 0.05, confidence = 0.5, minlen = 2, maxlen = 4))
# 查看前10条规则
arules::inspect(head(rules, 10))
## lhs rhs support confidence coverage lift count
## [1] {肾病} => {糖尿病} 0.081 0.6639344 0.122 2.924821 81
## [2] {冠心病} => {高血压} 0.107 0.5846995 0.183 1.942523 107
# 按提升度排序
rules_by_lift <- sort(rules, by = "lift", decreasing = TRUE)
# 查看提升度最高的规则
print("提升度最高的10条规则:")
## [1] "提升度最高的10条规则:"
arules::inspect(head(rules_by_lift, 10))
## lhs rhs support confidence coverage lift count
## [1] {肾病} => {糖尿病} 0.081 0.6639344 0.122 2.924821 81
## [2] {冠心病} => {高血压} 0.107 0.5846995 0.183 1.942523 107
# 筛选特定规则的子集
# 例如:找出所有涉及"糖尿病"的规则
diabetes_rules <- subset(rules, subset = items %in% "糖尿病")
print("涉及糖尿病的规则:")
## [1] "涉及糖尿病的规则:"
arules::inspect(head(diabetes_rules, 5))
## lhs rhs support confidence coverage lift count
## [1] {肾病} => {糖尿病} 0.081 0.6639344 0.122 2.924821 81
library(arulesViz)
# 方法1:散点图
plot(rules, measure = c("support", "confidence"), shading = "lift",
main = "关联规则散点图\n颜色深浅表示提升度")
# 方法2:网络图(显示规则结构)
plot(head(rules_by_lift, 20), method = "graph",
main = "关联规则网络图(前20条)")
## Available control parameters (with default values):
## layout = stress
## circular = FALSE
## ggraphdots = NULL
## edges = <environment>
## nodes = <environment>
## nodetext = <environment>
## colors = c("#EE0000FF", "#EEEEEEFF")
## engine = ggplot2
## max = 100
## verbose = FALSE
# 方法3:平行坐标图
plot(head(rules_by_lift, 15), method = "paracoord",
main = "关联规则平行坐标图")
核心意义:Apriori算法能够高效发现数据中的频繁模式和关联规则。
举例:药物相互作用发现
为什么这种数据适合用Apriori?
药物不良反应数据中,某些药物组合可能导致特定的不良反应。Apriori可以发现这些药物-不良反应的关联模式。
研究目的是什么?
可能得到什么样的结果?
可能发现”药物A + 药物B → 不良反应C”的规则,提示这两种药物联用需要谨慎。
需要注意什么?
Apriori需要多次扫描数据库,对大规模数据效率较低。最小支持度和置信度的设置对结果影响很大。
FP-Growth(Frequent Pattern Growth) 是Apriori的改进算法,不需要生成候选项集:
核心思想:使用FP树(Frequent Pattern Tree)压缩存储数据,直接从FP树挖掘频繁项集。
优点: 1. 只需扫描数据库两次 2. 不生成候选项集 3. 效率比Apriori高
缺点: 1. FP树可能占用大量内存 2. 实现较复杂
# FP-Growth在R中通过rCBA包实现
# 这里使用arules包的eclat函数作为替代(类似效果)
# 使用eclat算法挖掘频繁项集
frequent_itemsets <- arules::eclat(trans,
parameter = list(
support = 0.05,
minlen = 2,
maxlen = 4
))
## Eclat
##
## parameter specification:
## tidLists support minlen maxlen target ext
## FALSE 0.05 2 4 frequent itemsets TRUE
##
## algorithmic control:
## sparse sort verbose
## 7 -2 TRUE
##
## Absolute minimum support count: 50
##
## create itemset ...
## set transactions ...[6 item(s), 1000 transaction(s)] done [0.00s].
## sorting and recoding items ... [6 item(s)] done [0.00s].
## creating bit matrix ... [6 row(s), 1000 column(s)] done [0.00s].
## writing ... [7 set(s)] done [0.00s].
## Creating S4 object ... done [0.00s].
# 查看频繁项集
print("频繁项集:")
## [1] "频繁项集:"
arules::inspect(head(sort(frequent_itemsets, by = "support"), 10))
## items support count
## [1] {高血压, 冠心病} 0.107 107
## [2] {肾病, 糖尿病} 0.081 81
## [3] {高血压, 糖尿病} 0.077 77
## [4] {高血压, 高血脂} 0.076 76
## [5] {肥胖, 高血脂} 0.056 56
## [6] {肥胖, 高血压} 0.053 53
## [7] {高血脂, 糖尿病} 0.050 50
# 从频繁项集生成规则
rules_from_fp <- arules::ruleInduction(frequent_itemsets, trans,
confidence = 0.5)
# 查看生成的规则
print("从频繁项集生成的规则:")
## [1] "从频繁项集生成的规则:"
arules::inspect(head(sort(rules_from_fp, by = "lift"), 10))
## lhs rhs support confidence lift itemset
## [1] {肾病} => {糖尿病} 0.081 0.6639344 2.924821 1
## [2] {冠心病} => {高血压} 0.107 0.5846995 1.942523 4
| 类型 | 定义 | 示例 |
|---|---|---|
| 频繁项集 | 支持度≥阈值的项集 | {高血压, 糖尿病} |
| 闭频繁项集 | 不存在超集有相同支持度 | {高血压, 糖尿病}支持度=0.1,无超集支持度=0.1 |
| 最大频繁项集 | 不存在频繁超集 | {高血压, 糖尿病, 冠心病} |
# 挖掘不同类型的频繁项集
# 普通频繁项集
freq_items <- arules::eclat(trans, parameter = list(support = 0.05))
## Eclat
##
## parameter specification:
## tidLists support minlen maxlen target ext
## FALSE 0.05 1 10 frequent itemsets TRUE
##
## algorithmic control:
## sparse sort verbose
## 7 -2 TRUE
##
## Absolute minimum support count: 50
##
## create itemset ...
## set transactions ...[6 item(s), 1000 transaction(s)] done [0.00s].
## sorting and recoding items ... [6 item(s)] done [0.00s].
## creating bit matrix ... [6 row(s), 1000 column(s)] done [0.00s].
## writing ... [13 set(s)] done [0.00s].
## Creating S4 object ... done [0.00s].
print(paste("普通频繁项集数量:", length(freq_items)))
## [1] "普通频繁项集数量: 13"
# 闭频繁项集
closed_items <- arules::eclat(trans, parameter = list(support = 0.05,
target = "closed frequent itemsets"))
## Eclat
##
## parameter specification:
## tidLists support minlen maxlen target ext
## FALSE 0.05 1 10 closed frequent itemsets TRUE
##
## algorithmic control:
## sparse sort verbose
## 7 -2 TRUE
##
## Absolute minimum support count: 50
##
## create itemset ...
## set transactions ...[6 item(s), 1000 transaction(s)] done [0.00s].
## sorting and recoding items ... [6 item(s)] done [0.00s].
## creating bit matrix ... [6 row(s), 1000 column(s)] done [0.00s].
## writing ... [13 set(s)] done [0.00s].
## Creating S4 object ... done [0.00s].
print(paste("闭频繁项集数量:", length(closed_items)))
## [1] "闭频繁项集数量: 13"
# 最大频繁项集
maximal_items <- arules::eclat(trans, parameter = list(support = 0.05,
target = "maximally frequent itemsets"))
## Eclat
##
## parameter specification:
## tidLists support minlen maxlen target ext
## FALSE 0.05 1 10 maximally frequent itemsets TRUE
##
## algorithmic control:
## sparse sort verbose
## 7 -2 TRUE
##
## Absolute minimum support count: 50
##
## create itemset ...
## set transactions ...[6 item(s), 1000 transaction(s)] done [0.00s].
## sorting and recoding items ... [6 item(s)] done [0.00s].
## creating bit matrix ... [6 row(s), 1000 column(s)] done [0.00s].
## writing ... [7 set(s)] done [0.00s].
## Creating S4 object ... done [0.00s].
print(paste("最大频繁项集数量:", length(maximal_items)))
## [1] "最大频繁项集数量: 7"
# 可视化对比
itemset_counts <- data.frame(
类型 = c("普通频繁项集", "闭频繁项集", "最大频繁项集"),
数量 = c(length(freq_items), length(closed_items), length(maximal_items))
)
ggplot2::ggplot(itemset_counts, ggplot2::aes(x = 类型, y = 数量)) +
ggplot2::geom_bar(stat = "identity", fill = "steelblue") +
ggplot2::geom_text(ggplot2::aes(label = 数量), vjust = -0.5) +
ggplot2::labs(
title = "不同类型频繁项集数量对比",
subtitle = "闭频繁项集和最大频繁项集是压缩表示",
x = "项集类型", y = "数量"
) +
ggplot2::theme_minimal()
序列模式(Sequential Pattern) 发现事件发生的时间顺序规律:
示例: - 购物序列:{牛奶} → {面包} → {鸡蛋} - 疾病发展:{高血压} → {冠心病} → {心衰}
与关联规则的区别: - 关联规则:发现同时出现的关系 - 序列模式:发现先后出现的关系
# 序列模式挖掘示例
library(arulesSequences)
# 创建模拟的疾病发展序列数据
# 假设跟踪患者的诊断历史
set.seed(42)
n_patients_seq <- 100
# 创建序列数据框
seq_data <- data.frame(
sequenceID = rep(1:n_patients_seq, each = 3), # 患者ID
eventID = rep(1:3, n_patients_seq), # 时间点
items = "" # 诊断
)
# 生成模拟序列
for (i in 1:n_patients_seq) {
idx <- ((i-1)*3 + 1):(i*3)
# 时间点1:初始诊断
initial <- sample(c("高血压", "糖尿病", "高血脂"), 1)
seq_data$items[idx[1]] <- initial
# 时间点2:进展(与初始诊断相关)
if (initial == "高血压") {
seq_data$items[idx[2]] <- sample(c("冠心病", "肾病", "无进展"), 1,
prob = c(0.4, 0.2, 0.4))
} else if (initial == "糖尿病") {
seq_data$items[idx[2]] <- sample(c("肾病", "视网膜病变", "无进展"), 1,
prob = c(0.3, 0.2, 0.5))
} else {
seq_data$items[idx[2]] <- sample(c("冠心病", "无进展"), 1,
prob = c(0.3, 0.7))
}
# 时间点3:进一步进展
seq_data$items[idx[3]] <- sample(c("心衰", "脑卒中", "终末期肾病", "稳定"), 1,
prob = c(0.2, 0.2, 0.1, 0.5))
}
# 转换为transactions格式
seq_trans <- as(seq_data, "transactions")
# 查看序列数据结构
print("序列数据示例:")
## [1] "序列数据示例:"
head(seq_data, 9)
## sequenceID eventID items
## 1 1 1 高血压
## 2 1 2 肾病
## 3 1 3 稳定
## 4 2 1 高血压
## 5 2 2 无进展
## 6 2 3 心衰
## 7 3 1 糖尿病
## 8 3 2 无进展
## 9 3 3 心衰
# 使用cSPADE算法挖掘序列模式
# 注意:需要正确格式的数据
# 简单示例:计算序列支持度
seq_support <- seq_data %>%
dplyr::group_by(sequenceID) %>%
dplyr::summarise(
seq = paste(items, collapse = " → "),
.groups = "drop"
)
# 统计常见序列
seq_freq <- seq_support %>%
dplyr::count(seq, sort = TRUE)
print("最常见的疾病发展序列:")
## [1] "最常见的疾病发展序列:"
head(seq_freq, 10)
## # A tibble: 10 × 2
## seq n
## <chr> <int>
## 1 高血脂 → 无进展 → 稳定 13
## 2 糖尿病 → 无进展 → 稳定 8
## 3 高血压 → 冠心病 → 稳定 7
## 4 高血脂 → 无进展 → 脑卒中 6
## 5 糖尿病 → 肾病 → 稳定 5
## 6 高血压 → 肾病 → 稳定 5
## 7 糖尿病 → 无进展 → 心衰 4
## 8 糖尿病 → 肾病 → 脑卒中 4
## 9 高血压 → 无进展 → 稳定 4
## 10 高血脂 → 冠心病 → 稳定 4
# 可视化
ggplot2::ggplot(head(seq_freq, 10), ggplot2::aes(x = reorder(seq, n), y = n)) +
ggplot2::geom_bar(stat = "identity", fill = "steelblue") +
ggplot2::coord_flip() +
ggplot2::labs(
title = "最常见的疾病发展序列",
x = "序列", y = "频次"
) +
ggplot2::theme_minimal()
特征选择(Feature Selection) 从原始特征中选择最相关的子集,目的是:
| 目的 | 说明 |
|---|---|
| 降低维度 | 减少计算复杂度 |
| 提高性能 | 去除噪声特征,提高模型性能 |
| 防止过拟合 | 减少特征数量,降低模型复杂度 |
| 提高可解释性 | 特征更少,模型更易理解 |
原理:删除方差过小的特征(变化不大的特征信息量小)。
# 方差阈值示例
set.seed(42)
# 创建模拟数据:100个样本,20个特征
n <- 100
p <- 20
filter_data <- matrix(rnorm(n * p), nrow = n, ncol = p)
colnames(filter_data) <- paste0("Feature_", 1:p)
# 让某些特征方差很小
filter_data[, 1] <- rnorm(n, mean = 5, sd = 0.01) # 低方差
filter_data[, 2] <- rnorm(n, mean = 5, sd = 0.02) # 低方差
filter_df <- as.data.frame(filter_data)
# 计算各特征的方差
variances <- apply(filter_df, 2, var)
# 创建方差数据框
var_df <- data.frame(
Feature = names(variances),
Variance = variances
)
# 可视化方差分布
ggplot2::ggplot(var_df, ggplot2::aes(x = reorder(Feature, Variance), y = Variance)) +
ggplot2::geom_bar(stat = "identity", fill = "steelblue") +
ggplot2::geom_hline(yintercept = 0.1, color = "red", linetype = "dashed") +
ggplot2::coord_flip() +
ggplot2::labs(
title = "各特征方差分布",
subtitle = "红色虚线为阈值,低于阈值的特征将被删除",
x = "特征", y = "方差"
) +
ggplot2::theme_minimal()
# 选择方差大于阈值的特征
threshold <- 0.1
selected_features <- names(variances[variances > threshold])
print(paste("方差阈值筛选后保留的特征数:", length(selected_features)))
## [1] "方差阈值筛选后保留的特征数: 18"
原理:删除与目标变量相关性低的特征。
# 相关系数筛选示例
set.seed(42)
# 创建带有标签的数据
n <- 100
corr_data <- data.frame(
feature1 = rnorm(n),
feature2 = rnorm(n),
feature3 = rnorm(n), # 与目标无关
feature4 = rnorm(n), # 与目标无关
target = rnorm(n)
)
# 让某些特征与目标相关
corr_data$feature1 <- corr_data$target + rnorm(n, 0, 0.5)
corr_data$feature2 <- corr_data$target * 0.8 + rnorm(n, 0, 0.8)
# 计算与目标的相关系数
correlations <- sapply(corr_data[, 1:4], function(x) cor(x, corr_data$target))
# 创建相关系数数据框
cor_df <- data.frame(
Feature = names(correlations),
Correlation = abs(correlations)
)
# 可视化
ggplot2::ggplot(cor_df, ggplot2::aes(x = reorder(Feature, Correlation), y = Correlation)) +
ggplot2::geom_bar(stat = "identity", fill = "steelblue") +
ggplot2::geom_hline(yintercept = 0.3, color = "red", linetype = "dashed") +
ggplot2::coord_flip() +
ggplot2::labs(
title = "特征与目标变量的相关系数",
subtitle = "红色虚线为阈值",
x = "特征", y = "绝对相关系数"
) +
ggplot2::theme_minimal()
print("各特征与目标的相关系数:")
## [1] "各特征与目标的相关系数:"
print(round(correlations, 3))
## feature1 feature2 feature3 feature4
## 0.902 0.718 0.059 0.185
原理:衡量特征与目标变量之间的信息共享量。
# 互信息示例
# 使用caret包计算特征重要性
set.seed(42)
# 创建分类数据
n <- 200
mi_data <- data.frame(
feature1 = c(rnorm(n/2, 0, 1), rnorm(n/2, 2, 1)),
feature2 = c(rnorm(n/2, 0, 1), rnorm(n/2, 2, 1)),
feature3 = rnorm(n), # 无信息
feature4 = rnorm(n), # 无信息
target = factor(rep(c("ClassA", "ClassB"), each = n/2))
)
# 使用caret计算特征重要性
# 设置训练控制
ctrl <- caret::trainControl(method = "cv", number = 5)
# 训练简单模型获取重要性
model_imp <- caret::train(target ~ ., data = mi_data,
method = "rf",
trControl = ctrl,
importance = TRUE,
ntree = 50)
# 获取特征重要性
importance_scores <- caret::varImp(model_imp)
# 可视化
ggplot2::ggplot(importance_scores, ggplot2::aes(x = reorder(rownames(importance_scores), Overall), y = Overall)) +
ggplot2::geom_bar(stat = "identity", fill = "steelblue") +
ggplot2::coord_flip() +
ggplot2::labs(
title = "特征重要性(基于随机森林)",
x = "特征", y = "重要性分数"
) +
ggplot2::theme_minimal()
原理:检验类别特征与目标变量之间的独立性。
# 卡方检验示例
# 创建类别数据
set.seed(42)
n <- 200
chi_data <- data.frame(
symptom1 = sample(c("有", "无"), n, replace = TRUE, prob = c(0.3, 0.7)),
symptom2 = sample(c("有", "无"), n, replace = TRUE, prob = c(0.4, 0.6)),
disease = sample(c("患病", "健康"), n, replace = TRUE, prob = c(0.2, 0.8))
)
# 让symptom1与疾病相关
chi_data$symptom1[chi_data$disease == "患病"] <- sample(c("有", "无"),
sum(chi_data$disease == "患病"),
replace = TRUE,
prob = c(0.7, 0.3))
# 计算卡方检验
chi_test1 <- chisq.test(table(chi_data$symptom1, chi_data$disease))
chi_test2 <- chisq.test(table(chi_data$symptom2, chi_data$disease))
# 汇总结果
chi_results <- data.frame(
Feature = c("symptom1", "symptom2"),
ChiSquare = c(chi_test1$statistic, chi_test2$statistic),
PValue = c(chi_test1$p.value, chi_test2$p.value)
)
print("卡方检验结果:")
## [1] "卡方检验结果:"
print(chi_results)
## Feature ChiSquare PValue
## 1 symptom1 23.004376576 1.616330e-06
## 2 symptom2 0.006823627 9.341654e-01
原理:反复构建模型,每次删除最不重要的特征。
# RFE示例
set.seed(42)
# 创建模拟数据
n <- 150
p <- 20
rfe_data <- matrix(rnorm(n * p), nrow = n, ncol = p)
colnames(rfe_data) <- paste0("Feature_", 1:p)
# 让前5个特征与目标相关
target <- rfe_data[, 1] + rfe_data[, 2] + rfe_data[, 3] +
rnorm(n, 0, 0.5)
target <- factor(ifelse(target > median(target), "High", "Low"))
rfe_df <- as.data.frame(rfe_data)
rfe_df$target <- target
# 使用caret的RFE
# 定义控制参数
rfe_ctrl <- caret::rfeControl(
functions = caret::rfFuncs, # 使用随机森林
method = "cv",
number = 5
)
# 执行RFE
rfe_result <- caret::rfe(
x = rfe_df[, 1:p],
y = rfe_df$target,
sizes = c(1:10, 15, 20),
rfeControl = rfe_ctrl
)
# 查看结果
print("RFE结果:")
## [1] "RFE结果:"
print(rfe_result)
##
## Recursive feature selection
##
## Outer resampling method: Cross-Validated (5 fold)
##
## Resampling performance over subset size:
##
## Variables Accuracy Kappa AccuracySD KappaSD Selected
## 1 0.6933 0.3867 0.07601 0.15202
## 2 0.7133 0.4267 0.05055 0.10111
## 3 0.8067 0.6133 0.04944 0.09888 *
## 4 0.7800 0.5600 0.07303 0.14606
## 5 0.7733 0.5467 0.09545 0.19090
## 6 0.7600 0.5200 0.06412 0.12824
## 7 0.7733 0.5467 0.02789 0.05578
## 8 0.7800 0.5600 0.05055 0.10111
## 9 0.7600 0.5200 0.04346 0.08692
## 10 0.7733 0.5467 0.04944 0.09888
## 15 0.7867 0.5733 0.06912 0.13824
## 20 0.7733 0.5467 0.10382 0.20763
##
## The top 3 variables (out of 3):
## Feature_1, Feature_2, Feature_3
# 可视化
plot(rfe_result, main = "递归特征消除结果")
前向选择:从空集开始,逐步添加特征 后向消除:从全集开始,逐步删除特征
# 使用step函数进行逐步回归
set.seed(42)
# 创建线性回归数据
n <- 100
step_data <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n), # 噪声
x4 = rnorm(n), # 噪声
y = rnorm(n)
)
# 让y与x1, x2相关
step_data$y <- 2 * step_data$x1 - 1.5 * step_data$x2 + rnorm(n, 0, 0.5)
# 全模型
full_model <- lm(y ~ x1 + x2 + x3 + x4, data = step_data)
# 后向消除
backward_model <- step(full_model, direction = "backward", trace = 0)
# 查看最终模型
print("后向消除后的模型:")
## [1] "后向消除后的模型:"
summary(backward_model)
##
## Call:
## lm(formula = y ~ x1 + x2, data = step_data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.93302 -0.36500 0.02268 0.34716 1.65764
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -0.008394 0.052881 -0.159 0.874
## x1 1.981572 0.050792 39.013 <2e-16 ***
## x2 -1.616309 0.058498 -27.630 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.526 on 97 degrees of freedom
## Multiple R-squared: 0.9581, Adjusted R-squared: 0.9573
## F-statistic: 1110 on 2 and 97 DF, p-value: < 2.2e-16
# 前向选择
null_model <- lm(y ~ 1, data = step_data)
forward_model <- step(null_model,
scope = list(lower = null_model, upper = full_model),
direction = "forward", trace = 0)
print("前向选择后的模型:")
## [1] "前向选择后的模型:"
summary(forward_model)
##
## Call:
## lm(formula = y ~ x1 + x2, data = step_data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.93302 -0.36500 0.02268 0.34716 1.65764
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -0.008394 0.052881 -0.159 0.874
## x1 1.981572 0.050792 39.013 <2e-16 ***
## x2 -1.616309 0.058498 -27.630 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.526 on 97 degrees of freedom
## Multiple R-squared: 0.9581, Adjusted R-squared: 0.9573
## F-statistic: 1110 on 2 and 97 DF, p-value: < 2.2e-16
原理:Lasso回归自动将不重要特征的系数压缩为0。
# Lasso特征选择示例
library(glmnet)
set.seed(42)
# 创建高维数据
n <- 100
p <- 50
lasso_data <- matrix(rnorm(n * p), nrow = n, ncol = p)
# 只有前10个特征与目标相关
true_coef <- c(rep(1, 10), rep(0, 40))
target <- lasso_data %*% true_coef + rnorm(n, 0, 0.5)
# 执行Lasso回归
lasso_model <- glmnet::glmnet(lasso_data, target, alpha = 1) # alpha=1为Lasso
# 交叉验证选择lambda
cv_lasso <- glmnet::cv.glmnet(lasso_data, target, alpha = 1)
# 可视化
plot(cv_lasso, main = "Lasso交叉验证")
# 获取最优lambda下的系数
optimal_coef <- coef(cv_lasso, s = "lambda.min")
# 查看非零系数
non_zero_features <- which(optimal_coef != 0) - 1 # -1因为第一行是截距
print(paste("Lasso选择的特征数:", length(non_zero_features[non_zero_features > 0])))
## [1] "Lasso选择的特征数: 39"
print("选择的特征:")
## [1] "选择的特征:"
print(non_zero_features[non_zero_features > 0])
## [1] 1 2 3 4 5 6 7 8 9 10 11 12 14 15 18 19 20 21 23 24 25 26 27 28 29
## [26] 30 31 32 33 36 38 39 41 42 43 44 45 49 50
# 随机森林特征重要性
set.seed(42)
# 创建数据
n <- 200
rf_data <- data.frame(
feature1 = rnorm(n),
feature2 = rnorm(n),
feature3 = rnorm(n),
feature4 = rnorm(n),
feature5 = rnorm(n),
target = factor(rep(c("A", "B"), each = n/2))
)
# 让某些特征与目标相关
rf_data$feature1[rf_data$target == "B"] <- rf_data$feature1[rf_data$target == "B"] + 1
rf_data$feature3[rf_data$target == "B"] <- rf_data$feature3[rf_data$target == "B"] - 1
# 训练随机森林
rf_model <- randomForest::randomForest(target ~ ., data = rf_data, importance = TRUE)
# 获取特征重要性
importance_df <- as.data.frame(randomForest::importance(rf_model))
# 可视化
ggplot2::ggplot(importance_df, ggplot2::aes(x = reorder(rownames(importance_df), MeanDecreaseGini), y = MeanDecreaseGini)) +
ggplot2::geom_bar(stat = "identity", fill = "steelblue") +
ggplot2::coord_flip() +
ggplot2::labs(
title = "随机森林特征重要性(Mean Decrease Gini)",
x = "特征", y = "重要性"
) +
ggplot2::theme_minimal()
原理:随机打乱某个特征的值,观察模型性能下降程度。
# 排列重要性示例
# 使用iml包
set.seed(42)
# 训练模型
model <- randomForest::randomForest(target ~ ., data = rf_data)
# 创建预测器对象
predictor <- iml::Predictor$new(model, data = rf_data[, -6], y = rf_data$target)
# 计算排列重要性
perm_importance <- iml::FeatureImp$new(predictor, loss = "ce")
# 可视化
plot(perm_importance) +
ggplot2::labs(
title = "排列重要性",
subtitle = "打乱特征后模型性能下降越多,特征越重要"
)
| 对比维度 | 特征选择 | 特征提取 |
|---|---|---|
| 方法 | 选择原始特征子集 | 创建新特征 |
| 可解释性 | 保持原始特征含义 | 新特征难以解释 |
| 信息保留 | 可能丢失信息 | 压缩信息 |
| 典型方法 | 过滤法、包装法 | PCA、LDA |
# PCA特征提取示例
set.seed(42)
# 创建高维数据
n <- 100
p <- 20
pca_feat_data <- matrix(rnorm(n * p), nrow = n, ncol = p)
colnames(pca_feat_data) <- paste0("Feature_", 1:p)
# 添加分组结构
pca_feat_data[1:50, 1:5] <- pca_feat_data[1:50, 1:5] + 2
# 执行PCA
pca_feat <- prcomp(pca_feat_data, scale. = TRUE)
# 使用前5个主成分作为新特征
pca_features <- pca_feat$x[, 1:5]
# 查看新特征的方差贡献
var_explained <- pca_feat$sdev^2 / sum(pca_feat$sdev^2)
print("前5个主成分的方差贡献:")
## [1] "前5个主成分的方差贡献:"
print(round(cumsum(var_explained)[5], 3))
## [1] 0.466
线性判别分析(LDA) 寻找最能区分类别的投影方向,可用于特征提取。
# LDA特征提取示例
set.seed(42)
# 创建带标签的数据
n <- 100
lda_feat_data <- data.frame(
x1 = c(rnorm(n/2, 0, 1), rnorm(n/2, 3, 1)),
x2 = c(rnorm(n/2, 0, 1), rnorm(n/2, 3, 1)),
x3 = rnorm(n), # 噪声特征
x4 = rnorm(n), # 噪声特征
class = factor(rep(c("A", "B"), each = n/2))
)
# 执行LDA
lda_feat <- MASS::lda(class ~ x1 + x2 + x3 + x4, data = lda_feat_data)
# 查看判别函数
print("LDA判别函数系数:")
## [1] "LDA判别函数系数:"
print(lda_feat$scaling)
## LD1
## x1 0.621599492
## x2 0.820935594
## x3 0.030917345
## x4 0.005188053
# 投影到判别方向
lda_projection <- as.matrix(lda_feat_data[, 1:4]) %*% lda_feat$scaling
# 可视化
lda_feat_data$LD1 <- lda_projection[, 1]
ggplot2::ggplot(lda_feat_data, ggplot2::aes(x = LD1, fill = class)) +
ggplot2::geom_histogram(position = "identity", alpha = 0.5, bins = 30) +
ggplot2::labs(
title = "LDA特征提取结果",
subtitle = "投影到判别方向后,两类分离更明显",
x = "判别方向得分", y = "频数", fill = "类别"
) +
ggplot2::theme_minimal()
模型可解释性(Model Interpretability) 指人类能够理解模型决策原因的程度。在医学领域尤为重要:
| 原因 | 说明 |
|---|---|
| 信任建立 | 医生需要理解模型才能信任 |
| 伦理要求 | 医疗决策需要可解释 |
| 监管合规 | 医疗AI需要通过审核 |
| 错误诊断 | 发现模型的潜在问题 |
特征重要性 衡量每个特征对模型预测的贡献程度。
常用方法: 1. 基于模型的内置重要性(如随机森林的Gini重要性) 2. 排列重要性(Permutation Importance)
# 特征重要性示例
set.seed(42)
# 创建模拟疾病预测数据
n <- 300
disease_data <- data.frame(
age = round(rnorm(n, 50, 15)),
blood_pressure = rnorm(n, 120, 20),
cholesterol = rnorm(n, 200, 40),
bmi = rnorm(n, 25, 5),
glucose = rnorm(n, 100, 20),
smoking = factor(sample(c("Yes", "No"), n, replace = TRUE)),
disease = factor(sample(c("Disease", "Healthy"), n, replace = TRUE))
)
# 让某些特征与疾病相关
disease_data$disease <- factor(ifelse(
disease_data$blood_pressure > 130 & disease_data$glucose > 110,
"Disease", "Healthy"
))
# 训练随机森林模型
rf_model <- randomForest::randomForest(disease ~ ., data = disease_data,
importance = TRUE, ntree = 100)
# 获取特征重要性
importance_matrix <- randomForest::importance(rf_model)
# 创建数据框用于可视化
imp_df <- data.frame(
Feature = rownames(importance_matrix),
MeanDecreaseAccuracy = importance_matrix[, "MeanDecreaseAccuracy"],
MeanDecreaseGini = importance_matrix[, "MeanDecreaseGini"]
)
# 可视化
p1 <- ggplot2::ggplot(imp_df, ggplot2::aes(x = reorder(Feature, MeanDecreaseAccuracy), y = MeanDecreaseAccuracy)) +
ggplot2::geom_bar(stat = "identity", fill = "steelblue") +
ggplot2::coord_flip() +
ggplot2::labs(title = "Mean Decrease Accuracy", x = "", y = "") +
ggplot2::theme_minimal()
p2 <- ggplot2::ggplot(imp_df, ggplot2::aes(x = reorder(Feature, MeanDecreaseGini), y = MeanDecreaseGini)) +
ggplot2::geom_bar(stat = "identity", fill = "salmon") +
ggplot2::coord_flip() +
ggplot2::labs(title = "Mean Decrease Gini", x = "", y = "") +
ggplot2::theme_minimal()
p1 + p2 + patchwork::plot_annotation(
title = "随机森林特征重要性"
)
# 使用DALEX包计算排列重要性
library(DALEX)
# 创建解释器
explainer_rf <- DALEX::explain(
model = rf_model,
data = disease_data[, -7],
y = as.numeric(disease_data$disease == "Disease"),
label = "Random Forest",
verbose = FALSE
)
# 计算排列重要性
set.seed(42)
vip_rf <- DALEX::model_parts(explainer_rf,
loss_function = DALEX::loss_root_mean_square)
# 可视化
plot(vip_rf) +
ggplot2::labs(
title = "排列重要性(DALEX)",
subtitle = "打乱特征后模型性能下降越多,特征越重要"
)
部分依赖图(Partial Dependence Plot, PDP) 展示特征对预测的平均影响:
\[\text{PD}(x_s) = \frac{1}{n} \sum_{i=1}^{n} f(x_s, x_{c}^{(i)})\]
其中\(x_s\)是感兴趣的特征,\(x_c\)是其他特征。
# PDP示例
library(pdp)
# 计算血压的部分依赖
pdp_bp <- pdp::partial(rf_model,
pred.var = "blood_pressure",
train = disease_data[, -7],
prob = TRUE)
# 可视化
ggplot2::ggplot(pdp_bp, ggplot2::aes(x = blood_pressure, y = yhat)) +
ggplot2::geom_line(color = "steelblue", linewidth = 1) +
ggplot2::labs(
title = "部分依赖图:血压对疾病风险的影响",
x = "血压", y = "预测概率"
) +
ggplot2::theme_minimal()
ICE图 展示每个样本的预测曲线,比PDP提供更详细的信息。
# ICE图示例
ice_bp <- pdp::partial(rf_model,
pred.var = "blood_pressure",
train = disease_data[, -7],
ice = TRUE,
prob = TRUE)
# 可视化ICE曲线
ggplot2::ggplot(ice_bp, ggplot2::aes(x = blood_pressure, y = yhat, group = yhat.id)) +
ggplot2::geom_line(alpha = 0.1, color = "steelblue") +
ggplot2::labs(
title = "ICE图:每个样本的血压-风险曲线",
subtitle = "每条线代表一个样本",
x = "血压", y = "预测概率"
) +
ggplot2::theme_minimal()
SHAP(SHapley Additive exPlanations) 基于博弈论,计算每个特征对预测的边际贡献:
核心思想:考虑所有可能的特征组合,计算特征加入时的平均贡献。
\[\phi_i = \sum_{S \subseteq F \setminus \{i\}} \frac{|S|!(|F|-|S|-1)!}{|F|!} [f(S \cup \{i\}) - f(S)]\]
# SHAP值示例
# 使用iml包
set.seed(42)
# 创建预测器
predictor <- iml::Predictor$new(
rf_model,
data = disease_data[, -7],
y = disease_data$disease,
class = "Disease"
)
# 计算单个样本的SHAP值
shap_single <- iml::Shapley$new(predictor, x.interest = disease_data[1, -7])
# 可视化
plot(shap_single) +
ggplot2::labs(
title = "SHAP值:单个样本的特征贡献",
subtitle = "正值增加疾病概率,负值降低疾病概率"
)
# 计算多个样本的SHAP值
shap_values <- data.frame()
for (i in sample(1:nrow(disease_data), 20)) {
shap_i <- iml::Shapley$new(predictor, x.interest = disease_data[i, -7])
shap_df <- shap_i$results
shap_df$sample_id <- i
shap_values <- rbind(shap_values, shap_df)
}
# 可视化SHAP值分布
ggplot2::ggplot(shap_values, ggplot2::aes(x = feature, y = phi, fill = phi > 0)) +
ggplot2::geom_boxplot() +
ggplot2::scale_fill_manual(values = c("TRUE" = "red", "FALSE" = "blue")) +
ggplot2::coord_flip() +
ggplot2::labs(
title = "SHAP值分布",
subtitle = "正值增加疾病概率,负值降低疾病概率",
x = "特征", y = "SHAP值"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "none")
LIME(Local Interpretable Model-agnostic Explanations) 通过在局部拟合简单模型来解释复杂模型:
核心思想:在待解释样本附近生成扰动数据,用简单模型拟合局部行为。
# LIME示例
library(lime)
library(caret)
# 为LIME准备数据(仅使用数值特征)
lime_data <- disease_data[, c("age", "blood_pressure", "cholesterol", "bmi", "glucose")]
lime_labels <- disease_data$disease
# 使用caret训练模型(lime对caret模型有更好的支持)
caret_model <- caret::train(
x = lime_data,
y = lime_labels,
method = "rf",
ntree = 100,
trControl = caret::trainControl(method = "none"),
tuneGrid = data.frame(mtry = 2)
)
# 创建LIME解释器
lime_explainer <- lime::lime(
x = lime_data,
model = caret_model
)
# 解释单个样本
lime_explanation <- lime::explain(
x = lime_data[1:3, ],
explainer = lime_explainer,
n_features = 5,
n_labels = 1
)
# 可视化
plot_features(lime_explanation)
替代模型(Surrogate Model) 用简单可解释的模型近似复杂模型:
步骤: 1. 用复杂模型预测所有样本 2. 用简单模型(如决策树)拟合预测结果 3. 解释简单模型
# 替代模型示例
set.seed(42)
# 获取复杂模型的预测
complex_predictions <- predict(rf_model, disease_data[, -7], type = "prob")[, 2]
# 用决策树拟合预测结果
surrogate_tree <- rpart::rpart(
complex_predictions ~ age + blood_pressure + cholesterol + bmi + glucose + smoking,
data = disease_data,
method = "anova",
control = rpart::rpart.control(maxdepth = 3)
)
# 可视化决策树
rpart.plot::rpart.plot(surrogate_tree,
main = "替代模型:决策树近似随机森林",
type = 2,
extra = 101)
模型公平性(Model Fairness) 确保模型对不同群体没有歧视:
| 公平性指标 | 定义 |
|---|---|
| 人口统计学均等 | 不同群体的预测率相同 |
| 机会均等 | 不同群体的真阳性率相同 |
| 预测均等 | 不同群体的精确率相同 |
# 公平性评估示例
# 假设我们关注模型对不同年龄组的公平性
# 创建年龄组
disease_data$age_group <- cut(disease_data$age,
breaks = c(0, 40, 60, 100),
labels = c("Young", "Middle", "Senior"))
# 获取预测
disease_data$predicted <- predict(rf_model, disease_data[, -7])
# 计算各年龄组的性能
fairness_metrics <- disease_data %>%
dplyr::group_by(age_group) %>%
dplyr::summarise(
accuracy = mean(predicted == disease),
disease_rate = mean(disease == "Disease"),
predicted_rate = mean(predicted == "Disease")
)
print("各年龄组的模型表现:")
## [1] "各年龄组的模型表现:"
print(fairness_metrics)
## # A tibble: 3 × 4
## age_group accuracy disease_rate predicted_rate
## <fct> <dbl> <dbl> <dbl>
## 1 Young 1 0.103 0.103
## 2 Middle 1 0.0452 0.0452
## 3 Senior 1 0.104 0.104
# 可视化
ggplot2::ggplot(fairness_metrics, ggplot2::aes(x = age_group, y = accuracy, fill = age_group)) +
ggplot2::geom_bar(stat = "identity") +
ggplot2::labs(
title = "模型公平性评估:各年龄组准确率",
x = "年龄组", y = "准确率"
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "none")
时间序列数据 具有时间依赖性,样本间存在顺序关系:
| 特点 | 说明 |
|---|---|
| 时间依赖 | 当前值与历史值相关 |
| 趋势 | 长期上升或下降趋势 |
| 季节性 | 周期性重复模式 |
| 周期性 | 非固定周期的波动 |
| 不规则性 | 随机波动 |
核心思想:将时间序列转换为监督学习问题,用历史值预测未来值。
# 时间序列监督学习示例
set.seed(42)
# 创建模拟的时间序列数据:患者每日血糖监测
n_days <- 200
dates <- seq(as.Date("2023-01-01"), by = "day", length.out = n_days)
# 生成带有趋势和季节性的血糖数据
trend <- seq(100, 130, length.out = n_days)
seasonal <- 10 * sin(2 * pi * (1:n_days) / 30) # 月周期
noise <- rnorm(n_days, 0, 5)
glucose <- trend + seasonal + noise
ts_data <- data.frame(
date = dates,
glucose = glucose
)
# 可视化原始时间序列
ggplot2::ggplot(ts_data, ggplot2::aes(x = date, y = glucose)) +
ggplot2::geom_line(color = "steelblue") +
ggplot2::labs(
title = "患者每日血糖监测时间序列",
x = "日期", y = "血糖值"
) +
ggplot2::theme_minimal()
# 构建滑动窗口特征
create_lagged_features <- function(data, lag = 7) {
n <- nrow(data)
result <- data.frame(date = data$date[(lag+1):n])
for (i in 1:lag) {
result[[paste0("lag_", i)]] <- data$glucose[(lag+1-i):(n-i)]
}
result$target <- data$glucose[(lag+1):n]
return(result)
}
# 创建滞后特征
lagged_data <- create_lagged_features(ts_data, lag = 7)
# 查看数据结构
head(lagged_data)
## date lag_1 lag_2 lag_3 lag_4 lag_5 lag_6 lag_7
## 1 2023-01-08 118.4074 109.7337 111.2846 111.0480 107.9950 101.3946 108.9339
## 2 2023-01-09 110.5272 118.4074 109.7337 111.2846 111.0480 107.9950 101.3946
## 3 2023-01-10 120.8087 110.5272 118.4074 109.7337 111.2846 111.0480 107.9950
## 4 2023-01-11 109.7035 120.8087 110.5272 118.4074 109.7337 111.2846 111.0480
## 5 2023-01-12 115.4633 109.7035 120.8087 110.5272 118.4074 109.7337 111.2846
## 6 2023-01-13 118.9694 115.4633 109.7035 120.8087 110.5272 118.4074 109.7337
## target
## 1 110.52720
## 2 120.80871
## 3 109.70347
## 4 115.46333
## 5 118.96937
## 6 98.93211
# 滞后特征示例
# 添加多个滞后特征
ts_data$glucose_lag1 <- dplyr::lag(ts_data$glucose, 1)
ts_data$glucose_lag7 <- dplyr::lag(ts_data$glucose, 7)
# 可视化滞后关系
p1 <- ggplot2::ggplot(ts_data, ggplot2::aes(x = glucose_lag1, y = glucose)) +
ggplot2::geom_point(alpha = 0.5) +
ggplot2::geom_smooth(method = "lm", se = FALSE, color = "red") +
ggplot2::labs(title = "1天滞后相关性", x = "昨天血糖", y = "今天血糖") +
ggplot2::theme_minimal()
p2 <- ggplot2::ggplot(ts_data, ggplot2::aes(x = glucose_lag7, y = glucose)) +
ggplot2::geom_point(alpha = 0.5) +
ggplot2::geom_smooth(method = "lm", se = FALSE, color = "red") +
ggplot2::labs(title = "7天滞后相关性", x = "7天前血糖", y = "今天血糖") +
ggplot2::theme_minimal()
p1 + p2 + patchwork::plot_annotation(title = "滞后特征与目标的关系")
# 滚动统计特征
# 计算滚动均值和滚动标准差
ts_data$rolling_mean_7 <- zoo::rollmean(ts_data$glucose, k = 7, fill = NA, align = "right")
ts_data$rolling_sd_7 <- zoo::rollapply(ts_data$glucose, width = 7, FUN = sd, fill = NA, align = "right")
# 可视化滚动统计
ggplot2::ggplot(ts_data, ggplot2::aes(x = date)) +
ggplot2::geom_line(ggplot2::aes(y = glucose), color = "gray50", alpha = 0.5) +
ggplot2::geom_line(ggplot2::aes(y = rolling_mean_7), color = "steelblue", linewidth = 1) +
ggplot2::geom_ribbon(ggplot2::aes(ymin = rolling_mean_7 - rolling_sd_7,
ymax = rolling_mean_7 + rolling_sd_7),
alpha = 0.2, fill = "steelblue") +
ggplot2::labs(
title = "滚动统计特征:7天均值和标准差",
subtitle = "阴影区域为均值±标准差",
x = "日期", y = "血糖值"
) +
ggplot2::theme_minimal()
# 提取季节特征
ts_data$day_of_week <- weekdays(ts_data$date)
ts_data$day_of_month <- as.numeric(format(ts_data$date, "%d"))
ts_data$month <- as.numeric(format(ts_data$date, "%m"))
ts_data$is_weekend <- ts_data$day_of_week %in% c("Saturday", "Sunday")
# 可视化周内模式
weekly_pattern <- ts_data %>%
dplyr::group_by(day_of_week) %>%
dplyr::summarise(
mean_glucose = mean(glucose, na.rm = TRUE),
sd_glucose = sd(glucose, na.rm = TRUE)
)
# 设置星期顺序
weekly_pattern$day_of_week <- factor(weekly_pattern$day_of_week,
levels = c("Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"))
ggplot2::ggplot(weekly_pattern, ggplot2::aes(x = day_of_week, y = mean_glucose)) +
ggplot2::geom_bar(stat = "identity", fill = "steelblue") +
ggplot2::geom_errorbar(ggplot2::aes(ymin = mean_glucose - sd_glucose,
ymax = mean_glucose + sd_glucose),
width = 0.2) +
ggplot2::labs(
title = "周内血糖模式",
x = "星期", y = "平均血糖"
) +
ggplot2::theme_minimal() +
ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 45, hjust = 1))
重要原则:不能用未来数据预测过去,必须保持时间顺序。
# 时间序列交叉验证示例
library(forecast)
# 创建时间序列对象
ts_glucose <- ts(ts_data$glucose, frequency = 30) # 假设月周期
# 使用forecast包的时间序列CV
# 创建训练集和测试集
train_size <- floor(n_days * 0.8)
train_data <- ts_data$glucose[1:train_size]
test_data <- ts_data$glucose[(train_size + 1):n_days]
# 滚动预测
h <- 7 # 预测步长
n_windows <- 5 # 滚动窗口数
predictions <- data.frame()
for (i in 1:n_windows) {
# 训练集结束点
train_end <- train_size + (i - 1) * h
if (train_end + h > n_days) break
# 训练模型
train_subset <- ts_data$glucose[1:train_end]
fit <- forecast::auto.arima(train_subset)
# 预测
fc <- forecast::forecast(fit, h = h)
# 存储结果
predictions <- rbind(predictions, data.frame(
window = i,
date = ts_data$date[(train_end + 1):(train_end + h)],
actual = ts_data$glucose[(train_end + 1):(train_end + h)],
predicted = as.numeric(fc$mean)
))
}
# 计算预测误差
predictions$error <- predictions$actual - predictions$predicted
rmse <- sqrt(mean(predictions$error^2))
print(paste("RMSE:", round(rmse, 2)))
## [1] "RMSE: 8.4"
# 可视化预测结果
ggplot2::ggplot(predictions, ggplot2::aes(x = date)) +
ggplot2::geom_line(ggplot2::aes(y = actual, color = "实际值")) +
ggplot2::geom_line(ggplot2::aes(y = predicted, color = "预测值"), linetype = "dashed") +
ggplot2::scale_color_manual(values = c("实际值" = "steelblue", "预测值" = "red")) +
ggplot2::labs(
title = "时间序列交叉验证预测结果",
x = "日期", y = "血糖值", color = ""
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
# 使用随机森林进行时间序列预测
set.seed(42)
# 准备特征数据
ts_features <- lagged_data[, -c(1, ncol(lagged_data))] # 移除日期和目标
ts_target <- lagged_data$target
# 划分训练集和测试集
train_idx <- 1:floor(nrow(lagged_data) * 0.8)
train_x <- ts_features[train_idx, ]
train_y <- ts_target[train_idx]
test_x <- ts_features[-train_idx, ]
test_y <- ts_target[-train_idx]
# 训练随机森林
rf_ts <- randomForest::randomForest(
x = as.matrix(train_x),
y = train_y,
ntree = 100,
importance = TRUE
)
# 预测
rf_predictions <- predict(rf_ts, as.matrix(test_x))
# 评估
rf_rmse <- sqrt(mean((test_y - rf_predictions)^2))
print(paste("随机森林RMSE:", round(rf_rmse, 2)))
## [1] "随机森林RMSE: 6.43"
# 可视化
results_df <- data.frame(
date = lagged_data$date[-train_idx],
actual = test_y,
predicted = rf_predictions
)
ggplot2::ggplot(results_df, ggplot2::aes(x = date)) +
ggplot2::geom_line(ggplot2::aes(y = actual, color = "实际值")) +
ggplot2::geom_line(ggplot2::aes(y = predicted, color = "预测值"), linetype = "dashed") +
ggplot2::scale_color_manual(values = c("实际值" = "steelblue", "预测值" = "red")) +
ggplot2::labs(
title = "随机森林时间序列预测",
x = "日期", y = "血糖值", color = ""
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
Prophet 是Facebook开发的时间序列预测模型,适合具有趋势和季节性的数据:
模型组成: \[y(t) = g(t) + s(t) + h(t) + \epsilon_t\]
# Prophet模型示例
library(prophet)
# 准备数据(Prophet需要ds和y两列)
prophet_data <- data.frame(
ds = ts_data$date,
y = ts_data$glucose
)
# 训练Prophet模型
prophet_model <- prophet::prophet(
prophet_data,
yearly.seasonality = FALSE,
weekly.seasonality = TRUE,
daily.seasonality = FALSE
)
# 创建未来数据框
future <- prophet::make_future_dataframe(prophet_model, periods = 30)
# 预测
prophet_forecast <- predict(prophet_model, future)
# 可视化
plot(prophet_model, prophet_forecast) +
ggplot2::labs(
title = "Prophet预测结果",
subtitle = "蓝色区域为预测区间",
x = "日期", y = "血糖值"
) +
ggplot2::theme_minimal()
# 时间序列聚类示例
set.seed(42)
# 创建多个患者的时间序列
n_patients <- 20
n_days <- 100
# 生成不同模式的时间序列
ts_list <- list()
for (i in 1:n_patients) {
pattern_type <- sample(1:3, 1)
if (pattern_type == 1) {
# 上升趋势
ts_list[[i]] <- seq(80, 120, length.out = n_days) + rnorm(n_days, 0, 5)
} else if (pattern_type == 2) {
# 下降趋势
ts_list[[i]] <- seq(120, 80, length.out = n_days) + rnorm(n_days, 0, 5)
} else {
# 稳定趋势
ts_list[[i]] <- rep(100, n_days) + rnorm(n_days, 0, 5)
}
}
# 转换为矩阵
ts_matrix <- do.call(rbind, ts_list)
# 计算距离矩阵(动态时间规整)
# 简化:使用欧氏距离
dist_matrix <- dist(ts_matrix)
# 层次聚类
hc_result <- hclust(dist_matrix, method = "ward.D2")
# 可视化
plot(hc_result, main = "时间序列聚类树状图", xlab = "患者", ylab = "距离")
# 时间序列异常检测
# 使用预测残差检测异常
# 计算预测残差
residuals <- test_y - rf_predictions
# 定义异常阈值(3倍标准差)
threshold <- 3 * sd(residuals)
# 识别异常点
anomalies <- which(abs(residuals) > threshold)
print(paste("检测到", length(anomalies), "个异常点"))
## [1] "检测到 0 个异常点"
# 可视化异常
results_df$is_anomaly <- abs(residuals) > threshold
ggplot2::ggplot(results_df, ggplot2::aes(x = date)) +
ggplot2::geom_line(ggplot2::aes(y = actual), color = "steelblue") +
ggplot2::geom_point(data = dplyr::filter(results_df, is_anomaly),
ggplot2::aes(y = actual), color = "red", size = 3) +
ggplot2::labs(
title = "时间序列异常检测",
subtitle = "红点为检测到的异常值",
x = "日期", y = "血糖值"
) +
ggplot2::theme_minimal()
文本挖掘(Text Mining) 从非结构化文本中提取有价值的信息:
| 步骤 | 说明 |
|---|---|
| 文本收集 | 获取原始文本数据 |
| 文本预处理 | 清洗和标准化文本 |
| 特征提取 | 将文本转换为数值特征 |
| 模型构建 | 应用机器学习方法 |
分词(Tokenization) 将文本切分为词语或标记:
# 中文分词示例
# 使用简单的字符串分割方法演示分词概念
# 实际应用中可使用jiebaR等专门包
# 创建模拟的医疗文本数据
medical_texts <- c(
"患者主诉头痛三天,伴有恶心呕吐症状",
"既往有高血压病史五年,规律服药控制",
"体格检查显示血压偏高,心率正常",
"建议进行头部CT检查排除颅内病变"
)
# 简单分词:按标点和空格分割
simple_tokenize <- function(text) {
# 使用正则表达式分割
tokens <- unlist(strsplit(text, "[,。、;:!?\\s]+"))
tokens <- tokens[nchar(tokens) > 0]
paste(tokens, collapse = " | ")
}
# 进行分词
tokenized_texts <- sapply(medical_texts, simple_tokenize)
# 查看分词结果
print("分词结果:")
## [1] "分词结果:"
for (i in seq_along(medical_texts)) {
cat("原文:", medical_texts[i], "\n")
cat("分词:", tokenized_texts[i], "\n\n")
}
## 原文: 患者主诉头痛三天,伴有恶心呕吐症状
## 分词: 患者主诉头痛三天 | 伴有恶心呕吐症状
##
## 原文: 既往有高血压病史五年,规律服药控制
## 分词: 既往有高血压病史五年 | 规律服药控制
##
## 原文: 体格检查显示血压偏高,心率正常
## 分词: 体格检查显示血压偏高 | 心率正常
##
## 原文: 建议进行头部CT检查排除颅内病变
## 分词: 建议进行头部CT检查排除颅内病变
停用词(Stop Words) 是高频但信息量小的词,如”的”、“是”等。
# 停用词处理
# 定义停用词列表
stop_words <- c("的", "是", "有", "在", "和", "了", "进行", "显示", "伴有")
# 移除停用词(简单示例)
remove_stopwords <- function(text, stop_words) {
tokens <- unlist(strsplit(text, "[,。、;:!?\\s]+"))
tokens <- tokens[!tokens %in% stop_words]
tokens <- tokens[nchar(tokens) > 0]
paste(tokens, collapse = " | ")
}
# 应用停用词移除
cleaned_texts <- sapply(medical_texts, function(x) {
remove_stopwords(x, stop_words)
})
print("移除停用词后:")
## [1] "移除停用词后:"
for (i in seq_along(cleaned_texts)) {
cat(cleaned_texts[i], "\n")
}
## 患者主诉头痛三天 | 伴有恶心呕吐症状
## 既往有高血压病史五年 | 规律服药控制
## 体格检查显示血压偏高 | 心率正常
## 建议进行头部CT检查排除颅内病变
词干提取(Stemming):将词还原为词干 词形还原(Lemmatization):将词还原为词典形式
# 英文文本处理示例
library(SnowballC)
# 英文医疗文本
english_texts <- c(
"The patient was diagnosed with diabetes",
"Patients are receiving treatments for various diseases",
"The medication effectively reduced the symptoms"
)
# 创建语料库
corpus <- tm::Corpus(tm::VectorSource(english_texts))
# 文本预处理:转小写、移除标点、移除停用词
corpus <- tm::tm_map(corpus, tm::content_transformer(tolower))
corpus <- tm::tm_map(corpus, tm::removePunctuation)
corpus <- tm::tm_map(corpus, tm::removeWords, tm::stopwords("en"))
# 词干提取
corpus <- tm::tm_map(corpus, tm::stemDocument)
# 查看处理结果
print("预处理后的文本:")
## [1] "预处理后的文本:"
for (i in 1:length(corpus)) {
cat(corpus[[i]]$content, "\n")
}
## patient diagnos diabet
## patient receiv treatment various diseas
## medic effect reduc symptom
词袋模型(Bag of Words) 将文本表示为词频向量,忽略词序:
# 词袋模型示例
# 使用tm包创建文档-词矩阵
# 创建模拟的医疗文本数据集
documents <- c(
"高血压 糖尿病 冠心病",
"高血压 冠心病 治疗",
"糖尿病 肾病 治疗",
"冠心病 糖尿病 高血压 治疗"
)
# 创建语料库
corpus <- tm::Corpus(tm::VectorSource(documents))
# 创建文档-词矩阵
dtm <- tm::DocumentTermMatrix(corpus)
# 查看矩阵
print("文档-词矩阵:")
## [1] "文档-词矩阵:"
tm::inspect(dtm)
## <<DocumentTermMatrix (documents: 4, terms: 5)>>
## Non-/sparse entries: 13/7
## Sparsity : 35%
## Maximal term length: 3
## Weighting : term frequency (tf)
## Sample :
## Terms
## Docs 高血压 冠心病 肾病 糖尿病 治疗
## 1 1 1 0 1 0
## 2 1 1 0 0 1
## 3 0 0 1 1 1
## 4 1 1 0 1 1
# 转换为普通矩阵查看
dtm_matrix <- as.matrix(dtm)
print(dtm_matrix)
## Terms
## Docs 冠心病 糖尿病 高血压 治疗 肾病
## 1 1 1 1 0 0
## 2 1 0 1 1 0
## 3 0 1 0 1 1
## 4 1 1 1 1 0
TF-IDF(Term Frequency-Inverse Document Frequency) 衡量词的重要性:
\[\text{TF-IDF}(t, d) = \text{TF}(t, d) \times \text{IDF}(t)\]
\[\text{IDF}(t) = \log\frac{N}{df(t)}\]
其中: - \(\text{TF}(t, d)\):词t在文档d中的频率 - \(N\):文档总数 - \(df(t)\):包含词t的文档数
# TF-IDF计算
# 创建带TF-IDF权重的文档-词矩阵
dtm_tfidf <- tm::weightTfIdf(dtm)
# 查看TF-IDF矩阵
tfidf_matrix <- as.matrix(dtm_tfidf)
print("TF-IDF矩阵:")
## [1] "TF-IDF矩阵:"
print(round(tfidf_matrix, 3))
## Terms
## Docs 冠心病 糖尿病 高血压 治疗 肾病
## 1 0.138 0.138 0.138 0.000 0.000
## 2 0.138 0.000 0.138 0.138 0.000
## 3 0.000 0.138 0.000 0.138 0.667
## 4 0.104 0.104 0.104 0.104 0.000
# 找出每个文档中最重要的词
for (i in 1:nrow(tfidf_matrix)) {
top_words <- names(sort(tfidf_matrix[i, ], decreasing = TRUE))[1:2]
print(paste("文档", i, "最重要的词:", paste(top_words, collapse = ", ")))
}
## [1] "文档 1 最重要的词: 冠心病, 糖尿病"
## [1] "文档 2 最重要的词: 冠心病, 高血压"
## [1] "文档 3 最重要的词: 肾病, 糖尿病"
## [1] "文档 4 最重要的词: 冠心病, 糖尿病"
N-gram 是连续的N个词的序列,可以捕捉局部词序信息:
| N-gram类型 | 示例 |
|---|---|
| Unigram (1-gram) | “高血压”, “糖尿病” |
| Bigram (2-gram) | “高血压 患者”, “糖尿病 治疗” |
| Trigram (3-gram) | “高血压 患者 治疗” |
# N-gram示例
library(tidytext)
# 创建医疗文本数据框
text_df <- data.frame(
doc_id = 1:4,
text = c(
"高血压患者需要长期治疗",
"糖尿病患者应控制饮食",
"高血压和糖尿病都是慢性病",
"慢性病需要长期管理和治疗"
)
)
# 分词
tokens <- tidytext::unnest_tokens(text_df, word, text)
# 查看unigram结果
print("Unigram结果:")
## [1] "Unigram结果:"
head(tokens, 10)
## doc_id word
## 1 1 高血压
## 2 1 患者
## 3 1 需要
## 4 1 长期
## 5 1 治疗
## 6 2 糖尿病
## 7 2 患者
## 8 2 应
## 9 2 控制
## 10 2 饮食
# 创建bigram
bigrams <- text_df %>%
tidytext::unnest_tokens(bigram, text, token = "ngrams", n = 2)
# 查看bigram结果
print("Bigram结果:")
## [1] "Bigram结果:"
head(bigrams, 10)
## doc_id bigram
## 1 1 高血压 患者
## 2 1 患者 需要
## 3 1 需要 长期
## 4 1 长期 治疗
## 5 2 糖尿病 患者
## 6 2 患者 应
## 7 2 应 控制
## 8 2 控制 饮食
## 9 3 高血压 和
## 10 3 和 糖尿病
# 统计bigram频率
bigram_counts <- bigrams %>%
dplyr::count(bigram, sort = TRUE)
print("最常见的bigram:")
## [1] "最常见的bigram:"
head(bigram_counts, 10)
## bigram n
## 1 需要 长期 2
## 2 和 治疗 1
## 3 和 糖尿病 1
## 4 应 控制 1
## 5 患者 应 1
## 6 患者 需要 1
## 7 慢性病 需要 1
## 8 控制 饮食 1
## 9 管理 和 1
## 10 糖尿病 患者 1
潜在狄利克雷分配(LDA) 是一种生成式概率模型,用于发现文档集合中的潜在主题:
核心假设: 1. 每个文档是多个主题的混合 2. 每个主题是词语的概率分布
# LDA主题模型示例
library(topicmodels)
# 创建模拟的医疗文档集合
medical_docs <- c(
"高血压 血压 心血管 治疗 药物 患者",
"糖尿病 血糖 胰岛素 治疗 患者 饮食",
"高血压 心血管 疾病 风险 预防 治疗",
"糖尿病 并发症 肾病 视网膜 病变",
"心血管 冠心病 心脏 血管 疾病",
"血糖 胰岛素 糖尿病 控制 治疗"
)
# 创建语料库和文档-词矩阵
corpus <- tm::Corpus(tm::VectorSource(medical_docs))
dtm <- tm::DocumentTermMatrix(corpus)
# 训练LDA模型
lda_model <- topicmodels::LDA(dtm, k = 2, control = list(seed = 42))
# 查看主题-词分布
topics <- tidytext::tidy(lda_model, matrix = "beta")
# 找出每个主题最重要的词
top_terms <- topics %>%
dplyr::group_by(topic) %>%
dplyr::slice_max(beta, n = 5) %>%
dplyr::ungroup() %>%
dplyr::arrange(topic, -beta)
# 可视化
ggplot2::ggplot(top_terms, ggplot2::aes(x = reorder(term, beta), y = beta, fill = factor(topic))) +
ggplot2::geom_bar(stat = "identity", show.legend = FALSE) +
ggplot2::facet_wrap(~ topic, scales = "free") +
ggplot2::coord_flip() +
ggplot2::labs(
title = "LDA主题模型:每个主题最重要的词",
x = "词", y = "概率"
) +
ggplot2::theme_minimal()
文本分类 将文档分配到预定义的类别:
# 文本分类示例
# 使用朴素贝叶斯分类器
set.seed(42)
# 创建模拟的医疗文本分类数据
text_class_data <- data.frame(
text = c(
"患者血压偏高,诊断为高血压",
"血糖控制不佳,糖尿病诊断明确",
"心脏彩超显示心功能下降",
"胰岛素治疗糖尿病效果良好",
"高血压药物治疗后血压稳定",
"冠心病患者需要长期服药"
),
category = factor(c("高血压", "糖尿病", "心血管", "糖尿病", "高血压", "心血管"))
)
# 创建语料库
corpus <- tm::Corpus(tm::VectorSource(text_class_data$text))
# 预处理
corpus <- tm::tm_map(corpus, tm::content_transformer(tolower))
corpus <- tm::tm_map(corpus, tm::removePunctuation)
# 创建文档-词矩阵
dtm <- tm::DocumentTermMatrix(corpus)
# 划分训练集和测试集
train_idx <- sample(1:nrow(text_class_data), size = 4)
test_idx <- setdiff(1:nrow(text_class_data), train_idx)
train_dtm <- dtm[train_idx, ]
test_dtm <- dtm[test_idx, ]
train_labels <- text_class_data$category[train_idx]
test_labels <- text_class_data$category[test_idx]
# 使用朴素贝叶斯分类器
# 训练模型
classifier <- e1071::naiveBayes(as.matrix(train_dtm), train_labels)
# 预测
predictions <- predict(classifier, as.matrix(test_dtm))
# 评估
print("预测结果:")
## [1] "预测结果:"
print(data.frame(
实际 = test_labels,
预测 = predictions
))
## 实际 预测
## 1 糖尿病 高血压
## 2 心血管 高血压
# 计算准确率
accuracy <- mean(predictions == test_labels)
print(paste("准确率:", round(accuracy, 2)))
## [1] "准确率: 0"
词嵌入(Word Embedding) 将词映射到低维向量空间,捕捉语义关系:
| 方法 | 特点 |
|---|---|
| Word2Vec | 预测上下文或目标词 |
| GloVe | 基于全局共现矩阵 |
| FastText | 考虑子词信息 |
核心思想:语义相似的词在向量空间中距离较近。
# 词嵌入概念演示
# 注意:实际应用需要大量文本数据训练
# 模拟词嵌入向量
set.seed(42)
# 创建模拟的医疗词向量
words <- c("高血压", "血压", "心血管", "糖尿病", "血糖", "胰岛素",
"治疗", "药物", "患者", "疾病")
# 模拟2维词向量(实际通常是100-300维)
word_vectors <- data.frame(
word = words,
dim1 = c(1.2, 1.1, 1.0, -1.0, -1.1, -1.2, 0.1, 0.2, 0.0, 0.1),
dim2 = c(0.5, 0.4, 0.6, 0.5, 0.4, 0.6, -0.8, -0.7, -0.9, -0.8)
)
# 可视化词向量
ggplot2::ggplot(word_vectors, ggplot2::aes(x = dim1, y = dim2)) +
ggplot2::geom_point(size = 3, color = "steelblue") +
ggplot2::geom_text(ggplot2::aes(label = word), vjust = -1, size = 3) +
ggplot2::labs(
title = "词嵌入可视化(模拟)",
subtitle = "语义相似的词距离较近",
x = "维度1", y = "维度2"
) +
ggplot2::theme_minimal()
原理:找到与目标用户相似的用户,推荐这些用户喜欢的物品。
# 协同过滤示例
library(recommenderlab)
# 创建模拟的用户-物品评分矩阵
# 假设是患者-治疗方案评分
set.seed(42)
# 创建评分矩阵
n_users <- 20
n_items <- 10
# 模拟评分数据(1-5分)
ratings <- matrix(sample(c(1:5, NA), n_users * n_items, replace = TRUE,
prob = c(0.1, 0.1, 0.2, 0.3, 0.2, 0.1)),
nrow = n_users, ncol = n_items)
colnames(ratings) <- paste0("治疗", 1:n_items)
rownames(ratings) <- paste0("患者", 1:n_users)
# 转换为realRatingMatrix
rating_matrix <- as(ratings, "realRatingMatrix")
# 查看数据摘要
print("评分矩阵摘要:")
## [1] "评分矩阵摘要:"
print(dim(rating_matrix))
## [1] 20 10
print("评分分布:")
## [1] "评分分布:"
print(table(getRatings(rating_matrix)))
##
## 1 2 3 4 5
## 26 14 40 56 33
# 训练基于用户的协同过滤模型
ubcf_model <- recommenderlab::Recommender(
rating_matrix,
method = "UBCF",
param = list(method = "cosine", nn = 5)
)
# 为前3个患者推荐治疗
recommendations <- recommenderlab::predict(ubcf_model, rating_matrix[1:3], n = 3)
# 查看推荐结果
print("推荐结果:")
## [1] "推荐结果:"
rec_list <- as(recommendations, "list")
for (i in 1:3) {
cat("患者", i, "的推荐治疗:", rec_list[[i]], "\n")
}
## 患者 1 的推荐治疗:
## 患者 2 的推荐治疗:
## 患者 3 的推荐治疗: 治疗4
原理:找到与用户喜欢的物品相似的物品进行推荐。
# 基于物品的协同过滤
ibcf_model <- recommenderlab::Recommender(
rating_matrix,
method = "IBCF",
param = list(k = 5)
)
# 推荐
recommendations_ibcf <- recommenderlab::predict(ibcf_model, rating_matrix[1:3], n = 3)
# 查看推荐结果
print("基于物品的协同过滤推荐结果:")
## [1] "基于物品的协同过滤推荐结果:"
rec_list_ibcf <- as(recommendations_ibcf, "list")
for (i in 1:3) {
cat("患者", i, "的推荐治疗:", rec_list_ibcf[[i]], "\n")
}
## 患者 1 的推荐治疗:
## 患者 2 的推荐治疗:
## 患者 3 的推荐治疗: 治疗4
原理:将用户-物品矩阵分解为用户矩阵和物品矩阵的乘积:
\[R \approx U \times V^T\]
# 矩阵分解示例
# 使用SVD进行推荐
# 将评分矩阵转换为数值矩阵(NA用0填充)
ratings_filled <- ratings
ratings_filled[is.na(ratings_filled)] <- 0
# 执行SVD
svd_result <- svd(ratings_filled)
# 选择前k个奇异值
k <- 5
U_k <- svd_result$u[, 1:k]
V_k <- svd_result$v[, 1:k]
D_k <- diag(svd_result$d[1:k])
# 重构评分矩阵
ratings_predicted <- U_k %*% D_k %*% t(V_k)
# 查看预测评分
print("预测评分矩阵(前5行5列):")
## [1] "预测评分矩阵(前5行5列):"
print(round(ratings_predicted[1:5, 1:5], 2))
## [,1] [,2] [,3] [,4] [,5]
## [1,] 1.32 1.47 4.95 2.34 4.33
## [2,] 0.88 2.63 4.60 2.59 5.23
## [3,] 2.69 1.46 3.86 2.28 4.22
## [4,] 2.29 1.98 1.39 1.73 2.56
## [5,] 3.63 2.77 4.06 3.21 0.97
基于内容的推荐 根据物品特征和用户偏好进行推荐:
# 基于内容的推荐示例
# 假设治疗方案有特征描述
# 创建治疗方案特征
treatment_features <- data.frame(
治疗 = paste0("治疗", 1:6),
类型 = c("药物", "手术", "药物", "物理", "药物", "手术"),
侵入性 = c(1, 5, 1, 2, 1, 4),
成本 = c(2, 5, 3, 2, 4, 5),
恢复时间 = c(1, 5, 2, 2, 3, 4)
)
print("治疗方案特征:")
## [1] "治疗方案特征:"
print(treatment_features)
## 治疗 类型 侵入性 成本 恢复时间
## 1 治疗1 药物 1 2 1
## 2 治疗2 手术 5 5 5
## 3 治疗3 药物 1 3 2
## 4 治疗4 物理 2 2 2
## 5 治疗5 药物 1 4 3
## 6 治疗6 手术 4 5 4
# 用户偏好(假设用户偏好低侵入性、低成本)
user_preference <- c(侵入性 = -0.5, 成本 = -0.3, 恢复时间 = -0.2)
# 计算每个治疗的得分
scores <- as.matrix(treatment_features[, 3:5]) %*% user_preference
treatment_features$推荐得分 <- scores
# 排序推荐
treatment_features <- treatment_features[order(-treatment_features$推荐得分), ]
print("推荐排序:")
## [1] "推荐排序:"
print(treatment_features[, c("治疗", "类型", "推荐得分")])
## 治疗 类型 推荐得分
## 1 治疗1 药物 -1.3
## 3 治疗3 药物 -1.8
## 4 治疗4 物理 -2.0
## 5 治疗5 药物 -2.3
## 6 治疗6 手术 -4.3
## 2 治疗2 手术 -5.0
| 类型 | 说明 | 解决方案 |
|---|---|---|
| 新用户 | 没有用户历史数据 | 基于人口统计学推荐 |
| 新物品 | 没有物品评分数据 | 基于内容推荐 |
| 系统冷启动 | 系统刚上线 | 使用热门推荐 |
# 冷启动解决方案示例
# 新用户:基于人口统计学特征推荐
user_profiles <- data.frame(
用户 = paste0("用户", 1:5),
年龄段 = c("青年", "中年", "老年", "青年", "中年"),
性别 = c("男", "女", "男", "女", "男"),
偏好治疗 = c("治疗1", "治疗2", "治疗3", "治疗1", "治疗2")
)
# 对于新用户,根据人口统计学特征推荐
new_user <- data.frame(年龄段 = "青年", 性别 = "男")
# 找到相似用户
similar_users <- user_profiles %>%
dplyr::filter(年龄段 == new_user$年龄段, 性别 == new_user$性别)
print("相似用户的偏好:")
## [1] "相似用户的偏好:"
print(similar_users)
## 用户 年龄段 性别 偏好治疗
## 1 用户1 青年 男 治疗1
| 指标 | 公式 | 说明 |
|---|---|---|
| RMSE | \(\sqrt{\frac{1}{n}\sum(r - \hat{r})^2}\) | 预测误差 |
| 精确率@K | 相关推荐数/K | 前K个推荐的准确率 |
| 召回率@K | 相关推荐数/总相关数 | 找到相关物品的比例 |
| NDCG | 考虑排序位置的指标 | 排序质量 |
# 评估指标计算示例
# 使用交叉验证评估推荐模型
# 创建评估方案
evaluation_scheme <- recommenderlab::evaluationScheme(
rating_matrix,
method = "split",
train = 0.8,
given = 3,
goodRating = 4
)
# 训练多个模型
models <- list(
UBCF = recommenderlab::Recommender(getData(evaluation_scheme, "train"), method = "UBCF"),
IBCF = recommenderlab::Recommender(getData(evaluation_scheme, "train"), method = "IBCF")
)
# 评估
results <- recommenderlab::evaluate(evaluation_scheme, models, n = c(1, 3, 5))
## Error in h(simpleError(msg, call)) :
## error in evaluating the argument 'method' in selecting a method for function 'evaluate': $ operator not defined for this S4 class
## Error in h(simpleError(msg, call)) :
## error in evaluating the argument 'method' in selecting a method for function 'evaluate': $ operator not defined for this S4 class
# 查看结果
print("评估结果:")
## [1] "评估结果:"
print(results)
## List of evaluation results for 0 recommenders:
##
## named list()
# 绘制ROC曲线(如果数据充足)
tryCatch({
plot(results, annotate = TRUE, main = "推荐系统ROC曲线")
}, error = function(e) {
print("ROC曲线绘制需要更多数据")
})
## [1] "ROC曲线绘制需要更多数据"