NA는 "알 수 없음"을 뜻하며 퍼져 나갑니다
실제 데이터셋에는 구멍이 있습니다. 비워 둔 설문 문항, 관측값을 놓친 센서 같은 것들이죠. R은 각 구멍을 NA, 즉 not available로 표현합니다. 핵심이 되는 심상은 이것입니다. NA는 0도, 빈 문자열도, 특별한 숫자도 아닙니다. **"여기 값이 있긴 한데 그것이 무엇인지 우리는 모른다"**를 뜻합니다.
이를 진지하게 받아들이면 R의 동작이 논리적으로 보입니다. 알 수 없는 숫자에 1을 더하면? 알 수 없습니다. 4와 8과 알 수 없는 무언가의 평균은? 알 수 없습니다. 결측값이 무엇이든 될 수 있으므로 평균도 무엇이든 될 수 있습니다.
세 줄 모두 NA를 출력합니다. 이 전염성은 버그가 아니라 기능입니다. R은 알지 못하는 답을 아는 척 조용히 넘어가기를 거부합니다. 빈칸을 조용히 건너뛰는 스프레드시트는 한 열의 절반이 비어 있다는 사실을 숨길 수 있지만, R은 여러분이 그것을 알아채고 결측값이 분석에서 무엇을 의미해야 하는지 결정하게 만듭니다. 이 글의 나머지는 그 결정을 의도적으로 내리는 방법에 관한 것입니다.
NA 검사: is.na(), 절대 ==는 안 됩니다
모두가 빠지는 첫 함정입니다. 결측값을 찾고 싶어서 비교를 쓰는데, 동작하지 않습니다.
x == NA는 TRUE 하나 없이 NA NA NA를 돌려줍니다. 왜일까요? "알 수 없음"의 논리를 따라가 보세요. 4가 어떤 알 수 없는 값과 같은가요? 말할 수 없습니다. 그 알 수 없는 값이 4일 수도 있으니까요. NA 자리조차 NA로 비교됩니다. 알 수 없는 것 하나가 다른 알 수 없는 것과 같은가요? 알 수 없습니다. NA와의 비교는 결코 TRUE나 FALSE를 돌려줄 수 없으므로 검사로서는 쓸모가 없고, 더 나쁘게는 전부 NA인 마스크를 [ ] 안에 넣으면 기대한 것이 선택되지 않습니다.
올바른 도구는 정확히 이 질문에 답하도록 만들어져 정직한 논리값 FALSE TRUE FALSE를 돌려주는 is.na()입니다. 여기서 끊임없이 쓰게 될 관용구가 두 가지 나옵니다.
sum(is.na(x))는 결측값의 개수를 셉니다(TRUE는 1로 더해집니다). 새 데이터셋의 모든 열에 대해 무엇보다 먼저 실행하세요. which(is.na(x))는 위치를 찾아 줍니다. 그리고 x[!is.na(x)]는 관측된 값만 남깁니다. 논리 마스크를 통한 수동 제거이며, 늘 쓰던 벡터 필터링 패턴 그대로입니다.
요약에서 NA 건너뛰기: na.rm = TRUE
보통은 NA를 손으로 제거할 필요가 없습니다. R의 요약 함수에 탈출구가 내장되어 있기 때문입니다. 바로 na.rm(NA remove) 인자입니다.
na.rm = TRUE를 주면 함수가 결측값을 버리고 남은 것으로 계산합니다. 4와 8의 평균은 6입니다. sum(), sd(), median(), min(), max(), var() 등 기술 통계 계열 전체가 이 인자를 받습니다.
기본값이 FALSE라는 점을 눈여겨보세요. R이 여러분에게 유리한 방향으로 고집을 부리는 것입니다. 결측 데이터를 무시하는 일이 조용한 기본값이 아니라 여러분이 적어 내리는 명시적 선택이길 원하기 때문이죠. na.rm = TRUE를 입력할 때 여러분은 "여기서는 결측값을 무시해도 안전하다"고 주장하는 셈입니다. 센서가 몇 개의 관측값을 무작위로 놓친 경우라면 참이고, 소득이 가장 낮은 응답자들이 소득 문항을 건너뛴 경우라면 위험할 정도로 거짓입니다. 이 인자는 그 판단의 책임을 여러분이 지게 합니다.
불완전한 행 제거하기: na.omit()과 complete.cases()
데이터 프레임에서 결측은 셀에 존재하지만 분석은 종종 행 단위로 이루어집니다. 그래서 흔한 작업은 NA가 하나라도 있는 행을 버리는 것입니다.
na.omit(df)는 NA를 하나라도 포함한 모든 행을 뺀 데이터 프레임을 돌려줍니다. 여기서는 Rosa만 살아남습니다. complete.cases(df)는 같은 아이디어 뒤에 있는 논리 행 마스크(TRUE FALSE FALSE)를 돌려주며, 그것으로 인덱싱하면 동일한 결과에 두 가지 이점이 더해집니다. 무엇을 잃게 될지 먼저 셀 수 있고(sum(!complete.cases(df))), 어떤 열이 중요한지 한정할 수 있습니다. df[complete.cases(df[, "age"]), ]는 age가 결측인 행만 버리므로 점수를 알 수 없는 Mia는 남습니다.
열을 한정하는 이 형태는 보기보다 훨씬 중요합니다. 넓은 데이터 프레임에 na.omit()을 쓰면 분석할 계획조차 없던 열의 NA 때문에 행 대부분이 조용히 버려질 수 있습니다. 버리기 전에 몇 행을 왜 버리는지 알고 있으세요.
NA 대체하기
때로는 행을 지우기보다 구멍을 채우는 것이 옳습니다. 관용구는 is.na()와 대입을 결합합니다.
"visits가 결측인 자리에 0을 넣어라"로 읽으세요. NA가 알려진 값을 인코딩하고 있을 때 정확히 정당합니다. 방문 기록이 없는 고객은 실제로 방문 횟수가 0이니까요.
하지만 그것이 언제 참인지에 대해 정직하세요. NA가 "측정에 실패했다"를 뜻한다면 0으로 대체하는 것은 데이터를 지어내는 일입니다. 결측 시험 점수를 0으로 바꾸면 그 학생들이 0점을 받은 것처럼 평균이 끌려 내려가지만, 사실 그들이 몇 점을 받았는지 여러분은 모릅니다. 위의 평균(2.4)을 원본의 na.rm 평균(4)과 비교해 보세요. 같은 데이터인데 주장이 다릅니다. 평균이나 중앙값으로 대체하면 왜곡은 덜하지만 여전히 변동성을 과소평가합니다. 규칙은 이렇습니다. 그 값이 결측 항목의 실제 값이었던 이유를 평범한 말로 설명할 수 있을 때만 대체하세요. 그렇지 않다면 NA를 유지하고 na.rm을 쓰세요. "알 수 없음"이 데이터셋에서 가장 진실한 값인 경우가 많습니다.
NA, NULL, NaN, Inf의 차이
R에는 서로 닮았지만 정말로 다른 것을 뜻하는 특수값이 네 가지 있습니다.
| 값 | 의미 | 길이 | 전형적인 출처 |
|---|---|---|---|
NA | 값은 있으나 알 수 없음 | 1 (칸을 채움) | 결측 데이터 |
NULL | 객체 자체가 없음 | 0 (칸 없음) | 삭제된 리스트 원소, 빈 결과 |
NaN | 답이 정의되지 않는 계산 | 1 | 0 / 0, log(-1) |
Inf | 표현 범위를 넘어선 수 | 1 | 1 / 0, 오버플로 |
실무에서 중요한 구분은 이렇습니다. NULL은 벡터 안에서 사라지지만(c(1, NULL, 3)은 원소가 둘입니다. 결측 관측값을 표현할 수 없습니다), NA는 자리를 지킵니다. NaN은 결측으로 취급되므로(is.na(NaN)은 TRUE이고 na.rm도 제거합니다) 반대는 성립하지 않습니다. is.nan(NA)는 FALSE입니다. 그리고 Inf는 결측이 아닙니다. 비교 가능한 실제 숫자이므로(Inf > 1e300은 TRUE) na.rm이 제거하지 않습니다. 평범한 숫자만 남기려면 is.finite()로 걸러 내세요.
덧붙이자면, NA는 조용히 타입별 형태(NA_integer_, NA_real_, NA_character_)로도 존재해 단일 타입 규칙을 깨지 않고 어떤 벡터에도 놓일 수 있습니다. 직접 입력할 일은 드물지만 패키지 코드와 dplyr 오류 메시지에서 보게 됩니다.
핵심 정리
NA는 "알 수 없음"을 뜻하고, 알 수 없음은 전염됩니다. NA에 닿는 모든 계산은 설계상 NA를 돌려줍니다.== NA가 아니라is.na()로 검사하고,sum(is.na(x))로 구멍을 세세요.na.rm = TRUE는 요약 함수가 NA를 건너뛰게 합니다. 결측값을 무시해도 된다는, 호출마다의 명시적 결정입니다.na.omit()은 불완전한 행을 통째로 버리고,complete.cases()는 셀 수 있고 중요한 열로 한정할 수 있는 마스크를 줍니다.- NA 대체(
x[is.na(x)] <- value)는 결측값이 실제로 무엇이었는지 정당화할 수 있을 때만 하세요. - NA ≠ NULL ≠ NaN ≠ Inf입니다. 각각 결측값, 부재하는 객체, 정의되지 않은 계산, 무한한 수입니다.
다음 순서는 함수입니다. 여러분의 논리를 재사용 가능한 이름 있는 조각으로 포장하는 방법을 다룹니다.
자주 묻는 질문
R에서 mean()이 왜 NA를 돌려주나요?
벡터에 NA가 하나 이상 있고, NA는 전염되기 때문입니다. 입력 중 하나라도 알 수 없다면 R은 답도 알 수 없다고 말합니다. mean(x, na.rm = TRUE)처럼 na.rm = TRUE를 전달하면 존재하는 값들의 평균을 계산합니다. 대부분의 요약 함수(sum, sd, median, min, max)가 같은 인자를 받습니다.
R에서 NA는 어떻게 확인하나요?
is.na(x)를 쓰면 값이 결측인 곳마다 TRUE를 돌려줍니다. x == NA로 검사하지 마세요. 무엇이든 알 수 없는 값과 비교하면 TRUE나 FALSE가 아니라 NA가 나오므로 검사가 조용히 실패합니다. sum(is.na(x))는 결측값의 개수를 세고, which(is.na(x))는 위치를 찾아 줍니다.
R에서 데이터 프레임의 NA가 있는 행을 어떻게 제거하나요?
na.omit(df)는 NA를 하나라도 포함한 모든 행을 버립니다. 더 세밀하게 다루려면 complete.cases(df)가 완전히 관측된 행을 표시하는 논리 벡터를 돌려주므로, df[complete.cases(df), ]가 같은 일을 명시적으로 합니다. 또한 중요한 열에만 적용할 수도 있습니다. 예: df[complete.cases(df[, c("age", "score")]), ].
R에서 NA와 NULL의 차이는 무엇인가요?
NA는 결측 값입니다. 벡터에서 한 칸을 차지하고 길이가 1입니다. NULL은 객체의 부재로, 길이가 0이고 벡터에 넣으면 사라집니다. c(1, NULL, 3)은 원소가 둘뿐입니다. 결측 데이터 지점에는 NA를 쓰고, NULL은 리스트 원소를 제거하거나 빈 반환값으로 나타납니다.