1. 前言
在 R
中,因子用于处理类别变量,即具有固定且已知所有可能值的变量。当您希望以非字母顺序显示字符向量时,它们也很有用
以经验上来看,因子通常比字符串更容易处理,因此,R
中提供的许多基础函数都会自动将字符串转换为因子。
但是这些转换通常是没有意思的,所以,在 tidyverse
中并不会出现这种问题。
1.1 导入
为了处理因子,我们需要用到 tidyverse
中的 forcat
包。
它提供了处理分类变量的工具,包含了许多的辅助函数用于处理因子。
library(tidyverse)
2. 构建因子
假设您有一个记录月份的变量
x1 <- c("Dec", "Apr", "Jan", "Mar")
我们使用字符串来记录这个变量会有两个问题:
- 月份只有
12
个可能的值,但是拼写错误的问题应该是无法避免的,例如
x2 <- c("Dec", "Apr", "Jam", "Mar")
- 无法对其进行排序
> sort(x1)
[1] "Apr" "Dec" "Jan" "Mar"
你可以用一个因子来解决这两个问题。
要创建因子,必须先创建有效的 levels
> month_levels <- c(
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+ )
然后可以创建因子了
> (y1 <- factor(x1, levels = month_levels))
[1] Dec Apr Jan Mar
Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
# 排序
> sort(y1)
[1] Jan Mar Apr Dec
Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
不在 levels
中的任何值都将自动转换为 NA
> (y2 <- factor(x2, levels = month_levels))
[1] Dec Apr <NA> Mar
Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
如果你想在出现这一问题时抛出一个警告,可以使用 readr::parse_factor()
> y2 <- parse_factor(x2, levels = month_levels)
Warning: 1 parsing failure.
row col expected actual
3 -- value in level set Jam
如果您省略了 levels
,则将按字母顺序来获取它们
> factor(x1)
[1] Dec Apr Jan Mar
Levels: Apr Dec Jan Mar
有时您可能希望 levels
的顺序与数据第一次出现的顺序相匹配
在创建因子时,可以通过将 levels
设置为 unique(x)
,或者在创建之后使用 fct_inorder()
来做到这一点
> (f1 <- factor(x1, levels = unique(x1)))
[1] Dec Apr Jan Mar
Levels: Dec Apr Jan Mar
> (f2 <- x1 %>% factor() %>% fct_inorder())
[1] Dec Apr Jan Mar
Levels: Dec Apr Jan Mar
如果你想要直接访问 levels
,可以使用 levels()
进行访问
> levels(f2)
[1] "Dec" "Apr" "Jan" "Mar"
3. 社会调查
在本节的后续部分,我们将重点讨论 forcats::gss_cat
数据,这是一份来自一般社会调查的数据样本。
> gss_cat
# A tibble: 21,483 x 9
year marital age race rincome partyid relig denom tvhours
<int> <fct> <int> <fct> <fct> <fct> <fct> <fct> <int>
1 2000 Never marri… 26 White $8000 to 9999 Ind,near rep Protestant Southern bapt… 12
2 2000 Divorced 48 White $8000 to 9999 Not str republi… Protestant Baptist-dk wh… NA
3 2000 Widowed 67 White Not applicab… Independent Protestant No denominati… 2
4 2000 Never marri… 39 White Not applicab… Ind,near rep Orthodox-chris… Not applicable 4
5 2000 Divorced 25 White Not applicab… Not str democrat None Not applicable 1
6 2000 Married 25 White $20000 - 249… Strong democrat Protestant Southern bapt… NA
7 2000 Never marri… 36 White $25000 or mo… Not str republi… Christian Not applicable 3
8 2000 Divorced 44 White $7000 to 7999 Ind,near dem Protestant Lutheran-mo s… NA
9 2000 Married 44 White $25000 or mo… Not str democrat Protestant Other 0
10 2000 Married 47 White $25000 or mo… Strong republic… Protestant Southern bapt… 3
# … with 21,473 more rows
可以使用 ?gss_cat
查看数据变量的含义
当因子存储在一个 tible
表中时,你可能没那么容易能看到它们的 levels
。一种方法是使用 count()
> gss_cat %>%
+ count(race)
# A tibble: 3 x 2
race n
<fct> <int>
1 Other 1959
2 Black 3129
3 White 16395
或条形图
> ggplot(gss_cat, aes(race)) +
+ geom_bar()
默认情况下,ggplot2
将删除没有任何值的 levels
,您可以强制它们显示
> ggplot(gss_cat, aes(race)) +
+ geom_bar() +
+ scale_x_discrete(drop = FALSE)
使用因子时,最常见的两种操作是更改 levels
的顺序以及对应的值,这些操作将在下面展开介绍。
3.1 思考练习
- 在这项调查中最常见的
relig
是哪个?最常见的partyid
是什么?
4. 修改因子顺序
在可视化应用中改变因子的 levels
的顺序通常是有用的。
例如,您想了解不同宗教每天看电视的平均时间
relig_summary <- gss_cat %>%
group_by(relig) %>%
summarise(
age = mean(age, na.rm = TRUE),
tvhours = mean(tvhours, na.rm = TRUE),
n = n()
)
#> `summarise()` ungrouping output (override with `.groups` argument)
ggplot(relig_summary, aes(tvhours, relig)) + geom_point()
这张图看起来不是很直观,我们可以使用 fct_reorder()
来重新排列宗教的 levels
来呈现更好的效果。
fct_reorder()
接受三个参数:
-
f
- 要修改的levels
的因子 -
x
- 要用于重新排列levels
的数字向量 -
fun
- 可选参数,汇总函数,默认median
> ggplot(relig_summary, aes(tvhours, fct_reorder(relig, tvhours))) +
+ geom_point()
通过对宗教进行重新排序,我们可以更容易地看到 Don’t know
类别的人看电视的时间更长,而印度教和其他东方宗教看得时间更少。
当您开始进行更复杂的转换时,我建议将它们从 aes()
移到单独的 mutate()
步骤中。
例如,您可以将上面的绘图写成
relig_summary %>%
mutate(relig = fct_reorder(relig, tvhours)) %>%
ggplot(aes(tvhours, relig)) +
geom_point()
如果我们想创建一个类似的图,来研究平均年龄和收入水平之间是如何变化的,该怎么做
rincome_summary <- gss_cat %>%
group_by(rincome) %>%
summarise(
age = mean(age, na.rm = TRUE),
tvhours = mean(tvhours, na.rm = TRUE),
n = n()
)
#> `summarise()` ungrouping output (override with `.groups` argument)
ggplot(rincome_summary, aes(age, fct_reorder(rincome, age))) + geom_point()
您可以使用 fct_relevel()
来重新排列 levels
,它传入一个因子 f
,然后是你想要移动到前面的任意数量的 levels
ggplot(rincome_summary, aes(age, fct_relevel(rincome, "Not applicable"))) +
geom_point()
如果你绘图使用的是彩色的线条,另一种类型的重排将会很有用的。
fct_reorder2()
将因子按与最大的 x
值相关的 y
值重新排序。因为线的颜色与图例对应,这使图形更加容易阅读。
by_age <- gss_cat %>%
filter(!is.na(age)) %>%
count(age, marital) %>%
group_by(age) %>%
mutate(prop = n / sum(n))
ggplot(by_age, aes(age, prop, colour = marital)) +
geom_line(na.rm = TRUE)
ggplot(by_age, aes(age, prop, colour = fct_reorder2(marital, age, prop))) +
geom_line() +
labs(colour = "marital")
最后,对于条形图,可以使用 fct_infreq()
以递增的频率对 levels
进行排序。
这是最简单的重新排序方式,它不需要任何额外的变量。您可以与 fct_rev()
结合使用。
gss_cat %>%
mutate(marital = marital %>% fct_infreq() %>% fct_rev()) %>%
ggplot(aes(marital)) +
geom_bar()
4.1 思考练习
在
tvhours
列中有一些特别高的值,使用均值来衡量是否准确?对于
gss_cst
中的每个因子,确定levels
的顺序是任意的还是有规律的。
5. 修改 levels
比改变 levels
顺序更强大的是改变他们的值,你可以使用 fct_recode()
函数来重新编码或更改每个 levels
的值。
例如,对于 gss_cat$partyid
> gss_cat %>% count(partyid)
# A tibble: 10 x 2
partyid n
<fct> <int>
1 No answer 154
2 Don't know 1
3 Other party 393
4 Strong republican 2314
5 Not str republican 3032
6 Ind,near rep 1791
7 Independent 4119
8 Ind,near dem 2499
9 Not str democrat 3690
10 Strong democrat 3490
它的 levels
非常简单且不太一致,让我们把它调整得更长一些,并使用并行结构
> gss_cat %>%
+ mutate(partyid = fct_recode(partyid,
+ "Republican, strong" = "Strong republican",
+ "Republican, weak" = "Not str republican",
+ "Independent, near rep" = "Ind,near rep",
+ "Independent, near dem" = "Ind,near dem",
+ "Democrat, weak" = "Not str democrat",
+ "Democrat, strong" = "Strong democrat"
+ )) %>%
+ count(partyid)
# A tibble: 10 x 2
partyid n
<fct> <int>
1 No answer 154
2 Don't know 1
3 Other party 393
4 Republican, strong 2314
5 Republican, weak 3032
6 Independent, near rep 1791
7 Independent 4119
8 Independent, near dem 2499
9 Democrat, weak 3690
10 Democrat, strong 3490
fct_recode()
将会保留未明确提及的 level
,并在引用不存在的 level
时发出警告
要合并组,可以将多个旧的 levels
分配给同一个新的 levels
> gss_cat %>%
+ mutate(partyid = fct_recode(partyid,
+ "Republican, strong" = "Strong republican",
+ "Republican, weak" = "Not str republican",
+ "Independent, near rep" = "Ind,near rep",
+ "Independent, near dem" = "Ind,near dem",
+ "Democrat, weak" = "Not str democrat",
+ "Democrat, strong" = "Strong democrat",
+ "Other" = "No answer",
+ "Other" = "Don't know",
+ "Other" = "Other party"
+ )) %>%
+ count(partyid)
# A tibble: 8 x 2
partyid n
<fct> <int>
1 Other 548
2 Republican, strong 2314
3 Republican, weak 3032
4 Independent, near rep 1791
5 Independent 4119
6 Independent, near dem 2499
7 Democrat, weak 3690
8 Democrat, strong 3490
您必须谨慎使用此技术:如果确实要将不同的类别组合在一起,则会导致误导性的结果
如果要折叠 levels
,则可以使用 fct_recode()
的变体 fct_collapse()
。对于每个新变量,您可以提供旧 levels
的向量
> gss_cat %>%
+ mutate(partyid = fct_collapse(partyid,
+ other = c("No answer", "Don't know", "Other party"),
+ rep = c("Strong republican", "Not str republican"),
+ ind = c("Ind,near rep", "Independent", "Ind,near dem"),
+ dem = c("Not str democrat", "Strong democrat")
+ )) %>%
+ count(partyid)
# A tibble: 4 x 2
partyid n
<fct> <int>
1 other 548
2 rep 5346
3 ind 8409
4 dem 7180
有时你只是想把所有的小组集中在一起,让绘图或显示表格更简单,可以使用 fct_lump()
> gss_cat %>%
+ mutate(relig = fct_lump(relig)) %>%
+ count(relig)
# A tibble: 2 x 2
relig n
<fct> <int>
1 Protestant 10846
2 Other 10637
默认是逐步将最小的组聚合在一起,确保聚合后仍然是最小的组。
在这种情况下,它不是很有用,因为本次调查中的大多数美国人都是新教徒。
此外,我们可以使用参数 n
来指定需要保留多少组(不包括其他组)
> gss_cat %>%
+ mutate(relig = fct_lump(relig, n = 10)) %>%
+ count(relig, sort = TRUE) %>%
+ print(n = Inf)
# A tibble: 10 x 2
relig n
<fct> <int>
1 Protestant 10846
2 Catholic 5124
3 None 3523
4 Christian 689
5 Other 458
6 Jewish 388
7 Buddhism 147
8 Inter-nondenominational 109
9 Moslem/islam 104
10 Orthodox-christian 95
5.1 思考练习
如何将
rincome
折叠为更小的分类集合?Democrat
,Republican
, 和Independent
的人数占比是如何随时间变化的?