<- 1:6
die die
[1] 1 2 3 4 5 6
metseq
March 11, 2024
看《R for Data Science》的时候,Hadley Wickham推荐了这本书《Hands-On Programming with R》,中文译名是《R语言入门与实践》。这本书关注的是R语言作为语言的基础,基本不涉及统计,我对R语言的了解基本是调用demo代码的时候,自己感觉的,缺少对R语言作为语言的整体了解,所以这本书是我需要的。
对象,就是一个名称而已,在R中存储的数据就是一个R对象。
元素方式执行(element-wise execution)
矩阵内乘法
矩阵外乘法
函数调用
查看函数参数
原子型向量就是最简单的包含数据的向量。
比如之前的`die`
至包含一个值的原子型向量
🤔R竟然把一个整数存成了长度为1的向量,理解向量是理解R语言的关键。
R可以识别6种基本类型的原子型向量:
双整型(double)
整型(integer)
字符型(character)
逻辑型(logical)
复数类型(complex)
原始类型(raw)
浮点误差
属性是附加给原始型向量的额外信息,可以将属性赋予一个原子型向量(或者任意一个R对象)。属性并不会影响这个数据的取值,在显示该对象时也不会出现属性信息。你可以把属性理解为对象的元数据(metadata)。
通常来说,R会选择忽略这些元数据信息,但某些R函数会检查是否附有某些特定的属性值。这些函数会根据该数据对象的特定属性信息决定是否进行某些特定的操作。
R用NULL表示空值,意即没有任何信息。这里就是没有属性的意思。
可以自己生成一个NULL
对象
原子型向量最常见的三种属性时:名称(name),维度(dim)和类(class)。
通过赋予原子型向量维度属性,将其转成一个n维数组
更改对象的维度属性不会改变其类型,但是会改变这个对象的class属性。
[1] "integer"
[1] "integer"
[1] "integer"
[1] "matrix" "array"
$dim
[1] 2 3
R用一个特殊的类表示日期和时间数据。
顺序是:
逻辑型 ——> 数值型 ——> 字符型
也就是如果同时存在逻辑型和数值型,会转换成数值型,以此类推。
df <- data.frame(
face = c("ace", "two", "six"),
suit = c("clubs", "clubs", "clubs"),
value = c(1, 2, 3)
)
df
face suit value
1 ace clubs 1
2 two clubs 2
3 six clubs 3
创建列表或向量时,也可以命名
要从一个数据框中提取某个值或某一组值,先写出该数据框的名称,在其后紧跟一堆中括号。中括号有两个参数,以逗号分隔。R用第一个索引参数选择行,第二个列。
`deck[ 行, 列 ]`
有6种创建索引参数的方式:
正整数
负整数
零
空格
逻辑值
名称
face suit value
1 king spades 13
2 queen spades 12
3 jack spades 11
4 ten spades 10
5 nine spades 9
6 eight spades 8
face suit value
1 king spades 13
face suit value
1 king spades 100
face suit value
1 king spades 13
可以看到,new是一个副本,修改new并不会修改deck。
如果从一个数据框提取两列或以上,返回一个新的数据框。如果只提取一列,返回一个向量,可以添加drop = FALSE参数,仍返回数据框。
R返回不包含负整数索引对应的元素。注意,和python中切片的负整数不一样。
没什么用,返回一个空对象
空格代表选取该维度的所有元素。
使用$提取一列
[1] 13 12 11 10 9 8 7 6 5 4 3 2 1 13 12 11 10 9 8 7 6 5 4 3 2
[26] 1 13 12 11 10 9 8 7 6 5 4 3 2 1 13 12 11 10 9 8 7 6 5 4 3
[51] 2 1
如果列表对象中的元素有名称,也可以用$提取。
$number
[1] 1 2
$logical
[1] TRUE
$strings
[1] "a" "b" "c"
[1] 1 2
用单中括号和双中括号提取列表元素
使用单中括号提取一个列表对象的自己,R返回一个列表对象。使用双中括号,R返回元素值。
忽然想到的一个提取例子:
[1] 0
[1] 1000 0 0 0 0 0
[1] 1 0 1 0 1 0
创建一个原先对象中并不存在的新值。R会自动将对象的长度延伸以适应这个新值。
使用$为数据集添加新变量:
face suit value new
1 king spades 13 1
2 queen spades 12 2
3 jack spades 11 3
4 ten spades 10 4
5 nine spades 9 5
6 eight spades 8 6
将NULL赋予一个变量,就可以删除这个变量了。
逻辑向量和需要取子集的向量等长。
运算符 | 语法 | 判别 |
---|---|---|
> | a > b | a是否大于b? |
>= | a >= b | 大于等于 |
< | a < b | 小于 |
<= | a <= b | 小于等于 |
== | a == b | 等于 |
!= | a != b | 不等于 |
%in% | a %in% c(a, b, c) | c(a, b, c)中是否包含a? |
向量化的运算符
%in%是唯一不进行一一对比的运算符。如果左边提供一个向量,%in%会独立测试左边向量中的元素是否出现在右边向量中。
[1] FALSE
[1] FALSE FALSE
[1] FALSE FALSE TRUE
[1] FALSE FALSE TRUE TRUE
看到这里,发现作者对例子都是精挑细选的,上面的例子,左边向量长度分别从1到4,右边是3,没有变,注意比较输出结果的差异。
布尔运算符可以将多个逻辑测试的结果整合并输出为一个TRUE或FALSE。R有六种布尔运算符。
运算符 | 语法 | 判别 |
---|---|---|
& | cond1 & cond2 | 与 |
| | cond1 | cond2 | 或 |
! | ! cond1 | 非 |
any | any(cond1, cond2, cond3, …) | 所有条件,是否至少一个为真? |
all | all(cond1, cond2, cond3, …) | 所有条件,是否同时为真? |
xor | xor(cond1, cond2) | cond1和cond2是否只有一个为真? |
[1] FALSE
[1] TRUE
[1] FALSE
[1] TRUE
向量化的布尔运算符
R用特殊字符NA代表“不可用”(Not Available),可用于存储缺失信息。NA具有传染性。
去掉缺失信息
判断是否是NA
不能用==来判断NA
这一张作者讲的非常好,用目录树的结构来形象的比喻了R的环境系统。
建议查看原文全文。这里列出几个重要的知识点。
全局环境(R_GlobalEnv),最底层的环境,可以看到我创建的对象基本都在全局环境下。
[1] "a" "ar" "b" "bar" "c" "comp" "deck" "deck2"
[9] "df" "die" "five" "foo" "gender" "list1" "logic" "lst"
[17] "m" "new" "now" "roll" "roll2" "text" "vec"
空环境,空环境没有父环境,相当于根目录了。
在某个环境中,提取另一环境的变量
face suit value
1 king spades 13
2 queen spades 12
3 jack spades 11
4 ten spades 10
5 nine spades 9
6 eight spades 8
在某个环境中,将对象存到另一个环境
活动环境
在搜索对象时,R会遵循一系列的规则。这些规则被称作R的作用域规则(scoping rules)。
R在每一次函数求值时,都会创建一个新环境,然后带着运行结果回到调用该函数时的环境。
函数内创建的对象的环境时单独的,所以不会覆盖已经存在的对象。
[1] "I'm in R_GlobalEnv"
show_env <- function(){
a <- "I'm in show_env."
print('show_env函数内的环境:')
print(environment())
print('show_env函数的父环境:')
print(parent.env(environment()))
print('列出show_env环境下的对象')
ls(environment())
a
}
show_env()
[1] "show_env函数内的环境:"
<environment: 0x7f9e339a4600>
[1] "show_env函数的父环境:"
<environment: R_GlobalEnv>
[1] "列出show_env环境下的对象"
[1] "I'm in show_env."
[1] "I'm in R_GlobalEnv"
R会将一个函数的运行时环境与第一次创建该函数时所在的环境相连接。该函数的所有运行环境都将其作为父环境。
R是如何对待函数求值的?
当你调用一个函数之前,R是在活动环境中工作。我们可以将此活动环境称为调用环境(calling environment)。R会从该环境中调用函数。
调用这个函数时,R会创建一个新的运行时环境。这个环境是调用环境的子环境,也就是说调用环境是这个环境的父环境。R会将该函数的所有参数赋值到运行环境中,然后将运行时环境作为当前的活动环境。
接着,R会运行函数主体中的代码。如果代码创建了任何对象,R会将它们存储在活动环境中,也就是运行时环境。如果R调用了某些对象,遵循作用域规则搜索这些对象:首先搜索运行时环境,如果没有搜索父环境,还是没有搜索父环境的父环境,以此类推。
最后,当R完成函数运行时,它会将活动环境切换为调用环境。如果将函数运行的结果用赋值符<-赋给某个对象,那么这个新对象会存储在调用环境中。
如果对象是数据框,也是同样对待的。
有三个简单的策略可以简化编程任务:
if语法
if (this) {
that
}
else语法
[1] 0.14
[1] 3
else if 语法
有点像python里的字典
S3指的是R自带的类系统。这个系统掌管着R如何处理具有不同类的对象。一些函数会首先查询对象的S3类,再根据其类属性作出相应的响应。
print就是这样一个函数。
[1] 1e+09
[1] "2001-09-09 09:46:40 CST"
之前觉得很神奇,原来是这么回事!
R的S3系统有三个组成部分:
在3.2节中,讲到了R对象都具有的属性(名称,维度,类),这些属性包含了关于这个对象的某些额外信息并且被赋予了属性名称,附加在该对象上。属性不会影响对象的实际取值,但是作为该对象的某种类型的元数据,可以被R用于控制和管理这个对象。
使用attributes函数查看属性
$names
[1] "face" "suit" "value"
$class
[1] "data.frame"
$row.names
[1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
[26] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
[51] 51 52
R提供可很多辅助函数,可以帮忙设置和查看一些常见的属性
[1] "face" "suit" "value"
[1] 52 3
[1] "data.frame"
[1] "1" "2" "3" "4" "5" "6" "7" "8" "9" "10" "11" "12" "13" "14" "15"
[16] "16" "17" "18" "19" "20" "21" "22" "23" "24" "25" "26" "27" "28" "29" "30"
[31] "31" "32" "33" "34" "35" "36" "37" "38" "39" "40" "41" "42" "43" "44" "45"
[46] "46" "47" "48" "49" "50" "51" "52"
$names
[1] "face" "suit" "value"
$class
[1] "data.frame"
$row.names
[1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
[26] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
[51] 51 52
$levels
[1] "level 1" "level 2" "level 3"
在对待属性方面,R持放任主义态度。R允许你为某个对象添加你觉得必要的属性(R会忽略大多数属性)。只有在某个函数需要找到某个属性却又找不到时,R才会抱怨。
可以利用attr函数个某个对象添加任何属性。
NULL
[1] 1 2 3
attr(,"symbol")
[1] "one" "two" "three"
attr(,"pinyin")
[1] "yi" "er" "san"
[1] 2 3 4
attr(,"symbol")
[1] "one" "two" "three"
attr(,"pinyin")
[1] "yi" "er" "san"
structure函数可以创建带有一组属性的R对象。该函数第一个参数是对象或者对象的取值,其余参数是属性。
重点中的重点来了
刚才提到print函数是泛型函数。每次在控制台显示输出也是调用print函数
face suit value
1 king spades 13
2 queen spades 12
3 jack spades 11
4 ten spades 10
5 nine spades 9
6 eight spades 8
face suit value
1 king spades 13
2 queen spades 12
3 jack spades 11
4 ten spades 10
5 nine spades 9
6 eight spades 8
[1] "2024-03-14 23:02:18 CST"
[1] 1 2 3 4 5 6 7 8 9 10
你可能会觉得print会查找某个对象的类属性,再根据类属性的不同,使用一个if语句分配合理的输出。作者说很好,其实print的实现方式更简单。往下看👀
看看print的代码,它其实调用了一个特别的函数,叫做UseMethod。
head函数也是调用了UseMethod。
UseMethod检查你提供给print函数第一个参数的类属性,然后再将你所提供的待输出的对象交给一个新函数处理。比如提供一个POSIXct对象,就交给print.POSIXct函数;提供一个factor对象,就交给print.factor函数。
print.POSIXct和print.factor被称为print函数的方法(method)。这两个函数是普通函数,特别之处是UseMethod会调用它们处理对应的类。
function (x, tz = "", usetz = TRUE, max = NULL, ...)
{
if (is.null(max))
max <- getOption("max.print", 9999L)
FORM <- if (missing(tz))
function(z) format(z, usetz = usetz)
else function(z) format(z, tz = tz, usetz = usetz)
if (max < length(x)) {
print(FORM(x[seq_len(max)]), max = max + 1, ...)
cat(" [ reached 'max' / getOption(\"max.print\") -- omitted",
length(x) - max, "entries ]\n")
}
else if (length(x))
print(FORM(x), max = max, ...)
else cat(class(x)[1L], "of length 0\n")
invisible(x)
}
<bytecode: 0x7f9e345516d0>
<environment: namespace:base>
function (x, quote = FALSE, max.levels = NULL, width = getOption("width"),
...)
{
ord <- is.ordered(x)
if (length(x) == 0L)
cat(if (ord)
"ordered"
else "factor", "()\n", sep = "")
else {
xx <- character(length(x))
xx[] <- as.character(x)
keepAttrs <- setdiff(names(attributes(x)), c("levels",
"class"))
attributes(xx)[keepAttrs] <- attributes(x)[keepAttrs]
print(xx, quote = quote, ...)
}
maxl <- if (is.null(max.levels))
TRUE
else max.levels
if (maxl) {
n <- length(lev <- encodeString(levels(x), quote = ifelse(quote,
"\"", "")))
colsep <- if (ord)
" < "
else " "
T0 <- "Levels: "
if (is.logical(maxl))
maxl <- {
width <- width - (nchar(T0, "w") + 3L + 1L +
3L)
lenl <- cumsum(nchar(lev, "w") + nchar(colsep,
"w"))
if (n <= 1L || lenl[n] <= width)
n
else max(1L, which.max(lenl > width) - 1L)
}
drop <- n > maxl
cat(if (drop)
paste(format(n), ""), T0, paste(if (drop)
c(lev[1L:max(1, maxl - 1)], "...", if (maxl > 1) lev[n])
else lev, collapse = colsep), "\n", sep = "")
}
if (!isTRUE(val <- .valid.factor(x)))
warning(val)
invisible(x)
}
<bytecode: 0x7f9e346aef98>
<environment: namespace:base>
使用methods函数可以查看泛型函数所支持的方法。
[1] head.array* head.data.frame* head.default* head.ftable*
[5] head.function* head.matrix
see '?methods' for accessing help and source code
c,+,-和<等,其工作方式也类似于泛型函数,只是它们不会调用UseMethod函数,而会调用.primitive函数。简直就是魔法。
可以自己写一个S3风格的函数,对于特定对象,做特定的处理。
可以利用R的S3系统为对象新建一个稳健的类(class)。R会以一致且合理的方式对待同属一类的对象。要想创建一个类,应该执行以下操作:
查看某个类的方法:
[1] [ [[ [[<- [<- all.equal
[6] as.character as.data.frame as.Date as.list as.logical
[11] as.POSIXlt as.vector c coerce droplevels
[16] format initialize is.na<- length<- levels<-
[21] Math Ops plot print relevel
[26] relist rep show slotsFromS3 summary
[31] Summary xtfrm
see '?methods' for accessing help and source code
S4和R5是R另外两个可以用来创建类属性行为的系统。使用难度更大,参考 Hadley Wickham 的《Advanced R Programming》。
expand.grid可以方便快捷地写出n个向量元素的所有组合。
语法
repeat会一直重复运行某段代码,直到你中止循环(按Esc),或遇到了break命令。
replicate函数可以重复运行某段代码多少次。
快速的R代码经常用到的三大法宝:逻辑测试,取子集和按元素方式执行。
看看下面这个例子,一个是用for循环的非向量化代码,一个是向量化代码。
# for循环
abs_loop <- function(vec) {
for (i in 1:length(vec)) {
if (vec[i] < 0) {
vec[i] <- -vec[i]
}
}
vec
}
# 向量化代码
abs_set <- function(vec) {
negs <- vec < 0
vec[negs] <- -1 * vec[negs]
vec
}
# 生成一个很长的测试向量
long <- rep(c(-1, 1), 5000000)
# 统计代码运行的实际
system.time(abs_loop(long))
user system elapsed
0.469 0.022 0.494
user system elapsed
0.177 0.038 0.217
向量化代码比for循环快5倍,书中是快30倍,应该是新版的R把for循环优化了。
做两件事可以改善for循环的效率:
system.time({
# 这句代码是关键,给output预先分配了足够的容量
output <- rep(NA, 1000000)
for (i in 1:1000000) {
output[i] <- i + 1
}
})
user system elapsed
0.043 0.002 0.045
user system elapsed
0.230 0.061 0.291
为什么第二段慢呢?在第二段代码中,循环每前进一步,都要为output在内存中寻找位置,以存放新的变大的output。第一段代码没有这个问题,直接放。
到这里这本书就结束了,如果你觉得不错,请在下面点赞👍或评论支持吧😄