class="layout-aside-right paging-number">
본문 바로가기
데이터 분석/Python

결측치, 이상치(Z-SCORE, IQR)

by heestory323 2025. 3. 18.


●  이상치 : 전체 데이터 범위에서 벗어난 아주 작은 값이나 큰 값
  결측치: 데이터 수집 과정에서 측정되지 않거나 누락된 데이터

  데이터EDA : 분석 전에 데이터 정제 과정, Data Cleaning
                     이상치처리, 결측치 처리, 데이터 시각화


1. 결측치 : 누락된 데이터

-결측치 제거

 

결측값 확인

.isnull(), .isna() : 결측치인 값 확인 True/False , 결측치이면 True

.notnull(), .notna() :  결측치가 아닌 값 확인 True/False, 결측치이면 False

 

 결측치의 개수 

.isnull().sum()

 

 결측치 제거  .dropna()

df.dropna(axis=0, how='any',  inplace=False)

  • axis = 0 행index / 1 열columns
  • how = any 하나라도 있으면 제거 / all 모든값이 결측치이면 제거
  • inplace = True 원본변경 / False 원본 유지

 

  • # 열 전체를 제거 : axis=1
    df3 = df3.drop('Unnamed: 4', axis=1)

✅ .drop('열/행 이름',axis=1) : 특정 행,열을 결측치에 상관없이 이름/위치로 직접 제거
✅ .dropna(anxis=0, how='any') : 결측치(NaN)를 기준으로 행,열 제거

 

  • # 결측치가 있는 행 모두 제거 : axis=0, how='any'
    df3.dropna()
    df3.dropna(axis=0, how='any')

  • # 결측치가 (하나라도) 있는 열 모두 제거 : axis=1
      열로 제거 -> 컬럼이 제거되는 현상이 발생하므로 매우 위험
    df3.dropna(axis=1)
  • # 전체 행이 결측값인 경우만 삭제 : how='all'.
    df3.dropna(how='all')

  • # 결측치 제거 후 결과를 바로 저장하고 싶을 때 : inplace=True 
df3.dropna(inplace=True) 
df3 =df3.drop('Unnamed: 4', axis=1) -- df3. 처리~~한 거를 df3 으로 받아줌 (업데이트)

 

# drop 이후 결측치가 잘 제거되었는지 체크

 df3.isnull().sum() = 0 이 나와야함


-결측치 대체 : 리스크가 큼

최빈값  ->  범주형 변수에 주로 사용
중앙값  ,평균값 -> 수치형 변수에 주로 사
행 기준 바로 위,아래 값
group by 연산 결과

결측값 대체(변경)    df[컬럼].fillna(어떤값으로 대체할건지)

 

df[컬럼].fillna (value=None, method=None, axis=None, inplace=False, limit=None, downcast=None)

  • axis = 0 index / 1 columns
  • how = any 존재하면 제거 / all 모든 결측치 제거
  • inplace = True 원본변경 / False 원본 유지

대체 후 검증   df.isnul.sum() = 0

대체 후 인덱스 재구성 .reset_index()  *재구성시 결측치는 카운트 되지 않음*

결측치(NaN)을 대체한 후 df의 인덱스를 재구성

 

대체 값 종류 

      • 최빈값(가장많이나온값) .mode   <범주형 데이터>

df[컬럼] = df[컬럼].fillna( df[컬럼].mode()[0] )

# mode 는 최빈값을 의미
# df3 의 Interaction type 컬럼을 fillna함수를 이용하여 채워주되, mode() 함수를 사용하여 최빈값으로 넣어줌
# mode 함수는 시리즈를 output으로 가집니다. 
# 따라서,[0]을 통해 시리즈 중 단일값을 가져와야 합니다. 
df3 = df3['Interaction type'].fillna(df3['Interaction type'].mode()[0])

 

 

[적용]

df3의 전체 행 = 3298

Interaction type 결측치 = 423개

Interaction type그룹을 기준으로 product id를  .count()한 뒤 reset_index() 로  재구성하면

각 type별 product id가 나옴 1145+855+871 = NaN을 뺸 Interactiontype의 index갯수

=>   NaN을 뺸 Interaction type의 index갯수 + 결측치NaN = 전체행

            1145+855+871                                 +   NaN     =     3294

    NaN = 3294 - (1145+855+871)  = 423  == 결측치 423      [검증 완]

 

0. Interaction type의 index 중 가장 많은 값  1145개 = Like

1. 컬럼의 최빈값 구하기 df[컬럼].mode()[0]   

+

2. 구한 최빈값을 .fillna() 에 넣기

=> df3 = df3['Interaction type'].fillna(df3['Interaction type'].mode()[0])

= .mode()로 최빈값이 Like 임을 구한 뒤 그 Like로 NaN을 채워넣는다는 것을 의미

 

3. 검증 

  :   최빈값 like 로 NaN 값들이 다 채워졌을까? 

group by 한 걸로 다시 확인 :

         df3.groupby(
'Interaction type').count() = 기존 NaN값 + 기존 최빈값이 가지고 잇던 값
   다시 interaction ttype으로 group by한 갯수 =      423 + 1145개   
                                                                     = 1568   [검증 완]

 

 

    • 평균값 .mean
    • df['sw'] = df['sw'].fillna(df['sw'].mean()) df.isnull().sum() -- 검증

 

    • 중간값 .median
    • df['sw'] = df['sw'].fillna(df['sw'].median()) df.isnull().sum()

      - inplace=True 로 하면 원본 데이터가 바뀌게 됩니다

 

 

    • 바로 위 값으로 대체 method='ffill'
    • df['sw'] = df['sw'].fillna(method='ffill') df.isnull().sum()

 

 

    • 바로 아래 값으로 대체 method='bfill'
    • df['sw'] = df['sw'].fillna(method='bfill') #df.isnull().sum()

 

 

    • group by 값(그룹의 통계값)으로 대체 :  .fillna ( df.groupby, .transform )
    • df['sw'] = df['sw'].fillna(df.groupby(기준컬럼)[계산할컬럼].transform(계산방식)) df['sw'] = df['sw'].fillna(df.groupby('Is Amazon Seller')['sw'].transform('median')) df.isnull().sum()

 

 

[적용]
1. 사전 데이터 확인 = 그룹별 통계값 구하기 .groupby()

df.groupby('Is Amazon Seller')['sw'].median()

📌 데이터 그룹화 groupby() 
df.groupby('기준그룹')['계산할컬럼'].집계함수()

#그룹이 여러개 [ , ]
df.groupby(['기준컬럼1','기준컬럼2'])['계산할컬럼'].집계함수()

#여러개의 집계연산 .agg()
df.groupby('카테고리').agg({'계산할컬럼1':'집계함수1'},{'계산할컬럼2':'집계함수2'})

 

2. group by한 데이터를 + 데이터프레임의 컬럼에 추가하기 : .transform 함수 사용

== 평균값 구한걸  .fillna() 결측치자리에 넣기

df['sw'] = df['sw'].fillna(df.groupby(기준컬럼)[계산할컬럼].transform(계산방식))

 

2. 이상치

-'이상하다'의 기준 ? 정답 x  통용되는 이상치 처리 방식은 존재 but 각각 방식마다 결과를 보고 분석가가 선택해야함.
-데이터 크기 가늠 할 수 없을 때는 ? 
(컬럼별로 하나하나)  Z-Scoore, IQR 둘 다 해보고 비교해보기~


방법론 1. Z-Score 🔥🔥
(데이터의 분포가 정규분포일때) 평균으로부터 얼마나 떨어져있는가 ? python의 scikit-learn 라이브러리가 지원

 

1.데이터 타입을 보고 필요한 양식에 맞춰 변경

2. 데이터마다 z-score (표준점수) 구하기.

  • 표준점수(Z) =  = X에서 평균을 뺀 데이터를 표준편차로 나눈 값
                       = 평균으로부터 얼마나 멀리 떨어져 있는지?
                      (일반적으로 -3 ~ 3 사이 값, ±3 이상이면 이상치로 간주)
    •  Z-score = 0 : 평균에서 떨어진 거리가 0 = 평균과 같음
    •  Z-score  > 0 : 평균으로부터 떨어진 거리가 n, 양수(+) 평균보다 높은 값 
    •  Z-score  < 0 : 평균으로부터 떨어진 거리가 n, 음수(-)  평균보다 낮은 값
  • 표준화 도구 : Scikit-learn의 StandardScaler
    • 데이터는 2차원 형태 [[ ]]
    • .fit_transform() 메소드로 변환함
    • 표준화된 데이터는 평균이 0, 표준편차가 1
scale_df = StandardScaler().fit_transform(데이터)

 

  • 이상치 조건 설정 : Z-SCORE 기반, -3 보다 작거나 (OR) 3보다 큰 경우를 이상치로 판별
    mask = ((merge_df['zscore']<-3) | (merge_df['zscore']>3))
    strange_df = merge_df[mask]

 

 


[적용]
 df 의 Shipping Weight(배송무게)의 이상치를 Z-Score를 이용해 찾기!  

 

1. 데이터 불러오기

df = pd.read_csv("p.csv")

 

📌 데이터 확인하기
df.info() : 총 행(row) 수와 열(column), 열에 포함된 값의 개수(Non-Null Count), 데이터타입(dtype) 표시
df.dtypes : 각 열의 데이터타입 확인 (메소드형태 x)
특정열.dtype : 특정 열의 데이터 타입 확인

 

shipping weight = object이므로 숫자만을 뽑아서 타입을 변경해야함 : string -> float -> int

 

1-1.  split으로 문자열 자르고

df['sw'] = df['Shipping Weight'].str.split().str[0]

✅ .split으로 문자열을 자르고 + 리스트에서 숫자만 꺼내기 [ ]
 .split() : 공백을 기준으로 문자열을 나누는 함수 -> 자른 문자열을 리스트로 변환
df['Shipping Weight'].str.split()    # "3.5kg" -> ["3.5", "kg"] 
 .str[0] : 나눈 문자열의 리스트str의 첫번째 요소(숫자 부분)[0]만 가져옴

 

1-2 .문자열 -> 숫자로 변환

df['sw'] = pd.to_numeric(df['sw'] , errors='coerce').fillna(0.0).astype(int)

 

- .fillna(0.0) -> 0.0 으로 채우는 이유 : sw가 소수점으로 되어있는 숫자(실수 float)이므로 0.0 
  -> NaN(비어있는 값)을 0.0으로 채운다

- .astype(int) 로 실수를 정수(int)로 변환 
=> sw 컬럼에 Shipping Weight의 숫자값만 정수형으로 저장됨

📌데이터 타입 변경하기
.astype() : df의 컬럼타입 변경
df[컬럼이름].astype(변경할타입) - (int, float, str)
df.astype({컬럼명1:변경할타입1, 컬럼명2:변경할타입2}) : 여러 열의 타입 변경

pd.to_datetime() 함수 : 문자 -> 날짜 형식으로 변환
df[컬럼명] = to_datetime(df[컬럼명])
- errors='coerce' : 변환할 수 없는 값 NaN(결측값) 처리 

pd.to_numeric() : 문자열 -> 숫자로 변환
df['A'] = pd.to_numeric(df['A'], errors='coerce') 

.map()  : 데이터 값을 매핑하여 타입 변경
df['backend'] = df['backend'].map({'true': True, 'false': False})

.apply() : 사용자 정의 함수를 적용하여 데이터 타입을 변경
df['A'] = df['A'].apply(int)

 

2. 표준화 진행 = Z-Score 구하기

2-1. z-score 를 적용할 컬럼(객체) 지정 [[ ]]

df1 = df[['sw']]

df[['sw']] 에  df[ [    ] ] 를 쓰는 이유 : Scikit-learn의 StandardScaler는 2차원 데이터만 취급
                                                  -> 데이터를 표 형태로 만들어줌

df['sw'] → 1차원 데이터 (리스트) # [1.2, 2.3, 3.4]
df[['sw']] → 2차원 데이터 (행과 열이 있는 표 형)
#
sw
0 1.2
1 2.3
2 3.4

 

2-2. 데이터 표준화 진행 StandardScaler

평균과 표준편차를 계산한 후 데이터를 표준화(Z-Score 변환)해주는 함수
데이터 평균을 0, 표준편차를 1로 만들어줌

scale_df = StandardScaler().fit_transform(데이터)

 

함수 적용 후 변수로 이름지어줌.

scale_df = StandardScaler().fit_transform(df1)

 

2-3.데이터프레임에 추가 : 데이터 합치는 작업 .concat()

: .concat() 함수로 원본 데이터(df) + 표준화된 데이터(scal_df)를 옆으로 합침

 

merge_df = pd.concat([df1, pd.DataFrame(StandardScaler().fit_transform(df1))],axis=1)
merge_df.columns = ['Shipping Weight', 'zscore']

pd.concat( [데이터1, 데이터2], axis=방향) : 데이터 합치기
-axis = 0 세로방향(행 기준) / 1 가로방향(열 기준) 

  컬럼 이름 바꾸기 .columns
데이터프레임.columns = [새로운 컬럼이름1, 새로운 컬럼이름2 ... ]



3. 이상치 감지 

 

3-1.이상치 조건 설정 : Z-SCORE 기반, -3 보다 작거나 (OR) 3보다 큰 경우를 이상치로 판별 

mask = ((merge_df['zscore']<-3) | (merge_df['zscore']>3))
                                                   or

📌 조건 걸기
df [ 조건 ]  : 특정 조건이 True인  행만 필터링, 기본조건

df[(조건1) & (조건2)]  :여러 조건 조합  
   & : AND /  | : OR  /  ~ : NOT

df.loc[조건, 컬럼이름] = 값 # 조건에 맞는 컬럼을 값으로 변경

.where(조건) : 조건에 만족하는 행은 정상출력, 나머지 NaN

반복문을 사용
하여 각 행 별로 조건을 처리
for i in range(len(df)):
    if df.loc[i, '열 이름'] 조건: 
        df.loc[i, '열 이름'] = 값

.apply()  : 각 행, 열에 대해 사용자 정의 함수를 적용
df['새로운 열'] = df.apply(함수, axis=1) # 행 단위로 함수 적용

.filter() : 특정 열, 행 이름을 기준으로 데이터 필터링
df.filter(items=[열 이름1, 열 이름2], axis=1) # 열 기준 필터링
df.filter(like='패턴', axis=0) # 행 기준 필터링



3-2. 이상치만 필터링해서 따로 저장

: mask 라는 변수를 만들어서 조건을 저장 -> 이 변수 mask를 사용하여 데이터 필터링

왜? 일반 조건을 거는 df[조건] 은 True인 행만 필터링 하니까.

 

mask = ((merge_df['zscore']<-3) | (merge_df['zscore']>3)) -> 이상치가 되는 조건을 mask라는 변수에 저장
strange_df = merge_df[mask] -> 이 조건을 만족한 것을 또 새로운 변수strange_df에 저장

 

📌 변수에 원하는 조건 저장 후->  필터링하기
mask = ( (조건1) & (조건2) )
df[mask]  -- mask라는 조건을 만족하는(True)인 애들만 필터링함.


4. 이상치 개수 확인 : 총 55 건 탐지 
strange_df.count() -> strange_df의 갯수 세기 .count
 

 

plus. 이상치를 뺸 정상치를 구하려면?

# Z-score 기준 정상치 찾기 (-3 이상, 3 이하)
normal_df = merge_df[(merge_df['Z-score'] >= -3) & (merge_df['Z-score'] <= 3)]

# 정상치 개수 확인
print(normal_df.count())

 

 

<Z-Score를 이용하여 이상치 탐지하기>

import pandas as pd
from sklearn.preprocessing import StandardScaler

# Step 1: 데이터 불러오기 및 준비
df = pd.read_csv("p.csv")  # 데이터 불러오기
df['sw'] = df['Shipping Weight'].str.split().str[0]  # 문자열에서 숫자 추출
df['sw'] = pd.to_numeric(df['sw'], errors='coerce').fillna(0.0).astype(int)  # 숫자로 변환

# Step 2: Z-Score 계산
# z-score 를 적용할 컬럼 선정 
df1 = trans_df[['price']]

#표준화하기
from sklearn.preprocessing import StandardScaler
scale_df = StandardScaler().fit_transform(df1)

scale_df = pd.DataFrame(scale_df)
merge_df = pd.concat([df1, scale_df],axis=1)
merge_df.columns = ['Shipping Weight', 'z-score'] # Z-Score 계산

# Step 3: 이상치 조건 설정 및 필터링
mask = ((merge_df['z-score']<-3) | (merge_df['z-score']>3))  # Z-Score가 -3보다 작거나 3보다 큰 경우
strange_df = merge_df[mask]   # 이상치만 필터링

# Step 4: 이상치 개수 확인
strange_df.count()

#step5 : 정상치 찾기
normal_df = merge_df.drop(strange_df.index) #전체df에서 이상치 삭제
normal_df = normal_df.drop('Z-score', axis=1) #z-scroe컬럼 제거
normal_df.reset_index(drop=True) #인덱스 재정렬

# step 6 # 이상치 삭제 #trans_df에서도 적용시키기
trans_df1 = trans_df.drop(strange_df.index) 
trans_df1.reset_index(drop=True)

#step7 : 통계
# 소수점 2자리까지만 보여주는 옵션
pd.options.display.float_format = '{:.2f}'.format #지수 표현식 x

#price 컬럼의 통계
trans_df['price'].describe()

 

 

 

 


방법론 2. IQR (Interquartile Range)🔥

: 데이터 분포가 정규분포를 이루지 않을 때 (모집단이 작은경우) (근데 둘 다 가능)
데이터의 상위 25% ~ 75% 의 범위(IQR) / 벗어나면 이상치

1.

I

  • Q3,Q1, IQR 구하는 방법
    df.quantile() : 백분위수 구해주는 함수
q3 = df1['sw'].quantile(0.75)  # 상위 25% (Q3)
q1 = df1['sw'].quantile(0.25)  # 하위 25% (Q1)

iqr = q3 - q1                  # IQR 계산
  • 이상치 경계 설정
    Q3 + 1.5 * IQR    <    이상치    <     Q1 - 1.5 * IQR  

  • 경계 계산
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
  • 이상치 strange_df 탐지하기 <- outlier 지정
iqr_outliers = trans_df[(trans_df['price'] < lower_bound) | (trans_df['price'] > upper_bound)]
iqr_outliers['price'].count()

 



[적용]
 df 의 Shipping Weight(배송무게)의 이상치를 IQR을 이용해 찾기!  

 

1. 데이터 준비 

df = pd.read_csv("p.csv")

 

shipping weight = object이므로 숫자만을 뽑아서 타입을 변경해야함 : string -> float -> int

 

1-1.  문자열 자르기 .split

df['sw'] = df['Shipping Weight'].str.split().str[0]

 

1-2 .문자열 -> 숫자 변환

df['sw'] = pd.to_numeric(df['sw'] , errors='coerce').fillna(0.0).astype(int)

 

- .fillna(0.0) -> 0.0 으로 채우는 이유 : sw가 소수점으로 되어있는 숫자(실수 float)이므로 0.0 
  -> NaN(비어있는 값)을 0.0으로 채운다

- .astype(int) 로 실수를 정수(int)로 변환 
=> sw 컬럼에 Shipping Weight의 숫자값만 정수형으로 저장됨

 

2. IQR 계산
2-1. 이상치를 감지할 컬럼 선정
df1 = df[['sw']]  # 2차원 데이터 값

2-2. Q3, Q1, IQR 값 구하기
 : 백분위수를 구해주는 quantile 함수를 적용하여 쉽게 구할 수 있음

   데이터프레임 전체 혹은 특정 열에 대하여 모두 적용이 가능

✅ df.quantile() : 백분위수 구해주는 함수
df[컬럼명].quantile(백분위수)

 

q3 = df1['sw'].quantile(0.75)  # 상위 25% (Q3)
q1 = df1['sw'].quantile(0.25)  # 하위 25% (Q1)

iqr = q3 - q1                  # IQR 계산

 

2-3. 이상치 경계 설정 : 아래쪽 경계, 위쪽 경계 계산

       Q3 + 1.5 * IQR    <    이상치    <     Q1 - 1.5 * IQR  

lower_bound = q1 - 1.5 * iqr  # 아래쪽 경계
upper_bound = q3 + 1.5 * iqr  # 위쪽 경계

 


3. 이상치 판별 및 dataframe 저장 
 

3-1. 이상치 판별 함수 is_outlier() 정의

[복붙하시오]

def is_outlier(df1):
    score = df1['sw']
    if score > 7 + (1.5 * 6) or score < 1 - (1.5 * 6):
        return '이상치'
    else:
        return '이상치아님'


3-2.  각 값의 이상치 여부 판별  후 apply 함수를 통해 저장
= 새롭게 컬럼을 만들어서 이상치/이상치아님 나타내기

df1['이상치여부'] = df1.apply(is_outlier, axis = 1) 
                  *axis = 1 : 이상치 함수를 모든 열에 적용하겠다 는 뜻


혹은

# step4 이상치 경계 계산하기
lower_bound = q1 - 1.5 * iqr
upper_bound = q3 + 1.5 * iqr

 후 바로 이상치outliers 판별하기

# Step 5: 이상치outliers 찾기
# outliers = 상한선upper_bound과 하한선lower_bound 바깥에 있는 값
outliers = df[(df['sw'] < lower_bound) | (df['sw'] > upper_bound)]

# plus: 이상치를 제거하기
# cleand_df = df[(df['sw'] >= lower_bound) | (df['sw'] <= upper_bound)]

 

4. 이상치 개수 확인

df1.groupby('이상치여부').count()

#IQR 방식으로 구한 이상치 개수는 349 개 

<IQR 방식으로 이상치 탐지하기>

import pandas as pd

# Step 1: 데이터 불러오기 및 준비
df = pd.read_csv("p.csv")  # 데이터 불러오기
df['sw'] = df['Shipping Weight'].str.split().str[0]  # 문자열에서 숫자 추출
df['sw'] = pd.to_numeric(df['sw'], errors='coerce').fillna(0.0).astype(int)  # 숫자로 변환

# Step 2: IQR 계산
q3 = df['sw'].quantile(0.75)  # 상위 25% (Q3)
q1 = df['sw'].quantile(0.25)  # 하위 25% (Q1)
iqr = q3 - q1                 # IQR 계산

# Step 3: 이상치 경계 설정
lower_bound = q1 - (1.5 * iqr)  # 아래쪽 경계
upper_bound = q3 + (1.5 * iqr)  # 위쪽 경계

# Step 4: 이상치 판별 함수 정의 및 적용
def is_outlier(row):
    score = row['sw']
    if score > upper_bound or score < lower_bound:
        return '이상치'
    else:
        return '정상'

df['이상치여부'] = df.apply(is_outlier, axis=1)

# Step 5: 결과 분석
df1[df1['이상치여부'] == '이상치'].count() ##349

혹은

# Step 4: 이상치 경계 설정
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# Step 5: 이상치outliers 찾기
# outliers = 상한선upper_bound과 하한선lower_bound 바깥에 있는 값
outliers = df[(df['sw'] < lower_bound) | (df['sw'] > upper_bound)]
outlier['sw'].count() ##np.int64(349)

# plus: 이상치를 제거하기
# cleand_df = df[(df['sw'] >= lower_bound) | (df['sw'] <= upper_bound)]

print("이상치 경계값: ({}, {})".format(lower_bound, upper_bound))
print("이상치:", outliers.values)



방법론 3. Isolation Forest🔥🔥
머신러닝 기법, 컬럼갯수가 많을 때 이상치 판별에 용이
결정트리 형태 : 이상치의 경우, 질문 했을 때 어디에도 속하지 않을 확률이 높음.
데이터의 경로길이가 짧은 편. <1 에 가까워질수록 이상치로 간주.


방법론 4. DBScan
밀도기반 클러스터링(집단) 알고리즘,
핵심 포인트들이 서로 연결되어 군집 = 이상치
연결되지 않고, 어떠한 집단에도 포함되지 않은 데이터 = 이상치로 판별
크고 복잡한 구조의 데이터에서 이상치 식별에 용이
ex-지리 데이터, 이미지 데이터