Introduction to DS - R 语言初探之贰
Data Scientist spend up to 80% of the time on data cleaning and 20% on actual data analysis.
与上一节不同,这一节中我们主要关注 data frame 而不是 vector;我们将引入 tidy format 的概念,并介绍能够有效操纵 tidy data 的开源包 tidyverse。
在开始之前,我们在 RStudio 的 REPL 中输入
install.packages("tidyverse")
安装 tidyverse
包。
This article is a self-administered course note.
It will NOT cover any exam or assignment related content.
Tidy Data
我们怎样根据数据的含义 (semantics) 来组织其结构 (structure) 呢?一般来说,datasets are a collection of values, either quantitative or qualitative. These values are organized in 2 ways:
- Variables - all values that measure the same underlying attribute across units.
- Observations - all values measured on the same unit across attributes.
We say a dataset is tidy if:
- each row represents one observation.
- columns represent the different variables available for each of these observations.
- each cell is a single value.
我们来看看这个例子:下面这组数据反映了德国与韩国 1960-1962 年的生育率。
1 | #> country 1960 1961 1962 |
这一组数据显然不是 tidy 的 (或者说,是 messy 的);这是因为:
- Each row includes several observations.
- One of the variables, year, is stored in the header.
1 | #> country year fertility |
我们对这组数据进行整理,使其变得 tidy:每组 observation
占据一行,抽象出 year
, fertility
变量并整理到列中。接下来,我们可以使用 tidyverse
包中提供的各种函数对 tidy data 进行操纵。
Manipulating Data Frames
本小节中的函数引入自 tidyverse
中的 dplyr
包。
dplyr
functions are aware of variable names. (no need to specifymurders$total
)- most
dplyr
functions take data frames as their first argument.
Adding a column
使用 mutate()
函数。
1 | library(dslabs) |
将为 murders
data frame 新增一个变量 (一列)
rate
。
Subsetting
使用 filter()
对 data frame 进行 subsetting。(横向
filter
)
1 | filter(murders, rate <= 0.71) |
Selecting columns
select()
函数选定给定 data frame 中指定的 variables
并形成一个新的 data frame。(纵向 select
)
1 | new_table <- select(murders, state, region, rate) |
The Pipe: |>
or
%>%
与 shell 中的 pipe |
的用法一致。在 R 中,the pipe
|>
or %>%
sends the result of
the left side of the pipe to be the first argument of the function on
the right side of the pipe.
注意,我们目前接触的所有 dplyr
函数
(mutate
, filter
与 select
)
的第一个参数都是给定的 data frame;这一性质为 pipe
的应用创造了良好的条件。再回到之前的程序:
1 | new_table <- select(murders, state, region, rate) |
数据的流向是 \(\rm{original \ data\to
select\to newtable}\),再由 \(\rm{newtable\to filter\to result}\)。使用
pipe,我们可以直接将 select
的输出导入到
filter
的输入,这样既省去了中间变量的定义,又提升了代码可读性。
1 | murders |> select(state, region, rate) |> filter(rate <= 0.71) |
当我们想将某个函数的输出导入至其他函数的第二个 (或其他所有非第一的)
参数的输入时,使用 placeholder (占位符) _
。联想到 shell
中的 pipe 也有相同的 idiom。
1 | log(8, base = 2) |
Summarizing Data
最简单的 data summary 例子:求平均值 (average) 与标准差 (standard deviation)。
summarize
对于给定的 data frame,summarize()
函数返回一个新的
summarized table。该 data frame:
- one row for each grouping variable [stay tuned].
- one column for each of the summary statistics that you have specified.
1 | data(heights) |
由于这里没有指定 grouping variables,summarize
函数仅仅返回了一行数据,每一列对应一个我们指定的 summary statistic
(平均数与标准差)。
summarize
函数要与group_by
函数配合使用才能显出其强大之处。
Group then Summarize
分组摘要:A common operation in data exploration is to first split data into groups and then compute summaries for each group.
group_by
函数将指定的 data frame 按照某个 variable
分组,并返回一个 grouped data frame。该 variable
中每个不同的值对应一个 grouping variable。(例:sex
中有
Male
与 Female
两种 grouping variables)
这一特殊的 grouped data frame 被称为 tibble [stay tuned].
1 | heights |> group_by(sex) |
summarize
函数将对 grouped data frame
进行分组摘要;每一组 (每一个 grouping variable) 占据一行。
1 | heights |> |
pull
pull()
函数抽出给定 data frame
中的某一列。就功能上来说,其与 $
的作用一致;"it's mostly
useful because it looks a little nicer in pipe expression."
1 | pull(murders, total) |
再看一个例子:
1 | us_murder_rate <- murders |> |
us_murder_rate
只是个 single value,却被储存在 data
frame 中;这显然不合理。于是我们在 pipe 中添加 pull
函数:(us_murder_rate |> pull(rate)
等价于
us_murder_rate$rate
)
1 | us_murder_rate <- murders |> |
Sorting Data Frames
之前我们介绍了一系列排序函数,例如 sort
,
order
等;但那是对于 vector 而言的。data frames
有另外的排序函数 (同样在 dplyr
包中引入)。
arrange
对于给定的 data frame,arrange
函数根据指定的某个变量
(某一列) 进行排序。
1 | murders |> |
arrange
函数默认由小到大进行排序,若想降序排序,我们使用
desc()
函数。
1 | murders |> |
若排序变量的类型是数字 (numeric
或
integer
),arrange(-population)
可以达到相同的效果。
Nested sorting
If we are ordering by a column with ties, we can use a second (or more) column to break the tie.
1 | murders |> |
实际上就是 arrange
函数允许传入多个排序变量,第 \(n\) 个排序变量就是排序的第 \(n\) 关键字。
The top \(n\)
top_n()
函数的定义有点奇怪,很容易被它的名字所迷惑。在其
manual page 中作者也声明该函数已经过时 (deprecated),建议使用
slice_min()
与 slice_max()
函数进行替代
(superseded)。
但既然 slide 中提到了,还是稍微说明一下:top_n(x, n, wt)
把 data frame x
的 wt
变量作为排序变量,选出前
n
行。但它并不对这些行进行排序!也就是说,返回的
n
行仍遵循原来在 x
中的相对顺序。
还是来看一个例子:
1 | df <- data.frame(x = c(8, 9, 10)) |
Tibbles
(需要 tidyverse
包) Tibbles tbl
是一种特殊的 data frame。在之前我们已经接触过,group_by()
函数返回的是分组后的 grouped data frame,也就是 tibbles。
1 | murders |> group_by(region) |
可以看到,class()
函数返回了许多奇怪的东西:tbl
即
tibble,summary()
与 group_by()
函数总是返回该类型的 data frame。其中,group_by()
函数返回的 tbl
又与 summary()
函数不同,是一种
grouped_df
;其中还存储了额外的分组信息 (grouping
information)。
除了 tibbles can be grouped 这一特性之外,tibbles 与普通的 data frames 还有许多不同之处。
Tibbles display better
The print method for tibbles is more readable than that of a data frame. (在 RStudio 上试试即可)
- 使用
tibble()
来创建一个新的 tibble (格式与data.frame()
一致)。 - 使用
as_tibble()
来将某个 data frame 转化为 tibble。
1 | murders |
Subsets of tibbles are tibbles
对 data frame 进行 subset 后得到的不一定是 data frame,还可能是 vector 或 scalar;但对 tibble 进行 subset 后得到的仍然是 tibble。
1 | class(murders[, 4]) # pull the 4th column |
Tibbles give better error msg
试图访问某个 data frame 不存在的 column 时 $
将会返回一个不带任何警告的 NULL
。这十分的 error-prone。但
tibble 则会正常的弹出警告信息。
1 | murders$Population |
Tibbles can have complex entries
While data frame columns need to be vectors of numbers, strings, or logical values, tibbles can have more complex objects, such as lists or functions.
1 | tibble(id = c(1, 2, 3), func = c(mean, median, sd)) |
Tidyverse Conditionals
我们之前已经介绍了 ifelse()
这一
conditional;tidyverse
包中还提供了其他 conditionals。
case_when
和 Standard ML 中的 []
语义很像,本质上是连续的
if-elseif-...-elseif
嵌套。
1 | x <- c(-2, -1, 0, 1, 2) |
A common use for this function is to define categorical variables based on existing variables.
1 | murders |> |
between
我们使用 between(v, a, b)
函数来确定某个值
v
是否在区间 [a, b]
中。下面的两个命令等价:
1 | x >= a & x <= b |
data.table
除了 tidyverse
包提供的 tibble
外,data.table
包提供的 data.table
也是传统
data frames 的一种 alternative。
在使用 data.table
object 前,先导入
data.table
包:library(data.table)
。使用
setDT()
函数将一个 data frame 转为 data.table
类 object:murders <- setDT(murders)
。
Selecting
以下的 selecting 方式,不仅适用于 data.table
类对象,也适用于普通的 data frame。
1 | murders[, c("state", "region")] |> head() |
但 .()
这一特殊的函数只能在导入了
data.table
包后才能使用。R 将 .()
中的变量识别为 column names,而不是 R 环境中的其他对象。
1 | murders[, .(state, region)] |> head() |
Manipulating columns
回忆起在 dplyr
包中我们使用 mutate
函数来为
data frame 添加新的一列。在 data.table
包中我们则使用
:=
函数。
1 | s <- murders[, rate := total / population * 100000] |
若想同时定义多个 columns,向 :=
函数中传入多个参数。(注意要加引号;真是奇怪的 syntax)
1 | s <- murders[, ":="(rate = total / population * 100000, rank = rank(population))] |
另外,如果 :=
函数中的 column name 是原
data.table
中一个已存在的 column name,那么 :=
函数的作用是 changing (columns) 而不是 adding (columns)。
Reference versus Copy
data.table
包在设计时的一个目的就是为了尽量节省空间。因此,与许多编程语言一样,在使用
data.table
包时一定要注意 reference 与 copy 的区别。
1 | x <- data.table(a = 1) |
在上例中,y
仅仅是 x
的一个 reference
(或称为 alias)。
1 | x <- data.table(a = 1) |
使用 copy
函数创建一个拷贝。
Subsetting
data.table
对象的 sebsetting:
1 | murders[rate <= 0.7] |
等价于使用 dplyr
包中的 filter
函数。
1 | filter(murders, rate <= 0.7) |
使用 data.table
包提供的特性,我们可以将
filter
与 select
函数压缩为 one succinct
command:
1 | murders[rate <= 0.7, .(state, rate)] |
该命令等价于:
1 | murders |> filter(rate <= 0.7) |> select(state, rate) |
Importing Data
在实际应用中,我们面对的大量数据通常是从外部导入的;system package
中的众多数据集 (如 murders
, heights
)
起到的多数是 demonstrative purpose。因此,了解如何 importing data
非常重要。
Dealing with paths
使用 system.file
函数获取 system package
所在的文件夹路径。(现实中几乎不可能用到)
1 | system.file(package="dslabs") |
当定位到某个文件所在的文件夹时,可以使用 file.path
函数获得该文件的路径。
1 | dir <- system.file(package="dslabs") |
或者使用更简单粗暴的 paste
方法,更适合程序员体质:
1 | paste(dir, "extdata", sep='/') |
Showing files
使用 list.files(dir)
方法输出 dir
指定路径下文件夹中的所有文件。
1 | dir <- system.file(package="dslabs") |
使用 wd
函数获取 working directory 的路径。结合该函数与
list.file
可以实现 ls
的功能:
1 | wd <- getwd() |
Copying files with paths
使用 file.copy
函数将指定路径下的文件拷贝到 working
directory 中。
1 | fullpath <- file.path(system.file("extdata", package="dslabs"), "murders.csv") |
Reading files
readr
与 readxl
包提供了读取不同类型数据集的函数。不同类型指:
- format: spaces, commas, semicolons, tabs...-separated values.
- suffix: txt, csv, tsv, xls, xlsx...
调用 readr
包中的 read.csv
直接对 working
directory 下指定的 csv 数据集进行读取。
1 | library(readr) |
此外,readr
包中的函数还可以读取给定 url
指向的某个远程资源。
1 | url <- "https://raw.githubusercontent.com/.../extdata/murders.csv" |
我们也可以使用 download.file
函数将其下载到本地后再进行读取。
1 | download.file(url, "murders.csv") |
Reference
This article is a self-administered course note.
References in the article are from corresponding course materials if not specified.
Course info:
Code: COMP2501, Lecturer: Dr. H.F. Ting.
Course textbook:
Data Analysis and Prediction Algorithms with R - Rafael A. Irizarry.