本节书摘来自华章计算机《数据科学R语言实践:面向计算推理与问题求解的案例研究法》一书中的第2章,第2.5节,作者:[美] 德博拉·诺兰(Deborah Nolan) 邓肯·坦普·朗(Duncan Temple Lang) 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
2.5 为跨年度的个人参赛选手构造记录
我们需要为参加过多次樱花赛的参赛选手进行记录匹配。由于每位选手的比赛结果没有唯一的标识,因此我们需要从每位参赛选手的信息中构造唯一标识。理想情况下,我们将使用记录的全部信息,即选手的姓名、居住地、年龄、跑步时间和比赛年份。然而,如果这些信息从某一个年份到下一个年份被记录的不一致,可能会减少匹配的条数。另一方面,即使使用全部的信息,我们可能也会错误地匹配来自两个不同运动员的记录。任何设计的方法都不可能完全准确,本节的目的是探讨几种可能的方法,然后确定其中我们认为合理的一个。
现在考虑下列问题:
14年里总共有多少参赛选手?
这些参赛选手中有多少不重复的姓名?
有多少姓名出现两次、三次、四次,等等,最常见的姓名是什么?
一年之内多次出现的姓名的出现频率是多少?
回答以上这些问题可以使我们体会到匹配问题的量级大小。此外,我们可以考虑如何通过清洗name和home的值来提高匹配率。例如,记得前面我们解析文本表格时,在字段末尾附加了一些空格,现在可能是消除它们的好时机。在前面我们也注意到大小写的不一致问题,可以证明匹配记录的不确定性。伴随我们开始进一步仔细检查记录,其他有关清洗字符串的问题也会随之涌现。在回答这些问题之前,我们先来清洗姓名。
任何出现在一个姓名前面或后面的空格都可以被舍弃。此外,如果出现多个空格,例如,在名字和姓氏之间有多个空格,我们可以把它们转换成一个。gsub()函数可以实现这些操作。下面我们创建一个帮助函数,trimBlanks()来完成该工作:
第一次替代消除所有开始的空格,第二次替代消除所有末尾的空格,第三次用一个空格替代多个连续的空格。注意我们使用元字符“[:blank:]”,这样可以找到所有形式的空格,包括制表符。清洗姓名如下:
现在可以开始回答关于姓名唯一性的问题了。我们通过检查概要统计和记录集合来完成该工作。
14次比赛中共有多少参赛选手呢?用length()函数进行查找:
回顾在前面的部分,我们舍弃了那些比赛用时少于30分钟和年龄低于16岁的记录。
那么有多少不重复的姓名呢?
有多少姓名出现一次、两次等?我们可以通过调用两次table()函数来进行判定,也就是
这个表格说明什么呢?我们可以看到14次比赛中有超过7000个姓名出现了两次。有一个姓名出现了30次,而我们知道这个姓名至少对应3个人,因为仅仅有14次的比赛结果。
哪个姓名出现了30次呢?我们可以用如下代码进行查找:
下面来检查关于30个Michael Smith的其他信息。我们从数据框中提取它们:
其居住地包括:
由于额外的空格,这里出现几个不同的Annapolis MD版本。显然我们也需要清洗home字段。
进一步考虑,我们可能会问:可以通过更多的清洗潜在地提高匹配的效率吗?我们已知列标题有大小写不一致的情况,毫无疑问,姓名也存在同样的问题。我们可以检查大小写,但也可以简单地将所有字符变为小写字母:
再次检查最常见的姓名:
通过这次额外清洗又多找到了3个Michael Smith。
此外,可以删除标点符号,例如某人中间名缩写的后面及任意零散的逗号,我们通过调用一次gsub()函数进行处理:
既然有如此多重复的姓名,下面我们计算一个姓名在相同年份中出现的次数。可以创建一个年份-姓名的组合表格:
然后调用max()函数来查找有最大计数值的表格单元,即
这又是Michael Smith吗?还需要通过一些处理找到与这个最大计数值关联的名字。这个存储在tabNameYr里的表格为table类,我们看到它是一个具有3个属性的数值向量:dim、dimnames和class。调用class()、mode()和attributes()函数,可以帮助我们得到以上信息,即
这个数据结构有多重含义。首先,一些矩阵函数可以在table类上进行处理,例如,我们可以调用dim()和colnames()函数进行查找:
注意我们发现了另一条混乱的数据!为了找出是哪个单元中的计数值为5,我们可以使用which()函数,但是要找到行和列的位置,需要在调用中包含arr.ind参数。也就是
最后,对姓名进行定位如下:
它是Michael Brown,不是Michael Smith!
现在我们有了一个清洗过的选手姓名的版本,下面将它添加到我们的数据框中:
我们使用这种格式的姓名去创建唯一的选手标识。
因为我们有选手的年龄和比赛的年份,所以也可以获得选手近似的出生年份。比赛是在每年4月的第一个星期日举行,因此这两者之间的差值是对年龄的一个近似。那些生日在4月开始七天内的参赛选手,有可能对他们年龄的记录在两个比赛年份中是不一致的。可以预知记录的哪些部分会出现这种问题吗?我们在数据框中创建一个新的变量yob:
另外,我们发现在居住地名里也有空格和大小写的问题。这里将对它们的处理留作练习,要求清洗home值,并将清洗过的home版本添加到cbMenSub为homeClean。
下面让我们更加仔细地查看在数据框中的这些新的、清洗过的关于Michael Brown的变量。代码如下:
针对以上各种不同的michael brown行可以得到怎样的观察结果呢?
出生于1953年的3条michael brown记录好像是同一个人,因为他们的居住地都是“north east md”。除此之外,他3场比赛的用时相差不到7分钟。
出生于1958年的4条michael brown记录参加比赛的时间分别是2008年、2009年、2010年和2012年。最近一条记录的居住地登记的是Reston VA,而其他的3个居住地显示为Ashburn VA。那么会有1、2、3或者4个不同的michael brown吗?2010年的该选手是4次比赛中跑得最慢的,差了约11分钟,其他3次更接近一些。互联网搜索显示,Reston和Ashburn相距不到22千米。可以猜想这4条记录属于同一个人,他可能在2010年4月到2012年4月间从Reston搬到了Ashburn,但对此我们并不能完全确定。
另外3条记录中michael brown都出生于1966年。除了在2006年的记录中州名MD没有提供外,3条记录的居住地都是Chevy Chase。当我们检查2006年其他的参赛选手时,发现他们中也没有人列出州名。3条michael brown记录中比赛时间之间也存在11分钟之差,其中在中间年份2010年跑得最快。因此这些记录很可能属于同一个人,但是我们发现2006年记录中的居住地和其他年份的不同。
接下来,还有4条michael brown的记录是出生于1984年,参加比赛的时间分别是2008年、2010年、2011年和2012年。其中,2010年记录中的选手似乎和其他三条记录中的选手不是同一个人,因为他的居住地是New York,NY,且他的跑步时间也比其他三条纪录慢了25~40分钟。其他三条记录的居住地同为Arlington,VA,他们的跑步时间也从2008年的84分钟提升到2012年的71分钟。因此认为这三条记录同属于一个人似乎是合理的,因为一个人随着年龄的增长,他训练和跑步的速度也会更快。而对此我们同样不能完全确定。
最后,我们注意到2012年注册的5条michael brown记录具有不同的出生年份(1953、1958、1966、1984和1988)和5个不同的居住地,因此这是5位不同的参赛选手。
我们总结各种不同的观察结果,并第一次尝试为个体选手创建标识符,然后可能将清洗后的姓名和推导出的出生年份粘合在一起。代码如下:
我们忽略居住地和跑步时间所提供的信息,因此创建了限制最少的标识符。
由于我们的目标是研究一个运动员跑步时间随着年龄增长的变化,下面让我们重点关注那些ID至少出现8次的选手。为了完成这个目标,我们首先判定每个ID在cbMenSub中出现的次数,代码如下:
然后选择那些ID至少出现8次的记录:
我们选择属于这些标识符的记录构建子集menRes:
最后,组织数据框使相同ID的记录相连,这样就使诸如手工检查记录等操作变得更加容易:
另一种数据组织方式是将races8中每一个ID的元素存储为一个列表。该列表中,每个元素是一个数据框且仅包含具有相同ID的记录结果。通过如下代码创建列表:
其中哪种数据结构更好呢?这将取决于我们想如何处理数据。接下来,我们将展示如何使用两种方法来完成一项任务,以便在这两种数据结构之间进行比较。在下一节中,我们会发现使用数据框中的列表进行处理是最容易的,因为我们常常需要将含有多个参数的函数应用于每个参赛选手的记录。
创建列表后还剩下多少个ID呢?
这和代码length(men8L)的功能是一样的。如果两年间的比赛成绩变化太大,我们可能就会放弃记录的匹配。那么多大的起伏变化会让我们认为是错误地连接了两个不同的选手呢?当然,我们不想通过消除跑步时间变化很大的个体而使结果产生偏见。下面让我们查看一些在相邻两年内跑步时间之差超过20分钟的记录。使用如下方法找出那些满足该约束的记录:
或者使用
有多少时间差超过20分钟的选手?
前两位运动员经过稍微重新格式化的显示结果如下:
姓名为abiy zewde的选手似乎非同寻常,很可能是同一个人参加了14次比赛中的12次,虽然有几年居住地改变了,且最近一次年龄为45岁的比赛结果与最快的一次相差了几乎40分钟。事实上,谷歌搜索定位了一个网页http://storage.athlinks.com/racer/results/65866776,发布了他在几次不同比赛中的跑步时间。网页截图如图2-16所示,可以清晰地看到这些记录是属于同一个人的。
图2-16 一个参赛选手比赛结果的网页截图。这个网址为http://storage.athlinks.com的网页包含一个选手在14年中的12次参加樱花公路赛的比赛结果数据。注意他最快的跑步时间是在最近的2012年,完成比赛的时间少于85分钟,那时他45岁。而他最慢的时间是2002年的123分钟,那时他35岁
想用同一个居住地来进一步约束我们的匹配项吗?这样可以消除诸如abiy zewde记录中的不同,尽管我们相当确定这些记录是属于同一个人的。我们可以识别不当匹配,并手动检查潜在的错误匹配结果。因为2006年的记录中没有州名,所以我们需要从那些记录项的末尾去除州名的缩写。可以用一个空字符串替换出现在字符串末尾的一个空格及后面跟着的两个字母。例如:
这可能会导致太过自由的匹配,例如,匹配Springfield IL和Springfield MA。在此我们将它留作练习,确定如何将匹配限制到那些具有相同居住地的记录中,并评估是否应该在匹配过程中添加额外的限制条件。
这里,我们考虑一个不太严格的匹配,仅仅匹配那些具有相同居住州的记录。为此,我们创建一个新的变量来保存州名的两个缩写字母。因为数据结构比较简单,我们返回去处理cbMenSub,并保持一致性。从每个居住地的字符串中提取最后2个字母。如果存在,它就是州名。我们知道在2006年的记录中没有出现州名,因此将其设置为NA。对于来自美国以外的运动员,提取国家或者省份的最后两个字母,但这些不应该显著地影响我们的匹配。
首先确定在每个home值中有多少个字母,代码如下:
然后使用如下代码提取最后两个字母,并将它们添加回数据框里:
并且将2006年的值设置为NA:
接下来,我们重新创建一个新的ID以使其包含state,代码如下:
然后,我们再次选择那些至少出现8次的ID。代码如下:
在下一节中,我们将单独使用列表结构来进行处理。
这次添加完选手的ID后,进一步减少了完成8次比赛的选手数量,即
现在有306名运动员具有相同的姓名、出生年份和州名,并且都完成了14次比赛中的8次。迄今为止我们一直在已获得的匹配集合上进行处理,在下一节,我们将研究参赛选手的成绩如何随着年龄增长而变化