AICE (에이스) 시험 노트개념 정리14 MIN
결측치·중복·이상치 점검하기
AICE Associate에서 모델에 넣기 전에 표의 상태를 재는 자리입니다. isnull().sum()으로 빈 칸 세기, duplicated로 겹친 행 찾기, describe의 사분위수와 IQR 경계, boxplot으로 이상치 후보를 잡는 법을 연습 문제 7개로 짚습니다.
골라 세는 것까지 됐으면 다음은 그 표가 쓸 만한지를 재는 자리입니다. AICE Associate는 「결측치가 있는 열과 그 개수를 출력하시오」·「이상치 경계를 구하시오」처럼 점검 자체를 한 문항으로 냅니다. 고치는 것은 그다음 과목이고 여기서는 찾기만 합니다 — 무엇이 얼마나 비었는지, 같은 행이 두 번 들어왔는지, 튀는 값이 있는지 세 가지입니다.
셋을 따로 배우면 나란한 세 가지처럼 읽히는데 실제로는 순서가 있습니다. 빈 칸을 먼저 세는 이유는 그 수가 뒤의 모든 통계를 바꾸기 때문이고, 중복을 그다음에 보는 이유는 겹친 행이 평균과 분포를 소리 없이 끌어당기기 때문이며, 이상치를 마지막에 보는 이유는 앞의 둘을 정리한 뒤라야 사분위수가 믿을 만한 값이 되기 때문입니다.
결측치
isnull
isnull()은 표와 같은 크기의 참·거짓 표를 돌려줍니다. 빈 칸이면 True입니다. 그 표에 sum()을 붙이면 열마다 True의 개수가 나오는데, 파이썬에서 True가 1로 더해지기 때문입니다.
df.isnull().sum() # 열마다 빈 칸 개수
df.isnull().sum().sum() # 표 전체의 빈 칸 개수
df.isnull().any() # 열마다 빈 칸이 하나라도 있는가
isna()는 isnull()과 완전히 같은 함수입니다. 이름만 둘이고 하는 일이 같으므로 어느 쪽을 써도 됩니다.
반대로 값이 든 칸을 세려면 notnull()을 씁니다. df.notnull().sum()은 열마다 실제로 값이 들어 있는 개수입니다.
비율
개수만 보면 표가 클 때 감이 안 옵니다. 10,000행에서 30개가 빈 것과 100행에서 30개가 빈 것은 전혀 다른 상황인데 둘 다 「30」으로 찍히기 때문입니다. 전체 행 수로 나눠 비율로 봅니다.
(df.isnull().sum() / len(df) * 100).round(1)
이 비율이 뒤에서 「지울까 채울까」를 정하는 기준이 됩니다. 몇 퍼센트에서 갈리는지는 다음 과목에서 다루고, 여기서는 그 수를 정확히 뽑는 것까지입니다.
info
info()는 열 이름·결측이 아닌 값의 개수·자료형을 한 번에 보여 줍니다. 맨 위에 찍히는 전체 행 수와 각 열의 non-null count를 비교하면 어느 열이 비었는지 바로 보입니다.
df.info()
전체가 1,000행인데 Age 열이 970 non-null이면 그 열에 빈 칸이 30개입니다. isnull().sum()이 빠진 개수를 직접 주는 데 반해 info()는 남은 개수를 주므로, 빼서 읽어야 한다는 것만 주의합니다.
중복 행
duplicated
duplicated()는 행마다 「앞에 같은 행이 이미 나왔는가」를 참·거짓으로 돌려줍니다. 처음 나온 행은 False, 두 번째부터가 True입니다.
df.duplicated().sum() # 겹친 행이 몇 개인가
df[df.duplicated()] # 겹친 쪽만 보기
df[df.duplicated(keep=False)] # 원본과 사본을 모두 보기
keep 인자가 무엇을 False로 남길지를 정합니다. 기본값 'first'는 처음 것을 남기고, 'last'는 마지막 것을 남기며, keep=False는 겹친 행 전부를 True로 표시합니다. 눈으로 대조할 때는 keep=False가 편합니다 — 원본과 사본이 나란히 보여야 어디가 같은지 확인할 수 있기 때문입니다.
열 일부를 기준으로
모든 열이 같아야만 중복인 것은 아닙니다. 고객 번호가 같으면 같은 사람인데 접속 시각만 다를 수 있습니다. subset에 기준 열을 주면 그 열들만 봅니다.
df.duplicated(subset=['CustomerID']).sum()
df.duplicated(subset=['Name', 'Phone']).sum()
전체 열로 세면 0인데 subset으로 세면 수십 개가 나오는 일이 흔합니다. 문항이 「고객 기준 중복」을 물으면 subset을 반드시 줍니다.
describe
사분위수
describe()는 숫자 열마다 여덟 줄을 냅니다 — count·mean·std·min·25%·50%·75%·max입니다. 이 가운데 25%와 75%가 사분위수, 곧 값을 작은 순으로 줄 세웠을 때 4분의 1 지점과 4분의 3 지점에 오는 값입니다. 50%는 가운데 값이라 중앙값과 같습니다.
df.describe()
df['Age'].describe()
count부터 봅니다. 그 수가 전체 행 수보다 작으면 그 열에 빈 칸이 있다는 뜻이라, isnull().sum()을 안 돌려도 결측이 보입니다.
그다음은 min과 max입니다. 나이가 -3이거나 999이면 그건 분포의 꼬리가 아니라 잘못 들어온 값입니다. 평균과 중앙값의 차이도 같이 봅니다 — 평균이 중앙값보다 크게 높으면 위쪽에 큰 값 몇이 끌어당기고 있다는 신호입니다.
숫자가 object로 들어온 열
describe()는 숫자 열만 집계합니다. 그래서 분명히 숫자인 열이 결과에서 통째로 빠져 있으면, 그 열은 숫자로 안 읽힌 것입니다. dtypes로 확인합니다.
df.dtypes
df.describe(include='object') # 문자 열만 따로 보기
Income 열이 object로 찍히는 원인은 대개 셋입니다. 값에 천 단위 쉼표가 섞였거나("1,200"), 빈 값이 공백이나 '-'로 들어왔거나, 단위 글자가 붙어 있는 경우("25세")입니다. 한 칸만 그래도 열 전체가 object가 됩니다.
describe(include='object')를 주면 문자 열에 대해 count·unique·top·freq 네 줄이 나옵니다. unique가 값의 가짓수, top이 가장 많이 나온 값, freq가 그 값의 개수입니다. 갈래가 몇 종류인지 셀 때 유용하고, Income처럼 숫자여야 할 열이 여기 끼어 있으면 그게 바로 자료형 문제입니다.
IQR
경계 계산
IQR은 3사분위수에서 1사분위수를 뺀 값으로, 가운데 절반이 차지하는 폭입니다.
이 폭의 1.5배를 위아래로 더 벌린 구간 밖에 있는 값을 이상치 후보로 봅니다.
Q1 = df['Income'].quantile(0.25)
Q3 = df['Income'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
outliers = df[(df['Income'] < lower) | (df['Income'] > upper)]
print(len(outliers))
조건 둘을 |로 잇고 각각 괄호를 씌운 것은 앞 글의 불리언 인덱싱 그대로입니다.
1.5라는 수에 이론적 근거가 있는 것은 아닙니다. 상자그림을 만든 사람이 정한 관습이고, 데이터에 따라 3을 쓰기도 합니다. 시험에서는 별말이 없으면 1.5입니다.
후보와 오류는 다르다
경계를 넘었다고 해서 잘못된 값은 아닙니다. 소득이 아주 높은 고객은 실제로 있고, 그 행을 지우면 오히려 사실을 버리는 것입니다. IQR이 돌려주는 것은 후보이고, 지울지 남길지는 그 값이 있을 수 있는 값인지를 보고 정합니다. 나이 -3은 있을 수 없으니 오류이고, 나이 97은 드물지만 있을 수 있는 값입니다.
boxplot
상자그림은 이 계산을 그림으로 그린 것입니다. 상자의 아래 변이 , 가운데 선이 중앙값, 위 변이 이고, 수염은 경계 안쪽에 실제로 존재하는 가장 먼 값까지 뻗습니다. 그 밖의 값이 점으로 따로 찍히는데 그 점들이 곧 이상치 후보입니다.
import matplotlib.pyplot as plt
df.boxplot(column='Income')
plt.show()
수염 끝이 경계선 자체가 아니라는 점이 자주 틀리는 자리입니다. 경계가 130이고 실제 값 중 가장 큰 것이 125이면 수염은 125에서 끝납니다.
이 영역의 출제 형태는 대개 셋입니다. 결측 개수를 열마다 출력하게 하거나, 사분위수를 주고 경계를 계산하게 하거나, 상자그림을 주고 이상치가 어느 쪽에 몰려 있는지를 읽게 합니다.
연습 문제
전체 1,000행인 표에서
df.info()가Age열을970 non-null로 찍었다.df['Age'].isnull().sum()의 값과 결측 비율을 구하시오.결측 개수는 이고 비율은 퍼센트입니다.info()는 남은 개수를,isnull().sum()은 빠진 개수를 줍니다.Income값이 작은 순으로10, 20, 30, 40, 50, 60, 70, 80, 200인 9행 표가 있다. 이 30, 이 70일 때 IQR 경계를 구하고 이상치 후보를 모두 쓰시오.이고, 아래 경계는 , 위 경계는 입니다. 경계 밖은 200 하나뿐이라 이상치 후보는 200입니다.df.duplicated().sum()이 0인데df.duplicated(subset=['CustomerID']).sum()은 47이 나왔다. 어떤 상황인가?고객 번호는 같은데 다른 열의 값이 하나라도 달라서 전체 열 기준으로는 겹치지 않는 행이 47개 있다는 뜻입니다. 같은 고객의 여러 접속 기록처럼 시각이나 금액만 다른 행이 이런 모양이 됩니다.df.describe()결과에Income열이 아예 나오지 않았다. 원인으로 가장 알맞은 것은?
①Income열에 결측치가 있다
②Income열이object자료형이다
③Income열의 값이 모두 같다
④Income열의 이름에 공백이 있다②.describe()는 기본적으로 숫자 열만 집계하므로object로 읽힌 열은 결과에서 빠집니다. ①은 결측이 있어도count가 줄 뿐 열은 나옵니다. ③도 표준편차가 0으로 찍힐 뿐 나옵니다. ④는 열 이름 문제라 집계 여부와 무관합니다.df.duplicated()와df.duplicated(keep=False)는 무엇이 다른가?기본값은keep='first'라 처음 나온 행을False로 남기고 두 번째부터를True로 표시합니다.keep=False는 처음 것까지 포함해 겹친 행 전부를True로 표시하므로, 같은 행이 두 번 있으면 앞의 것은 1개, 뒤의 것은 2개를 돌려줍니다.어떤 열의 이 25, 이 45다. 값 78은 이상치 후보인가? 계산 과정을 함께 쓰시오.
후보입니다. 이고 위 경계는 입니다. 78이 75를 넘으므로 경계 밖입니다.상자그림에서 위 경계가 130으로 계산되었고 실제 값 중 가장 큰 것이 125다. 위쪽 수염은 어디서 끝나는가?
125에서 끝납니다. 수염은 경계선까지 무조건 뻗는 것이 아니라 경계 안쪽에 실제로 존재하는 가장 먼 값까지만 뻗습니다. 경계 자체는 그림에 선으로 그려지지 않습니다.
여기서 손이 굳어야 하는 것은 세 줄입니다 — df.isnull().sum(), df.duplicated().sum(), 그리고 quantile(0.25)과 quantile(0.75)로 경계를 내는 다섯 줄짜리 IQR 계산입니다. 표의 상태를 숫자로 재는 것까지 됐으면 다음은 그 분포를 눈으로 보는 자리이고, 거기서 seaborn의 countplot·histplot·heatmap을 다룹니다.

