手把手教你用R处理常见的数据清洗问题(附步骤解析、R语言代码)
作者:James D. Miller
翻译:王雨桐
校对:万文菁
本文约5300字,建议阅读10+分钟。
本文将介绍数据清洗过程的主要步骤,并通过案例和代码演示如何利用R语言进行数据清洗。
R是进行运算、清洗、汇总及生成概率统计等数据处理的一个绝佳选择。此外,由于它独立于平台、短期内不会消失,所以生成的程序可以在任何地方运行。并且,它具备非常棒的辅助资源。
本文摘录自James D.Miller撰写的《数据科学统计学》(Statistics for Data Science)一书,该书由Packt Publishing出版。
R是一种易上手的语言和环境,它本身很灵活且专注于统计计算,因此成为运算、清洗、汇总及生成概率统计等数据处理的一个绝佳选择。
此外,以下是用R进行数据清洗的其他原因:
- 由于大量数据科学家都在使用R,所以它短时间内不会消失。
- R独立于平台,因此可以在任意地方运行程序。
- R有绝佳的辅助资源---Google一下,你就可以看到。
注:尽管作者将示例数据命名为“赌博数据”(Gamming Data),它只是用来演示代码的赌博数据。
离群点
对离群点最简单的解释是:离群点是和其余数据不匹配的数据点。按照惯例,任何过高、过低或者异常(基于项目背景)的数据都是离群点。作为数据清洗的一部分,数据科学家通常要识别出离群点并用通用的方法解决它:
- 删除离群点的值,甚至是离群点对应的实际变量。
- 转换变量值或变量本身。
让我们来看一下实际案例中如何用R识别并解决数据离群点。
老虎机在赌博界十分流行(老虎机的操作方法是把硬币投入到机器中,并拉动把手来决定回报)。如今大部分老虎机都电子化了,编程使它们的所有活动都能被持续追踪。在本文的案例中,赌场的投资者希望利用这些数据(以及各种补充数据)来调整盈利策略。换句话说,什么能让老虎机赚更多钱?是机器的主题还是类型?新机器比旧机器或老式机器更有利可图吗?机器的位置会产生怎样的影响?低面额的机器会赚更多钱吗?我们尝试用离群点来找到答案。
给定一个集合或赌博数据库(格式为逗号分隔或CSV文本文件),其中包括的数据如老虎机的位置、钱的面额、月份、日、年、机器类型、机器的年龄、促销、优惠券、天气和投币量(投币量是放入机器的钱币总额减去支付的数额)。
作为一个数据科学家,第一步要对数据进行综评(有时称为概述),此时我们要确定是否存在异常值,第二步是解决这些离群点。
步骤一 数据概述
R使这一步骤变得非常简单。尽管可以通过很多方式编程求解,但我们要尝试用最少的程序代码或脚本来解决问题。将CSV文件定义为R的变量(命名为MyFile)并将文件读入为数据框(命名为Mydata):
MyFile<-"C:/GammingData/SlotsResults.csv"
MyData<- read.csv(file=MyFile, header=TRUE, sep=",")
在统计学上,箱型图是一种简单的方式以得到统计数据集的分布、变异性和中心(或中位数)相关信息,所以我们将用箱型图来研究我们能否识别出中位数Coin-in以及能否找到离群点。为了达成这些,我们可以让R画出文件中每个老虎机的Coin-in值,绘制箱型图的函数如下:
boxplot(MyData[11],main='GammingData Review', ylab = "Coin-in")
注:Coin-in是文件中的第11列,所以直接将它作为boxplot函数的参数。此外还添加了一个可选择的参数(再次强调,本文已尽量保持代码的简洁度),以便在可视化图中添加标题。
执行前文的代码可以得到下图效果,包括中位数(中位数在箱型图中是中间横穿的线)以及四个离群点:
步骤2-处理离群点
现在我们发现数据中确实存在离群点,我们要解决这些点以保证它们不会对本研究产生负面影响。首先,我们知道Coin-in有负值是不合理的,因为机器输出的钱币一定不会比投入到机器中的硬币多。基于这个原则,我们可以从文件中删除Coin-in为负值的记录。此外,R可以帮助我们用subset生成一个新的数据框,新数据集中只有Coin-in中的非负值。
我们要将subset数据框命名为noNegs:
noNegs<- subset(MyData, MyData[11]>0)
接下来,我们要再一次画图以确定已经删除负值离群点:
boxplot(noNegs[11],main='GammingData Review', ylab = "Coin-in")
这就产生了新的箱型图,如下图中所示:
我们可以用同样的方法去除Coin-in中极端的正值(大于1500美元)得到另一个数据子集并再次画图:
noOutliers<-subset(noNegs, noNegs[11]<1500)
boxplot(noOutliers[11],main='GammingData Review', ylab = "Coin-in")
当你对数据进行不同的迭代后,建议你保存大部分版本的数据(如果不是最重要的)。你可以用write.csv这个R函数:
write.csv(noOutliers,file="C:/GammingData/MyData_lessOutliers.csv")
注:大部分数据科学家在整个项目中采取通用的命名规律。文件的名字应该尽可能清晰以便今后帮助你节省时间。此外,特别是在处理大量数据时,你需要注意内存空间的问题。
以上代码的输出结果如下:
领域知识
接下来,另一个数据清洗的技术是基于领域知识清理数据。这并不复杂,这种技术的关键是使用数据中无法察觉的信息。例如,当我们知道Coin-in不可能有负值时,我们排除了Coin-in负值的情况。另一个案例是飓风Sandy袭击美国东北部的时间。在这段时间内,机器的Coin-in值都很低(非零)。数据科学家应该基于信息判断是否要移除某段特定时期内的数据。
有效性检查
交叉验证是一种帮助数据科学家在数据库中使用规则的技术。
注:有效性检查是统计数据清洗中最普遍的形式,并且是数据开发者和数据科学家都非常熟悉的流程。
数据清洗时可以设定任意数量的有效性原则,这些原则要遵循数据科学家的意图或目标。例如有如下原则:数据类型(例如,某个字段一定要是数值型),范围限制(数据或日期要在一个特定范围内),要求(某个字段不能为空或没有值),唯一性(一个字段,或字段的结合,一定是数据库中唯一的),组成员(这个值一定是列表中的值),外键(案例中一定要被定义的明确的值或满足特殊规则),正则表达式模式(简单地说就是这个值的格式满足预设的格式),交叉字段验证(案例中的字段组合要满足特定标准)。
按照前文提到的内容,我们来看一些案例,从数据类型开始(也称为强制原则)。R提供的六个强制函数如下:
- as.numeric
- as.integer
- as.character
- as.logical
- as.factor
- as.ordered
- as.Date
这些函数,结合一些R的知识,使得在数据库中转换数据变得简单。例如,以前文的赌博数据为例,我们可以生成新的赌博结果文件,其中年龄值被存为字符型(或文本值)。为清理它,我们需要将其转化为数据型。我们可以运用以下R代码完成快速转化:
noOutliers["Age"]<-as.numeric(noOutliers["Age"])
一个需要注意的地方:用这种简单方法时,如果有数据不能转化,需要将其设定为NA值。在类型转换中,最大的工作是理解需要输入什么数据以及哪些数据类型是合法的;R有很广泛的数据类型,包括标量、向量(数值型,字符型,逻辑型),矩阵,数据框及列表。
数据清洗中我们要关注的另一个领域是正则表达式。在实践中,特别是当处理的数据来源于很多渠道时,数据科学家确实面对如下问题:字段不是理想的格式(对于当下目标而言)或者字段值的格式不一致(可能会引发错误的结果)。例如日期、社会安全号码(SSN)以及手机号码。基于数据的来源,你不得不重新输入(如前文描述),但是通常情况下,你需要基于目标将数据重新定义为可以使用的模式。
注:重新输入数据是很重要的,这样R就知道将值作为目前的数据并且你可以正确使用各种R数据函数。
一个常见的案例是当数据包括形式为YYYY/MM/DD的日期数据时,你想按每周汇总的形式呈现出时间序列分析,或者其他需要日期值的操作但是可能需要重新定义日期格式,或者你需要将其变为R日期类型。所以,假定一个新的赌博文件——只有两列数据:日期和投币量,这个文件是一个老虎机每天的投币量。
新的文件记录如下截图所示:
数据科学家可以用各种数据清洗的案例。从验证每个数据点的数据类型入手,我们可以用R函数class来验证文档的数据类型。首先(如我们在前文案例中所作),读入CSV文件存为数据框:
MyFile<-"C:/GammingData/SlotsByMachine.csv"
MyData<- read.csv(file=MyFile, header=TRUE, sep=",")
随后,我们可以使用class函数,如下图截图所示:
从上图中可以看到用class来显示数据类型。
MyData是用来保存赌博数据的数据框,日期Date是向量类型,投币量Coinin是一个整数。所以,数据框和整数是有意义的,但是要注意R将日期设置为向量(factor)类型。向量是分类变量,在汇总统计、绘图和回归中非常有用,但它不是非常适用日期型。为了解决这个问题,我们可以使用R函数substr和paste,如下所示:
MyData$Date<-paste(substr(MyData$Date,6,7),substr(MyData$Date,9,10), substr(MyData$Date,1,4),sep="/")
以上代码重新定义了日期字段的格式。它将数据字段值分成三部分(月、日和年)然后按照理想的顺序(/分隔符(sep))粘贴在一起,如下截图所示:
我们发现这一行脚本将日期字段转换为字符类型,最后我们可以用as.Date函数将值重设为日期(Date)类型:
稍微尝试一下,就可以重新格式化来得到理想的字符串或字符数据点。
改善数据
通过改善进行数据清理是另一种常见的技术,添加相关信息、事实或数据使得数据变得完整(可能更有价值)。这些附加数据的来源可以是用数据中现有信息或从其他来源添加信息进行计算。数据科学家花费时间完善数据的原因有很多。
基于当前的目的或目标,数据科学家补充的信息可能用于参考、比较、对比或发现趋势。
典型的用例包括:
- 衍生事实计算
- 对比日历与财政年度的使用
- 转换时区
- 货币转换
- 添加当前和前期指标
- 计算价值,如每天总出货量
- 保持缓慢变化的维度
注:作为数据科学家,你要经常用脚本来改善数据,这个方法要比直接编辑数据文档好得多,因为这样出错的可能性更低并且可以维持原始文件的完整性。此外,建立脚本可让你将改善的过程重复应用于多个文件或收到的新版文件中,不需要重做同样的工作。
回到我们的赌博数据中,假定我们在接收老虎机的投币量文档,同时公司在美国大陆外的地方设立赌场。这些新地点正在向我们发送文件,并且数据将纳入到我们的统计分析中。我们发现这些国际文件是以当地货币计算的投币量。为了正确地对数据建模,我们要将数据转化为美元。
场景如下:
文件来源:英国
使用货币:英镑
将英镑转化为美元的公式十分简单,只要用数额乘以汇率即可。所以,在R中:
MyData$Coinin<-MyData$Coinin* 1.4
以上代码可以完成我们想要的转换;然而,数据科学家要决定那种货币将被转化(英镑)以及汇率应当是多少。这并不是什么大问题,但是我们可以尝试创建一个用户定义的函数来确定要使用的汇率,如下所示:
getRate<- function(arg){
if(arg=="GPB") {
myRate <- 1.4
}
if(arg=="CAD") {
myRate <-1.34
}
return(myRate)
}
尽管之前的代码更简单,但以上代码说明了创建逻辑的要点,以便我们今后可以重复使用:
最终,为了使整个过程更完美,我们要将函数储存(在R文档中)以便将来使用:
source("C:/GammingData/CurerncyLogic.R")
随后:
MyFile<-"C:/GammingData/SlotsByMachine.csv"
MyData<- read.csv(file=MyFile, header=TRUE, sep=",")
MyData$Coin<- MyData$Coinin * getRate("CAD")
注:当然,在最理想的情况下,我们可改进函数以便在表或文件中根据国家代码查找汇率,这样汇率能够随即时价值而改变并且可以从程序中解耦数据。
数据调和
基于研究分析的整体目标,数据科学家可以通过数据调和来转换、翻译、或将数据值映射到其他理想值。最普遍的案例是性别或国家代码。例如,如果你的文档中将性别编码为0和1或M和F,你想将数据转化为一致的MALE或FEMALE。
关于国家代码,数据科学家想要绘制地区的汇总:北美、南美和欧洲,而不是分开的美国、加拿大、墨西哥、巴西、智利、英国、法国和德国。在这种情况下,将产生合计值如下:
北美=美国+加拿大+墨西哥
南美=巴西+智利
欧洲=英国+法国+德国
需要强调的是,数据科学家可能会将所有包括性别的调查文档合并在一起,称为gender.txt,但是文档中的性别编码不同(1,0,M,F,Male和Female)。如果我们尝试用R函数表,我们会看到如下可理解的结果:
如果在最理想的状态下进行可视化分析:
lbs= c("Male", "Female")
pie(table(MyData),main="Gambling by Gender")
我们看到如下截图:
为了解决性别数据编码不一致的问题,我借用了前文案例中的概念并生成简单的函数来帮助我们重新编码:
setGender<- function(arg){
if(substr(arg,1,1)=="0"| toupper(substr(arg,1,1))=="M") { Gender <- "MALE" }
if(substr(arg,1,1)=="1"| toupper(substr(arg,1,1))=="F") { Gender <- "FEMALE" }
return(Gender)
}
此次,我加入了toupper函数,因此我们不必担忧大小写,并且有substr来控制长度大于一个字符的值。
注:假定参数的值是0,1,m,M,f,F,Male或Female,否则将会引发报错。
由于R将性别作为向量类型,我发现很难应用简单的函数,所以我决定生成新的R数据框来容纳调和后的数据。并且用一个循环来读入文档中的记录并将其转化为Male 或Female:
MyFile<-"C:/GammingData/Gender.txt"
MyData<- read.csv(file=MyFile, header=TRUE, sep=",")
GenderData<-data.frame(nrow(MyData))
for(iin 2:nrow(MyData))
{
x<-as.character(MyData[i,1])
GenderData[i,1] <-setGender(x)
}
现在我们将通过以下语句得到更适合的可视化结果:
lbls= c("Male", "Female")
pie(table(GenderData),labels=lbls, main="Gambling by Gender")
以上代码的输出结果如下所示:
标准化
大多数主流数据科学家都已经注意到在开始统计研究或分析项目之前,将数据标准化作为数据清理过程一部分的重要性。这是很重要的,如果没有标准化,量纲不同的数据点对分析的贡献会不均等。
如果你认为在0到100之间的数据点比0到1范围内的变量影响更大,你可以理解数据标准化的重要性。使用这些未经过标准化的变量,事实上在分析中赋予较大范围的变量更多的权重。为了解决这一问题并均衡这些变量,数据科学家试图将数据转化为可比的量纲。
数据点的中心化是数据标准化中最常见的例子(尽管还有很多)。为了使数据点中心化,数据科学家把文件中的每个数据点减去所有数据的平均值。
R不是做运算,它提供了scale函数,其默认方法可以通过一行代码将文件中的数值中心化或缩减。让我们来看一个简单的例子。
回到老虎机的案例中!在我们的赌博文件中,你可能还记得有一个字段叫投币量(Coinin),它是一个表示投入到机器中美元总额的值,这被看作衡量机器盈利能力的指标。这似乎是我们盈利能力分析中使用的一个重要的数据点。然而这些金额可能是误导性的,因为不同的机器有不同面额(换句话说,一些机器接受美分,而其他机器接受一角硬币或美元)。也许机器面值的差别造成了不同的量纲,我们可以使用scale函数来解决这种情况。首先,我们在下面的截图中看到,Coin.in的值:
我们可以通过以下语句对数据点Coin.in进行中心化处理:
scale(MyData[11],center = TRUE, scale = TRUE)
center的值决定了如何行中心化。center为TRUE是需要对应的行减去Coin.in均值(省略NA)。scale的值决定了如何行缩放(在中心化之后)。如果scale的值是TRUE且center值是TRUE,那么缩放是通过除以(中心化后的)Coin.in的标准差来进行的。如果center值是False,将得到均方根值。
在下图截屏中看到了差别:
原文标题:How to tackle common datacleaning issues in R
原文链接:https://www.kdnuggets.com/2018/05/ packt-tackle-common-data-cleaning-issues-r.html
译者简介
王雨桐,统计学在读,数据科学硕士预备,跑步不停,弹琴不止。梦想把数据可视化当作艺术,目前日常是摸着下巴看机器学习。