购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.1 R中的数据类型

2.1.1 基本数据类型

第1章介绍了R语言的概貌,下面将以一个简单的电影票房实际数据为例,介绍在实际数据处理中R语言的基本类型和基本操作。

1.热门电影数据集简介

在电影的宣传期,往往能看到其主演、导演频频现身各大头条,吸引看客眼球,最后的落脚点往往是“祝×××电影票房大卖”。虽然观影习惯已经开始慢慢养成,电影的方方面面也成为人们茶余饭后的谈资,但你是否想过通过数据的形式统计一下电影的基本信息呢?比如,本月上映了几部电影,动作戏偏多还是喜剧为主,主演是不是当红花旦等,这些信息都可以通过简单的R语言操作来一一获得。这里从网络上收集了19部热门电影共10个变量的基本信息,我们将以此为例说明如何在R语言中进行相关操作。

数据来源于中国电影发行放映协会(http://www.chinafilm.org.cn)、豆瓣电影(https://movie.douban.com)、百度指数(http://index.baidu.com)等网站(见图2-1),图2-2和图2-3展示了电影《火锅英雄》的一些基本情况。

图2-1 中国电影发行放映协会电影月度上映数据

图2-2 电影《火锅英雄》豆瓣主页

图2-3 《火锅英雄》主演百度指数

详细的数据变量说明如表2-1所示。

表2-1 数据变量说明

部分数据示例如表2-2所示。

表2-2 数据示例

当把表2-2这个数据集第一次读入R中时,它会以数据框(data.frame)的形式存储,我们把这个数据框命名为movie。数据框是类似于Excel里常见的表格一样的对象,具体的定义和内容后面介绍。下面就从这个简单的数据集出发,依次介绍R中的各种数据类型。

2.基本数据类型介绍

(1)数值型(numeric)。数值型变量很简单,统计教材中的定量数据就是R中的数值型数据。比如数据集中的doubanscore,boxoffice等就是这类数据。通常,当用符号“<-”或者“=”给一个变量赋予数字时,就默认生成数值型数据。在R中,我们可以使用class函数来显示出一个数据对象的数据类型,数值数据在R中的名称即为numeric [1]

这种数据类型虽然看似简单,但不可大意,有时会出现状况。比如,下面几个命令会输出什么结果呢?

exp(1000)
-10/0
exp(1000)/exp(990)
exp(10)

答案如下:

出现这种结果,其实是因为数值类型中还包含几种特殊情况:正无穷(Inf)、负无穷(-Inf)以及NaN即非数值(Not a Number)。R会把所有超过电脑存储限制的数字当作正无穷,一般来说这个限制大约为1.8×10 38 。一旦算式中有正无穷或者负无穷的子项出现,结果就很可能是无穷或者NaN型的数。比如上面命令的第三项,两个指数相除并不会给出exp(10)的答案,而是NaN,这一点一定要多加留意。

(2)字符型(character)。字符型变量从字面上很好理解,就是用来储存文字的,比如数据集中的director,star1就是这种类型,然而未必都是如此。首先,不是文字的也可能是字符型,比如:

其次,文字也许并不是字符型。比如前面提到的电影数据集中,name,type等变量看起来都是文字,但如果导入R时不加特别设置,很可能会得到如下结果:

以为是字符型的,其实它是因子(因子类型,后面详解)。

字符型数据到底是什么呢?首先,简单来说,用单引号或双引号定义的就是字符型(注意是英文格式的引号),所以,在不是文字的也可能是字符型的例子中,由于2上面加了双引号,所以它就被R识别为字符型,而不是数值;其次,大多数情况下,文字就是字符型数据,但是当一串文字被放在一个数据框中读进R时,它就极易被自动转换为因子型数据。混淆数据类型会带来什么问题呢?它们的区别主要在数据运算、字符存储上。如感兴趣,不妨试试在R中输入"1"+"1",看看得到什么结果,体会一下其中的区别。所以,看到大数据表(数据框)中的文字时一定要注意,它很可能默默地欺骗了你的眼睛。

(3)逻辑型(logical)。逻辑型数据取值很有限,只有TRUE和FALSE两个值,但它的作用却不可小觑。它常常出现在各种条件设定语句中,比如if条件语句中,或是在选取某些符合条件的数值时,都暗含着逻辑型数据的产生。最简单的,当进行一个条件判断时,就会产生逻辑型的数据结果。另外,逻辑型的结果还可以进行加减运算,原因就是TRUE在R中对应数字1,FALSE对应数字0,因此TRUE和FALSE两个值便可类似数字进行加减计算。

(4)因子型(factor)。

1)什么是因子型数据?因子型数据通常用命令factor()来定义。

这里,因子型数据转换为字符型数据分为两步:第一步区分字符有几类,形成类型到整数的映射;第二步将原字符按照整数形式存储。具体过程如图2-4所示。

图2-4 字符型数据存储过程

或许你会问,这个因子存储的不就是名义型变量吗?那举一反三,可不可以存储有序型变量呢?答案是肯定的。有序型变量就是带有顺序的名义型变量,因此只需把factor()中的ordered参数设置好就可以了。

仔细观察有序型因子和普通因子类型的区别,可以发现levels的显示等级有顺序了,这时它内部的存储方式是1=Excellent,2=Improved,3=Poor。那么电脑怎么知道我们想要的排列逻辑是什么样的呢?没错,电脑并不知道,它只是按照默认的字母顺序创建,这里仅仅是因为首字母顺序恰好与我们的逻辑顺序相同而已。

2)如何改变因子型数据各水平的编码顺序?如果按照字母排的顺序不是我们想要的逻辑顺序怎么办呢?同样好解决,只要设置factor()的levels参数即可。仔细思考,什么时候会用到因子但又不满意因子水平的排列顺序呢?举个例子,画分组箱线图时,可能会发现几个箱子的排列顺序不对劲或者不满意,这其实就是因为因子的水平定义顺序不当,这时就可以通过levels参数改变因子水平编码方式,从而让分组箱线图按照设想排列好。

3)如何将因子型和字符型数据互相转换?前面提到,当读入一个数据表格(数据框)时,如果不做任何处理,软件会自动把字符型变量变成因子型变量。如果我们需要自己操作,如何才能实现字符型和因子型数据的自由转换?转换后对象所需内存有何变化?又是在什么情况下需要把字符型变成因子型呢?

首先是字符型和因子型数据的自由转换,秘诀是一类as.函数。as.factor()可以把其他类型数据转换成因子型;as.character()可以把其他类型数据转换成字符型。另外,is.类函数可以查看数据类型是不是你想要的那种,仔细研读以下代码:

其次是切换后对象大小的变化。一般来说,如果字符串包含的水平较少(比如男、女),那么因子型数据会比字符本身更节省空间,但如果字符串包含的水平很多(比如电影名称),转换成因子型数据反而会占用更多空间(在数据量较大时尤其严重)。如感兴趣,可采用object.size()函数进行观察。

最后是什么时候需要把字符型数据转换成因子型数据。前面提到,R中的“因子”实际对应的是定性和定序变量,因此如果需要这两种类型的变量出现,就可以考虑把字符型变成因子型。比如,在作图中需要对数据分组,用来分组的变量就应该变成因子型;需要做包含定性变量的回归模型,定性变量就要变成因子型进入模型……这些都是因子型数据的用武之地。

(5)时间型数据(Date/POSIXct/POSIXlt)。实际上,时间型数据并不是一种单独的数据类型,然而在很多实践项目中,时间型数据曝光率极高。

通常,时间型数据是以字符串形式输入R中的,因此首先需要把这些字符转换成R可以识别的时间型数据。R语言的基础包中提供了两种类型:一类是Date日期数据,它不包括时间和时区信息;另一类是POSIXct/POSIXlt类型数据,其中包括日期、时间和时区信息。下面分别介绍这两类数据如何从字符转换过来以及后续可进行的操作。

1)将字符转换成Date日期格式。所谓Date日期数据,就是精确到日的时间形式。一般来说,用as.Date()函数转换时需要通过参数format指定输入字符的格式(包括年月日排列的顺序及表达方式),该函数默认可自动识别以斜杠(2017/12/23)和短横线(2017-12-23)相连接的年月日格式,并统一转换为以短横线连接的输出形式。例如对前面给出的movie数据集中的“showtime”进行转换,代码如下:

如果对于特殊形式不指定format或者指定错误,R就会报错。表2-3是format对应法则和一个小示例。

表2-3 日期格式示意表

小功课: 如何将一个数字变成日期型数据?可参阅help(as.Date())。

2)将字符转换成POSIXct/POSIXlt时间格式。所谓POSIXct/POSIXlt时间格式,其实就是精确到秒级的时间戳。当我们周围智能化设备、传感器越来越多时,很多数据都可以精确记录到秒级。一个典型的例子就是车载记录仪,它会每秒实时记录你开车行驶的速度、方向等信息,这样的数据就是每秒采集上传并记录的。那么对于这样的数据,该怎样转换呢?可以使用另一个类似的函数:as.POSIXct()。

这个函数的使用方法和as.Date()类似,同样需要定义好被转换字符的format才能被正确识别转换。与as.Date()相同的是,默认可以转换的格式仍然是2017/12/23 01:20:34或者2017-12-23 01:20:34这两种,其他格式都需要自行对照表2-3来具体指定,否则R语言就会“罢工”。

3)将时间数据转换成你想要的形式。从前面内容可知,as.Date()和as.POSIXct()函数中的参数format并不能任意设置,只有输入与字符显示相匹配的格式才能有效识别转换。

如果想要其他格式输出的时间数据该如何操作呢?函数format()就可以用来更改时间数据的输出格式,甚至还可以提取你想要的一个部分。比如,如果想知道电影是什么月份哪一周上映,应该如何提取呢?下面分别以前面提到的movie数据集中的电影上映时间showtime、系统时间两种类型为例,见证一下format()的神奇力量。

4)一款处理时间数据的专用包:lubridate。以上介绍的都是base基础包中自带的函数,下面再来介绍一款专门高效处理时间数据的包lubridate。这是一个实践中口碑极佳的数据预处理包。

lubridate包主要有两类函数:一类处理时点数据;另一类处理时段数据。它不仅功能强大,而且相应函数也很直观易懂,比如把字符转换成时间类型,根本不需要输入匹配的format参数;再如提取时间数据细节,也只是一个函数即可,不附带任何参数。下面的代码就是一个典型的例子,更多内容可见lubridate的帮助文档。

5)时间型数据的操作。在字符型数据被转换成“正统”的时间型后,便可以进行后续的操作和建模了。以下介绍两类常见的基本操作。

①做差。如果想看两个日期之间相差多久,可以直接把两个数据做减法,也可以用difftime()函数提取。

②排序。由于时间型数据本质上是用数值形式存储,因此它可以按类似数值方式进行排序。比如想看一下按照上映时间先后顺序排列的影片分别如何,可以参考对“单列时间数据”和“依照时间对整个数据表”进行排序的示范。

2.1.2 向量

前面介绍了R中的数据类型,包括数值型、字符型、因子型等,下面开始介绍数据结构。数据类型和数据结构有什么区别呢?简单地说,如果把一个个数据元素比作一块块砖头的话,那么数据类型就是说砖是圆形的还是方形的,主要针对的是砖本身的特性;而数据结构则表示盖房时砖头是怎样排列的,是横着排还是竖着排,是要垒成一个面还是一个体,主要着眼于砖头的排列组织方式。这里要讲的数据结构也类似,它更倾向于表达的是数据元素组织在一起的方式。

总体来说,R中常用的数据结构有四种:向量、矩阵、数据框和列表,不同的数据结构能够存储的数据类型不同,用来处理的函数也有很大差别。向量是用于存储同一种类型数据的一维数组,是所有数据结构中最基础的形式。它的存储方式如图2-5所示,每个格子存储同种类型(比如,这里是字符型)的元素。下面就通过向量的基本操作和常见类型两部分详细讲解。

图2-5 向量的样子

1.基本操作

(1)创建。一般来说,采用函数c()即可完成向量的创建,只要在括号中输入每个向量元素就可以了;同时,它还可以把两个向量组合成一个。如果知道向量是以什么规律排列的,也可以按照规律生成向量。比如,创建的向量是等差数列的,就可以使用seq()函数;创建从a到b的连续整数,使用a:b就可实现;从一串数字中随机抽取几个数,使用sample()函数就可完美实现;用字符串的粘贴功能函数paste0(),可以把字符和数字有规律地组合起来(比如,当批量命名变量时)。具体示例如下:

(2)引用。要从一个数据串中提取出其中一部分元素,在方括号中指定元素所处的位置即可调用。然而更多遇见的情形是:只知道想要哪个元素,却不知道它在“哪里”,这时可以通过which()函数来实现。另外,which.max()和which.min()还可以直接获取最大值和最小值的位置,是个很方便的定位操作(向量是很多数据结构的基础,之后介绍的数据框、列表结构中也会大量使用)。

(3)集合运算。一个向量可以看作数学中的一个集合,因此自然可以对其进行许多常见的集合运算。这部分最常用的是以下三个:求交集intersect();求并集union();求差集setdiff()。

这几个函数虽然平淡无奇,但在有些场合的巧妙使用能大幅提升处理大型数据集的效率。

以上就是一些向量的通用操作命令,下面将针对两种常见类型——数值向量和字符向量,详细讲解适合它们各自特点的操作技法。

2.常见类型

(1)数值向量。处理数值向量有很多函数。表2-4中罗列了常用的函数及其简单用法。下面主要对最容易混淆和特色功能比较隐蔽的函数进行介绍,这些函数也是在数据分析中常用的函数。

表2-4 数值向量常用函数说明表

下面具体介绍两个特色功能很隐蔽的函数:match()和cut()。

很明显,match()函数可为我们在 y 中找到 x 的元素所对应的位置,这在做两个对象匹配时很有用。上面的例子是什么意思呢? x 是一组整数向量,letters是从a到z的26个字母向量,因此letters[x]实际上完成了一件什么事呢?就是把letters对应位置的字母取出来了。再来看match一行代码的过程是什么意思呢?是来看看, y 中的每个字母分别在letters[1:4](即a,b,c,d)的哪一位呢?然后R返回结果告诉我们,分别在第1位,第1位,第1位,第2位……这样你就能够理解match的具体匹配过程了吧。

另一个函数cut()则可以帮我们完成一项数据分析的重要功能——连续数据离散化,也就是把连续型数据变成离散的定性数据来参与建模。用好cut()函数,就可以省去自己用条件语句转换的麻烦。下面的代码完成了一个什么功能呢?Age是一个由整数组成的向量,然后cut()函数的意思是,将Age的每个整数转换成对应的标签。怎么转换呢?将20~30岁分为一组,标记为壮年;30~50岁分为一组,标记为中年;50~70岁为一组,标记为长辈;70~100岁为一组,标记为老年。这样,原始的Age数值数据就变成了由标签组成的“因子”类型的向量了。

下面再来看两个最容易混淆的函数:sort()和order()。

从以上操作可以看出,通过sample()抽样得到的 x 为1 5 4 6 7。sort()函数简单,就是把一个向量排序,默认是递增顺序,这里得到的结果是:1 4 5 6 7。如果想得到递减顺序,可以设置decreasing=T。order()函数的逻辑则稍复杂些:它能够输出把原向量升序排序后(也就是得到:1 4 5 6 7后),每个排序后数据在原始向量中的位置。如在本例中,order()函数每个输出的含义是:最小值1在原始向量 x 中位于第1个,因此order()函数的输出结果中第一个元素就是1,而次小值4在原始向量 x 中位于第3个,因此order()函数的输出结果中第二个元素就是3,依次类推。因此,命令x[order(x)]的实现效果跟sort(x)一样。

相对于sort(),order()的功能差不多,但明显难懂一些,那么order()是不是用处就不大呢?下面给一个场景,在操作Excel时常用一个功能:对一个数据表,先按 x 列排序,排好后再按 y 列排序,甚至还得按 z 列再排次序,这类有先后的复杂排序功能在R中如何实现呢?此时就可以通过order()函数来实现,具体的操作将在介绍数据框时详细讲解。

(2)字符向量。字符这种变量类型不同于数值,它有很多独有的特征,处理时需要用专用函数来实现。先拿单独一个字符对象来说,就有很多独特性,比如我们通常不太会对一个数值求它的长度,但对一个字符,很可能就需要了解它的长度。

既然有了长度,就可以对字符进行切分,提取出一个子字符串,这时会使用到的函数是substr(),具体用法是substr("char",begin_position,end_position)。示例如下:

切分可以让字符变小,要想让字符变大就要用到另一个粘贴函数paste(),这个函数可以把一个向量的各个元素粘起来,也可以把多个向量对应位置上的元素统一粘起来。示例如下:

需要注意的是,collapse是用来给结果的各个元素加连接符的参数。当然除了collapse,还有另外一个连接参数sep。观察以下结果,看是否能体会参数collapse和sep的区别。

collapse可以把一个向量内部的元素粘连起来,而sep则适用于把不同向量分别粘起来,所以它在上面代码的第二行命令中其实并没有起什么作用。

为了进一步熟悉这两个参数的区别,读者可以尝试是否能从下面的命令看出它们的实现结果。

下面再介绍一下大家最关注的查找替换函数,这个在Office里非常热门的功能可快速扫描大量文本查找特定的字符。在R中,用来查找的函数是grep(),用来替换的函数是gsub(),基本用法非常简单,可以参考以下例子:

grep()和gsub()的用法虽然简单,但却是清洗数据必备函数。设想几个场景。近年来,青春片热度超前,那么在前面提到的电影数据集中,青春片的票房表现如何呢?最简单的方法是,通过grep()提取片名含有“青春”的进行观测,就能一目了然了。

从以上命令可以看出,电影数据集中的青春片是改编自刘同同名小说的《谁的青春不迷茫》,它在3个月内达到了近1.78亿元的票房,豆瓣评分6.4。我们再通过简单的逻辑比较命令就能马上获知,在电影数据集中,这部影片的票房在均值之下,但豆瓣得分在平均水平以上。

再比如,目前,数据分析师这个职业炙手可热,大家对其可观的收入也是众说纷纭。假如我们获得了5个数据分析师年薪统计的一手资料,想看看收入的平均值和中位数如何,可不巧的是,它们后面均带了“万”这个单位,这时该怎么办呢?gsub()函数就可以派上用场了(别忘了再转换成numeric格式)。

最后,我们通过一个总体的表格来整体回顾一下字符向量的处理要点(见表2-5)。

表2-5 字符向量处理函数说明表

向量是R语言中最基本的数据结构,实际上,向量化处理在R语言中是非常重要的编程思想,它能让我们避免很多循环,使代码更为简洁、高效和易于理解。什么是向量化呢?简单来说,就是把应用于每一个元素的操作应用于这些元素组成的向量,从而实现批量计算。

2.1.3 矩阵

前面介绍了R中重要的数据结构——向量。R中的向量与数学中的向量非常相似,都只有一个维度。但实际上,信息丰富的数据通常需要多个向量来描述。比如,在狗熊会微信公众号上,熊大的男粉丝和女粉丝数目分别是100人、200人,那么在R中可以用向量c(100,200)来代表熊大的粉丝统计;狗熊会不断壮大,加入了水妈、政委、段子手,他们的男女粉丝数目分别用向量c(100,0),c(0,100),c(50,100)表示。这样,需要用4个向量才能表示狗熊会中熊大、水妈、政委、段子手的粉丝数目。而且随着狗熊会队伍的不断壮大,就需要用越来越多的向量代表每个人的粉丝数目。那么,能不能把这些向量放在一起表示呢?它们统计的不都是“男粉丝”“女粉丝”吗?没错,当然可以。将上述变量叠加起来,就是我们要讲到的矩阵(见图2-6)。

图2-6 狗熊会粉丝图

在R中,矩阵其实就是一个二维数组,外表类似Excel中的表格,但重点是其中的每个元素都必须具有相同的数据类型。矩阵也是在各种数值模拟运算中使用最多的数据结构。下面介绍对矩阵的典型操作。

1.创建及引用

在R中创建矩阵基本分成两种情形:

(1)生成一个矩阵。生成一个矩阵很简单,使用matrix()函数即可,其语法是:matrix(vector,nrow=number_of_rows,ncol=number_of_columns,byrow=T/F),即把要组成矩阵的元素、矩阵的行列数以及排列模式设置好。如果想生成对角矩阵,那么直接用diag()函数。下面举两个典型例子:生成一个全部取同样值(例如,全部取0)的矩阵,以及生成一个对角线元素全是1(也可以是其他同样的取值)的矩阵。

(2)把已有数据转换成矩阵类型。比如把向量转换成矩阵,通过以下代码就可以将向量1:12转换成3行4列的矩阵。

需要注意的是,这里在转换时矩阵元素的排列方式是将向量按列排列的(也可以通过设置byrow = T将其改变成按行排列)。

另外,也可以用diag(vector)函数转换成以vector为对角线的对角矩阵。

2.基本的矩阵操作

通常拿到一个矩阵后,会按什么步骤处理呢?首先,要了解这个矩阵的概貌,比如用dim()查看矩阵的行列数,或采用nrow()提取矩阵的行数,ncol()提取矩阵的列数。其次,如果引用矩阵中的某些元素,与向量类似,将目标元素的位置用方括号括住即可,只不过由于矩阵是二维,因此引用时常常需要具体定义其行列号。若只是提取或者更改矩阵的行列名,则采用rownames()和colnames()即可。由此可见,给一个矩阵的行(列)批量命名实际就是在给一个向量赋值,那么前面提到的paste()函数就可以派上用场了。

此外,还可能需要将多个矩阵合并扩充信息量,cbind(),rbind()就可以实现最简单的矩阵之间的合并:前者代表按列合并,后者代表按行合并。

3.对矩阵的数学操作

矩阵作为高等数学的“宠儿”,一直作为重要的数学工具出现在我们的视野,因此R中自然少不了对矩阵数学方面的操作,最简单的加减乘除、求逆的运算等都有对应的函数来实现。值得注意的是,在R中,矩阵的加法和减法使用的符号是“+”“-”,但乘法有所不同,使用的符号是“%*%”,而不是“*”。下面的代码给出了矩阵A%*%B以及A*B的结果,你能找出它们的区别吗?

从以上代码可以发现,如果采用A*B这种写法,只会计算出A中每个元素与B中每个元素对应相乘的结果,并不是线性代数中用到的乘法。

另外,矩阵的逆在R中需要用函数solve(M)计算(而不能直接用M^{-1}计算得到)。还有更复杂的,比如对一个矩阵做特征值分解或者奇异值分解等操作,手工计算过程非常烦琐,而R语言可以立即给出结果。常见的使用矩阵的数学操作如表2-6所示。

表2-6 矩阵的数学操作函数

R中自带的矩阵处理函数基本可以解决大部分的矩阵运算问题。但是,当矩阵规模增大时,某些函数的运算效率就会显得捉襟见肘,例如,矩阵求逆,特征值、奇异值求解等。为了提高大规模矩阵的运算效率,向大家推荐一个R包:rARPACK。此包主要针对大规模矩阵运算,包内函数eigs()可用来进行特征值分解,svds()用来进行SVD分解。实际上,它们提高分解效率的关键在于,仅对矩阵计算一部分有代表性的特征值(奇异值)来近似分解,类似于主成分分析中只选取前几个主成分来概括原始信息的思想。下面通过一个简单的例子进行展示。

从以上例子可以看出,当分解一个1 000维的矩阵时,rARPACK包就比基本包里的分解函数效率提升了十几倍;当维数进一步增加时,它们的差距会更大。

4.稀疏矩阵

下面介绍一种特殊矩阵——稀疏矩阵。这是一个与时俱进的新名词,近年来越来越多地被提到。

所谓稀疏矩阵,指的是这样一种矩阵:它所包含的元素中,数值为0的元素远远多于非0元素。随着现代数据采集设备越来越多,在很多领域收集到的数据都有可能带有稀疏的特征。最典型的比如电商网站的用户购买记录、社交网络中的关注关系矩阵。假如把淘宝所有商品的用户购买记录做成一个矩阵,每一列是一个商品,每一行是一个用户,其中的数值代表这个用户是否购买过这个商品,那么就可以用表2-7表示出来。

表2-7 购物篮示例

表2-7只是一个小示例,如果这是一张囊括天猫商城的购物列表,它起码会有数十万的商品列,那么每个用户所购买的商品一定只有极少的一部分,表现在矩阵里就是大量的0,这就形成了一个高度稀疏的矩阵。

再比如,把微博用户之间的关注数据抽象成邻接矩阵A,行和列都代表微博的所有用户(见表2-8)。

表2-8 微博用户相互关注矩阵

表2-8中,数值为1代表第 i 个用户关注了第 j 个用户,那么这必然是巨大的稀疏矩阵,因为茫茫人海中,我们关注的只能是寥寥无几(见图2-7)。

图2-7 社交网络示意图

了解到稀疏矩阵的确真实存在,下面介绍R中擅长处理这类矩阵的包——Matrix。Matrix包提供了很多独特的存储、处理稀疏矩阵的方法。比如生成一个稀疏矩阵有两个函数可用:Matrix()函数和spMatrix()函数。Matrix()函数使用的参数与普通matrix()函数类似,通过输入数值以及行列数字来定义,区别在于需要设定参数sparse=T或F来定义是不是稀疏矩阵。需要注意的是,虽然参数设置类似,但Matrix()函数生成的矩阵对象却是与matrix()完全不同的类型。如果sparse设置为T,它会生成dgCMatrix矩阵类型,也就是以一种先将列按顺序排好再存储起来的方式存储。spMatrix()函数则是通过定义非0元素的行列位置来生成dgTMatrix稀疏矩阵类型,其存储方式是将非0元素所在的行、列以及它的值构成一个三元组( i,j,v ),然后再按某种规律把它们存储起来。下面通过几个例子来介绍稀疏矩阵的生成方法。

首先是Matrix()函数。通过设定参数sparse=T就可以生成一个稀疏矩阵;如果不设定该参数,则会自动数矩阵中0的个数,超过一半就会设置为稀疏模式。下面来看一个稀疏矩阵。

其次是spMatrix()函数。它的使用语法是spMatrix(nrow,ncol,i=integer(),j=integer(),x=numeric()),其中前两个参数设定矩阵的行列数, i 设定需要填补数字的行号, j 为列号, x 就是需要填补的元素,再对矩阵进行summary()就可以统一看到它填补元素情况了。

理论上讲,这种方式更适合存储大规模的稀疏矩阵,规模越大,优势越明显;同时,矩阵中0的比例不能太低,否则不如直接存储成一般矩阵。当矩阵维数较低时,稀疏矩阵占用的内存反而比普通矩阵更大,生成时间也可能更长,但是随着矩阵维数的增加,稀疏矩阵在内存大小和生成时间方面的优势会越来越明显。

除了在存储上的优势以外,稀疏矩阵在运算上也有巨大威力,仔细观察下面的结果。

从以上命令可以发现,在做乘积运算时,存储为稀疏矩阵模式会大大提高运算效率,矩阵维数进一步增大,它们的差距就会更加明显。所以常做大规模矩阵运算的读者需要注意,如果面对的矩阵很稀疏,就可以考虑使用Matrix包。

2.1.4 数据框

前面介绍了矩阵这种数据结构,接下来介绍另外一种长相类似但更常见、功能更丰富的数据结构——数据框(data.frame)。如果说矩阵是数值运算的“明星”,那么数据框就是存储、处理实际数据的“主角”,是实战中的核心主力,有着丰富的变换技法。下面就从基本的创建引用操作出发,展示它变大、变小、变序、变形的各类操作。

数据框是R中最常见的数据结构,一般来讲,从csv或txt文件读入时就会自动存储为数据框对象。该结构同样为表格状,但与矩阵不同的是,矩阵只可以存储一种数据类型(比如,数值型与字符型数据不能同时存在于矩阵中)。而实际中,我们看到的数据表格往往有很多类型,想要在R中读入并表示这种数据,数据框就可以派上用场了。需要特别注意的是:数据框的每一列必须是同一种数据类型。如果不符合规定,R会在一定范围内强制转换数据类型,比如输入的一列里既有文本又有数值,它会把该列强制转换成文本格式。

以下就以前文表2-2所示的数据为例,详细讲解针对数据框对象常用的操作,看看如何创建一个数据框,又如何让它变大、变小、变序、变形“随心玩”。

1.创建

如果已有外部数据源,将外部数据读入R中并赋值给一个对象就可以了;如果需要自己创建数据框,输入数据也很方便,data.frame()就是专门构造数据框的函数。它的语法是data.frame(col1,col2,col3),即先定义好每列的向量,然后组合成一个数据框即可。

2.汇总

拿到一个陌生的数据集,我们通常想先睹为快,快速了解一下数据情况,这时使用一定的汇总类函数就可满足需求。比如用函数head()提取数据前6行,就可以看到数据概貌;用函数str()来展示每列的数据类型,就可以确定是连续的数值还是离散的因子;如果不仅想看部分数据、了解数据类型,还想知道每列数据整体情况、整体的取值范围,这时就可以使用summary()。summary()是清洗数据的必备之选,可自动根据数据类型调整输出结果。具体来说,对于连续数据,它能给出输出数据的分位数值,这样,那些异常值就无处遁形;对于类别数据(以factor存储形式),它能给出输出每个类别的数目统计,这样,各类数据样本数是否平衡就一目了然。

这里要特别提醒的是:这些函数看似简单,但在实操过程中非常重要。当对不同数据源做各种合并、匹配、排序、删除等操作时,时刻要有一种“看到新数据先summary(),看看是不是符合自己的预期,有没有新异常”的习惯,也就是要有“时刻把握你的数据状态、进度”的意识,否则,待到后面报错时再重新回来查看就费时费力,万一本来匹配有误最后程序还没报错,后果会很严重。所以我们要记得不断查看处理的每一步到底对数据做了什么。

下面是str()函数的演示结果,该结果可显示出几种信息:(1)不同变量的数据类型。票房boxoffice和豆瓣得分doubanscore为数值型;电影时长duration、主演1的百度搜索指数index1、主演2的百度搜索指数index2为整型;电影名字name、类型type以及导演与演员的名字都是字符型变量。(2)变量的前几个取值。例如前几部电影的豆瓣得分doubanscore分别是6.4、6.9、4.5、5.7等。

3.变大——数据框的增列、合并

实践中,我们拿到的原始数据可能并不满足需要,常常需要增加新列,甚至合并新表进来扩充信息,下面就介绍一些可为你的数据“加瓦添砖”的操作。

首先是在数据框后面增加新列,语法为datolumn_name=vector,比如如下操作:

以上方法虽然实现简单,但并不适用于大规模增加、合并信息,这种方法更适合数据处理中“新变量生成”阶段。当需要把描述同一对象(即至少有一个共同列)但包含不同信息的几张表合在一起时,就需要另外一个强大的函数——merge()来实现。该函数的基本用法是:merge(x,y,by),其中x,y分别是要合并的两个数据框,by是它们共有的列。如果这个共有的列在两个数据框中的名字不同,还需要通过by.x,by.y分别定义识别。另外,和数据库中的操作类似,我们经常会发现两个数据框中匹配列的值域并不相同,这时还需要用all类的参数设置以哪个所包含的值域为准,如以下示范代码:

4.变小——数据框的筛选引用

信息稀缺是问题,信息过载也是障碍。对一个数据框缩小聚焦也是常用的技能,主要运用的是引用、筛选功能。基本的引用语法与矩阵类似,使用A[i,j]就可以提取出A中第 i 行第 j 个元素,即通过行列号来引用。筛选分为选列和选行,选列很简单,通过符号$配列名即可实现(例如,用movie $ name可以提取出name这一列);选行则一般通过行号,或者条件语句返回一个逻辑结果向量,而后R把其中为TRUE的行摘出来。

5.变序——数据框的内部排序

为了使数据框更加整齐有序,我们拿到数据后可能需要对它先进行排序工作。前面提到对一个向量排序很简单,用sort()整理就行,可是对于一个大的数据框该如何操作呢?在Excel中,我们经常用到先按某一列排序,再按另一列排序的功能,这样的功能在R中也能实现吗?可以按照下面的做法,其中decreasing参数用来设置是按升序还是降序排列。

6.变形——长表宽表互换

有丰富数据处理经验的读者可能见过这种情形:拿到一个数据集,虽然也是规规整整的表格形状,但它与常见的一行一个观测、一列一个变量的表有点差别,比如表2-9中,每一列分别记录了熊大和水妈在2020—2022年的粉丝数(表中数据纯属虚构)。

表2-9 2020—2022年熊大和水妈的粉丝数(宽表)

表2-9中后三列都在时间这个维度上,那么可以直接添加时间维度变量并列出取值吗?这就是著名的宽表变长表问题,变换之后如表2-10所示。

表2-10 2020—2022年熊大和水妈的粉丝数(长表)

如何才能把宽表整理成长表呢?可以使用reshape2包中的melt()函数。下面先来看从表2-9变身为表2-10的详细代码。

melt()是一个可以把数据从宽格式转换为长格式的函数,它能把诸如“2020”“2021”“2022”这样的多列数据直接转化为两列,将原始的列名转化成为新列的取值。具体而言,参数id.vars用来设定要把哪列定住不动,然后其他列就会自动收入这一列中;参数variable.name用来设定这个新列的列名。以上的代码就是以id.vars为基准,其他所有的原始变量都排成一个新列,然后原始列下面的数值就会被记录在一个新的value列中。比如在上面的例子中,melt(mWide,id.vars = c("Name","Type"),variable.name = "Year")这一句关键性的代码是如何发挥作用的呢?它是保证Name和Type两列不变,其他的2020、2021、2022三列的列名组合成为一个新列的取值。这个新列的列名呢?由variable.name来界定,就是“Year”。

了解了从宽表变为长表,那么如何从长表变成宽表呢?我们可以使用函数dcast(),它同样在reshape2包中。其中,dcast()函数的第一个参数是要变形的数据框,第二个参数是公式参数形式,实现的代码如下:

这行代码的意思是,将mLong这个数据框展开成宽表,展开的具体形式是Name和Type保持不变,Year这列变量的不同取值分别作为新数据框的一列,由此表2-10就变成了表2-9。

7.R中的数据透视表——神奇的ddply()

在数据分析实战中,Excel最常用的功能应该是vlookup和数据透视表,尤其是后者。那么在R中,什么函数可以完成类似数据透视表的分组计算不同量的功能呢?那就是ddply()。ddply()是plyr包中的函数,常用于数据整理汇总等。下面就介绍一下ddply()的使用方式。

从以上代码可知,ddply()是一个把数据框按某种属性分组,然后分别应用同一函数的操作。其基本用法是ddply(.data,.variables,.fun=NULL),第一个参数是要处理的数据框;第二个参数是分组标记,这个标记可以是多个分类变量,例如上面的代码中就同时加入了类型和时长;第三个参数便是一个函数。ddply()处理数据的逻辑是按照第二个参数定义的分组变量把数据框分组成多个子数据框,然后作为第三个函数的输入。如果第三个函数需要有额外的参数输入,需要在第四个参数的位置设定,感兴趣的读者可以详细阅读ddply()的帮助文档。关于这类分组计算统计量的函数还有很多,比如base包中的by()函数,它运用与ddply()相似的语法可以完成一些分组计算,但最后会返回一个“by”类对象,它可以被转换为向量但却无法轻易变成数据框对象,大家可以根据个人情况选择合适的函数进行数据处理。感兴趣的读者还可以继续查看其他相关函数

数据框作为最常用的数据结构,处理解决它的函数多不胜数,完成同一个功能的函数也不一而足,读者在熟悉了前面介绍的基本函数后,还要养成持续关注R相关网站、博客的习惯,了解新包、新功能的开发与应用,思考和比较不同函数的异同,以及时补充自己的函数库,不断优化、简化自己的程序。

2.1.5 列表

下面接着介绍一个像机器猫的口袋一样神奇的数据结构——列表(见图2-8)。

图2-8 神奇的列表

列表可以容纳各种类型的数据对象,向量、矩阵、数据框,甚至一个列表也可以成为另一个列表的元素。下面的对象example就是一个列表,它的第一个元素是一个字符,第二个元素是一个数值向量,第三个元素是一个矩阵,第四个元素则是一个数据框。

列表同样是一种非常重要的数据结构,很多复杂的统计函数最终的返回结果就是列表形式,方便后续分析时按需索引。下面介绍处理列表数据的常用操作。

1.创建

创建一个新列表很容易,采用函数list(a,b,c,d)就可以把a,b,c,d四个对象组合成一个list对象。请读者尝试生成上面的示例列表example,顺便复习前面所讲的各种类型变量的创建方法。

2.基本操作

面对一个list对象,首先要学会的三种基本操作是:查看、引用和添加元素。查看时仍使用函数str(),尤其是在列表这种数据结构中,快速查看列表内容有时在帮我们理清头绪方面大有用处。先来玩个小游戏,你能否在10秒内观察说出下面这个list的每个元素分别是什么?

可能你已经看晕了,这时采用str()函数就可以帮我们理清楚。别看这个函数非常简单,但它在实际处理列表数据时大有用处。它用层级告诉我们,这个list对象由两个子list构成,其中第一个list包含两个整数,第二个list包含两个对象:一个是英文字母的文本向量,一个是一个小list。这样就比之前清楚多了。

关于引用,可以采用list对象中子元素的名字引用,也可以使用它们的序号来引用,添加list元素的操作也类似。下面的示范展示了如何用名字、序号来引用complex的第一个元素,以及如何通过这两种方式为其添加新元素。

3.列表中的**ply函数

前面介绍过一个非常有用的可以对数据进行“高效分组,同步计算”的函数:ddply()。这个函数的优点在于把一整块数据按照某种规则分拆成多个部分,然后同步计算,各个击破。对比这里介绍的数据结构,它已经天然地把数据分好块,存储在一个列表中。我们不禁要问,有没有一种函数能够对列表中的每个元素同步计算,继续执行类似ddply()的第二步呢?当然有,常用的lapply(),sapply(),mapply(),以及tapply(),vapply(),rapply()等都可以实现这类操作,而且各具特色,感兴趣的读者可以查看相应的帮助文档。

下面以一份模拟的“老王耗子药的销售数据”为例,来说明这三个函数到底能帮我们做什么。

2014—2016年老王卖耗子药的价格如下:

老王基本上每年每个季度都要调整一次价格,那怎么看这三年耗子药价格的水平呢?一一求均值?其实lapply()就可以同时求出所有的均值。

lapply()函数可以对列表中的每个元素实施某种“相同的操作”,在上面的示例中这个“相同的操作”就是求均值,当然也可以换成求方差、求分位数等其他可以对数值向量操作的函数,如下所示:

均值求出来了,但它仍以列表形式输出,那么如何以向量、矩阵形式输出呢?此处就可以使用函数sapply()。sapply()与lapply()的工作原理类似,只是输出的结果就是想要的矩阵或向量,无须再做转换,操作结果如下所示:

最后要介绍的函数是mapply(),它能对多个list中相同位置的元素共同作用,也就是sapply()的多变量版本。以老王耗子药的数据为例,假如现在老王已经大概了解了这几年耗子药价格的波动,但是还不够,老王想知道自己每年最终收入是多少,这就需要结合耗子药的销量数据。老王找出了他每年年底统计的各个季度的销量数据,即下面的amount:

接下来需要做的是:把每年每个季度的价格与它对应的销量相乘,算出每个季度的总收入。这似乎需要在两个数据对象(列表)中操作,lapply()和sapply()都不符合要求。此时,就需要用到mapply()函数,如下所示:

这里,mapply()函数分别把price的第一个元素year2014与amount的第一个元素year2014相乘,price的第二个元素year2015与amount的第二个元素year2015相乘,依次类推。由于这里所指的每个元素都是一个向量,因此它们相乘的结果仍然是一个向量,最终组合起来即是一个矩阵。需要注意的是:使用mapply()函数需要在第一个参数中规定一个函数(这里是“*”),后面几个参数填入列表、向量等,mapply()的作用原理就是对后面几个参数对应位置的元素作用相应函数。

本节介绍了向量、矩阵、数据框及列表这四种常用的结构。后面还会学习各种各样的数据分析算法的R语言实现过程,亲眼见证一个有力的工具如何帮我们实现精妙的点子,如何一点点挖掘数据,让规律现身,让事实说话。当然,这一切实现的基础就是我们对数据的类型、结构的精准把握。


注释:

[1] 其中movie$"boxoffice"的含义是将movie数据集中的boxoffice这一列取出,这是“数据框”这种数据类型的一种引用方式,下文均类似。 nRtyX5JXF2rEwfyehXXcpFWG/qY2weGlezG/e8r1jcDsLihKDn6EgnNXBjUEk6fs

点击中间区域
呼出菜单
上一章
目录
下一章
×