面向医学生/医生的实用机器学习教程系列推文
数据预处理对获得表现良好的模型有非常重要的作用!
是金子总会发光,一个未经雕琢的数据,本身的价值也难以得到体现,通过数据预处理,可以让数据展现真正的价值;另外,不同的模型对数据有不同的要求,通过预处理可以让数据符合算法的要求,这样也能提高模型的预测能力。这就是数据预处理的重要作用!
但是,一个本身就没有什么价值的数据,再好的模型也得不出理想的结果,这就是我常说的:鸭子是不会变成天鹅的!
这样一个雕琢数据,精细打磨的过程有一个专门的名字:特征工程。
但是要注意,数据预处理不是单纯的数字操作,一定要结合自己的实际情况!
今天的推文给大家介绍一些临床预测模型和机器学习常用的数据预处理方法。
最有效的数据预处理的方法来自于建模者对数据的理解,而不是通过任何数学方法。
本期目录:
中心化和标准化可以解决这样的问题。
中心化是将所有变量减去其均值,其结果是变换后的变量均值为0;标准化是将每个变量除以其自身的标准差,标准化迫使变量的标准差为1。
R语言中scale()函数可实现中心化和标准化,就不多做介绍了。
无偏分布类似我们常说的正态分布,有偏分布又分为右偏和左偏,分别类似正偏态分布和负偏态分布。
一个判断数据有偏的黄金标准:如果最大值与最小值的比例超过20,那么我们认为数据有偏。
可以通过计算偏度统计量来衡量偏度。如果预测变量分布是大致对称的,那么偏度将接近于0,右偏分布偏度大于0,越大说明偏的越厉害;左偏分布偏度小于0,越小说明偏的越厉害。
计算偏度的包很多。
使用e1071包查看变量的偏度
library(e1071)#查看偏度skewness(segData$AngleCh1)##[1]-0.02426252#查看每一列的偏度skewValues<-apply(segData,2,skewness)head(skewValues)##AngleCh1AreaCh1AvgIntenCh1AvgIntenCh2AvgIntenCh3AvgIntenCh4##-0.024262523.525107452.959185240.848160332.202342141.90047128也可以通过psych包查看:
psych::skew(segData$AngleCh1)#偏度##[1]-0.02426252psych::kurtosi(segData$AngleCh1)#峰度##[1]-0.8594789通过对数据进行变换可以一定程度解决偏度的问题,常用的方法有:取对数(log),平方根,倒数,Box&Cox法等。
log、平方根、倒数这些很简单,就不演示了,下面演示下BoxCox变换。
#准备对数据进行BoxCox变换Ch1AreaTrans<-BoxCoxTrans(segData$AreaCh1)Ch1AreaTrans##Box-CoxTransformation####1009datapointsusedtoestimateLambda####Inputdatasummary:##Min.1stQu.MedianMean3rdQu.Max.##150.0194.0256.0325.1376.02186.0####Largest/Smallest:14.6##SampleSkewness:3.53####EstimatedLambda:-0.9#进行变换AreaCh1_transed<-predict(Ch1AreaTrans,segData$AreaCh1)#查看变换前、后的数据head(segData$AreaCh1)##[1]819431298256258358head(AreaCh1_transed)##[1]1.1084581.1063831.1045201.1035541.1036071.105523这里可以看到caret对数据预处理的方式,首先是选择方法,然后使用predict()函数把变换应用到具体的变量上。这是caret的基本操作,大家一定要记住!
对于变换前后的数据变化,只看数字没有直观的感受,下面给大家画图演示。
psych::skew(AreaCh1_transed)##[1]0.0976087下面是BoxCox变换的一点点扩展,不看也影响不大。
BoxCox变换需要一个参数lambda,这个参数需要我们计算并指定,如上使用caret进行变换时,它会自动帮我们处理好,其中一句代码显示EstimatedLambda:-0.9,也就是lambda=0.9。
还有很多R包可以实现BoxCox变换,其中比较简单的是forecast,简单演示如下:
library(forecast)##RegisteredS3methodoverwrittenby'quantmod':##methodfrom##as.zoo.data.framezoobest.lambda<-BoxCox.lambda(segData$AreaCh1)#计算lambdabest.lambda##[1]-0.9999264AreaCh1.transformed<-BoxCox(segData$AreaCh1,lambda=best.lambda)#变换head(AreaCh1.transformed)##[1]0.99885190.99775220.99671630.99616550.99619580.9972789y0<-InvBoxCox(AreaCh1.transformed,lambda=best_lambda)#还原##ErrorinInvBoxCox(AreaCh1.transformed,lambda=best_lambda):object'best_lambda'notfound解决离群值离群值其实是有明确定义的,通常我们会选择直接删除离群值,但是还是要根据实际情况来看,有的离群值是非常有意义的,这样的离群值不能直接删除。
有些模型对离群值很敏感,比如线性模型,这样是需要处理的,一个常见的方法是空间表示变换,该变换将预测变量取值映射到高纬的球上,它会把所有样本变换到离球心相等的球面上。在caret中可以实现。关于它的具体数学运算过程,感兴趣的自己了解即可,我不太感兴趣。
在进行空间表示变换前,最好先进行中心化和标准化,这也和它的数学计算有关,我也不太感兴趣。
对数据进行PCA变换之前,最好先解决偏度问题,然后进行中心化和标准化,和它的数学计算过程有关,感兴趣的自己了解。
可视化前后不同:
这里的过滤和解决共线性,其实部分属于特征选择的范围,就是大家常见的自变量选择问题,这个问题在以后的推文中还会详细介绍。
冗余的变量通常增加了模型的复杂度而非信息量
如果一个变量只有1个值,那么这个变量的方差为0;如果一个变量只有少量不重复的取值,这种变量称为近零方差变量;这2种变量包含的信息太少了,应当过滤;
检测近零方差变量的准则是:
移除共线变量的方法如下:
caret可以轻松实现以上过程。
使用mdrr数据集演示。其中一列nR11大部分都是501,这种变量方差是很小的!
data(mdrr)table(mdrrDescr$nR11)#大部分值都是0####012##501423sd(mdrrDescr$nR11)^2#方差很小!##[1]0.1731787使用nearZeroVar()找出零方差和近零方差变量,结果中会给出zeroVar和nzv两列,用逻辑值表示是不是近零方差变量或者零方差变量。
nzv<-nearZeroVar(mdrrDescr,saveMetrics=TRUE)nzv[nzv$nzv,][1:10,]##freqRatiopercentUniquezeroVarnzv##nTB23.000000.3787879FALSETRUE##nBR131.000000.3787879FALSETRUE##nI527.000000.3787879FALSETRUE##nR03527.000000.3787879FALSETRUE##nR08527.000000.3787879FALSETRUE##nR1121.782610.5681818FALSETRUE##nR1257.666670.3787879FALSETRUE##D.Dr03527.000000.3787879FALSETRUE##D.Dr07123.500005.8712121FALSETRUE##D.Dr08527.000000.3787879FALSETRUE去掉近零方差变量:
ltfrDesign<-matrix(0,nrow=6,ncol=6)ltfrDesign[,1]<-c(1,1,1,1,1,1)ltfrDesign[,2]<-c(1,1,1,0,0,0)ltfrDesign[,3]<-c(0,0,0,1,1,1)ltfrDesign[,4]<-c(1,0,0,1,0,0)ltfrDesign[,5]<-c(0,1,0,0,1,0)ltfrDesign[,6]<-c(0,0,1,0,0,1)ltfrDesign##[,1][,2][,3][,4][,5][,6]##[1,]110100##[2,]110010##[3,]110001##[4,]101100##[5,]101010##[6,]101001findLinearCombos()可以通过算法给出需要去除的变量,关于具体的方法可以官网查看。
comboInfo<-findLinearCombos(ltfrDesign)comboInfo##$linearCombos##$linearCombos[[1]]##[1]312####$linearCombos[[2]]##[1]6145######$remove##[1]36结果给出了需要去除的变量是第3列和第6列。
这里介绍下独热编码(one-hotencoding),和哑变量编码稍有不同,哑变量是变成k-1个变量,独热编码是变成k个变量。
使用以下数据进行演示
data("cars",package="caret")head(cars)##PriceMileageCylinderDoorsCruiseSoundLeatherBuickCadillacChevy##122661.052010564100100##221725.011345762110001##329142.713165542111000##430731.942247942100000##533358.771759042111000##630315.172363542100000##PontiacSaabSaturnconvertiblecoupehatchbacksedanwagon##100000010##200001000##301010000##401010000##501010000##601010000type<-c("convertible","coupe","hatchback","sedan","wagon")cars$Type<-factor(apply(cars[,14:18],1,function(x)type[which(x==1)]))carSubset<-cars[sample(1:nrow(cars),20),c(1,2,19)]#上面是数据生成过程,不重要,记住下面这个数据的样子即可!!head(carSubset)##PriceMileageType##62120902.1029649sedan##15417675.845131coupe##57327703.2024738sedan##60113106.9021910coupe##36117119.4618277sedan##43032501.2517508sedanlevels(carSubset$Type)#Type是一个因子型变量##[1]"convertible""coupe""hatchback""sedan""wagon"现在把Type这个变量进行独热编码。
使用dummyVars构建虚拟变量:
simpleMod<-dummyVars(~Mileage+Type,#用mileage和Type对价格进行预测data=carSubset,levelsOnly=TRUE)#从列名中移除因子变量的名称simpleMod##DummyVariableObject####Formula:~Mileage+Type##2variables,1factors##Factorvariablenameswillberemoved##Alessthanfullrankencodingisused接下来就可以使用predict和simpleMod对训练集进行生成虚拟变量的操作了:
predict(simpleMod,head(carSubset))##Mileageconvertiblecoupehatchbacksedanwagon##6212964900010##154513101000##5732473800010##6012191001000##3611827700010##4301750800010可以看到Type变量没有了,完成了虚拟变量的转换。
假如你认为车型和里程有交互影响,则可以使用:表示:
withInteraction<-dummyVars(~Mileage+Type+Mileage:Type,data=carSubset,levelsOnly=TRUE)withInteraction##DummyVariableObject####Formula:~Mileage+Type+Mileage:Type##2variables,1factors##Factorvariablenameswillberemoved##Alessthanfullrankencodingisused应用于新的数据集:
predict(withInteraction,head(carSubset))##MileageconvertiblecoupehatchbacksedanwagonMileage:Typeconvertible##62129649000100##1545131010000##57324738000100##60121910010000##36118277000100##43017508000100##Mileage:TypecoupeMileage:TypehatchbackMileage:TypesedanMileage:Typewagon##62100296490##1545131000##57300247380##60121910000##36100182770##43000175080区间化预测变量主要是为了好解释结果,比如把血压分为高血压1级、2级、3级,把贫血分为轻中重极重等,这样比如你做logistic回归,可以说血压每增高一个等级,因变量的风险增加多少,但是你如果说血压值每增加1mmHg,因变量增加多少倍,这就有点扯了。
在caret中是通过preProcess()函数里面的method参数实现的,把不同的预处理步骤按照顺序写好即可。
以上就是数据预处理的一般过程,一个caret包可以解决上面所有的问题,有兴趣的小伙伴可以自行学习。
数据预处理是一个非常系统且专业的过程,如同开头说的那样:最有效的编码数据的方法来自于建模者对数据的理解,而不是通过任何数学方法,在对数据进行预处理之前,一定要仔细理解自己的数据哦,结果导向的思维是不对的哦!