num_df = df.select_dtypes(include='number')
z_scores = np.abs(stats.zscore(num_df))
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 만드는 가장 기본적 방식
⚙️ 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 스케일링을 하면 편차가 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 결과가 “표준정규분포”가 된다.
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할 때 자동으로 제거