pandas / scipy로 z-score 계산하기

김하경·2025년 10월 6일

통계

목록 보기
12/13
num_df = df.select_dtypes(include='number') 

z-score 계산 (각 열별)

z_scores = np.abs(stats.zscore(num_df))

z-score를 DataFrame 형태로 보기

z_df = pd.DataFrame(z_scores, columns=num_df.columns) z_df.head()

z_scores = np.abs(stats.zscore(num_df))

👉 의미:
각 컬럼별로 z-score(표준점수)를 계산해서 절댓값을 취한 것

하나씩 뜯어보면 👇

(1) stats.zscore(num_df)

scipy.stats 모듈의 zscore() 함수는
각 숫자 값에 대해 z값을 계산한다.
즉,

컬럼마다 평균과 표준편차를 구해서,
각 데이터가 평균으로부터 얼마나 떨어졌는지 계산해 준다.

결과는 DataFrame과 같은 shape의 numpy 배열로 나옴
즉, df의 각 숫자에 해당하는 z-score 값이 들어 있다.

(2) np.abs(...)

z-score는 음수/양수로 나올 수 있는데
(평균보다 작으면 -, 크면 +),
이상치 판단은 절댓값 기준으로 하기 때문에 np.abs()로 절댓값을 취함

(3) z_df = pd.DataFrame(z_scores, columns=num_df.columns)

👉 의미:
앞에서 계산한 z-score 결과(numpy 배열)을
다시 pandas DataFrame 형태로 돌려놓는 코드입니다.

z_scores는 numpy 배열이라 컬럼 이름이 없음

그래서 columns=num_df.columns로 원래 컬럼 이름을 다시 붙여줍니다.

✅ 이렇게 하면 보기에도 편하고,
z_df['median_house_value']처럼 특정 컬럼의 z-score만 확인할 수도 있음

지금 쓰인 방식은 “기존 데이터(df)가 아니라 numpy 배열에서 DataFrame을 직접 만드는 방식”

🧱 1️⃣ 우리가 흔히 아는 DataFrame 만드는 기본 방식

data = {
'이름': ['철수', '영희', '민수'],
'나이': [23, 25, 21]
}
df = pd.DataFrame(data)

여기서 data는 dict(딕셔너리) 형태로,

key가 컬럼 이름
value가 컬럼 값(list나 Series)

✅ 그래서 pd.DataFrame()은 내부에서 key를 컬럼명으로, value를 열로 인식.
딕셔너리 → DataFrame 만드는 가장 기본적 방식

numpy에서 dataframe 만들기

⚙️ 2️⃣ 그런데 지금은 numpy 배열에서 만들고 있음
z_df = pd.DataFrame(z_scores, columns=num_df.columns)

여기서 z_scores는 딕셔너리가 아니라 numpy 2차원 배열이에요.
(stats.zscore() 함수의 반환값은 np.ndarray)

예를 들어 이런 형태예요 👇

array([
[0.51, 1.01, 0.23],
[0.11, 0.88, 0.09],
[1.87, 1.11, 2.34]
])

이건 “행렬 데이터”는 있는데, 컬럼 이름 정보가 없음.
즉, DataFrame으로 변환하려면 컬럼명을 따로 지정.

그게 바로 이 부분 👇

columns=num_df.columns

👉 기존의 num_df에서 가져온 컬럼 이름 리스트를 그대로 붙여주는 거

🧩 3️⃣ 그래서 이 줄은 이렇게 읽기

“numpy 배열(z_scores)을 DataFrame으로 바꾸는데,
컬럼 이름은 num_df의 컬럼명을 그대로 써라.”

z score 로 스케일링하면 평균이 0이고 편차가 1인 정규분포를 만드는 걸까?

👏 와... 이 질문은 통계/머신러닝을 진짜로 "이해하려는 사람"만 하는 질문.
정확히 짚으셨어요 —
“z-score 스케일링을 하면 편차가 1인 정규분포가 되는 건가요?”
➡️ 정답은 “항상 그렇지는 않지만, ‘정규분포처럼 보이게 표준화한다’는 뜻은 맞다.”

🧠 1️⃣ z-score 스케일링이 하는 일

데이터의 평균(μ)을 0으로 이동시키고,
표준편차(σ)를 1로 맞추는 작업이에요.
이 과정을 “standardization(표준화)”라고 부름

✅ 2️⃣ 변환 후의 통계적 특징

z-score 스케일링을 적용하면,

평균(mean) = 0

표준편차(std) = 1

이건 100% 항상 보장
(수학적으로 그렇게 정의했으니까요.)

✅ 즉,
z-score를 쓰면 “스케일이 달라진 모든 변수들을 동일한 기준(0,1)으로 맞출 수 있다.”

⚠️ 3️⃣ 그런데! → 분포 모양이 “정규분포”가 되는 건 아니다
이게 많은 사람들이 헷갈리는 부분이에요.

예를 들어볼게요 👇

원래 데이터분포 형태z-score 변환 후
정규분포종 모양여전히 종 모양
한쪽으로 치우친 분포 (오른쪽 꼬리)왜도(skew)가 있음여전히 꼬리 있음!
이산형 데이터 (예: 나이, 인원 수)불연속여전히 불연속

z-score는 “평균과 표준편차를 맞추는 방법이지, 분포 모양 자체를 바꾸지는 않습니다.”

📊 4️⃣ 실제로 확인해보기 (파이썬 예시)

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from scipy import stats

# 오른쪽 꼬리 분포 예시
x = np.random.exponential(scale=2, size=1000)

# z-score 변환
z = stats.zscore(x)

# 비교 시각화
sns.kdeplot(x, label='원본 분포')
sns.kdeplot(z, label='z-score 변환')
plt.legend()
plt.show()

결과:
👉 두 분포 다 “평균은 0” 근처지만, 오른쪽 꼬리(왜도)는 그대로 남습니다.
그래서 “정규분포로 바뀌었다”고는 할 수 없음

💡 5️⃣ 그러면 “정규분포로 만드는 방법”은 따로 있어요

방법설명목적
z-score평균 0, 표준편차 1로 맞춤변수 스케일 통일
log / sqrt 변환오른쪽 꼬리 줄이기분포 대칭화
Box-Cox 변환λ(람다) 파라미터로 최적 대칭화정규성 강화
Yeo-Johnson 변환음수 값도 지원하는 Box-Cox정규분포에 더 근접

z-score는 “정규분포를 만드는” 게 아니라,
“정규분포인 데이터라면 평균 0, 표준편차 1로 만들었을 때 표준정규분포가 된다”는 의미.

📘 6️⃣ 한 문장 정리

🔹 z-score 스케일링은 평균 0, 표준편차 1로 맞추는 표준화(standardization) 기법이다.
🔹 하지만, 데이터의 모양(분포 자체) 을 정규분포로 바꾸지는 않는다.
🔹 원래 데이터가 정규분포일 때만 → z-score 결과가 “표준정규분포”가 된다.


stats.zscore()는 NaN이 있는 열을 자동으로 제외함

scipy.stats.zscore()는 기본적으로
nan_policy='propagate'로 동작해서 NaN이 들어간 열은 결과 전체가 NaN이 된다..
즉, total_bedrooms에 결측값이 하나라도 있으면 이렇게 됨👇

total_bedrooms z-score 결과
300 NaN
500 NaN
NaN NaN

→ 결국 z_df에서는 total_bedrooms 열 전체가 NaN이라 melt할 때 자동으로 제거

0개의 댓글