《数据科学R语言实践:面向计算推理与问题求解的案例研究法》一一2.2 将比赛结果表读入R中-阿里云开发者社区

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

《数据科学R语言实践:面向计算推理与问题求解的案例研究法》一一2.2 将比赛结果表读入R中

简介:

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

2.2 将比赛结果表读入R中

我们在这一节的目标是将比赛结果的原始文本表转换成可以在R中分析的数据。这些表已经从网站上被下载下来,并被存储到以1999.txt,…,2012.txt命名的文件中,男子组成绩的存储目录名为MenTxt,女子组成绩的存储目录名为WomenTxt。从网页上下载并抽取表格的任务在2.7节中处理。如果你想从“起始点”开始这个项目,可以先跳到那一节,然后从互联网中获得文本文件之后再返回来。
现在让我们分析这些文本表以了解它们的格式。之后,对于如何将这些表中包含的信息提取到变量中以进行统计分析,我们应该会有一些想法。图2-2和图2-3是出现在网站上的两个表的截图。通过检验,我们发觉调用read.table()不能正确地将文本读入数据框,这是因为如位次和区段这样的信息是由空格分隔,但是空格同时也出现在数据值中,也就是说,空格会出现在不是作为变量分隔符的地方。例如,对于参赛者的居住地,我们看到有Kenya、Tucson AZ和Blowing Rock NC,那些位于“居住地”中不同部分之间的空格将会使read.table()产生混淆。在我们试图用read.table()输入数据时证实了这一点:

注意跳过文件中前8行的原因是,我们在图2-2中观察到这些都是文件的头部。
我们需要一个自定义的方法来获取这个“表”。图中显示了变量被格式化为占据文本中每一行的特定位置。也就是,参赛选手的最终名次占前5个字符,紧接着是一个空格字符,选手在他或她的区段中的名次出现在接下来的11个位置,等等。将2011年和2012年男子组结果的前两列进行排列,我们可以看到在这些表中的列不完全相同。假设每年的格式都在改变,我们可以通过使用编程解析格式的方法,或者使用年份依赖的固定宽度的格式,从表中提取这些值。这里我们使用第一种方法,并指出哪些列是通过编程检查表头的方式来提取的。在此我们将第二种方法留作练习,要求检查所有的28个表,确定每一列的开始和结束位置,并使用read.fwf()将数据输入R中。
相比通过查看网页来确定文件格式,如果检查原始文件本身,我们可以更好地了解文件的格式。使用readLines()将文件的内容读到R中,返回读入的每一行文本字符串的字符向量。我们从读取2012年男子组的文件开始:

2012年男子组成绩表的前10行是
image

图2-2 2012年男子组比赛结果截图。该截图按比赛顺序显示了2012年樱花10英里公路赛男子组的比赛结果。注意到表中有5英里时间和净时间,通过表头的说明我们知道Time列是净时间
image

图2-3 男子组2011年比赛结果截图。该截图按比赛顺序显示了2011年樱花公路赛男子组的比赛结果。注意在2011年,记录了三个时间—完成前5英里的时间、全程的比赛时间和净时间。相比之下,2012年的结果中没有提供比赛时间

同时我们读入并显示2011年男子组比赛结果的前10行,这样就有了另一个可以与2012年进行对比的表。我们发现如下结果:

通过这个简单的检查可以发现什么?
两个表都有一个表头。
表头的最后一行是一行‘=’字符,也就是分隔线。
在‘=’字符行中有插入的空格,这些空格标记一个信息列的开始和结束,例如,配速(Pace)列占用5个空间的宽度。
‘=’字符行的上一行给出了列的名称。
在2011年记录了两个时间(即Gun Tim和Net Tim),而在2012年只记录了一个终场时间(Time)。2012文件的表头告诉我们Time就是净时间。
如果再多检查几个年度的比赛结果,我们会发现在数据如何组织上还有其他的差别:一些年份表的列名都采用大写;不包括在5英里处的时间;包含一个最右边的列保存某种类型的注释,例如#;表头包含3行而不是8行等。
下面让我们使用2012年男子组的结果作为开发读入所有文件代码的测试用例。不过,我们会设法采用一种通用的方法编写代码,这样可以适用于所有的28个文件。其中第一步是找到有等号的行,它下面的行包含数据,上面的行是列标题,其本身则提供了列间距。我们之前看到‘=’字符在2012年的表中的第8行。由于表的组织结构每年都略有不同,因此我们利用编程的方式来搜索等号。使用grep()搜索els中全部的字符串,找到一个像下面这样由3个等号开始的字符串:

注意一种对正则表达式和grep()函数的替代方法是使用substr()从每一行中提取前3个字符,并与字符串“===”比较。方法如下:

选择3个‘=’字符在某种程度上有些武断。如果等号标识没有在文档的其他地方出现,我们就可以仅使用一个等号。
现在已经定位到了表中的这一关键行,我们要提取它和它上面的行,并按下面的方法舍弃更靠前的那些行:

我们的下一个任务是从body中的每个字符串提取各种信息,也就是表的内容。如何能够提取参赛选手的年龄?通过检查我们知道,一个选手的年龄出现在标签为Ag或AG的列,所以首先要把列名称转换为小写,这样就不必分别搜索Ag和AG。我们使用tolower()来实现它:

可以通过如下方法在headerRow中搜索这两个字母的序列:

regexpr()的返回值告诉我们在字符串的第49个位置上发现了一个匹配项。如果没有找到匹配项,regexpr()将返回-1。现在我们知道有关参赛选手年龄的位置信息:它始于表中每一行的第49位,并在第50个位置结束。利用这些信息,我们使用substr()函数提取每位参赛选手的年龄如下:

现在看来我们已经正确定位了选手的年龄。在2012年男子组参赛选手中最年轻的为9岁,最年长的为89岁,并且有一位选手没有年龄记录。
我们可以用这种方法提取出所有的变量,但是每一年度的表中列的宽度可能不同,所以我们需要使用通用化的代码来搜索等号所在行中的空格,并利用这些空格的位置来确定列的位置。与搜索变量名的方法相比,该方法能更好地发现列值的开始位置。我们可以用下面的方法找到‘=’字符行中所有空格的位置:

这里gregexpr()中的g代表“全局”,它是指函数在字符串中搜索多个匹配,而不仅仅是第一个匹配。我们在第6行、18行、25行、48行、51行等位置都找到了空格。
一般情况下,要想编写自己的代码使它不依赖于在特定列开始或结束的变量名,我们可以使用blankLocs来确定列的开始和结束位置,以此提取所有列的值。每一列的开始位置是空格的后一个字符,它的结束位置是空格的前一个字符。为了正确处理第一列,我们给blankLocs增加0这个参数,这样第一列从位置0的后一个字符开始,即

我们可以使用substr()函数提取出所有的列:

我们把查找列的开始和结束位置的任务封装到一个函数中,将函数命名为findCol-Locs()。在该函数中,为了防止‘=’字符行中的最后一个字符不是空格,我们在位置向量的末尾处附加一个元素,该元素在字符串长度加1的位置。该函数如下:

我们可以从2012年文件中提取全部10个数据列的值,但是需要保存所有的这些变量吗?需要保存14年文件中的所有变量合集吗?还是只需要使用其中的一个子集呢?现在,我们提取姓名、年龄、居住地和全部3个时间,即比赛时间、净时间和终场时间,而忽略其余变量,如地点、区段、5英里跑步时间等。我们把提取所需列位置的代码封装成一个函数,我们需要所要提取列的名称、包含这些列名的首行和分隔符行中的空格位置,作为函数输入。该函数如下:

注意以上函数是一个调用sapply()的简单封装。而该封装可以使我们很容易地使用单独的列名来测试代码,并提取列的一个子集。例如,我们可以使用下面的代码找到年龄变量:

相较之前的方法,这是一个更通用的提取方法。
创建selectCols()和findColLocs()的另一个优势是这些函数可以使代码模块化,这样可以使我们的代码在大规模数据提取和清理过程中更容易地去跟踪和修改。
由于列名每年都稍有不同,因此我们只用前面少数的几个字符来唯一标识所需的列,例如:

另外,如果文件中没有某个想要的变量,那么就将该变量的值赋值为NA。可以提前预判这种情况,因为我们已经注意到在2011年的文件中有比赛时间和净时间,但是没有终场时间,而在2012年的文件中,有被标记为时间的列,但是没有比赛时间或净时间。

下面检查返回值。首先我们用如下方式检查返回值的类型:

输出结果形成一个字符串矩阵(我们还没有转换任何值,例如将年龄转换为数值型)。我们看到矩阵的前几行如下:

2012年的表中有时间的列但是没有比赛时间和净时间,所以比赛时间和净时间的值都是NA。另外检查最后的几行:

这里看到一个参赛选手没有年龄记录。以上结果表明我们已经成功地从MenTxt/2012.txt的表中获取了信息。
我们将提取列的过程打包到一个函数中,这样就可以将它应用到每一年的数据中,该函数调用我们的辅助函数selectCols()和findColLocs()。函数可能看起来如下:

我们准备为每年的数据创建数据框,但是extractVariables()函数希望传递给它的待抽取文件是一个字符向量。因此首先必须把表中的行读到R中,操作如下:

类似地,我们可以将女子组的比赛结果读到womenFiles中。这两个对象,menFiles和womenFiles是两个列表,每个列表包含14个字符向量,每个向量表示一年的结果。每个字符向量都包含对应文件中所有行的字符串。
现在我们可以对menFiles和womenFiles使用extractVariables()函数来获得特征矩阵列表。获得男子组列表如下:

可以看出我们得到了矩阵中行数的合理取值。我们的下一个任务是将这些特征矩阵转换成便于分析的格式。在完成这一任务的过程中,我们使用统计的方法检查结果,并发现额外的数据清洗是必要的。这便是下一节的主题。

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

分享: