第10回講義資料

データハンドリング (2)

スライド

新しいタブで開く

記述統計量

 記述統計量(descriptive statistics)とはある変数が持つ情報を要約した数値である。例えば、10人の学生の数学成績が格納されている以下のようなnumeric型ベクトルがあるとしよう。7番目の学生は試験を受けたものの、ポンコツな教師が解答用紙を紛失して欠損値となっている。

MathScore <- c(82, 45, 69, 94, 88, 73, NA, 51, 90, 63)
MathScore
 [1] 82 45 69 94 88 73 NA 51 90 63

 このクラスの数学成績について語る時、「うちのクラスの数学成績は82、45、69、94、…、63点ですよ」という人はいないだろう。数人のクラスならまだしも、数十人のクラスならあり得ない。我々は普段、「うちのクラスの数学成績はだいたいYY点ですよ」とか、「真ん中の成績はXX点ですよ」と言うだろう。また、クラス内の成績の格差/ばらつきを語るときも標準偏差や範囲(「できる子とできない子の点差は49点ですよ」)を言うのが普通である。このように元の長い情報を一つの数値として要約したものが記述統計量である。

 たとえば、MathScoreを代表する値(=代表値)としては平均値(mean)、中央値(median)、最頻値(mode)などがある。

# 平均値
mean(MathScore, na.rm = TRUE) 
[1] 72.77778
# 中央値
median(MathScore, na.rm = TRUE)
[1] 73

 また、MathScoreの格差、ばらつきの具合としては分散(variance)、標準偏差(standard deviation)、四分位範囲(interquartile range)、範囲(range)がある。

# 不偏分散
var(MathScore, na.rm = TRUE)
[1] 302.4444
# 不偏標準偏差
sd(MathScore, na.rm = TRUE)
[1] 17.39093
# 四分位範囲
IQR(MathScore, na.rm = TRUE)
[1] 25
# 範囲
max(MathScore, na.rm = TRUE) - min(MathScore, na.rm = TRUE)
[1] 49

 今回は{dplyr}パッケージを用いて変数の記述統計量を計算する方法を紹介する。まず、今回の実習用データを読み込む。第9回に使用したデータと同じデータを使う。

library(tidyverse)
df <- read_csv("Data/Micro09.csv")

 記述統計を計算する関数はsummarise()である。使い方は以下の通りである。

データフレーム %>%
  summarise(記述統計の関数(変数名, ...))

 たとえば、Population列の平均値を計算する場合は、

df %>%
  summarise(mean(Population))
# A tibble: 1 × 1
  `mean(Population)`
               <dbl>
1          41737773.

と入力する。HDI_2018のように欠損値が含まれている変数ならna.rm = TRUEを忘れないようにしよう。

df %>%
  summarise(mean(HDI_2018, na.rm = TRUE))
# A tibble: 1 × 1
  `mean(HDI_2018, na.rm = TRUE)`
                           <dbl>
1                          0.713

 平均値を計算するmean()以外にも様々な変数が使える。ここでは代表的な記述統計量の関数を紹介する。他にも様々な記述統計量があるが、詳細は教科書第13.1.2章を参照されたい。

  • mean(): 平均値
  • median(): 中央値
  • sd(): 不偏標準偏差
  • var(): 不偏分散
  • IQR(): 四分位範囲
  • min()max(): 最小値と最大値

 summarise()内には一つの変数、あるいは一つの記述統計量のみ書く必要はない。()内にいくらでも書ける。たとえば、dfPopulationAreaの平均値(mean())と標準偏差(sd())を計算する場合、計4つの記述統計量を計算することとなる。

df %>%
  summarise(mean(Population), # Populationの平均値
            sd(Population),   # Populationの不偏標準偏差
            mean(Area),       # Areaの平均値
            sd(Area))         # Areaの不偏標準偏差
# A tibble: 1 × 4
  `mean(Population)` `sd(Population)` `mean(Area)` `sd(Area)`
               <dbl>            <dbl>        <dbl>      <dbl>
1          41737773.       151270298.      696069.   1872412.

 出力結果はデータフレーム(tibble)形式で表示されるが、列名が長く、非常に読みにくい。この列名を修正するためにはsummarise()の後にrename()を使うことで修正することもできるが、summarise()内で指定することもできる。たとえば、dfPopulationAreaの平均値(mean())と標準偏差(sd())を計算し、結果の列名をMean_PopSD_Popなどとする。

df %>%
  summarise(Mean_Pop  = mean(Population),
            SD_Pop    = sd(Population),
            Mean_Area = mean(Area),
            SD_Area   = sd(Area))
# A tibble: 1 × 4
   Mean_Pop     SD_Pop Mean_Area  SD_Area
      <dbl>      <dbl>     <dbl>    <dbl>
1 41737773. 151270298.   696069. 1872412.

 出力される列名 = 記述統計の関数()を指定することで、このように出力結果が読みやすくなる。

グルーピング

 特定の変数の記述統計量を計算する場合はmean()sd()などの関数のみを使った方が効率的なケースが多い。しかし、グループごとに記述統計量を計算する場合は、{dplyr}パッケージが大変便利である。{dplyr}を使わずに大陸ごとのPPP_per_capitaの平均値を計算するとしよう。たとえば、Continentの値が"Africa"である国のPPP_per_capitaの平均値を計算する場合は以下のように書く。

# PPP_per_capitaは欠損値が含まれているため、na.rm = TRUEを指定
#(指定しないと、結果はNAとなる)
mean(df$PPP_per_capita[df$Continent == "Africa"], na.rm = TRUE)
[1] 5667.087

 これを全ての大陸に対して同じ計算を行う場合、以下のようなコードになる。

mean(df$PPP_per_capita[df$Continent == "Africa"], na.rm = TRUE)
[1] 5667.087
mean(df$PPP_per_capita[df$Continent == "America"], na.rm = TRUE)
[1] 18100.29
mean(df$PPP_per_capita[df$Continent == "Asia"], na.rm = TRUE)
[1] 22728.13
mean(df$PPP_per_capita[df$Continent == "Europe"], na.rm = TRUE)
[1] 37782.59
mean(df$PPP_per_capita[df$Continent == "Oceania"], na.rm = TRUE)
[1] 27572.65

 たった5行のコードであるが、一行一行がかなり長く、可読性も優れているとは言えない。ここで{dplyr}のgroup_by()関数が力を発揮する。

データフレーム名 %>%
  group_by(グループ化する変数名) %>%
  summarise(...)

 たとえば、dfContinentでデータをグループ化し、PPP_per_capitaの平均値を計算する場合、summarise()の前にgroup_by(Continent)を使いしてパイプをつなぐことでContinentの値ごとにPPP_per_capitaの平均値が計算される。

# PPP_per_capitaが欠損している国もあるので、na.rm = TRUEを追加
df %>%
  group_by(Continent) %>%
  summarise(Mean_PPP = mean(PPP_per_capita, na.rm = TRUE))
# A tibble: 5 × 2
  Continent Mean_PPP
  <chr>        <dbl>
1 Africa       5667.
2 America     18100.
3 Asia        22728.
4 Europe      37783.
5 Oceania     27573.

 コードが3行になっただけではなく、可読性も大きく改善されていることが分かる。group_by()関数は二つ以上の変数でグルーピングすることもできる。たとえば、dfContinentG20でデータをグループ化し、HDI_2018の平均値を計算してみよう。

df %>%
  group_by(Continent, G20) %>%
  summarise(Mean_HDI = mean(HDI_2018, na.rm = TRUE))
`summarise()` has grouped output by 'Continent'. You can override using the
`.groups` argument.
# A tibble: 10 × 3
# Groups:   Continent [5]
   Continent   G20 Mean_HDI
   <chr>     <dbl>    <dbl>
 1 Africa        0    0.550
 2 Africa        1    0.705
 3 America       0    0.727
 4 America       1    0.84 
 5 Asia          0    0.710
 6 Asia          1    0.798
 7 Europe        0    0.859
 8 Europe        1    0.877
 9 Oceania       0    0.729
10 Oceania       1    0.938

 Continentの値は5種類、G20の値は2種類であるため、計10個のグループができる。ちなみにこれを{dplyr}を使わず行うなら以下のようなコードになる。

mean(df$HDI_2018[df$Continent == "Africa" & df$G20 == 0], na.rm = TRUE)
mean(df$HDI_2018[df$Continent == "Africa" & df$G20 == 1], na.rm = TRUE)
mean(df$HDI_2018[df$Continent == "America" & df$G20 == 0], na.rm = TRUE)
mean(df$HDI_2018[df$Continent == "America" & df$G20 == 1], na.rm = TRUE)
mean(df$HDI_2018[df$Continent == "Asia" & df$G20 == 0], na.rm = TRUE)
mean(df$HDI_2018[df$Continent == "Asia" & df$G20 == 1], na.rm = TRUE)
mean(df$HDI_2018[df$Continent == "Europe" & df$G20 == 0], na.rm = TRUE)
mean(df$HDI_2018[df$Continent == "Europe" & df$G20 == 1], na.rm = TRUE)
mean(df$HDI_2018[df$Continent == "Oceania" & df$G20 == 0], na.rm = TRUE)
mean(df$HDI_2018[df$Continent == "Oceania" & df$G20 == 1], na.rm = TRUE)

 {dplyr}の素晴らしさが分かるだろう。次の内容に進む前に、以下のような謎のメッセージが出力されたのではないだろうか。

## `summarise()` has grouped output by 'Continent'. You can override using 
the `.groups` argument.

 このメッセージの理由は教科書第13.2章に譲るが、この気持ち悪いメッセージをなくすためには、とりあえず、group_by()の後にsummarise()を使う場合、summarise()の最後に.groups = "drop"を追加する。

df %>%
  group_by(Continent, G20) %>%
  summarise(Mean_HDI = mean(HDI_2018, na.rm = TRUE),
            .groups  = "drop")
# A tibble: 10 × 3
   Continent   G20 Mean_HDI
   <chr>     <dbl>    <dbl>
 1 Africa        0    0.550
 2 Africa        1    0.705
 3 America       0    0.727
 4 America       1    0.84 
 5 Asia          0    0.710
 6 Asia          1    0.798
 7 Europe        0    0.859
 8 Europe        1    0.877
 9 Oceania       0    0.729
10 Oceania       1    0.938

 これで謎のメッセージが出力されなくなっただろう。

 グルーピングを行う場合、各グループに属するケース数を調べたい場合もあろう。たとえば、大陸ごとにPPP_per_capitaの平均値と標準偏差を出すだけでなく、各大陸に属する国数も表示したいとする。この時に使う関数がn()である。()内に別途の引数は不要である。

df %>%
  group_by(Continent) %>%
  summarise(Mean_PPP = mean(PPP_per_capita, na.rm = TRUE),
            SD_PPP   = sd(PPP_per_capita, na.rm = TRUE),
            Cases    = n())
# A tibble: 5 × 4
  Continent Mean_PPP SD_PPP Cases
  <chr>        <dbl>  <dbl> <int>
1 Africa       5667.  6015.    54
2 America     18100. 12601.    36
3 Asia        22728. 24067.    42
4 Europe      37783. 21276.    50
5 Oceania     27573. 21984.     4

 最後に、詳細は解説しないが、across()関数を利用することで、複数の変数に対して複数の記述統計量をより短いコードで計算することができる。たとえば、dfPopulationからPPP列まで平均値と標準偏差を計算し、結果の変数名は元の変数名_Mean元の変数名_SDとするコードを書いてみよう。

df %>%
  summarise(Population_Mean = mean(Population, na.rm = TRUE),
            Population_SD   = sd(Population, na.rm = TRUE),
            Area_Mean       = mean(Area, na.rm = TRUE),
            Area_SD         = sd(Area, na.rm = TRUE),
            GDP_Mean        = mean(GDP, na.rm = TRUE),
            GDP_SD          = sd(GDP, na.rm = TRUE),
            PPP_Mean        = mean(PPP, na.rm = TRUE),
            PPP_SD          = sd(PPP, na.rm = TRUE),)
# A tibble: 1 × 8
  Population_Mean Population_SD Area_Mean  Area_SD GDP_Mean   GDP_SD PPP_Mean
            <dbl>         <dbl>     <dbl>    <dbl>    <dbl>    <dbl>    <dbl>
1       41737773.    151270298.   696069. 1872412.  473031. 1999504.  717953.
# … with 1 more variable: PPP_SD <dbl>

 以上の作業は、across()関数を使う場合、以下のようにたった4行でできる。

df %>%
  summarise(across(Population:PPP,
                   .fns = list(Mean = ~mean(.x, na.rm = TRUE),
                               SD   = ~mean(.x, na.rm = TRUE))))
# A tibble: 1 × 8
  Population_Mean Population_SD Area_Mean Area_SD GDP_Mean  GDP_SD PPP_Mean
            <dbl>         <dbl>     <dbl>   <dbl>    <dbl>   <dbl>    <dbl>
1       41737773.     41737773.   696069. 696069.  473031. 473031.  717953.
# … with 1 more variable: PPP_SD <dbl>

 across()関数の詳細については教科書第13.1章を参照されたい。ただし、無名関数(ラムダ式)の知識が必要である(そこまで難しいものではない)。

変数の計算

 以下ではmutate()関数を利用して、データフレームの変数を用いた計算を行い、新しい列として追加する方法を紹介する。mutate()関数の使い方は以下の通りである。

データフレーム名 %>%
  mutate(新しい列名 = 計算式)

 ここで、新しい列名が既に存在するの列名である場合、既存の列が上書きされる。一方、データフレームに存在しない列名の場合、新しい列が最後の列として追加される1

 ここでは、dfPopulationAreaで割り(=人口密度)、Densityという名の列として追加してみよう。{dplyr}を使わずにこの処理を行う場合、以下のようなコードとなる。

df$Density <- df$Population / df$Area

 一方、{dplyr}のmutate()関数を使用する場合、以下のようなコードになる。

df %>%
  mutate(Density = Population / Area)
# A tibble: 186 × 19
   Country  Population   Area    GDP     PPP GDP_per_capita PPP_per_capita    G7
   <chr>         <dbl>  <dbl>  <dbl>   <dbl>          <dbl>          <dbl> <dbl>
 1 Afghani…   38928346 6.53e5 1.91e4  8.27e4           491.          2125.     0
 2 Albania     2877797 2.74e4 1.53e4  3.97e4          5309.         13781.     0
 3 Algeria    43851044 2.38e6 1.70e5  4.97e5          3876.         11324.     0
 4 Andorra       77265 4.7 e2 3.15e3 NA              40821.            NA      0
 5 Angola     32866272 1.25e6 9.46e4  2.19e5          2879.          6649.     0
 6 Antigua…      97929 4.4 e2 1.73e3  2.08e3         17643.         21267.     0
 7 Argenti…   45195774 2.74e6 4.50e5  1.04e6          9949.         22938.     0
 8 Armenia     2963243 2.85e4 1.37e4  3.84e4          4614.         12974.     0
 9 Austral…   25499884 7.68e6 1.39e6  1.28e6         54615.         50001.     0
10 Austria     9006398 8.24e4 4.46e5  5.03e5         49555.         55824.     0
# … with 176 more rows, and 11 more variables: G20 <dbl>, OECD <dbl>,
#   HDI_2018 <dbl>, Polity_Score <dbl>, Polity_Type <chr>, FH_PR <dbl>,
#   FH_CL <dbl>, FH_Total <dbl>, FH_Status <chr>, Continent <chr>,
#   Density <dbl>

 出力される画面にDensity列が見当たらない。これは新しく追加されたDensity列が最後の列であり、画面に収まらないからである。むろん、Density列は問題なく追加されており、出力画面の下段に省略されている変数名に含まれていることが分かる。

 もし、新しく追加される列の順番を指定したい場合はrelocate() 同様、.after、または.beforeを指定すればよい。以下のコードはDensity列をArea列のに追加するコードである。

df %>%
  mutate(Density = Population / Area, 
         .after  = Area)
# A tibble: 186 × 19
   Country             Population    Area Density     GDP     PPP GDP_per_capita
   <chr>                    <dbl>   <dbl>   <dbl>   <dbl>   <dbl>          <dbl>
 1 Afghanistan           38928346  652860   59.6   1.91e4  8.27e4           491.
 2 Albania                2877797   27400  105.    1.53e4  3.97e4          5309.
 3 Algeria               43851044 2381740   18.4   1.70e5  4.97e5          3876.
 4 Andorra                  77265     470  164.    3.15e3 NA              40821.
 5 Angola                32866272 1246700   26.4   9.46e4  2.19e5          2879.
 6 Antigua and Barbuda      97929     440  223.    1.73e3  2.08e3         17643.
 7 Argentina             45195774 2736690   16.5   4.50e5  1.04e6          9949.
 8 Armenia                2963243   28470  104.    1.37e4  3.84e4          4614.
 9 Australia             25499884 7682300    3.32  1.39e6  1.28e6         54615.
10 Austria                9006398   82409  109.    4.46e5  5.03e5         49555.
# … with 176 more rows, and 12 more variables: PPP_per_capita <dbl>, G7 <dbl>,
#   G20 <dbl>, OECD <dbl>, HDI_2018 <dbl>, Polity_Score <dbl>,
#   Polity_Type <chr>, FH_PR <dbl>, FH_CL <dbl>, FH_Total <dbl>,
#   FH_Status <chr>, Continent <chr>

応用

 それでは前回と今回の内容を復習してみよう。最初の問題は「各国が世界人口に占める割合を計算し、降順で出力する」ことである。この問題を解くためには以下のような手順でデータをハンドリングする必要がある。

  1. dfPopulationの合計をTotal_Popという列として追加する。
    • mutate()使用
  2. PopulationTotal_Popで割り、100を掛ける。結果はShare_Popという名の列としてPopulation後に追加する。
    • mutate()使用
  3. CountryからShare_Popまでの列のみ残すが、Total_Pop列は除外する。
    • select()使用
  4. Share_Popが大きい順で行を並び替える
    • arrange()使用

 以上の計算仮定の内、1と2は一つのmutate()関数内にまとめることができる。したがって、データ %>% mutate() %>% select() %>% arrange()の順番でコードを書く必要がある。

df %>% 
  mutate(Total_Pop = sum(Population),              # 手順1
         Share_Pop = Population / Total_Pop * 100, # 手順2
         .after    = Population) %>%               # 手順2
  select(Country:Share_Pop, -Total_Pop) %>%        # 手順3
  arrange(desc(Share_Pop))                         # 手順4
# A tibble: 186 × 3
   Country       Population Share_Pop
   <chr>              <dbl>     <dbl>
 1 China         1447470092     18.6 
 2 India         1380004385     17.8 
 3 United States  334308644      4.31
 4 Indonesia      273523615      3.52
 5 Pakistan       220892340      2.85
 6 Brazil         212559417      2.74
 7 Nigeria        206139589      2.66
 8 Bangladesh     164689383      2.12
 9 Russia         145934462      1.88
10 Mexico         128932753      1.66
# … with 176 more rows

 次の問題は「G7、G20、OECDのいずれかに加盟している国を"先進国"、それ以外は"その他"とし、二つのグループの人口密度、人間開発指数、民主主義度の平均値を出力する」ことである。

  1. dfDevelopedという列を追加し、G7G20OECDのいずれかに加盟した国なら"先進国"、それ以外なら"その他"とする。
    • mutate()使用
  2. 人口密度をDensityという名の列として追加する。
    • mutate()使用
  3. HDI_2018Polity_Scoreのいずれかが欠損した行を除外する。
    • filter()使用
  4. Developed変数でデータをグルーピングする。
    • group_by()使用
  5. HDI_2018Polity_ScoreDensityの平均値を求める。
    • summarise()使用
  6. df2という名前のオブジェクトとして作業環境内に格納する。
    • 代入演算子(<-)使用
  7. df2を出力する。

 今回も手順1と2は同じmuate()関数を使用するため、新しいオブジェクト名 <- データ %>% mutate() %>% filter() %>% group_by() %>% summarise()の順番でコードを各必要がある。

df2 <- df %>%
  mutate(Developed = G7 + G20 + OECD,
         Developed = if_else(Developed > 1, "先進国", "その他"), # 上書き
         Density   = Population / Area) %>%
  filter(!is.na(HDI_2018), !is.na(Polity_Score)) %>%
  group_by(Developed) %>%
  summarise(Density = mean(Density),
            HDI     = mean(HDI_2018),
            Polity  = mean(Polity_Score))

df2
# A tibble: 2 × 4
  Developed Density   HDI Polity
  <chr>       <dbl> <dbl>  <dbl>
1 その他       197. 0.695   3.92
2 先進国       174. 0.892   7.91

 いきなり出てきたif_else()関数だが、これは何だろう。これから解説する。

名目変数の扱い方

 先ほどのdf2だが、出力順番が"その他" \(\rightarrow\) "先進国"になっている。これを"先進国" \(\rightarrow\) "その他"の順番にするならどうすれば良いだろうか。summarise()を行う場合、グルーピング変数のアルファベット順で表示される。ただし、これはアルファベットや数字に限定される話であり、日本語の場合、50音順になるとは限らない。ひらがな、カタカナなら50音順になるが、漢字はそうではない。そもそも読み方が複数あるので、どの基準にすればもよく分からない。しかも、漢字は日本だけが使う文字でもないため、読み方も国によって異なるだろう。この場合、summarise()を使う前にグルーピング変数をfactor型に変換する必要がある。

 ある変数をfactor型に変換する場合はfactor()関数を使う。第一引数は元となる変数名であり、levels =に順番を指定する。スペルミスに注意すること。

df %>%
  mutate(Developed = G7 + G20 + OECD,
         Developed = if_else(Developed > 1, "先進国", "その他"),
         Developed = factor(Developed, levels = c("先進国", "その他")),
         Density   = Population / Area) %>%
  filter(!is.na(HDI_2018), !is.na(Polity_Score)) %>%
  group_by(Developed) %>%
  summarise(Density = mean(Density),
            HDI     = mean(HDI_2018),
            Polity  = mean(Polity_Score))
# A tibble: 2 × 4
  Developed Density   HDI Polity
  <fct>       <dbl> <dbl>  <dbl>
1 先進国       174. 0.892   7.91
2 その他       197. 0.695   3.92

 それではこれまで放置してきたif_else()について解説しよう。if_else()は後ほど解説するcase_when()recode()のように変数のリコーディング(re-coding)に使う関数である。if_else()関数の使い方は以下の通りである。

if_else(条件式, TRUEの場合の戻り値, FALSEの場合の戻り値)

 たとえば、dfOECD1なら"OECD加盟国"、それ以外なら"OECD非加盟国"に変換し、OECD_Jという列として追加する場合、以下のコードになる。リコーディングの場合、既存の変数を上書きするより、新しい列として追加することを推奨する。

df <- df %>%
  mutate(OECD_J = if_else(OECD == 1, "OECD加盟国", "OECD非加盟国"))

df
# A tibble: 186 × 19
   Country  Population   Area    GDP     PPP GDP_per_capita PPP_per_capita    G7
   <chr>         <dbl>  <dbl>  <dbl>   <dbl>          <dbl>          <dbl> <dbl>
 1 Afghani…   38928346 6.53e5 1.91e4  8.27e4           491.          2125.     0
 2 Albania     2877797 2.74e4 1.53e4  3.97e4          5309.         13781.     0
 3 Algeria    43851044 2.38e6 1.70e5  4.97e5          3876.         11324.     0
 4 Andorra       77265 4.7 e2 3.15e3 NA              40821.            NA      0
 5 Angola     32866272 1.25e6 9.46e4  2.19e5          2879.          6649.     0
 6 Antigua…      97929 4.4 e2 1.73e3  2.08e3         17643.         21267.     0
 7 Argenti…   45195774 2.74e6 4.50e5  1.04e6          9949.         22938.     0
 8 Armenia     2963243 2.85e4 1.37e4  3.84e4          4614.         12974.     0
 9 Austral…   25499884 7.68e6 1.39e6  1.28e6         54615.         50001.     0
10 Austria     9006398 8.24e4 4.46e5  5.03e5         49555.         55824.     0
# … with 176 more rows, and 11 more variables: G20 <dbl>, OECD <dbl>,
#   HDI_2018 <dbl>, Polity_Score <dbl>, Polity_Type <chr>, FH_PR <dbl>,
#   FH_CL <dbl>, FH_Total <dbl>, FH_Status <chr>, Continent <chr>, OECD_J <chr>

 それではまず、OECD変数でデータをグルーピングし、HDI_2018FH_Totalの平均値を計算してみよう。

df %>%
  group_by(OECD) %>%
  summarise(HDI = mean(HDI_2018, na.rm = TRUE),
            FH  = mean(FH_Total, na.rm = TRUE))
# A tibble: 2 × 3
   OECD   HDI    FH
  <dbl> <dbl> <dbl>
1     0 0.667  49.9
2     1 0.894  89.1

 続いて、OECDでなく、先ほど作成したOECD_J列でグルーピングしてみよう。

df %>%
  group_by(OECD_J) %>%
  summarise(PPP = mean(PPP_per_capita, na.rm = TRUE),
            HDI = mean(HDI_2018, na.rm = TRUE),
            FH  = mean(FH_Total, na.rm = TRUE))
# A tibble: 2 × 4
  OECD_J          PPP   HDI    FH
  <chr>         <dbl> <dbl> <dbl>
1 OECD加盟国   46000. 0.894  89.1
2 OECD非加盟国 14229. 0.667  49.9

 出力される結果の第1列がOECD_Jになっているが、これをOECDに変更したい場合はgroup_by(OECD_J)group_by(OECD = OECD_J)に変更すれば良い。

 if_else()は条件が一つのみの場合に使用する関数である。もし、条件式を複数使いたい場合はどうすれば良いだろうか。ここではまず、mutate()内にcase_when()を使用する方法から紹介する。

データフレーム名 %>%
  mutate(新しい変数名 = case_when(条件1 ~ 新しい値,
                                条件2 ~ 新しい値,
                                ...
                                TRUE ~ 新しい値))

 TRUE ~ 新しい値は「上記の条件全てが満たされない場合の値」を意味する。ここではdfContinent列を日本語にし、Continent_Jとして追加してみよう。

df %>%
  mutate(Continent_J = case_when(Continent == "Africa"  ~ "アフリカ",
                                 Continent == "America" ~ "アメリカ",
                                 Continent == "Asia"    ~ "アジア",
                                 Continent == "Europe"  ~ "ヨーロッパ",
                                 TRUE                   ~ "オセアニア")) %>%
  group_by(大陸 = Continent_J) %>%
  # 日本語は非推奨だが、一応使える(_と.を除く特殊記号不可)
  summarise(OECD加盟国比率 = mean(OECD),
            国家数         = n())
# A tibble: 5 × 3
  大陸       OECD加盟国比率 国家数
  <chr>               <dbl>  <int>
1 アジア             0.0714     42
2 アフリカ           0          54
3 アメリカ           0.139      36
4 オセアニア         0.5         4
5 ヨーロッパ         0.54       50

 この「新しい値」は条件の数だけ存在する必要はない。たとえば、Continent列の値が"Asia""Oceania""America"なら"Asia-Pafific"に、それ以外は"Others"に変更し、その結果をAPという名の新しい列として追加するとしよう。

df %>%
  mutate(AP = case_when(Continent == "Africa"  ~ "Others",
                        Continent == "America" ~ "Asia-Pacific",
                        Continent == "Asia"    ~ "Asia-Pacific",
                        Continent == "Europe"  ~ "Others",
                        TRUE                   ~ "Asia-Pacific")) %>%
  group_by(Continent = AP) %>%
  summarise(Population = mean(Population),
            Countries  = n())
# A tibble: 2 × 3
  Continent    Population Countries
  <chr>             <dbl>     <int>
1 Asia-Pacific  68064598.        82
2 Others        20980085.       104

 以上の例は戻り値が2種類、つまり、"Asia-Pacific""Others"のみである。実はこの場合、if_else()関数を使うこともできる。ここで便利な演算子が%in%演算子である。これは左側の値が右側のベクトルに含まれていればTRUE、含まれていなければFALSEが返ってくる関数である。たとえば、dfContinent列が"Asia""Oceania""America"のいずれかに該当するか確認してみよう。

df$Continent %in% c("Asia", "Oceania", "America")
  [1]  TRUE FALSE FALSE FALSE FALSE  TRUE  TRUE FALSE  TRUE FALSE FALSE  TRUE
 [13]  TRUE  TRUE  TRUE FALSE FALSE  TRUE FALSE  TRUE  TRUE FALSE FALSE  TRUE
 [25]  TRUE FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE  TRUE FALSE FALSE  TRUE
 [37]  TRUE  TRUE FALSE FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE FALSE
 [49] FALSE  TRUE  TRUE  TRUE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE  TRUE
 [61] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE  TRUE FALSE
 [73]  TRUE  TRUE FALSE  TRUE FALSE FALSE  TRUE  TRUE  TRUE  TRUE FALSE  TRUE
 [85] FALSE  TRUE  TRUE  TRUE  TRUE FALSE  TRUE FALSE  TRUE  TRUE  TRUE FALSE
 [97]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE FALSE
[109] FALSE FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE  TRUE
[121] FALSE  TRUE  TRUE FALSE FALSE FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE
[133]  TRUE  TRUE FALSE FALSE  TRUE FALSE FALSE FALSE  TRUE  TRUE  TRUE FALSE
[145] FALSE  TRUE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE
[157] FALSE  TRUE FALSE  TRUE FALSE FALSE  TRUE  TRUE  TRUE FALSE  TRUE  TRUE
[169] FALSE  TRUE FALSE FALSE FALSE FALSE  TRUE FALSE  TRUE  TRUE  TRUE  TRUE
[181]  TRUE  TRUE FALSE  TRUE FALSE FALSE

 これをif_else()関数の条件式として使用する。

df %>%
  mutate(AP = if_else(Continent %in% c("Asia", "America", "Oceania"), "Asia-Pacific", "Others")) %>%
  group_by(Continent = AP) %>%
  summarise(Population = mean(Population),
            Countries  = n())
# A tibble: 2 × 3
  Continent    Population Countries
  <chr>             <dbl>     <int>
1 Asia-Pacific  68064598.        82
2 Others        20980085.       104

 最後にrecode()関数について解説する。これまで紹介したif_else()case_when()は条件式を使うため、==!=%in%以外にも><=なども使える。ただし、書き方がやや複雑である。recode()は使う条件式が==のみの場合、if_else()case_when()を代替可能な関数である。ここでは例だけ紹介しよう。

df %>%
  mutate(AP = recode(Continent,
                     "Asia"    = "Asia-Pacific",
                     "America" = "Asia-Pacific",
                     "Oceania" = "Asia-Pacific",
                     .default  = "Others")) %>%
  group_by(Continent = AP) %>%
  summarise(Population = mean(Population),
            Countries  = n())
# A tibble: 2 × 3
  Continent    Population Countries
  <chr>             <dbl>     <int>
1 Asia-Pacific  68064598.        82
2 Others        20980085.       104

 recode()の第一引数はリコーディングする変数名であり、.defaultはいずれの条件にも該当しない場合の値を意味する。

欠損値の扱い方

 世論調査などの場合、欠損値がNAでなく、999""などの場合がある。たとえば、以下のようなmy_dataというデータがあるとしよう。

my_data <- tibble(ID       = 1:10,
                  Age      = c(32, 35, 57, 999, 74, 66, 999, 49, 78, 67),
                  HighEduc = c(1, 0, 0, 1, 0, 9, 1, 1, 9, 9))
my_data
# A tibble: 10 × 3
      ID   Age HighEduc
   <int> <dbl>    <dbl>
 1     1    32        1
 2     2    35        0
 3     3    57        0
 4     4   999        1
 5     5    74        0
 6     6    66        9
 7     7   999        1
 8     8    49        1
 9     9    78        9
10    10    67        9

 IDは回答者の識別番号、Ageは回答者の年齢、HighEducは回答者が大卒以上なら1、それ以外は0を意味する。そして、年齢を回答しなかった回答者の年齢は999、学歴については9となっている。ここで回答者の年齢と学歴の平均値を出せばどうなるだろうか。

mean(my_data$Age)
[1] 245.6
mean(my_data$HighEduc)
[1] 3.1

 回答者の平均年齢は245.6歳(!!)、学歴の平均値は3.1となっている。医学の発展によって人間の寿命が伸びたとしても、最大値が1のはずのHighEducの平均値が3.1ということはナンセンスである。データ分析の前に欠損値の確認と処理は必須であり、これを怠ると研究結果に大きな歪みな生じかねない。とりわけ、欠損値がNAでなく、9999""などになっているケースはより注意が必要である。今回の実習データは既に欠損値の処理済み、つまり、欠損している値はNAになっている。ここでは上のmy_dataの使って考えてみよう。

 まず、YoungAge変数を作成し、Ageが39以下なら1、それ以外は0にする。ただし、999ならNAとする。続いて、HighEduc2変数を作成し、HighEducが1なら"大卒以上"、それ以外は"大卒未満"にする。ただし、9ならNAとする。この作業を行うにはmutate()内にcase_when()を使えば良いだろう。

my_data %>%
  mutate(YoungAge  = case_when(Age == 999 ~ NA,
                               Age <=  39 ~ 1,
                               TRUE       ~ 0),
         HighEduc2 = case_when(HighEduc == 9 ~ NA,
                               HighEduc == 1 ~ "大卒以上",
                               TRUE          ~ "大卒未満"))
Error in `mutate()`:
! Problem while computing `YoungAge = case_when(Age == 999 ~ NA, Age <=
  39 ~ 1, TRUE ~ 0)`.
Caused by error in `` names(message) <- `*vtmp*` ``:
! 'names' attribute [1] must be the same length as the vector [0]

 エラーが返ってきた。なぜだろう。問題はNAである。確かにRにおいて欠損値はNAのみである。しかし、{tidyverse}の世界は、同じNAでもnumeric型のNAとcharcter型のNAは区別される。上のコードの場合、YoungAgeはnumeric型、HighEduc2はcharacter型変数になるだろう。case_when()if_else()などを使う場合、NAでなく、生成される列のデータ型に応じてNA_real_(numeric型)、またはNA_character_(character型)を使用(logical型ならNAのままでOK)する必要がある。

my_data %>%
  mutate(YoungAge  = case_when(Age == 999 ~ NA_real_,
                               Age <=  39 ~ 1,
                               TRUE       ~ 0),
         HighEduc2 = case_when(HighEduc == 9 ~ NA_character_,
                               HighEduc == 1 ~ "大卒以上",
                               TRUE          ~ "大卒未満"))
# A tibble: 10 × 5
      ID   Age HighEduc YoungAge HighEduc2
   <int> <dbl>    <dbl>    <dbl> <chr>    
 1     1    32        1        1 大卒以上 
 2     2    35        0        1 大卒未満 
 3     3    57        0        0 大卒未満 
 4     4   999        1       NA 大卒以上 
 5     5    74        0        0 大卒未満 
 6     6    66        9        0 <NA>     
 7     7   999        1       NA 大卒以上 
 8     8    49        1        0 大卒以上 
 9     9    78        9        0 <NA>     
10    10    67        9        0 <NA>     

 もう一つの例として特定の値を欠損値とし、それ以外の値は元も値にする場合を考えよう。つまり、AgeHighEduc列においてそれぞれ999、9をNAにし、そのまま上書きするものである。例えばAgeAgeの値が999なら(Age == 999)、NA_real_を返し、それ以外の場合は既存のAgeの値を取る。この作業は以下のコードで実行できる。

my_data %>%
  mutate(Age      = if_else(Age == 999, NA_real_, Age),
         HighEduc = if_else(HighEduc == 9, NA_real_, HighEduc))
# A tibble: 10 × 3
      ID   Age HighEduc
   <int> <dbl>    <dbl>
 1     1    32        1
 2     2    35        0
 3     3    57        0
 4     4    NA        1
 5     5    74        0
 6     6    66       NA
 7     7    NA        1
 8     8    49        1
 9     9    78       NA
10    10    67       NA

 欠損を意味する値が複数の場合もある。my_dataAge999のみが欠損値であるが、世論調査によっては888999-1のように複数の欠損値が存在するケースがある。この場合はcase_when()を使うか、if_else()関数内にOR演算子(| / %in%)を使えば良い。

 最後に欠損値処理に特化した{naniar}パッケージのreplace_with_na()関数を紹介する。引数はリスト型オブジェクトであり、リストの各要素は変数名 = 欠損値の値となる。たとえば、Ageの欠損値は999だからAge = 999である。これは条件式ではないため、==でなく、=を使う。欠損値の値が複数ある場合は、変数名 = c(値1, 値2, ...)のように書く。

library(naniar) # 事前に install.package(naniar) でインストール
my_data %>%
  # Ageは999、HighEducは9が欠損値
  replace_with_na(list(Age = 999, HighEduc = 9))
# A tibble: 10 × 3
      ID   Age HighEduc
   <int> <dbl>    <dbl>
 1     1    32        1
 2     2    35        0
 3     3    57        0
 4     4    NA        1
 5     5    74        0
 6     6    66       NA
 7     7    NA        1
 8     8    49        1
 9     9    78       NA
10    10    67       NA

 他にも似たような関数として{expss}のna_if()関数などがあるが、自分が使いやすいものを使えば良い。

教科書

  • データのグルーピングと要約: 教科書第13.1章第13.2章
  • 変数の計算: 教科書第13.3章第13.4章
  • Factor型変数の扱い: 教科書第14章
    • Factor型については可視化の時にも解説する。詳しい内容は教科書を参照すること

  1. relocate()同様、.before.afterで指定することもできる。↩︎