1.3.3 因子(factor)
数据(变量)可划分为:定量数据(数值型)、定性数据(分类型),定性数据又分为名义型(无好坏顺序之分,如性别)、有序型(有好坏顺序之分,如疗效)。
R 提供了因子这一数据结构(容器),专门用来存放名义型和有序型的分类变量。因子本质上是一个带有水平(level) 属性的整数向量,其中‘‘水平’’ 是指事前确定可能取值的有限集合。例如,性别有两个水平:男、女。
直接用字符向量也可以表示分类变量,但它只有字母顺序,不能规定想要的顺序,也不能表达有序分类变量。所以,有必要把字符型的分类变量转化为因子型,这更便于对其做后续描述汇总、可视化、建模等。
1. 创建与使用因子
函数factor() 用来创建因子,基本格式为:
factor(x, levels, labels, ordered, ...)
x:为创建因子的数据向量;
levels:指定因子的各水平值,默认为x 中不重复的所有值;
labels:设置各水平名称(前缀) ,与水平一一对应;
ordered:设置是否对因子水平排序,默认FALSE 为无序因子, TRUE 为有序因子;
该函数还包含参数exclude:指定有哪些水平是不需要的(设为NA) ;nmax 设定水平数的上限。
若不指定参数levels,则因子水平默认按字母顺序。
比如,现有6 个人的按等级的成绩数据,先以字符向量创建,并对其排序:
x = c(" 优", " 中", " 良", " 优", " 良", " 良") # 字符向量
x
## [1] "优" "中" "良" "优" "良" "良"
sort(x) # 排序是按字母顺序
## [1] "良" "良" "良" "优" "优" "中"
它的顺序只能是字母顺序,如果想规定顺序:中、良、优,正确的做法就是创建成因子,用levels指定想要的顺序:
x1 = factor(x, levels = c(" 中", " 良", " 优")) # 转化因子型
x1
## [1] 优中良优良良
## Levels: 中良优
as.numeric(x1) # x 的存储形式: 整数向量
## [1] 3 1 2 3 2 2
注意:不能直接将因子数据当字符型操作,需要用as.character() 转化。
转化为因子型后,数据向量显示出来(外在表现)与原来是一样的,但内在存储已经变了。因子型是以整数向量存储的,将各水平值按照规定的顺序分别对应到整数,将原向量的各值分别用相应的整数存储,输出和使用的时候再换回对应的水平值。整数是有顺序的,这样就相当于在不改变原数据的前提下规定了顺序,同时也节省了存储空间。
变成因子型后,无论是排序、统计频数、绘图等,都有了顺序:
sort(x1)
## [1] 中良良良优优
## Levels: 中良优
table(x1)
## x1
## 中良优
## 1 3 2
ggplot(tibble(x1), aes(x1)) +
geom_bar()
用levels() 函数可以访问或修改因子的水平值(将改变数据的外在表现):
levels(x1) = c("Excellent", "Good", "Fair") # 修改因子水平
x1
## [1] Fair Excellent Good Fair Good Good
## Levels: Excellent Good Fair
有时候你可能更希望让水平的顺序与其在数据集中首次出现的次序相匹配,设置参数levels =unique(x)。
转化为因子型的另一个好处是,可以’’ 识错’’:因子数据只认识出现在水平值中的值,否则将识别为NA。
很多人将因子固有的顺序与有序因子混淆,二者不是一回事:上述反复提到的顺序,可称为因子固有的顺序,正是有了它,才能方便地按想要的顺序排序、统计频数、绘图等;而无序因子与有序因子,是与变量本身的数据类型相对应的,名义型(无顺序好坏之分的分类变量)用无序因子存放,有序型(有顺序好坏之分的分类变量)用有序因子存放,该区分是用于不同类型的数据,建模时适用不同的模型。
示例的成绩数据是有好坏之分的,创建为有序因子:
x2 = factor(x, levels = c(" 中", " 良", " 优"), ordered = TRUE)
x2
## [1] 优中良优良良
## Levels: 中< 良< 优
如果对x2 做排序、统计频数、绘图,你会发现与无序因子时没有任何区别。它们的区别体现在对其建模时适用的模型不同。
2. 有用函数
函数table(),可以统计因子各水平的出现次数(频数) ,也可以统计向量中每个不同元素的出现次数,返回结果为命名向量。
table(x)
## x
## 良优中
## 3 2 1
函数cut(),用来做连续变量离散化:将数值向量切分为若干区间段,返回因子。基本格式为:
cut(x, breaks, labels, ...)
x:为要切分的数值向量;
breaks:切分的界限值构成的向量,或表示切分段数的整数。
该函数还包含参数right 设置区间段是否左开右闭, include.lowest 设置是否包含下界,ordered_result 设置是否对结果因子排序。
Age = c(23,15,36,47,65,53)
cut(Age, breaks = c(0,18,45,100),
labels = c("Young","Middle","Old"))
## [1] Middle Young Middle Old Old Old
## Levels: Young Middle Old
函数gl() 用来生成有规律的水平值组合因子。对于多因素试验设计,用该函数可以生成多个因素完全组合,基本格式为:
gl(n, k, length, labels, ordered, ...)
n:为因子水平个数;
k:为同一因子水平连续重复次数;
length:为总的元素个数,默认为n * k, 若不够则自动重复;
labels:设置因子水平值;
ordered:设置是否为有序,默认为FALSE。
tibble(
Sex = gl(2, 3, length = 12, labels = c(" 男"," 女")),
Class = gl(3, 2, length = 12, labels = c(" 甲"," 乙"," 丙")),
Score = gl(4, 3, length = 12, labels = c(" 优"," 良"," 中", " 及格")))
## # A tibble: 12 x 3
## Sex Class Score
## <fct> <fct> <fct>
## 1 男甲优
## 2 男甲优
## 3 男乙优
## 4 女乙良
## 5 女丙良
## 6 女丙良
## # ... with 6 more rows
3. forcats 包
tidyverse 系列中的forcats 包是专门为处理因子型数据而设计的,提供了一系列操作因子的方便函数:
as_factor(): 转化为因子,默认按水平值的出现顺序
fct_count():计算因子各水平频数、占比,可按频数排序
fct_c(): 合并多个因子的水平
改变因子水平的顺序:
fct_relevel(): 手动对水平值重新排序
fct_infreq(): 按高频优先排序
fct_inorder(): 按水平值出现的顺序
fct_rev(): 将顺序反转
fct_reorder(): 根据其他变量或函数结果排序(绘图时有用)
修改水平:
fct_recode(): 对水平值逐个重编码
fct_collapse(): 推倒手动合并部分水平
fct_lump_*(): 将多个频数小的水平合并为其他
fct_other(): 将保留之外或丢弃的水平合并为其他
增加或删除水平:
fct_drop(): 删除若干水平
fct_expand: 增加若干水平
fct_explicit_na(): 为NA 设置水平
读者需要明白这样一个基本逻辑:操作因子是操作一个向量,该向量更多的时候是以数据框的一列的形式存在的。所以,来演示一下更常用的操作数据框中的因子列。涉及数据操作和绘图的语法,在第2、3 章才会讲到。这里只需要知道大意和理解因子操作部分即可。
mpg 是汽车数据集,class 列是分类变量车型,先统计各种车型的频数,共有7 类;对该列做因子合并,合并为5 类+Other 类再统计频数,频数少的类合并为Other 类:
count(mpg, class)
## # A tibble: 7 x 2
## class n
## <chr> <int>
## 1 2seater 5
## 2 compact 47
## 3 midsize 41
## 4 minivan 11
## 5 pickup 33
## 6 subcompact 35
## # ... with 1 more row
mpg1 = mpg %>%
mutate(class = fct_lump(class, n = 5))
count(mpg1, class)
## # A tibble: 6 x 2
## class n
## <fct> <int>
## 1 compact 47
## 2 midsize 41
## 3 pickup 33
## 4 subcompact 35
## 5 suv 62
## 6 Other 16
若直接对class 各类绘制条形图,是按水平顺序,频数会参差不齐;改用根据频数多少排序,则条形图变的整齐易读:
p1 = ggplot(mpg, aes(class)) +
geom_bar()
p2 = ggplot(mpg, aes(fct_infreq(class))) +
geom_bar()
library(patchwork)
p1 | p2
1.4 数据结构III:字符串、日期时间
1.4.1 字符串
字符串是用双引号或单引号括起来的若干字符,建议用双引号,除非字符串中包含双引号。字符串构成的向量,简称为字符向量。
tidyverse 系列中的stringr 包提供了一系列接口一致的、简单易用的字符串操作函数,足以代替R 自带字符串函数。这些函数都是向量化的,即作用在字符向量上,对字符向量中的每个字符串做某种操作。
library(stringr)
1. 字符串的长度(包含字符个数)
str_length(c("a", "R for data science", NA))
## [1] 1 18 NA
str_pad(c("a", "ab", "abc"), 3) # 填充到长度为3
## [1] " a" " ab" "abc"
str_trunc("R for data science", 10) # 截断到长度为10
## [1] "R for d..."
str_trim(c("a ", "b ", "a b")) # 移除空格
## [1] "a" "b" "a b"
后三个函数都包含参数side=c("both", "left", "right") 设定操作的方向。
2. 字符串合并
str_c(..., sep = "", collapse = NULL)
sep:设置间隔符,默认为空字符;-collapse:指定间隔符,将字符向量推倒合并为一个字符串。
str_c("x", 1:3, sep = "") # 同paste0("x", 1:3), paste("x", 1:3, sep="")
## [1] "x1" "x2" "x3"
str_c("x", 1:3, collapse = "_")
## [1] "x1_x2_x3"
注: 1:3 自动向下兼容以适应字符串运算,效果同c("1","2","3")
将字符串重复n 次,基本格式为:
str_dup(string, times)
string:为要重复的字符向量;
times:为重复的次数。
str_dup(c("A","B"), 3)
## [1] "AAA" "BBB"
str_dup(c("A","B"), c(3,2))
## [1] "AAA" "BB"
3. 字符串拆分
str_split(string, pattern) # 返回列表
str_split_fixed(string, pattern, n) # 返回矩阵,n 控制返回的列数
string:为要拆分的字符串;
pattern:指定拆分的分隔符,可以是正则表达式。
x = "10,8,7"
str_split(x, ",")
## [[1]]
## [1] "10" "8" "7"
str_split_fixed(x, ",", n = 2)
## [,1] [,2]
## [1,] "10" "8,7"
4. 字符串格式化输出
只要在字符串内用{变量名} ,则函数str_glue() 和str_glue_data 就可以将字符串中的变量名替换成变量值,后者的参数.x 支持引入数据框、列表等。
str_glue("Pi = {pi}")
## Pi = 3.14159265358979
name = " 李明"
tele = "13912345678"
str_glue(" 姓名: {name}", " 电话号码: {tele}", .sep=";")
## 姓名: 李明;电话号码: 13912345678
5. 字符串排序
str_sort(x, decreasing, locale, ...)
str_order(x, decreasing, locale, ...)
默认decreasing = FALSE 表示升序,前者返回排好序的元素,后者返回排好序的索引;参数locale可设定语言,默认为"en" 英语。
x = c("banana", "apple", "pear")
str_sort(x)
## [1] "apple" "banana" "pear"
str_order(x)
## [1] 2 1 3
str_sort(c(" 香蕉", " 苹果", " 梨"), locale = "ch")
## [1] "梨" "苹果" "香蕉"
6. 检测匹配
str_detect(string, pattern, negate=FALSE) ——检测是否存在匹配
str_which(string, pattern, negate=FALSE) ——查找匹配的索引
str_count(string, pattern) ——计算匹配的次数
str_locate(string, pattern) —— 定位匹配的位置
str_starts(string, pattern) —— 检测是否以pattern 开头
str_ends(string, pattern) ——检测是否以pattern 结尾
string:为要检测的字符串;
pattern:为匹配的模式,可以是正则表达式;
negate:默认为FALSE 表示正常匹配,若为TRUE 则反匹配(找不匹配) 。
x
## [1] "banana" "apple" "pear"
str_detect(x, "p")
## [1] FALSE TRUE TRUE
str_which(x, "p")
## [1] 2 3
str_count(x, "p")
## [1] 0 2 1
str_locate(x, "a.") # 正则表达式, . 匹配任一字符
## start end
## [1,] 2 3
## [2,] 1 2
## [3,] 3 4
7. 提取字符串子集
根据指定的起始和终止位置提取子字符串,基本格式为:
str_sub(string, start = 1, end = -1)
str_sub(x, 1, 3)
## [1] "ban" "app" "pea"
str_sub(x, 1, 5) # 若长度不够, 则尽可能多地提取
## [1] "banan" "apple" "pear"
str_sub(x, -3, -1)
## [1] "ana" "ple" "ear"
提取字符向量中匹配的字符串,基本格式为:
str_subset(string, pattern, negate=FALSE)
若negate = TRUE, 则返回不匹配的字符串。
str_subset(x, "p")
## [1] "apple" "pear"
8. 提取匹配的内容
str_extract(string, pattern)
str_match(string, pattern)
str_extract() 只提取匹配的内容;
str_match() 提取匹配的内容以及各个分组捕获,返回矩阵,每行对应于字符向量中的一个字符串,每行的第一个元素是匹配内容,其他元素是各个分组捕获,没有匹配则为NA
x = c("1978-2000", "2011-2020-2099")
pat = "\\d{4}" # 正则表达式, 匹配4 位数字
str_extract(x, pat)
## [1] "1978" "2011"
str_match(x, pat)
## [,1]
## [1,] "1978"
## [2,] "2011"
9. 修改字符串
用新字符串赋值给str_sub() 提取的子字符串;
做字符替换,基本格式为:
str_replace(string, pattern, replacement)
pattern:要替换的子字符串或模式;
replacement:要替换为的新字符串。
x
## [1] "1978-2000" "2011-2020-2099"
str_replace(x, "-", "/")
## [1] "1978/2000" "2011/2020-2099"
10. 其他函数
转化大小写
str_to_upper(): 转换为大写;
str_to_lower(): 转换为小写;
str_to_title(): 转换标题格式(单词首字母大写)
str_to_lower("I love r language.")
## [1] "i love r language."
str_to_upper("I love r language.")
## [1] "I LOVE R LANGUAGE."
str_to_title("I love r language.")
## [1] "I Love R Language."
str_conv(string, encoding): 转化字符串的字符编码
str_view(string, pattern, match): 在Viewer 窗口输出(正则表达式) 模式匹配结果
word(string, start, end, sep = " "): 从英文句子中提取单词
str_wrap(string, width = 80, indent = 0, exdent = 0): 调整段落格式
关于stringr 包的注:以上查找匹配的各个函数,只是查找第一个匹配,要想查找所有匹配,各个函数都有另一版本:
加后缀_all,例如str_extract_all()
以上各个函数中的参数pattern 都支持用正则表达式(Regular Expression) 表示模式。关于正则表达式,请参阅下一节。
1.4.2 日期时间
日期时间值通常以字符串形式传入R 中,然后转化为以数值形式存储的日期时间变量。
R 的内部日期是以1970 年1 月1 日至今的天数来存储,内部时间则是以1970 年1 月1 日至今的秒数来存储。
tidyverse 系列的 lubridate 包提供了更加方便的函数,生成、转换、管理日期时间数据,足以代替R 自带的日期时间函数。
library(lubridate)
1. 识别日期时间
today()
## [1] "2021-11-07"
now()
## [1] "2021-11-07 11:56:10 CST"
as_datetime(today()) # 日期型转日期时间型
## [1] "2021-11-07 UTC"
as_date(now()) # 日期时间型转日期型
## [1] "2021-11-07"
无论年月日/时分秒按什么顺序及以什么间隔符分隔,总能正确地识别成日期时间值:
ymd("2020/03~01")
## [1] "2020-03-01"
myd("03202001")
## [1] "2020-03-01"
dmy("03012020")
## [1] "2020-01-03"
ymd_hm("2020/03~011213")
## [1] "2020-03-01 12:13:00 UTC"
注:根据需要可以ymd_h/myd_hm/dmy_hms 任意组合;可以用参数tz ="…" 指定时区。
也可以用make_date() 和make_datetime() 从日期时间组件创建日期时间:
make_date(2020, 8, 27)
## [1] "2020-08-27"
make_datetime(2020, 8, 27, 21, 27, 15)
## [1] "2020-08-27 21:27:15 UTC"
2. 格式化输出日期时间
用format() 函数
d = make_date(2020, 3, 5)
format(d, '%Y/%m/%d')
## [1] "2020/03/05"
用stamp() 函数,按给定模板格式输出
t = make_datetime(2020, 3, 5, 21, 7, 15)
fmt = stamp("Created on Sunday, Jan 1, 1999 3:34 pm")
fmt(t)
## [1] "Created on Sunday, 03 05, 2020 21:07 下午"
3. 提取日期时间数据的组件
日期时间数据中的‘‘年、月、日、周、时、分、秒’’ 等,称为其组件。
t = ymd_hms("2020/08/27 21:30:27")
t
## [1] "2020-08-27 21:30:27 UTC"
year(t)
## [1] 2020
quarter(t) # 第几季度
## [1] 3
month(t)
## [1] 8
day(t)
## [1] 27
yday(t) # 当年的第几天
## [1] 240
hour(t)
## [1] 21
minute(t)
## [1] 30
second(t)
## [1] 27
weekdays(t)
## [1] "星期四"
wday(t) # 数值表示本周第几天, 默认周日是第1 天
## [1] 5
wday(t,label = TRUE) # 字符因子型表示本周第几天
## [1] 周四
## Levels: 周日< 周一< 周二< 周三< 周四< 周五< 周六
week(t) # 当年第几周
## [1] 35
tz(t) # 时区
## [1] "UTC"
用with_tz() 将时间数据转换为另一个时区的同一时间;force_tz() 将时间数据的时区强制转换为另一个时区:
with_tz(t, tz = "America/New_York")
## [1] "2020-08-27 17:30:27 EDT"
force_tz(t, tz = "America/New_York")
## [1] "2020-08-27 21:30:27 EDT"
还可以模糊提取(取整)到不同时间单位:
round_date(t, unit="hour") # 四舍五入取整到小时
## [1] "2020-08-27 22:00:00 UTC"
注:类似地,向下取整: floor_date(); 向上取整: ceiling_date()
rollback(dates, roll_to_first=FALSE, preserve_hms=TRUE):回滚到上月最后一天或本月第一天
4. 时间段数据
interval(): 计算两个时间点的时间间隔,返回时间段数据
begin = ymd_hm("2019-08-10 14:00")
end = ymd_hm("2020-03-05 18:15")
gap = interval(begin, end)
gap
## [1] 2019-08-10 14:00:00 UTC--2020-03-05 18:15:00 UTC
time_length(gap, "day") # 计算时间段的长度为多少天
## [1] 208.1771
time_length(gap, "minute") # 计算时间段的长度为多少分钟
## [1] 299775
t %within% gap # 判断t 是否属于该时间段
## [1] FALSE
duration(): 以数值+ 时间单位存储时段的长度
duration(100, units = "day")
## [1] "8640000s (~14.29 weeks)"
int = as.duration(gap)
int
## [1] "17986500s (~29.74 weeks)"
period(): 基本同duration()
二者区别:duration 是基于数值线,不考虑闰年和闰秒;period 是基于时间线,考虑闰年和闰秒。
比如,duration 中的1 年总是365.25 天,而period 的平年365 天闰年366 天。
固定单位的时间段
period 时间段:years(), months(), weeks(), days(), hours(), minutes(), seconds();
duration 时间段:dyears(), dmonths(), dweeks(), ddays(), dhours(), dminutes(), dseconds().
dyears(1)
## [1] "31557600s (~1 years)"
years(1)
## [1] "1y 0m 0d 0H 0M 0S"
5. 日期的时间的计算
时间点+ 时间段生成一个新的时间点:
t + int
## [1] "2021-03-24 01:45:27 UTC"
leap_year(2020) # 判断是否闰年
## [1] TRUE
ymd(20190305) + years(1) # 加period 的一年
## [1] "2020-03-05"
ymd(20190305) + dyears(1) # 加duration 的一年, 365 天
## [1] "2020-03-04 06:00:00 UTC"
t + weeks(1:3)
## [1] "2020-09-03 21:30:27 UTC" "2020-09-10 21:30:27 UTC"
## [3] "2020-09-17 21:30:27 UTC"
除法运算:
gap / ddays(1) # 除法运算, 同time_length(gap,'day')
## [1] 208.1771
gap %/% ddays(1) # 整除
## [1] 208
gap %% ddays(1) # 余数
## [1] 2020-03-05 14:00:00 UTC--2020-03-05 18:15:00 UTC
as.period(gap %% ddays(1))
## [1] "4H 15M 0S"
月份加运算: %m+%,表示日期按月数增加。例如,生成每月同一天的日期数据:
date = as_date("2019-01-01")
date %m+% months(0:11)
## [1] "2019-01-01" "2019-02-01" "2019-03-01" "2019-04-01" "2019-05-01"
## [6] "2019-06-01" "2019-07-01" "2019-08-01" "2019-09-01" "2019-10-01"
## [11] "2019-11-01" "2019-12-01"
prety_dates() 生成近似的时间刻度:
x = seq.Date(as_date("2019-08-02"), by = "year", length.out = 2)
pretty_dates(x, 12)
## [1] "2019-08-01 UTC" "2019-09-01 UTC" "2019-10-01 UTC"
## [4] "2019-11-01 UTC" "2019-12-01 UTC" "2020-01-01 UTC"
## [7] "2020-02-01 UTC" "2020-03-01 UTC" "2020-04-01 UTC"
## [10] "2020-05-01 UTC" "2020-06-01 UTC" "2020-07-01 UTC"
## [13] "2020-08-01 UTC" "2020-09-01 UTC"
1.4.3 时间序列
为了研究某一事件的规律,依据时间发生的顺序将事件在多个时刻的数值记录下来,就构成了一个时间序列,用{Yt} 表示。
例如,国家或地区的年度财政收入,股票市场的每日波动,气象变化,工厂按小时观测的产量等等。另外,随温度、高度等变化而变化的离散序列,也可以看作时间序列。
ts 对象
base R 提供的ts 数据类型是专门为时间序列设计的,一个时间序列数据,其实就是一个数值型向量,且每个数都有一个时刻与之对应。
用ts() 函数生成时间序列,基本格式为:
ts(data, start=1, end, frequency=1, ...)
data:为数值向量或矩阵
start:设置起始时刻
end:设置结束时刻
frequency:设置时间频率,默认为1,表示一年有1 个数据。
ts(data = 1:10, start = 2010, end = 2019) # 年度数据
## Time Series:
## Start = 2010
## End = 2019
## Frequency = 1
## [1] 1 2 3 4 5 6 7 8 9 10
ts(data = 1:10, start = 2010, frequency = 4) # 季度数据
## Qtr1 Qtr2 Qtr3 Qtr4
## 2010 1 2 3 4
## 2011 5 6 7 8
## 2012 9 10
同理,月度数据则frequency = 12,周度数则为frequency = 52,日度数据则为frequency =365。
tsibble
fpp3 生态下的tsibble 包提供了整洁的时间序列数据结构tsibble.时间序列数据,无非就是指标数据+ 时间索引(或者再+ 分组索引)。注:多元时间序列,就是包含多个指标列。
对于分组时间序列数据,首先是一个数据框,若有分组变量需采用’’ 长格式’’ 作为一列(长宽格
式及转化见2.4 节),只需要指定时间索引、分组索引,就能变成时间序列数据结构。
例如,现有tibble 格式的3 个公司2017 年的日度股票数据,其中存放3 只股票的Stock 列为分组索引:
load("datas/stocks.rda")
stocks
## # A tibble: 753 x 3
## Date Stock Close
## <date> <chr> <dbl>
## 1 2017-01-03 Google 786.
## 2 2017-01-03 Amazon 754.
## 3 2017-01-03 Apple 116.
## 4 2017-01-04 Google 787.
## 5 2017-01-04 Amazon 757.
## 6 2017-01-04 Apple 116.
## # ... with 747 more rows
用as_tsibble() 将数据框转化为时间序列对象tsibble, 只需要指定时间索引(index)、分组索
引(key):
library(fpp3)
stocks = as_tsibble(stocks, key = Stock, index = Date)
stocks
## # A tsibble: 753 x 3 [1D]
## # Key: Stock [3]
## Date Stock Close
## <date> <chr> <dbl>
## 1 2017-01-03 Amazon 754.
## 2 2017-01-04 Amazon 757.
## 3 2017-01-05 Amazon 780.
## 4 2017-01-06 Amazon 796.
## 5 2017-01-09 Amazon 797.
## 6 2017-01-10 Amazon 796.
## # ... with 747 more rows
tsibble 对象非常便于后续处理和探索:
stocks %>%
group_by_key() %>%
index_by(weeks = ~ yearweek(.)) %>% # 周度汇总
summarise(max_week = mean(Close))
## # A tsibble: 156 x 3 [1W]
## # Key: Stock [3]
## Stock weeks max_week
## <chr> <week> <dbl>
## 1 Amazon 2017 W01 772.
## 2 Amazon 2017 W02 805.
## 3 Amazon 2017 W03 809.
## 4 Amazon 2017 W04 830.
## 5 Amazon 2017 W05 827.
## 6 Amazon 2017 W06 818.
## # ... with 150 more rows
autoplot(stocks) # 可视化
1.5 正则表达式
笔者推荐一个正则表达式学习网站正则表达式入门。
正则表达式,是根据字符串规律按一定法则,简洁表达一组字符串的表达式。正则表达式通常就是从貌似无规律的字符串中发现规律性,进而概括性地表达它们所共有的规律或模式,以方便地操作处理它们,这是真正的化繁为简,以简驭繁的典范。
几乎所有的高级编程语言都支持正则表达式,正则表达式广泛应用于文本挖掘、数据预处理,例如:
检查文本中是否含有指定的特征词
找出文本中匹配特征词的位置
从文本中提取信息
修改文本
正则表达式包括:只能匹配自身的普通字符(如英文字母、数字、标点等) 和被转义了的特殊字符(称为‘‘元字符”) 。
1.5.1 基本语法
-
常用的元字符
其他语言中的转义字符一般是\;在多行模式下,^ 和$ 就表示行的开始和结束。
# 创建多行模式的正则表达式
pat = regex("^\\(.+?\\)$", multiline = TRUE)
-
特殊字符类与反义
\\S+: 匹配不包含空白符的字符串
\\d: 匹配数字,同[0-9]
[a-zA-Z0-9]: 匹配字母和数字
[\u4e00-\u9fa5] 匹配汉字
[^aeiou]: 匹配除aeiou 之外的任意字符,即匹配辅音字母
-
POSIX 字符类
- 运算优先级
圆括号括起来的表达式最优先,其次是表示重复次数的操作(即* + { }) ;再次是连接运算(即几个字符放在一起,如abc) ;最后是或者运算(|) 。
另外,正则表达式还有若干高级用法,常用的有零宽断言和分组捕获,将在下面实例中进行演示。
1.5.2 若干实例
以上正则表达式语法组合起来使用,就能产生非常强大的匹配效果,对于匹配到的内容,根据需要可以提取它们,可以替换它们。
若只是调试和查看正则表达式的匹配效果,可用str_view() 及其_all 后缀版本,将在RStudio的Viewer 窗口显示匹配结果,在原字符向量中高亮显示匹配内容,非常直观。
若要提取正则表达式匹配到的内容,则用str_extract() 及其_all 后缀版本。
若要替换正则表达式匹配到的内容,则用str_replace() 及其_all 后缀版本。
使用正则表达式关键是,能够从貌似没有规律的字符串中发现规律性,再将规律性用正则表达式语法表示出来。下面看几个正则表达式比较实用的实例。
例1.1 直接匹配
适合想要匹配的内容具有一定规律性,该规律性可用正则表达式表示出来。比如,数据中包含字母、符号、数值,我们想提取其中的数值,按正则表达式语法规则直接把要提取的部分表示出来:
x = c("CDK 弱(+)10%+", "CDK(+)30%-", "CDK(-)0+", "CDK(++)60%*")
str_view(x, "\\d+%")
str_view(x, "\\d+%?")
\\d 表示匹配一位数字,+ 表示前面数字重复1 次或多次,接着% 原样匹配%.若后面不加? 则必须匹配到% 才会成功,故第3 个字符串就不能成功匹配;若后面加上? 则表示匹配前面的% 0 次或1 次,从而能成功匹配第3 个字符串。
例1.2 (零宽断言) 匹配两个标志之间的内容
适合想要匹配的内容没有规律性,但该内容位于两个有规律性的标志之间,标志也可以是开始和结束。
通常想要匹配的内容不包含两边的‘‘标志’’,这就需要用零宽断言。简单来说,就是一种引导语法告诉既要匹配到‘‘标志’’,但又不包含‘‘标志’’。左边标志的引导语法是(?<= 标志),右边标志的引导语法是(?= 标志),而真正要匹配的内容放在它们中间。
比如,来自问卷星‘‘来自IP” 数据,想要提取IP、省份。
x = c("175.10.237.40(湖南-长沙)", "114.243.12.168(北京-北京)",
"125.211.78.251(黑龙江-哈尔滨)")
# 提取省份
str_extract(x, "\\(.*-") # 对比,不用零宽断言
## [1] "(湖南-" "(北京-" "(黑龙江-"
str_extract(x, "(?<=\\().*(?=-)") # 用零宽断言
## [1] "湖南" "北京" "黑龙江"
# 提取IP
# str_extract(x, "\\d.*\\d") # 直接匹配
str_extract(x, "^.*(?=\\()") # 用零宽断言
## [1] "175.10.237.40" "114.243.12.168" "125.211.78.251"
省份位于两个标志“(” 和“-” 之间,但又不包含该标志,这就需要用到零宽断言。
IP 位于两个标志‘‘开始” 和“(” 之间,左边用开始符号^,右边用零宽断言。
再比如,用零宽断言提取专业(位于‘‘级’’ 和数字之间) :
x = c("18 级能源动力工程2 班", "19 级统计学1 班")
str_extract(x, "(?<= 级).*?(?=[0-9])")
## [1] "能源动力工程" "统计学"
关于懒惰匹配:
正则表达式正常都是贪婪匹配,即重复直到文本中能匹配的最长范围,例如匹配小括号:
str_extract("(1st) other (2nd)", "\(.+\)")
## [1] "(1st) other (2nd)"
若想只匹配到第1 个右小括号,则需要懒惰匹配,在重复匹配后面加上? 即可:
str_extract("(1st) other (2nd)", "\(.+?\)")
## [1] "(1st)"
例1.3 分组捕获
正则表达式中可以用圆括号来分组,作用是
确定优先规则
组成一个整体
拆分出整个匹配中的部分内容(称为捕获)
捕获内容供后续引用或者替换。
比如,来自瓜子二手车的数据:若型号是中文,则品牌与型号中间有空格;若型号为英文或数字,则品牌与型号中间没有空格。
若用正则表达式匹配‘‘字母或数字” 并分组,然后捕获该分组并用添加空格替换:
x = c(" 宝马X3 2016 款", " 大众速腾2017 款", " 宝马3 系2012 款")
str_replace(x, "([a-zA-Z0-9])", " \\1")
## [1] "宝马X3 2016款" "大众速腾2017款" "宝马3系2012款"
后续再用空格分割列即可。更多分组的引用还有\\2, \\3, . . .
最后,再推荐一个来自Github 可以推断正则表达式的包inferregex,用函数infer_regex() 可根据字符串推断正则表达式。