R 语言完整入门指南:从数据探索到统计建模的实战教程
R 语言完整入门指南:从数据探索到统计建模的实战教程
本文是基于 learnxinyminutes-docs 仓库中 R 语言教程 整理的中文完整指南,面向完全零基础或刚接触统计编程的读者。你将跟随教程脉络,先在不写程序的情况下用 data()、hist()、stem() 等函数探索 R 预装数据集,再逐步掌握数据类型、向量化运算、数据框与 data.table、apply() 函数族、read.csv() 数据读写、lm()/glm() 统计建模,以及从基础绘图到 ggplot2 的图形体系。读完本文,你将具备一套可以立即上手做数据探索与统计分析的 R 实战技能。
R 是什么:统计计算语言与科学计算生态
R 是一门统计计算语言(statistical computing language),拥有大量用于数据上传与清洗、统计过程分析和绘图的扩展包(libraries)。它天然围绕"向量"与"数据框"设计,使得对整行、整列数据的批量操作远快于逐元素循环。R 的另一个特点是文档生态完善:你可以在 LaTeX 文档中直接运行 R 命令(即 Sweave / knitr 的文学化编程思路),把统计代码与排版报告融为一体。
本仓库中该教程的配套数据文件 pets.csv 也随仓库一起提供,供"数据加载"章节的本地读写练习使用。
获取 R 与运行环境
- 从 R 项目官网获取 R 本体与 R GUI(Windows/macOS/Linux 均有对应安装包);
- RStudio 是另一个流行的集成开发界面(GUI),提供脚本编辑、变量查看、绘图面板等一站式体验。
运行代码的交互方式:
- 在 Windows 上可用
CTRL-ENTER执行当前行; - 在 Mac 上对应为
COMMAND-ENTER; - 也可以把代码保存为脚本文件(本教程对应的文件名是
learnr.r),整文件或选区执行。
注释语法是入门第一课:R 中注释以井号(#)开头。R 不支持多行注释,但可以像下面这样堆叠多行注释:
# Comments start with hash signs, also known as number symbols (#).
# You can't make multi-line comments,
# but you can stack multiple comments like so.
第一站:不懂编程也能做的数据探索
R 内置了大量"开箱即用"的数据集,这一节完全不需要编程知识,只要会敲函数就能体验统计探索的乐趣。先浏览并加载一个数据集:
data() # browse pre-loaded data sets
data(rivers) # get this one: "Lengths of Major North American Rivers"
ls() # notice that "rivers" now appears in the workspace
head(rivers) # peek at the data set
# 735 320 325 392 524 450
rivers 记录了北美主要河流的长度(英里)。ls() 显示工作区对象,head() 查看数据开头。接下来用几个统计函数快速"体检":
length(rivers) # how many rivers were measured?
# 141
summary(rivers) # what are some summary statistics?
# Min. 1st Qu. Median Mean 3rd Qu. Max.
# 135.0 310.0 425.0 591.2 680.0 3710.0
length() 给出样本量(141 条河流),summary() 一次给出最小值、四分位数、中位数、均值与最大值——这是每个数据集上手后最该先跑的几行命令。
茎叶图(stem-and-leaf plot)是一种直方图式的可视化,能直接展示数据分布形态:
stem(rivers)
# The decimal point is 2 digit(s) to the right of the |
#
# 0 | 4
# 2 | 011223334555566667778888899900001111223333344455555666688888999
# 4 | 111222333445566779001233344567
# 6 | 000112233578012234468
# 8 | 045790018
# 10 | 04507
# 12 | 1471
# 14 | 56
# 16 | 7
# 18 | 9
# 20 |
# 22 | 25
# 24 | 3
# 26 |
# 28 |
# 30 |
# 32 |
# 34 |
# 36 | 1
对数据取对数后再画一次茎叶图,可以检验数据的分布形态——rivers 既不是正态分布,也不是对数正态分布:
stem(log(rivers)) # Notice that the data are neither normal nor log-normal!
# Take that, Bell curve fundamentalists.
# The decimal point is 1 digit(s) to the left of the |
#
# 48 | 1
# 50 |
# 52 | 15578
# 54 | 44571222466689
# 56 | 023334677000124455789
# 58 | 00122366666999933445777
# 60 | 122445567800133459
# 62 | 112666799035
# 64 | 00011334581257889
# 66 | 003683579
# 68 | 0019156
# 70 | 079357
# 72 | 89
# 74 | 84
# 76 | 56
# 78 | 4
# 80 |
# 82 | 2
画直方图(histogram)可以更直观地观察分布。breaks 控制分箱数量,col 与 border 控制填充色与边框色,这些参数稍后绘图章节还会反复用到:
hist(rivers, col = "#333333", border = "white", breaks = 25)
hist(log(rivers), col = "#333333", border = "white", breaks = 25)
# play around with these parameters, you'll do more plotting later
再看另一个预装数据集 discoveries(每年重要科学发现的数量,R 中这类预装数据集非常多):
data(discoveries)
plot(discoveries, col = "#333333", lwd = 3, xlab = "Year",
main="Number of important discoveries per year")
plot(discoveries, col = "#333333", lwd = 3, type = "h", xlab = "Year",
main="Number of important discoveries per year")
plot() 画折线图,type = "h" 切换为垂直柱线图(histogram 式);lwd 控制线宽,xlab/main 分别设置横轴与标题。与其保持默认的按年份排列,不如排序后看看"典型年份"长什么样:
sort(discoveries)
# [1] 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2
# [26] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3
# [51] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4
# [76] 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 8 9 10 12
stem(discoveries, scale = 2) 用更大的刻度画出其茎叶图,并配合 max() 与 summary() 了解极值与分布概况:
stem(discoveries, scale = 2)
#
# The decimal point is at the |
#
# 0 | 000000000
# 1 | 000000000000
# 2 | 00000000000000000000000000
# 3 | 00000000000000000000
# 4 | 000000000000
# 5 | 0000000
# 6 | 000000
# 7 | 0000
# 8 | 0
# 9 | 0
# 10 | 0
# 11 |
# 12 | 0
max(discoveries)
# 12
summary(discoveries)
# Min. 1st Qu. Median Mean 3rd Qu. Max.
# 0.0 2.0 3.0 3.1 4.0 12.0
随机数体验:掷几次骰子、从标准正态分布抽样。注意随机结果依赖随机种子,想复现别人的结果必须设置相同的种子:
# Roll a die a few times
round(runif(7, min = .5, max = 6.5))
# 1 4 6 1 4 6 4
# Your numbers will differ from mine unless we set the same random.seed(31337)
# Draw from a standard Gaussian 9 times
rnorm(9)
# [1] 0.07528471 1.03499859 1.34809556 -0.82356087 0.61638975 -1.88757271
# [7] -0.59975593 0.57629164 1.08455362
runif(n, min, max) 从均匀分布抽取 n 个值,round() 取整模拟骰子点数;rnorm(n) 从标准正态分布抽取 n 个值。
数据类型与基本运算
从这一节开始进入"编程向"内容。R 中最重要的基础数据类型有:整数(integer)、数值(numeric,即双精度浮点数)、字符(character)、逻辑(logical)和因子(factor)。除此之外还有日期、时间序列等更多类型,但这五个是起步的底线。
整数(Integer)
长整型整数以 L 后缀书写。R 中每一个值本质上都是长度为 1 的向量:
5L # 5
class(5L) # "integer"
# (Try ?class for more information on the class() function.)
# In R, every single value, like 5L, is considered a vector of length 1
length(5L) # 1
长度大于 1 的整数向量用 c() 组合函数创建:
c(4L, 5L, 8L, 3L) # 4 5 8 3
length(c(4L, 5L, 8L, 3L)) # 4
class(c(4L, 5L, 8L, 3L)) # "integer"
class() 返回对象类型,?class 可查看帮助文档——R 中任何函数前加 ? 都能查帮助,这是自学的核心习惯。
数值(Numeric,双精度浮点)
不带 L 的普通数字就是 numeric:
5 # 5
class(5) # "numeric"
数值向量同样用 c() 创建,支持科学计数法,还支持无穷大/无穷小:
c(3, 3, 3, 2, 2, 1) # 3 3 3 2 2 1
5e4 # 50000
6.02e23 # Avogadro's number
1.6e-35 # Planck length
class(Inf) # "numeric"
class(-Inf) # "numeric"
Inf(正无穷)的典型用途:integrate(dnorm, 3, Inf) 计算标准正态分布从 3 到无穷的积分,免去查 Z 分数表(Z-score tables)。
基本算术
10L + 66L # 76 # integer plus integer gives integer
53.2 - 4 # 49.2 # numeric minus numeric gives numeric
2.0 * 2L # 4 # numeric times integer gives numeric
3L / 4 # 0.75 # integer over numeric gives numeric
3 %% 2 # 1 # the remainder of two numerics is another numeric
- 整数加整数得整数;混入数值后结果升级为数值;
%%是取余运算符;- 非法算术产生"非数"
NaN(not-a-number),其类型仍是numeric:
0 / 0 # NaN
class(NaN) # "numeric"
R 的算术天然向量化:只要较长向量的长度是较短向量的整数倍,就可以直接对两个向量做运算(较短向量会被循环补齐 recycling):
c(1, 2, 3) + c(1, 2, 3) # 2 4 6
# Since a single number is a vector of length one, scalars are applied
# elementwise to vectors
(4 * c(1, 2, 3) - 2) / 2 # 1 3 5
标量本质上是长度 1 的向量,因此"标量×向量"就是逐元素(elementwise)运算。对不同长度向量运算要格外小心——虽然可行,但可读性差:
c(1, 2, 3, 1, 2, 3) * c(1, 2) # 1 4 3 2 2 6
# Matching lengths is better practice and easier to read most times
c(1, 2, 3, 1, 2, 3) * c(1, 2, 1, 2, 1, 2) # 1 4 3 2 2 6
字符(Character)
R 中"字符串"与"字符"没有区别,统一是 character 类型:
"Horatio" # "Horatio"
class("Horatio") # "character"
class("H") # "character"
# Those were both character vectors of length 1
c("alef", "bet", "gimmel", "dalet", "he")
# => "alef" "bet" "gimmel" "dalet" "he"
length(c("Call","me","Ishmael")) # 3
字符向量支持正则相关操作:substr() 按位置截取子串,gsub() 做全局替换:
substr("Fortuna multis dat nimis, nulli satis.", 9, 15) # "multis "
gsub('u', 'ø', "Fortuna multis dat nimis, nulli satis.") # "Fortøna møltis dat nimis, nølli satis."
R 还内置了一批常用字符向量,如小写字母表 letters 和月份缩写 month.abb:
letters
# =>
# [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
# [20] "t" "u" "v" "w" "x" "y" "z"
month.abb # "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"
逻辑(Logical)
R 中的"逻辑"即布尔值:
class(TRUE) # "logical"
class(FALSE) # "logical"
比较运算行为符合直觉:
TRUE == TRUE # TRUE
TRUE == FALSE # FALSE
FALSE != FALSE # FALSE
FALSE != TRUE # TRUE
缺失数据 NA 本身也是逻辑型:
class(NA) # "logical"
逻辑运算用 |(或)和 &(与),作用于向量时返回逐元素的逻辑结果:
# OR
TRUE | FALSE # TRUE
# AND
TRUE & FALSE # FALSE
# Applying | and & to vectors returns elementwise logic operations
c(TRUE, FALSE, FALSE) | c(FALSE, TRUE, FALSE) # TRUE TRUE FALSE
c(TRUE, FALSE, TRUE) & c(FALSE, TRUE, TRUE) # FALSE FALSE TRUE
isTRUE() 判断是否为 TRUE;向量与单个值比较会逐元素广播:
isTRUE(TRUE) # TRUE
c("Z", "o", "r", "r", "o") == "Zorro" # FALSE FALSE FALSE FALSE FALSE
c("Z", "o", "r", "r", "o") == "Z" # TRUE FALSE FALSE FALSE FALSE
因子(Factor)
因子类用于分类数据(categorical data),可分有序(如年级)与无序(如颜色)两类。因子的"水平"(levels)就是该分类变量可取的值:
factor(c("blue", "blue", "green", NA, "blue"))
# blue blue green <NA> blue
# Levels: blue green
# The "levels" are the values the categorical data can take
# Note that missing data does not enter the levels
levels(factor(c("green", "green", "blue", NA, "blue"))) # "blue" "green"
注意缺失值 NA 不会进入 levels。长度为 1 的因子向量,其 levels 长度也为 1:
length(factor("green")) # 1
length(levels(factor("green"))) # 1
因子在数据框中极为常见(数据框后面章节会详细讲)。加载预装数据集 infert("Infertility after Spontaneous and Induced Abortion")即可看到教育程度被编码为因子:
data(infert) # "Infertility after Spontaneous and Induced Abortion"
levels(infert$education) # "0-5yrs" "6-11yrs" "12+ yrs"
NULL
NULL 用来"清空"一个向量——它是 R 中比较特殊的存在:
class(NULL) # NULL
parakeet = c("beak", "feathers", "wings", "eyes")
parakeet # "beak" "feathers" "wings" "eyes"
parakeet <- NULL
parakeet # NULL
类型强制转换(Type Coercion)
强制转换是把一个值强行变成另一种类型,as.character()、as.logical() 等函数是常用入口:
as.character(c(6, 8)) # "6" "8"
as.logical(c(1,0,1,1)) # TRUE FALSE TRUE TRUE
把不同类型元素放进同一个向量会触发"怪异"的自动转换——数值与逻辑混合时逻辑被转成 0/1,字符一出现则全部升格为字符:
c(TRUE, 4) # 1 4
c("dog", TRUE, 4) # "dog" "TRUE" "4"
无法转换的值变成 NA 并给出警告:
as.numeric("Bilbo")
# =>
# [1] NA
# Warning message:
# NAs introduced by coercion
以上只是最基础的数据类型;R 还有日期、时间序列等更多类型,在后续学习中会逐步遇到。
变量、循环与分支
变量就像一个盒子,把值存进去供以后使用。有了变量,才能写循环、函数和 if/else 分支。
变量赋值
R 有多种赋值方式,传统上更推荐 <-:
x = 5 # this is possible
y <- "1" # this is preferred traditionally
TRUE -> z # this works but is weird
=、<-、-> 三种写法都合法,社区习惯与偏好细节可查阅相关资料。
循环
for 循环遍历序列:
for (i in 1:4) {
print(i)
}
while 循环按条件迭代,cat() 不换行地拼接输出,sep 控制分隔符:
a <- 10
while (a > 4) {
cat(a, "...", sep = "")
a <- a - 1
}
性能提示:R 中的 for 与 while 循环运行较慢,应优先使用对整个向量(整行、整列)的操作,或使用后文要讲的 apply() 家族函数。
if/else
if/else 语法与其他语言基本一致:
if (4 > 3) {
print("4 is greater than 3")
} else {
print("4 is not greater than 3")
}
# =>
# [1] "4 is greater than 3"
函数
函数用 function(x) 定义,可以像任何内置函数一样被调用。下面这个 jiggle() 给输入加上一点受控的随机噪声:
jiggle <- function(x) {
x = x + rnorm(1, sd=.1) # add in a bit of (controlled) noise
return(x)
}
# Called like any other R function:
jiggle(5) # 5±ε. After set.seed(2716057), jiggle(5)==5.005043
设置相同的随机种子(set.seed(2716057))后,jiggle(5) 会稳定返回 5.005043,这也是随机试验可复现的关键。
数据结构:向量、矩阵、数据框与数组
一维:向量(Vector)
向量是最基础的结构,前面已经多次见到。索引(subsetting)用方括号,注意 R 从 1 开始计数:
vec <- c(8, 9, 10, 11)
vec # 8 9 10 11
vec[1] # 8
letters[18] # "r"
LETTERS[13] # "M"
month.name[9] # "September"
c(6, 8, 7, 5, 3, 0, 9)[3] # 7
which() 查找满足条件的元素下标,head()/tail() 取首尾若干元素,any() 判断是否存在满足条件的元素:
which(vec %% 2 == 0) # 1 3
head(vec, 1) # 8
tail(vec, 2) # 10 11
any(vec == 10) # TRUE
越界索引返回 NA;length() 求长度:
vec[6] # NA
length(vec) # 4
向量(或其子集)可以直接参与运算,R 内置大量向量汇总函数:
vec * 4 # 32 36 40 44
vec[2:3] * 5 # 45 50
any(vec[2:3] == 8) # FALSE
mean(vec) # 9.5
var(vec) # 1.666667
sd(vec) # 1.290994
max(vec) # 11
min(vec) # 8
sum(vec) # 38
mean(均值)、var(方差)、sd(标准差)、max/min(极值)、sum(求和)是统计入门第一梯队函数。生成序列的便捷写法:: 冒号运算符与 seq() 函数:
5:15 # 5 6 7 8 9 10 11 12 13 14 15
seq(from = 0, to = 31337, by = 1337)
# =>
# [1] 0 1337 2674 4011 5348 6685 8022 9359 10696 12033 13370 14707
# [13] 16044 17381 18718 20055 21392 22729 24066 25403 26740 28077 29414 30751
二维(全部同类型):矩阵(Matrix)
矩阵要求所有元素同类型。matrix() 的第一个参数是数据向量,nrow/ncol 指定行列数,数据按列填充:
mat <- matrix(nrow = 3, ncol = 2, c(1, 2, 3, 4, 5, 6))
mat
# =>
# [,1] [,2]
# [1,] 1 4
# [2,] 2 5
# [3,] 3 6
# Unlike a vector, the class of a matrix is "matrix", no matter what's in it
class(mat) # "matrix" "array"
矩阵按 [行, 列] 双下标索引,留空表示整行/整列:
mat[1, ] # 1 4
3 * mat[, 1] # 3 6 9
mat[3, 2] # 6
转置用 t(),矩阵乘法用 %*%:
t(mat)
# =>
# [,1] [,2] [,3]
# [1,] 1 2 3
# [2,] 4 5 6
mat %*% t(mat)
# =>
# [,1] [,2] [,3]
# [1,] 17 22 27
# [2,] 22 29 36
# [3,] 27 36 45
cbind() 按列拼接向量成矩阵,rbind() 按行拼接。注意一个关键陷阱:矩阵必须同类型,混入字符后一切都会被强制转为字符:
mat2 <- cbind(1:4, c("dog", "cat", "bird", "dog"))
mat2
# =>
# [,1] [,2]
# [1,] "1" "dog"
# [2,] "2" "cat"
# [3,] "3" "bird"
# [4,] "4" "dog"
class(mat2) # matrix
# Again, note what happened!
# Because matrices must contain entries all of the same class,
# everything got converted to the character class
c(class(mat2[, 1]), class(mat2[, 2]))
mat3 <- rbind(c(1, 2, 4, 5), c(6, 7, 0, 4))
mat3
# =>
# [,1] [,2] [,3] [,4]
# [1,] 1 2 4 5
# [2,] 6 7 0 4
# Ah, everything of the same class. No coercions. Much better.
二维(不同类型):数据框(data.frame)
当各列类型不同时,用数据框(data frame)。这个结构对统计编程如此重要,以至于 Python 的 pandas 包也加入了它的对应实现。用 data.frame() 创建,names() 设置列名:
students <- data.frame(c("Cedric", "Fred", "George", "Cho", "Draco", "Ginny"),
c( 3, 2, 2, 1, 0, -1),
c( "H", "G", "G", "R", "S", "G"))
names(students) <- c("name", "year", "house") # name the columns
class(students) # "data.frame"
students
# =>
# name year house
# 1 Cedric 3 H
# 2 Fred 2 G
# 3 George 2 G
# 4 Cho 1 R
# 5 Draco 0 S
# 6 Ginny -1 G
数据框的各列可以类型各异——数值列、因子列并存:
class(students$year) # "numeric"
class(students[,3]) # "factor"
维度信息用 nrow()/ncol()/dim():
nrow(students) # 6
ncol(students) # 3
dim(students) # 6 3
版本提示:data.frame() 过去默认把字符向量转换为因子,该默认行为在 R 4.0.0 中已改变。若你的 R 版本较旧,创建时可用 stringsAsFactors = FALSE 关闭这一行为(?data.frame 查看详情)。
数据框有多种等价的取列方式,各有细微差别:
students$year # 3 2 2 1 0 -1
students[, 2] # 3 2 2 1 0 -1
students[, "year"] # 3 2 2 1 0 -1
data.table 速览:大数据场景的增强版数据框
处理海量数据(panel data)、需要合并多个数据集时,data.table 包是很好的选择。它是数据框结构的增强版。先安装并加载:
install.packages("data.table") # download the package from CRAN
require(data.table) # load it
把数据框转成 data.table(注意打印输出略有不同,行号带冒号):
students <- as.data.table(students)
students # note the slightly different print-out
# =>
# name year house
# 1: Cedric 3 H
# 2: Fred 2 G
# 3: George 2 G
# 4: Cho 1 R
# 5: Draco 0 S
# 6: Ginny -1 G
data.table 的核心语法是 DT[i, j, by]:i 中写行条件,直接按条件筛行:
students[name == "Ginny"] # get rows with name == "Ginny"
# =>
# name year house
# 1: Ginny -1 G
students[year == 2] # get rows with year == 2
# =>
# name year house
# 1: Fred 2 G
# 2: George 2 G
合并两个数据集非常方便。先造一个"学院创始人"表,再用 setkey() 设定连接键、以 founders[students] 方式按键合并:
founders <- data.table(house = c("G" , "H" , "R" , "S"),
founder = c("Godric", "Helga", "Rowena", "Salazar"))
founders
# =>
# house founder
# 1: G Godric
# 2: H Helga
# 3: R Rowena
# 4: S Salazar
setkey(students, house)
setkey(founders, house)
students <- founders[students] # merge the two data sets by matching "house"
setnames(students, c("house", "houseFounderName", "studentName", "year"))
students[, order(c("name", "year", "house", "houseFounderName")), with = F]
# =>
# studentName year house houseFounderName
# 1: Fred 2 G Godric
# 2: George 2 G Godric
# 3: Ginny -1 G Godric
# 4: Cedric 3 H Helga
# 5: Cho 1 R Rowena
# 6: Draco 0 S Salazar
setkey() 设定键,setnames() 批量改名,with = F 允许在 j 位置直接引用列名做列重排。data.table 的分组汇总同样简洁——by 指定分组变量,j 写聚合表达式:
students[, sum(year), by = house]
# =>
# house V1
# 1: G 3
# 2: H 3
# 3: R 1
# 4: S 0
删除列:赋 NULL 值即可(data.frame 与 data.table 通用):
students$houseFounderName <- NULL
students
# =>
# studentName year house
# 1: Fred 2 G
# 2: George 2 G
# 3: Ginny -1 G
# 4: Cedric 3 H
# 5: Cho 1 R
# 6: Draco 0 S
删除行:data.table 用条件子集,data.frame 用逻辑索引:
# Using data.table:
students[studentName != "Draco"]
# =>
# house studentName year
# 1: G Fred 2
# 2: G George 2
# 3: G Ginny -1
# 4: H Cedric 3
# 5: R Cho 1
# Using data.frame:
students <- as.data.frame(students)
students[students$house != "G", ]
# =>
# house houseFounderName studentName year
# 4 H Helga Cedric 3
# 5 R Rowena Cho 1
# 6 S Salazar Draco 0
多维(全部同类型):数组(Array)
数组创建 n 维表格,所有元素必须同类型。二维数组类似矩阵,三维数组则是"多个矩阵叠起来":
array(c(c(1, 2, 4, 5), c(8, 9, 3, 6)), dim = c(2, 4))
# =>
# [,1] [,2] [,3] [,4]
# [1,] 1 4 8 3
# [2,] 2 5 9 6
array(c(c(c(2, 300, 4), c(8, 9, 0)), c(c(5, 60, 0), c(66, 7, 847))), dim = c(3, 2, 2))
# =>
# , , 1
#
# [,1] [,2]
# [1,] 2 8
# [2,] 300 9
# [3,] 4 0
#
# , , 2
#
# [,1] [,2]
# [1,] 5 66
# [2,] 60 7
# [3,] 0 847
dim 参数依次指定各维度大小,数据按列填充。
列表(List):多维、可"参差"、可异构
列表由向量组成,各成员可以长度不同、类型不同。用 list() 创建,支持命名成员:
list1 <- list(time = 1:40)
list1$price = c(rnorm(40, .5*list1$time, 4)) # random
取列表成员有三种常见方式:$ 符号、双括号 [[]] 按名、双括号按位置:
list1$time # one way
list1[["time"]] # another way
list1[[1]] # yet another way
# =>
# [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 27 28 29 30 31 32 33
# [34] 34 35 36 37 38 39 40
列表成员取到后可以像普通向量一样继续索引:
list1$price[4]
使用提示:列表不是 R 中最高效的数据结构,除非有充分理由,否则优先使用 data.frame;不过列表常被线性回归等函数的返回值使用(后面会看到 lm() 的返回对象)。
apply() 函数族与 plyr
还记得之前的矩阵 mat 吗?apply(X, MARGIN, FUN) 把函数 FUN 作用到矩阵 X 的每一行(MARGIN = 1)或每一列(MARGIN = 2)上——这比手写 for/while 循环快得多:
mat
# =>
# [,1] [,2]
# [1,] 1 4
# [2,] 2 5
# [3,] 3 6
apply(mat, MAR = 2, jiggle)
# =>
# [,1] [,2]
# [1,] 3 15
# [2,] 7 19
# [3,] 11 23
# Other functions: ?lapply, ?sapply
上例对每一列应用了前面定义的 jiggle() 加噪函数。?lapply、?sapply 等函数可查帮助——lapply 返回列表、sapply 尽量简化返回向量/矩阵。不必被它们吓到,所有人都承认这套函数族确实有点绕。
plyr 包旨在替代(并在很多方面改进)*apply() 函数族:
install.packages("plyr")
require(plyr)
?plyr
数据加载与保存
把外部数据读进 R 是统计工作的日常。教程中的示例文件 pets.csv 就存放在本仓库根目录(pets.csv),内容是几只宠物的 name, age, weight, species 记录,无论文件在网络上还是本地电脑上,read.csv() 的读法一致:
require(RCurl)
pets <- read.csv(textConnection(getURL("https://learnxinyminutes.com/pets.csv")))
pets
head(pets, 2) # first two rows
tail(pets, 1) # last row
读入后同样用 head()/tail() 快速浏览首尾。若文件就在本地工作目录,更常见的写法是直接 read.csv("pets.csv"),无需 RCurl 包。
保存数据框或矩阵为 CSV 文件:
write.csv(pets, "pets2.csv") # to make a new .csv file
# set working directory with setwd(), look it up with getwd()
工作目录用 setwd() 设置、getwd() 查询。更多细节可查阅 ?read.csv 与 ?write.csv。
统计分析:线性回归与广义线性模型
线性回归 lm()
基于前面构造的 list1(时间 time 与带噪声的价格 price),用 lm() 拟合 price ~ time 的线性回归:
linearModel <- lm(price ~ time, data = list1)
linearModel # outputs result of regression
# =>
# Call:
# lm(formula = price ~ time, data = list1)
#
# Coefficients:
# (Intercept) time
# 0.1453 0.4943
summary() 给出更完整的回归报告:残差分布、系数估计与显著性、拟合优度与 F 检验:
summary(linearModel) # more verbose output from the regression
# =>
# Call:
# lm(formula = price ~ time, data = list1)
#
# Residuals:
# Min 1Q Median 3Q Max
# -8.3134 -3.0131 -0.3606 2.8016 10.3992
#
# Coefficients:
# Estimate Std. Error t value Pr(>|t|)
# (Intercept) 0.14527 1.50084 0.097 0.923
# time 0.49435 0.06379 7.749 2.44e-09 ***
# ---
# Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#
# Residual standard error: 4.657 on 38 degrees of freedom
# Multiple R-squared: 0.6124, Adjusted R-squared: 0.6022
# F-statistic: 60.05 on 1 and 38 DF, p-value: 2.44e-09
time 的系数 p 值为 2.44e-09(带 *** 标记),说明时间对价格有极显著影响。提取结果的方法:
coef(linearModel) # extract estimated parameters
# =>
# (Intercept) time
# 0.1452662 0.4943490
summary(linearModel)$coefficients # another way to extract results
# =>
# Estimate Std. Error t value Pr(>|t|)
# (Intercept) 0.1452662 1.50084246 0.09678975 9.234021e-01
# time 0.4943490 0.06379348 7.74920901 2.440008e-09
summary(linearModel)$coefficients[, 4] # the p-values
# =>
# (Intercept) time
# 9.234021e-01 2.440008e-09
coef() 直接取系数向量;summary() 返回对象可用 $coefficients 继续取系数矩阵,再取第 4 列即得到 p 值列。
广义线性模型:逻辑回归 glm()
先造一个随机二元响应变量(伯努利试验):
set.seed(1)
list1$success = rbinom(length(list1$time), 1, .5) # random binary
glm() 配合 family = binomial(link = "logit") 拟合逻辑回归:
glModel <- glm(success ~ time, data = list1, family=binomial(link="logit"))
glModel # outputs result of logistic regression
# =>
# Call: glm(formula = success ~ time,
# family = binomial(link = "logit"), data = list1)
#
# Coefficients:
# (Intercept) time
# 0.17018 -0.01321
#
# Degrees of Freedom: 39 Total (i.e. Null); 38 Residual
# Null Deviance: 55.35
# Residual Deviance: 55.12 AIC: 59.12
summary() 给出完整报告,包括偏差残差(Deviance Residuals)、系数 z 检验与 AIC 信息准则:
summary(glModel) # more verbose output from the regression
# =>
# Call:
# glm(
# formula = success ~ time,
# family = binomial(link = "logit"),
# data = list1)
#
# Deviance Residuals:
# Min 1Q Median 3Q Max
# -1.245 -1.118 -1.035 1.202 1.327
#
# Coefficients:
# Estimate Std. Error z value Pr(>|z|)
# (Intercept) 0.17018 0.64621 0.263 0.792
# time -0.01321 0.02757 -0.479 0.632
#
# (Dispersion parameter for binomial family taken to be 1)
#
# Null deviance: 55.352 on 39 degrees of freedom
# Residual deviance: 55.121 on 38 degrees of freedom
# AIC: 59.121
#
# Number of Fisher Scoring iterations: 3
由于 success 是纯随机生成的,time 的系数不显著(p = 0.632),符合预期——这也演示了如何读懂"不显著"的结果。
绘图:从内置函数到 ggplot2
内置绘图函数
散点图:plot(x, y, main = ...),然后用 abline() 把回归线叠加到已有图上:
plot(list1$time, list1$price, main = "fake data")
# Plot regression line on existing plot
abline(linearModel, col = "red")
plot(linearModel) 直接对回归模型对象绘图,得到一组丰富的诊断图(残差、Q-Q 图等):
plot(linearModel)
直方图与柱状图:
hist(rpois(n = 10000, lambda = 5), col = "thistle")
barplot(c(1, 4, 5, 1, 2), names.arg = c("red", "blue", "purple", "green", "yellow"))
rpois() 从泊松分布抽样,names.arg 为柱状图各柱命名。
ggplot2:更美观的图形语法
以上还不是 R 最漂亮的图形。ggplot2 包提供更强大、更美观的图形体系:
install.packages("ggplot2")
require(ggplot2)
?ggplot2
ggplot2 的核心理念是"图形语法":先用 ggplot(data, aes(...)) 声明数据与美学映射,再用 + 叠加几何图层:
pp <- ggplot(students, aes(x = house))
pp + geom_bar()
把 list1 转成 data.table 后画散点:
ll <- as.data.table(list1)
pp <- ggplot(ll, aes(x = time, price))
pp + geom_point()
geom_bar() 画柱状图、geom_point() 画散点图。ggplot2 拥有非常优秀的官方文档(docs.ggplot2.org),是 R 绘图的进阶必学内容。
配套资源与深入学习路径
- 教程正文完整版见仓库根目录的 r.md,其 front matter 中声明的文件名是
learnr.r,表明全部示例可直接整理为同名.r脚本运行; - 数据加载章节引用的示例数据就存放在仓库根目录的 pets.csv(3 条宠物记录:fluffy/vesuvius/rex),可直接用
read.csv("pets.csv")做本地练习; - 该教程在仓库中还有多语言翻译版本,包括 be/r.md、es/r.md、fr/r.md、ja/r.md、pt-br/r.md、ru/r.md,适合对照学习与查证术语;
- 仓库
lint/目录下有 frontmatter.py 等脚本,用于校验文档元数据格式,与本教程内容无关,仅作仓库工程化背景参考。
掌握本文内容后,你已具备 R 的核心语言能力:向量化思维、五种基础数据类型、数据框与 data.table 操作、apply 家族、数据读写,以及线性回归/逻辑回归与基础绘图。下一步建议深入官方文档(?function 帮助系统)、ggplot2 文档与 data.table 文档,把统计理论真正落到可复现的代码上。