9  데이터 요약하기 (= 분석하기)

R의 기본 문법과 데이터를 R로 불러들이는 방법까지 배웠으니, 이제 본격적으로 데이터 분석을 시작할 차례입니다. 이 장의 제목은 ‘데이터 요약하기’ 이지만, 사실 데이터 분석이라는 것은 어떻게 생각하면 요약 그 이상도 이하도 하닙니다. 예컨대, ‘대통령의 국정 수행 지지율은 40%이다’라는 분석은 수천명에 달하는 설문 응답자의 각기 다른 응답을 지지하는 비율 이라는 하나의 척도로 요약한 것이라고 할 수 있습니다. 물론, 이 설문조사가 전체 대한민국 국민, 또는 투표권을 가진 국민의 의견을 대표하는지는 불확실하기 때문에, ’표본오차’ (±2%) 같은 추가적인 정보를 더하기도 합니다만, 이 역시 많은 데이터 수에 비하면 매우 간단하게 요약된 숫자이지요. 따지고 보면, 넓게는 데이터 과학, 좁게는 데이터 저널리즘이 제시하는 대부분의 분석은 큰 데이터를 몇 가지의 숫자로 요약하고, 이를 특정 그룹 간에 비교한 것에 지나지 않습니다. 이를 통계에서는 멋지게 ’기술통계(descriptive statistics)’라고 부르기도 합니다만, 그러한 용어법이 중요한 것은 아닙니다.

우리가 다음 장에서 볼 시각화는 이렇게 요약된 숫자를 그래프, 인포그래픽과 같은 시각 요소로 표현한 것에 지나지 않습니다. 즉, 데이터 요약하기는 분석의 시작이자 끝이라고 할 수 있죠.

우리가 앞서 배운 tidyverse는 데이터를 요약하기 위한 간단하면서도 강력한 문법들을 제공합니다. 이제 여기에 대해서 알아보도록 하죠. 일단, 이번 챕터에서 사용할 수 있도록 donorschoose.org 데이터를 불러 보도록 하지요.

library(tidyverse)
projects <- read_csv("data/projects.csv")

9.1 summarise()

tidyverse 문법에서 데이터를 요약할 때에는 그게 어떤 방식의 요약이든간에, 요약을 한다고 선언해 주어야 합니다. 그 선언은 간단하게 summarise()라는 함수를 사용하면 됩니다. 그리고 그 함수 안에 ‘어느 변수를’ ‘어떤 방식으로’ 요약할 것인지만 써주면 됩니다.

바로 다음과 같이 말이지요.

projects |>
    summarise(avg_size = mean(total_price_excluding_optional_support),
              avg_opt = mean(total_price_including_optional_support - 
                                 total_price_excluding_optional_support))
# A tibble: 1 × 2
  avg_size avg_opt
     <dbl>   <dbl>
1     542.    103.

첫번째 줄의 의미는, total_price_excluding_optional_support 칼럼의 평균(mean())을 낸 다음 이를 avg_size라고 부르자는 의미 입니다. 두번째는 total_price_including_optional_support에서 total_price_excluding_optional_support을 뺀 값들의 평균을 낸 다음 이를 avg_opt라고 부르자는 의미입니다. 이렇게, 여러개의 요약을 한 번에 할 수 있습니다.

어떻게 요약을 할 것인지에 해당하는 함수로 이번에는 평균, mean()을 사용했지만 그 외에도 여러가지가 있습니다.

함수 기능
mean() 평균
median() 중앙값
sd() 표준편차
n() 갯수
max() 최대값
min() 최소값

9.2 그룹별 요약

전체 데이터를 요약하는 것이 유용할 때도 많지만, 사실 데이터 분석에서 훨씬 더 자주 사용하는 것은 그룹별 요약 입니다. 예컨대, 대한민국 1인당 국민소득이 관심사일 수도 있지만, 지역별 1인당 국민소득에 관심을 가질 때도 있는 것이지요.

특히, 의미 있는 분석 결과를 도출하기 위해서는 특정 그룹 사이를 비교하는 것이 효과적이기 때문에, 이러한 그룹별 비교는 더욱 자주 사용하게 됩니다. 앞서의 예에서라면, 전라남도와 경상남도의 1인당 국민소득을 비교하는 것도 그룹별 비교이지만, 연도를 하나의 그룹이라고 생각한다면, 연도별 1인당 국민소득을 비교하는 것 역시 그룹별 비교라고 할 수 있습니다.

tidyverse 문법에서 그룹별 요약을 하기 위해서는 summarise() 함수 앞에 group_by() 함수를 사용해 주면 됩니다. 단, group_by() 함수의 인수로 요약에 사용하고 싶은 그룹 정보가 담긴 컬럼명을 표시해주면 됩니다.

projects |>
    group_by(school_state) |>
    summarise(avg_size = mean(total_price_excluding_optional_support))
# A tibble: 52 × 2
   school_state avg_size
   <chr>           <dbl>
 1 AK               516.
 2 AL               507.
 3 AR               524.
 4 AZ               500.
 5 CA               599.
 6 CO               505.
 7 CT               480.
 8 DC               511.
 9 DE               411.
10 FL               488.
# ℹ 42 more rows

이렇게 하면, donorschoose.org에 생성된 프로젝트들의 주별 평균 목표액 크기가 구해진 것입니다.

이렇게 group_by()를 이용한 요약에서 두 가지를 이해하면 좋습니다.

첫째, group_by()까지 실행했을 때는 것으로 보기에 아무 일도 일어나지 않습니다. 이를 확인하기 위해서 위의 코드에서 일부만을 실행해 보겠습니다.

projects |>
    group_by(school_state)
# A tibble: 664,098 × 35
# Groups:   school_state [52]
   projectid               teacher_acctid schoolid school_ncesid school_latitude
   <chr>                   <chr>          <chr>    <chr>                   <dbl>
 1 316ed8fb3b81402ff6ac8f… 42d43fa6f3731… c0e6ce8… 063627006187             36.6
 2 90de744e368a7e4883223c… 864eb466462bf… d711e47… 483702008193             32.9
 3 32943bb1063267de6ed19f… 37f85135259ec… 665c361… 410327000109             45.2
 4 bb18f409abda2f264d5acd… 2133fc46f951f… 4f12c3f… 360015302507             40.6
 5 24761b686e18e5eace6346… 867ff478a63f5… 10179fd… 062271003157             34.0
 6 eac7d156205f1333de3887… ff064802c18e6… 929336e… 040187001058             33.3
 7 5a3bfdf2e05781ccd0654d… 085794a9e315b… dc34b02… 010060000256             32.8
 8 afda16eb54d8992db7bb42… 1d94a31c2dc38… 86fff7c… 130129000571             33.9
 9 2ab3efb23acc84017cd789… 5a497c425e05b… ed170a1… 261200001297             42.4
10 3118962680bb062323c356… eb0855cc6ea55… 71b4dee… 340264001386             40.0
# ℹ 664,088 more rows
# ℹ 30 more variables: school_longitude <dbl>, school_city <chr>,
#   school_state <chr>, school_zip <chr>, school_metro <chr>,
#   school_district <chr>, school_county <chr>, school_charter <lgl>,
#   school_magnet <lgl>, school_year_round <lgl>, school_nlns <lgl>,
#   school_kipp <lgl>, school_charter_ready_promise <lgl>,
#   teacher_prefix <chr>, teacher_teach_for_america <lgl>, …

이는 전체 projects 테이블을 출력하는 것과 동일합니다. 다만, 좌측 상단에 보면 ’Groups: school_state [52]’라는 표시가 있습니다. 즉, group_by() 함수를 실시하면 이용자가 지정한대로 그룹만 만들어둔 상태가 되는 것입니다. 실제로 효과가 나타나는 것은 summarise() 함수나 다른 함수를 이용해서 그룹별 연산을 한 다음이지요.

한 가지만 더 이야기 하자면, 코드의 일부분만을 실행시키기 위해서는 위와 같이 부분을 새로 작성해서 실행해 주어도 되지만, 실행시키고 싶은 부분만으로 블록 설정한 다음 ’Alt+Enter’를 눌러주어도 됩니다. 이는 사실 여러분들의 코드에서 에러가 발생했을 때 버그가 있는 부분을 찾는 좋은 방법이기도 합니다.

둘째, summarise() 함수를 이용해 요약한 결과 그 자체도 테이블 입니다. 따라서 그 결과물에도 서브세트, 요약, 피봇 등의 작업을 다시 적용할 있습니다. 바로 다음과 같은 경우가 그렇습니다.

projects |>
    group_by(school_state) |>
    summarise(avg_size = mean(total_price_excluding_optional_support),
              count = n()) |>
    filter(avg_size > 600)
# A tibble: 3 × 3
  school_state avg_size count
  <chr>           <dbl> <int>
1 HI               703.  2586
2 La               654.     3
3 NY               705. 73182

이렇게 하면, 평균 프로젝트 목표액이 600달러를 초과하는 주만 결과를 확인할 수 있네요.

9.3 날짜 정보를 이용한 그룹별 요약

모든 프로그램 언어에서 날짜는 에러를 자주 발생시키는 골치덩어리 입니다. 하지만, 날짜 정보는 데이터 분석에서 빠질 수 없는 요소이기도 하지요. 제일 흔하게 하는 분석 중 하나가 ‘연도별 추이’ 따위를 계산하는 것인데, 이는 앞서도 언급했던 것처럼 연도에 따라 그룹별 평균을 내는 것에 지나지 않습니다.

다행히 R에는 날짜 정보를 쉽게 다루기 위한 패키지 lubridate이 있습니다. 해당 패키지는 다른 패키지들과 유사하게 다음과 같이 설치합니다.

install.packages("lubridate")

설치가 완료되면, 패키지를 불러옵니다.

library(lubridate)

우리가 지금까지 사용해 온 projects 테이블에서 날짜 정보, 즉, 프로젝트가 언제 만들어졌는가는 date_posted 컬럼에 포함되어 있습니다. 먼저, 이 칼럼의 데이터 타입을 확인해 볼까요?

str(projects$date_posted)
 Date[1:664098], format: "2014-05-12" "2014-05-12" "2014-05-11" "2014-05-11" "2014-05-11" ...

Date라는 데이터 타입을 가지고 있네요. 즉, R이 이미 이 컬럼에 담겨있는 값들을 날짜로 인식하고 있다는 것입니다. 하지만, 이는 운이 좋은 경우입니다. 많은 경우, R은 사람이 보기에는 날짜가 분명한 값들은 일반 문자라고 인식하는 경우가 많습니다. 이러한 “운이 덜 좋은” 경우를 가정하기 위해 다음과 같이 데이터 타입을 바꾸어 줍시다.

projects <- projects |>
    mutate(date_posted = as.character(date_posted))
str(projects$date_posted)
 chr [1:664098] "2014-05-12" "2014-05-12" "2014-05-11" "2014-05-11" ...

이제 데이터 타입이 문자(chr)로 바뀐 것이 보이지요? 이렇게 되면, 날짜 사이의 간격을 계산할 수도, 날짜에서 연도만 추출할 수도 없을것입니다. 하지만, lubridate의 기능을 이용하면 언제든지 이를 다시 날짜 타입으로 바꾸어 줄 수 있습니다.

문자를 날짜로 인식시켜주기 위한 함수는 여러개가 있는데, 이는 날짜를 표현하는 포맷에 따라 달라집니다. 위의 경우에는 “년(year)-월(month)-일(day)”의 순서대로 표시하는 방식이니 해당 함수의 이름은 ymd() 입니다.

projects <- projects |>
    mutate(date_posted = ymd(date_posted))
str(projects$date_posted)
 Date[1:664098], format: "2014-05-12" "2014-05-12" "2014-05-11" "2014-05-11" "2014-05-11" ...

이제 다시 데이터 타입이 Date으로 돌아왔습니다. 만약 날짜를 표시하는 방식이 “2014-05-11”이 아니고, “May 11, 2014” 였다면, 해당 함수는 mdy()겠지요. 영국식으로 “11/5/2014” 였다면, dmy()였을 테고요.

Rdate_posted를 날짜로 인식하고 있기 때문에, 여기서 연도를 추출하는 것도 어렵지 않습니다.

projects |> 
    mutate(year = year(date_posted)) |>
    select(year, date_posted)
# A tibble: 664,098 × 2
    year date_posted
   <dbl> <date>     
 1  2014 2014-05-12 
 2  2014 2014-05-12 
 3  2014 2014-05-11 
 4  2014 2014-05-11 
 5  2014 2014-05-11 
 6  2014 2014-05-11 
 7  2014 2014-05-11 
 8  2014 2014-05-11 
 9  2014 2014-05-11 
10  2014 2014-05-11 
# ℹ 664,088 more rows

이제 year 칼럼을 이용해 연도별 평균 프로젝트 목표액을 구해보도록 하죠.

projects |> 
    mutate(year = year(date_posted)) |>
    group_by(year) |>
    summarise(avg_size = mean(total_price_excluding_optional_support))
# A tibble: 13 × 2
    year avg_size
   <dbl>    <dbl>
 1  2002     609.
 2  2003     952.
 3  2004     435.
 4  2005     641.
 5  2006     693.
 6  2007     547.
 7  2008     443.
 8  2009     625.
 9  2010     484.
10  2011     478.
11  2012     516.
12  2013     587.
13  2014     626.

연도별 평균을 주(state)에 따라 따로 계산할 수도 있을까요? 물론입니다. group_by()를 두 변수에 대해서 해 주면 되지요.

projects |> 
    mutate(year = year(date_posted)) |>
    group_by(school_state, year) |>
    summarise(avg_size = mean(total_price_excluding_optional_support))
`summarise()` has grouped output by 'school_state'. You can override using the
`.groups` argument.
# A tibble: 455 × 3
# Groups:   school_state [52]
   school_state  year avg_size
   <chr>        <dbl>    <dbl>
 1 AK            2007     380.
 2 AK            2008     414.
 3 AK            2009     412.
 4 AK            2010     419.
 5 AK            2011     442.
 6 AK            2012     462.
 7 AK            2013     577.
 8 AK            2014     522.
 9 AL            2003     339.
10 AL            2004     546.
# ℹ 445 more rows

이렇게 다양한 방식으로 요약한 결과를 그래프로 표현하면, 그것이 바로 시각화 입니다. 드디어 여러분은 데이터 시각화를 이용할 준비가 되었습니다.