generated from rstudio/bookdown-demo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
data-tidyr.Rmd
473 lines (344 loc) · 17.9 KB
/
data-tidyr.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
```{r setup, echo = FALSE, message = FALSE}
source("_common.R")
library(tidyverse)
library(patchwork)
library(lubridate)
knitr::opts_chunk$set(
echo = TRUE)
```
# 깔끔화 방법 {#tidy-data-how}
## 깔끔화 동사 {#tidyr-verbs-how}
`dplyr` 패키지 데이터 문법에 `select`, `filter`, `mutate`, `group_by + summarize`, `arrange` 5개 주요 동사(Verb)를
사용해서 데이터와 커뮤니케이션하며 데이터 조작을 하듯이 `tidyr` 패키지 깔끔한 데이터 조작 동사를 익혀두면 엉망진창인 데이터에 특효약이다.
깔끔한 데이터(Tidy Data)를 만드는데 동원되는 동사에 해당되는 함수는 다음과 같다.
다음 데이터 깔끔이 함수들을 이용하여 엉망진창인 데이터(Messy Data)를 깔끔한 데이터로 만들어서
후속 작업에 속도를 높일 수 있다.
`tidyr` 팩키지에 포함된 Tidy Data 관련 핵심을 이루는 함수로 다음을 꼽을 수 있다.
- `pivot_longer()` / `pivot_wider()` : 데이터프레임 형태 변환 동사
- `separate()`, `separate_rows()` / `unite()` : 변수 쪼개고 합하는 동사
- `expand_grid()` : 결측값 처리 동사
**깔끔한 데이터 관련 R 패키지**는 해드릴 위컴이 주축이 되어 10년 이상 발전시켜
완성한 개념으로 그 단계 단계마다 개발된 팩키지가 있고 개념을
구체화하며 실제로 구현한 함수들도 점점 진화해 나갔다.
과거 진화과정을 살펴보면 어느 순간 깔끔한 데이터(Tidy Data) 개념과 동시에 도구가
나온 것이 아니라 경험과 새로운 지식이 축적되며 진화를 거듭한 결과로 볼 수 있다.
- `reshape`, `reshape2` ([Wickham 2007](http://www.jstatsoft.org/v21/i12/))
- `plyr` ([Wickham 2011](http://www.jstatsoft.org/v40/i01/))
- `tidyr` ([Wickham 2013](https://www.jstatsoft.org/article/view/v059i10))
엉망진창인 데이터는 빅데이터와 공공데이터가 널려있는 현시점 어디서나 마주할 수 있지만,
다행히도 앞선 데이터 과학자들이 각자 맞닥드린 문제를 해결하며 경험을 공유하고 있다.
다음에 나온 사례가 엉망진창 데이터 전부는 아니지만 각 사례별로 깔끔한 데이터로 변환시키는
과정을 살펴보면 향후 맞닥드릴 수 있는 데이터 문제에 좋은 영감을 줄 것으로 생각된다.
## `pivot_*()` 동사 {#pivot-revolution}
해들리 위컴이 언급했듯이, `gather`/`spread`는 사라지지 않고 훨씬 더 나은 모습으로 재탄생했습니다.
`pivot_longer()`, `pivot_wider()` 함수는 `gather()`, `spread()` 함수를 개선하면서 다른 팩키지의 최신 기능을 추가시켰다.
- `pivot_longer()`은 `data.table` 패키지 `melt()`, `dcast()`와 연관됨.
- `cdata` 패키지에 영감을 받아 `pivot_longer()`, `pivot_wider()` 명칭으로 통일됨.
기억하기 좋게 인텍스(index)를 갖는 긴 자료형(long)과 데카르트 평면(Cartesian)과 같은
넓은(wide) 자료형으로 구분한다.
`tidyr` 패키지가 버전 1.0으로 정식 버젼업하면서 생겨난 가장 큰 변화 중 하나가 아닐까 싶다.
### 데카르트 평면 → 인덱스: `pivot_longer()` {#pivot-longer-example}
`pivot_longer()` 함수를 사용해서 데카르트 평면과 같이 넓은 데이터(wide)를 인덱스가 붙은 긴 형태(long) 데이터로 변환시킬 수 있다. `relig_income` 데이터는 `gather/spread` 시절부터 자주 예제 데이터로 사용되던 예제 데이터셋이다.
`religion` 변수를 제외하고 나머지 변수가 `names_to`를 통해 인덱스 명을 바꿀 수 있고 이들이 담고 있던 값은 `values_to`로 떨어지게 된다.
<style>
div.blue { background-color:#e6f0ff; border-radius: 5px; padding: 10px;}
</style>
<div class = "blue">
```{r pivot-longer-cols, eval=FALSE}
dataframe %>%
pivot_longer(
cols,
...
)
```
`cols`: `dplyr::select()` 구문과 동일하게 작성함.
</div>
<div class = "row">
<div class = "col-md-6">
**데카르트 평면 넓은 데이터**
```{r pivot-long}
library(tidyr)
relig_income %>% head
```
</div>
<div class = "col-md-6">
**인덱스 긴 형식 데이터**
```{r pivot-long-relig}
relig_income %>%
pivot_longer(-religion, names_to="소득", values_to = "사람수")
```
</div>
</div>
### 접두사(prefix) 제거: `pivot_longer()` {#pivot-longer-example-prefix}
`names_prefix`를 사용해서 `$`, `<$`, `>` 붙은 값을 제거시킬 수 있다.
```{r pivot-long-relig-prefix}
relig_income %>%
pivot_longer(-religion, names_to="소득",
values_to = "명수",
names_prefix = "\\$|<\\$|>")
```
### 자료형 변환: `pivot_longer()` {#pivot-longer-example-type}
<div class = "row">
<div class = "col-md-6">
**빌보드 원본 데이터: 자료변환 전**
일단 주간 순위는 어찌해서 만들었지만, 주간 칼럼에 `wk`가 들어 있어 이를 `names_prefix`를 통해 날려버리고 나도 숫자로 되어 있는 문자를 다시 숫자형으로 칼럼 자료형을 바꿔야 한다.
```{r billboard}
billboard %>%
pivot_longer(
cols = starts_with("wk"),
names_to = "주간",
values_to = "순위",
values_drop_na = TRUE
)
```
</div>
<div class = "col-md-6">
**빌보드 원본 데이터: 자료변환 후**
```{r billboard-convert}
billboard %>%
pivot_longer(
cols = starts_with("wk"),
names_to = "주간",
values_to = "순위",
names_prefix = "wk",
names_ptypes = list(`주간` = as.character()),
values_drop_na = TRUE
)
```
</div>
</div>
### 다수 칼럼: `pivot_longer()` {#pivot-longer-example-columns}
`who` 데이터셋과 같이 다수 칼럼이 포함된 경우가 있을 수 있다. 이런 경우 `names_pattern` 정규표현식을 적용시켜 변수를 추출할 수 있다. 먼저 `who` 데이터셋을 살펴보자.
- `new_`/`new` 접두어
- `sp`/`rel`/`sp`/`ep` 진단 구분
- `m`/`f` 성별
- `014`/`1524`/`2535`/`3544`/`4554`/`65` 연령대
먼저, `country`, `iso2`, `iso3`, `year` 4개 변수는 정리되어 있어 그대로 두고, 나머지 변수를 `dplyr::select` 문법에 맞춰 선택하고 이를 변경시킨다.
`names_to`로 변수명 3개를 지정하고, `new_`/`new` 접두어는 고정되어 `names_pattern`에서 정규표현식으로 패턴을 적의한다. 이와 더불어 `names_ptypes`에서 자료형도 함께 지정한다.
```{r pivot-longer-who}
who %>% pivot_longer(
cols = new_sp_m014:newrel_f65,
names_to = c("diagnosis", "gender", "age"),
names_pattern = "new_?(.*)_(m|f)(.*)",
names_ptypes = list(
gender = factor(levels = c("f", "m")),
age = factor(
levels = c("014", "1524", "2534", "3544", "4554", "5564", "65"),
ordered = TRUE
)
),
values_to = "count",
) %>%
arrange(desc(count))
```
### 한행에 다수 관측점: `pivot_longer()` {#pivot-longer-example-rows}
한행에 관측점이 다수 있는 재미있는 데이터도 있다.
즉, 첫번째 가정에 아이가 2명 있는데 첫째 아이 생일과 성별, 둘째 아이 생일과 성별이 한 행에 놓여있는 경우가 이에 해당된다.
```{r pivot-longer-rows}
family <- tribble(
~family, ~dob_child1, ~dob_child2, ~gender_child1, ~gender_child2,
1L, "1998-11-26", "2000-01-29", 1L, 2L,
2L, "1996-06-22", NA, 2L, NA,
3L, "2002-07-11", "2004-04-05", 2L, 2L,
4L, "2004-10-10", "2009-08-27", 1L, 1L,
5L, "2000-12-05", "2005-02-28", 2L, 1L,
)
family
```
이를 원하는 이름으로 변경시키기 위해서 `names_to=`에 `.value`라는 특수명칭을 사용하여 한 관측점에 다수 관측점 정보가 포함된 문제를 해결한다.
```{r pivot-longer-rows-clean}
family %>%
pivot_longer(
-family,
names_to = c(".value", "child"),
names_sep = "_",
values_drop_na = TRUE
) %>%
dplyr::mutate(dob = parse_date(dob))
```
### 칼럼명이 중복됨: `pivot_longer()` {#pivot-longer-example-duplicated}
종복된 칼럼명이 존재하는 경우 작업하기 까다로운데... `pivot_longer()`에서 자동으로 칼럼을 추가시켜 문제를 풀어준다.
<div class = "row">
<div class = "col-md-6">
**중복칼럼 데이터셋**
```{r pivot-longer-duplicated-columns}
df <- tibble(x = 1:3, y = 4:6, y = 5:7, y = 7:9, .name_repair = "minimal")
df
```
</div>
<div class = "col-md-6">
**중복칼럼 데이터셋 작업결과**
```{r pivot-longer-duplicated-columns-clean}
df %>%
pivot_longer(-x, names_to = "name", values_to = "value")
```
</div>
</div>
## 인덱스 → 데카르트 평면: `pivot_wider()` {#pivot-longer-wider}
`pivot_wider()`는 깔끔한 데이터를 만드는데 그다지 흔한 경우는 아니지만, 요약표를 만드거나 할 때 종종 사용되고, `pivot_longer()`와 정반대라고 보면 된다.
<style>
div.blue { background-color:#e6f0ff; border-radius: 5px; padding: 10px;}
</style>
<div class = "blue">
```{r pivot-wider-cols, eval=FALSE}
dataframe %>%
pivot_wider(
names_from,
values_from,
...
)
```
- `names_from`은 칼럼값을 지정하는 칼럼
- `values-from`은 값(value)을 지정하는 칼럼
</div>
`tidyr::fish_encounters` 패키지에 내장된 `fish_encounters` 데이터셋은 ["Visualizing Fish Encounter Histories", February 3 2018](https://fishsciences.github.io/post/visualizing-fish-encounter-histories/)에 나온 물고기 포획 방류, 재포획 즉, capture-recapture 데이터셋이다. `pivot_wider()` 함수에 `names_from`, `value_from`을 지정하여 데카르트 평면에 좌표로 찍듯이 데이터를 펼친다. 결측값이 생기는 것은 `values_fill`을 사용해서 0으로 채워넣게 되면 결측값도 정리되고 사람이 이해하기 쉬운 데이터로 즉시 일별하여 확인이 가능하다.
<div class = "row">
<div class = "col-md-4">
**깔끔한 데이터: 인덱스 긴 형식**
```{r pivot-wider-fish}
fish_encounters
```
</div>
<div class = "col-md-8">
**요약표 형태 데이터**
```{r pivot-wider-fish-wider}
fish_encounters %>%
pivot_wider(names_from = station,
values_from = seen,
values_fill = list(seen = 0)) %>%
head(10)
```
</div>
</div>
### 총계(aggregation): `pivot_wider()` {#pivot-longer-wider-aggregation}
`pivot_wider()`를 통해 총계(aggregate)를 내야하는 상황이 발생하곤 한다.
`datasets` 패키지에 내장된 실험계획법이 적용된 데이터 `warpbreaks`를 보면 `wool`, `tension` 두가지 요인으로 총 9번 실험한 결과가 `breaks`에 담겨진 것을 확인할 수 있다.
```{r warpbreaks}
warpbreaks <- datasets::warpbreaks %>%
as_tibble() %>%
select(wool, tension, breaks)
warpbreaks %>%
count(wool, tension)
```
현재 인덱스로 잘 정제된 긴형태 데이터를 요약표 형태로 데카르트 평면과 같이 정리하고자 하면 다음과 같이 작업하게 되면 `wool`, `tension` 요인별로 총 9개 `breaks`값이 한 곳에 몰려있는 것을 파악할 수 있다. `values_fn`을 사용해서 총계로 평균, 최대, 최소 등을 사용해서 하나의 값으로 요약할 수 있다.
<div class = "row">
<div class = "col-md-6">
**요인별 원데이터**
```{r warpbreaks-wider}
warpbreaks %>%
pivot_wider(
names_from = wool,
values_from = breaks
)
```
</div>
<div class = "col-md-6">
**요인별 총계: 최대값**
```{r warpbreaks-aggregate}
warpbreaks %>%
pivot_wider(
names_from = wool,
values_from = breaks,
values_fn = list(breaks = max)
)
```
</div>
</div>
## `separate()`와 `unite()` {#separate-unite-verbs}
### 칼럼에 변수 두개 포함 {#separate-columns}
칼럼 하나에 다수 변수가 포함된 경우를 흔히 발견할 수 있다. `dplyr` 팩키지에는 `starwars` 데이터셋에 등장하는 인물에 대한 인적정보가 담겨있다. 예를 들어, `name` 칼럼은 두 변수가 숨어 있다. 하나는 성(`last name`) 다른 하나는 이름(`first name`)이다. 이를 두개로 쪼개어 두는 것이 Tidy Data를 만든다고 볼 수 있다. 꼭 그런 것은 아니고 경우에 따라 차이가 있지만, 개념적으로 그렇다는 것이다. 상황에 맞춰 유연하게 사용한다.
```{r separate-unite-verbs}
starwars_name_df <- dplyr::starwars %>%
select(name, species, height, mass) %>%
filter(str_detect(species, "Human"))
starwars_name_df
```
`separate()` 함수를 사용해서 칼럼을 두개로 쪼갠다. 이런 경우 `sep=` 인자를 통해 구분자를 지정한다. 공백, `;`, `,` 등 문제에 따라 구분자를 달리 사용한다.
```{r separate-unite-verbs-again}
starwars_name_df %>%
separate(name, into = c("first_name", "last_name"), sep=" ")
```
### 칼럼에 변수 다수 포함 {#separate-columns-many}
TidyTuesday에서 나왔던 칵테일 데이터를 보게 되면 각 칵테일을 제작하는데 필요한 재표가 `ingredient` 칼럼 안에 콤마(`,`)로 묶여있다. 이런 데이터는 절대로 Tidy한 데이터가 아니라서 적절한 조치가 필요하다.
```{r many-variables-in-column-messy}
# cocktail_data <- tidytuesdayR::tt_load('2020-05-26')
cocktails <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2020/2020-05-26/cocktails.csv')
cocktail_df <- cocktails %>%
select(drink, ingredient)
cocktail_tbl <- cocktail_df %>%
group_by(drink) %>%
nest() %>%
mutate(ingredient_tmp = map(data, function(df) df %>% select(ingredient) %>% pull) ) %>%
mutate(ingredient = map_chr(ingredient_tmp, paste, collapse = ", ")) %>%
ungroup
cocktail_tbl %>%
select(drink, ingredient)
```
먼저 `stringr` 팩키지 `str_split()` 함수로 `ingredient` 칼럼을 `, `을 구분자로 삼아 쪼갠 후에 list-column 형태 칼럼(`ingredient_lc`)으로 저장시킨다. 그리고 나서 `unnest()` 함수로 중첩된 것을 풀게 되면 다음과 같은 티블 데이터프레임이 되어 Tidy Data로 변환된다.
```{r many-variables-in-column-messy-tidy}
cocktails_tbl <- cocktail_tbl %>%
select(drink, ingredient) %>%
mutate(ingredient_lc = str_split(ingredient, ", ")) %>%
unnest(ingredient_lc)
cocktails_tbl
```
자, 이제 깔끔한 데이터의 힘을 느껴보자. 칵테일 중에 가장 다양한 재료가 포함된 칵테일은 무엇인가? 라는 질문에 단순한 `dplyr` 동사로 확인이 바로 가능하다.
```{r many-variables-in-column-cocktail}
cocktails_tbl %>%
group_by(drink) %>%
summarise(num_ingredients = n()) %>%
arrange(desc(num_ingredients))
```
### `separate_rows()` 함수로 빠르게 {#separete-rows-quickly}
상기 과정이 다소 많은 동사를 조합해서 결과를 도출하고 있다고 생각된다면 `separate_rows()` 함수를 사용해서 깔끔한 데이터를 만든 후에 `count` 함수와 `sort = TRUE`를 활용하여 동일한 작업을 간단히 마무리 할 수 있다.
```{r quickly-separate-rows}
cocktail_tbl %>%
separate_rows(ingredient, sep = ", ") %>%
count(drink, sort = TRUE)
```
## 결측 데이터 {#fix-missing-data}
[data.world Nuclear Weapon Explosions BY THOMAS DRÄBING](https://data.world/tdreabing/nuclear-weapon-explosions) 웹사이트에서 핵폭탄 실험 데이터를 다운로드 받을 수 있다.
데이터 자체는 완벽하지만 의미상 문제가 있다.
즉 1945년 이후 데이터를 보자면 숨어있는 결측값이 보인다.
확인이 안된 것 한건 빼면 7개국이 핵폭탄 실험을 한 것으로 나오는데 1945년에는 아무런 기록이 없다.
데이터프레임에는 정상인 깔끔한 데이터지만 사실 1945년 0 건에 예를 들어 중국이나 러시아가 포함되어 있는 것이 맞다.
이런 데이터 자체 결측값을 찾아내서 채워넣는 것이 깔끔한 데이터를 만드는 또하나 중요한 과정이다.
```{r nuclear-testing-data}
library(lubridate)
nuclear_raw <- read_csv("data/nuclear_weapon_explosions_1945-1998.csv")
nuclear_trials_df <- nuclear_raw %>%
janitor::clean_names() %>%
mutate(datetime = lubridate::parse_date_time(datetime, "%m/%d/%Y %H:%M:%S %p")) %>%
mutate(연도 = year(datetime)) %>%
count(country, 연도, name = "핵실험횟수") %>%
rename(국가 = country) %>%
arrange(연도) %>%
filter(연도 <= 1954)
nuclear_trials_df
```
### 결측값 명시: `expand_grid()` {#expand-grid-comes-in}
상기와 같은 문제를 해결하기 위해 `expand_grid()` 함수를 사용해서 핵실험을 수행한 모든 국가에 대해 해당 년도를 모두 생성할 필요가 있다.
3개 국가에 대해 10년을 `expand_grid()` 함수로 조합하게 되면 30개 관측점이 생기는데 미국이 핵실험을 하지 않은 1947년의 경우 `NA` 값이 생긴다. 이를 채워주어야 한다. 앞서 `expand_grid()` 함수로 생성된 Tidy Data를 염두에 두고 이를 `left_join()` 함수와 조인을 걸어 나중에 치환시킬 데이터프레임을 사전 제작한다.
```{r expand-grid-nuclear}
country_year_tbl <- expand_grid(국가 = c("USA", "Russia", "England"),
연도 = seq(1945, 1954, 1))
nuclear_tbl <- country_year_tbl %>%
left_join(nuclear_trials_df)
nuclear_tbl
```
### 결측값 치환: `replace_na()` {#expand-grid-comes-in-replace_na}
결측값을 특정 값으로 채워 넣고자 하는 경우 `replace_na()` 함수를 사용한다.
먼저 `replace_na()` 함수를 사용해서 `n_bombs` 변수에 `NA` 결측값을 0으로 채워넣는다.
다른 국가명이나 연도에 결측값이 있는 경우 동일하게 변수명을 지정하고 결측값을 지정하여 해결한다.
```{r fill-na-with-replace-na}
nuclear_tbl %>%
replace_na(list(핵실험횟수 = 0,
연도 = 1945))
```
### 결측값 제거: `drop_na()` {#expand-grid-comes-in-drop}
다른 것 다 모르겠고 데이터프레임에 결측값이 있으면 안되기 때문에 그냥 제거한다.
이럴 때 사용하는 함수가 `drop_na()`다.
```{r nuclear-drop-dataset}
nuclear_tbl %>%
drop_na(핵실험횟수)
```