2017年12月21日

因子

在R中,因子常被用来表示分类变量。分类变量是有固定和已知的可能值集合的一类变量。
因子比字符串更容易处理。R语言中的许多基本函数会自动将字符串转换为因子(比如read.csv()),但很多时候这样的转换并不合适,需要注意(比如read.csv()中,需要设置参数stringsAsFactors = FALSE)。

创建因子

x1为记录月份的字符型向量。

x1 <- c("Dec", "Apr", "Jan", "Mar")

使用字符型变量有两个问题:

  1. 不能避免拼写错误(尽管只有十二个月):

    x2 <- c("Dec", "Apr", "Jam", "Mar")
  2. 不能以有用的方式进行排序(从1月到12月):

    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)
y1
[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

没有在因子水平集合中的值会被转换为NA,但不会给出警告(warning)。

y2 <- factor(x2, levels = month_levels)
y2
[1] Dec  Apr  <NA> Mar 
Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

如果忽略了levels参数,会将字母顺序作为因子水平的排序。

factor(x1)
[1] Dec Apr Jan Mar
Levels: Apr Dec Jan Mar

factor(c("一月", "二月", "三月"))
[1] 一月 二月 三月
Levels: 二月 三月 一月

使用levels()可以获取因子水平集合。

y1
[1] Dec Apr Jan Mar
Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
levels(y1)
 [1] "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov"
[12] "Dec"

综合社会调查数据

本章使用forcats包,以及包中的gss_cat数据。这是一份由芝加哥大学负责的美国综合社会调查的样本数据。调查包括数千个问题,gss_cat数据从中选择了一些问题来演示因子的处理。

# install.packages("forcats")
library(forcats)
library(dplyr)
library(ggplot2)
data("gss_cat")

由于gss_cat是包中提供的数据,因此可以用?gss_cat获取数据的介绍。

str(gss_cat)
Classes 'tbl_df', 'tbl' and 'data.frame':   21483 obs. of  9 variables:
 $ year   : int  2000 2000 2000 2000 2000 2000 2000 2000 2000 2000 ...
 $ marital: Factor w/ 6 levels "No answer","Never married",..: 2 4 5 2 4 6 2 4 6 6 ...
 $ age    : int  26 48 67 39 25 25 36 44 44 47 ...
 $ race   : Factor w/ 4 levels "Other","Black",..: 3 3 3 3 3 3 3 3 3 3 ...
 $ rincome: Factor w/ 16 levels "No answer","Don't know",..: 8 8 16 16 16 5 4 9 4 4 ...
 $ partyid: Factor w/ 10 levels "No answer","Don't know",..: 6 5 7 6 9 10 5 8 9 4 ...
 $ relig  : Factor w/ 16 levels "No answer","Don't know",..: 15 15 15 6 12 15 5 15 15 15 ...
 $ denom  : Factor w/ 30 levels "No answer","Don't know",..: 25 23 3 30 30 25 30 15 4 25 ...
 $ tvhours: int  12 NA 2 4 1 NA 3 NA 0 3 ...

当因子存储在tibble中时,无法直接看到因子水平,这时可以使用count()

gss_cat %>% count(race)
# A tibble: 3 x 2
    race     n
  <fctr> <int>
1  Other  1959
2  Black  3129
3  White 16395

或者绘制柱状图。

qplot(race, data = gss_cat, geom = "bar")

需要注意,在默认情况下,qplot不会绘制没有因子的因子水平。

练习

  1. 绘制rincome(reported income)的分布图,绘制出的柱状图难懂的原因是什么?
  2. 在综合社会调查数据中,最常见的宗教relig是什么?最常见的政党partyid是什么?
  3. 在综合社会调查数据中,哪些宗教relig有教派denom

改变因子水平的顺序

处理因子时,最常见的操作是改变因子水平的顺序或因子水平的值。
改变因子水平的顺序常用于绘图时。比如分析不同宗教信仰的人每天看电视的平均小时数。

relig_summary <- gss_cat %>% group_by(relig) %>% 
    summarise(age = mean(age, na.rm = TRUE), 
              tvhours = mean(tvhours, na.rm = TRUE), n = n())
qplot(tvhours, relig, data = relig_summary)

使用fct_reorder()改变relig的因子水平顺序,能使图形更加易读。fct_reorder(f, x, fun)有三个参数:

  • f 想要改变因子水平顺序的因子。
  • x 用来排序因子水平的数值向量。
  • fun 可选函数,如果同一因子水平存在多个x,则用fun计算每一个因子水平中x的取值。默认为median
qplot(tvhours, fct_reorder(relig, tvhours), data = relig_summary)

relig的因子水平进行重新排序后可以很容易的看出,宗教信仰为Don't know的人看电视时间最长,为Hinduism & Other Eastern religions的人看电视时间最短。
如果需要做更复杂的数据变换,可以单独的写一个mutate()函数。

relig_summary <- relig_summary %>% 
    mutate(relig = fct_reorder(relig, tvhours))
qplot(tvhours, relig, data = relig_summary)

采用同样的方法分析不同收入水平的平均年龄?

rincome_summary <- gss_cat %>% group_by(rincome) %>% 
    summarise(age = mean(age, na.rm = TRUE), 
              tvhours = mean(tvhours, na.rm = TRUE), n = n())
qplot(age, fct_reorder(rincome, age), data = rincome_summary)

在上个图形中,对rincome的因子水平进行重新排序并不合适,因为rincome原先就有更加合理的因子水平顺序。

levels(rincome_summary$rincome)
 [1] "No answer"      "Don't know"     "Refused"        "$25000 or more"
 [5] "$20000 - 24999" "$15000 - 19999" "$10000 - 14999" "$8000 to 9999" 
 [9] "$7000 to 7999"  "$6000 to 6999"  "$5000 to 5999"  "$4000 to 4999" 
[13] "$3000 to 3999"  "$1000 to 2999"  "Lt $1000"       "Not applicable"

唯一需要调整的是应该将Not applicable调整到其他因子水平之前。

可以使用fct_relevel(),第一个参数为因子,第二个参数为需要调整到前面的因子水平。

qplot(age, fct_relevel(rincome, "Not applicable"), data = rincome_summary)

思考为什么Not applicable的平均年龄特别高?

对于柱状图,还可以用fct_infreq()根据因子出现频率排序因子水平,如果要改变顺序,可以结合使用fct_rev()

gss_cat <- gss_cat %>% 
    mutate(marital = marital %>% fct_infreq() %>% fct_rev())
qplot(marital, data = gss_cat, geom = "bar")

练习

  1. 判断gss_cat数据框中每个因子的因子水平顺序是任意指定的还是遵循一定原则的?
  2. fct_relevel函数中还有一个after参数,研究该参数有什么用?

改变因子水平的名称

改变因子水平的名称比改变因子水平的顺序功能更强大。最通用的改变因子水平名称的函数是fct_recode()。以gss_cat$partyid为例。

gss_cat %>% count(partyid)
# A tibble: 10 x 2
              partyid     n
               <fctr> <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

其中因子水平名称均为缩写,改成全称后更加易懂。fct_recode()不会改变没有提及的因子水平名称,一旦遇到了不存在因子水平名称,会给出警告。

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
                  <fctr> <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

另外可以将多个因子水平名称赋予同一个新的因子水平名称以达到归并因子水平的目的。使用这一技巧必须小心,如果归并的因子水平显著不同,会误导结果。

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
                 <fctr> <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

如果需要归并许多因子水平,可以使用fct_collapse(),其是fct_recode()的一个变体。对一个新的因子水平名称,提供一个旧的因子水平名称的向量。

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
   <fctr> <int>
1   other   548
2     rep  5346
3     ind  8409
4     dem  7180

练习

  1. 尝试合并一些rincome中的因子水平(比如将收入分为高、中、低等)?