【R实战 入门】五、高级数据管理

这里是佳奥!让我们继续R的数据管理学习!

本篇分为三个基本部分。

在第一部分中,我们将快速浏览R中的多种数学、统计和字符处理函数。

接下来,我们将讲解如何自己编写函数来完成数据处理和分析任务。包括探索控制程序流程的多种方式,循环和条件执行语句,以及研究用户自编函数的结构,在编写完成如何调用它们。

最后,我们将了解数据的整合和概述方法,以及数据集的重塑和重构方法。

让我们开始吧!

1 一个数据处理难题

让我们首先考虑一个数据处理问题:

一组学生参加了数学、科学和英语考试。为了给所有学生确定一个单一的成绩衡量指标,需要将这些科目的成绩组合起来。

另外,我们还想将前20%的学生评定为A,接下来20%的学生评定为B,依次类推。

最后,我们希望按字母顺序对学生排序。数据如下:

观察此数据集,马上可以发现一些明显的障碍。

首先,三科考试的成绩是无法比较的。由于它们的均值和标准差相去甚远,所以对它们求平均值是没有意义的。我们在组合这些考试成绩之前,必须将其变换为可比较的单元。

其次,为了评定等级,我们需要一种方法来确定某个学生在前述得分上百分比排名。再次,表示姓名的字段只有一个,这让排序任务复杂化了。为了正确地将其排序,需要将姓和名拆开。

以上每一个任务都可以巧妙地利用R中的数值和字符处理函数完成。在学习完下一节中的各种函数之后,我们将考虑一套可行的解决方,以解决这项数据处理难题。

2 数值和字符处理函数

本节我们将综述R中作为数据处理基石的函数,它们可分为数值(数学、统计、概率)函数和字符处理函数。并展示如何将函数应用到矩阵和数据框的行列上。

2.1 数学函数

对数据做变换是这些函数的一个主要用途。

上表中的示例将数学函数应用到了标量(单独的数值)上。

当这些函数被应用于数值向量、矩阵或数据框时,它们会作用于每一个独立的值。

例如:sqrt(c(4, 16, 25))的返回值为c(2,4, 5)。

2.2 统计函数

常用的统计函数如下表所示,其中许多函数都拥有可以影响输出结果的可选参数。举例来说:

提供了对象x中元素的算术平均数,而:


则提供了截尾平均数,即丢弃了最大5%和最小5%的数据和所有缺失值后的算术平均数。

更多实用方法参见help( )。

下面这段代码演示了计算某个数值向量的均值和标准差的两种方式:

第二种方式中修正平方和(css)的计算过程是很有启发性的:

1、 x等于c(1, 2, 3, 4, 5, 6, 7, 8),x的平均值等于4.5(length(x)返回了x中元素的数量)。

2、(x – meanx) 从x的每个元素中减去了4.5,结果为c(-3.5, -2.5, -1.5, -0.5, 0.5,1.5, 2.5, 3.5)。

3、 (x – meanx)^2 将 (x - meanx )的每个元素求平方,结果为c(12.25, 6.25, 2.25,0.25, 0.25, 2.25, 6.25, 12.25)。

4、sum((x - meanx)^2) 对 (x - meanx)^2) 的所有元素求和,结果为42。

数据的标准化

默认情况下,函数scale( )对矩阵或数据框的指定列进行均值为0、标准差为1的标准化:

要对每一列进行任意均值和标准差的标准化,可以使用如下的代码:

其中的M是想要的均值,SD为想要的标准差。

在非数值型的列上使用scale( )函数将会报错。要对指定列而不是整个矩阵或数据框进行标准化,我们可以使用这样的代码:


此句将变量myvar标准化为均值50、标准差为10的变量。

我们将在后续的数据处理问题的解决方法章节中用到scale( )函数。

2.3 概率函数

概率函数通常用来生成特征已知的模拟数据,以及在用户编写的统计函数中计算概率值。

在R中,概率函数形如:

其中第一个字母表示其所指分布的某一方面:

d = 密度函数(density)p = 分布函数(distribution function)q = 分位数函数(quantile function)r = 生成随机数(随机偏差)

常用的概率函数如下表:

我们先来了解正态分布有关函数:

如果不指定一个均值和一个标准差,则函数将假定其为标准正态分布(均值为0,标准差为1)。

密度函数(dnorm)、分布函数(pnorm)、分位数函数(qnorm)和随机数生成函数(rnorm)的使用示例见下图:

1、在区间[-3,3]绘制标准正态分布曲线:


2、位于 z=1.96 左侧的标准正态曲线下方面积是多少?

3、均值为500,标准差为100的正态分布的0.9分位点值为多少?

4、生成50个均值为50,标准差为10的正态随机数

1 设定随机数种子

在每次生成伪随机数的时候,函数都会使用一个不同的种子,因此也会产生不同的结果。

我们可以通过函数set.seed( )显式指定这个种子,让结果可以重现(reproducible)。

下面的函数runif( )用来生成0到1区间上服从均匀分布的伪随机数:

通过手动设定种子,就可以重现我们的结果了。

2 生成多元正态数据

在模拟研究和蒙特卡洛方法中,经常需要获取来自给定均值向量和协方差阵的多元正态分布的数据。

MASS包中的mvrnorm( )函数可以让这个问题变得很容易:

其中n是想要的样本大小,mean为均值向量,而sigma是方差-协方差矩阵(或相关矩阵)。

下列代码将从一个参数如下所示的三元正态分布中抽取500个观测:

生成服从多元正态分布的数据:

上述代码中设定了一个随机数种子,这样就可以在之后重现结果。

首先,指定了想要的均值向量和方差—协方差阵,并生成了500个伪随机观测。

为了方便,结果从矩阵转换为数据框,并为变量指定了名称。

最后,我们确认了拥有500个观测和3个变量,并输出了前10个观测。

注意的是,由于相关矩阵同时也是协方差阵,所以其实可以直接指定相关关系的结构。

2.4 字符处理函数

数学和统计函数是用来处理数值型数据的,而字符处理函数可以从文本型数据中抽取信息,或者为打印输出和生成报告重设文本的格式。

举例来说,我们可能希望将某人的姓和名连接在一起,并保证姓和名的首字母大写。下表是一些字符处理函数:

函数grep( )、sub( )和strsplit( )能够搜索某个文本字符串(fixed=TRUE)或某个正则表达式(fixed=FALSE,默认值为FALSE)。

正则表达式为文本模式的匹配提供了一套清晰而简练的语法。例如,正则表达式:

可匹配任意以0个或1个h或c开头、后接at的字符串。因此,此表达式可以匹配hat、cat和at,但不会匹配bat。了解更多请参考维基百科的regular expression(正则表达式)条目。

2.5 其他实用函数

表中的最后一个例子演示了在输出时转义字符的使用方法。

\n表示新行,\t为制表符,'为单引号,\b为退格,等等。(键入?Quotes以了解更多。)

例如,代码可生成:

请注意第二行缩进了一个空格。当cat输出连接后的对象时,它会将每一个对象都用空格分开。这就是在句号之前使用退格转义字符(\b)的原因。不然,生成的结果将是“Hello Bob .”。

在数值、字符串和向量上使用我们最近学习的函数是直观而明确的,具体运用于矩阵和数据框就是下一节的内容:

2.6 将函数应用于矩阵和数据框

R函数的诸多有趣特性之一,就是它们可以应用到一系列的数据对象上,包括标量、向量、矩阵、数组和数据框。

下面代码演示了将函数应用于数据对象:

上述代码中对矩阵c求均值的结果为一个标量(0.465)。

函数mean( )求得的是矩阵中全部12个元素的均值。但如果希望求的是各行的均值或各列的均值呢?

R中提供了一个apply( )函数,可将一个任意函数“应用”到矩阵、数组、数据框的任何维度上。apply函数的使用格式为:

其中,x为数据对象,MARGIN是维度的下标,FUN是由你指定的函数,而...则包括了任何想传递给FUN的参数。

在矩阵或数据框中,MARGIN=1表示行,MARGIN=2表示列。下面的例子是将一个函数应用到矩阵的所有行(列):

首先,生成了一个包含正态随机数的6×5矩阵。

然后你计算了6行的均值,以及5列的均值。

最后,计算了每列的截尾均值(在本例中,截尾均值基于中间60%的数据,最高和最低20%的值均被忽略)。

FUN可为任意R函数,这也包括我们自行编写的函数,所以apply( )是一种很强大的机制。

apply( )可把函数应用到数组的某个维度上,而lapply( )和sapply( )则可将函数应用到列表(list)上。

我们将在下一节中看到sapply(它是lapply的更好用的版本)的一个示例。

现在,让我们小试身手。

3 数据处理难题的一套解决方案

第1节中提出的问题是:

将学生的各科考试成绩组合为单一的成绩衡量指标、基于相对名次(前20%,下20%,等等)给出从A到F的评分、根据学生姓氏和名字的首字母对花名册进行排序。

下列代码给出了一种解决方案:


在第一次运行的时候,最后结果:

有些信息不完整,是名字录入的时候出了问题,后面排查发现是有的名字" "与名字之间有一个空格导致了识别的错误,修改后正常。

那么我们继续:

以上代码写得比较紧凑,逐步分解如下:

步骤1、原始的学生花名册已经给出了。options(digits=2) 限定了输出小数点后数字的位数,并且让输出更容易阅读。

步骤2、由于数学、科学和英语考试的分值不同(均值和标准差相去甚远),在组合之前需要先让它们变得可以比较。

一种方法是将变量进行标准化,这样每科考试的成绩就都是用单位标准差来表示,而不是以原始的尺度来表示了。这个过程可以使用scale( )函数来实现:

这里打断一下,关于这和函数我觉得有需要补充的:

1、数据的中心化

所谓数据的中心化是指数据集中的各项数据减去数据集的均值。例如有数据集1, 2,3, 6, 3,其均值为3,那么中心化之后的数据集为1-3,2-3,3-3,6-3,3-3,即: -2,-1,0,3,0

2、数据的标准化

所谓数据的标准化是指中心化之后的数据在除以数据集的标准差,即数据集中的各项数据减去数据集的均值再除以数据集的标准差。例如有数据集1,2,3, 6, 3,其均值为3,其标准差为1.87,那么标准化之后的数据集为(1-3)/1.87.(2-3)/1.87,(3-3)/1 .87,(6-3)/1.87,(3-3)/1.87,即: -1.069,-0.535,0,1 .604,0

我们上述函数scale( )实现的就是数据的标准化。

那么我们继续吧:

步骤3、然后,可以通过函数mean( )来计算各行的均值以获得综合得分,并使用函数cbind( )将其添加到花名册中:

步骤4、函数quantile( )给出了学生综合得分的百分位数。

可以看到,成绩为A的分界点为0.74,B的分界点为0.44,等等。

步骤5、通过使用逻辑运算符,你可以将学生的百分位数排名重编码为一个新的类别型成绩变量。下面在数据框roster中创建了变量grade。

步骤6、你将使用函数strsplit( )以空格为界把学生姓名拆分为姓氏和名字。

把strsplit( )应用到一个字符串组成的向量上会返回一个列表:


步骤7、我们可以使用函数sapply( )提取列表中每个成分的第一个元素,放入一个储存名字的向量,并提取每个成分的第二个元素,放入一个储存姓氏的向量。

"["是一个可以提取某个对象的一部分的函数——在这里它是用来提取列表name各成分中的第一个或第二个元素的。

我们将使用cbind( )把它们添加到花名册中。由于已经不再需要student变量,可以将其丢弃(在下标中使用-1)。

步骤8、最后,可以使用函数order( )依姓氏和名字对数据集进行排序:

先按照lastname排序,再按照firstname排序。

瞧!小事一桩!

完成这些任务的方式有许多,只是以上代码体现了相应函数的设计初衷。现在到了学习控制结构和自己编写函数的时候了。

4 控制流

在正常情况下,R程序中的语句是从上至下顺序执行的。但有时我们可能希望重复执行某些语句,仅在满足特定条件的情况下执行另外的语句。

这就是控制流结构发挥作用的地方了。

首先我们将看到用于条件执行的结构,接下来是用于循环执行的结构。

牢记以下概念:语句(statement):是一条单独的R语句或一组复合语句(包含在花括号{ } 中的一组R语句,使用分号分隔)。条件(cond):是一条最终被解析为真(TRUE)或假(FALSE)的表达式。表达式(expr):是一条数值或字符串的求值语句。序列(seq):是一个数值或字符串序列。

在讨论过控制流的构造后,我们将学习如何编写函数。

4.1 重复和循环

循环结构重复地执行一个或一系列语句,直到某个条件不为真为止。循环结构包括for和while结构。

1 for结构

for循环重复地执行一个语句,直到某个变量的值不再包含在序列seq中为止。语法为:

在下例中:

单词Hello被输出了10次。

2 while结构

while循环重复地执行一个语句,直到条件不为真为止。语法为:

作为第二个例子,代码:

又将单词Hello输出了10次。请确保括号内while的条件语句能够改变,即让它在某个时刻不再为真——否则循环将永不停止!

在上例中,语句:

在每步循环中为对象i减去1,这样在十次循环过后,它就不再大于0了。反之,如果在每步循环都加1的话,R将不停地输出Hello。这也是while循环可能较其他循环结构更危险的原因。

在处理大数据集中的行和列时,R中的循环可能比较低效费时。只要可能,最好联用R中的内建数值/字符处理函数和apply族函数。

4.2 条件执行

在条件执行结构中,一条或一组语句仅在满足一个指定条件时执行。条件执行结构包括if-else、ifelse和switch。

1 if-else结构

控制结构if-else在某个给定条件为真时执行语句。也可以同时在条件为假时执行另外的语句。语法为:

示例如下:

在第一个实例中,如果grade是一个字符向量,它就会被转换为一个因子。

在第二个实例中,两个语句择其一执行。如果grade不是一个因子(注意符号),它就会被转换为一个因子。如果它是一个因子,就会输出一段信息。

2 ifelse结构

ifelse结构是if-else结构比较紧凑的向量化版本,其语法为:

若cond为TRUE,则执行第一个语句;若cond为FALSE,则执行第二个语句。示例如下:

在程序的行为是二元时,或者希望结构的输入和输出均为向量时,请使用ifelse。

3 switch结构

switch根据一个表达式的值选择语句执行。语法为:

其中的...表示与expr的各种可能输出值绑定的语句。

通过观察下列代码,可以轻松地理解switch的工作原理:

它展示了switch的主要功能。

我们将在下一节学习如何使用switch编写自己的函数。

5 用户自编函数

R的最大优点之一就是用户可以自行添加函数。事实上,R中的许多函数都是由已有函数构成的。

一个函数的结构看起来大致如此:

函数中的对象只在函数内部使用。返回对象的数据类型是任意的,从标量到列表皆可。

假设我们想编写一个函数,用来计算数据对象的集中趋势和散布情况。

此函数应当可以选择性地给出参数统计量(均值和标准差)和非参数统计量(中位数和绝对中位差)。

结果应当以一个含名称列表的形式给出。

另外,用户应当可以选择是否自动输出结果。除非另外指定,否则此函数的默认行为应当是计算参数统计量并且不输出结果。代码如下:

mystats( ):一个由用户编写的描述性统计量计算函数:

要看此函数的实战情况,首先需要生成一些数据(服从正态分布的,大小为500的随机样本):

在执行语句:

之后,y$center将包含均值(0.00184),y$spread将包含标准差(1.03),并且没有输出结果。

如果执行语句:

y$center将包含中位数(–0.0207),y$spread将包含绝对中位差(1.001)。另外,还会输出以下结果:

下一个例子:

下面让我们看一个使用了switch结构的用户自编函数,此函数可让用户选择输出当天日期的格式。在函数声明中为参数指定的值将作为其默认值。

在函数mydate( )中,如果未指定type,则long将为默认的日期格式:

实战中的函数如下:

请注意,函数cat( )仅会在输入的日期格式类型不匹配"long"或"short"时执行。使用一个表达式来捕获错误输入的参数值通常来说是一个好主意。

有若干函数可以用来为函数添加错误捕获和纠正功能。你可以使用函数warning( )来生成一条错误提示信息,用message( )来生成一条诊断信息,或用stop( )停止当前表达式的执行并提示错误。

6 整合与重构

R中提供了许多用来整合(aggregate)和重塑(reshape)数据的强大方法。

在整合数据时,往往将多组观测替换为根据这些观测计算的描述性统计量。

在重塑数据时,则会通过修改数据的结构(行和列)来决定数据的组织方式。

本节描述了用来完成这些任务的多种方式,在接下来的两个小节中,我们将使用已包含在R基本安装中的数据框mtcars。这个数据集是从Motor Trend 杂志(1974)提取的,它描述了34种车型的设计和性能特点(汽缸数、排量、马力、每加仑汽油行驶的英里数,等等)。

6.1 转置

转置(反转行和列)也许是重塑数据集的众多方法中最简单的一个了。

使用函数t( )即可对一个矩阵或数据框进行转置。对于后者,行名将成为变量(列)名。

6.2 整合数据

在R中使用一个或多个by变量和一个预先定义好的函数来折叠(collapse)数据是比较容易的。

调用格式为:

其中x是待折叠的数据对象,

by是一个变量名组成的列表,这些变量将被去掉以形成新的观测,

FUN则是用来计算描述性统计量的标量函数,它将被用来计算新观测中的值。

作为一个示例,我们将根据汽缸数和挡位数整合mtcars数据,并返回各个数值型变量的均值:

在结果中,Group.1表示汽缸数量(4、6或8),Group.2代表挡位数(3、4或5)。

举例来说,拥有4个汽缸和3个挡位车型的每加仑汽油行驶英里数(mpg)均值为21.5。

在使用aggregate( )函数的时候,by中的变量必须在一个列表中(即使只有一个变量)。我们可以在列表中为各组声明自定义的名称, 例如 by=list(Group.cyl=cyl, Group.gears=gear)。

指定的函数可为任意的内建或自编函数,这就为整合命令赋予了强大的力量。

6.3 reshape包

reshape包是一套重构和整合数据集的万能工具。

我们将慢慢地梳理整个过程,并使用一个小型数据集作为示例,这样每一步发生了什么就很清晰了。

大致说来,我们需要首先将数据“融合”(melt),以使每一行都是一个唯一的标识符—变量组合。

然后将数据“重铸”(cast)为你想要的任何形状。在重铸过程中,你可以使用任何函数对数据进行整合。

将使用的数据集如下表所示:

在这个数据集中,测量(measurement)是指最后两列中的值(5、6、3、5、6、1、2、4)。

每个测量都能够被标识符变量(在本例中,标识符是指ID、Time以及观测属于X1还是X2)唯一地确定。

举例来说,在知道ID为1、Time为1,以及属于变量X1之后,即可确定测量值为第一行中的5。

1 融合

数据集的融合是将它重构为这样一种格式:每个测量变量独占一行,行中带有要唯一确定这个测量所需的标识符变量。

要融合表中的数据,可使用以下代码:

将得到如下表所示的结构:

注意,必须指定要唯一确定每个测量所需的变量(ID和Time),而表示测量变量名的变量(X1或X2)将由程序为你自动创建。

既然已经拥有了融合后的数据,现在就可以使用cast( )函数将它重铸为任意形状了。

2 重铸

cast( )函数读取已融合的数据,并使用你提供的公式和一个(可选的)用于整合数据的函数将其重塑。

调用格式为:

其中的md为已融合的数据,formula描述了想要的最后结果,而FUN是(可选的)数据整合函数。

其接受的公式形如:

在这一公式中,rowvar1 + rowvar2 + ...定义了要划掉的变量集合,以确定各行的内容。

colvar1 + colvar2 + ...则定义了要划掉的、确定各列内容的变量集合。参见下图中的示例:


由于右侧(d、e和f)的公式中并未包括某个函数,所以数据仅被重塑了。

反之,左侧的示例(a、b和c)中指定了mean作为整合函数,从而就对数据同时进行了重塑与整合。

例如,(a)中给出了每个观测所有时刻中在X1和X2上的均值;示例(b)则给出了X1和X2在时刻1和时刻2的均值,对不同的观测进行了平均;在(c)中则是每个观测在时刻1和时刻2的均值,对不同的X1和X2进行了平均。

如你所见,函数melt( )和cast( )提供了很多灵活性。很多时候,我们不得不在进行分析之前重塑或整合数据。举例来说,在分析重复测量数据(为每个观测记录了多个测量的数据)时,通常需要将数据转化为类似于上表中所谓的“长格式”。

7 小结

本章总结了用于处理数据的数学、统计和概率函数。

我们看到了如何将这些函数应用到范围广泛的数据对象上,其中包括向量、矩阵和数据框。我们学习了控制流结构的使用方法:

用循环重复执行某些语句,或用分支在满足某些特定条件时执行另外的语句。

然后编写了自己的函数,并将它们应用到了数据上。

最后,我们探索了折叠、整合以及重构数据的多种方法。

我们准备好告别第一部分,并进入激动人心的数据分析世界了!在接下来的几篇中,我们将探索多种将数据转化为信息的统计方法和图形方法。

那么我们,下篇再见!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容