# 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)
제 6강 : 깔끔한 데이터
데이터과학 입문
시작하기 전에
다음의 패키지가 설치되어 있지 않으면 설치한다.
깔끔한 데이터 (tidy data)
Gapminder HIV 데이터
비영리단체인 Gapminder에서 정리한 국가별, 연도별 15–49세 사이 인구의 HIV 유병률에 대한 데이터를 이용하여 미국, 프랑스, 남아프리카 공화국의 1979년부터 2009년까지 매 10년마다의 HIV 유병률을 살펴보고자 한다.
데이터는 구글 스프레드시트로 클라우드 상에 저장되어 있고, 접근을 위한 인증이 필요하다. 이를 위해
googlesheets4
패키지를 사용한다.
<- "1kWH_xdJDM4SMfT_Kzpkk-1yuxWChfurZuWYjfmv51EA"
hiv_key <- googlesheets4::read_sheet(hiv_key) %>%
hiv rename(Country = 1) %>%
filter(
%in% c("United States", "France", "South Africa")
Country %>%
) 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 감염 비율
스프레드시트형 자료표
- 스프레드시트형의 장점
- 모든 데이터를 (화면이 충분히 크다면) 볼 수 있다.
- 특정 국가의 시간 경과에 따른 추세를 빠르게 추적할 수 있다.
- 누락된 데이터의 비율(예:
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 열
단점: 스프레드시트형보다 유병률의 변화 등을 시각적으로 알아차리기 어려움
장점
- 효율성: 컴퓨터가 데이터를 저장하고 검색하는 데 더 효율적
- 확장성: 다른 질병의 유병률도 함께 보고 싶은 경우, 열을 하나만 추가하면 됨 + 스프레드시트형이라면 자료표가 하나 더 필요, 혹은 항목이 벡터
원시 데이터와 자료분석의 분리
- 데이터 집합이 작을 때는 모든 데이터를 한 번에 볼 수 있는 것이 유용
- 그러나 거대자료(big data) 시대에는 스프레드시트 표현에서 모든 데이터를 한 번에 보려는 것은 어리석은 일
- 프로그래밍을 통해 데이터를 관리하면 MS 엑셀 등의 클릭 앤 드래그 패러다임에서 벗어나 임의 크기의 데이터로 작업할 수 있고 오류를 줄일 수 있음
- 데이터 관리 작업을 코드로 기록하면 재현이 가능 — 협업의 시대에 점점 더 필요
코드를 통한 자료분석 프로세스
- 항상 원시 데이터와 분석 코드를 별도의 파일에 보관하라.
- 수정되지 않은 데이터 파일(오류 및 문제 포함)을 저장하고 원시 데이터를 실제로 분석할 데이터로 변환하는 스크립트 파일을 사용하여 수정하라.
- 이 프로세스를 통해 데이터의 출처를 유지하고 데이터 랭글링을 처음부터 다시 시작하지 않고도 새 데이터로 분석을 업데이트할 수 있다.
깔끔한 데이터
Gapminder HIV 데이터의 긴 자료표 형식을 깔끔한 데이터(tidy data)라고 부른다.
복잡한 데이터를 다루기 위해서는 간단한 일을 하는 표준적인 도구들을 순차적으로 적용하는 것이 효과적
깔끔한 데이터: 표준 도구가 적용될 수 있도록 단순하지만 정확하게 정의된 패턴으로 정렬된 데이터
예제: babynames
데이터
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
– 성별)데이터가 깔끔한 형태로 정리되어 있으면 흥미로운 질문에 답하는 데 더 유용한 배열로 데이터를 비교적 간단하게 변환할 수 있음.
연도를 통틀어 가장 인기있는 이름?
<- babynames %>%
popular_names group_by(sex, name) %>%
summarize(total_births = sum(n)) %>%
arrange(desc(total_births))
sex | name | total_births |
---|---|---|
M | James | 5150472 |
M | John | 5115466 |
M | Robert | 4814815 |
M | Michael | 4350824 |
F | Mary | 4123200 |
M | William | 4102604 |
M | David | 3611329 |
M | Joseph | 2603445 |
M | Richard | 2563082 |
M | Charles | 2386048 |
데이터 랭글링
자료표에 내재된 정보를 명시적으로 드러내는 다른 자료표로 변환하는 과정
깔끔한 데이터에 “data verb”를 적용하여 다른 형태의 깔끔한 데이터로 변환함으로써 실행 (4, 5장)
깔끔하지 않은 데이터
깔끔한 데이터의 법칙
- 각 행(사례)은 동일한 기본 속성, 즉 같은 종류의 것을 나타내야 한다.
- Figure 1 표의 대부분에서 행은 단일 투표소(precint)를 나타내나 어떤 행은 선거구(ward) 또는 시 전체 합계를 나타냄.
- 처음 두 행은 사례가 아니라 데이터를 설명하는 캡션
- 각 열은 각 사례에 대해 동일한 유형의 값을 포함하는 변수
- Figure 1 표의 대부분에서 그렇지만 변수가 아닌 레이블로 인해 깔끔한 패턴이 방해받음
- 15행의 첫 두 항목은 첫 번째 열의 대부분 값인 선거구/투표소 식별자와는 다른 “Ward 1 Subtotal”라는 레이블
정돈된 데이터에 대한 규칙을 준수하면 데이터의 요약, 분석이 간단해짐.
babynames
자료표(깔끔)에서 컴퓨터로 총 아기 수를 찾으려면 변수n
에 있는 모든 숫자를 더하면 됨.표본 크기는 행을 세기만 하면 됨.
Minneapolis 선거 데이터(안 깔끔)에서는 총 투표자 수?
Figure 1 의 I열(“Total Ballots Cast”)에 있는 숫자를 더하면 일부 행에 사례가 아닌 요약이 포함되어 있으므로 결과는 실제 투표자 수의 3배
깔끔한 Minneapolis 선거 데이터
<- mdsr::Elections %>%
neat 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")
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")
- 각 선거구 내 투표율을 각 선거구별로 표시하여 선거구 내 및 선거구 간에 얼마나 많은 편차가 있는지 쉽게 확인
깔끔한 Minneapolis 선거 데이터
사례와 그 의미
깔끔한 자료표의 한 행은 하나의 사례를 나타낸다.
현실 세계에서 사례가 무엇을 의미하는가?
::Minneapolis2013[c(6,2,10,5,27), ] %>%
mdsras_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.") %>%
::column_spec(2:4, width = "9em") kableExtra
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)
<- Cherry
runners mdsr_table(runners[15996:16010, c(1,5,2,6,4)], caption = "An excerpt of runners' performance over time in a 10-mile race.") %>%
::column_spec(1, width = "10em") %>%
kableExtra::column_spec(2, width = "3em") kableExtra
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_full %>%
BP_summary 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_full %>%
BP_nested 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
$data[[1]] BP_nested
# 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()
data
는tibble
로 이루어진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_list
는double
로 이루어진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
으로 지정
<- babynames %>%
baby_wide 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
등은 모두 다른 이름.
- 다양한 사람들이 다양한 관습을 사용하여 코드를 작성
- 스타일 가이드를 하나 정하고 이를 고수하는 것이 좋다.
- 변수 및 함수명에는 밑줄(
_
)을 사용한다. 함수 이름에 마침표(.
)를 사용하는 것은 S3 메소드로 제한한다. - 공백을 자유롭게 사용하고 한 줄의 넓은 코드보다 여러 줄의 좁은 코드 블록을 선호한다.
- 변수 및 함수명에는 snake_case를 사용합니다. 즉, 각 단어는 소문자이며 공백은 없고 밑줄만 사용한다.
styler
패키지를 사용하여 코드를 tidyverse 스타일 가이드를 구현하는 형식으로 다시 포맷할 수 있다.
데이터 불러오기
웹 스크래핑
쉬운 데이터 형식은 모두 비슷하다. 모든 어려운 데이터 형식은 각자의 방식으로 어렵다.
인터넷상의 데이터를 (구조화된) 텍스트로 처리하여 데이터로 변환하는 데이터 수집 형태
데이터 입력의 실수나 저장 또는 코딩 방식의 결함으로 인해 발생하는 오류가 있는 경우가 많음.
데이터 정제(data cleaning) – 이러한 오류를 수정하는 작업
R 기본 이진 파일 형식
.rda
또는.RData
확장자로 구분
saveRDS(mtcars, file = "mtcars.rda", compress = TRUE)
<- readRDS("mtcars.rda") mtcars
- 다른 프로그램으로 읽기 어려움
- 분석의 시작부터 끝까지 데이터의 출처를 유지하는 것은 재현가능 작업 흐름에 중요.
- 데이터 랭글링을 수행하고 분석 데이터 집합을 생성(
saveRDS()
사용)하여 두 번째 마크다운 파일에서 읽을 수 있는(readRDS()
사용) 하나의 마크다운 파일 또는 노트북을 만들면 편리.
자료표 형식
- CSV (“comma-separated values”): 서로 다른 소프트웨어 패키지 간의 데이터 교환에 널리 사용되는 비독점적인 텍스트 형식.
- 이해하기 쉬움
- 압축되지 않으므로 다른 형식에 비해 디스크 공간을 더 많이 차지할 수 있음
- 소프트웨어 패키지별 형식
- MATLAB/Octave (
.mat
): 공학 및 물리학에서 널리 사용 - Stata (
.dta
): 경제 연구에 주로 사용 - SPSS (
.sav
): 사회과학 연구에 주로 사용 - Minitab (
.mtw
): 비즈니스 애플리케이션에 자주 사용됨 - SAS (
.sas7bdat
): 대규모 데이터 세트에 자주 사용 - Epi Info: 미국 질병관리청(CDC)에서 건강 및 역학 데이터에 사용
- MATLAB/Octave (
- 관계형 데이터베이스: 기관에서 활발하게 업데이트되는 대부분의 데이터가 저장되는 형태
- 비즈니스 거래 기록, 정부 기록, 웹 로그 등
- Excel (
.xlsx
): 비즈니스에서 많이 사용되는 독점 스프레드시트 형식.- 반드시 자료표 형식이 아닐 수도 있음 (Minneapolis 선거 자료)
- 웹 관련
- HTML (hypertext markup language):
<table>
형식 - XML (extensible markup language) 형식, 트리 기반 문서 구조
- JSON (JavaScript Object Notation)은 “행과 열” 패러다임을 깨는 일반적인 데이터 형식
- 구글 스프레드시트: HTML로 게시
- 응용 프로그래밍 인터페이스(API)
- HTML (hypertext markup language):
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 파일은 로컬 하드 드라이브에 존재하지 않아도 됨:
<- "https://raw.githubusercontent.com/beanumber/mdsr/master/data-raw/"
mdsr_url <- mdsr_url %>%
houses 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 에 있는 다음의 표를 불러오고 싶다.
<- "http://en.wikipedia.org/wiki/Mile_run_world_record_progression"
url <- url %>%
tables ::read_html() %>%
rvesthtml_nodes("table")
length(tables)
[1] 13
tables
에는 해당 페이지 내에 있는 총 13개의 표가 포함되어있다.
purrr
의pluck()
함수를 사용하여 원하는 표를 꺼낼 수 있다.
API
응용 프로그램 인터페이스(application programming interface, API): 사용자가 제어할 수 없는 컴퓨터 프로그램과 상호 작용하기 위한 프로토콜
텔레비전의 리모컨 설명서와 달리 “블랙박스”를 사용하기 위해 합의된 일련의 지침
다양한 소스에서 웹 상의 거대한 공개 데이터에 접근할 수 있게 해줌.
모든 API가 동일하지는 않지만, 이를 사용하는 방법을 익힘으로써 데이터를 수동으로 스크래핑하지 않고도 데이터를 효과적으로 끌어올 수 있다.
데이터 정제
Table 6 의 표에서
Time
과Date
변수는 문자열로 저장됨이 정보를 사용하려면 먼저 날짜 및 시간을 컴퓨터가 처리할 수 있는 형식으로 변환해야 함
데이터 정제란 변수에 포함된 정보를 가져와서 해당 정보를 사용할 수 있는 형태로 변환하는 것을 말함
재코딩
- 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.")
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
등의 경우, 범주형임에도 불구하고 수치형로 표기됨- 숫자는 범주에 의미 있는 순서가 없음에도 잘못된 의미를 부여할 수 있음
- 수치를 보다 유익한 코딩으로 변환하려면 먼저 다양한 코드가 무엇을 의미하는지 알아내야 함
- 이 정보는 코드북에서 제공하는 경우가 많지만 때로는 데이터를 수집한 사람에게 문의해야 하는 경우도 있음
<- mdsr_url %>%
translations paste0("house_codes.csv") %>% ## mdsr::saratoga_codes
read_csv()
%>% head(5) translations
# 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
<- translations %>%
codes 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.")
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(
%>% select(code, fuel_type),
codes by = c(fuel = "code")
%>%
) left_join(
%>% select(code, heat_type),
codes by = c(heat = "code")
%>%
) left_join(
%>% select(code, sewer_type),
codes by = c(sewer = "code")
)
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 지역에서 잡았다 놓아준 새들에 대한 데이터
::ordway_birds %>%
mdsrselect(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()
<- ordway_birds %>%
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()
<- birds %>%
bird_summary 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은 날짜-시간을 나타내기 위해
POSIXct
및POSIXlt
두 클래스를 사용대부분의 경우 이 두 클래스는 동일한 것으로 취급할 수 있지만 내부적으로는 서로 다르게 저장됨
POSIXct
객체는 UNIX 시대 (1970-01-01) 이후 경과한 시간을 초 단위로 저장POSIXlt
객체는 연도, 월, 일 등의 문자열 목록으로 저장
now()
[1] "2024-04-09 13:38:12 KST"
class(now())
[1] "POSIXct" "POSIXt"
str(unclass(as.POSIXlt(now())))
List of 11
$ sec : num 12.9
$ min : int 38
$ 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"
예시
날짜-시간 연산
<- c("2021-04-29 06:00:00", "2021-12-31 12:00:00")
example str(example)
chr [1:2] "2021-04-29 06:00:00" "2021-12-31 12:00:00"
<- lubridate::ymd_hms(example)
converted 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"
2] - converted[1] converted[
Time difference of 246.25 days
요인? 문자열?
요인형(
factor
)는 범주형 데이터를 나타내는 데 사용되는 특별한 데이터형부작용: 문자열로 잘못 인식하기 쉬움. 수치형이나 날짜 형식으로 변환할 때 다르게 동작
readr::read_csv()
는 문자열을 요인형이 아닌 문자열형(chr
)으로 해석base::read.csv()
(R 4.0 이전)은 문자열을 기본적으로 요인형으로 변환이러한 데이터를 정제하려면 종종
parse_character()
를 사용하여 다시 문자 형식으로 변환해야 함- 이를 놓치면 완전히 잘못된 결과를 얻을 수 있으니 주의
부작용을 피하기 위해 교재에서 사용된 자료표는 범주형 또는 텍스트 데이터를 모두 문자열형으로 저장
- 다른 패키지에서 제공하는 데이터는 반드시 이 규칙을 따르지 않는다는 점에 유의하라.
- 항상 모든 변수와 데이터 랭글링 작업을 주의 깊게 확인하여 올바른 값이 생성되는지 확인하는 것이 좋다.
일본 원자력발전소 자료
# 데이터 받아오기
<- "https://en.wikipedia.org/wiki/List_of_commercial_nuclear_reactors" %>%
tables read_html() %>%
html_nodes(css = "table")
# 'Fukushima Daiichi'가 포함된 표의 번호
<- tables %>%
idx html_text() %>%
str_detect("Fukushima Daiichi") %>%
which()
# 해당 표를 가져온 후 열 이름을 변경
<- tables %>%
reactors ::pluck(idx) %>%
purrrhtml_table(fill = TRUE) %>%
::clean_names() %>%
janitorrename(
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)")
실제로 원자로 용량은 시간이 지남에 따라 증가하는 경향이 있는 반면, 오래된 원자로는 공식적으로 폐쇄되었을 가능성이 더 높음
이 데이터는 수작업으로 코딩하는 것이 간단했지만, 더 크고 복잡한 테이블에 대해서는 데이터 수집을 자동화하는 것이 더 효율적이고 오류 발생 가능성이 적음