R 语言完整入门指南:从数据探索到统计建模的实战教程

原创2026-10-08 21:31:201,686 阅读
文章标签:文档教程

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 文档,把统计理论真正落到可复现的代码上。

登录后查看全文
learnxinyminutes-docs