通过将《机器学习与R语言》一书中的代码tidyverse化,来学习这本书。

书中第一个例子是利用kNN算法来诊断乳腺癌。

首先载入需要用到的包:

library(tidyverse) # 清洗数据
library(here) # 设置数据文件路径
library(knitr) # 呈现更好看的表格
library(kableExtra) # 同上
library(class) # 使用包中的knn()函数
library(gmodels) # 使用包中的CrossTable()函数

然后导入数据并清洗:

wbcd <- read_csv(here('content', 'post', 'data', '01-wisc_bc_data.csv')) %>% 
  select(-id) %>% 
  mutate(diagnosis = factor(diagnosis, levels = c('B', 'M'),
                            labels = c('Benign', 'Malignant'))) %>% 
  mutate_if(is.numeric, ~ (.x - min(.x)) / (max(.x) - min(.x)))

首先使用here函数找到数据文件的路径,然后使用read_csv函数将其读入R中;随后通过select函数将id变量去掉;然后利用mutate函数将diagnosis变量改为因子型;最后利用mutate_if函数,将所有数值型的变量进行min-max标准化,这里用到了公式化的匿名函数,可以使代码更为简练。此时的数据是这样的:

wbcd %>% head() %>% 
  kable() %>% 
  kable_styling(bootstrap_options = "striped", font_size = 12) %>%
  scroll_box(width = "100%") 
diagnosis radius_mean texture_mean perimeter_mean area_mean smoothness_mean compactness_mean concavity_mean concave points_mean symmetry_mean fractal_dimension_mean radius_se texture_se perimeter_se area_se smoothness_se compactness_se concavity_se concave points_se symmetry_se fractal_dimension_se radius_worst texture_worst perimeter_worst area_worst smoothness_worst compactness_worst concavity_worst concave points_worst symmetry_worst fractal_dimension_worst
Malignant 0.5210374 0.0226581 0.5459885 0.3637328 0.5937528 0.7920373 0.7031396 0.7311133 0.6863636 0.6055181 0.3561470 0.1204694 0.3690336 0.2738113 0.1592956 0.3513984 0.1356818 0.3006251 0.3116452 0.1830424 0.6207755 0.1415245 0.6683102 0.4506980 0.6011358 0.6192916 0.5686102 0.9120275 0.5984624 0.4188640
Malignant 0.6431445 0.2725736 0.6157833 0.5015907 0.2898799 0.1817680 0.2036082 0.3487575 0.3797980 0.1413227 0.1564367 0.0825893 0.1244405 0.1256598 0.1193867 0.0813230 0.0469697 0.2538360 0.0845388 0.0911101 0.6069015 0.3035714 0.5398177 0.4352143 0.3475533 0.1545634 0.1929712 0.6391753 0.2335896 0.2228781
Malignant 0.6014956 0.3902604 0.5957432 0.4494168 0.5143089 0.4310165 0.4625117 0.6356859 0.5095960 0.2112468 0.2296216 0.0943025 0.1803704 0.1629218 0.1508312 0.2839547 0.0967677 0.3898466 0.2056903 0.1270055 0.5563856 0.3600746 0.5084417 0.3745085 0.4835898 0.3853751 0.3597444 0.8350515 0.4037059 0.2134330
Malignant 0.2100904 0.3608387 0.2335015 0.1029056 0.8113208 0.8113613 0.5656045 0.5228628 0.7762626 1.0000000 0.1390911 0.1758752 0.1266550 0.0381548 0.2514532 0.5432151 0.1429545 0.3536655 0.7281477 0.2872048 0.2483102 0.3859275 0.2413467 0.0940081 0.9154725 0.8140117 0.5486422 0.8848797 1.0000000 0.7737111
Malignant 0.6298926 0.1565776 0.6309861 0.4892895 0.4303512 0.3478928 0.4639175 0.5183897 0.3782828 0.1868155 0.2338222 0.0930649 0.2205626 0.1636876 0.3323588 0.1679184 0.1436364 0.3570752 0.1361794 0.1457996 0.5197439 0.1239339 0.5069476 0.3415749 0.4373638 0.1724151 0.3194888 0.5584192 0.1575005 0.1425948
Malignant 0.2588386 0.2025702 0.2679842 0.1415058 0.6786133 0.4619962 0.3697282 0.4020378 0.5186869 0.5511794 0.0807532 0.1171322 0.0687933 0.0380801 0.1970629 0.2343107 0.0927273 0.2153817 0.1937299 0.1446596 0.2682319 0.3126333 0.2639076 0.1367479 0.7127386 0.4827837 0.4277157 0.5982818 0.4770353 0.4549390

书中还提到了Z分数标准化,因为有现成的scale函数,所以代码会稍微简单:

wbcd <- read_csv(here('data', '01-wisc_bc_data.csv')) %>% 
  select(-id) %>% 
  mutate(diagnosis = factor(diagnosis, levels = c('B', 'M'),
                            labels = c('Benign', 'Malignant'))) %>% 
  mutate_if(is.numeric, scale)

下一步是创建训练数据集和测试数据集。首先先设定一个随机种子,保证结果可以复现,然后利用sample_n函数从完整数据中随机选择469行作为训练数据集,并利用setdiff函数筛选出训练数据集的补集作为测试数据集;最后利用pull函数把标签提取出来:

set.seed(0412)
wbcd_train <- wbcd %>% sample_n(469)
wbcd_test <- wbcd %>% setdiff(wbcd_train)
wbcd_train_labels <- wbcd_train %>% pull(1)
wbcd_test_labels <- wbcd_test %>% pull(1)

数据已经整理好,可以建模了,但是在书中没有看到将数据集中的标签变量去掉的过程,所以在这里的模型中,我把两个数据集的标签变量都去掉了:

wbcd_test_pred <- knn(train = wbcd_train[, -1], test = wbcd_test[, -1], 
                      cl = wbcd_train_labels, k = 21)

看一下模型的性能:

CrossTable(wbcd_test_labels, wbcd_test_pred, prop.chisq = FALSE)
## 
##  
##    Cell Contents
## |-------------------------|
## |                       N |
## |           N / Row Total |
## |           N / Col Total |
## |         N / Table Total |
## |-------------------------|
## 
##  
## Total Observations in Table:  100 
## 
##  
##                  | wbcd_test_pred 
## wbcd_test_labels |    Benign | Malignant | Row Total | 
## -----------------|-----------|-----------|-----------|
##           Benign |        68 |         0 |        68 | 
##                  |     1.000 |     0.000 |     0.680 | 
##                  |     0.986 |     0.000 |           | 
##                  |     0.680 |     0.000 |           | 
## -----------------|-----------|-----------|-----------|
##        Malignant |         1 |        31 |        32 | 
##                  |     0.031 |     0.969 |     0.320 | 
##                  |     0.014 |     1.000 |           | 
##                  |     0.010 |     0.310 |           | 
## -----------------|-----------|-----------|-----------|
##     Column Total |        69 |        31 |       100 | 
##                  |     0.690 |     0.310 |           | 
## -----------------|-----------|-----------|-----------|
## 
## 

跟书中的结果不一样,但也不错。

最后,书中还使用不同的k值对模型进行了评估,但没有给出相应的代码,我这里补充了一下:

k <- map(1:30, ~ knn(train = wbcd_train[, -1], test = wbcd_test[, -1], 
         cl = wbcd_train_labels, k = .x)) %>% 
  enframe(name = 'k', value = 'prediction') %>% 
  unnest() %>% 
  mutate(label = rep(wbcd_test_labels, 30),
         FN = prediction == 'Malignant' & label == 'Benign',
         FP = prediction == 'Benign' & label == 'Malignant') %>% 
  group_by(k) %>% 
  summarise(FN = sum(FN), 
            FP = sum(FP),
            total = FN + FP)

首先利用map函数将1到30分别映射到模型的k参数上,此时得到了会是一个长度为30的列表;随后利用enframe函数将列表变为行数为30的数据框,这时value变量下的每一个元素都包含100个字符;随后利用unnest将value变量中的字符解放出来,使数据框的行数变为3000;剩余的代码就比较简单,不多描述。

这时的数据是这样的:

k %>% kable() %>% 
  kable_styling(bootstrap_options = "striped", font_size = 12) %>%
  scroll_box(width = "100%") 
k FN FP total
1 2 1 3
2 4 0 4
3 2 0 2
4 4 0 4
5 2 0 2
6 2 0 2
7 3 0 3
8 3 0 3
9 2 0 2
10 3 1 4
11 0 0 0
12 0 0 0
13 0 0 0
14 0 0 0
15 0 0 0
16 0 0 0
17 0 0 0
18 0 1 1
19 0 1 1
20 0 1 1
21 0 1 1
22 1 1 2
23 1 1 2
24 1 1 2
25 1 1 2
26 1 1 2
27 1 0 1
28 1 1 2
29 1 1 2
30 1 0 1

可以看到,k值从11到17时的结果都很“完美”。