1. 데이터셋 선정
Kaggle의 'E-Commerce Shipping Data를 선정했다.
주문 및 배송 관련 데이터를 포함하고 CSV파일로 다운로드가 가능하기 때문이다.
https://www.kaggle.com/datasets/prachi13/customer-analytics
2. 구조 파악 코드 예제
import pandas as pd
# 데이터 불러오기
data = pd.read_csv('/Train.csv') # 파일 경로에 맞게 수정
# 데이터 구조 확인
print(data.info()) # 열 정보 및 데이터 타입
print(data.head()) # 상위 5개 행 출력
print(data.describe()) # 통계 요약
print(data.columns) # 열 이름 출력
print(data.isnull().sum()) # 결측치 확인
- ID - 고객 ID (고유값).
- Warehouse block - 제품이 위치한 창고 블록(A, B, C, D, E).
- Mode of shipment - 배송 수단(Ship, Flight, Road).
- Customer care calls - 고객센터 문의 횟수.
- Customer rating - 고객 평점 (1–5).
- Cost of the product - 제품 가격(USD).
- Prior purchases - 이전 구매 횟수.
- Product importance - 제품 중요도(낮음, 중간, 높음).
- Gender - 고객 성별(남, 여).
- Discount offered - 제공된 할인율.
- Weight_in_gms - 제품 무게(그램).
- Reached on time - 배송 시간 준수 여부(1: 지연, 0: 정시). (Target 변수)
data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 ID 10999 non-null int64
1 Warehouse_block 10999 non-null object
2 Mode_of_Shipment 10999 non-null object
3 Customer_care_calls 10999 non-null int64
4 Customer_rating 10999 non-null int64
5 Cost_of_the_Product 10999 non-null int64
6 Prior_purchases 10999 non-null int64
7 Product_importance 10999 non-null object
8 Gender 10999 non-null object
9 Discount_offered 10999 non-null int64
10 Weight_in_gms 10999 non-null int64
11 Reached.on.Time_Y.N 10999 non-null int64
dtypes: int64(8), object(4)
ID Warehouse_block Mode_of_Shipment Customer_care_calls Customer_rating \
0 1 D Flight 4 2
1 2 F Flight 4 5
2 3 A Flight 2 2
3 4 B Flight 3 3
4 5 C Flight 2 2
Cost_of_the_Product Prior_purchases Product_importance Gender \
0 177 3 low F
1 216 2 low M
2 183 4 low M
3 176 4 medium M
4 184 3 medium F
Discount_offered Weight_in_gms Reached.on.Time_Y.N
0 44 1233 1
1 59 3088 1
2 48 3374 1
3 10 1177 1
4 46 2484 1
1) 주요 특징 요약
- Customer_care_calls (고객센터 문의 횟수)
- 평균: 약 4회
- 최소값: 2회
- 최대값: 7회 (고객 불만 또는 복잡한 주문일 가능성)
- 분석 포인트: 문의 횟수가 많을수록 배송 지연과의 상관관계 분석 필요.
- Customer_rating (고객 평점)
- 평균: 약 3점
- 최대값: 5점
- 최소값: 1점
- 이상치: 7점(평점 범위를 벗어나는 데이터로 보임 → 정제 필요)
- 분석 포인트: 낮은 평점이 지연과 관련 있는지 분석 필요.
- Cost_of_the_Product (제품 가격)
- 평균: $210
- 최소값: $96
- 최대값: $310
- Prior_purchases (이전 구매 횟수)
- 평균: 약 3.57회
- 최대값: 10회
- 최소값: 2회
- Discount_offered (할인율)
- 평균: 13%
- 최대값: 65%
- 최소값: 1
- Weight_in_gms (제품 무게)
- 평균: 3634g
- 최대값: 5050g
- 최소값: 1001g
- 분석 포인트: 무거운 제품일수록 지연 위험이 높아질 가능성 검토.
- Reached.on.Time_Y.N (배송 시간 준수 여부)
- 평균 지연 비율: 59.7%
- 0(정시 도착): 약 40.3%
- 1(지연 도착): 약 59.7%
- 분석 포인트: 지연율이 상당히 높으므로, 주요 변수들과의 관계 분석 필요
데이터에 결측값이 없으므로 따로 데이터 전처리를 할 필요는 없다.
이커머스 데이터인데 날짜가 없는 게 조금 아쉽다.
2) 데이터 시각화
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# 데이터 불러오기
data = pd.read_csv('/Train.csv')
# 숫자형 열만 선택
numeric_data = data.select_dtypes(include=['number'])
# 상관관계 분석
plt.figure(figsize=(10, 8))
sns.heatmap(numeric_data.corr(), annot=True, cmap="coolwarm", fmt=".2f")
plt.title("Feature Correlation Heatmap")
plt.show()
전체적으로 관계가 강한거 같진 않다. 제일 높은 상관계수가 높은 게 0.6으로 ID와 할인율인데, 할인율에 따라서 한 고객이 다수의 구매로 이끄는 건지 생각할 필요가 있는 거 같다. 다음으로 할인율과 배송시간준수여부가 0.4로 그나마 관계도가 높다.
또한 Weight in gms 즉 물품의 무게 쪽이 상관계수가 다른 컬럼보다 높은 걸 확인할 수 있다.
피어슨 상관계수의 범위
피어슨 상관계수의 공식은 위와 같다. 보면 양쪽에 차이를 구하고 절댓값을 합치는 듯 수학적 계산을 한다.
스피어만이나 켄달 타우 같은 다른 상관계수도 있지만 피어슨 상관계수가 선형 관계 분석에 적합하다.
상관계수 (r) |
관계 |
0.0 ~ 0.1 | 거의 또는 전혀 상관 없음 |
0.1 ~ 0.3 | 약한 상관관계 |
0.3 ~ 0.5 | 보통 수준의 상관관계 |
0.5 ~ 0.7 | 강한 상관관계 |
0.7 ~ 1.0 | 매우 강한 상관관계 |
- 양의 상관관계 (+): 한 변수가 증가하면 다른 변수도 증가.
- 음의 상관관계 (-): 한 변수가 증가하면 다른 변수는 감소.
- 0.0 (상관 없음): 두 변수 간 관계가 없음.
보통은 0.5 이상일때부터 의미있는 계수다.
# 할인율과 배송 지연 관계
plt.figure(figsize=(8, 6))
sns.boxplot(x='Reached.on.Time_Y.N', y='Discount_offered', data=data)
plt.title('Discount Offered vs On-Time Delivery')
plt.show()
# 제품 무게와 배송 지연 관계
plt.figure(figsize=(8, 6))
sns.boxplot(x='Reached.on.Time_Y.N', y='Weight_in_gms', data=data)
plt.title('Weight vs On-Time Delivery')
plt.show()
- 무게가 가벼운 제품일수록 지연 도착(1)할 가능성이 높은 경향을 보임.
- 무거운 제품은 대체로 정시 도착(0)하는 비율이 높음.
- 이상치 존재: 특히 정시 도착 그룹에서 가벼운 제품들이 이상치로 나타남.
-> 제품이 무거울 수록 정시 도착이 어려울 거 같은건데 데이터 특성을 확인해보니 의외로 물건이 가벼울수룩 배송 지연 위험이 클 가능성이 높을 거 같다.
# 제품 중요도에 따른 배송 지연
plt.figure(figsize=(8, 6))
sns.countplot(x='Product_importance', hue='Reached.on.Time_Y.N', data=data)
plt.title('Product Importance vs On-Time Delivery')
plt.show()
# 배송 방식에 따른 지연 분석
plt.figure(figsize=(8, 6))
sns.countplot(x='Mode_of_Shipment', hue='Reached.on.Time_Y.N', data=data)
plt.title('Mode of Shipment vs On-Time Delivery')
plt.show()
중요도가 낮을 수록 지연배송이 많이 생긴다. 중요도가 높을 수록 관리가 잘 된다고 파악할 수 있다.
대부분 배를 통한 운송을 하고 있다. 배로 가는 게 지연배송 비율이 낮다.
3. Feature 엔지니어링
- 결측치 처리
캐글에서 넘어온 데이터 그런지 결측값이 없다.
만약 데이터에 결측값이 있다면 소수(5%이하)를 삭제 혹은 평균이나 중앙값, 최빈값을 대체한다. 회귀분석을 통해서 대체하는 방식도 있다. 시계열 값에 경우 앞뒤 값으로 채우기도 한다.
- 이상치 처리
데이터 왜곡 방지를 위해 박스플롯에서 확인된 이상치에 대해서는 z-scroe이나 IQR 방식을 활용하여 처리한다.
# 이상치 필터링
Q1 = data['Weight_in_gms'].quantile(0.25)
Q3 = data['Weight_in_gms'].quantile(0.75)
IQR = Q3 - Q1
#범위 1.5배 크기로 확장하여 정상 데이터 확인
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
*사분위수(Quartiles)
Q1(제1사분위수) : 하위 25%의 데이터를 포함하는 값 -> 데이터의 25%가 이 값 이하에 위치한다는 뜻이다.
Q2(제2사분위수) : 50% -> 중앙값(Median)
Q3(제3사분위수) : 하위 75%
IQR(Interquartile Range): Q3 - Q1
-> 데이터의 변동성을 나타내며 이상치를 탐지하는 기준
(추가) z-score(표준 점수)는 데이터가 평균에서 얼마나 떨어져 있는지를 표준편차(Standard Deviation)단위로 나태낸 값이다.
데이터가 평균과 비교했을 때 상대적인 위치를 알려주는 지표다.
z = 0 이면 평균과 동일한 값
z > 0 평균보다 크고 z < 0 이면 평균보다 작다.
|z| > 2 이면 일반적으로 이상치(oultier)로 간주한다.
여기서 IQR을 사용한 이유는 z-score의 경우에 정규분포일 때 조금 더 정확하기 때문이다. IQR은 비대칭 분포, 즉 왜도 또는 이상값이 많은 경우 유리하다. Weight_in_gms 변수에서 왜도가 나타날 수 있는데 z-score는 이상치를 과소/과대평가할 수있다. IQR은 데이터의 중앙 값 주변에 집중하기 때문에 극단적인 값에 영향을 덜 받는다. 현실적으로 이커머스 데이터는 득정 제품의 무게나 가격이 극단적으로 클 가능성이 높기 때문에 IQR을 사용한다.
기준 | Z-Score | IQR |
기본 가정 | 데이터가 정규분포를 따른다고 가정 | 데이터의 분포 형태에 의존하지 않음 |
계산 방식 | 평균(μ)과 표준편차(σ)를 사용 | 사분위수(Q1, Q3)와 IQR을 사용 |
이상치 기준 | Z > 3 또는 Z < -3 | 값이 Q1 - 1.5 * IQR 또는 Q3 + 1.5 * IQR 밖에 존재 |
적합한 상황 | 정규분포일 때 더 정확함 | 비대칭 분포(왜도) 또는 이상값이 많은 경우 유리 |
단점 | 정규분포가 아니면 부정확할 수 있음 | 데이터의 극단적인 이상값을 모두 제거할 수 없음 |
lower랑 upper에 1.5를 곱하는 이유는 Empirical Ruel 때문인다. 통계적으로 약 99.7%의 데이터는평균 ± 3σ(표준편차) 안에 존재한다고 한다. 1.5*IQR은 약 ±2.7σ에 해당하므로 이상치를 검출하는 적절한 기준으로 설정한다. 물론 상황마다 적절한 배수의 설정이 필요하다.
- 데이터 변환 및 스케일링
모델 성능 향상을 위한 데이터 변환 및 스케일링을 수행한다.
무게나 가격 등의 연속형 변수는 표준화(Standardization) 또는 정규화(Normalization)을 적용한다.
from sklearn.preprocessing import StandardScaler
#StandardScaler는 표준화를 수행하는 도구
#객체 생성, 데이터를 평균 0, 표준편차 1로 변환
scaler = StandardScaler()
data[['Cost_of_the_Product', 'Weight_in_gms']] = scaler.fit_transform(
data[['Cost_of_the_Product', 'Weight_in_gms']]
)
Q. 왜 표준화할까?
A: 학습 성능 저하
: 각 변수의 값들이 서로 다른 범위를 가지면 머신러닝 모델의 학습 성능이 저하된다.
Cost_of_the_Product는 96 ~ 310, Weight_in_gms는 1001~5050으로 범위 차이가 크다.
경사하강법 기반 모델 최적화, 거리기반 알고리즘 성능향상처럼 많은 모델들이 크기가 큰 변수를 통해 학습 속도가 느려지거나 측정이 어려워진다.
Cost_of_the_Product | Z(Cost_of_the_Product) |
200 | (200 - μ) / σ |
300 | (300 - μ) / σ |
표준화를 거치면 변수의 범위가 평균 0, 표준편차 1로 조정된다. 이를 통해 편향(Bias)를 방지한다.
범위 말고 두 변수는 범주형 변수가 아닌 (여기서는 Gender, Product_importance가 대상이다) 연속형 변수이기 때문에 표준화를 진행한다. 또 1~5로 범위가 좁다거나 상대적크기만 중요한 경우에는 표준화가 필요하지 안다.
- 파생 변수 생성
추가 인사이트를 위한 파생 변수를 생성한다.
mport numpy as np
# 1. 할인율과 배송 지연 관계 (Discount Offered vs On-Time Delivery)
# 높은 할인율이 지연 위험을 증가시킬 수 있다는 가설 기반 변수 생성
data['High_discount'] = (data['Discount_offered'] > data['Discount_offered'].median()).astype(int)
# 할인율 위험 점수 (할인율 * 중요도 반영)
importance_mapping = {'low': 1, 'medium': 2, 'high': 3}
data['Importance_score'] = data['Product_importance'].map(importance_mapping)
data['Risk_score'] = data['Discount_offered'] * data['Importance_score']
# 2. 제품 무게와 배송 지연 관계 (Weight vs On-Time Delivery)
# 무게 범주화 (경량, 중량, 초중량)
bins_weight = [0, 2000, 4000, 6000]
labels_weight = ['Light', 'Medium', 'Heavy']
data['Weight_category'] = pd.cut(data['Weight_in_gms'], bins=bins_weight, labels=labels_weight)
# 무게당 가격 (Cost_per_gram) 생성
data['Cost_per_gram'] = data['Cost_of_the_Product'] / data['Weight_in_gms']
# 3. 제품 중요도에 따른 배송 지연 (Product Importance vs On-Time Delivery)
# 중요도 점수 활용 (이미 생성됨)
data['High_importance'] = (data['Product_importance'] == 'high').astype(int)
# 4. 배송 방식에 따른 지연 분석 (Mode of Shipment vs On-Time Delivery)
# 배송 방식 더미 변수화
data = pd.get_dummies(data, columns=['Mode_of_Shipment'], drop_first=True)
# 5. 기타 변수 생성
# 고객 문의 횟수 구간화
bins_calls = [0, 3, 6, 10]
labels_calls = ['Low', 'Medium', 'High']
data['Call_frequency'] = pd.cut(data['Customer_care_calls'], bins=bins_calls, labels=labels_calls)
# 반복 구매 여부
data['Repeat_customer'] = (data['Prior_purchases'] > 3).astype(int)
# Weight_category 생성
data['Weight_category'] = pd.cut(data['Weight_in_gms'],
bins=[0, 2000, 4000, 6000],
labels=['Light', 'Medium', 'Heavy'],
include_lowest=True).cat.add_categories(['Unknown']).fillna('Unknown')
# Call_frequency 생성
data['Call_frequency'] = pd.cut(data['Customer_care_calls'],
bins=[0, 3, 6, 10],
labels=['Low', 'Medium', 'High'],
include_lowest=True).cat.add_categories(['Unknown']).fillna('Unknown')
# Call_frequency 숫자형 변환
call_frequency_map = {'Low': 1, 'Medium': 2, 'High': 3}
data['Call_frequency_score'] = data['Call_frequency'].map(call_frequency_map).fillna(0).astype(float)
# 6. 위험도 종합 점수 (배송 지연 위험)
# 위험도 종합 점수 계산 (새로운 변수 추가)
data['Total_risk_score'] = (
data['Risk_score'] +
data['High_discount'] * 2 + # 높은 할인율 가중치
data['High_importance'] * 3 + # 높은 중요도 가중치
data['Weight_category'].map({'Light': 1, 'Medium': 2, 'Heavy': 3}).fillna(0) + # 무게 영향
data['Call_frequency_score'] * 1.5 + # 고객 문의 빈도 가중치
data['Repeat_customer'] * -2 # 반복 구매 고객 가중치 감소
)
# 결과 확인
print(data.head())
1) 평균 할인율 대비 할인율 비율
# 1. 할인율과 배송 지연 관계 (Discount Offered vs On-Time Delivery)
# 높은 할인율이 지연 위험을 증가시킬 수 있다는 가설 기반 변수 생성
data['High_discount'] = (data['Discount_offered'] > data['Discount_offered'].median()).astype(int)
# 할인율 위험 점수 (할인율 * 중요도 반영)
importance_mapping = {'low': 1, 'medium': 2, 'high': 3}
data['Importance_score'] = data['Product_importance'].map(importance_mapping)
data['Risk_score'] = data['Discount_offered'] * data['Importance_score']
(할인율)할인율이 높을 수록 배송지연 위험이 증가할 가능성이 있다는 가설을 기반으로 높은 할인율 여부를 나타내는 이진변수를 생성
할인율이 할인율의 중앙값보다 큰 경우 True, 작거나 같으면 False인데 .astype(int)를 통해서 True는 1로 False 0으로 변환한다.
High_discount =1이면 높은 할인율이고 0이면 낮거나 평균인 할인율이다.
(중요도)Product_importance를 importance_mapping 에 따라 변환한다.
리스크 점수를 계산하는데 이는 할인율과 중요도를 곱하여 배송지연의 잠재적 리스크점수를 계산하는 거다.
- Discount_offered = 30, Importance_score = 3 → Risk_score = 90.
- Discount_offered = 10, Importance_score = 1 → Risk_score = 10.
# 4. 배송 방식에 따른 지연 분석 (Mode of Shipment vs On-Time Delivery)
# 배송 방식 더미 변수화
data = pd.get_dummies(data, columns=['Mode_of_Shipment'], drop_first=True)
Mode_Of_Shipment(배송 방식) 열을 더미 변수(dummy Variable)로 변환한다.
범주형 데이터를 머신러닝 모델에서 사용할 수 있도록 숫자형 데이터로 변환하는 방법이다.
*더미 변수화란?
더미 변수(dummy variable)
범주형 데이터를 0과 1로 구성된 이진 변수로 변환
각 범주는 별도의 열로 분리되며, 데이터가 해당 범주에 속하면 1, 그렇지 않으면 0이 된다.
원핫코드처럼 해당 컬럼있으면 1로 만들어준다.
Mode_of_Shipment
Mode_of_Shipment |
Ship |
Flight |
Road |
Ship |
변환된 데이터:
Mode_of_Shipment_Ship | Mode_of_Shipment_Road |
1 | 0 |
0 | 0 |
0 | 1 |
1 | 0 |
- Mode_of_Shipment_Ship = 1: 배송 방식이 Ship.
- Mode_of_Shipment_Road = 1: 배송 방식이 Road.
- 모두 0: 배송 방식이 Flight.
drop_first=True를 사용하는 이유
다중공선성 방지를 유해서다.
모든 더미 변수를 포함하면 변수 간 상관관계가 발생해 모델 학습에 문제가 될 수 있다.
Mode_of_Shipment_Ship + Mode_of_Shipment_Road + Mode_of_Shipment_Flight = 1인 경우, 세 변수가 완전히 종속적이기 때문에 첫번째 범주를 제거해 효율성을 높이는 거다.
*pd.cut
연속형 데이터를 특정 구간으로 나누어 범주형 데이터로 변환한다.
예를 들어 제품 무게가 1500g이면 Light, 3000g이면 Medium으로 범주화한다.
.cat.add_categories(['Unknown']).fillna('Unknown')
- add_categories(['Unknown']):
- 새로운 카테고리 'Unknown'을 추가하여 나머지 값을 처리 가능.
- .fillna('Unknown'):
- **결측값(NaN)**을 'Unknown'으로 대체.
- 예: Weight_in_gms 값이 비어 있는 경우 해당 값이 'Unknown'으로 처리.
- .fillna(0)으로 하면 빈값이 0으로 채워짐
'데이터분석' 카테고리의 다른 글
이커머스 주문 데이터 분석 및 자동화 파이프라인 구축 (0) | 2024.12.24 |
---|---|
[합격 후기] ADsP 2일 벼락치기 (0) | 2023.03.19 |
[책 리뷰] 빅데이터 시대, 성과를 이끌어 내는 데이터 문해력 (3) | 2023.01.28 |
데이터리안 1월 세미나: 데이터 분석가 되면 어떤 일을 하나요? (0) | 2023.01.22 |
[책 리뷰] 데이터 천재들은 어떻게 기획하고 분석할까? (0) | 2023.01.22 |