16  팩터(Factors)

16.1 소개

팩터(Factors)는 범주형 변수, 즉 고정되고 알려진 가능한 값 집합을 가진 변수에 사용됩니다. 또한 문자 벡터를 알파벳 순서가 아닌 순서로 표시하려는 경우에도 유용합니다.

데이터 분석을 위해 팩터가 필요한 이유1factor()로 팩터를 생성하는 방법부터 시작하겠습니다. 그런 다음 실험할 수 있는 많은 범주형 변수가 포함된 gss_cat 데이터셋을 소개합니다. 그 후 해당 데이터셋을 사용하여 팩터의 순서와 값을 수정하는 연습을 한 다음, 순서형 팩터에 대한 논의로 마무리하겠습니다.

16.1.1 선수 지식

기본(base) R은 팩터를 생성하고 조작하기 위한 몇 가지 기본 도구를 제공합니다. 핵심 tidyverse의 일부인 forcats 패키지로 이를 보완할 것입니다. 이 패키지는 팩터 작업을 위한 광범위한 도우미를 사용하여 범주형(categorical) 변수(그리고 factors의 애너그램입니다!)를 다루는 도구를 제공합니다.

library(tidyverse)
#> Warning: package 'ggplot2' was built under R version 4.5.2
#> Warning: package 'readr' was built under R version 4.5.2

16.2 팩터 기초

월을 기록하는 변수가 있다고 상상해 보세요:

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

문자열을 사용하여 이 변수를 기록하면 두 가지 문제가 있습니다:

  1. 가능한 달은 12개뿐이며 오타로부터 당신을 구해줄 것이 없습니다:

    x2 <- c("Dec", "Apr", "Jam", "Mar")
  2. 유용한 방식으로 정렬되지 않습니다:

    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로 변환됩니다:

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

이것은 위험해 보이므로 대신 forcats::fct()를 사용하고 싶을 수 있습니다:

y2 <- fct(x2, levels = month_levels)
#> Error in `fct()`:
#> ! All values of `x` must appear in `levels` or `na`
#> ℹ Missing level: "Jam"

수준을 생략하면 데이터에서 알파벳 순서로 가져옵니다:

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

알파벳 순서로 정렬하는 것은 모든 컴퓨터가 문자열을 같은 방식으로 정렬하지 않기 때문에 약간 위험합니다. 따라서 forcats::fct()는 첫 번째 등장 순서로 정렬합니다:

fct(x1)
#> [1] Dec Apr Jan Mar
#> Levels: Dec Apr Jan Mar

유효한 수준 집합에 직접 액세스해야 하는 경우 levels()를 사용하여 수행할 수 있습니다:

levels(y2)
#>  [1] "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"

col_factor()를 사용하여 readr로 데이터를 읽을 때 팩터를 생성할 수도 있습니다:

csv <- "
month,value
Jan,12
Feb,56
Mar,12"

df <- read_csv(csv, col_types = cols(month = col_factor(month_levels)))
df$month
#> [1] Jan Feb Mar
#> Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

16.3 종합 사회 조사(General Social Survey)

이 장의 나머지 부분에서는 forcats::gss_cat을 사용할 것입니다. 이것은 시카고 대학의 독립 연구 기관인 NORC가 수행하는 장기 미국 설문 조사인 종합 사회 조사(General Social Survey)의 데이터 샘플입니다. 설문 조사에는 수천 개의 질문이 있으므로 gss_cat에서 해들리(Hadley)는 팩터로 작업할 때 직면하게 될 몇 가지 일반적인 문제를 설명할 수 있는 몇 가지를 선택했습니다.

gss_cat
#> # A tibble: 21,483 × 9
#>    year marital         age race  rincome        partyid           
#>   <int> <fct>         <int> <fct> <fct>          <fct>             
#> 1  2000 Never married    26 White $8000 to 9999  Ind,near rep      
#> 2  2000 Divorced         48 White $8000 to 9999  Not str republican
#> 3  2000 Widowed          67 White Not applicable Independent       
#> 4  2000 Never married    39 White Not applicable Ind,near rep      
#> 5  2000 Divorced         25 White Not applicable Not str democrat  
#> 6  2000 Married          25 White $20000 - 24999 Strong democrat   
#> # ℹ 21,477 more rows
#> # ℹ 3 more variables: relig <fct>, denom <fct>, tvhours <int>

(이 데이터셋은 패키지에서 제공하므로 ?gss_cat으로 변수에 대한 자세한 정보를 얻을 수 있음을 기억하세요.)

팩터가 티블에 저장되어 있으면 수준을 그렇게 쉽게 볼 수 없습니다. 보는 한 가지 방법은 count()를 사용하는 것입니다:

gss_cat |>
  count(race)
#> # A tibble: 3 × 2
#>   race      n
#>   <fct> <int>
#> 1 Other  1959
#> 2 Black  3129
#> 3 White 16395

팩터로 작업할 때 가장 일반적인 두 가지 작업은 수준의 순서를 변경하는 것과 수준의 값을 변경하는 것입니다. 이러한 작업은 아래 섹션에 설명되어 있습니다.

16.3.1 연습문제

  1. rincome(보고된 소득)의 분포를 탐색하세요. 기본 막대 차트를 이해하기 어렵게 만드는 것은 무엇입니까? 플롯을 어떻게 개선할 수 있습니까?

  2. 이 설문 조사에서 가장 흔한 relig는 무엇입니까? 가장 흔한 partyid는 무엇입니까?

  3. denom(교파)은 어떤 relig에 적용됩니까? 테이블로 어떻게 알아낼 수 있습니까? 시각화로 어떻게 알아낼 수 있습니까?

16.4 팩터 순서 수정

시각화에서 팩터 수준의 순서를 변경하는 것이 유용한 경우가 많습니다. 예를 들어 종교 전반에 걸쳐 하루 평균 TV 시청 시간을 탐색하고 싶다고 상상해 보세요:

relig_summary <- gss_cat |>
  group_by(relig) |>
  summarize(
    tvhours = mean(tvhours, na.rm = TRUE),
    n = n()
  )

ggplot(relig_summary, aes(x = tvhours, y = relig)) +
  geom_point()

x축에 tvhours, y축에 religion이 있는 산점도. y축은 겉보기에 임의로  정렬되어 있어 전체적인 패턴을 파악하기 어렵습니다.

전체적인 패턴이 없기 때문에 이 플롯을 읽기 어렵습니다. fct_reorder()를 사용하여 relig의 수준을 재정렬하여 개선할 수 있습니다. fct_reorder()는 세 가지 인수를 취합니다:

  • .f, 수준을 수정하려는 팩터.
  • .x, 수준을 재정렬하는 데 사용하려는 수치형 벡터.
  • 선택적으로 .fun, .f의 각 값에 대해 .x의 값이 여러 개 있는 경우 사용되는 함수. 기본값은 median입니다.
ggplot(relig_summary, aes(x = tvhours, y = fct_reorder(relig, tvhours))) +
  geom_point()

위와 동일한 산점도이지만 이제 종교가 tvhours의 증가하는 순서로  표시됩니다. "Other eastern"은 2시간 미만으로 tvhours가 가장 적고  "Don't know"는 가장 많습니다(5시간 이상).

종교를 재정렬하면 “Don’t know” 범주의 사람들이 TV를 훨씬 더 많이 보고 힌두교 및 기타 동양 종교는 훨씬 덜 본다는 것을 훨씬 쉽게 알 수 있습니다.

더 복잡한 변환을 만들기 시작하면 aes() 밖으로 이동하여 별도의 mutate() 단계로 옮기는 것이 좋습니다. 예를 들어 위의 플롯을 다음과 같이 다시 쓸 수 있습니다:

relig_summary |>
  mutate(
    relig = fct_reorder(relig, tvhours)
  ) |>
  ggplot(aes(x = tvhours, y = relig)) +
  geom_point()

보고된 소득 수준에 따라 평균 연령이 어떻게 변하는지 살펴보는 유사한 플롯을 만들면 어떨까요?

rincome_summary <- gss_cat |>
  group_by(rincome) |>
  summarize(
    age = mean(age, na.rm = TRUE),
    n = n()
  )

ggplot(rincome_summary, aes(x = age, y = fct_reorder(rincome, age))) +
  geom_point()

x축에 나이, y축에 소득이 있는 산점도. 소득은 평균 연령 순으로  재정렬되었는데 이는 별로 의미가 없습니다. y축의 한 섹션은 $6000-6999,  그 다음 <$1000, 그 다음 $8000-9999로 진행됩니다.

여기서 수준을 임의로 재정렬하는 것은 좋은 생각이 아닙니다! rincome은 이미 우리가 건드려서는 안 되는 원칙적인 순서를 가지고 있기 때문입니다. fct_reorder()는 수준이 임의로 정렬된 팩터에만 사용하세요.

그러나 “Not applicable”을 다른 특수 수준과 함께 맨 앞으로 끌어오는 것은 의미가 있습니다. fct_relevel()을 사용할 수 있습니다. 이것은 팩터 .f와 줄의 맨 앞으로 이동하려는 임의의 수의 수준을 취합니다.

ggplot(rincome_summary, aes(x = age, y = fct_relevel(rincome, "Not applicable"))) +
  geom_point()

동일한 산점도이지만 이제 "Not Applicable"이 y축 하단에 표시됩니다.  일반적으로 소득과 연령 사이에는 양의 연관성이 있으며 평균 연령이 가장 높은  소득 구간은 "Not applicable"입니다.

“Not applicable”의 평균 연령이 그렇게 높은 이유는 무엇이라고 생각합니까?

플롯의 선에 색상을 입힐 때 유용한 또 다른 유형의 재정렬이 있습니다. fct_reorder2(.f, .x, .y)는 가장 큰 .x 값과 연관된 .y 값으로 팩터 .f를 재정렬합니다. 이렇게 하면 플롯의 맨 오른쪽에 있는 선의 색상이 범례와 정렬되기 때문에 플롯을 읽기 쉬워집니다.

by_age <- gss_cat |>
  filter(!is.na(age)) |>
  count(age, marital) |>
  group_by(age) |>
  mutate(
    prop = n / sum(n)
  )

ggplot(by_age, aes(x = age, y = prop, color = marital)) +
  geom_line(linewidth = 1) +
  scale_color_brewer(palette = "Set1")

ggplot(by_age, aes(x = age, y = prop, color = fct_reorder2(marital, age, prop))) +
  geom_line(linewidth = 1) +
  scale_color_brewer(palette = "Set1") +
  labs(color = "marital")

x축에 나이, y축에 비율이 있는 선 플롯. 결혼 상태의 각 범주(무응답,  미혼, 별거, 이혼, 사별, 기혼)에 대해 하나의 선이 있습니다. 범례의  순서가 플롯의 선과 관련이 없기 때문에 플롯을 읽기가 조금 어렵습니다.  범례를 재배열하면 범례 색상이 이제 플롯의 맨 오른쪽에 있는 선의  순서와 일치하기 때문에 플롯을 읽기 쉬워집니다. 미혼 비율은 나이가  들수록 감소하고, 기혼은 거꾸로 된 U자 모양을 형성하며, 사별은 낮게  시작하지만 60세 이후 가파르게 증가하는 등 놀랍지 않은 패턴을 볼 수  있습니다.

x축에 나이, y축에 비율이 있는 선 플롯. 결혼 상태의 각 범주(무응답,  미혼, 별거, 이혼, 사별, 기혼)에 대해 하나의 선이 있습니다. 범례의  순서가 플롯의 선과 관련이 없기 때문에 플롯을 읽기가 조금 어렵습니다.  범례를 재배열하면 범례 색상이 이제 플롯의 맨 오른쪽에 있는 선의  순서와 일치하기 때문에 플롯을 읽기 쉬워집니다. 미혼 비율은 나이가  들수록 감소하고, 기혼은 거꾸로 된 U자 모양을 형성하며, 사별은 낮게  시작하지만 60세 이후 가파르게 증가하는 등 놀랍지 않은 패턴을 볼 수  있습니다.

마지막으로 막대 플롯의 경우 fct_infreq()를 사용하여 빈도 내림차순으로 수준을 정렬할 수 있습니다. 추가 변수가 필요하지 않기 때문에 가장 간단한 재정렬 유형입니다. 막대 플롯에서 가장 큰 값이 왼쪽이 아닌 오른쪽에 오도록 빈도 오름차순으로 하려면 fct_rev()와 결합하세요.

gss_cat |>
  mutate(marital = marital |> fct_infreq() |> fct_rev()) |>
  ggplot(aes(x = marital)) +
  geom_bar()

결혼 상태의 막대 차트가 가장 적은 것부터 가장 많은 순으로 정렬됨:  무응답 (~0), 별거 (~1,000), 사별 (~2,000), 이혼 (~3,000),  미혼 (~5,000), 기혼 (~10,000).

16.4.1 연습문제

  1. tvhours에 의심스럽게 높은 숫자가 몇 개 있습니다. 평균이 좋은 요약입니까?

  2. gss_cat의 각 팩터에 대해 수준의 순서가 임의적인지 원칙적인지 식별하세요.

  3. “Not applicable”을 수준의 맨 앞으로 이동시켰는데 왜 플롯의 맨 아래로 이동했습니까?

16.5 팩터 수준 수정

수준의 순서를 변경하는 것보다 더 강력한 것은 값을 변경하는 것입니다. 이를 통해 출판을 위해 레이블을 명확히 하고 상위 수준 디스플레이를 위해 수준을 축소할 수 있습니다. 가장 일반적이고 강력한 도구는 fct_recode()입니다. 각 수준의 값을 다시 코딩하거나 변경할 수 있습니다. 예를 들어 gss_cat 데이터 프레임의 partyid 변수를 가져와 보겠습니다:

gss_cat |> count(partyid)
#> # A tibble: 10 × 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
#> # ℹ 4 more rows

수준이 간결하고 일관성이 없습니다. 더 길게 수정하고 병렬 구조를 사용해 보겠습니다. tidyverse의 대부분의 이름 바꾸기 및 다시 코딩 함수와 마찬가지로 새 값은 왼쪽에 가고 이전 값은 오른쪽에 갑니다:

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 × 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
#> # ℹ 4 more rows

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",
      "Other"                 = "No answer",
      "Other"                 = "Don't know",
      "Other"                 = "Other party"
    )
  )

이 기술을 주의해서 사용하세요. 진정으로 다른 범주를 함께 그룹화하면 오해의 소지가 있는 결과를 초래할 수 있습니다.

많은 수준을 축소하려는 경우 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 × 2
#>   partyid     n
#>   <fct>   <int>
#> 1 other     548
#> 2 rep      5346
#> 3 ind      8409
#> 4 dem      7180

때로는 플롯이나 테이블을 더 간단하게 만들기 위해 작은 그룹을 덩어리로 묶고 싶을 때가 있습니다. 그것이 fct_lump_*() 함수 패밀리의 역할입니다. fct_lump_lowfreq()는 가장 작은 그룹 범주를 “Other”로 점진적으로 묶는 간단한 시작점이며, 항상 “Other”를 가장 작은 범주로 유지합니다.

gss_cat |>
  mutate(relig = fct_lump_lowfreq(relig)) |>
  count(relig)
#> # A tibble: 2 × 2
#>   relig          n
#>   <fct>      <int>
#> 1 Protestant 10846
#> 2 Other      10637

이 경우 별로 도움이 되지 않습니다. 이 설문 조사의 대다수 미국인이 개신교인 것은 사실이지만 아마도 더 자세한 내용을 보고 싶을 것입니다! 대신 fct_lump_n()을 사용하여 정확히 10개의 그룹을 원한다고 지정할 수 있습니다:

gss_cat |>
  mutate(relig = fct_lump_n(relig, n = 10)) |>
  count(relig, sort = TRUE)
#> # A tibble: 10 × 2
#>   relig          n
#>   <fct>      <int>
#> 1 Protestant 10846
#> 2 Catholic    5124
#> 3 None        3523
#> 4 Christian    689
#> 5 Other        458
#> 6 Jewish       388
#> # ℹ 4 more rows

다른 경우에 유용한 fct_lump_min()fct_lump_prop()에 대해 알아보려면 문서를 읽어보세요.

16.5.1 연습문제

  1. 민주당, 공화당, 무소속으로 식별되는 사람들의 비율은 시간이 지남에 따라 어떻게 변했습니까?

  2. rincome을 어떻게 작은 범주 집합으로 축소할 수 있습니까?

  3. 위의 fct_lump 예제에 9개의 그룹(other 제외)이 있음을 주목하세요. 왜 10개가 아닙니까? (힌트: ?fct_lump를 입력하고 other_level 인수의 기본값이 “Other”인지 확인하세요.)

16.6 순서형 팩터(Ordered factors)

계속하기 전에 특수 유형의 팩터인 순서형 팩터에 대해 간단히 언급하는 것이 중요합니다. ordered() 함수로 생성된 순서형 팩터는 수준 간의 엄격한 순서를 의미하지만 수준 간의 차이의 크기에 대해서는 아무것도 지정하지 않습니다. 수준에 순위가 있지만 정확한 수치적 순위가 없음을 알 때 순서형 팩터를 사용합니다.

인쇄될 때 팩터 수준 사이에 < 기호를 사용하므로 순서형 팩터를 식별할 수 있습니다:

ordered(c("a", "b", "c"))
#> [1] a b c
#> Levels: a < b < c

기본 R과 tidyverse 모두에서 순서형 팩터는 일반 팩터와 매우 유사하게 작동합니다. 동작의 차이를 알아차릴 수 있는 곳은 두 곳뿐입니다:

  • 순서형 팩터를 ggplot2의 color 또는 fill에 매핑하면 순위를 암시하는 색상 척도인 scale_color_viridis()/scale_fill_viridis()가 기본값이 됩니다.
  • 선형 모델에서 순서형 예측 변수를 사용하면 “다항식 대비(polynomial contrasts)”를 사용합니다. 이것들은 약간 유용하지만 통계학 박사 학위가 있지 않는 한 들어본 적이 없을 것이며, 그렇다 하더라도 아마 일상적으로 해석하지는 않을 것입니다. 더 알고 싶다면 Lisa DeBruine의 vignette("contrasts", package = "faux")를 추천합니다.

이 책의 목적을 위해 일반 팩터와 순서형 팩터를 올바르게 구별하는 것은 특별히 중요하지 않습니다. 그러나 더 넓게 보면 특정 분야(특히 사회 과학)에서는 순서형 팩터를 광범위하게 사용합니다. 이러한 맥락에서는 다른 분석 패키지가 적절한 동작을 제공할 수 있도록 올바르게 식별하는 것이 중요합니다.

16.7 요약

이 장에서는 팩터 작업을 위한 편리한 forcats 패키지를 소개하고 가장 일반적으로 사용되는 함수를 소개했습니다. forcats에는 여기서 논의할 공간이 없었던 다른 다양한 도우미가 포함되어 있으므로 이전에 경험해보지 못한 요인 분석 문제에 직면할 때마다 참조 색인을 훑어보고 문제를 해결하는 데 도움이 될 수 있는 미리 준비된 함수가 있는지 확인하는 것을 강력히 추천합니다.

이 장을 읽은 후 팩터에 대해 더 알고 싶다면 Amelia McNamara와 Nicholas Horton의 논문 Wrangling categorical data in R을 읽어보는 것을 추천합니다. 이 논문은 stringsAsFactors: An unauthorized biographystringsAsFactors = <sigh>에서 논의된 역사의 일부를 설명하고 이 책에 설명된 범주형 데이터에 대한 깔끔한 접근 방식과 기본 R 방법을 비교합니다. 논문의 초기 버전은 forcats 패키지에 동기를 부여하고 범위를 지정하는 데 도움이 되었습니다. Amelia와 Nick에게 감사합니다!

다음 장에서는 기어를 바꿔 R에서 날짜와 시간에 대해 배우기 시작할 것입니다. 날짜와 시간은 겉보기에는 단순해 보이지만 곧 알게 되겠지만 더 많이 알수록 더 복잡해지는 것 같습니다!


  1. 모델링에도 정말 중요합니다.↩︎