제 6강 : 깔끔한 데이터

데이터과학 입문

원중호

서울대학교 통계학과

April 2024

시작하기 전에

다음의 패키지가 설치되어 있지 않으면 설치한다.

# install.packages("tidyverse")
# install.packages("mdsr")
# install.packages("googlesheets4")
# install.packages("babynames")
# install.packages("rvest")
library(tidyverse)
library(mdsr)
library(googlesheets4)
library(babynames)
library(rvest)

깔끔한 데이터 (tidy data)

Gapminder HIV 데이터

  • 비영리단체인 Gapminder에서 정리한 국가별, 연도별 15–49세 사이 인구의 HIV 유병률에 대한 데이터를 이용하여 미국, 프랑스, 남아프리카 공화국의 1979년부터 2009년까지 매 10년마다의 HIV 유병률을 살펴보고자 한다.

  • 데이터는 구글 스프레드시트로 클라우드 상에 저장되어 있고, 접근을 위한 인증이 필요하다. 이를 위해 googlesheets4 패키지를 사용한다.

hiv_key <- "1kWH_xdJDM4SMfT_Kzpkk-1yuxWChfurZuWYjfmv51EA"
hiv <- googlesheets4::read_sheet(hiv_key) %>%
  rename(Country = 1) %>%
  filter(
    Country %in% c("United States", "France", "South Africa")
  ) %>%
  select(Country, `1979`, `1989`, `1999`, `2009`) %>%
  unnest(cols = c(`2009`)) %>%   ### more on unnest() later
  mutate(across(matches("[0-9]"), as.double))
hiv
# A tibble: 3 × 5
  Country        `1979` `1989` `1999` `2009`
  <chr>           <dbl>  <dbl>  <dbl>  <dbl>
1 France        NA          NA    0.3    0.4
2 South Africa  NA          NA   14.8   17.2
3 United States  0.0318     NA    0.5    0.6
  • 2차원 배열 형태
  • \(n = 3\) 행: 국가, \(p = 4\) 열: 연도
  • 각 항목은 \(i\)번 국가에 거주하는 15–49세 성인의 \(j\)번째 연도 HIV 감염 비율

스프레드시트형 자료표

  • 스프레드시트형의 장점
  1. 모든 데이터를 (화면이 충분히 크다면) 볼 수 있다.
  2. 특정 국가의 시간 경과에 따른 추세를 빠르게 추적할 수 있다.
  3. 누락된 데이터의 비율(예: NA)을 매우 쉽게 추정할 수 있다.
  • 시각적 검사가 주요 분석 기법이라면 스프레드시트형의 표현이 편리

긴 자료표

  • 같은 데이터의 다른 표현
hiv %>%
  pivot_longer(-Country, names_to = "Year", values_to = "hiv_rate")
# A tibble: 12 × 3
   Country       Year  hiv_rate
   <chr>         <chr>    <dbl>
 1 France        1979   NA     
 2 France        1989   NA     
 3 France        1999    0.3   
 4 France        2009    0.4   
 5 South Africa  1979   NA     
 6 South Africa  1989   NA     
 7 South Africa  1999   14.8   
 8 South Africa  2009   17.2   
 9 United States 1979    0.0318
10 United States 1989   NA     
11 United States 1999    0.5   
12 United States 2009    0.6   
  • \(np = 12\) 행, 3 열
  • 단점: 스프레드시트형보다 유병률의 변화 등을 시각적으로 알아차리기 어려움

  • 장점

  1. 효율성: 컴퓨터가 데이터를 저장하고 검색하는 데 더 효율적
  2. 확장성: 다른 질병의 유병률도 함께 보고 싶은 경우, 열을 하나만 추가하면 됨 + 스프레드시트형이라면 자료표가 하나 더 필요, 혹은 항목이 벡터

원시 데이터와 자료분석의 분리

  • 데이터 집합이 작을 때는 모든 데이터를 한 번에 볼 수 있는 것이 유용
  • 그러나 거대자료(big data) 시대에는 스프레드시트 표현에서 모든 데이터를 한 번에 보려는 것은 어리석은 일
  • 프로그래밍을 통해 데이터를 관리하면 MS 엑셀 등의 클릭 앤 드래그 패러다임에서 벗어나 임의 크기의 데이터로 작업할 수 있고 오류를 줄일 수 있음
  • 데이터 관리 작업을 코드로 기록하면 재현이 가능 — 협업의 시대에 점점 더 필요

코드를 통한 자료분석 프로세스

  • 항상 원시 데이터와 분석 코드를 별도의 파일에 보관하라.
  • 수정되지 않은 데이터 파일(오류 및 문제 포함)을 저장하고 원시 데이터를 실제로 분석할 데이터로 변환하는 스크립트 파일을 사용하여 수정하라.
  • 이 프로세스를 통해 데이터의 출처를 유지하고 데이터 랭글링을 처음부터 다시 시작하지 않고도 새 데이터로 분석을 업데이트할 수 있다.

깔끔한 데이터

Gapminder HIV 데이터의 긴 자료표 형식을 깔끔한 데이터(tidy data)라고 부른다.

  • 복잡한 데이터를 다루기 위해서는 간단한 일을 하는 표준적인 도구들을 순차적으로 적용하는 것이 효과적

  • 깔끔한 데이터: 표준 도구가 적용될 수 있도록 단순하지만 정확하게 정의된 패턴으로 정렬된 데이터

예제: babynames 데이터

Table 1: A data table showing how many babies were given each name in each year in the United States, for a few names.
year sex name n
1999 M Kavon 104
1984 F Somaly 6
2017 F Dnylah 8
1918 F Eron 6
1992 F Arleene 5
1977 F Alissia 5
1919 F Bular 10
  • 행: 사례 혹은 관측치. 특정하고 고유하며 유사한 유형의 것을 가리킴 (예: 이름이 Somaly인 1984년생 여자아이)

  • 열: 변수. 각 행마다 동일한 종류의 값을 가짐 (예: n – 신생아 수, sex – 성별)

  • 데이터가 깔끔한 형태로 정리되어 있으면 흥미로운 질문에 답하는 데 더 유용한 배열로 데이터를 비교적 간단하게 변환할 수 있음.

연도를 통틀어 가장 인기있는 이름?

popular_names <- babynames %>% 
  group_by(sex, name) %>%
  summarize(total_births = sum(n)) %>% 
  arrange(desc(total_births))

데이터 랭글링

  • 자료표에 내재된 정보를 명시적으로 드러내는 다른 자료표로 변환하는 과정

  • 깔끔한 데이터에 “data verb”를 적용하여 다른 형태의 깔끔한 데이터로 변환함으로써 실행 (4, 5장)

깔끔하지 않은 데이터

Figure 1: Ward and precinct votes cast in the 2013 Minneapolis mayoral election

깔끔한 데이터의 법칙

  1. 각 행(사례)은 동일한 기본 속성, 즉 같은 종류의 것을 나타내야 한다.
  • Figure 1 표의 대부분에서 행은 단일 투표소(precint)를 나타내나 어떤 행은 선거구(ward) 또는 시 전체 합계를 나타냄.
  • 처음 두 행은 사례가 아니라 데이터를 설명하는 캡션
  1. 각 열은 각 사례에 대해 동일한 유형의 값을 포함하는 변수
  • Figure 1 표의 대부분에서 그렇지만 변수가 아닌 레이블로 인해 깔끔한 패턴이 방해받음
  • 15행의 첫 두 항목은 첫 번째 열의 대부분 값인 선거구/투표소 식별자와는 다른 “Ward 1 Subtotal”라는 레이블
  • 정돈된 데이터에 대한 규칙을 준수하면 데이터의 요약, 분석이 간단해짐.

  • babynames 자료표(깔끔)에서 컴퓨터로 총 아기 수를 찾으려면 변수 n에 있는 모든 숫자를 더하면 됨.

  • 표본 크기는 행을 세기만 하면 됨.

  • Minneapolis 선거 데이터(안 깔끔)에서는 총 투표자 수?

  • Figure 1 의 I열(“Total Ballots Cast”)에 있는 숫자를 더하면 일부 행에 사례가 아닌 요약이 포함되어 있으므로 결과는 실제 투표자 수의 3배

깔끔한 Minneapolis 선거 데이터

neat <- mdsr::Elections %>%
  mutate(Ward = factor(Ward)) %>%
  mutate(Precinct = factor(Precinct, 
                        levels = c("1","1C","2","2D","3","3A","4","4D",
                                 "5","5A","6","6C","7","8","9","10"))) %>%
  select(1,2,6,7,8,10)
names(neat) <- c("ward", "precinct","registered","voters","absentee","total_turnout")
Table 3: A selection from the Minneapolis election data in tidy form.
ward precinct registered voters absentee total_turnout
1 1 28 492 27 0.2723
1 4 29 768 26 0.3662
1 7 47 291 8 0.1579
2 1 63 1011 39 0.3642
2 4 53 117 3 0.0734
2 7 39 138 7 0.1378
2 10 87 196 5 0.0691
3 3 71 893 101 0.3735
3 6 102 927 71 0.3531

다른 형태로 변환이 용이

ggplot(data = neat, aes(x = ward, y = 100 * total_turnout)) + 
  geom_jitter(width = 0.05, alpha = 0.5) + ylim(0,55) + 
  ylab("Voter Turnout (%)") + xlab("Ward")

A graphical depiction of voter turnout by precinct in the different wards.

  • 각 선거구 내 투표율을 각 선거구별로 표시하여 선거구 내 및 선거구 간에 얼마나 많은 편차가 있는지 쉽게 확인

깔끔한 Minneapolis 선거 데이터

사례와 그 의미

  • 깔끔한 자료표의 한 행은 하나의 사례를 나타낸다.

  • 현실 세계에서 사례가 무엇을 의미하는가?

mdsr::Minneapolis2013[c(6,2,10,5,27), ] %>%
  as_tibble() %>%
  mdsr_table(caption = "Individual ballots in the Minneapolis election. Each voter votes in one precinct within one ward. The ballot marks the voter's first three choices for mayor.") %>%
  kableExtra::column_spec(2:4, width = "9em")
Table 4: Individual ballots in the Minneapolis election. Each voter votes in one precinct within one ward. The ballot marks the voter's first three choices for mayor.
Precinct First Second Third Ward
P-04 undervote undervote undervote W-6
P-06 BOB FINE MARK ANDREW undervote W-10
P-02D NEAL BAXTER BETSY HODGES DON SAMUELS W-7
P-01 DON SAMUELS undervote undervote W-5
P-03 CAM WINTON DON SAMUELS OLE SAVIOR W-1
  • Table 4 에서는 한 행이 개별 투표지를 나타냄

  • Table 3 에서는 한 행이 (선거구, 투표소) 조합을 나타냄

  • babynames 데이터: Table 1 에서는 (이름, 성별, 생년), Table 2 에서는 (이름, 성별) 조합이 사례 하나

어떤 설명이 모든 사례를 고유하게 만드는가?

  • 투표 요약 자료표에서 투표소는 사례를 고유하게 식별하지 않음

  • 같은 투표소는 여러 행에 걸쳐 나타남

  • 투표소-선거구 조합은 단 한 번만 나타남

  • 마찬가지로 Table 1 에서 이름과 성별은 사례를 유일하게 지정하지 않음

  • 이름-성별-생년의 조합이 있어야 행을 유일하게 식별

사례 식별 예시

Cherry Blossom 10 Miler, Washington D.C., 1999–2008

data(mdsr::Cherry)
runners <- Cherry
mdsr_table(runners[15996:16010, c(1,5,2,6,4)], caption = "An excerpt of runners' performance over time in a 10-mile race.") %>%
  kableExtra::column_spec(1, width = "10em") %>%
  kableExtra::column_spec(2, width = "3em")
Table 5: An excerpt of runners' performance over time in a 10-mile race.
name.yob sex age year gun
jane polanek 1974 F 32 2006 114.50000
jane poole 1948 F 55 2003 92.71667
jane poole 1948 F 56 2004 87.28333
jane poole 1948 F 57 2005 85.05000
jane poole 1948 F 58 2006 80.75000
jane poole 1948 F 59 2007 78.53333
jane schultz 1964 F 35 1999 91.36667
jane schultz 1964 F 37 2001 79.13333
jane schultz 1964 F 38 2002 76.83333
jane schultz 1964 F 39 2003 82.70000
jane schultz 1964 F 40 2004 87.91667
jane schultz 1964 F 41 2005 91.46667
jane schultz 1964 F 42 2006 88.43333
jane smith 1952 F 47 1999 90.60000
jane smith 1952 F 49 2001 97.86667
  • 각 사례는 경주에 참여한 사람에 대응되는 것으로 것으로 보인다.
  • 그러나 자세히 보면, 1948년생 Jane Poole이 여러 행에 걸쳐 번 나타남을 알 수 있다. 즉, 각 행은 경주에 참여한 사람과 참여한 해의 조합으로 식별된다.

코드북 (codebooks)

  • 데이터에 대해 자세히 설명된 문서
  • 자료표에는 각 행을 고유하게 만드는 요소를 파악하는 데 필요한 모든 정보가 반드시 표시되지는 않음
  • 데이터가 어떻게 수집되었는지, 각 행은 어떤 열들의 조합을 통해 고유하게 식별되는지 등에 대한 설명을 포함
help(Cherry)
  • gun 변수는 무엇을 뜻하는가?

다수의 자료표

  • 분석과 관련된 정보를 포함하지만 사례의 종류가 다른 여러 개의 깔끔한 자료표는 5장에서 배운 inner_join()left_join() 함수를 사용하여 결합, 새로운 깔끔한 자료표를 만들 수 있음

데이터 모양 바꾸기

넓게, 좁게

혈압 데이터

BP_wide
# A tibble: 3 × 3
  subject before after
  <chr>    <dbl> <dbl>
1 BHO        160   115
2 GWB        120   135
3 WJC        105   145
  • 사례는 환자
  • 스트레스에 노출되기 전과 후의 수축기 혈압(SBP)을 측정한 별도의 변수가 있음
BP_narrow
# A tibble: 6 × 3
  subject when     sbp
  <chr>   <chr>  <dbl>
1 BHO     before   160
2 GWB     before   120
3 WJC     before   105
4 BHO     after    115
5 GWB     after    135
6 WJC     after    145
  • 사례는 혈압 측정을 위한 개별적인 상황 (환자-스트레스전/후)
  • 넓은 자료표는 스트레스 전후 혈압 차이를 보기 좋음
BP_wide %>%
  mutate(change = after - before)
# A tibble: 3 × 4
  subject before after change
  <chr>    <dbl> <dbl>  <dbl>
1 BHO        160   115    -45
2 GWB        120   135     15
3 WJC        105   145     40
  • 좁은 자료표는 이완기 혈압(DBP) 등 새로운 변수를 추가하기 쉬움
BP_full
# A tibble: 9 × 5
   ...1 subject when     sbp   dbp
  <dbl> <chr>   <chr>  <dbl> <dbl>
1     1 BHO     before   160    69
2     2 GWB     before   120    54
3     3 BHO     before   155    65
4     4 WJC     after    145    75
5     5 WJC     after     NA    65
6     6 WJC     after    130    50
7     7 GWB     after    135    NA
8     8 WJC     before   105    60
9     9 BHO     after    115    78
  • 환자 WJC에 대한 여러 개의 “after” 측정값 – 반복측정자료

넓게 <–> 좁게

pivot_wider()

BP_narrow %>% 
  pivot_wider(names_from = when, values_from = sbp)
# A tibble: 3 × 3
  subject before after
  <chr>    <dbl> <dbl>
1 BHO        160   115
2 GWB        120   135
3 WJC        105   145
  • names_from: 넓은 자료표에서 변수 이름으로 사용할 좁은 자료표의 변수

  • values_from: 넓은 자료표에서 변수의 값으로 사용할 좁은 자료표의 변수

  • BP_narrow에서 ‘when’ 열의 값(before/after)을 변수로, ‘sbp’ 열의 값을 새 변수의 값으로 사용

연습문제

다음 자료가 세 변수 (city, large, small)를 가지는 깔끔한 자료라면 어떻게 생겼을까?













pivot_wider(pollution, names_from = size, values_from = amount)


pivot_wider(pollution, names_from = size, values_from = amount)

pivot_longer()

BP_wide %>% 
  pivot_longer(-subject, names_to = "when", values_to = "sbp")
# A tibble: 6 × 3
  subject when     sbp
  <chr>   <chr>  <dbl>
1 BHO     before   160
2 BHO     after    115
3 GWB     before   120
4 GWB     after    135
5 WJC     before   105
6 WJC     after    145
  • 넓은 자료표에서 변수의 이름(before/after)은 수집되어(gather) 좁은 자료표의 범주형 변수 레벨이 됨
  • names_to: 이 범주형 변수의 이름을 지정해 주어야 하는데, when으로 지정
  • values_to: 수집되는 변수의 을 저장할 변수의 이름도 지정해야 하는데, sbp로 지정
  • 넓은 자료표에서 수집할 변수를 지정해야 하는데, subject는 제외

연습문제

다음 자료가 세 변수 (country, year, n)를 가지는 깔끔한 자료라면 어떻게 생겼을까?
















pivot_longer(cases, cols = 2:4, names_to = "year", values_to = "n")


pivot_longer(cases, cols = 2:4, names_to = "year", values_to = "n")


pivot_longer(cases, cols = 2:4, names_to = "year", values_to = "n")

항목이 리스트인 열 만들기

  • 개인별 노출 전후 수축기 혈압 평균
BP_full %>%
  group_by(subject, when) %>%
  summarize(mean_sbp = mean(sbp, na.rm = TRUE))
# A tibble: 6 × 3
# Groups:   subject [3]
  subject when   mean_sbp
  <chr>   <chr>     <dbl>
1 BHO     after      115 
2 BHO     before     158.
3 GWB     after      135 
4 GWB     before     120 
5 WJC     after      138.
6 WJC     before     105 
  • 요약하면서 개별 측정값 정보가 사라짐

모든 관측값을 포함하는 데이터 요약을 만들 수 있을까?

BP_summary <- BP_full %>%
  group_by(subject, when) %>%
  summarize(
    sbps = paste(sbp, collapse = ", "),
    dbps = paste(dbp, collapse = ", ")
  )
BP_summary
# A tibble: 6 × 4
# Groups:   subject [3]
  subject when   sbps         dbps      
  <chr>   <chr>  <chr>        <chr>     
1 BHO     after  115          78        
2 BHO     before 160, 155     69, 65    
3 GWB     after  135          NA        
4 GWB     before 120          54        
5 WJC     after  145, NA, 130 75, 65, 50
6 WJC     before 105          60        

평균 SBP 계산

BP_summary %>%
  mutate(mean_sbp = mean(parse_number(sbps)))
# A tibble: 6 × 5
# Groups:   subject [3]
  subject when   sbps         dbps       mean_sbp
  <chr>   <chr>  <chr>        <chr>         <dbl>
1 BHO     after  115          78             138.
2 BHO     before 160, 155     69, 65         138.
3 GWB     after  135          NA             128.
4 GWB     before 120          54             128.
5 WJC     after  145, NA, 130 75, 65, 50     125 
6 WJC     before 105          60             125 
  • sbps, dbps 항목이 문자열이기 때문에 평균이 제대로 계산되지 않음

tidyr::nest()

BP_nested <- BP_full %>%
  group_by(subject, when) %>%
  nest()
BP_nested
# A tibble: 6 × 3
# Groups:   subject, when [6]
  subject when   data            
  <chr>   <chr>  <list>          
1 BHO     before <tibble [2 × 3]>
2 GWB     before <tibble [1 × 3]>
3 WJC     after  <tibble [3 × 3]>
4 GWB     after  <tibble [1 × 3]>
5 WJC     before <tibble [1 × 3]>
6 BHO     after  <tibble [1 × 3]>
  • 데이터프레임에서 그룹화되지 않은 모든 변수를 tibble로 축소
  • 새로운 변수의 형은 list, 이름의 기본값은 data

List-columns

  • 데이터프레임에서 list 형 변수를 list-column이라고 부름
  • 데이터프레임은 길이가 같은 벡터의 list일 뿐이고, 그 벡터의 변수형은 임의
  • data 열은 tibble로 구성된 list형 벡터
  • tibble의 크기(data열의 항목)는 다를 수 있음
# BHO, before에 해당되는 data
BP_nested$data[[1]]
# A tibble: 2 × 3
   ...1   sbp   dbp
  <dbl> <dbl> <dbl>
1     1   160    69
2     3   155    65

dplyr::pull()

  • tibble의 특정 열을 가져올 수 있다.
BP_nested %>%
  mutate(sbp_list = pull(data, sbp))   
Error in `mutate()`:
ℹ In argument: `sbp_list = pull(data, sbp)`.
ℹ In group 1: `subject = "BHO"` and `when = "after"`.
Caused by error in `UseMethod()`:
! no applicable method for 'pull' applied to an object of class "list"
  • 왜 안 되는가?

purrr::map()

  • datatibble로 이루어진 list. tibble이 아님!
  • map()을 사용하면 data의 각 원소에 pull()을 적용할 수 있음 (7장)
BP_nested <- BP_nested %>%
  mutate(sbp_list = map(data, pull, sbp))
BP_nested
# A tibble: 6 × 4
# Groups:   subject, when [6]
  subject when   data             sbp_list 
  <chr>   <chr>  <list>           <list>   
1 BHO     before <tibble [2 × 3]> <dbl [2]>
2 GWB     before <tibble [1 × 3]> <dbl [1]>
3 WJC     after  <tibble [3 × 3]> <dbl [3]>
4 GWB     after  <tibble [1 × 3]> <dbl [1]>
5 WJC     before <tibble [1 × 3]> <dbl [1]>
6 BHO     after  <tibble [1 × 3]> <dbl [1]>
  • sbp_listdouble로 이루어진 list
  • 각 리스트의 길이는 같을 필요가 없음
BP_nested %>% 
  pluck("sbp_list")
[[1]]
[1] 160 155

[[2]]
[1] 120

[[3]]
[1] 145  NA 130

[[4]]
[1] 135

[[5]]
[1] 105

[[6]]
[1] 115
  • 이제 map()을 한 번 더 사용하여 SBP의 평균을 구할 수 있다.
BP_nested <- BP_nested %>%
  mutate(sbp_mean = map(sbp_list, mean, na.rm = TRUE))
BP_nested
# A tibble: 6 × 5
# Groups:   subject, when [6]
  subject when   data             sbp_list  sbp_mean 
  <chr>   <chr>  <list>           <list>    <list>   
1 BHO     before <tibble [2 × 3]> <dbl [2]> <dbl [1]>
2 GWB     before <tibble [1 × 3]> <dbl [1]> <dbl [1]>
3 WJC     after  <tibble [3 × 3]> <dbl [3]> <dbl [1]>
4 GWB     after  <tibble [1 × 3]> <dbl [1]> <dbl [1]>
5 WJC     before <tibble [1 × 3]> <dbl [1]> <dbl [1]>
6 BHO     after  <tibble [1 × 3]> <dbl [1]> <dbl [1]>

tidyr::unnest()

  • sbp_mean이 길이 1의 double형 리스트이므로, unnest()를 사용하여 list-column의 중첩 구조를 풀어줌
BP_nested %>%
  unnest(cols = c(sbp_mean))
# A tibble: 6 × 5
# Groups:   subject, when [6]
  subject when   data             sbp_list  sbp_mean
  <chr>   <chr>  <list>           <list>       <dbl>
1 BHO     before <tibble [2 × 3]> <dbl [2]>     158.
2 GWB     before <tibble [1 × 3]> <dbl [1]>     120 
3 WJC     after  <tibble [3 × 3]> <dbl [3]>     138.
4 GWB     after  <tibble [1 × 3]> <dbl [1]>     135 
5 WJC     before <tibble [1 × 3]> <dbl [1]>     105 
6 BHO     after  <tibble [1 × 3]> <dbl [1]>     115 

예시 : 중성적인 이름들

“Sue”(여자 이름)라는 이름을 가진 사람의 성별 분포

  babynames %>%
    filter(name == "Sue") %>%
    group_by(name, sex) %>%
    summarize(total = sum(n))
# A tibble: 2 × 3
# Groups:   name [1]
  name  sex    total
  <chr> <chr>  <int>
1 Sue   F     144465
2 Sue   M        519
  • 여자 아이가 남자 아이보다 300배 정도 많음

중성적인 이름 찾기

  • Sue, Robin, Leslie 중에서 가장 남녀 비율이 비슷한 이름?
  babynames %>%
    filter(name %in% c("Sue", "Robin", "Leslie")) %>%
    group_by(name, sex) %>%
    summarize(total = sum(n)) 
# A tibble: 6 × 3
# Groups:   name [3]
  name   sex    total
  <chr>  <chr>  <int>
1 Leslie F     266474
2 Leslie M     112689
3 Robin  F     289395
4 Robin  M      44616
5 Sue    F     144465
6 Sue    M        519
  • pivot_wider()로 대조를 쉽게
  babynames %>%
    filter(name %in% c("Sue", "Robin", "Leslie")) %>%
    group_by(name, sex) %>%
    summarize(total = sum(n)) %>%
    pivot_wider(
      names_from = sex, 
      values_from = total)
# A tibble: 3 × 3
# Groups:   name [3]
  name        F      M
  <chr>   <int>  <int>
1 Leslie 266474 112689
2 Robin  289395  44616
3 Sue    144465    519
  • 전체 이름으로 계산
  • 일부 이름에서 남자 또는 여자 아이가 아예 존재하지 않을 수 있음
  • NA로 기록되는 것을 방지하기 위해 values_fill = 0으로 지정
baby_wide <- babynames %>%
  group_by(sex, name) %>%
  summarize(total = sum(n)) %>%
  pivot_wider(
    names_from = sex, 
    values_from = total, 
    values_fill = 0
  )
head(baby_wide, 3)
# A tibble: 3 × 3
  name          F     M
  <chr>     <int> <int>
1 Aabha        35     0
2 Aabriella    32     0
3 Aada          5     0

가장 중성적인 이름

  • 남/녀 비율과 여/남 비율이 비슷 == 두 비율 중 작은 것이 1이 가까움
baby_wide %>% 
  filter(M > 50000, F > 50000) %>%
  mutate(ratio = pmin(M / F, F / M) ) %>% 
  arrange(desc(ratio)) %>%  # ratio를 기준으로 정렬
  head(3)
# A tibble: 3 × 4
  name        F      M ratio
  <chr>   <int>  <int> <dbl>
1 Riley  100881  92789 0.920
2 Jackie  90604  78405 0.865
3 Casey   76020 110165 0.690
  • Riley가 가장 남녀 비율이 비슷한 이름

명명 관습

변수 또는 함수명을 정할 때 고려해야할 사항

  • 숫자로 시작할 수 없다. 100NCHS (X), NCHS100 (O)
  • . 또는 _를 제외한 다른 특수기호들은 사용할 수 없다. ?NCHS (X), N*Hanes (X)
  • 함수의 이름에는 .을 사용하지 않는 것이 좋다. (_을 사용하라.)
  • R은 대문자와 소문자를 구별한다. NCHS, ncHs, nChs 등은 모두 다른 이름.
  • 다양한 사람들이 다양한 관습을 사용하여 코드를 작성
  • 스타일 가이드를 하나 정하고 이를 고수하는 것이 좋다.

Tidyverse 스타일 가이드

  1. 변수 및 함수명에는 밑줄(_)을 사용한다. 함수 이름에 마침표(.)를 사용하는 것은 S3 메소드로 제한한다.
  2. 공백을 자유롭게 사용하고 한 줄의 넓은 코드보다 여러 줄의 좁은 코드 블록을 선호한다.
  3. 변수 및 함수명에는 snake_case를 사용합니다. 즉, 각 단어는 소문자이며 공백은 없고 밑줄만 사용한다.
  • styler 패키지를 사용하여 코드를 tidyverse 스타일 가이드를 구현하는 형식으로 다시 포맷할 수 있다.

데이터 불러오기

웹 스크래핑

쉬운 데이터 형식은 모두 비슷하다. 모든 어려운 데이터 형식은 각자의 방식으로 어렵다.

  • 인터넷상의 데이터를 (구조화된) 텍스트로 처리하여 데이터로 변환하는 데이터 수집 형태

  • 데이터 입력의 실수나 저장 또는 코딩 방식의 결함으로 인해 발생하는 오류가 있는 경우가 많음.

  • 데이터 정제(data cleaning) – 이러한 오류를 수정하는 작업

R 기본 이진 파일 형식

  • .rda 또는 .RData 확장자로 구분
saveRDS(mtcars, file = "mtcars.rda", compress = TRUE)
mtcars <- readRDS("mtcars.rda")
  • 다른 프로그램으로 읽기 어려움

Note

  • 분석의 시작부터 끝까지 데이터의 출처를 유지하는 것은 재현가능 작업 흐름에 중요.
  • 데이터 랭글링을 수행하고 분석 데이터 집합을 생성(saveRDS() 사용)하여 두 번째 마크다운 파일에서 읽을 수 있는(readRDS() 사용) 하나의 마크다운 파일 또는 노트북을 만들면 편리.

자료표 형식

  • CSV (“comma-separated values”): 서로 다른 소프트웨어 패키지 간의 데이터 교환에 널리 사용되는 비독점적인 텍스트 형식.
    • 이해하기 쉬움
    • 압축되지 않으므로 다른 형식에 비해 디스크 공간을 더 많이 차지할 수 있음
  • 소프트웨어 패키지별 형식
    • MATLAB/Octave (.mat): 공학 및 물리학에서 널리 사용
    • Stata (.dta): 경제 연구에 주로 사용
    • SPSS (.sav): 사회과학 연구에 주로 사용
    • Minitab (.mtw): 비즈니스 애플리케이션에 자주 사용됨
    • SAS (.sas7bdat): 대규모 데이터 세트에 자주 사용
    • Epi Info: 미국 질병관리청(CDC)에서 건강 및 역학 데이터에 사용

  • 관계형 데이터베이스: 기관에서 활발하게 업데이트되는 대부분의 데이터가 저장되는 형태
    • 비즈니스 거래 기록, 정부 기록, 웹 로그 등
  • Excel (.xlsx): 비즈니스에서 많이 사용되는 독점 스프레드시트 형식.
    • 반드시 자료표 형식이 아닐 수도 있음 (Minneapolis 선거 자료)
  • 웹 관련
    • HTML (hypertext markup language): <table> 형식
    • XML (extensible markup language) 형식, 트리 기반 문서 구조
    • JSON (JavaScript Object Notation)은 “행과 열” 패러다임을 깨는 일반적인 데이터 형식
    • 구글 스프레드시트: HTML로 게시
    • 응용 프로그래밍 인터페이스(API)

CSV

"year","sex","name","n","prop"
1880,"F","Mary",7065,0.07238359
1880,"F","Anna",2604,0.02667896
1880,"F","Emma",2003,0.02052149
1880,"F","Elizabeth",1939,0.01986579
1880,"F","Minnie",1746,0.01788843
1880,"F","Margaret",1578,0.0161672
  • 맨 위 행에는 일반적으로(항상 그런 것은 아니지만) 변수 이름이 포함
  • 따옴표는 문자열의 시작과 끝에 자주 사용되는데, 문자열 내용의 일부가 아니지만 텍스트에 쉼표를 포함하려는 경우에 유용
  • 확장자: .csv, .txt, .dat
  • 쉼표 이외의 문자: 탭 문자 (\t, .tsv), |

CSV 읽기

  • base::read.csv(), readr::read_csv() (큰 파일에서 더 빠름)

  • CSV 파일은 로컬 하드 드라이브에 존재하지 않아도 됨:

mdsr_url <- "https://raw.githubusercontent.com/beanumber/mdsr/master/data-raw/"
houses <- mdsr_url %>%
  paste0("houses-for-sale.csv") %>%
  read_csv()
head(houses, 3)
# A tibble: 3 × 16
   price lot_size waterfront   age land_value construction air_cond  fuel  heat
   <dbl>    <dbl>      <dbl> <dbl>      <dbl>        <dbl>    <dbl> <dbl> <dbl>
1 132500     0.09          0    42      50000            0        0     3     4
2 181115     0.92          0     0      22300            0        0     2     3
3 109000     0.19          0   133       7300            0        0     2     3
# ℹ 7 more variables: sewer <dbl>, living_area <dbl>, pct_college <dbl>,
#   bedrooms <dbl>, fireplaces <dbl>, bathrooms <dbl>, rooms <dbl>
  • 코드의 재현성을 보장하려면 파일 경로를 구체적으로 지정하는 것이 중요

HTML

http://en.wikipedia.org/wiki/Mile_run_world_record_progression 에 있는 다음의 표를 불러오고 싶다.

Mile run world records

url <- "http://en.wikipedia.org/wiki/Mile_run_world_record_progression"
tables <- url %>% 
  rvest::read_html() %>% 
  html_nodes("table")

length(tables)
[1] 13
  • tables에는 해당 페이지 내에 있는 총 13개의 표가 포함되어있다.
  • purrrpluck() 함수를 사용하여 원하는 표를 꺼낼 수 있다.
Table 6
amateur <- tables %>%
  purrr::pluck(1) %>% # 첫번째 테이블
  html_table()

amateur
# A tibble: 11 × 5
   Time    Athlete          Nationality    Date              Venue     
   <chr>   <chr>            <chr>          <chr>             <chr>     
 1 4:28    Charles Westhall United Kingdom 26 July 1855      London    
 2 4:28    Thomas Horspool  United Kingdom 28 September 1857 Manchester
 3 4:23    Thomas Horspool  United Kingdom 12 July 1858      Manchester
 4 4:221⁄4 Siah Albison     United Kingdom 27 October 1860   Manchester
 5 4:213⁄4 William Lang     United Kingdom 11 July 1863      Manchester
 6 4:201⁄2 Edward Mills     United Kingdom 23 April 1864     Manchester
 7 4:20    Edward Mills     United Kingdom 25 June 1864      Manchester
 8 4:171⁄4 William Lang     United Kingdom 19 August 1865    Manchester
 9 4:171⁄4 William Richards United Kingdom 19 August 1865    Manchester
10 4:161⁄5 William Cummings United Kingdom 14 May 1881       Preston   
11 4:123⁄4 Walter George    United Kingdom 23 August 1886    London    

API

  • 응용 프로그램 인터페이스(application programming interface, API): 사용자가 제어할 수 없는 컴퓨터 프로그램과 상호 작용하기 위한 프로토콜

  • 텔레비전의 리모컨 설명서와 달리 “블랙박스”를 사용하기 위해 합의된 일련의 지침

  • 다양한 소스에서 웹 상의 거대한 공개 데이터에 접근할 수 있게 해줌.

  • 모든 API가 동일하지는 않지만, 이를 사용하는 방법을 익힘으로써 데이터를 수동으로 스크래핑하지 않고도 데이터를 효과적으로 끌어올 수 있다.

  • 예제: Google sheets, googlesheets4

데이터 정제

  • Table 6 의 표에서 TimeDate 변수는 문자열로 저장됨

  • 이 정보를 사용하려면 먼저 날짜 및 시간을 컴퓨터가 처리할 수 있는 형식으로 변환해야 함

  • 데이터 정제란 변수에 포함된 정보를 가져와서 해당 정보를 사용할 수 있는 형태로 변환하는 것을 말함

재코딩

  • CSV 파일 읽기에서 불러왔던 houses-for-sale.csv는 미국 뉴욕주 사라토가 지역의 1728개 주택 매물에 대한 데이터이다 (mdsr::saratoga_houses).
houses %>% 
  select(fuel, heat, sewer, construction) %>% 
  head(5) %>%
  mdsr_table(caption = "Four of the variables from the tables giving features of the Saratoga houses stored as integer codes. Each case is a different house.")
Four of the variables from the tables giving features of the Saratoga houses stored as integer codes. Each case is a different house.
fuel heat sewer construction
3 4 2 0
2 3 2 0
2 3 3 0
2 2 2 0
2 2 3 1
  • sewer, heat 등의 경우, 범주형임에도 불구하고 수치형로 표기됨
    • 숫자는 범주에 의미 있는 순서가 없음에도 잘못된 의미를 부여할 수 있음
  • 수치를 보다 유익한 코딩으로 변환하려면 먼저 다양한 코드가 무엇을 의미하는지 알아내야 함
    • 이 정보는 코드북에서 제공하는 경우가 많지만 때로는 데이터를 수집한 사람에게 문의해야 하는 경우도 있음
translations <- mdsr_url %>%
  paste0("house_codes.csv") %>%    ## mdsr::saratoga_codes
  read_csv()
translations %>% head(5)
# A tibble: 5 × 3
   code system_type meaning
  <dbl> <chr>       <chr>  
1     0 new_const   no     
2     1 new_const   yes    
3     1 sewer_type  none   
4     2 sewer_type  private
5     3 sewer_type  public 
codes <- translations %>%
  pivot_wider(
    names_from = system_type,
    values_from = meaning,
    values_fill = "invalid"
  )

codes %>%
  mdsr_table(caption = "The Translations data table rendered in a wide format.")
The Translations data table rendered in a wide format.
code new_const sewer_type central_air fuel_type heat_type
0 no invalid no invalid invalid
1 yes none yes invalid invalid
2 invalid private invalid gas hot air
3 invalid public invalid electric hot water
4 invalid invalid invalid oil electric
  • 이제 자료표를 결합해서 숫자를 범주로 바꾼다.
houses <- houses %>%
  left_join(
    codes %>% select(code, fuel_type), 
    by = c(fuel = "code")
  ) %>%
  left_join(
    codes %>% select(code, heat_type), 
    by = c(heat = "code")
  ) %>%
  left_join(
    codes %>% select(code, sewer_type), 
    by = c(sewer = "code")
  )
The Saratoga houses data with re-coded categorical variables.
fuel_type heat_type sewer_type
electric electric private
gas hot water private
gas hot water public
gas hot air private
gas hot air public
gas hot air private

문자열을 수치형으로

  • 의미는 수치형이지만 표현은 문자열인 변수가 있는 자료표가 종종 있음.
    • 하나 이상의 사례에 숫자가 아닌 값이 주어질 때 발생

ordway_birds 데이터

  • 미국 미네소타 주의 Katharine Ordway Natural History Study Area 지역에서 잡았다 놓아준 새들에 대한 데이터
mdsr::ordway_birds %>% 
  select(Timestamp, Year, Month, Day) %>% 
  glimpse()
Rows: 15,829
Columns: 4
$ Timestamp <chr> "4/14/2010 13:20:56", "", "5/13/2010 16:00:30", "5/13/2010 1…
$ Year      <chr> "1972", "", "1972", "1972", "1972", "1972", "1972", "1972", …
$ Month     <chr> "7", "", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", "…
$ Day       <chr> "16", "", "16", "16", "16", "16", "16", "16", "16", "16", "1…
  • Year, Month, Day가 문자열
  • parse_number()
ordway_birds <- ordway_birds %>%
  mutate(
    Month = parse_number(Month), 
    Year = parse_number(Year),
    Day = parse_number(Day)
  )

ordway_birds %>% 
  select(Timestamp, Year, Month, Day) %>% 
  glimpse()
Rows: 15,829
Columns: 4
$ Timestamp <chr> "4/14/2010 13:20:56", "", "5/13/2010 16:00:30", "5/13/2010 1…
$ Year      <dbl> 1972, NA, 1972, 1972, 1972, 1972, 1972, 1972, 1972, 1972, 19…
$ Month     <dbl> 7, NA, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,…
$ Day       <dbl> 16, NA, 16, 16, 16, 16, 16, 16, 16, 16, 17, 18, 18, 18, 18, …
  • 빈 문자열(예: "")이 자동으로 NA로 변환되는 방식에 유의

날짜

  • 날짜는 문자열로 기록되는 경우가 많음(예: 29 October 2014).

    • ordway_birds 데이터에서 Timestamp 변수: 데이터가 원래의 실험 노트에서 컴퓨터 파일로 기록된 시간
  • 날짜의 중요한 속성: 자연스러운 순서. 16 December 2015 < 29 October 2016

  • 문자열로 저장된 날짜가 주어지면 일반적으로 날짜를 위해 특별히 고안된 데이터 유형으로 변환해야 함 (lubridate 패키지)

lubridate::mdy_hms()

birds <- ordway_birds %>% 
  mutate(When = mdy_hms(Timestamp)) %>% 
  select(Timestamp, Year, Month, Day, When, DataEntryPerson)
birds %>% 
  glimpse()
Rows: 15,829
Columns: 6
$ Timestamp       <chr> "4/14/2010 13:20:56", "", "5/13/2010 16:00:30", "5/13/…
$ Year            <dbl> 1972, NA, 1972, 1972, 1972, 1972, 1972, 1972, 1972, 19…
$ Month           <dbl> 7, NA, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,…
$ Day             <dbl> 16, NA, 16, 16, 16, 16, 16, 16, 16, 16, 17, 18, 18, 18…
$ When            <dttm> 2010-04-14 13:20:56, NA, 2010-05-13 16:00:30, 2010-05…
$ DataEntryPerson <chr> "Jerald Dosch", "Caitlin Baker", "Caitlin Baker", "Cai…
  • month/day/year hour:minute:second 형태의 문자열을 날짜-시간형(dttm)으로 변환
  • 변환으로 인해 기록자가 일한 기간을 시각화하기 편리
birds %>% 
  ggplot(aes(x = When, y = DataEntryPerson)) + 
  geom_point(alpha = 0.1, position = "jitter") 
  • 일한 기간: first(), last(), interval()
bird_summary <- birds %>% 
  group_by(DataEntryPerson) %>% 
  summarize(
    start = first(When), 
    finish = last(When)
  ) %>%
  mutate(duration = interval(start, finish) / ddays(1))
  • 다른 lubridate 함수들: ymd(), dmy(), hour(), yday()
DataEntryPerson start finish duration
NA NA NA
Abby Colehour 2011-04-23 15:50:24 2011-04-23 15:50:24 0.0000000
Brennan Panzarella 2010-09-13 10:48:12 2011-04-10 21:58:56 209.4657870
Caitlin Baker NA 2010-05-28 19:41:52 NA
Emily Merrill 2010-06-08 09:10:01 2010-06-08 14:47:21 0.2342593
Jerald Dosch 2010-04-14 13:20:56 2010-04-14 13:20:56 0.0000000
Jolani Daney 2010-06-08 09:03:00 2011-05-03 10:12:59 329.0485995
Keith Bradley-Hewitt 2010-09-21 11:31:02 2011-05-06 17:36:38 227.2538889
Mary Catherine Muñiz 2012-02-02 08:57:37 2012-04-30 14:06:27 88.2144676

R 내부의 시간 표현

  • 내부적으로 R은 날짜-시간을 나타내기 위해 POSIXctPOSIXlt 두 클래스를 사용

  • 대부분의 경우 이 두 클래스는 동일한 것으로 취급할 수 있지만 내부적으로는 서로 다르게 저장됨

    • POSIXct 객체는 UNIX 시대 (1970-01-01) 이후 경과한 시간을 단위로 저장
    • POSIXlt 객체는 연도, 월, 일 등의 문자열 목록으로 저장
now()
[1] "2024-04-09 13:37:33 KST"
class(now())
[1] "POSIXct" "POSIXt" 
str(unclass(as.POSIXlt(now())))
List of 11
 $ sec   : num 33.7
 $ min   : int 37
 $ hour  : int 13
 $ mday  : int 9
 $ mon   : int 3
 $ year  : int 124
 $ wday  : int 2
 $ yday  : int 99
 $ isdst : int 0
 $ zone  : chr "KST"
 $ gmtoff: int 32400
 - attr(*, "tzone")= chr [1:3] "" "KST" "KDT"
 - attr(*, "balanced")= logi TRUE
  • 시간을 포함하지 않는 날짜형은 Date
as.Date(now())
[1] "2024-04-09"

예시

날짜-시간 연산

example <- c("2021-04-29 06:00:00", "2021-12-31 12:00:00")
str(example)
 chr [1:2] "2021-04-29 06:00:00" "2021-12-31 12:00:00"
converted <- lubridate::ymd_hms(example)
str(converted)
 POSIXct[1:2], format: "2021-04-29 06:00:00" "2021-12-31 12:00:00"
converted
[1] "2021-04-29 06:00:00 UTC" "2021-12-31 12:00:00 UTC"
converted[2] - converted[1]
Time difference of 246.25 days

요인? 문자열?

  • 요인형(factor)는 범주형 데이터를 나타내는 데 사용되는 특별한 데이터형

  • 부작용: 문자열로 잘못 인식하기 쉬움. 수치형이나 날짜 형식으로 변환할 때 다르게 동작

  • readr::read_csv()는 문자열을 요인형이 아닌 문자열형(chr)으로 해석

  • base::read.csv() (R 4.0 이전)은 문자열을 기본적으로 요인형으로 변환

  • 이러한 데이터를 정제하려면 종종 parse_character()를 사용하여 다시 문자 형식으로 변환해야 함

    • 이를 놓치면 완전히 잘못된 결과를 얻을 수 있으니 주의
  • 부작용을 피하기 위해 교재에서 사용된 자료표는 범주형 또는 텍스트 데이터를 모두 문자열형으로 저장

Note

  • 다른 패키지에서 제공하는 데이터는 반드시 이 규칙을 따르지 않는다는 점에 유의하라.
  • 항상 모든 변수와 데이터 랭글링 작업을 주의 깊게 확인하여 올바른 값이 생성되는지 확인하는 것이 좋다.

일본 원자력발전소 자료

https://en.wikipedia.org/wiki/List_of_commercial_nuclear_reactors

# 데이터 받아오기
tables <- "https://en.wikipedia.org/wiki/List_of_commercial_nuclear_reactors" %>%
  read_html() %>% 
  html_nodes(css = "table")

# 'Fukushima Daiichi'가 포함된 표의 번호
idx <- tables %>%
  html_text() %>%
  str_detect("Fukushima Daiichi") %>%
  which()
# 해당 표를 가져온 후 열 이름을 변경
reactors <- tables %>%
  purrr::pluck(idx) %>%
  html_table(fill = TRUE) %>%
  janitor::clean_names() %>%
  rename(
    name = plantname,
    reactor_type = type,
    reactor_model = model,
    capacity_net = capacity_mw,
    construction_start = beginbuilding,
    commercial_operation = commercialoperation,
    closure = closed
  ) %>%
  tail(-1)
glimpse(reactors)
Rows: 61
Columns: 9
$ name                 <chr> "Fukushima Daiichi", "Fukushima Daiichi", "Fukush…
$ unit_no              <int> 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3…
$ reactor_type         <chr> "BWR", "BWR", "BWR", "BWR", "BWR", "BWR", "BWR", …
$ reactor_model        <chr> "BWR-3", "BWR-4", "BWR-4", "BWR-4", "BWR-4", "BWR…
$ status               <chr> "Inoperable", "Inoperable", "Inoperable", "Inoper…
$ capacity_net         <int> 439, 760, 760, 760, 760, 1067, 1067, 1067, 1067, …
$ construction_start   <chr> "25 Jul 1967", "9 Jun 1969", "28 Dec 1970", "12 F…
$ commercial_operation <chr> "26 Mar 1971", "18 Jul 1974", "27 Mar 1976", "12 …
$ closure              <chr> "19 May 2011", "19 May 2011", "19 May 2011", "19 …
  • 원자력 기술이 발전함에 따라 발전소의 용량이 증가했을 가능성이 높음
  • 최근 몇 년 동안 원자로 상당수가 폐쇄됨. 발전소의 수명과 관련된 용량 변화가 있을까?

mutate()lubridate::dmy()를 사용하여 랭글링

reactors <- reactors %>% 
  mutate(
    plant_status = ifelse(
      str_detect(status, "Shut down"), 
      "Shut down", "Not formally shut down"
    ), 
    construct_date = dmy(construction_start), 
    operation_date = dmy(commercial_operation), 
    closure_date = dmy(closure)
  )
glimpse(reactors)
Rows: 61
Columns: 13
$ name                 <chr> "Fukushima Daiichi", "Fukushima Daiichi", "Fukush…
$ unit_no              <int> 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3…
$ reactor_type         <chr> "BWR", "BWR", "BWR", "BWR", "BWR", "BWR", "BWR", …
$ reactor_model        <chr> "BWR-3", "BWR-4", "BWR-4", "BWR-4", "BWR-4", "BWR…
$ status               <chr> "Inoperable", "Inoperable", "Inoperable", "Inoper…
$ capacity_net         <int> 439, 760, 760, 760, 760, 1067, 1067, 1067, 1067, …
$ construction_start   <chr> "25 Jul 1967", "9 Jun 1969", "28 Dec 1970", "12 F…
$ commercial_operation <chr> "26 Mar 1971", "18 Jul 1974", "27 Mar 1976", "12 …
$ closure              <chr> "19 May 2011", "19 May 2011", "19 May 2011", "19 …
$ plant_status         <chr> "Not formally shut down", "Not formally shut down…
$ construct_date       <date> 1967-07-25, 1969-06-09, 1970-12-28, 1973-02-12, …
$ operation_date       <date> 1971-03-26, 1974-07-18, 1976-03-27, 1978-10-12, …
$ closure_date         <date> 2011-05-19, 2011-05-19, 2011-05-19, 2011-05-19, …
ggplot(
  data = reactors, 
  aes(x = construct_date, y = capacity_net, color = plant_status
  )
) +
  geom_point() + 
  geom_smooth() + 
  xlab("Date of Plant Construction") + 
  ylab("Net Plant Capacity (MW)")
  • 실제로 원자로 용량은 시간이 지남에 따라 증가하는 경향이 있는 반면, 오래된 원자로는 공식적으로 폐쇄되었을 가능성이 더 높음

  • 이 데이터는 수작업으로 코딩하는 것이 간단했지만, 더 크고 복잡한 테이블에 대해서는 데이터 수집을 자동화하는 것이 더 효율적이고 오류 발생 가능성이 적음