タグ:ifelse

データを扱う中で「外れ値を外す方法」に関して検討してみます。

apply関数の記事で以下のコメントがありました。




大変勉強になりました。
列ごとに、ファイルの値(NAがある)が2SDより高い値をNAにしたいです。
Data01 <-read_excel("raw.xlsx",1)
Data02 <-apply(Data01, 2, sd, na.rm = TRUE)
Data01[Data01 > 2*Data02]<- NA
以上のコードをして最後は列ごとにうまく出来なかったです。
ご提言があれば大変嬉しいです。

自分もそうですが、実際にやってみるとうまくいかないことってたくさんあります。
今回は質問を一緒に考えながらどう考えるか?どんな解決方法があるかを考えて、今までの理解を深めるきっかけになればと思います。

1.データの作成

コードを確認してみます。
Data01 <-read_excel("raw.xlsx",1)
これはraw.xlsxの1シート目を読み込むという意味なので、今回は仮のデータを作ります。

set.seed(2020)
a <- c(rnorm(998,10,1),NA,30)
b <- c(rnorm(998,20,1),NA,40)
c <- c(rnorm(998,30,1),NA,50)
Data01 <- data.frame(a,b,c)
head(Data01)
簡単に言うと以下のとおりです。
a : 平均10, 標準偏差1のデータ998個にNAと30を加えた1000個のデータ
b : 平均20, 標準偏差1のデータ998個にNAと40を加えた1000個のデータ
c : 平均30, 標準偏差1のデータ998個にNAと50を加えた1000個のデータ

スクリーンショット 2020-03-28 2.34.45
この赤色が2SDを超えたデータです。やりたいことはこの赤い点のデータを削除することです(これの是非については今回問いません)

2.apply関数のおさらい

次の行を確認します。
Data02 <-apply(Data01, 2, sd, na.rm = TRUE)
apply関数【2-1】Rのfor関数、apply関数を使ってまとめて標準偏差などの統計量を求める方法で紹介しました。下はイメージ図です。
スクリーンショット 2020-03-27 13.46.36
上図ではsd関数(標準偏差)を求めました。
また欠損値(NA)があるのでrm.na = TRUEをつけることでNAの値を外して計算してくれます。

スクリーンショット 2020-03-27 13.54.01
それぞれの列の標準偏差が表示されました。
ただ元データが2標準偏差を超えているかどうかを確認するにはこれだけでは足りません。


3.どう考えるか?
スクリーンショット 2020-03-28 4.00.30

それぞれのデータが2SDを超えているかどうかを判定する必要があります。
具体的には(平均-2SD) < それぞれの値 < (平均 + 2SD)の条件を満たしているかどうかとなります。

そのため以下の2つを行えばなんとかできそうです。
①a,b,cそれぞれの平均±2標準偏差の値を求める
②条件に合えばそのまま、条件にあわなければNAと変換する


 4.平均±2SDを求める

まずa,b,cの平均と標準偏差を求めます。
これには前述のapply関数かsummarize関数を使います。
まずはapply関数でやってみます。

Data01_mean <-apply(Data01, 2, mean, na.rm = TRUE)
Data01_sd <-apply(Data01, 2, sd, na.rm = TRUE)
Data01_mean
Data01_sd

low <- Data01_mean - 2*Data01_sd
high <- Data01_mean + 2*Data01_sd
low
high
スクリーンショット 2020-03-28 3.17.41

ここからlow[2]、もしくはlow["b"]とすればbの平均-2SDが取り出せます。
high[1]、もしくはhigh["a"]であればaの平均+2SDとなります。

5.条件に合わないものをNAにする

次に条件に合わないものを抽出しNAにします。
データを抽出するにはデータ[条件]を使います

(aのデータ < 平均-2sd) または (平均+2SD < aのデータ)だとこうなります
Data01$a[(Data01$a < low["a"]) | (high["a"] < Data01$a)]
スクリーンショット 2020-03-28 4.08.45

条件式の記号については以下をご参照ください。今回はまたはの意味の を使いました。
スクリーンショット 2019-02-11 2.04.40
抽出した条件に <- を使うことでデータを置き換えることができます。
Data02 <- Data01
Data02$a[(Data02$a < low["a"]) | (high["a"] < Data02$a)] <- NA
Data02$b[(Data02$b < low["b"]) | (high["b"] < Data02$b)] <- NA
Data02$c[(Data02$c < low["c"]) | (high["c"] < Data02$c)] <- NA
これで完成です!
ただ他にも方法があります。どれもよく使う方法なので、もし余力があればぜひ比べてみてください。


6.tidyverseパッケージを使う(dplyrパッケージでも同じ)

第2章で紹介しているtidyverseパッケージのsummarize関数とmutate関数を使うこともできます。
tidyverseパッケージを使うときには先にlibrary関数で呼び出します。なければ先にインストールします。
#もし初めての場合はインストール
install.packages("tidyverse")
#インストール済ならlibrary関数で呼び出す
library(tidyverse)
low_high <-
  summarize(Data01,
          a_low = mean(a, na.rm = TRUE)-2*sd(a, na.rm = TRUE),
          a_high = mean(a, na.rm = TRUE)+2*sd(a, na.rm = TRUE),
          b_low = mean(b, na.rm = TRUE)-2*sd(b, na.rm = TRUE),
          b_high = mean(b, na.rm = TRUE)+2*sd(b, na.rm = TRUE),
          c_low = mean(c, na.rm = TRUE)-2*sd(c, na.rm = TRUE),
          c_high = mean(c, na.rm = TRUE)+2*sd(c, na.rm = TRUE)) 
class(low_high) #型を確認するとdata.frameとなっている
low_high <- unlist(low_high)
class(low_high) #型を確認するとnumeric(数値型)となっている
low_high
スクリーンショット 2020-03-28 3.37.43
文字は多いですが、コピペを使えば作業自体は楽になります。
ただsummarize関数の結果は表形式(data.frame型)で、このままでは数値として計算できません。
そのためunlist関数を使って数値型に戻します。
low_high[5]、もしくはlow_high["c_low"]とすればcの平均-2SDとなります。


既に第2章を読まれた方は%>%を使う事もできます。
#%>%使う場合
low_high <- summarize(Data01, a_low = mean(a, na.rm = TRUE)-2*sd(a, na.rm = TRUE), a_high = mean(a, na.rm = TRUE)+2*sd(a, na.rm = TRUE), b_low = mean(b, na.rm = TRUE)-2*sd(b, na.rm = TRUE), b_high = mean(b, na.rm = TRUE)+2*sd(b, na.rm = TRUE), c_low = mean(c, na.rm = TRUE)-2*sd(c, na.rm = TRUE), c_high = mean(c, na.rm = TRUE)+2*sd(c, na.rm = TRUE)) %>%
unlist()
low_high
スクリーンショット 2020-03-28 3.32.12

次に条件に応じて分類するifelse関数を使うこともできます。

ifelse(条件TRUEの場合FALSEの場合)

列の追加や修正を行うmutate関数を使うことでこうなります。
Data02 <- 
  Data01 %>% 
  mutate(a = ifelse((low_high["a_low"] < a) & (a < low_high["a_high"]), a, NA),
         b = ifelse((low_high["b_low"] < b) & (b < low_high["b_high"]), b, NA),
         c = ifelse((low_high["c_low"] < c) & (c < low_high["c_high"]), c, NA))
スクリーンショット 2020-03-28 4.57.19

条件式を少し変えているので確認してみてください。今回は&を使っています。

tidyverseパッケージは現在のRを使う上でとても重要になります。
ifelse,mutate関数に関してはこちらで紹介していますので難しかったという方はご参照ください。




7.関数を自作しapply関数でまとめて処理する
他の方法としては関数を自作するという方法があります。

関数名 <- function(○) {中身のコード}

今回はdrop_2sdという名前の関数を作ります(名前は何でもいいけど文字の最初に数字を使うのは×)。


a = ifelse((aの平均-2SD < a) & (a < aの平均+2SD), a, NA),
b = ifelse((bの平均-2SD < b) & (b < bの平均+2SD), b, NA),
c = ifelse((cの平均-2SD < c) & (c < cの平均+2SD), c, NA)
 ↓
function(x){
 ifelse((xの平均-2SD < x) & (x < xの平均+2SD), x, NA)
}
と共通部分をxにします。ちなみにxでなくてもcolとか別の名前をつけても大丈夫です。

drop_2sd <- function(x){
  ifelse((mean(x, na.rm = TRUE) - 2 * sd(x, na.rm = TRUE) < x) & (x < mean(x, na.rm = TRUE) + 2 * sd(x, na.rm = TRUE)), x, NA)
}
そしてapply関数で各列にdrop_2sd関数を使います。
Data02 <- apply(Data01, 2, drop_2sd)
スクリーンショット 2020-03-28 4.50.32
2行のコードで終わりました。
関数を作るのは最初は慣れないですが、使えるようになると強力です。


8.まとめ

今回は標準偏差±2SDの除去を3つの方法で紹介しました。

本やサイトで独学すると1つの方法でしか紹介してないことが多いです。

本当は自分の知っている知識でもできるはずなのに、知らない方法を偶然読んでしまいドツボにはまることもあります。そういった紆余曲折は勉強する上で必要な苦労でもありますが、いくつかの方法を見比べることでショートカットできることもあるのではと思っています。

また自分が躓いたところや他の記事でもコメントがあれば検討していきますので、ブログのコメントやtwitterで連絡いただければ幸いです。

(2019.09.17 更新)

前回はmutate関数を使って列の追加や修正を行いました。
そして最後に以下のコメントで終了しました。
mutate関数を使うと「FIMの合計点」とかだけでなく、年齢を「年代ごとに分類する」だったり、ある評価の「カットオフ値以上とそれ以下」に分けた列をExcelを使わずに作成することができます。
【2-2】Rのmutate関数を使って列の追加や修正を行うより)

今回は連続変数を条件に応じて複数のカテゴリーに変換していきます。

Rではいくつか方法があるのですが、今回はifelse関数、case_when関数、cut関数を紹介します。


今回使う架空のデータは年齢、性別、病名が入っています。
下のコードをまとめてスクリプトにコピペして実行してください。
library(tidyverse)
set.seed(1) 年齢 <- floor(rnorm(100,60,20)) 性別 <- sample(c("男性","女性"),100,replace = TRUE) 病名 <- sample(c("脳梗塞", "脳出血", "骨折", "靭帯損傷","心筋梗塞"), 100, replace = TRUE) data <- data.frame(年齢,性別,病名) head(data)
スクリーンショット 2019-02-10 22.48.49



今回は%>%やmutate関数を使って作業します。
これらの使い方に不安がある場合は前回の記事をご参照ください



1.ifelse関数をつかって分類する

条件から2つに分類する場合

ifelse関数は条件からTRUEとFALSEの2つに分けることができます。

ifelse(条件TRUEの場合FALSEの場合)



まず例としてdata_20という変数名に以下のように分類していきます。
・20歳未満を「未成年」、20歳以上を「成人」
・性別の男性を「male」、女性を「female」

data_20 <- data %>% 
  mutate(成人 = ifelse(.$年齢 < 20 , "未成年", "成人")) %>% 
  mutate(性別 = ifelse(.$性別 == "男性", "male", "female"))
head(data_成人)

スクリーンショット 2019-02-11 0.44.03

スクリーンショット 2019-02-11 0.46.48



条件から3つ以上分類する場合

3つ以上分類するはifelse関数のFALSEにifelseを入れ込みます。

ifelse(条件, TRUE, ifelse(条件2, TRUE, ifelse(条件3, TRUE, FALSE)))

このような形を「入れ子」と呼ぶそうです。繰り返せばいくらでも条件を増やすことができます。


今度は「年代」という変数名で20歳ごとに区切ってみます。
20歳未満なら「0〜」、
そうでなければ40歳未満なら「20〜」
そうでなければ60歳未満なら「40〜」・・・

上のイメージをコードにするとこうなります。

data_age <- data %>% 
  mutate(年代 = ifelse(.$年齢 < 20,"0〜", 
                      		ifelse(.$年齢 <40 ,"20〜", 
                        		ifelse(.$年齢 < 60,"40〜",
                        			ifelse(.$年齢 < 80, "60〜",
                        				ifelse(.$年齢 < 100, "80〜", "100〜")))))) %>% 
  mutate(年代 = factor(.$年代, levels = c("0〜", "20〜", "40〜", "60〜", "80〜", "100〜"), ordered = TRUE))
head(data_age)
スクリーンショット 2019-02-12 21.22.24

 mutate(年代 = factor(.$年代, levels = c("0〜", "20〜", "40〜", "60〜", "80〜", "100〜"), ordered = TRUE)の部分はfactorの順番を並べ替えています。

factorは50音順に並ぶので、もしこれをしなかったらグラフなど作る時に「0〜, 100〜, 20〜・・・」と100歳以上の場所がずれてしまいます。それを修正するためにlevels = でfactorの並べ替えをしています。
参照:【1-10】Rでよく使われる型について説明します。


個人的に感じる「ifelse関数+入れ子」のメリットは「他の関数を覚えなくていい」で、デメリットは終わりの))))))))の数が多すぎて間違えやすく、エラーに悩まされることです(笑)



2.case_when関数を使って分類する

3つ以上の分類で入れ子構造対策で慣れるとわかりやすいのがcase_when関数かもしれません。

case_when(条件A ~ 結果A, 条件B ~ 結果B, 条件C ~ 結果C)

今回は0歳以上 かつ 20歳未満といったように「かつ」が入る時は & を使います

年齢が0歳以上   かつ 年齢が20歳未満 を「0〜」
年齢が20歳以上 かつ 年齢が40歳未満 を「20〜」
年齢が40歳以上 かつ 年齢が60歳未満 を「60〜」・・・

上のイメージをコードにするとこうなります。

data_age <- data %>% 
  mutate(年代 = case_when(
    .$年齢 >= 0  & .$年齢 <20 ~ "0〜",
    .$年齢 >= 20 & .$年齢 <40 ~ "20〜",
    .$年齢 >= 40 & .$年齢 <60 ~ "40〜",
    .$年齢 >= 60 & .$年齢 <80 ~ "60〜",
    .$年齢 >= 80 & .$年齢 <100 ~ "80〜",
    .$年齢 >= 100 ~ "100〜")) %>% 
  mutate(年代 = factor(.$年代, levels = c("0〜", "20〜", "40〜", "60〜", "80〜", "100〜"), ordered = TRUE))

head(data_age)
スクリーンショット 2019-02-12 23.58.42


入れ子よりも( )の数が少ないのがありがたいところです。


数値でなくカテゴリーを条件で更に分類する場合

上記の場合は数値でしたが次のような場合はどうでしょう?

最初に行ったdata_20という変数名に以下のように分類していきます。
・成人という変数名で20歳未満を「未成年」、20歳以上を「成人」とする
・性別の男性を「male」、女性を「female」に変更
・疾患分類という変数名で、骨折・靭帯損傷を「運動器疾患」、脳梗塞・脳出血を「脳血管疾患」、心筋梗塞を「心疾患」とする

data_age <- data %>% 
  mutate(年代 = case_when(
    .$年齢 < 20 ~ "未成年",
    .$年齢 <= 20 ~ "成人")) %>% 
  mutate(性別 = case_when(
    .$性別 == "男性" ~ "male",
    .$性別 == "女性" ~ "female")) %>%
  mutate(疾患分類 = case_when(
    .$病名 %in% c("骨折", "靭帯損傷") ~ "運動器疾患",
    .$病名 %in% c("脳梗塞", "脳出血") ~ "脳血管疾患",
    .$病名 %in% "心筋梗塞" ~ "心疾患"))
head(data_age)
スクリーンショット 2019-02-13 0.58.43


ここで新たな ==%in% が出てきました。

スクリーンショット 2019-02-11 2.04.40
A == Bと A %in% Bは似ていますが、Bにあたる部分で==であれば1つしか入りません。
%in%は1つでも複数でも大丈夫です。

上記のコードで言うと .$性別 == "男性" ~ "male" の==は %in%に変えても大丈夫ですし、.$病名 %in% "心筋梗塞" ~ "心疾患" の %in% は == に変えても大丈夫です。

しかし.$病名 %in% c("骨折", "靭帯損傷") ~ "運動器疾患" に関しては ==に変えるとエラーが出ます。

上記図は今回の記事だけでなく条件式を作る時に必要になる考え方です。


case_when関数のメリットは入れ子構造にならないことで、デメリットとしては2つに分類するならifelseの方がコードが短くなります。



その他を作る(2019.09.17追記)

Exploratoryというソフトを開発しているKan Nishidaさんのツイートでcase_whenの解説を見つけました。ExploratoryはRをベースにしたデータ分析のソフトです。Rはプログラムを勉強してからでしか使えないところがありますが、ExploratoryはRコマンダーのようにプログラムを使わずにデータ分析ができます。

すべての人にデータサイエンスを

最新のデータサイエンスでは、さまざまな種類のデータを扱うことができ、機械学習や統計アルゴリズムを使用して、深い統計の知識がなくてもデータに隠れているパターンや傾向を見つけることができます。しかし、それはデータサイエンティストとプログラマーに限られています。

Exploratoryは誰もが、プログラミングを必要とせずに、モダンで最先端のデータサイエンスのアルゴリズムに簡単にアクセスできるようにします。私たちは誰もが最新のテクノロジーにアクセスし、データを通じて世界をより深く理解していくべきだと強く信じています。
(https://exploratory.io/より)





Exploratoryのサイトでもcase_when紹介がありました。

 


もし運動器疾患脳血管疾患以外をその他にしたければ以下のようになります。
TRUEはここでは残り全部といった意味になります。
data_age <- data %>% 
  mutate(年代 = case_when(
    .$年齢 < 20 ~ "未成年",
    .$年齢 <= 20 ~ "成人")) %>% 
  mutate(性別 = case_when(
    .$性別 == "男性" ~ "male",
    .$性別 == "女性" ~ "female")) %>%
  mutate(疾患分類 = case_when(
    .$病名 %in% c("骨折", "靭帯損傷") ~ "運動器疾患",
    .$病名 %in% c("脳梗塞", "脳出血") ~ "脳血管疾患",
    TRUE ~ "その他"

勉強になりました!


3.cut関数を使って分類する


数値を分類するに限って言うとcut関数も使えます。
cut関数はコードはスッキリしますが、少しクセがあり、おまじないが必要です。

cut(目的の列,
      breaks = (下限, カットする数値, 上限),
      right = FALSE,
      include.lowest = TRUE,
      labels =c("カテゴリー毎の名前"))

スクリーンショット 2019-02-13 1.49.04


「10代・20代」などでは0〜19のような分け方をするので、right = FALSEが必要になります。
加えてinclude.lowest = TRUEを加えないと端の値を読み込まず<NA>とデータなしとみなされてしまいます。


これらをふまえると以下のコードになります。

 data_age <- data %>% 
  mutate(年代 = cut(.$年齢, 
                  breaks = c(0, 20, 40, 60, 80, 100, 120),
                  right = FALSE, 
                  include.lowest = TRUE,
                  labels = c("0〜", "20〜", "40〜", "60〜", "80〜", "100〜")))
head(data_age)
スクリーンショット 2019-02-13 1.51.09


cut関数を使えば細かい条件が必要にならない分、right = や include.lowest = を忘れないように注意が必要です。


補足.mutate関数の組み合わせについて

mutate関数を複数回行うにはいくつかの方法があります。

スクリーンショット 2019-02-16 19.31.02
(追記)2019/02/16更新
すみません。mutate関数では②の方法は使えませんでした。
②の方法は次で紹介するfilter関数で使うことができます。


本来は,でつなぐと1つの行で済むのですが、初心者だと、()や,の数がうまくあわなかったりします。プログラミングの上級者からは意見があるかもしれませんが、プログラミング未経験・初心者はまず自分の覚えやすいものから始めていいと思っています。



まとめ

今回は条件によって分類する方法についてifelse関数、case_when関数、cut関数を紹介しました。

それぞれに特徴がありますので、必要に応じて使い分けてください。

また今回は演算子(<, ==, %in%など)の紹介も行いました。

演算子はデータを集計するときに必要になりますのでまた紹介していきたいと思います。

↑このページのトップヘ