통계 4회차 세션에서는 지금까지 배운(?) 내용을 가지고 h&m 데이터로 실습을 해보는 시간이었다.
튜터님이 초반 데이터 전처리 하는 부분을 보니 정말 사람에 따라 스타일이 다르다는 것이 확연히 느껴졌다.
"나는 어떤 스타일로 성장하게 될까?" 싶은 생각도 들면서..."오 이 방법은 알아둬야겠다" 싶은 것들이 몇 가지 있었어서 기록해보려고 한다. 아.. 이거 너무 긴데, 다시 볼까 싶네😵💫
기본세팅
from scipy import stats
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import platform
pd.set_option("display.float_format","{:.2f}".format)
# OS에 따라 다른 폰트 지정
if platform.system() == 'Darwin': # macOS
plt.rcParams['font.family'] = 'AppleGothic'
elif platform.system() == 'Windows': # Windows
plt.rcParams['font.family'] = 'Malgun Gothic'
else: # Linux (예: Colab, Ubuntu)
plt.rcParams['font.family'] = 'NanumGothic'
이거는 기본셋팅인데, 맥과 윈도우 모두 별도 주석 필요없이 사용 가능!!
pd.set_option("display.float_format","{:.2f}".format)
이 부분은 데이터 타입(?)이 float 이면 형식을 지정해주는 거라고 했다. {:.2f} : 소수 둘째자리 까지
가설 검정 전 데이터 전처리 과정
merge
데이터셋이 3개라서 테이블 병합이 필요했다. 튜터님 방식을 보는데, 신기했다. 한꺼번에 3개 테이블 합체!
# 데이터 조인
df = pd.merge(
pd.merge(
transaction_df,
article_df,
on = 'article_id',
how = 'left'
),
customer_df,
on = 'customer_id',
how = 'left'
)
처음엔 조금 헷깔렸는데, 직접 내가 일렬로 작성해보고 저렇게 보기좋게 세팅해보니 이해가 됐다.
# 데이터 조인
df = pd.merge(pd.merge(transaction_df, article_df, on = 'article_id', how = 'left'), customer_df, on = 'customer_id', how = 'left')
크게 보면은 쉽게 이해가 된다. 내부 merge구문이 하나의 테이블이라고 생각해보면,
df = pd.merge(테이블1, 테이블2, on=' ', how=' ') 이렇게 된다!!
다음 info 한 번 찍어도 보고
결측치 대체
신기했던 게 여기서도 있었다. 결측치가 있는 컬럼만 불러오기?!!
nan_col = df.columns[df.isnull().sum()>0]
해석해보면 : df에서 null 값이 있는 걸 찾아서 그걸 컬럼만(즉, 조건의 True인 것들) 불러온다는 말 이다.
다시 생각해보면, 준석 튜터님과도 많이 했던 구조이다. 불린 조건 + 대괄호 인덱싱! 맞나?
하지만, 앞에 .columns 가 새롭게 다가와서 인상 깊었다. 결측치가 있는 컬럼들은 뭐가 있는지 확인하는 절차이다.
df[nan_col].head() : 찍어보고 데이터(결측치 포함된 컬럼) 확인

세션 시간을 돌아보면, 데이터 프레임의 컬럼들을 가지고 요래저래 지지고 볶았던 것 같기도...?
이제는 본격적인 결측치 처리
df['detail_desc'] = df['detail_desc'].fillna('-')
df['FN'] = df['FN'].fillna(0)
df['Active'] = df['Active'].fillna(0)
df['club_member_status'] = df['club_member_status'].fillna('-')
해석해보면 : detail 컬럼에서 결측치를 '-' 로 채우고 & FN 컬럼은 0으로 채우고 & Active 컬럼도 0으로 채우고 & club_member_status 컬럼은 '-'로 채우기로 한다!
그럼 fashion_news_frequency 컬럼은 어떻게 대체 할거야? FN 이랑 밀접한 관계가 있잖아 !
df.loc[df['FN'] == 0, 'fashion_news_frequency'] = 'NONE'
df.loc[df['FN'] == 1, 'fashion_news_frequency'] = df.loc[df['FN'] == 1, 'fashion_news_frequency'].fillna('Regularly')
윗 줄은 FN = 0 (즉, 패션뉴스 비구독) 인 사람들의 fashion_news_frequency 값을 'NONE'로 대체한다는 말인 것 같은데,,,왜냐하면 구독도 안하는데, 알림주기 설정을 했겠냐면서~~
아래 줄은...어디보자👀... 패션뉴스를 구독한 사람들의 알림주기 설정은 NONE이 아닌 값 중 최빈값은 'Regularly'로 대체!!인듯하다.
마지막으로, age 의 결측치 처리 = age 의 중앙값(median)으로 대체
df['age'] = df['age'].fillna(df['age'].median())
결측치가 채워졌나 확인해보고
df.isna().sum().sum()

변수 구분하기
본격적인 통계 실습을 앞두고 있다.
우리가 가진 데이터를 3가지로 나눠서 확인해보면 좋다.
- 범주형
- 연속형
- 순위형 : 이 데이터에는 없다.
우선, 수치형(연속형) 데이터
# 숫자로 되어 있는 변수 구분하기
numeric_df = df.select_dtypes(include='number')
이 코드처럼 작성하면 데이터타입이 number인 것들만 포함해서 묶을 수 있다. 이따가 exclude= 도 나올껄?
# 오 !! .T를 하면 컬럼명이 길때, 아래처럼 인덱스/컬럼 위치를 바꿔서 한번에 볼수가 있구나!!!
numeric_df.head().T
head( ) 까지만 하면 컬럼이 상단 위에 쭈욱 배치되어 옆으로 넘겨야만 확인이 되는데(컬럼이 많고 길 때), .T 를 옆에 살짝 붙여주면 아래와 같이 인덱스처럼 세로로 나열된 것을 볼 수 있다. (오! 한 눈에 파악하기 쉽겠군!)

numeric_df 컬럼 한번 확인해주고,
numeric_df.columns

여기서 연속형(숫자로된 데이터) 내에서 연속형 변수와 범주형 변수를 다시 나눠준다.
# 연속형 변수
continuous_columns = ['price', 'age']
# 범주형 변수
categorical_columns = ['sales_channel_id', 'product_code',
'product_type_no', 'graphical_appearance_no', 'colour_group_code',
'perceived_colour_value_id', 'perceived_colour_master_id',
'department_no', 'index_group_no', 'section_no', 'garment_group_no',
'FN', 'Active']
df.select_dtypes(exclude='number').head().columns
이거는 전체 데이터에서 찐 '범주형 변수(문자 데이터)' 컬럼들을 뽑아내는 거! exclude= ' '
그런다음에 저기 위에 연속형 중 범주형 변수들과 합쳐주자!!
categorical_columns.extend(['product_type_name',
'product_group_name', 'graphical_appearance_name', 'colour_group_name',
'perceived_colour_value_name', 'perceived_colour_master_name',
'department_name', 'index_code', 'index_name', 'index_group_name',
'section_name', 'garment_group_name',
'club_member_status', 'fashion_news_frequency'])
.extend() 매서드를 사용해서 리스트를 연장했다. (하나추가는 .append()를 사용하겠지만)
연속형 변수
- 기술통계
df[continuous_columns].describe()

# 이상치가 제거된 가격/나이 히스토그램 뽑아보기
df[continuous_columns].hist()

다음 절차는 표준화
표준화는 평균이 0이면서, 분산이 1이 되게끔 만드는 것!
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
df['price_scaled'] = scaler.fit_transform(df[['price']])
df['age_scaled'] = scaler.fit_transform(df[['age']])
찐 '수치형' 변수를 표준화 시켜주는 작업 같아 보이는군! 각각 요소들을 살펴보자 👀
이 코드는 price, age 두 변수를 각각 표준화(z-score 변환) 해서 새로운 컬럼 price_scaled, age_scaled를 만든 것이다.
df[['price_scaled', 'age_scaled']].describe()

▼ 자세히 (with GPT)
1. from sklearn.preprocessing import StandardScaler
- scikit-learn 라이브러리에서 StandardScaler 클래스를 불러오는 코드예요.
- StandardScaler는 데이터를 평균 0, 표준편차 1로 변환(표준화)해 줍니다.
2. scaler = StandardScaler()
- StandardScaler 객체를 생성한 거예요.
- 이 객체를 가지고 데이터를 학습(fit)하고 변환(transform)할 수 있습니다.
3. df['price_scaled'] = scaler.fit_transform(df[['price']])
- df[['price']]: price 컬럼을 **DataFrame 형태(2차원)**로 선택합니다. (Series로 하면 에러 나니까 [['price']] 처리를 해줌)
- fit : price 컬럼의 평균과 표준편차를 계산
- transform : 각 값을 (값 - 평균) / 표준편차를 계산
- 변환된 결과를 df['price_scaled']라는 새로운 컬럼에 저장
- 즉, price 변수를 표준화한 값을 담은 새로운 컬럼을 만드는 작업이에요.
4. df['age_scaled'] = scaler.fit_transform(df[['age']])
- 위와 같은 과정이지만 이번엔 age 컬럼에 대해 같은 작업을 합니다.
- 즉, age 변수를 표준화한 값을 담은 age_scaled 컬럼이 추가됩니다.
🚨 주의사항
같은 scaler 객체를 다시 쓰면서 fit_transform을 하면, 앞에 했던 price의 평균/표준편차 정보가 덮어씌워져서 사라진다고 한다.
실무에서는 보통 이렇게 씁니다: 기억하자!
scaler = StandardScaler()
df[['price_scaled', 'age_scaled']] = scaler.fit_transform(df[['price', 'age']])
보통 연속형 변수의 경우는 히스토그램(hist)를 뽑아보는데...다음을 필요없이 그냥 그래프만!
df[['price_scaled', 'age_scaled']].hist()
표준화 해도 분포 모양이 바뀌진 얂고 동일한 스케일로 만들어준다고 이해하면 된다.

이상치 제거
(항상 이상치를 제거해야 하는 것은 아님.) 이상치르 볼 때는 모다? box plot !
sns.boxplot(df[['price_scaled','age_scaled']])

IQR 방식으로 이상치를 제거해보자!
여기서도 재밌었다. ~ (물결) 특수문자로 부정을 표현
# IQR 방식의 이상치 제거
q_1 = np.percentile(df['price'], q=25)
q_3 = np.percentile(df['price'], q=75)
iqr = q_3 - q_1
lower_whisker = q_1 - 1.5*iqr
upper_whisker = q_3 + 1.5*iqr
# ~ 는 부정 : 즉, 이상치가 아닌 애들 ~()
df_processed = df.loc[~((df['price']<lower_whisker) | (df['price']>upper_whisker))]
이렇게 IQR 방식으로 가격 컬럼의 이상치를 제거해주었다. 여기서 내가 궁금한 점은(항상 헷깔려서) 마지막 줄 .loc의 역할이다.
▼ .loc 의 역할과 예시
✅ .loc의 역할
- 라벨(label)을 기준으로 행/열을 선택하는 방법입니다.
- 그래서 조건식과 자주 쓰이긴 하지만, 꼭 조건식이랑만 쓰는 건 아니에요.
📌 사용 예시
1) 조건식과 함께 (가장 흔한 경우)
👉 price가 1000보다 큰 행만 가져옴.
2) 특정 행 라벨로 가져오기
👉 인덱스 라벨이 5인 행 가져오기.
(숫자 5라는 "위치"가 아니라 라벨이 5인 행이에요 → 그래서 iloc랑 차이가 남)
3) 행과 열 동시에 지정
👉 인덱스 라벨 0~3 구간, 그리고 price, age 컬럼만 가져오기.
4) 특정 값 변경에도 활용
👉 price가 0보다 작은 값들은 전부 0으로 바꾸기.
✅ .loc와 DataFrame
- .loc[...]를 쓰면 기본적으로 DataFrame이나 Series 형태로 결과가 나와요.
- 조건식 넣으면: 여러 행이 걸리니까 DataFrame
- 특정 열 하나만 지정하면: Series
- 행/열 딱 하나만 지정하면: 스칼라 값
즉, .loc = "무조건 DataFrame"이라고 생각하면 약간 위험해요.
👉 상황에 따라 DataFrame / Series / 스칼라가 될 수 있음.
📌 요약
- .loc는 라벨 기반 인덱싱 도구.
- 조건식이랑 많이 쓰이지만, 조건 전용은 아님.
- 반환값은 상황에 따라 DataFrame / Series / 값 하나가 될 수 있음.
상관계수
plt.scatter(df['age'], df['price'])

df[continuous_columns].corr()

예상한대로 상관계수가 0.04로 상관관계가 거의 없다.
범주형 변수
df_processed[categorical_columns].head().T

df_processed['FN'].value_counts()
각 컬럼들 value_counts() 확인해보면서, 어떤 컬럼을 사용해볼지 결정해보자!
# 컬럼이 많은 거 같으면 잘 안씀. value_counts() 찍어보고 적당한 것을 고르기!!!
# garment_group_name, perceived_colour_master_name,
use_cols = ['sales_channel_id','FN','Active','garment_group_name','perceived_colour_master_name','club_member_status','fashion_news_frequency']
이렇게 사용하기로 하고, use_cols 변수에 저.장!
# 오오오,,, 이렇게 막대그래프 나타낼수 있군!!! 위에 use_cols 에 있는 모든 컬럼 찍어보자!
df['garment_group_name'].value_counts().plot(kind='barh')
이렇게 밸류카운츠 뒤에 바로 .polt(kind=' ')를 붙여서 뽑을수가!!?

# 오! 이렇게 막대그래프 나타낼수 있군!!! 위에 use_cols 에 있는 모든 컬럼 찍어보자!
for col in use_cols:
plt.figure() # 매번 도화지를 새로 만들어줘~ 느낌!! 없으면 마지막 하나만 나와요.
df[col].value_counts().plot(kind='barh')
for 반복문을 사용해서 컬럼들의 막대그래프를 모두 한 번에 뽑아봄.😎
▼ 막대 그래프 확인
fashion_news_frequency, club_member_status는 null값을 제외하고 한 쪽으로 치우쳐져 있어서 가설 검점 대상에서 제외해야겠다.
가설검정
1. 패션구독여부에 따라 평균구매금액이 달라지는가? - t-검정
2. 같은 카테고리 내에서 색마다 평균구매금액이 달라지는가? - ANOVA 검정
3. 카테고리와 색은 독립적인가?
- 카테고리별로 선호되는 색이 다를까?
- 독립적이면, 카테고리별로 색상 선호 비율이 동일
- 독립적이지 않으면, 카테고리별로 색상 선호 비율이 다를지도..!!
🎯 가설검정 단계
STEP 1: 가설 설정
- H₀: 귀무가설 : 패션 구독여부에 따른 평균구매금액 차이는 없다.
- H₁: 대립가설 : 패션 구독여부에 따른 평균구매금액 차이는 있다.
STEP 2: 검정 방법 선택
- 독립표본 t-검정
- 전제조건 확인
- 정규성 -> 표본수가 충분히 크므로 정규성 가정 완화
- 등분산성 : levene's test 진행
STEP 3: 유의수준 결정
- 보통 **α = 0.05** 사용
STEP 4: 검정통계량 & p-value 계산
- 표본 → 검정통계량 → p-value
STEP 5: 결론
- p ≤ α → 귀무가설 기각
- p > α → 귀무가설 채택(유지)
1. 패션뉴스 구독여부에 따라 평균 구매금액이 달라지는가?
데이터 처리
# -- 지금은 구매 데이터를 기준으로 데이터를 병합해씀(merge). 이렇게 되면 여러번 구매한 고객의 데이터가 여러번 들어가게 됨.
# 고객의 주문 수에 따라서 치우침. 고객별로 패션 구독여부와 평균 구매금액을 먼저 구해야 된다.
customer_price_mean = df_processed.groupby(['customer_id', 'FN'])['price'].mean()
customer_price_df = pd.DataFrame(customer_price_mean)
customer_price_df.reset_index(inplace=True)
customer_price_df

# 값들을 불러온다.
fn_0 = customer_price_df.loc[customer_price_df['FN'] == 0, 'price'].values
fn_1 = customer_price_df.loc[customer_price_df['FN'] == 1, 'price'].values

등분산 검정 : Levene 검정
stat, p = stats.levene(fn_0, fn_1)
print(stat, p)
if p > 0.05:
print("등분산 가정 만족(일반 t-test 사용 가능)")
else:
print("등분산 가정 불만족(Welch's t-test 권장)")

# Welch t-test : 등분산성이 깨졌기 때문 (양측검정)
t_stat, p_value = stats.ttest_ind(fn_0, fn_1, equal_var=False) # False 하면 됨.
t_stat, p_value

p-value가 0.05보다 작기 때문에, 패션뉴스 구독여부에 따라 평균 구매금액이 차이가 난다(대립가설)!!
그러면 다음에 뭐가 더 큰지 알아봐야 해!! (단측검정)
# Welch t-test : 등분산성이 깨졌기 때문 (단측검정)
t_stat, p_value = stats.ttest_ind(fn_0, fn_1, equal_var=False, alternative='less') # False 하면 됨.
t_stat, p_value

- alternative = 'greater' : 대립가설이 fn_0 >= fn_1
- alternative = 'less' : 대립가설이 fn_0 =< fn_1
p-value가 1로 0.05 보다 크기 때문에 귀무가설 채택 fn_0 > fn_1 라는 것을 의미! 이상하네.,, 한 번 확인해보면
np.mean(fn_0)
> np.float64(0.024887151212367637)
np.mean(fn_1)
> np.float64(0.02442841793114605)
정말로 fn_0이 더 컸네...
>>> 가설1에 대한 결론 : 패션뉴스 구독하지 않은 사람들의 평균 구매금액이 더 크다.
2. 같은 카테고리 내에서 색마다 평균 구매금액의 차이가 있을까?
STEP 1: 가설 설정
- H₀: 귀무가설 : 같은 카테고리 내에서 색상에 따른 평균구매금액 차이는 없다.
- H₁: 대립가설 : 같은 카테고리 내에서 색상에 따른 평균구매금액 차이는 있다.
STEP 2: 검정 방법 선택
- 일원 ANOVA
- 전제조건 확인
- 정규성 -> 표본수가 충분히 크므로 정규성 가정 완화
- 등분산성
STEP 3: 유의수준 결정
- 보통 **α = 0.05** 사용
STEP 4: 검정통계량 & p-value 계산
- 표본 → 검정통계량 → p-value
STEP 5: 결론
- p ≤ α → 귀무가설 기각
- p > α → 귀무가설 채택(유지)
data_jersy = df_processed.loc[df_processed['garment_group_name']=='Jersey Fancy']
데이터 처리
Jersey Fancy 안에서도 색상이 다양한데~ 그 색마다 평균 구매금액이 과연 다를까? 를 확인하는 과정!
data_anova = data_jersy.groupby(['customer_id', 'perceived_colour_master_name'])['price_scaled'].mean()
data_anova = pd.DataFrame(data_anova).reset_index()
data_anova
색상별로 구매자 수를 확인해보자.
data_anova.groupby('perceived_colour_master_name').count()
▼ 출력결과
결과 중에서 Unknown과 1000개 미만인 것들은 제외하는 게 낫겠다!

df_anova= data_anova.loc[~data_anova['perceived_colour_master_name'].isin(['Metal', 'Lilac Purple', 'Unknown', 'undefined'])]
해당 색상들은 빼주었다.
df_anova.loc[df_anova['perceived_colour_master_name']=='White', 'price_scaled'].values
grouped = [g['price_scaled'].values for name, g in df_anova.groupby('perceived_colour_master_name')]
grouped
색상별로 price_scaled 리스트가 들어감!! 대충 요렇게?

등분산성 검정
오 이것도 처음보는 것이당!
([a, b, c]) --> (*grouped)
grouped[0], grouped[1], grouped[2]..... grouped[13] : 색상 14개 여서 그거를 *grouped 로 대체
a, b, c 색상의 price_scaled 여러 색상을 한번에 하려니깐 이렇게 됨.
# 등분산 검정
stat, p = stats.levene(*grouped)
stat, p
> (np.float64(171.95657802525636), np.float64(0.0))
p-value가 0.05 미만이므로 등분산성 가정 불만족, Welch ANOVA 권장함!
Welch ANOVA
# welch ANOVA
import pingouin as pg
result = pg.welch_anova(dv='price_scaled', between='perceived_colour_master_name', data=df_anova)
dv = (dependent variable) : 비교 대상
between = : 어떤 걸 비교하고 있지? 색상 컬럼!(perceived ~name)
result

p=value가 0 에 근사(0.0보다 한참 작음) -> 귀무가설 기각 -> 같은 카테고리(jersey fancy)안에서 색에 따른 평균 구매금액의 차이가 있다.
사후 검정: 투키검정 또는 games howel(등분산성❌ 일때)
지금은 등분산성을 만족하지 못하니깐 games howel로 사후 검정 진행(사후검정표)
result = pg.pairwise_gameshowell(dv='price_scaled', between='perceived_colour_master_name', data=df_anova)
result

📌 해석을 해보면 : 각각의 조합에 대해서 t-검정을 하는게 사후검정
- 베이지와 블랙을 비교할 때 차이가 있냐 없냐는 p-value로 확인(p-value가 0.05 보다 작으면 차이가 있다)하고,
- 그 차이가 얼마만큼이냐는 diff 를 보고 음수면 뒤에꺼, 양수면 앞에꺼가 더 크구나! 생각해 볼 수있다.
▼ 평균 구매금액이 음수가 있는 이유?
표준화를 하면 데이터가 평균 0, 표준편차 1로 맞춰지는데,
이때 평균보다 작은 값은 음수, 큰 값은 양수로 나타나는 게 자연스러운 현상이다.
jersey fancy 카테고리 안에는 전체 평균 구매금액보다는 낮은 금액들의 상품들이 있어서 대부분 음수값인 것이다.
>>> 가설2에 대한 결론 : 평균 구매금액의 차이가 있다.
3. 카테고리와 색은 독립적인가? - 카이제곱 독립검정
- 카테고리별로 선호되는 색이 다를까?
- 카테코리별로 선호되는 색상이 다르면 독립적이지 않다고 보고, 카테고리와 상관없이 선호되는 색상이 같으면 독립적이라고 함!
from scipy.stats import chi2_contingency
#1. 교차표를 만들어야 함
ct = pd.crosstab(df['index_group_name'],df['perceived_colour_master_name'])
ct
✅ crosstab 을 사용하면 교차표 라는 것이 만들어짐!!

🤔 해석을 해보면 : 베이지 색상의 유아동 옷을 주문한 건수가 804 건이다.
전체로 보나 유아동 옷으로 보나 색상별 구매 비율이 같아야 독립적인 것이다.
즉, 카테고리(옷)와 색상 사이에서 서로 영향을 주지 않기 때문에 독립적 이라고 할 수 있다 !
카이제곱 검정
#2. 카이제곱 검정 실행
result = chi2_contingency(ct)
result
>>> 가설3 - 카이제곱 검정 결론: p-value가 0 이므로 두 변수는 독립적이지 않다. 즉, 상호 영향이 있다.
연관성 정도가 얼마냐를 보는 게 Cramer's V !!
def cramers_v(contingency_table):
chi2 = chi2_contingency(contingency_table).statistic
n = ct.sum().sum()
r = ct.shape[0]
c = ct.shape[1]
v = np.sqrt(chi2/(n*min(r-1,c-1)))
return v
cramers_v(ct)
> np.float64(0.13962337880821765)
>>> 가설3 Cramer's V 결론 : 값이 0.1 이상이므로 약한 상관관계를 갖는다고 할 수 있다.
끝.
'통계 (Statistics)' 카테고리의 다른 글
| [통계/인강] 챕터6 - 가설검정의 주의점 (0) | 2025.10.04 |
|---|---|
| [통계/인강] 챕터5 - 피어슨/스피어만/켄달타우 상관계수 | 상호정보 상관계수 (0) | 2025.10.01 |
| [통계/세션] 3회차 (이론)- 가설검정 & 상관관계 (3) | 2025.10.01 |






