R 결측값 확인 및 처리: is.na(), sum(is.na()), na.rm=TRUE, na.omit(), complete.cases()
R 분석과 프로그래밍/R 데이터 전처리 2015. 7. 22. 22:20외부 텍스트 파일로 대용량의 데이터 셋을 R로 불러들이고 나면 가장 먼저 하는 것이 str()로 데이터 구조 파악하기, head(), tail()로 데이터 몇 개 미리보기, 그 다음에 하는 것이 바로 결측값 확인 및 처리, 특이값/영향치 확인 및 처리 등의 탐색적 데이터 분석입니다.
R에서 결측값이 들어있는 상태에서 통계 분석을 진행하면 NA 라는 결과가 나올 뿐, 원하는 결과를 얻지 못합니다. 그리고 대부분의 R 통계 함수에는 옵션으로 "na.rm = TRUE" 라는 옵션을 제공해서 결측값을 통계량 계산할 때 포함하지 말지를 선택할 수 있게 해줍니다.
이번 포스팅에서는 데이터 셋에 (1) 결측값이 포함되어 있는지 확인하는 방법, (2) 결측값이 들어있다면 결측값을 제거하는 방법, (3) 결측값을 다른 값으로 대체하는 방법에 대해서 알아보도록 하겠습니다.
R 결측값 확인 및 처리: is.na(), na.omit(), complete.cases() |
(1) 결측값이 포함되어 있는지 확인하는 방법: is.na()
> x <- c(1, 2, 3, 4, NA, 6, 7, 8, 9, NA) > is.na(x) [1] FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE TRUE |
위의 벡터처럼 구성요소 갯수가 몇 개 안될 경우 is.na() 한 후에 TRUE, FALSE 논리형 값을 눈으로 보고 확인할 수 있습니다. 하지만 아래의 Cars93 데이터 프레임처럼 변수 갯수도 많고, 관측치 갯수도 많은 경우 (대부분의 실무에서 쓰는 데이터 셋은 이처럼 변수도 많고 관측치도 많지요) is.na() 함수만 가지고서는 아무래도 결측치 현황을 파악하는데 무리가 있습니다.
|
(2) 결측값이 총 몇 개인지 계산하는 방법: sum(is.na())
> sum(is.na(x)) [1] 2 |
> str(Cars93) 'data.frame': 93 obs. of 27 variables: $ Manufacturer : Factor w/ 32 levels "Acura","Audi",..: 1 1 2 2 3 4 4 4 4 5 ... $ Model : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1 6 24 54 74 73 35 ... $ Type : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 3 2 2 3 2 ... $ Min.Price : num 12.9 29.2 25.9 30.8 23.7 14.2 19.9 22.6 26.3 33 ... $ Price : num 15.9 33.9 29.1 37.7 30 15.7 20.8 23.7 26.3 34.7 ... $ Max.Price : num 18.8 38.7 32.3 44.6 36.2 17.3 21.7 24.9 26.3 36.3 ... $ MPG.city : int 25 18 20 19 22 22 19 16 19 16 ... $ MPG.highway : int 31 25 26 26 30 31 28 25 27 25 ... $ AirBags : Factor w/ 3 levels "Driver & Passenger",..: 3 1 2 1 2 2 2 2 2 2 ... $ DriveTrain : Factor w/ 3 levels "4WD","Front",..: 2 2 2 2 3 2 2 3 2 2 ... $ Cylinders : Factor w/ 6 levels "3","4","5","6",..: 2 4 4 4 2 2 4 4 4 5 ... $ EngineSize : num 1.8 3.2 2.8 2.8 3.5 2.2 3.8 5.7 3.8 4.9 ... $ Horsepower : int 140 200 172 172 208 110 170 180 170 200 ... $ RPM : int 6300 5500 5500 5500 5700 5200 4800 4000 4800 4100 ... $ Rev.per.mile : int 2890 2335 2280 2535 2545 2565 1570 1320 1690 1510 ... $ Man.trans.avail : Factor w/ 2 levels "No","Yes": 2 2 2 2 2 1 1 1 1 1 ... $ Fuel.tank.capacity: num 13.2 18 16.9 21.1 21.1 16.4 18 23 18.8 18 ... $ Passengers : int 5 5 5 6 4 6 6 6 5 6 ... $ Length : int 177 195 180 193 186 189 200 216 198 206 ... $ Wheelbase : int 102 115 102 106 109 105 111 116 108 114 ... $ Width : int 68 71 67 70 69 69 74 78 73 73 ... $ Turn.circle : int 37 38 37 37 39 41 42 45 41 43 ... $ Rear.seat.room : num 26.5 30 28 31 27 28 30.5 30.5 26.5 35 ... $ Luggage.room : int 11 15 14 17 13 16 17 21 14 18 ... $ Weight : int 2705 3560 3375 3405 3640 2880 3470 4105 3495 3620 ... $ Origin : Factor w/ 2 levels "USA","non-USA": 2 2 2 2 2 1 1 1 1 1 ... $ Make : Factor w/ 93 levels "Acura Integra",..: 1 2 4 3 5 6 7 9 8 10 ...
|
sum(is.na()) 함수를 이용하니 변수가 많거나 관측값이 많은 경우도 결측값 현황을 금방 파악할 수 있습니다. R은 TRUE 를 '1'로, FALSE 를 '0'으로 인식하기 때문에 sum(is.na())를 하게 되면 TRUE 값을 '1'로 해서 합계를 내기 때문에 가능한 함수가 되겠습니다.
colSums() 함수를 사용하면 데이터 프레임 내 다수 변수들에 대해서 한번에 각 개별 변수별 결측값의 개수 합계를 구할 수 있습니다. 바로 위에서 개별 함수별로 일일이 sum(is.na(Cars93$Manufacturer))...이런 식으로 변수의 개수만큼 쓰는 것을 colSums() 함수로는 한줄이면 해결할 수 있으니 훨씬 편합니다.
> colSums(is.na(Cars93)) Manufacturer Model Type Min.Price Price 0 0 0 0 0 Max.Price MPG.city MPG.highway AirBags DriveTrain 0 0 0 0 0 Cylinders EngineSize Horsepower RPM Rev.per.mile 0 0 0 0 0 Man.trans.avail Fuel.tank.capacity Passengers Length Wheelbase 0 0 0 0 0 Width Turn.circle Rear.seat.room Luggage.room Weight 0 0 2 11 0 Origin Make 0 0 |
(3) 결측값을 통계 분석 시 제외(미포함): na.rm = TRUE
> sum(x) [1] NA > mean(x) [1] NA > sum(x, na.rm = TRUE) [1] 40 > mean(x, na.rm = TRUE) [1] 5 |
결측값이 들어있는 벡터에 대해서 sum(), mean(), sd(), min(), max(), range() 등의 통계 함수를 적용하면 'NA'만 나오게 됩니다. 결측값을 포함하지 말고 통계 함수 계산을 하라는 옵션이 na.rm = TRUE 가 되겠습니다.
> sum(Cars93$Luggage.room) [1] NA > mean(Cars93$Luggage.room) [1] NA > > sum(Cars93$Luggage.room, na.rm = TRUE) [1] 1139 > mean(Cars93$Luggage.room, na.rm = TRUE) [1] 13.89024 |
위 예제는 결측값이 포함된 데이터 프레임의 특정 변수에 대해 indexing을 해서 통계 함수를 적용해본 경우 입니다. 역시 na.rm = TRUE 옵션을 설정해 주어야 통계 계산이 제대로 됨을 알 수 있습니다. na.rm = FALSE 가 디폴트이다보니 na.rm=TRUE를 설정하지 않는 경우 결측값이 포함되어 있으면 NA가 결과로 나타나게 됩니다.
(4) 결측값이 들어있는 행 전체를 데이터 셋에서 제거: na.omit()
na.rm = TRUE 옵션은 원래의 데이터 셋은 그대로 둔채 통계량 계산할 때만 포함하지 않게 됩니다. 따라서 다수의 통계 함수 혹은 다수의 변수에 통계 함수를 사용해야 하는 경우 매번 na.rm = TRUE 옵션을 설정해주는게 번거로울 수 있겠지요? 차라리 원래 데이터 셋에서 결측값을 제거해버리면 되겠다는 생각이 드셨을 겁니다.
결측값이 들어있는 행을 통째로 무식하게 제거하는 함수가 na.omit()이며, 좀더 예리하게 특정 행과 열을 지정해서 그곳에 결측값이 있는 경우만 메스로 정밀 수술하는 함수가 complete.cases()가 되겠습니다.
> Cars93_1 <- na.omit(Cars93) > str(Cars93_1) 'data.frame': 82 obs. of 27 variables: $ Manufacturer : Factor w/ 32 levels "Acura","Audi",..: 1 1 2 2 3 4 4 4 4 5 ... $ Model : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1 6 24 54 74 73 35 ... $ Type : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 3 2 2 3 2 ... $ Min.Price : num 12.9 29.2 25.9 30.8 23.7 14.2 19.9 22.6 26.3 33 ... $ Price : num 15.9 33.9 29.1 37.7 30 15.7 20.8 23.7 26.3 34.7 ... $ Max.Price : num 18.8 38.7 32.3 44.6 36.2 17.3 21.7 24.9 26.3 36.3 ... $ MPG.city : int 25 18 20 19 22 22 19 16 19 16 ... $ MPG.highway : int 31 25 26 26 30 31 28 25 27 25 ... $ AirBags : Factor w/ 3 levels "Driver & Passenger",..: 3 1 2 1 2 2 2 2 2 2 ... $ DriveTrain : Factor w/ 3 levels "4WD","Front",..: 2 2 2 2 3 2 2 3 2 2 ... $ Cylinders : Factor w/ 6 levels "3","4","5","6",..: 2 4 4 4 2 2 4 4 4 5 ... $ EngineSize : num 1.8 3.2 2.8 2.8 3.5 2.2 3.8 5.7 3.8 4.9 ... $ Horsepower : int 140 200 172 172 208 110 170 180 170 200 ... $ RPM : int 6300 5500 5500 5500 5700 5200 4800 4000 4800 4100 ... $ Rev.per.mile : int 2890 2335 2280 2535 2545 2565 1570 1320 1690 1510 ... $ Man.trans.avail : Factor w/ 2 levels "No","Yes": 2 2 2 2 2 1 1 1 1 1 ... $ Fuel.tank.capacity: num 13.2 18 16.9 21.1 21.1 16.4 18 23 18.8 18 ... $ Passengers : int 5 5 5 6 4 6 6 6 5 6 ... $ Length : int 177 195 180 193 186 189 200 216 198 206 ... $ Wheelbase : int 102 115 102 106 109 105 111 116 108 114 ... $ Width : int 68 71 67 70 69 69 74 78 73 73 ... $ Turn.circle : int 37 38 37 37 39 41 42 45 41 43 ... $ Rear.seat.room : num 26.5 30 28 31 27 28 30.5 30.5 26.5 35 ... $ Luggage.room : int 11 15 14 17 13 16 17 21 14 18 ... $ Weight : int 2705 3560 3375 3405 3640 2880 3470 4105 3495 3620 ... $ Origin : Factor w/ 2 levels "USA","non-USA": 2 2 2 2 2 1 1 1 1 1 ... $ Make : Factor w/ 93 levels "Acura Integra",..: 1 2 4 3 5 6 7 9 8 10 ... - attr(*, "na.action")=Class 'omit' Named int [1:11] 16 17 19 26 36 56 57 66 70 87 ... .. ..- attr(*, "names")= chr [1:11] "16" "17" "19" "26" .. |
처음에 Cars93이 "'data.frame': 93 obs. of 27 variables:", 즉 93개의 관측치가 있었는데요,
na.omit(Cars93) 함수를 적용한 후에 Cars93_1 이라는 이름으로 새로 저장해서 str()로 데이터 구조를 보니 "'data.frame': 82 obs. of 27 variables:", 즉 82개 관측치로 총 11개 관측치가 줄어들었음을 알 수 있습니다. 결측값이 하나라도 들어있는 행 11개가 통째로 삭제되어 버렸기 때문입니다.
위의 예처럼 결측값이 들어있는 행을 통째로 삭제할 때는 만약의 사태를 대비해서 원본은 그대로 유지하고, 행을 삭제한 데이터 셋을 별도의 이름으로 저장해서 분석을 진행하는 것을 추천합니다.
(5) 특정 행과 열에 결측값이 들어있는 행을 데이터 셋에서 제거 : complete.cases()
> sum(is.na(Cars93)) [1] 13 >
|
(6) 결측값을 다른 값으로 대체: dataset$var[is.na(dataset$var)] <- new_value
>
> Cars93_4 <- Cars93 > Cars93_4$Luggage.room[is.na(Cars93_4$Luggage.room)] <- 0 > Cars93_4$Luggage.room [1] 11 15 14 17 13 16 17 21 14 18 14 13 14 13 16 0 0 20 0 15 14 17 11 13 14 0 16 11 11 15 12 12 13 12 18 0 18 [38] 21 10 11 8 12 14 11 12 9 14 15 14 9 19 22 16 13 14 0 0 12 15 6 15 11 14 12 14 0 14 14 16 0 17 8 17 13 [75] 13 16 18 14 12 10 15 14 10 11 13 15 0 10 0 14 15 14 15
|
sum(is.na(Cars93$Luggage.room)) 함수를 사용해 Cars93의 Luggage.room 변수에 보면 11개의 결측값이 있음을 알 수 있습니다. Luggage.room 변수 내 결측값을 indexing 기법을 활용해서 '0'로 대체하는 방법이 위의 예제가 되겠습니다.
물론 '0'이 아니라 다른 값으로도 대체가 가능합니다. 아래의 예제에서는 결측값을 미포함(na.rm = TRUE)했을 때의 Luggage.room의 평균값으로 결측값을 대체하여 보도록 하겠습니다.
> Cars93_5 <- Cars93 > Cars93_5$Luggage.room[is.na(Cars93_5$Luggage.room)] <- mean(Cars93_5$Luggage.room, na.rm = TRUE) > sum(is.na(Cars93_5$Luggage.room)) [1] 0 > Cars93_5$Luggage.room [1] 11.00000 15.00000 14.00000 17.00000 13.00000 16.00000 17.00000 21.00000 14.00000 18.00000 14.00000 13.00000 [13] 14.00000 13.00000 16.00000 13.89024 13.89024 20.00000 13.89024 15.00000 14.00000 17.00000 11.00000 13.00000 [25] 14.00000 13.89024 16.00000 11.00000 11.00000 15.00000 12.00000 12.00000 13.00000 12.00000 18.00000 13.89024 [37] 18.00000 21.00000 10.00000 11.00000 8.00000 12.00000 14.00000 11.00000 12.00000 9.00000 14.00000 15.00000 [49] 14.00000 9.00000 19.00000 22.00000 16.00000 13.00000 14.00000 13.89024 13.89024 12.00000 15.00000 6.00000 [61] 15.00000 11.00000 14.00000 12.00000 14.00000 13.89024 14.00000 14.00000 16.00000 13.89024 17.00000 8.00000 [73] 17.00000 13.00000 13.00000 16.00000 18.00000 14.00000 12.00000 10.00000 15.00000 14.00000 10.00000 11.00000 [85] 13.00000 15.00000 13.89024 10.00000 13.89024 14.00000 15.00000 14.00000 15.00000
|
(7) 데이터프레임의 모든 행의 결측값을 특정 값(가령, '0')으로 일괄 대체
: dataset[is.na(dataset)] <- 0
> Cars93_6 <- Cars93 > # counting the number of missing values in Cars93 dataset > sum(is.na(Cars93_6)) # 13 [1] 13 > > # converting the missing value in Cars93 dataframe to '0' > Cars93_6[is.na(Cars93_6)] <- 0 > > # counting the number of missing values in Cars93 dataset > sum(is.na(Cars93_6)) # 0 [1] 0
|
(8) 데이터프레임의 각 변수의 결측값을 각 변수 별 평균값으로 일괄 대체
: sapply(dataset, function(x) ifelse(is.na(x), mean(x, na.rm=TRUE), x))
위의 (6)번에서 결측값을 그 열의 평균으로 대체하는 방법을 소개했는데요, 만약 결측값을 포함한 열이 매우 많다면 일일이 변수이름을 지정해가면서 나열해서 입력하기가 번거롭습니다. 이럴 때 sapply() 함수를 사용하면 일괄로 결측값을 포함한 변수에 대해서는 해당 변수의 평균으로 대체하라고 프로그래밍할 수 있습니다. sapply()를 적용하면 matrix를 반환하므로 dataframe으로 만들기 위해서 앞에 data.frame() 을 추가로 붙여주었습니다.
> # converting the missing value in Cars93 dataframe to each column's mean value > Cars93_7 <- Cars93[1:20,c("Rear.seat.room", "Luggage.room")] > > colSums(is.na(Cars93_7)) Rear.seat.room Luggage.room 1 3 > > Cars93_7 Rear.seat.room Luggage.room 1 26.5 11 2 30.0 15 3 28.0 14 4 31.0 17 5 27.0 13 6 28.0 16 7 30.5 17 8 30.5 21 9 26.5 14 10 35.0 18 11 31.0 14 12 25.0 13 13 26.0 14 14 25.0 13 15 28.5 16 16 30.5 NA 17 33.5 NA 18 29.5 20 19 NA NA 20 31.0 15 > > sapply(Cars93_7, function(x) mean(x, na.rm=T)) Rear.seat.room Luggage.room 29.10526 15.35294 > > > Cars93_7 <- data.frame(sapply(Cars93_7, + function(x) ifelse(is.na(x), + mean(x, na.rm = TRUE), x))) > > Cars93_7 Rear.seat.room Luggage.room 1 26.50000 11.00000 2 30.00000 15.00000 3 28.00000 14.00000 4 31.00000 17.00000 5 27.00000 13.00000 6 28.00000 16.00000 7 30.50000 17.00000 8 30.50000 21.00000 9 26.50000 14.00000 10 35.00000 18.00000 11 31.00000 14.00000 12 25.00000 13.00000 13 26.00000 14.00000 14 25.00000 13.00000 15 28.50000 16.00000 16 30.50000 15.35294 17 33.50000 15.35294 18 29.50000 20.00000 19 29.10526 15.35294 20 31.00000 15.00000
|
(9) 그룹 별 평균값으로 결측값 대체하기 (filling missing values by group mean)
base 패키지를 사용할 수도 있긴 한데요, dplyr 패키지의 group_by 와 ifelse 조건문을 사용한 mutate 함수로 그룹 별 평균값으로 결측값 채우는 방법을 소개하겠습니다.
> # make a sample DataFrame > grp <- c(rep('a', 5), rep('b', 5)) > val <- c(1, 2, 3, NaN, 6, 2, 4, NaN, 10, 8) > df <- data.frame(grp, val) > df grp val 1 a 1 2 a 2 3 a 3 4 a NaN 5 a 6 6 b 2 7 b 4 8 b NaN 9 b 10 10 b 8 |
결측값을 제외하고 그룹 'a'와 그룹 'b' 별 평균을 계산해보겠습니다.
> # mean value by group 'a' and 'b' > library(dplyr) > df %>% group_by(grp) %>% summarise(grp_mean = mean(val, na.rm = TRUE)) # A tibble: 2 x 2 grp grp_mean <fct> <dbl> 1 a 3 2 b 6 |
그룹 'a'의 평균은 3, 그룹 'b'의 평균은 6이군요. 그러면 4번째 행에 있는 그룹 'a'의 'val' 칼럼 결측값을 그룹 'a'의 평균 3으로 대체(replace) 또는 채워넣기(fill in)를 해보겠습니다. 그리고 8번째 행에 있는 그룹 'b'의 'val' 칼럼 결측값을 그룹 'b'의 평균 6으로 대체해보겠습니다.
> df %>% + group_by(grp) %>% + mutate(val = ifelse(is.na(val), mean(val, na.rm=TRUE), val)) # A tibble: 10 x 2 # Groups: grp [2] grp val <fct> <dbl> 1 a 1 2 a 2 3 a 3 4 a 3 5 a 6 6 b 2 7 b 4 8 b 6 9 b 10 10 b 8 |
--------------------
우에서 NA(Not Available)에 대해서 소개를 했는데요, 참고로 벡터에서 NaN (Not a Number), 무한값 Inf (Infinity), 유한값 finite 확인하는 방법도 마저 소개하겠습니다.
유의할 것은, is.na()에서 NA 뿐만 아니라 NaN 도 TRUE 를 반환합니다.
> my_data <- c(-1, 0, 10, NA, NaN, Inf)
|
많은 도움이 되었기를 바랍니다.
이번 포스팅이 도움이 되었다면 아래의 '공감 ~♡' 단추를 꾸욱 눌러주세요.^^