054 회귀 실전 - 수요 예측
키워드: 수요, 예측
개요
수요 예측은 공급망 관리, 재고 최적화, 생산 계획에 필수적입니다. 이 글에서는 PyCaret을 활용하여 제품 수요를 예측하는 실전 프로젝트를 진행합니다.
실습 환경
- Python 버전: 3.11 권장
- 필요 패키지:
pycaret[full]>=3.0
비즈니스 문제 정의
목표: 제품별 일일/주간 수요량 예측 활용: 재고 수준 최적화, 품절 방지, 과잉 재고 감소
데이터 준비
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# 054 수요 데이터 시뮬레이션
np.random.seed(42)
n_samples = 2000
# 054 제품 카테고리
categories = ['Electronics', 'Clothing', 'Food', 'Home']
products = {
'Electronics': ['Laptop', 'Phone', 'Tablet'],
'Clothing': ['Shirt', 'Pants', 'Shoes'],
'Food': ['Snacks', 'Drinks', 'Frozen'],
'Home': ['Furniture', 'Decor', 'Kitchen']
}
# 054 데이터 생성
data = []
for _ in range(n_samples):
category = np.random.choice(categories)
product = np.random.choice(products[category])
day_of_week = np.random.randint(0, 7)
month = np.random.randint(1, 13)
# 기본 수요
base_demand = {
'Electronics': 50,
'Clothing': 100,
'Food': 200,
'Home': 30
}[category]
# 가격 (수요에 영향)
price = np.random.uniform(10, 500)
# 프로모션
is_promotion = np.random.choice([0, 1], p=[0.8, 0.2])
# 경쟁사 가격
competitor_price = price * np.random.uniform(0.8, 1.2)
# 재고 수준
stock_level = np.random.randint(0, 1000)
# 리드 타임
lead_time = np.random.randint(1, 14)
# 수요 계산
demand = (
base_demand
- price * 0.1 # 가격 탄력성
+ is_promotion * 30 # 프로모션 효과
+ (competitor_price - price) * 0.2 # 경쟁 가격 효과
+ np.sin(day_of_week / 7 * 2 * np.pi) * 20 # 요일 패턴
+ np.sin(month / 12 * 2 * np.pi) * 30 # 계절 패턴
+ np.random.normal(0, 20) # 노이즈
)
demand = max(0, demand)
data.append({
'category': category,
'product': product,
'day_of_week': day_of_week,
'month': month,
'price': price,
'is_promotion': is_promotion,
'competitor_price': competitor_price,
'stock_level': stock_level,
'lead_time': lead_time,
'demand': demand
})
data = pd.DataFrame(data)
print(f"데이터 크기: {len(data)}")
print(f"카테고리: {data['category'].unique()}")
print(f"수요 범위: {data['demand'].min():.0f} ~ {data['demand'].max():.0f}")
탐색적 데이터 분석
import matplotlib.pyplot as plt
import seaborn as sns
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 054 카테고리별 수요
data.boxplot(column='demand', by='category', ax=axes[0, 0])
axes[0, 0].set_title('Demand by Category')
axes[0, 0].set_xlabel('Category')
# 054 요일별 수요
day_demand = data.groupby('day_of_week')['demand'].mean()
axes[0, 1].bar(range(7), day_demand)
axes[0, 1].set_title('Average Demand by Day of Week')
axes[0, 1].set_xlabel('Day (0=Mon, 6=Sun)')
axes[0, 1].set_xticks(range(7))
# 054 가격 vs 수요
axes[1, 0].scatter(data['price'], data['demand'], alpha=0.3)
axes[1, 0].set_title('Price vs Demand')
axes[1, 0].set_xlabel('Price ($)')
axes[1, 0].set_ylabel('Demand')
# 054 프로모션 효과
data.boxplot(column='demand', by='is_promotion', ax=axes[1, 1])
axes[1, 1].set_title('Demand: Promotion vs No Promotion')
plt.tight_layout()
plt.savefig('demand_eda.png', dpi=150)
특성 엔지니어링
# 054 추가 특성 생성
data['price_ratio'] = data['price'] / data['competitor_price'] # 가격 경쟁력
data['is_weekend'] = (data['day_of_week'] >= 5).astype(int) # 주말 여부
data['high_stock'] = (data['stock_level'] > 500).astype(int) # 높은 재고
# 054 카테고리 평균 수요 (타겟 인코딩 효과)
category_mean = data.groupby('category')['demand'].transform('mean')
data['category_demand_level'] = category_mean
print("추가 특성:")
print(data[['price_ratio', 'is_weekend', 'high_stock', 'category_demand_level']].describe())
PyCaret 설정
from pycaret.regression import *
# 054 환경 설정
reg = setup(
data=data,
target='demand',
categorical_features=['category', 'product'],
numeric_features=['day_of_week', 'month', 'price', 'competitor_price',
'stock_level', 'lead_time', 'price_ratio',
'category_demand_level'],
session_id=42,
verbose=False
)
print("설정 완료!")
모델 비교 및 선택
# 054 모델 비교
best_models = compare_models(n_select=5)
# 054 XGBoost 선택 (수요 예측에 효과적)
xgb = create_model('xgboost', verbose=False)
print("XGBoost 기본 성능:")
print(pull())
# 054 튜닝
tuned_xgb = tune_model(xgb, optimize='RMSE')
print("\n튜닝 후 성능:")
print(pull())
특성 중요도 분석
import pandas as pd
# 054 특성 중요도
feature_names = get_config('X_train').columns
importances = tuned_xgb.feature_importances_
importance_df = pd.DataFrame({
'Feature': feature_names,
'Importance': importances
}).sort_values('Importance', ascending=False)
print("특성 중요도 (상위 10개):")
print(importance_df.head(10))
# 054 시각화
plot_model(tuned_xgb, plot='feature')
가격 탄력성 분석
import numpy as np
import matplotlib.pyplot as plt
# 054 가격 변화에 따른 수요 예측
sample = data.iloc[0:1].copy()
prices = np.linspace(10, 500, 50)
demands = []
for p in prices:
sample_copy = sample.copy()
sample_copy['price'] = p
sample_copy['price_ratio'] = p / sample_copy['competitor_price'].values[0]
pred = predict_model(tuned_xgb, data=sample_copy, verbose=False)
demands.append(pred['prediction_label'].values[0])
plt.figure(figsize=(10, 6))
plt.plot(prices, demands, 'b-', linewidth=2)
plt.xlabel('Price ($)')
plt.ylabel('Predicted Demand')
plt.title('Price Elasticity of Demand')
plt.grid(True, alpha=0.3)
plt.savefig('price_elasticity.png', dpi=150)
# 054 가격 탄력성 계산
mid_idx = len(prices) // 2
price_change = (prices[mid_idx+1] - prices[mid_idx]) / prices[mid_idx]
demand_change = (demands[mid_idx+1] - demands[mid_idx]) / demands[mid_idx]
elasticity = demand_change / price_change
print(f"\n가격 탄력성: {elasticity:.2f}")
print("(음수 = 가격 상승 시 수요 감소)")
프로모션 효과 분석
# 054 프로모션 유무에 따른 수요 비교
sample_no_promo = data[data['is_promotion']==0].head(100).copy()
sample_promo = sample_no_promo.copy()
sample_promo['is_promotion'] = 1
pred_no_promo = predict_model(tuned_xgb, data=sample_no_promo, verbose=False)
pred_promo = predict_model(tuned_xgb, data=sample_promo, verbose=False)
avg_no_promo = pred_no_promo['prediction_label'].mean()
avg_promo = pred_promo['prediction_label'].mean()
print(f"프로모션 없음 평균 수요: {avg_no_promo:.1f}")
print(f"프로모션 있음 평균 수요: {avg_promo:.1f}")
print(f"프로모션 효과: +{avg_promo - avg_no_promo:.1f} ({(avg_promo/avg_no_promo - 1)*100:.1f}%)")
카테고리별 수요 예측
# 054 카테고리별 예측 성능
categories = data['category'].unique()
print("카테고리별 예측 성능:")
for cat in categories:
cat_data = data[data['category'] == cat]
# 해당 카테고리만 setup
cat_reg = setup(
data=cat_data,
target='demand',
categorical_features=['product'],
session_id=42,
verbose=False
)
cat_model = create_model('xgboost', verbose=False)
metrics = pull()
print(f"\n{cat}:")
print(f" RMSE: {metrics['RMSE'].mean():.2f}")
print(f" R2: {metrics['R2'].mean():.4f}")
재고 수준 최적화
# 054 수요 예측 기반 안전 재고 계산
def calculate_safety_stock(predictions, service_level=0.95):
"""
서비스 수준에 따른 안전 재고 계산
"""
from scipy import stats
mean_demand = np.mean(predictions)
std_demand = np.std(predictions)
# Z-score for service level
z = stats.norm.ppf(service_level)
safety_stock = z * std_demand
reorder_point = mean_demand + safety_stock
return {
'mean_demand': mean_demand,
'std_demand': std_demand,
'safety_stock': safety_stock,
'reorder_point': reorder_point
}
# 054 예측 수행
X_test = get_config('X_test')
predictions = predict_model(tuned_xgb, data=X_test, verbose=False)
# 054 안전 재고 계산
stock_info = calculate_safety_stock(predictions['prediction_label'].values)
print("\n재고 최적화 권장:")
print(f" 평균 수요: {stock_info['mean_demand']:.1f}")
print(f" 수요 표준편차: {stock_info['std_demand']:.1f}")
print(f" 안전 재고 (95% 서비스 수준): {stock_info['safety_stock']:.1f}")
print(f" 재주문점: {stock_info['reorder_point']:.1f}")
앙상블 모델
from pycaret.regression import *
# 054 전체 데이터로 다시 설정
reg = setup(
data=data,
target='demand',
categorical_features=['category', 'product'],
session_id=42,
verbose=False
)
# 054 여러 모델 생성
lgbm = create_model('lightgbm', verbose=False)
rf = create_model('rf', verbose=False)
xgb = create_model('xgboost', verbose=False)
# 054 블렌딩
blended = blend_models([lgbm, rf, xgb])
print("블렌딩 모델:")
print(pull())
최종 모델 및 예측
# 054 최종 모델
final_model = finalize_model(tuned_xgb)
# 054 새로운 데이터 예측
new_data = pd.DataFrame({
'category': ['Electronics', 'Food', 'Clothing'],
'product': ['Laptop', 'Snacks', 'Shirt'],
'day_of_week': [1, 5, 3],
'month': [12, 12, 12],
'price': [999, 5, 29],
'is_promotion': [1, 0, 1],
'competitor_price': [1099, 4.5, 35],
'stock_level': [50, 500, 200],
'lead_time': [7, 2, 5],
'price_ratio': [999/1099, 5/4.5, 29/35],
'is_weekend': [0, 1, 0],
'high_stock': [0, 0, 0],
'category_demand_level': [50, 200, 100]
})
predictions = predict_model(final_model, data=new_data)
print("\n수요 예측:")
print(predictions[['category', 'product', 'price', 'is_promotion', 'prediction_label']])
모델 저장
# 054 모델 저장
save_model(final_model, 'demand_prediction_model')
print("\n모델 저장 완료: demand_prediction_model.pkl")
실무 적용 팁
- 리드 타임 고려: 예측 기간 = 리드 타임 + 안전 기간
- SKU 레벨: 가능하면 개별 제품 수준으로 예측
- 외부 요인: 날씨, 이벤트, 경제 지표 반영
- 예측 오차 모니터링: MAPE, Bias 추적
- 계층적 예측: 상위 레벨(카테고리) → 하위 레벨(제품) 조정
정리
- 수요 예측은 재고 최적화의 핵심
- 가격, 프로모션, 경쟁사, 계절성이 주요 요인
- 가격 탄력성 분석으로 가격 전략 수립
- 예측 불확실성을 안전 재고에 반영
- XGBoost/LightGBM이 효과적
다음 글 예고
다음 글에서는 회귀 실전 - 가격 최적화를 다룹니다.
PyCaret 머신러닝 마스터 시리즈 #054