Abstract
在对数据进行分析之前,需要对数据做一些预处理,包括数据分割、缺失值处理、删除近零方差变量、删除高度线性相关变量、数据标准化。
第一步是读入数据,对数据进行初步了解。下面以小说《三生三世十里桃花》中的人物信息为背景,具体的变量解释表如表1所示,其中因变量Y为“决定”这个变量。
data <- read.csv(here::here("Machine_Learning_and_Causal_Inference/data/相亲数据2.csv"),fileEncoding = "UTF-8")
data
## 'data.frame': 15 obs. of 12 variables:
## $ 名字 : chr "夜华" "白浅" "东华" "素锦" ...
## $ 决定 : int 1 0 0 0 1 0 0 0 1 1 ...
## $ 性别 : int 1 0 1 0 1 0 1 1 1 0 ...
## $ 是否喜欢矮矬穷 : int 0 0 0 0 0 0 0 0 0 0 ...
## $ 年龄 : int 50000 140000 360000 70000 140000 300 370000 23 31 23 ...
## $ 智力 : int 9 8 10 5 6 6 10 9 9 7 ...
## $ 对方名字 : chr "白浅" "离镜" "凤九" "离镜" ...
## $ 对方决定 : int 1 1 1 0 1 1 0 1 1 1 ...
## $ 对方性别 : int 0 1 0 1 0 1 0 0 1 1 ...
## $ 对方是否喜欢矮矬穷: int 0 0 0 0 0 0 0 0 0 0 ...
## $ 对方年龄 : int 140000 140000 30000 140000 100000 90000 70000 20 31 30 ...
## $ 对方智力 : int 8 6 5 6 4 7 5 7 7 8 ...
## Rows: 15
## Columns: 12
## $ 名字 <chr> "夜华", "白浅", "东华", "素锦", "离镜", "成玉", "折颜", "宇文玥", "...
## $ 决定 <int> 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0
## $ 性别 <int> 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1
## $ 是否喜欢矮矬穷 <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
## $ 年龄 <int> 50000, 140000, 360000, 70000, 140000, 300, 370000,...
## $ 智力 <int> 9, 8, 10, 5, 6, 6, 10, 9, 9, 7, 8, 5, 8, NA, 9
## $ 对方名字 <chr> "白浅", "离镜", "凤九", "离镜", "玄女", "连宋", "素锦", "大梁公主", "靖...
## $ 对方决定 <int> 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1
## $ 对方性别 <int> 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0
## $ 对方是否喜欢矮矬穷 <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
## $ 对方年龄 <int> 140000, 140000, 30000, 140000, 100000, 90000, 70000,...
## $ 对方智力 <int> 8, 6, 5, 6, 4, 7, 5, 7, 7, 8, 6, 9, 8, 8, 6
Name | Piped data |
Number of rows | 15 |
Number of columns | 12 |
_______________________ | |
Column type frequency: | |
character | 2 |
numeric | 10 |
________________________ | |
Group variables | None |
Variable type: character
skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
---|---|---|---|---|---|---|---|
名字 | 0 | 1 | 2 | 3 | 0 | 15 | 0 |
对方名字 | 0 | 1 | 2 | 4 | 0 | 14 | 0 |
Variable type: numeric
skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
---|---|---|---|---|---|---|---|---|---|---|
决定 | 0 | 1.00 | 0.47 | 0.52 | 0 | 0.00 | 0 | 1 | 1 | ▇▁▁▁▇ |
性别 | 0 | 1.00 | 0.53 | 0.52 | 0 | 0.00 | 1 | 1 | 1 | ▇▁▁▁▇ |
是否喜欢矮矬穷 | 0 | 1.00 | 0.00 | 0.00 | 0 | 0.00 | 0 | 0 | 0 | ▁▁▇▁▁ |
年龄 | 0 | 1.00 | 75447.73 | 127536.03 | 21 | 33.00 | 300 | 105000 | 370000 | ▇▂▁▁▂ |
智力 | 1 | 0.93 | 7.79 | 1.72 | 5 | 6.25 | 8 | 9 | 10 | ▇▂▆▇▃ |
对方决定 | 0 | 1.00 | 0.67 | 0.49 | 0 | 0.00 | 1 | 1 | 1 | ▃▁▁▁▇ |
对方性别 | 0 | 1.00 | 0.53 | 0.52 | 0 | 0.00 | 1 | 1 | 1 | ▇▁▁▁▇ |
对方是否喜欢矮矬穷 | 0 | 1.00 | 0.00 | 0.00 | 0 | 0.00 | 0 | 0 | 0 | ▁▁▇▁▁ |
对方年龄 | 0 | 1.00 | 47368.13 | 59187.35 | 20 | 31.00 | 300 | 95000 | 140000 | ▇▁▁▂▃ |
对方智力 | 0 | 1.00 | 6.67 | 1.40 | 4 | 6.00 | 7 | 8 | 9 | ▆▇▆▇▂ |
数据拿到之后,要先划分训练集和测试集。测试集是黑盒子,是不能触碰的炸弹,所做的任何处理,包括标准化、缺失值填补都只能基于训练集。下面介绍几个典型划分训练集和测试集的方法。
留出法分割是将样本分为两个互斥的子集,通常情况下,划分数据的80%为训练集,剩下的20%为测试集
之前提过,caret包
中的createDataPartition()函数
不仅可以实现这样的划分,而且可以保证训练集和测试集中Y的比例是一致的,简而言之就是按照Y进行分层抽样。
## Resample1
## [1,] 2
## [2,] 4
## [3,] 5
## [4,] 6
## [5,] 8
## [6,] 9
## [7,] 10
## [8,] 11
## [9,] 12
## [10,] 13
## [11,] 15
## 决定
## 0 1
## 0.5333333 0.4666667
## 决定
## 0 1
## 0.5454545 0.4545455
## 决定
## 0 1
## 0.5 0.5
交叉验证法将原始数据分成K组(一般是均分),每次训练将其中一组作为测试集,另外K-1组作为训练集。
实际应用中一般十折交叉验证用得最多,但是这里由于数据量太少,就以3折交叉验证为例(见图2),展示代码如下:
## [1] 3 1 3 2 3 3 1 2 1 2 2 2 3 1 1
## .
## 1 2 3
## 5 5 5
data_training <- data[-which(data_id == 1),]
data_testing <- data[which(data_id == 1),]
data_training
当数据量比较少时,Bootstrap抽样会成为“救命稻草”,它是一种从给定训练集中有放回的均匀抽样。也就是说,每当选中一个样本,它依然会被再次选中并被再次添加到训练集中。
createResample()函数
中times参数用于设定生成几份随机样本,当times为3,意味着生成3份样本。不仅不同sample之间会有交叉,就连同一份sample中也会有重复的样本。
## Resample1 Resample2 Resample3
## [1,] 2 3 2
## [2,] 4 4 3
## [3,] 5 4 4
## [4,] 5 4 5
## [5,] 6 4 7
## [6,] 6 4 8
## [7,] 6 5 9
## [8,] 6 8 10
## [9,] 7 8 11
## [10,] 9 8 12
## [11,] 10 14 13
## [12,] 10 14 14
## [13,] 12 14 15
## [14,] 12 14 15
## [15,] 15 15 15
上面这些划分训练集和测试集的方法都是针对横截面数据而言的,那么对于时间序列又该如何进行数据分割呢?
接下来,利用caret包
来分割时间序列。createTimeSlices()函数
需要输入以下参数:initialWindow
表示第一个训练集中的样本数;horizon参数
表示每个测试集中的样本数;fixedWindow参数
表示是否每个训练集中的样本数都相同。
从结果可以看出来,一共有2组训练集和测试集。第一组的训练集为1、2、3、4、5行观测,测试集为6、7行观测。那么第二组呢?从下面的数据就可以看出。
data_id <- createTimeSlices(data_growth$数值,initialWindow = 3,horizon = 2,fixedWindow = TRUE)
data_id
## $train
## $train$Training3
## [1] 1 2 3
##
## $train$Training4
## [1] 2 3 4
##
##
## $test
## $test$Testing3
## [1] 4 5
##
## $test$Testing4
## [1] 5 6
## $Training3
## [1] 1 2 3
##
## $Training4
## [1] 2 3 4
## $Testing3
## [1] 4 5
##
## $Testing4
## [1] 5 6
data_id1 <- createTimeSlices(data_growth$数值,initialWindow = 3,horizon = 2,fixedWindow = FALSE)
data_id1
## $train
## $train$Training3
## [1] 1 2 3
##
## $train$Training4
## [1] 1 2 3 4
##
##
## $test
## $test$Testing3
## [1] 4 5
##
## $test$Testing4
## [1] 5 6
caret包
中preProcess()函数
实现了两种常用的缺失值处理方法:中位数填补法、K近邻方法。
该方法直接用训练集的中位数代替缺失值,所以对于每个变量而言,填补的缺失值都相同,为训练集的中位数。该方法的优点是速度非常快,但填补的准确率有待验证。
data <- read.csv(here::here("Machine_Learning_and_Causal_Inference/data/相亲数据2.csv"),fileEncoding = "UTF-8")
data
set.seed(1234)
data_id <- createDataPartition(data$决定,p = 0.7,times = 1,list = FALSE)
data_training <- data[data_id,]
data_testing <- data[-data_id,]
data_training
训练集中所有人的智力中位数值是不是7.5?
## [1] 8
## [1] 0
## [1] 1
## Created from 11 samples and 12 variables
##
## Pre-processing:
## - ignored (2)
## - median imputation (10)
data_training_imputation <- predict(data_imputation_median,data_training)
all.equal(data_training,data_training_imputation) # 知道为什么是true吗?
## [1] TRUE
该方法的思想是“近朱者赤近墨者黑”。K近邻法对于需要插值的记录,基于欧氏距离计算k个和它最近的观测,然后接着利用k个近邻的数据来填补缺失值。
K近邻法会自动利用训练集的均值标准差信息对数据进行标准化,所以最后得到的数据是标准化之后的。如果想看原始值,那么还需要将其去标准化倒推回来。
## Warning in preProcess.default(data_training, method = "knnImpute"): These
## variables have zero variances: 是否喜欢矮矬穷, 对方是否喜欢矮矬穷
## Created from 11 samples and 12 variables
##
## Pre-processing:
## - centered (10)
## - ignored (2)
## - 5 nearest neighbor imputation (10)
## - scaled (10)
data_training_imputation <- predict(data_imputation_knn,data_training)
all.equal(data_training,data_training_imputation) # 知道为什么不是true吗?
## [1] "Component \"决定\": Mean relative difference: 1.088932"
## [2] "Component \"性别\": Mean relative difference: 1.088932"
## [3] "Component \"年龄\": Mean relative difference: 1"
## [4] "Component \"智力\": Mean relative difference: 1"
## [5] "Component \"对方决定\": Mean relative difference: 1"
## [6] "Component \"对方性别\": Mean relative difference: 1"
## [7] "Component \"对方年龄\": Mean relative difference: 1"
## [8] "Component \"对方智力\": Mean relative difference: 1"
data_training$智力 <- data_training_imputation$智力 * sd(data_training$智力,na.rm = TRUE) +
mean(data_training$智力,na.rm = TRUE)
data_testing$智力 <- data_testing_imputation$智力 * sd(data_training$智力,na.rm = TRUE) + mean(data_training$智力,na.rm = TRUE)
data_testing
零方差或者近零方差的变量传递不了什么信息,因为几乎所有人的取值都一样。可以利用caret包
中的nearZeroVar()函数
,一行代码就能找出近零方差的变量,操作过程非常简单。
## [1] 4 10
data_training_dropvariable <- data_training[,-nearZeroVar(data_training)]
data_testing_dropvariable <- data_testing[,-nearZeroVar(data_training)]
data_training_dropvariable
## 'data.frame': 4 obs. of 10 variables:
## $ 名字 : chr "夜华" "东华" "折颜" "罗子君"
## $ 决定 : int 1 0 0 1
## $ 性别 : int 1 1 1 0
## $ 年龄 : int 50000 360000 370000 35
## $ 智力 : num 9 10 10 7
## $ 对方名字: chr "白浅" "凤九" "素锦" "贺函"
## $ 对方决定: int 1 1 0 1
## $ 对方性别: int 0 0 0 1
## $ 对方年龄: int 140000 30000 70000 40
## $ 对方智力: int 8 5 5 8
caret包
中的findCorrelation()函数
会自动找到高度共线性的变量,并给出建议删除的变量。
但需要注意,这个函数对输入的数据要求比较高:
data_training %<>% drop_na()
data_training %>%
select(-nearZeroVar(data_training)) %>%
select(where(is.numeric)) %>%
cor() -> data_cor
data_cor
## 决定 性别 年龄 智力 对方决定
## 决定 1.00000000 0.26666667 -0.06595876 -0.16791512 -0.06900656
## 性别 0.26666667 1.00000000 -0.06198019 0.57091142 0.31052950
## 年龄 -0.06595876 -0.06198019 1.00000000 -0.26212272 0.19983184
## 智力 -0.16791512 0.57091142 -0.26212272 1.00000000 0.39396631
## 对方决定 -0.06900656 0.31052950 0.19983184 0.39396631 1.00000000
## 对方性别 -0.06900656 -0.82807867 -0.04628580 -0.37079182 -0.17857143
## 对方年龄 -0.35717624 -0.35636285 0.81215697 -0.48262196 0.10071829
## 对方智力 0.33565856 -0.23237900 -0.69247720 -0.05636215 -0.30735043
## 对方性别 对方年龄 对方智力
## 决定 -0.06900656 -0.3571762 0.33565856
## 性别 -0.82807867 -0.3563629 -0.23237900
## 年龄 -0.04628580 0.8121570 -0.69247720
## 智力 -0.37079182 -0.4826220 -0.05636215
## 对方决定 -0.17857143 0.1007183 -0.30735043
## 对方性别 1.00000000 0.2299345 0.28062430
## 对方年龄 0.22993447 1.0000000 -0.54965318
## 对方智力 0.28062430 -0.5496532 1.00000000
## Compare row 7 and column 3 with corr 0.812
## Means: 0.413 vs 0.31 so flagging column 7
## Compare row 2 and column 6 with corr 0.828
## Means: 0.378 vs 0.271 so flagging column 2
## All correlations <= 0.75
## [1] "对方年龄" "性别"
## Note: Using an external vector in selections is ambiguous.
## i Use `all_of(data_high_cor)` instead of `data_high_cor` to silence this message.
## i See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.
## This message is displayed once per session.
为什么要标准化?很简单,看看年龄,几十万岁,但是智力这个变量最高也才10分,这两列变量的量纲不同,为了防止年龄的权重过高,就需要将这些特征进行标准化才能学习各个变量真实的权重。需要注意的是:只能拿训练集的均值和标准差来对测试集进行标准化。
## Warning in preProcess.default(data_training, method = c("scale", "center")):
## These variables have zero variances: 是否喜欢矮矬穷, 对方是否喜欢矮矬穷
data_training_fin <- predict(data_proprocess_value,data_training)
data_testing_fin <- predict(data_proprocess_value,data_testing)
data_training