《数据科学R语言实践:面向计算推理与问题求解的案例研究法》一一2.3 数据清洗和变量格式化-阿里云开发者社区

开发者社区> 华章出版社> 正文
登录阅读全文

《数据科学R语言实践:面向计算推理与问题求解的案例研究法》一一2.3 数据清洗和变量格式化

简介:

本节书摘来自华章计算机《数据科学R语言实践:面向计算推理与问题求解的案例研究法》一书中的第2章,第2.3节,作者:[美] 德博拉·诺兰(Deborah Nolan)  邓肯·坦普·朗(Duncan Temple Lang)  更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.3 数据清洗和变量格式化

本节我们考虑如何将特征矩阵列表menResMat转换为合适的格式以便于数据分析。目前,这些数据值都是字符型,这对于诸如找到参赛者年龄的中位数这样的数据分析是无益的。但是,我们可以利用as.numeric()函数很容易地将年龄转换为数值型。我们需要将整个矩阵都转换为数值型矩阵吗?事实并非如此,比如将参赛者的名字转换为数值型就毫无意义。为此,我们需要创建一个可以允许拥有不同类型变量的数据框。现在我们有6个变量:参赛者姓名、居住地、年龄以及3种类型的时间。正如刚才所说,我们将年龄转换为数值型,而名字依然保留为字符型。那么其他的变量呢?我们或许也要将居住地保留为字符型。
时间被存储为一个字符串,其格式为hh:mm:ss。为了更容易地生成概要和进行建模,我们需要将时间转换为数值型。一种可行的方式是将时间转换为分钟数,即hh*60+mm+ ss/60。要执行这样的计算,我们必须将时间字段分割为成组片段,然后把每组片段转换为数值型。strsplit()函数能够帮助我们在诸如冒号的地方对字符串进行分割,另外,我们需要让3种不同类型的记录时间(比赛时间、净时间和平常的终场时间)保持一致。净时间被认为要比比赛时间更加准确,所以当可以获得净时间时,我们就使用净时间,否则,使用比赛时间或终场时间中被记录的任何一种。当然我们也可以保存全部3种不同类型的时间,由分析师来判断它们之间的关系并决定使用哪种时间,但是现在为了处理简便,我们仅考虑为每个参赛者记录一种时间。
在将字符串转换为数值型值之前,我们还要考虑是否应该创建一些新的变量。如果要将所有14年的记录数据整合到一个数据框中,那么我们就应该记录这些数据的年份。同样,如果我们将男女参赛选手的数据整合到一起,那么就需要一个变量来表示选手的性别。使用rep()函数可以很简单地实现这些操作。
我们首先使用as.numeric()函数来创建数值型变量—年龄,以2012年的男选手数据为例:
image

我们收到了一条警告信息,提示在将年龄从字符型转化到数值型的过程中出现了NA值,这意味着一些值并不对应相应的数字。要想进一步探究出现这些信息的具体原因,首先我们要检查age。
我们根据参赛选手每年的年龄分布,创建并排式箱线图以快速地检查年龄值的合理性。
image

图2-4揭示了其中2个年份数据中的问题。2003年中所有参赛选手的年龄均小于10,而2006年超过1/4的参赛选手的年龄小于10,显然这是有问题的。

图2-4 历年年龄数据的箱线图。这些并排的年龄箱线图表明2003年和2006年的数据存在一些问题。这几年的参赛选手异乎寻常地年轻
下面让我们来检查2003年和2006年的原始文本。
image

我们注意到在2003年中,年龄值列相较于它对应的‘=’字符向右偏移了一位,这就意味着我们只取了年龄值中十位上的数字。而在2006年,某些行(并不是所有行)的年龄值向外溢出了一个字符。
我们可以简单地通过将列之间的空白字符包含到列值中来解决这两个问题,在执行数据抽取时,通过改变每个变量结束的索引就可以实现这个操作。也就是说,修改selectCols()函数中定位每一列结束位置的那行代码以包含空白字符,即

当我们在selectCols()函数中使用这个修改计算后,每个字段后的空白字符将会被包含进来。而这并不影响我们将文本数据转换为数值型,而且如果不想让字符型变量以空白符结尾,我们也可以通过正则表达式很容易地移除这些空白字符。
在确认年龄从字符型转换为数值型的过程中,发现了数据抽取中存在的问题。我们需要修改2.2节中的辅助函数selectCols()来处理这个问题。由于我们要不断去检查得到的数据是否合理,因此该处理是个迭代的过程。当发现无意义的结果时,需要进一步研究它们,这可能使得我们需要折回到前面的步骤以清理脏数据。
在修改了selectCols()函数中的这一行代码后,我们将更新后的函数版本再次应用于比赛结果表中,使用箱线图重新对数据进行汇总统计,此时我们会发现存在过多年轻选手的问题已经清除(见图2-5)。
现在我们转向上面处理字符型年龄转换为数值型时显示的警告消息,有几条消息为“NA introduced by coercion”。统计每年数据中NA值的个数:
image

图2-5 历年年龄数据的箱线图。这些并排的年龄箱线图显示了一个合理的年龄分布。例如,所有年度年龄范围的下四分位数都在29到32之间。之前发现的2003年和2006年的数据问题已被解决
2001年中有61个值为NA的年龄,我们需要探讨此问题。为了更便于工作,我们使用如下方式,将2001年的年龄数据赋值到名为age2001的向量中:

下面我们来检查与向量age2001中某个NA值对应的原始文件中的行。回想一下,我们在抽取变量值之前舍弃了原始文件的头部,因此,为了能够从原始数据表中读取到正确的行,我们需要给age2001中NA所在行的位置加上一个偏移量。采用如下方法确定偏移量:

然后我们采用下列方法,找到在原始文件中年龄值为坏数据的行:
image

除了最后一行外,其余的行全部为空串。最后一行对应的是脚注,它定义了“#”标识符的含义。那么这些空白行在表格中又处于什么位置呢?
image

可见这些空白行分散在文件中的各个位置。我们可以在数据抽取过程中使用正则表达式检测这些空白行并把它们移除。

上述表达式将定位所有只包含空格的行。grep的第一个参数采用几个元字符指定我们将要匹配的字符串模式。其中,“^”锚定字符串的开始位置,“$”锚定字符串的结尾,“[[:blank:]]”指代的是空格符或Tab符的等价类,“”表示空格符可以出现0次或多次。整个表达式“^[[:blank:]]$”表示能够匹配从开头到结尾含有任意个空格符的字符串。也就是只含有空格字符的行。
采用一个简单的表达式可以定位注解行,即以“#”或“*”开始的行。通过修改extractVariables()函数移除那些我们不想要的行,在此我们将该任务留作练习。通过添加上述代码对数据表进行额外的清洗后,2001年中的61个NA都被消除了,同时其他年份中的很多(但不是所有的)NA也被消除了。
继续考察图2-5所暴露的另一个问题:2001、2002、2003年中年龄的最小值都很小,接近于0,这显然是不可能的。下面让我们找出这些年龄值小于5的参赛选手,并从原始数据表中找出他们的参赛信息。以2001年数据为例:
image

显然有许多参赛选手的年龄输入为0!鉴于这些都是表中的实际值,我们在后面分析数据时,可以根据需要来决定如何处理这些选手的数据。至此,我们已经成功地创建了年龄变量。然而,由于一个变量基于位置上的错误往往会导致在其他变量上产生错误,一般我们需要对所有变量同时进行清洗。这样,在清洗其他变量时,我们可能也需要再次检查年龄数据,以确保年龄值仍然有效。
接下来我们进行时间变量的创建。如本节开头所述,时间格式显示为hh:mm:ss,我们希望将它转换成分钟数。然而,为了执行转换,我们必须将时间字段分割为分组片段的形式。此外,一些参赛选手的比赛用时不到一个小时,这样他们的时间显示稍有不同,即为mm:ss,因此,该过程中我们需要能够处理上述两种格式。为了简便起见,我们同样从转换某一年的时间变量开始,如以2012年的数据为例。编写如下代码来创建向量:
image

处理过程中字符串中的“:”将被舍弃,从strsplit()函数返回的是一个字符向量列表。每个输入的时间字符串对应一个向量,向量中的元素为时间字符串中被每个“:”分开的片段。我们通过检查第一个和最后一个时间来确认分割是否正确,即
image

显然我们的时间转换是正确的。在前面我们看到2012年最快的参赛选手完成比赛用时为45分15秒,也就是45.25分钟;最慢的参赛者用时为2小时30分钟59秒,也就是将近151分钟。在此我们留一练习:将该转换过程封装到名为convertTime()的函数中。
下面我们将这些转换过程打包到一个函数中,并将此函数应用于menResMat中的字符矩阵,然后返回一个包含所有变量的数据框以用于分析,我们将这一函数命名为createDF()。除了将字符串转换为数值外,我们还另外创建两个新的变量,year和sex。为了做到这一点,必须从输入参量得知我们正在清洗的是哪一年份的数据,以及结果是关于男选手还是女选手的。最后,我们还要以净时间优先的方式,从3种可用的时间变量中选择将哪一种时间包含到数据框中。函数定义如下:
image

将这个新函数createDF(),应用于所有男选手的结果数据,如下:
image

对收到的警告消息进行检查:
image

以上警告中提示的转换问题很可能来自于将时间由字符串转换成分钟数的转换过程中,因为前面我们已经处理了年龄的转换问题。下面检查runTime中出现的NA值的个数:
image

可以看到在2007年、2009年和2010年出现了大量的NA,并且显示在2006年数据中所有跑步时间的值都是NA。
下面让我们先检查几个在2007、2009 和2010年中跑步时间为NA值的记录。我们发现这是由于有的选手只完成了一半的比赛而没有最后的终场时间,另外就是一些选手的时间后面带有脚注符号,例如:
image

我们可以简单通过修改createDF()函数来消除时间信息中带有的脚注符(“#”和“*”),并去掉那些没有完成比赛的选手记录。函数修正如下:
image

当我们将修改后的函数应用于menResMat以创建数据框后,除了2006年的数据之外,其余年份中所有在时间上的缺失值都消失了。
image

对2006年文件的头部进行仔细观察,我们便可发现问题所在,但是为了内容简洁,我们还是将此问题留作练习。
最后,对于作为输入的数据框列表,用do.call()函数去调用rbind(),把所有年份的比赛结果和男选手的数据整合到一个数据框中。方法如下:
image

do.call()函数让我们很方便地将列表中的元素作为单个参量传入一个函数中。例如,rbind()函数的第一个参量是“…”,即
image

“…”参量表示允许调用者向此函数传入任意数量的参量,就rbind()函数而言,这些传入的参量被合并成为一个对象。我们也可以使用以下方法调用rbind()函数:
image

但这样的话就有些繁琐,并且需要提前知道menDF包含14个数据框。使用do.call()函数,可以将这些输入作为一个列表提供给rbind()函数作为参量,然后do.call()为我们一起调用rbind()函数。
检查合并后的数据框维度:

另外,我们对cbMen中变量的概要进行检查,查看是否还有问题出现,如在将各个数据框强制绑定到一起时可能产生的问题。
在这14年的比赛中,有70 070名男选手完成了樱花公路赛。另外,也有70 000多名女选手完成了该项赛事。这里我们将对女选手比赛结果的处理留作练习。在下一节,我们将进一步查看比赛结果。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享: