-
Notifications
You must be signed in to change notification settings - Fork 6
/
tidydata.qmd
599 lines (436 loc) · 36.6 KB
/
tidydata.qmd
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
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
# 整然データ構造 {#sec-tidydata}
```{r}
#| include: false
source("_common.R")
```
本章ではグラフの作成に適した形へデータを整形することについて学習します。ただし、これはグラフに限られた話ではありません。作図に適したデータは分析にも適します。
## 整然データ (tidy data)とは {#sec-tidydata-intro}
分析や作図に適したデータの形は整然データ、または簡潔データ (tidy data)と呼ばれます。整然データの概念はtidyverse世界の産みの親であるHadely Wickham先生が提唱した概念であり、詳細は @Wickham:2014 を参照してください。
整然データは目指す到達点は非常に単純です。それは「データの構造 (structure)と意味 (semantic)を一致させる」ことです。そして、この「意味」を出来る限り小さい単位で分解します。
例えば、3人で構成されたあるクラス内の被験者に対し、投薬前後に測定した数学成績があるとします。投薬前の成績は`"Control"`、投薬後の状況を`"Treatment"`とします。これをまとめたのが @tbl-tidydata_messy_data1 です。
```{r}
#| label: tbl-tidydata_messy_data1
#| echo: false
#| message: false
#| tbl-cap: "Messy Dataの例 (1)"
tibble(Name = c("Hadley", "Song", "Yanai"),
Control = c(90, 80, 100),
Treatment = c(90, 25, 95)) |>
gt()
```
また、以上の表は転置も可能であり、以下のように表現することが可能です ( @tbl-tidydata_messy_data2 )。
```{r}
#| label: tbl-tidydata_messy_data2
#| echo: false
#| message: false
#| tbl-cap: "Messy Dataの例 (2)"
tibble(Treat = c("Control", "Treatment"),
Hadely = c(90, 90),
Song = c(80, 25),
Yanai = c(100, 95)) |>
gt()
```
2つのデータが持つ情報は全く同じです。これは「同じ意味を持つが、異なる構造を持つ」とも言えます。このような多様性が生じる理由は行と列のあり方が各値を説明するに十分ではないからです。異なるデータ構造として表現される余地があるということです。
たとえば、 @tbl-tidydata_messy_data1 の場合、各列は以下のような3つの情報があります。
1. `Name`: 被験者名
2. `Control`: **投薬前**の**数学成績**
3. `Treatment`: **投薬後**の**数学成績**
このデータの問題は「投薬有無」と「数学成績」が2回登場したという点です。1は問題ありませんが、2と3の値は「投薬有無 $\times$ 数学成績」の組み合わせです。一つの変数に2つの情報が含まれていますね。これによって、投薬有無を行にしても列にしてもいいわけです。「ならばこっちの方が柔軟だしいいのでは?」と思う方もいるかも知れません。しかし、パソコンはこの曖昧さが嫌いです。なぜなら、人間のような思考ができないからです。データフレームは縦ベクトルの集合であるから、各列には一つの情報のみ格納する必要があります。たとえば、以下のように列を変更するとしましょう。
1. `Name`: 被験者名
2. `Treat`: 投薬有無
3. `Math_Score`: 数学成績
`Treat`は投薬前なら`"Control"`の値を、投薬後なら`"Treatment"`の値が入ります。`Math_Socre`には数学成績が入ります。これに則って表に直したのが @tbl-tidydata_tidydata です。
```{r tidydata1}
#| label: tbl-tidydata_tidydata
#| echo: false
#| message: false
#| tbl-cap: "整然データの例"
tibble(Name = rep(c("Hadley", "Song", "Yanai"), each = 2),
Treat = rep(c("Control", "Treatment"), 3),
Math_Score = c(90, 90, 80, 25, 100, 95)) |>
gt()
```
表が長くなりましたが、これなら一つの列に2つ以上の情報が含まれることはありません。この場合、 @tbl-tidydata_messy_data1 と @tbl-tidydata_messy_data2 のように、行と列を転置することができるでしょうか。
```{r}
#| label: tbl-tidydata_transdata
#| echo: false
#| message: false
#| tbl-cap: "整然データを転置した場合"
tibble(Name = c("Treat", "Math_Score"),
Hadley1 = c("Control", 90),
Hadley2 = c("Treatment", 90),
Song1 = c("Control", 80),
Song2 = c("Treatment", 25),
Yanai1 = c("Control", 100),
Yanai2 = c("Treatment", 95),) |>
gt() |>
cols_label("Hadley1" = "Hadley", "Hadley2" = "Hadley",
"Song1" = "Song", "Song2" = "Song",
"Yanai1" = "Yanai", "Yanai2" = "Yanai")
```
その結果が @tbl-tidydata_transdata ですが、いかがでしょうか。まず、列名が重複している時点でアウトですし、人間が見ても非常に分かりにくい表になりました。また、一つの列に異なるデータ (この場合、character型とnumeirc型)が混在しています。パソコンから見てはわけのわからないデータになったわけです。
ここまで来たら整然データのイメージはある程度掴めたかも知れません。具体的に整然データとは次の4つの条件を満たすデータです[@Wickham:2014]。
1. 1つの列は、1つの変数を表す。
2. 1つの行は、1つの観測を表す。
3. 1つのセル(特定の列の特定の行)は、1つの値を表す。
4. 1つの表は、1つの観測単位 (unit of observation)をもつ(異なる観測単位が混ざっていない)。
以下でも、 @tbl-tidydata_messy_data1 と @tbl-tidydata_tidydata を対比しながら、以上の4条件をより詳しく説明します。
### 1つの列は、1つの変数を表す
@tbl-tidydata_messy_data1 と @tbl-tidydata_tidydata に含まれる情報は以下の3つで共通しています。
1. 被験者名
2. 投薬有無
3. 数学成績
これらの情報がそれぞれデータの変数になるわけですが、整然データは一つの列が一つの変数を表します。それではまず、 @tbl-tidydata_messy_data1 ( @fig-tidydata_example1 の左)から考えてみましょう。この図には3つの情報が全て含まれています。しかし、数学成績は2列に渡って格納されており、「1列1変数」の条件を満たしておりません。一方、 @tbl-tidydata_messy_data1 ( @fig-tidydata_example1 の右)は投薬前後を表す`Treat`変数を作成し、その値に応じた数学成績が格納されており、「1列1変数」の条件を満たしています。
![1つの列は、1つの変数を表す](Figs/Tidydata/TidyData1.png){#fig-tidydata_example1 width="75%" fig-align="center"}
「1列1変数」は整然データの最も基本となる条件であり、整然データ作成の出発点とも言えます。
### 1つの行は、1つの観測を表す
@fig-tidydata_example2 の左は一行当たり、いくつの観察が含まれているでしょうか。それを確認するためには、このデータが何を観察しているかを考える必要があります。このデータは投薬**前後**の数学成績を観察し、量的に測定したものです。つまり、同じ人に対して2回観察を行ったことになります。したがって、投薬前の数学成績と投薬後の数学成績は別の観察であり、 @fig-tidydata_example2 の左は3行の表ですが、実は6回分の観察が含まれていることになります。1行に2つの観察が載っていることですね。
![1つの行は、1つの観測を表す](Figs/Tidydata/TidyData2.png){#fig-tidydata_example2 width="75%" fig-align="center"}
一方、 @fig-tidydata_example2 の右は6行のデータであり、観察回数とデータの行数が一致しています。つまり、1行に1観察となります。
今回は数学成績しか測っていたいので、簡単な例ですが、実際のデータには曖昧な部分があります。たとえば、投薬によって血圧が変化する可能性があるため、最高血圧もまた投薬前後に測定したとします。それが @tbl-tidydata_blood の左です。
```{r}
#| label: tbl-tidydata_blood
#| echo: false
#| message: false
#| tbl-cap: "1行1観察の例"
#| tbl-subcap:
#| - "雑然データ"
#| - "整然データ"
#| layout-ncol: 2
df1 <- tibble(
Name = rep(c("Hadley", "Song", "Yanai"), each = 2),
Treat = rep(c("Control", "Treatment"), 3),
Math = c(90, 90, 80, 25, 100, 95),
Blood = c(110, 115, 95, 110, 100, 95)
)
df2 <- df1 |>
pivot_longer(cols = c(Math, Blood),
names_to = "Type",
values_to = "Value")
gt(df1)
gt(df2)
```
3人に投薬前後に数学成績と最高血圧を測定した場合の観察回数は何回でしょう。3人 $\times$ 2時点 $\times$ 2指標の測定だから12回の測定でしょうか。ならば、 @tbl-tidydata_blood の右が整然データでしょう。しかし、この場合、1列1変数という条件が満たされなくなります。`Value`列には数学成績と血圧が混在しており、2つの変数になります。ならば、どれも整然データではないということでしょうか。実は整然データは @tbl-tidydata_blood の左です。なぜなら、「1観察=1値」ではないからです。データにおける観察とは観察単位ごとに測定された**値の集合**です。観察対象とは人や自治体、企業、国などだけでなく、時間も含まれます。たとえば、人の特徴 (性別、身長、所得、政治関心など)を測定しもの、ある日の特徴 (気温、株価など)を測定したもの全てが観察です。むろん、人 $\times$ 時間のような組み合わせが観察単位ともなり得ます。この一つ一つの観察単位から得られた値の集合が観察です。 @tbl-tidydata_blood の分析単位は「人 $\times$ 時間」です。成績や最高血圧は分析単位が持つ特徴や性質であって、分析単位ではありません。
### 1つのセルは、1つの値を表す
この条件に反するケースはあまりないかも知れません。たとえば、「Hadleyは処置前後の数学成績が同じだし、一行にまとめよう」という意味で @fig-tidydata_example3 の左のような表を作る方もいるかも知れませんが、あまりいないでしょう。
![1つのセルは、1つの値を表す](Figs/Tidydata/TidyData3.png){#fig-tidydata_example3 width="50%" fig-align="center"}
@fig-tidydata_example3 の例は「1セル1値」の条件に明らかに反します。しかし、基準が曖昧な変数もあり、その一つが日付です。
```{r date-table}
#| label: tbl-tidydata_date_table
#| echo: false
#| message: false
#| tbl-cap: "日付の扱い方"
#| tbl-subcap:
#| - "雑然データ?"
#| - "整然データ?"
#| layout-ncol: 2
df1 <- tibble(
Date = c("2020/06/29", "2020/06/30", "2020/07/01",
"2020/07/02", "2020/07/03"),
Stock = c(100, 105, 110, 85, 90)
)
df2 <- df1 |>
separate(Date, into = c("Year", "Month", "Date"),
sep = "/", convert = TRUE)
gt(df1)
gt(df2)
```
@tbl-tidydata_date_table の左側の表はどうでしょうか。5日間の株価を記録した架空のデータですが、たしかに`Date`列には日付が1つずつ、`Stock`には株価の値が1つずつ格納されています。しかし、解釈によっては「`Date`に年、月、日といった3つの値が含まれているぞ」と見ることもできます。この解釈に基づく場合、 @tbl-tidydata_date_table の右側の表が整然データとなり、左側は雑然データとなります。このケースは第一条件であった「一列一変数」とも関係します。なぜなら、`Date`という列が年・月・日といった3変数で構成されているとも解釈できるからです。
分析によっては左側のような表でも全く問題ないケースもあります。時系列分析でトレンド変数のみ必要ならこれでも十分に整然データと呼べます。しかし、季節変動などの要素も考慮するならば、左側は雑然データになります。データとしての使い勝手は右側の方が優れているのは確かです。
データを出来る限り細かく分解するほど情報量が豊かになりますが、それにも限度はあるでしょう。たとえば、「`Year`は実は世紀の情報も含まれているのでは...?」という解釈もできますが、これを反映してデータ整形を行うか否かは分析の目的と分析モデルによって異なります。この意味で、明らかな雑然データはあり得ますが、明らかな整然データは存在しないでしょう。どちらかといえば、整然さの度合いがあり、「これなら十分に整然データと言えないだろうか」と判断できれば十分ではないかと筆者 (Song)は考えます。
### 1つの表は、1つの観測単位をもつ
[e-stat](https://www.e-stat.go.jp)などから[国勢調査データ](https://www.e-stat.go.jp/stat-search/database?page=1&toukei=00200521&tstat=000001080615)をダウンロードした経験はあるでしょうか。以下の @fig-tidydata_example4 は2015年度国勢調査データの一部です。
![国勢調査データ](Figs/Tidydata/MessyData4.png){#fig-tidydata_example4 width="50%" fig-align="center"}
このデータの観察単位はなんでしょうか。データのの1行目は全国の人口を表しています。つまり、単位は国となります。しかし、2行目は北海道の人口です。この場合の観測単位は都道府県となります。つづいて、3行目は札幌市なので単位は市区町村になります。4行目は札幌市中央区、つまり観測単位が行政区になっています。そして14行目は函館市でまた単位は市区町村に戻っています。実際、会社や政府が作成するデータには @fig-tidydata_example4 や @fig-tidydata_example5 のようなものが多いです。とりわけ、 @fig-tidydata_example5 のように、最後の行に「合計」などが表記されている場合が多いです。
![1つの表は、1つの観測単位をもつ](Figs/Tidydata/TidyData4.png){#fig-tidydata_example5 width="50%" fig-align="center"}
このような表・データを作成することが悪いことではありません。むしろ、「読む」ための表ならこのような書き方が一般的でしょう。しかし、「分析」のためのデータは観察の単位を統一する必要があります。
## Wide型からLong型へ {#sec-tidydata-gather}
以下では「1列1変数」の条件を満たすデータの作成に便利な`pivot_longer()`と`pivot_wider()`関数について解説します。この関数群はおなじみの{dplyr}でなく、{tidyr}パッケージが提供している関数ですが、どれも{tidyverse}パッケージ群に含まれているため、{tidyverse}パッケージを読み込むだけで十分です。本節では`pivot_longer()`を、次節では`pivot_wider()`を取り上げます。
まず、`pivot_longer()`ですが、この関数は比較的に新しい関数であり、これまでは{tidyr}の`gather()`関数が使われてきました。しかし、`gahter()`関数は将来、なくなる予定の関数であり、今から{tidyr}を学習する方は`pivot_*()`関数群に慣れておきましょう。
まずは{tidyverse}パッケージを読み込みます。
```{r}
#| eval: false
pacman::p_load(tidyverse)
```
今回は様々な形のデータを変形する作業をするので、あるデータセットを使うよりも、架空の簡単なデータを使います。
```{r}
df1 <- tibble(
Name = c("Hadley", "Song", "Yanai"),
Control = c(90, 80, 100),
Treatment = c(90, 25, 95),
Gender = c("Male", "Female", "Female")
)
df1
```
このデータは既に指摘した通り「1列1変数」の条件を満たしております。この条件を満たすデータは以下のような形となります。
```{r}
#| echo: false
df1 |>
pivot_longer(cols = c(Control, Treatment),
names_to = "Treat",
values_to = "Math_Score")
```
`Treat`変数が作成され、元々は変数名であった`"Control"`と`"Treatment"`が値として格納されます。この変数をキー変数と呼びます。そして、キー変数の値に応じた数学成績が`Math_Score`という変数でまとめられました。この変数を値変数と呼びます。
「1列1変数」を満たさなかった最初のデータは「Wide型データ」、これを満たすようなデータは「Long型データ」と呼ばれます。これは**相対的**に最初のデータが横に広いから名付けた名前であって、「Wide型=雑然データ」もしくは「Long型=整然データ」ではないことに注意してください。実際、 @tbl-tidydata_blood はどれも同じ情報を持つ表であり、右の方がLong型データです。しかし、整然データはWide型である左の方ですね。つまり、「Long型データだから1列1変数だ」ということは間違いです。同じ情報を持つデータで、相対的にどれが横長か、縦長かの相対的な問題です。
Wide型データをLong型へ変換する関数が`pivot_longer()`であり、基本的な使い方は以下の通りです。
```{r}
#| eval: false
# pivot_longer()の使い方
データ名 |>
pivot_longer(cols = c(まとめる変数1, まとめる変数2, ...),
names_to = "キー変数名",
values_to = "値変数名")
```
ここでは同じ変数が`Control`と`Treatment`変数で分けられているため、まとめる変数はこの2つであり、`cols = c(Control, Treatment)`と指定します。`Control`と`Treatment`は`"`で囲んでも、囲まなくても同じです。また、{dplyr}の`select()`関数で使える変数選択の関数 (`starts_with()`、`where()`など)や`:`演算子も使用可能です。また、`cols`引数は`pivot_longer()`の第2引数であるため、`cols = `は省略可能です(第一引数はパイプにより既に渡されています)。
`names_to`と`values_to`引数はそれぞれキー変数名と値変数名を指定する引数で、ここは必ず`"`で囲んでください。この`df1`をLong型へ変換し、`df1_L`と名付けるコードが以下のコードです。
```{r}
df1_L <- df1 |>
pivot_longer(Control:Treatment,
names_to = "Treat",
values_to = "Math_Score")
df1_L
```
これだけでも`pivot_longer()`関数を使ってWide型からLong型への変換は問題なくできますが、以下ではもうちょっと踏み込んだ使い方について解説します。「ここまでで十分だよ」という方は、ここを飛ばしても構いません。
今回の実習データ`df3`は3人の体重を3日間に渡って計測したものです。ただし、ドジっ子のSongは2日目にうっかり測るのを忘れており、欠損値となっています。
```{r}
df2 <- tibble(
Name = c("Hadley", "Song", "Yanai"),
Day1 = c(75, 120, 70),
Day2 = c(73, NA, 69),
Day3 = c(71, 140, 71)
)
df2
```
まず、これをこれまでのやり方でLong型へ変形し、`df2_L`と名付けます。
```{r}
df2_L <- df2 |>
pivot_longer(starts_with("Day"),
names_to = "Days",
values_to = "Weight")
df2_L
```
これでも問題ないかも知れませんが、以下のような操作を追加に行うとします。
1. `Weight`が欠損している行を除去する
2. `Days`列の値から`"Day"`を除去し、numeric型にする
以上の作業を行うには、{dplyr}が便利でしょう。ちなみに`str_remove()`関数が初めて登場しましたが、これについては第[-@sec-string]章で詳細に解説します。簡単に説明しますと、`str_remove("X123", "X")`は`"X123"`から`"X"`を除去し、`"123"`のみ残す関すです。残された値が数字のみであってもデータ型はcharacter型なので、もう一回、numeric型に変換する必要があります[^long2]。{dplyr}を使ったコードは以下の通りです。
[^long2]: 実は`parse_number()`を使えばもっと簡単ですが、これについては後ほど解説します。
```{r}
# 1. WeightがNAのケースを除去
# 2. Days変数の値から"Day"を除去
# 3. Days変数をnumeric型へ変換
df2_L |>
filter(!is.na(Weight)) |> # 1
mutate(Days = str_remove(Days, "Day"), # 2
Days = as.numeric(Days)) # 3
```
実はこの作業、`pivot_longer()`内で行うことも可能です。たとえば、`values_to`で指定した変数の値が欠損しているケースを除去するには`values_drop_na`引数を`TRUE`に指定するだけです。
```{r}
# Weight変数がNAのケースを除去する
df2 |>
pivot_longer(starts_with("Day"),
names_to = "Days",
values_to = "Weight",
values_drop_na = TRUE)
```
それでは、キー変数から共通する文字列を除去するにはどうすれば良いでしょうか。この場合、`names_prefix`引数を使います。これは`names_to`で指定した新しく出来る変数の値における接頭詞を指定し、それを除去する引数です。今回は`"Day1"`、`"Day2"`、`"Day3"`から`"Day"`を除去するので、`names_prefix = "Day"`と指定します。こうすることで、`Days`列から`"Day"`が除去されます。ただし、数字だけ残っても、そのデータ型はcharacter型ですので、このデータ型を変換する必要があります。ここで使うのが`names_transform`引数であり、これはlist型のオブジェクトを渡す必要があります。`Days`列をnumeric型にする場合は`list(Days = as.numeric)`です。複数の列のデータ型を変える場合、`list()`の中に追加していきます。それでは実際に走らせてみましょう。
```{r}
# 1. Day変数の値から"Day"を除去する
# 2. Day変数をinteger型に変換
# 3. Weight変数がNAのケースを除去する
df2 |>
pivot_longer(starts_with("Day"),
names_to = "Days",
names_prefix = "Day", # 1
names_transform = list(Days = as.numeric), # 2
values_to = "Weight",
values_drop_na = TRUE) # 3
```
これでWide型をLong型が変換され、整然でありながら、より見栄の良いデータが出来上がりました。他にも`pivot_longer()`は様々な引数に対応しており、詳細は`?pivot_longer`や[レファレンスページ](https://tidyr.tidyverse.org/reference/pivot_longer.html)を参照してください。
## Long型からWide型へ {#sec-tidydata-spread}
ご存知の通り、「Long型データ=整然データ」ではありません。実際、 @tbl-tidydata_blood の右はLong型データですが、1列に2つの変数が含まれており、整然データとは言えません。このようなデータはいくらでもあります。とりわけ、「分析」のためじゃなく、「読む」ための表の場合において多く発見されます。
```{r}
#| label: tbl-tidydata_wider1
#| echo: false
#| message: false
#| tbl-cap: "Long型データの例"
#| tbl-subcap:
#| - "雑然データ"
#| - "整然データ"
#| layout-ncol: 2
df1 <- read_csv("Data/Population2015.csv")
df1 <- df1 |>
slice(1:8) |>
mutate(都道府県 = ifelse(is.na(都道府県), "", 都道府県))
df2 <- df1 |>
mutate(都道府県 = ifelse(都道府県 == "", NA, 都道府県)) |>
fill(都道府県) |>
pivot_wider(names_from = 区分,
values_from = 人口) |>
relocate(面積, .after = 外国人)
gt(df1)
gt(df2)
```
変数名が日本語になっていますが、これは「読むための表」を読み込むことを仮定しています。このように変数名として日本語は使えますが、自分でデータセットを作成する際、変数名はローマ字にすることを強く推奨します。
@tbl-tidydata_wider1 の左の場合、`人口`列に総人口と外国人人口といった2つの変数の値が格納されているため、整然データではありません。これを整然データにしたものが右の表です。本節ではLong型データをWide型データへ変換する`pivot_wider()`関数を紹介します。この関数は同じく{tidyr}が提供している`spread()`関数とほぼ同じ関数ですが、今は`pivot_wider()`の使用が推奨されており、`spread()`はいずれか{tidyr}から外される予定です。
まずは、実習用データを読み込みます。
```{r}
#| message: false
df3 <- read_csv("Data/Population2015.csv")
df3
```
このデータは2015年国勢調査から抜粋したデータであり、各変数の詳細は @tbl-tidydata_df3_detail の通りです。
```{r}
#| label: tbl-tidydata_df3_detail
#| message: false
#| echo: false
#| tbl-cap: "`df3`の詳細"
tibble(Var = c("`都道府県`", "`区分`", "`人口`", "`面積`"),
Desc = c("都道府県名",
"総人口/外国人人口の区分",
"人口 (人)",
"面積 (km<sup>2</sup>)")) |>
gt() |>
cols_label("Var" = "変数名", "Desc" = "説明") |>
fmt_markdown(columns = everything())
```
まずは変数名が日本語になっているので、`rename()`関数を使ってそれぞれ`Pref`、`Type`、`Population`、`Area`に変更します。
```{r}
df3 <- df3 |>
rename("Pref" = 都道府県, "Type" = 区分, "Population" = 人口, "Area" = 面積)
df3
```
次は、`Pref`列の欠損値を埋めましょう。ここの欠損値は、当該セルの一つ上のセルの値で埋まりますが、これは`fill()`関数で簡単に処理できます。欠損値を埋めたい変数名を`fill()`の引数として渡すだけです。
```{r}
df3 <- df3 |>
fill(Pref)
df3
```
そして、いよいよ`pivot_wider()`関数の出番ですが、基本的に使い方は以下の通りです。
```{r}
#| eval: false
# pivot_wider()の使い方
データ名 |>
pivot_wider(names_from = キー変数名,
values_from = 値変数名)
```
まず、キー変数名は列として展開する変数名であり、ここでは`Type`になります。そして、値変数名は展開される値の変数であり、ここでは`Population`になります。つまり、「`Population`を`Type`ごとに分けて別の列にする」ことになります。また、`values_from`引数は長さ2以上のベクトルを指定することで、複数の値変数を指定することも可能です。たとえば、`df3`に`Income`という平均所得を表す列があり、これらも総人口と外国人それぞれ異なる値を持っているとしたら、`values_from = c(Population, Income)`のように複数の値変数を指定することが出来ます。今回は値変数が1つのみですが、早速やってみましょう。
```{r}
df3_W <- df3 |>
pivot_wider(names_from = Type,
values_from = Population)
df3_W
```
また、日本語の変数名が出来てしまったので、それぞれ`Total`と`Foreigner`に変更し、`relocate()`関数を使って`Area`を最後の列に移動します。
```{r}
df3_W <- df3_W |>
rename("Total" = 総人口,
"Foreigner" = 外国人) |>
relocate(Area, .after = last_col())
df3_W
```
これで整然データの出来上がりです。
この`pivot_wider()`関数は`pivot_longer()`関数同様、様々な引数を提供しておりますが、主に使う機能は以上です。他には`pivot_wider()`によって出来た欠損値を埋める引数である`values_fill`があり、デフォルト値は`NULL`です。ここに`0`や`"Missing"`などの長さ1のベクトルを指定すれば、指定した値で欠損値が埋まります。
`pivot_wider()`関数の詳細は`?pivot_wider`もしくは、[レファレンスページ](https://tidyr.tidyverse.org/reference/pivot_wider.html)を参照してください。
## 列の分割 {#sec-tidydata-separate}
他にも「1列1変数」の条件を満たさないケースを考えましょう。`pivot_longer()`は1つの変数が複数の列に渡って格納されている際に使いましたが、今回は1つの列に複数の変数があるケースを考えてみましょう。たとえば、年月日が1つの列に入っている場合、これを年、月、日の3列で分割する作業です。また、これと関連して、列から文字列を除去し、数値のみ残す方法についても紹介します。
[実習用データ](Data/COVID19_JK.csv)を読み込んでみましょう。
```{r}
#| message: false
df4 <- read_csv("Data/COVID19_JK.csv")
df4
```
このデータは2020年1月16日から2020年7月5日まで、COVID-19 (新型コロナ)の新規感染者数を日本と韓国を対象に収集したものです。データはWikipedia ([日本](https://en.wikipedia.org/wiki/COVID-19_pandemic_in_Japan) / [韓国](https://en.wikipedia.org/wiki/COVID-19_pandemic_in_South_Korea))から収集しました。韓国の新規感染者数は最初の4日分が欠損値のように見えますが、最初の感染者が確認されたのが1月20日のため、1月19日までは欠損となっています。
```{r}
#| echo: false
#| messsage: false
data.frame(変数名 = c("`ID`", "`Date`", "`Week`",
"`Confirmed_Japan`", "`Confirmed_Korea`"),
説明 = c("ケースID", "年月日", "曜日",
"新規感染者数 (日本)", "新規感染者数 (韓国)")) |>
gt() |>
fmt_markdown(columns = 1)
```
このデータの場合、観察単位は「国 $\times$ 日」です。しかし、`df4`は1行に日本と韓国の情報が格納されており「1行1観察」の条件を満たしておりません。したがって、`pivot_longer()`を使ってLong型へ変換し、新しいデータの名前を`df4_L`と名付けます。
```{r}
df4_L <- df4 |>
pivot_longer(cols = starts_with("Confirmed"),
names_to = "Country",
names_prefix = "Confirmed_",
values_to = "Confirmed")
df4_L
```
続いて、新規感染者数を表す`Confirmed`列から「人」を除去しましょう。人間にとってはなんの問題もありませんが、パソコンにとって`1人`や`5人`は文字列に過ぎず、分析ができる状態ではありません。ここで使う関数が`parse_number()`です。引数として指定した列から数値のみ抽出します。`"$1000"`や`"1, 324, 392"`のような数値でありながら、character型として保存されている列から数値のみを取り出す際に使う関数です。使い方は以下の通りです。
```{r}
#| eval: false
データ名 |>
mutate(新しい変数名 = parse_number(数値のみ抽出する変数名))
```
似たようなものとして`parse_character()`があり、これは逆に文字列のみ抽出する関数です。
ここでは`Confimed`から数値のみ取り出し、`Confrimed`列に上書きし、それを`df4_S`と名付けます。
```{r}
df4_S <- df4_L |>
mutate(Confirmed = parse_number(Confirmed))
df4_S
```
それでは国、曜日ごとの新規感染者数を調べてみます。求める統計量は曜日ごとの新規感染者数の合計、平均、標準偏差です。まず、曜日は月から日の順になるよう、factor型に変換します。そして、国と曜日ごとに記述統計量を計算し、`df4_S_Summary1`という名で保存します。
```{r}
df4_S <- df4_S |>
mutate(Week = factor(Week,
levels = c("月", "火", "水", "木", "金", "土", "日")))
df4_S_Summary1 <- df4_S |>
group_by(Country, Week) |>
summarise(Sum = sum(Confirmed, na.rm = TRUE),
Mean = mean(Confirmed, na.rm = TRUE),
SD = sd(Confirmed, na.rm = TRUE),
.groups = "drop")
df4_S_Summary1
```
`df4_S_Summary1`はこの状態で整然データですが、もし人間が読むための表を作るなら、韓国と日本を別の列に分けた方が良いかも知れません。`pivot_wider()`を使って、日本と韓国のの新規感染者数を2列に展開します。
```{r}
df4_S_Summary1 |>
pivot_wider(names_from = Country,
values_from = Sum:SD)
```
これで人間にとって読みやすい表が出来ました。今は「日本の合計」、「韓国の合計」、「日本の平均」、...の順番ですが、これを日本と韓国それぞれまとめる場合は、`relocate()`を使います。
```{r}
df4_S_Summary1 |>
pivot_wider(names_from = Country,
values_from = Sum:SD) |>
relocate(Week, ends_with("Japan"), ends_with("Korea"))
```
新規感染者が確認されるのは金〜日曜日が多いことが分かります。
曜日ではなく、月ごとに記述統計料を計算する場合は、まず`Date`列を年、月、日に分割する必要があります。具体的には`Date`を`"/"`を基準に別ければいいです。そこで登場するのは`separate()`関数であり、使い方は以下の通りです。
```{r}
#| eval: false
# separate()の使い方
データ名 |>
separate(cols = 分割する変数名,
into = 分割後の変数名,
sep = "分割する基準")
```
`cols`には`Date`を指定し、`into`は新しく出来る列名を指定します。今回は`Date`が3列に分割されるので、長さ3のcharacter型ベクトルを指定します。ここでは`Year`、`Month`、`Day`としましょう。最後の`sep`引数は分割する基準となる文字を指定します。`df4`の`Date`は`"2020/06/29"`のように年月日が`"/"`で分けられているため、`"/"`を指定します。実際にやってみましょう。
```{r}
df4_S <- df4_S |>
separate(col = Date, into = c("Year", "Month", "Day"), sep = "/")
df4_S
```
新しく出来た変数は元の変数があった場所になります。ここまで来たら月ごとに新規感染者の記述統計量は計算できます。曜日ごとに行ったコードの`Week`を`Month`に変えるだけです。また、`Month`は数字のみで構成されたcharacter型であるため、このままでも問題なくソートされます。したがって、別途factor化の必要もありません(むろん、してもいいですし、むしろ推奨されます)。
```{r}
df4_S_Summary2 <- df4_S |>
group_by(Country, Month) |>
summarise(Sum = sum(Confirmed, na.rm = TRUE),
Mean = mean(Confirmed, na.rm = TRUE),
SD = sd(Confirmed, na.rm = TRUE),
.groups = "drop")
df4_S_Summary2
```
```{r}
df4_S_Summary2 |>
pivot_wider(names_from = Country,
values_from = Sum:SD) |>
relocate(Month, ends_with("Japan"), ends_with("Korea"))
```
平均値から見ると、日本は7都道府県を対象に緊急事態宣言が行われた4月がピークで緩やかに減少していますが、7月になって上がり気味です。韓国はカルト宗教団体におけるクラスターが発生した3月がピークで、6月からまた上がり気味ですね。傾向としては韓国が日本に1ヶ月先行しているように見えます。
それでは`separate()`関数の他の引数についても簡単に紹介します。まず、`sep`引数はnumeric型でも可能です。この場合、文字列内の位置を基準に分割されます。年月日が`20200629`のように保存されている場合は、何らかの基準となる文字がありません。この場合、`sep = c(4, 6)`にすると、「`"20200629"`の4文字目と5文字目の間で分割、6文字目と7文字目の間で分割」となります。また、`sep = c(-4, -2)`のように負の値も指定可能であり、この場合は右からの位置順で分割します。
また、`separate()`後は元の変数がなくなりますが、`remove = FALSE`の場合、元の変数 (ここでは`Date`)が残ります。他にも`convert`引数もあります。`convert = TRUE`の場合、適切なデータ型へ変換してくれます。デフォルト値は`FALSE`であり、この場合、character型として分割されます。先ほどの例だと`Year`も`Month`も`Day`も現在はcharacter型です。`separate()`内で`convert = TRUE`を追加すると、分割後の`Year`、`Month`、`Day`はnumeric型として保存されます。
`separate()`の詳細は`?separate`または、[レファレンスページ](https://tidyr.tidyverse.org/reference/separate.html)を参照してください。