第2章
数据清理工具
无论是人工还是传感器采集的数据,都或多或少地存在一些错误或者瑕疵。比如说,不同采样人员记录数据方式的不同会导致数据值重复或不准确,录入数据时的失误会导致数据输入错误,传感器断电会造成大段的数据默认,不同国家和地区对时间日期制式的不同标准等,各种各样的原因造成数据无法直接用来分析、可视化的情况非常普遍。一般来讲,在从数据收集到最后报告的整个 过程中,数据清理会占用整个流程80%的时间。如此耗时的原因是数据清理并非一次性工作,数据清理、计算、可视化是一个动态的循环,根据分析需求的不同,需要应用不同的清理思路和方式。例如,对于默认值的处理,在探索性数据分析阶段,一般都会尝试各种不同的处理方式,完全移除、部分移除或替换成其他数值,并参考分析的目的来决定如何清理默认值。
本章会向读者分享数据清理的一些基本原则,作为框架来指导数据清理工作,以帮助读者逐步形成一套属于自己的数据清理思路。本章还将重点介绍如何使用tibble、tidyr、lubridate和stringr这4个包来进行数据清理。希望读者在浏览过本章之后,会对以下3点有所了解。
1)“脏”数据和“干净”数据的标准是什么。
2)数据清理的指导原则。
3)可以使用的工具包。
2.1 基本概念
“脏”数据没有任何标准,只要是不能满足分析要求的数据集都将打上“脏”的标签。所以弄清楚与之相对的“干净”数据可以使我们更容易理解数据清理的概念。目前国际上公认的“干净”数据可以总结为如下3点。
1)属性相同的变量自成一列。
2)单一观测自成一行。
3)每个数据值必须独立存在。
表2-1中显示的数据不符合第1条原则,因为男、女都属于性别,所以可以归为一个变量,归为一个变量后如表2-2中所示。但表2-2中显示的数据不符合第3条原则,因为体重和年龄两个变量放在了同一列中,虽然用反斜杠分隔后,人类按常识很容易理解,但计算机并不会懂,其只会将两列本来是数字类型的数据当成是字符串来处理。表2-3中展示了一个3条原则都不满足的样本数据集,在完成清理之前,计算机无法对表2-3中的数据进行任何有效的数据分析。
表2-4中列出了清理后的数据集。对单一数据清理的第一个指导原则就是,按照上文介绍的3点将数据集清理成相应的形式。第2个原则需要按照实际需求进行,表2-5中的数据集是将“宽”数据(一般指多个同类或不同类型变量并存)转换成了“长”数据(同类型变量单独成列)。“宽”数据更符合人们日常对Excel格式数据的理解,而“长”数据对计算机来讲则更易进行数据存储和计算,在R环境中,计算“长”数据的速度优于“宽”数据。将表2-4中的数据转换成表2-5的形式只需一个函数gather,相关内容详见2.3节。
数据清理的第三个指导原则同样需要视情况而定,不同来源的数据应单独成表,独立存在。比如,元数据(解释变量名称或数据背景的数据,英文为metadata)与原始数据应同时存在一个文件或一个工作表中(参考第1章不规则数据读取)。简单来说,元数据通常会包含坐标、指标的具体含义等解释性信息,这类信息不应与原始数据本身同时存在一个数据集中,而应单独成为一个数据集,只在需要解释原始数据本身时才调用元数据。
2.2 tibble包—数据集准备
解决问题需要首先了解问题所在,对症下药。tibble包的存在就是为了给数据清理及后续的分析提供一个最佳的起点。tibble既是R包的名字也是数据在R中的一种存储格式。可以将tibble包理解为R中最常见的data.frame(数据框)格式的升级版。像下列代码所示,如果使用read.csv读取数据,那么数据会被存储在data.frame(数据框)格式中。但是当调用read_csv时,数据就会存在三种适用格式:tbl_df、tbl和data.frame。因为tibble和readr包都源自于Hadley的tidy系列,所以使用readr包时自动植入了tibble(以下简称tbl)的数据格式。那么,问题来了,为什么非要使用这个格式呢?
2.2.1 为什么使用tibble
tbl格式作为老旧的data.frame升级版,主要包含如下三点优势。
1)稳定性更好,可完整保存变量名称及属性。
2)更多的信息展示、警示提醒,有利于及时发现错误。
3)新的输出方式使得浏览数据时,屏幕的利用率极佳。
因为R语言已经诞生了将近20年,很多早期的函数都是围绕data.frame格式写就的,当调用这类函数时,“新兴”的tbl格式可能会出现不兼容的情况,这也是tbl格式目前被发现的唯一缺陷。
tbl格式的第一条优势需要读者在使用过程中对比两种格式的差异才会有直观感受,简单来讲就是,传统的data.frame在处理变量名称时,有时会悄悄改动名称以满足自身要求,这往往会给用户带来一些意料之外的错误。请看以下的例子。
两行代码分别使用函数data.frame和tibble创建了一个传统的数据框格式(见表2-6),以及一个tbl格式的数据框(见表2-7)。代码中定义的数据变量名称为“x+y”,但在data.frame格式中被修改成了“x...y”。大部分情况下,这种默认的修改是数据框格式的一种自我保护机制,目的是为了后续计算时引用变量名不会产生歧义。但是这种保护机制同时会与编程数据分析的另一项基本原则发生冲突,即常量输入等于常量输出(这里的常量可以理解为变量名),除非用户主动修改,否则其名称应保持一致。至于如何选择,就需要读者自行决断了。
第1章中提到查看data.frame中的变量类型时,通常需要调用str函数。但是在tbl格式中,无须调用任何函数,直接输入数据集名称即可查看相关信息。默认情况下,tbl格式会根据console窗口的大小,自动调整显示的内容。内容会包含数据格式、列总数、行总数、变量名称和类型,以及无法完全展示部分的变量信息。有一定data.frame使用经验的读者肯定知道,对于不调用str函数直接在console中运行data.frame格式的数据集,R会将小于1000列×1000行的所有内容都显示出来,而且其中还不包括变量属性等信息。tbl格式查看数据集相关信息的示例代码如下:
2.2.2 创建tbl格式
在练习使用tibble时,可以通过函数tibble或tribble来创建新的数据框。tibble函数创建新数据框的方法与baseR中data.frame函数的方法一致。等号左边为变量名称,右边为相应的数据值,不同变量之间以逗号相隔。下面的代码创建了一个包含变量a和b的数据框,变量a包含6个值,分别为数字1到6,变量b为a列中的值乘以2,因此同为6个数值。代表integer(整数),代表double(浮点型)数据类型。创建数据框的代码如下:
tribble函数比较适合用来创建小型数据集,可以采用常规excel表中数据分布的格式,直接手动输入数据,变量名称以“~”起始,逗号结束,数据值以逗号分隔。下面的代码即用来生成表2-2的。该函数在特定情况下会显得非常实用,比如,用来解释变量(或指标)和因子水平的元数据一般都会杂乱无章,使用软件清理既费时又费力,但当通过肉眼能够很容易提取到关键信息时,直接使用tribble函数手动生成变量和因子水平对照表会更高效。
2.2.3 as_tibble—转换已有格式的数据集
在转换数据的格式之前,可以使用is_tibble来测试目标对象是否已是tbl格式,该函数只需要对象名称这一个参数即可。
可以通过as_tibble函数将对象已有的格式(vector、matrix、list和data.frame等)转换成tbl。表2-8中列出了常见对象格式的转换注解。
下面通过具体的代码来说明使用as_tibble函数将常见的R对象转换成tibble格式的具体方法。
(1)as_tibble函数直接将vector格式转换成数据框格式
1)随机设置一组向量,保存为y。
2)检视向量y。
3)调用as_tibble函数直接转换,并将结果显示到console中。
实现代码具体如下:
向量转换成tibble格式的结果如表2-9所示。
(2)矩阵格式转换
首先创建一个名为b的示例矩阵,矩阵按照行排列数字1到9,行数和列数同为3,行名为“Row1”“Row2”“Row3”,列名为“col1”“col2”“col3”。表2-10中显示了移除行名—设置rownames = NULL的结果,表2-11为保留行名设置rownames = NA的结果。
实现代码具体如下:
(3)传统数据框格式转换
传统数据框格式转换与转换矩阵格式基本相同,读者可以自行试验。
(4)列表格式转换
列表格式转换一定要注意列表中要素的长度,即每个要素中所拥有的数值个数。
下面的代码创建了一个名为“l”的列表,列表中包含了三个要素,分别是a、b和c。其中,要素a包含三个数值,要素b包含三个字母数值,要素c只有一个数字,创建列表的代码如下:
执行函数as_tibble可以毫无压力地将该列表转换成“tbl”格式,每个要素单独成为一个变量,原列表中的要素c中的数值将被重复使用三次以对应其他变量的长度。如果要素c的长度为2,即包含两个数值,那么转换会失败。as_tibble函数代码如下:
表2-12显示的是列表转换成功后的结果。
tibble包中另一个可以转换数据格式的函数是enframe函数。该函数的优势在于格式转换时可对向量数据进行编号。以一个长度为3的数值向量为例,运行该函数之后,会得到一个拥有两个变量,每个变量包含三个数值的数据框,第一个变量名为“name”,是对数值向量的对应编号。
2.2.4 add_row/column—实用小工具
在微软的Excel中,用户可以随意插入或者删除一行/列数据,add_row/column函数也为R用户提供了类似的功能。使用baseR来完成新增列的需求相对来说很简单。下面的代码首先创建了一个“tbl”,然后使用“$”来为数据新增一列名为“k”的变量,变量的数值为3、2、1:
![image.png](https://ucc.alicdn.com/pic/developer-ecology/bebb3c9a46fd465b840344743dde7bc4.png)
在数据框的末尾加入一行新数据也可以实现新增列的功能,不过该功能需要读者对R的基本理论有一定的理解。如下代码所示,延续数据框“f”,配合使用函数“[”和“nrow”可在数据框的末尾新增一行数据。“nrow”的功能是计算对象的行数。
小知识
中括号紧跟在数据框后面,可以作为索引来选择数据框中的特定数值。逗号前面为行索引,后面为列索引:[行,列]。读者可以试着配合“ncol”函数,以类似新增行的方式来为“f”新增一列。
tibble包中这两个实用的小函数,可以随时随地任意新增行列数据到指定位置,而不是像baseR中的命令那样只能在数据尾部或已有变量后面新增行或列,在还不是很熟悉R的各种符号代码之前,这两个函数是可以帮助用户快速有效地解决实际问题的。
下面的代码为“f”又新增了一行数据,不过因为这里仅指定了两个变量的值,所以对于未指定的部分,系统将自动填入默认值,具体代码如下:
在第三行之前插入一行新数据,代码如下:
第一行之后插入新数据,代码如下:
在第一列之后插入新变量,代码如下:
2.3 tidyr—数据清道夫
2.3.1 为什么使用tidyr
tidyr包作为整个tidy系列里的支柱之一,可以称为目前最容易上手的数据清理和数据操控工具。开发者Hadley汲取了前作reshape和reshape2包中的精华,并最大限度地考虑到新用户的使用习惯,用精简的人类语言创造了tidyr包。根据笔者的使用经验,使用tidyr包进行数据清理的优势在于以下4点。
1)简洁直观的函数名称,可读性极强—易上手。
2)默认设置可以满足大部分使用需求,无须时刻参考帮助文档—易使用。
3)不同函数中的参数设置结构清晰—易于记忆。
4)处理数据过程中完整保留了变量属性及数据格式—不易出现未知错误。
本节将通过介绍tidyr包中最重要的几个函数来为读者展示tidyr包在数据清理和数据操控上的优势,并将通过代码演示来介绍其基本的使用方法。
2.3.2 gather/spread—“长”“宽”数据转换
1. gather—“宽”变“长”
2.1节讨论了“宽”数据格式的弊端,所以推荐将数据转换成“长”数据—全部变量名称为一列,相关数值为一列。gather函数因此而生。图2-1所示的示例用箭头标识出了数据由“宽”变“长”的具体路线。在理想情况下,整洁的数据框应为如图2-1a所示的格式,因子水平一列(性别),变量(或指标)一列,剩余所有数值型数据一列。
读者可以使用2.2.2节中介绍的tribble函数来构建如图2-1b所示的“脏”数据框,然后使用以下代码实现从“脏”和“宽”的形式到“干净”和“长”的转换。在代码清单2-1中,笔者将“脏”数据保存在名为df的数据框中(此处略去创建数据集的代码),然后使用管道函数“%>%”,将df传递给gather函数(中文释义见表2-13),因为管道函数的存在,所以无须重新引用df,而以“.”来代替,指定指标列为key,数值列为value,保留序号列(保留列需要使用负号加列名的形式进行设置),并移除默认值。之后会得到一个中间产物数据框,该数据框指标列中的“性别”和指标虽然以空格分隔开,但仍然在一列中,不满足“干净”数据的原则。所以再次使用管道函数将中间产物的数据框,传递给函数separate(详见2.3.3节),将key列拆分成两列,分别为性别和key,此时的数据库便如图2-1a所示。
小知识
上述代码中的“%>%”为'magrittr'包中的 forward-pipe operator,中文可以理解为管道函数。该函数能够与'tidyverse'内的所有函数完美结合使用,且易于理解记忆。有兴趣的读者可以尝试运行指令“?'%>%'”来查看具体的英文帮助。
因为tidy系列中各个的函数的结构非常简洁清晰,因此当读者熟悉各种参数的位置情况之后,完全可以省略各种参数名称,而只依靠位置来进行传参,具体代码如下:
2. spread—“长”数据变“宽”
函数spread是gather函数的逆向函数,即将“长”数据转换成“宽”数据。图2-2简要展示了函数的执行规则,将key列中的变量单独拆分成新列,value列中与变量中对应的数值同样会按规则进行放置。读者可以参考表2-14中的中文释义自行练习代码。
2.3.3 separate/unite—拆分合并列
2.3.2节中展示了函数separate的具体用法,该函数完全可以理解为是Excel中的拆分列,该函数无法对一个单独的数值位置进行操作。表2-15介绍了其所包含的参数及中文释义。unite函数则是其相对的逆向函数。
2.3.4 replace_na / drop_na/—默认值处理工具
一旦明确了默认值的替代方式,replace_na和drop_na两个函数就可以通过对指定列的查询来将NA替换成需要的数值,例如,去掉所有存在默认值的观察值。表2-16中列出了函数的功能简介及使用时应注意的事项。读者可以参照帮助文档中的例子结合表2-16中的提示来自行练习这两个函数的功能。
下面的代码列出了如何使用两个函数:
这里必须提醒一下读者关于默认值替换的情况,将所有默认值全部替换成0是很危险的行为,不推荐使用这种做法,因为0代表该数据是存在的,只是数值为0,而默认值则可能代表数据不存在和存在两种情况,只是因为某些原因而导致数据采集失败。因此对默认值的处理一定要视具体情况而定。
2.3.5 fill/complete—填坑神器
在处理日期或者计算累积值的时候,如果中间有一个默认数值,则意味着值不完整或累积值无法计算。fill函数可以自动填补默认的日期或等值,类似于Excel中拖动鼠标来完成单元格数值的复制或序列填充功能。complete函数是将三个函数揉在一起,这三个函数分别为:expand、dplyr::left_join和 replace_na。主要功能是将变量和因子的各种组合可能性全部罗列出来,并用指定的数值替代默认值部分。complete函数在日常练习中并不常用,所以这里不做过多介绍,感兴趣的读者可以参考帮助文档进行练习。
fill函数的参数及功能说明详见表2-17。
2.3.6 separate_rows/nest/unest—行数据处理
- separate_rows—拆分“单元格”
当遇到一个数据单位中出现多个数值的情况时,separate_rows函数就会显得非常有用。图2-3中展示了最基本的函数逻辑,将一个数据单位中的不同数值按照参数进行sep中给出的参进行数拆分,然后将拆分之后的结果顺序地放在同一列的不同行中,并自动增加行数。
separate_rows函数的参数及功能说明详见表2-18。
2. nest/unest—“压缩”和“解压缩”行数据
nest/unest是两个互逆函数,它们最重要的功能是将一个数据框,按照用户自定义的规则,将其压缩成一个新的数据框,新的数据框中包含列表型数据。Jenny Bryan认为这是目前最有实际操作意义的数据框形式,因为它比较符合人们对数据集形式的一般主观印象,而且数据框同时还保留了列表格式的灵活性。下面就来通过代码具体介绍nest函数的实现机制。
将2.3.4节中清理后的数据保存为df_tidy,然后再将该数据框传递给nest函数,并设定压缩除性别列以外的变量。函数运行的结果是生成了一个只有两列的新数据框,变量为性别和data,其中,data列包含了原数据框中其他三个变量的数据(具体见表2-19)。变量data列中的列表格式将三个变量存储为三个独立的元素,如果读者对两个列表中任意一个进行as_tibble运算,都会得到一个完整的数据框。示例代码如下:
上述代码运行结果如表2-19所示。
将序号列排除在外,压缩其余变量列,代码如下:
上述代码运行结果如表2-20所示。
单独使用nest函数没有任何实际价值,但是当配合循环(第4章)和purrr包(第5章)中的map函数家族时,nest函数就会显示出强大的功能性。对dplyr包有一定了解的读者可以跳过第3章,直接查看nest与其他具有循环功能的函数结合使用的例子。
表2-21和表2-22中列举了nest/unest这一对函数的参数及功能说明。需要提醒读者一点的是,如果需要使用unnest函数“解压缩”两列及以上时,那么每一列中数据框的行数都必须相等,否则无法成功“解压”。
2.4 lubridate日期时间处理
2.4.1 为什么使用lubridate
通常传感器记录的数据,是为了避免闰年导致的种种稀奇古怪的错误,纯数字形式的日期格式很常见(例如19710101或儒略日)。这些纯数字形式日期的可读性通常都较差,所以需要经过解析变成更易理解的格式。还有另外一种比较普遍的情况是不同国家使用不同的日期制式和时区,比如英联邦国家偏向使用“日月年”或“月日年”的形式记录日期,以及12小时制来表达时间,而国内则倾向使用“年月日”的形式和24小时制。由于以上这些情况的存在,在处理与时间有关的数值时,解析日期和时间变量往往无可避免。
对于日期时间的处理看似简单直接,但该问题却是数据分析当中与默认值处理难度相当的另一大挑战。在lubridate包问世之前,尽管已有其他功能强大的R包,诸如zoo、chron等,但都因为种种原因而无法达到与lubridate包一样简洁明了的效果。lubridate包的出现,极大地提高了用户解析日期数据的效率,从而使得开发人员能将更多时间用于分析数据而不是微调代码本身。lubridate包的最大优势可以总结为如下三点。
1)使用人类语言书写的编程语法,易于用户理解和记忆。
2)总结并融合了其他R包中的时间处理函数,并且优化了默认设置,更利于用户上手使用。
3)能够轻松完成时间日期数据的计算任务。
2.4.2 ymd/ymd_hms—年月日还是日月年?
一般情况下,ymd及其子函数可以完整地解析以数字或字符串形式出现的日期形式,只有当日期中对不同的成分以类似双引号作为分隔符的情况,或者是对象为奇数的情况时(详见代码演示),ymd等函数可能会无法直接进行解析,而是需要进行额外处理。ymd函数即代表年月日,ymd_hms函数则代表年月日时分秒。两类函数的参数名称、结构和位置完全一致,具体函数名称见表2-23。默认时区为世界标准时间UTC。表2-23中列出了两组函数所有的子函数。读者在解析时间时应当注意时区,因为北京时间比UTC早8个小时,所以是UTC+8。所有对象经过解析后都会输出为年月日(时分秒)的标准日期格式,并且类别为“Date”。
lubridate函数可以仅使用默认设置轻松解析偶数位的字符型向量,必须要注意的是,偶数位必须大于6位,否则会产生NA。在下列的代码中,“2018 1 2”因为其中存在空格,所以被默认解析为6位。同样的逻辑也适用于解析日期时间对象。参数tz用于设置时区,示例代码如下:
小提示
如果函数没有自动解析正确的时区,那么读者可以使用Sys.timezone()或Olson-Names()来寻找正确的时区,并传参设置时区。
2.4.3 year/month/week/day/hour/minute/second—时间单位提取
气象领域通常会计算若干年的月、日平均降雨量或气温等指标,这时就会涉及月和日的提取要求。lubridate包中的函数,包含了提取从年到秒所有单位的功能。而为了方便记忆,这些函数的名称也都与相应的组件一一对应。需要读者注意的一点是,该组函数只能提取时间日期格式的对象,这些对象可以是常见的“Date”“POSIXct”“POSIXlt”“Period”等,或者是其他日期时间处理R包中的格式“chron”“yearmon”“yearqtr”“zoo”“zooreg”等。
下面的代码演示了最基本的使用方法,详细的参数微调可以提供一些额外的信息,读者可以自行参阅帮助文档:
2.4.4 guess_formats/parse_date_time—时间日期格式分析
当遇到使用英文月份简写的日期,比如24 Jan 2018/Jan 24,或者其他更糟糕的情况时,如果使用传统的baseR中的函数,诸如strptime或是format之类,那么用户可能会浪费很多时间去猜测和组装正确的日期时间格式,因为只有顺序和格式都正确的时候,baseR中提供的相应函数才可以正确解析日期时间,否则就会不停地返回NA值。幸运的是,guess_formats和parse_date_time两个函数的存在,完全颠覆了以往的解析模式,从而使得这一过程变得简单有趣。
使用这两个函数解析日期时间的大体思路具体如下。
1)执行guess_formats函数以用于猜测需要解析对象的可能日期时间顺序及格式,用户必须指定可能存在的格式顺序。
2)复制guess_formats函数的返回结果。
3)执行parse_date_time,并将复制的内容以字符串向量的格式传参给函数。
4)若遇到解析不成功或不彻底的情况,则需要手动组建日期时间格式(组件列表请参看表2-24),并加入到guess_formats中的order参数中。
下面的代码简要解释了guess_formats和parse_date_time两个函数配合使用以解析日期时间的流程。首先生成一个名为example_messyDate的练习字符串向量,然后对该向量运行guess_formats,第二位参数orders中包含了可能存在的日期时间格式,并函数的返回结果中会报告匹配的顺序格式,并将报告结果复制到parse_date_time的第二位参数中。至此解析成功。
2.5 stringr字符处理工具
与Hadley出品的其他tidy系列包一样,stringr也具有同样清晰的逻辑结构和参数设置。在stringr包中,函数的参数很少有超过三个的情况,各个常用的函数都只需要指定两到三个参数,这极大地简化了参数设置的过程。加之参数在结构和名称上的一致性,用户很容易就能做到融会贯通。stringr包总体来讲是将stringi总结并优化而来的一个包。简单的字符处理能力,可以极大地提高数据清理的效率。使用stringr包用户能够快速上手使用正则表达式,从而快速处理数据,同时对表达式的基本概念也能有一定的理解,为以后更复杂的任务做好铺垫。
2.5.1 baseR vs stringr
baseR中已存在一些使用正则表达式处理字符串的函数,例如,以grep为母函数的一众函数,包括最常用的gsub,等等。熟悉Linux系统的读者可能会觉得grep看起来很眼熟,这是因为R语言与其他编程语言一样,都借鉴了各种计算机语言的精华部分。表2-25列出了baseR中与字符串有关的函数及其与stringr包中相应函数的对比及小结。该表的意义在于,可以通过学习stringr包中的主要函数来帮助了解baseR包中的对应函数。因为stringr虽然简单易上手,但是在实际处理应用数据时,其在速度上会比baseR又略逊一筹,读者可以通过stringr包中的函数来练习字符处理的能力,在实际工作中使用baseR中的函数来执行具体任务。
下面的代码简略演示了str_replace和str_replace_all的区别以及参数设置。首先加载stringr包,然后创建一个练习用的字符串向量example_txt。对练习对象执行str_replace函数,参数pattern被设置为“a”—意为查询“a”第一次出现的位置,参数replacement设置为符号“@”—意为使用“@”来替代字母“a”。结果可以看到只有第一个“a”被替换,字符串中其他的“a”仍被保留。但是str_replace_all会将所有符合要求的部分全部替换掉。BaseR中的sub和gsub函数逻辑与str_replace和str_replace_all相同,只是包含了格外的参数设置来满足更复杂的任务需求。感兴趣的读者可以自行尝试。示例代码具体如下:
2.5.2 正则表达式基础
Regular expression(正则表达式)在目前主流的统计语言上都有应用。使用符号型字符串大规模查找和替换数据,不仅可以提高工作效率,同时还能保证规则的一致性。R中正则表正则表达式的符号意义,请参看表2-26。表2-26中列出了最常见的正则表达式基础单位,读者可以将这些符号想象成儿时乐高积木的小构件,由简到繁地慢慢组合搭配这些构件,以实现不同的数据处理目标。简单构建正则表达式请参见2.5.3节。
2.5.3 简易正则表达式创建
数据集df是笔者从网络上获取的一组英文期刊作者名和年份(具体见表2-27),以此为例,简单演示正则表达式的组合过程。示例代码如下:
函数str_view/_all可以很直观地反映出数据内部匹配的项目。“.+”组合的意思是匹配除换行符“n”以外的所有字符,字符至少出现一次,所以全部的字符都被匹配出来。若只希望匹配“.”,则需要使用反斜杠来告知函数,这是因为独立存在的“.”会被解析为任何除换行符以外的字符、字母和数字(见表2-26)。所以第二行代码的意思就是匹配第一个出现的英文句号:
匹配所有字母和数字,代码如下:
小提示
str_view/_all的返回结果会显示在Rstuido的viwer中,而不会显示在console中。
当处理数据较多时,str_view/_all的速度可能会很慢,可以使用str_detect来检测所使用的表达式在数据中是否有匹配。该函数只返回逻辑判断,代码如下:
在df中,页码可以被归类为无用信息,所以需要清理掉。下面的代码使用了str_replace来将页码的部分完全替换掉。匹配模式为"pp\..+[:digit:]{2,3}\-[:digit:]{2,3}"。分解这个正则表达式:“pp”匹配“pp”;“ \.”匹配“.”;“.+”代表“pp.”后面的任何字符串;“[:digit:] {2,3}”代表2到3位数字;“\-”匹配“-”;最后的[:digit:]{2,3}表示数字出现2到3位。
处理后的结果显示在表2-28中。
更复杂的正则表达式可以参看Garrett Grolemund和Hadley Wickham合著的《R for Data Science》的第14章,或者全面介绍正则表达式的《Mastering Regular Expressions》,Jeffrey E. F. Friedl著。
2.5.4 文本挖掘浅析
文本(包括但不仅限于书刊)挖掘,或者更通俗地讲—自然语言处理(Nature Language Processing),是人工智能领域必不可少的一项技术。每一秒钟,世界范围内都有不计其数的新文本在以各种形式记录或保存起来。但这些以人类语言书写或录制下来的“数据”,并不像二进制的表格式数据那样容易被电脑接受并处理。如何分析人类历史中这些以文本形式保存的数据,就是文本挖掘需要解决的问题。看过《星际迷航》的读者应该会很熟悉舰长们经常说的一句台词“电脑,给我与某某事,某某东西相关的资料”。现实中,苹果的siri已经算是这方面很成功的商业应用模型。也有很多编程数据分析前辈,结合机器学习和自然语言处理来进行音乐创作、文献写作,等等。有关文本挖掘的详细内容已超出本书的讨论范围,所以在此仅简略地介绍文本挖掘的一般流程及可用R包,借此为感兴趣的读者提供一些继续学习的线索。在第3章中讨论完dplyr包中的一些实用函数之后,3.3节会向读者介绍一般的英文科技文献挖掘流程。
图2-4列出了文本挖掘的一般流程,大致可以总结为三个主要部分,具体如下。
1)文本数据的获取。
2)文本数据的准备。
3)数据分析。
数据获取的方法多种多样,可以使用网络爬虫抓取网络文本,使用pdftools包读取PDF格式的电子文档,jsonlite包读取JSON格式的文本数据,或者是安装janeaustenr包来获取简奥斯汀的6本著名小说来进行文本挖掘的练习。
文本数据的准备包括清理标点符号、页码等多余信息,以及分词标记和简单的初步统计。通常需要使用正则表达式,其主要目的是将文章中的句子打散以获取单个的词语或词组,并去掉某些无语义贡献的词汇,例如,介词或是助词,再进行一定程度的词频统计等操作。建议有一定英语基础的读者按照《Text Mining with R》这本书开始练习,书的作者就是最易上手的文本挖掘R包tidytext的开发者。希望分析中文的读者可以从quanteda包开始,因为这个包配有中文词库和简单的中文示意。
文本数据的分析根据目的的不同在难度上会有天壤之别。对于初学者来说,预先设置一些已知的规则来对文本数据进行查询式的分析(比如,在简奥斯汀的书中,哪一个角色的名字出现次数最多)这样的词频统计分析可视化,可有助于提高对数据的理解程度和使用各个函数的信心。监督和无监督机器学习需要依靠tm、quanteda、topicmodels等不同的R包的交互使用才可能实现复杂的分析目标。详细内容请感兴趣的读者参阅各个包的主页,这里不再过多讨论。