ねこすたっと

ねこの気持ちと統計について悩む筆者の備忘録的ページ。

因子型データに対する操作(forcatsパッケージ)[R]

forcatsパッケージを使った因子型データに対する操作を2つの記事に分けてまとめます。この記事は1本目です。

forcatsパッケージは因子型(factor型)*1データの操作を楽にしてくれるパッケージ。 tidyverseのコアパッケージに含まれているので、tidyverseパッケージを読み込めば使用可能(なはず)。

library(tidyverse)

因子型データとは

Rはカテゴリー変数を扱う際に、因子型という変数型を用いる。R独特の変数型。

因子型データは、一見すると文字列を値にとるデータに見えるが、内部では値は整数として与えられていて、各整数に対してカテゴリー名が紐付けられている、という構造になっている。別の言い方をすれば、カテゴリーには順番がつけられているということ。ある変数の1つ1つのカテゴリーのことを「水準(level)」と呼ぶ*2

因子型データの作り方

次の3つの関数のいずれかを使って作る。いずれも引数に文字列や数値のベクトルを渡す(パイプ記号%>%も可)。

  • factor( ):基本パッケージの関数。使い方は後述。
  • as.factor( ):基本パッケージの関数。英語文字列を渡すとアルファベット順に水準が与えられるが、日本語文字列を与えると登場順に水準が割り振られる。数値の場合は昇順(小さいものから)。
  • as_factor( ):forcatsパッケージの関数。文字列は英語・日本語に関わらず登場順に水準が割り振られる。数値の場合は昇順(小さいものから)。

例として、10から降順に1まで並んだ数値ベクトルを因子型にしてみると、

> 10:1 %>% as.factor()
 [1] 10 9  8  7  6  5  4  3  2  1 
Levels: 1 2 3 4 5 6 7 8 9 10

> 10:1 %>% as_factor()
 [1] 10 9  8  7  6  5  4  3  2  1 
Levels: 1 2 3 4 5 6 7 8 9 10

どちらも数値が小さいもの順に水準(Levels)が与えられた。

同じベクトルを文字列型にしてから渡してみると、as_factor( )では登場してくる順に水準が与えられている。

> 10:1 %>% as.character() %>% as_factor()
 [1] 10 9  8  7  6  5  4  3  2  1 
Levels: 10 9 8 7 6 5 4 3 2 1

これに対し、as.factor( )では辞書的な文字列の順序(まず数字から始まるものが先、その中で字数少ないもの順)になっている。

> 10:1 %>% as.character() %>% as.factor()
 [1] 10 9  8  7  6  5  4  3  2  1 
Levels: 1 10 2 3 4 5 6 7 8 9

似た名前の関数で微妙に挙動が違うのが疎まれる原因だと思う。

factor( )を作って因子型データを作る

主に使う引数は以下のとおり。

  • x:因子型化したい文字列ベクトル
  • levels:水準を順に指定

下のコードでは、4つの文字から構成される文字ベクトルをfactor( )に渡して、3つの水準を持った因子型データにしている。

> c("a", "c", "b", "a") %>% factor(levels = c("a", "b", "c"))
[1] a c b a
Levels: a b c

余計な話だが、内部ではlevelsで指定した順に整数が割り振られていることをunclass( )で確認してみる。

> c("a","c","b","a") %>% factor(levels=c("a","b","c")) %>% unclass()
[1] 1 3 2 1
attr(,"levels")
[1] "a" "b" "c"

なので、水準の順番を変えると、見た目に表示される文字列(つまり{a, c, b, a})は同じでも内部に割り振られた整数は異なっている。

> c("a","c","b","a") %>% factor(levels=c("c","b","a")) %>% unclass()
[1] 3 1 2 3
attr(,"levels")
[1] "c" "b" "a"

ちなみに、引数labelsは因子型データが内部で持っている整数に紐付ける水準名を指定するためのもの。例えば、下のコードで作成された因子型データfは、内部に整数値として {1, 3, 2, 1} を持っていて、整数値に対して {1="a", 2="b", 3="c"} という「ラベル」が紐つけられている。

> f <- c("a","c","b","a") %>% factor(levels=c("a","b","c"))
> f %>% unclass()
[1] 1 3 2 1
attr(,"levels")
[1] "a" "b" "c"

このラベルを {1="ド", 2="レ", 3="ミ"} に変更したい場合は、labelsを下のように指定する。

> f %>% factor(labels=c("ド","レ","ミ"))
[1] ド ミ レ ド
Levels: ド レ ミ

2つの因子型データを統合する

一部の水準を共有する2つの因子型データを単純に結合するときにはfct_c( )を使う。

例示用に以下の因子型データを作成しておく。

f1 <- factor(c("a", "c", "b", "a"), levels = c("a", "b", "c"))
f2 <- factor(c("a", "c", "d", "a"), levels = c("a", "c", "d"))

結合されて新しく作られたデータの水準は、第1引数となっている因子型データの水準に、不足分が後付けされる形になるので、結合する順番によって水準(の順序)が変わる。

> fct_c(f1,f2)
[1] a c b a a c d a
Levels: a b c d
> fct_c(f2,f1)
[1] a c d a a c b a
Levels: a c d b

2つの因子型データ(もしくは文字列)を結合させて新しい因子型データを作成するときはfct_cross( )を使う。引数に取るデータは何種類でもいいが、同じ長さでなくてはならない。

> fct_cross(f1,f2)
[1] a:a c:c b:d a:a
Levels: a:a c:c b:d

掛け合わせて生じた水準が新しい水準になっている。

因子型データの内容を確認する

例示用に下のような因子型データを作っておく。

f <- factor(c("a", "c", "b", "a"), levels = c("a", "b", "c"))

そのままオブジェクト名を入力すれば、内容と水準(Levels)が表示される。

> f
[1] a c b a
Levels: a b c

水準だけ取り出したいときはlevels( )を使う(sを忘れないように)。

> levels(f)
[1] "a" "b" "c"

unclass( )を使えば、内部に保持している数値と紐付いたラベルが表示されることは述べた。

> unclass(f)
[1] 1 3 2 1
attr(,"levels")
[1] "a" "b" "c"

データフレームに含まれている変数の概要を表示するstr( )やglimpse( )を使ってもよい。

> str(f)
 Factor w/ 3 levels "a","b","c": 1 3 2 1

forcatsパッケージにも因子型データの内容を確認できる関数がある。

  • fct_count( ):水準ごとの度数を表示
  • fct_unique( ):重複を取り除いたものを返す
  • fct_match( ):ある水準に一致しているかどうかの真偽を返す。参照する水準は引数lvlsで指定する。

出力は割愛。

水準の順序を変更する

下の関数を使って、水準に対してどのような順番で整数を割り当てるかを変更できる。

  • fct_inorder( ):早く登場する順に並べる
  • fct_infreq( ):登場機会の多いものから順に並べる
  • fct_inseq( ):数値の小さいものから順に並べる

fct_inseq( )は、次のように数値が因子型として認識されているような場合のみ有効。

f <- factor(3:1, levels = c("3", "2", "1"))
> f
[1] 3 2 1
Levels: 3 2 1

> f %>% fct_inseq()
[1] 3 2 1
Levels: 1 2 3

要素の並び順は変わっていないが、水準は数値昇順に変わっている。

他にも以下の関数がある。

  • fct_rev( ):元の順序と逆順に並べる
  • fct_shift( ):1つずらす(必要になる状況は不明)
  • fct_relevel("a","c","b"):水準の順番を指定する

他の変数に基づいて水準を並べ替える

fct_infreq( )は因子型データそのものの頻度に基づいて並べ替えたが、他の変数の順序(例えば平均値が高いもの順など)に基づいて水準の順番を変更することができる。

forcatsパッケージに含まれるgss_catデータを例に使う。

> glimpse(gss_cat)
Rows: 21,483
Columns: 9
$ year    <int> 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000$ marital <fct> Never married, Divorced, Widowed, Never married, Divorced,$ age     <int> 26, 48, 67, 39, 25, 25, 36, 44, 44, 47, 53, 52, 52, 51, 52$ race    <fct> White, White, White, White, White, White, White, White, Wh…
$ rincome <fct> $8000 to 9999, $8000 to 9999, Not applicable, Not applicab…
$ partyid <fct> "Ind,near rep", "Not str republican", "Independent", "Ind,…
$ relig   <fct> Protestant, Protestant, Protestant, Orthodox-christian, No…
$ denom   <fct> "Southern baptist", "Baptist-dk which", "No denomination",…
$ tvhours <int> 12, NA, 2, 4, 1, NA, 3, NA, 0, 3, 2, NA, 1, NA, 1, 7, NA, …

婚姻状態maritalの各水準に対して、年齢ageの箱ヒゲ図を描くと下のようになる。

ggplot(data=gss_cat, aes(x=marital, y=age))+
  geom_boxplot()

ここで年齢ageの中央値が高い順に並べ替えるためには、下のようにfct_reorder( )を使う。第2引数は第1引数の水準の順番を決めるために参照する変数。他に指定する変数は、

  • .fun:使用する要約関数。ここではmedianを使うが、欠測値を除外しないと計算できないのでna.rm=TRUEを追加している。
  • .desc:TRUEとすると降順になる。デフォルトは昇順。

いずれも最初の.を忘れないように。

ggplot(data=gss_cat, 
       aes(x=fct_reorder(marital, age, .fun=median, na.rm=TRUE, .desc=TRUE), y=age))+
  geom_boxplot()

婚姻状態の水準が年齢が高い順に並べ替えられている。

水準の値を変更する

例としてgtsummaryパッケージのtrialsデータを使う。

library(gtsummary)
head(trial)
# A tibble: 6 × 8
  trt      age marker stage grade response death ttdeath
  <chr>  <dbl>  <dbl> <fct> <fct>    <int> <int>   <dbl>
1 Drug A    23  0.16  T1    II           0     0    24  
2 Drug B     9  1.11  T2    I            1     0    24  
3 Drug A    31  0.277 T1    II           0     0    24  
4 Drug A    NA  2.07  T3    III          1     1    17.6
5 Drug A    51  2.77  T4    III          1     1    16.4
6 Drug B    39  0.613 T4    I            0     1    15.6

fct_recode( )で水準のラベル名を変更する

下のように「新しいラベル名 = 元々のラベル名」の要領で指定する。

trial$stage %>% fct_recode("1"="T1", "2"="T2", "3"="T3", "4"="T4")
  [1] 1 2 1 3 4 4 1 1 1 3 1 3 4 4 1 4 4 2 1 1 2 2 4 4 4 2 4 1 4 3 1 2 3 3 3
~~~(中略)~~~
[176] 2 4 4 2 3 4 3 4 4 1 1 4 4 4 1 2 2 2 4 3 3 2 2 4 3
attr(,"label")
[1] T Stage
Levels: 1 2 3 4

再帰的に元のデータでもラベル名を変更したい場合は、%<>%を使うとよい(magrittrパッケージが必要)。

fct_collapse( )で複数の水準をまとめる

fct_recode( )の書き方と同じく、「新しいラベル名 = 元々のラベル名」で複数の水準をまとめることができる。

trial$stage %>% fct_collapse("Localized" = c("T1","T2"), "Advanced" = c("T3", "T4")) 
  [1] Localized Localized Localized Advanced  Advanced  Advanced  Localized
~~~(中略)~~~
[197] Localized Localized Advanced  Advanced 
attr(,"label")
[1] T Stage
Levels: Localized Advanced

いくつかの水準を「その他」としてまとめる

頻度の少ない水準を「その他」として塊(lump)にする。 例として再度gss_catデータを使う。婚姻状態maritalの分布は以下のとおり。

> gss_cat$marital %>% fct_count()
# A tibble: 6 × 2
  f                 n
  <fct>         <int>
1 No answer        17
2 Never married  5416
3 Separated       743
4 Divorced       3383
5 Widowed        1807
6 Married       10117

指定した度数よりも少ない水準をまとめるにはfct_lump_min( )を使う。 下のように、度数の下限を引数minで指定する(minで指定した値と同数の水準がどうなるかわかるようにmin=1807とした)。

> gss_cat$marital %>% fct_lump_min(min=1807) %>% fct_count()
# A tibble: 5 × 2
  f                 n
  <fct>         <int>
1 Never married  5416
2 Divorced       3383
3 Widowed        1807
4 Married       10117
5 Other           760

1807例以上のものが残っていることがわかる。

相対頻度で指定するときには、fct_lump_prop( )を使う。 引数propで指定した割合よりも少ない頻度の水準はまとめられる(前述のminと同じで、指定と同数は残される)。

> gss_cat$marital %>% fct_lump_prop(prop=0.25) %>% fct_count()
# A tibble: 3 × 2
  f                 n
  <fct>         <int>
1 Never married  5416
2 Married       10117
3 Other          5950

次のように、引数propに負の数を指定すると、指定した割合よりも多い頻度の水準がまとめられる。

> gss_cat$marital %>% fct_lump_prop(prop=-0.25) %>% fct_count()
# A tibble: 5 × 2
  f             n
  <fct>     <int>
1 No answer    17
2 Separated   743
3 Divorced   3383
4 Widowed    1807
5 Other     15533

頻度の順位で指定するときはfct_lump_n( )を使う。 引数nで指定した数の水準が残される。

> gss_cat$marital %>% fct_lump_n(n=2) %>% fct_count()
# A tibble: 3 × 2
  f                 n
  <fct>         <int>
1 Never married  5416
2 Married       10117
3 Other          5950

こちらも引数に負の数を指定すると、少ないものからn個が残される。

> gss_cat$marital %>% fct_lump_n(n=-2) %>% fct_count()
# A tibble: 3 × 2
  f             n
  <fct>     <int>
1 No answer    17
2 Separated   743
3 Other     20723

fct_other( )を使えば、残したいもの(引数keep)や、まとめたいもの(引数drop)を自分で指定できる。

> gss_cat$marital %>% fct_other(keep=c("Separated", "Divorced")) %>% fct_count()
# A tibble: 3 × 2
  f             n
  <fct>     <int>
1 Separated   743
2 Divorced   3383
3 Other     17357
> gss_cat$marital %>% fct_other(drop=c("Separated","Divorced")) %>% fct_count()
# A tibble: 5 × 2
  f                 n
  <fct>         <int>
1 No answer        17
2 Never married  5416
3 Widowed        1807
4 Married       10117
5 Other          4126

ここで紹介した関数では「その他」としてまとめた水準のラベル名は引数other_levelを使って指定することができる。 サンプリングの重み付けを引数wで指定することや、同順位をどのように扱うかを引数ties.methodで指定することもできる(詳細割愛)

おわりに

  • このパッケージを使えば、作図のたびにカテゴリーを統合した新しい変数を作っておかなくてもよさそうです。
  • forcatsといいpurrrといい、Wickham先生は猫派なんでしょうか。

参考資料

  • 軽いトーンで書かかれているに、結構細かいところまで拾ってくださっているという印象です。勉強させてもらってます。

datasciencemore.com

  • 本家サイトです。

forcats.tidyverse.org

*1:「因子」という語は一般的に使われがちなので、ファクター型と呼ぶ方が混乱が少ないのかもしれません

*2:本当は内部