<aside> 💡
</aside>
컬럼명 | 데이터 타입 | 설명 | 생성시 컬럼별 조건 |
---|---|---|---|
spendId | int | 각 소비 기록의 고유 식별자 | 자동 증가 |
userId | int | 소비한 유저 아이디 | 1번 ~ 50번 랜덤 배정 |
emotionCategoryId | int | 감정 카테고리 아이디 | 1번 ~ 12번 랜덤 선택 |
spendCategoryId | int | 소비 카테고리 아이디 | 1번 ~ 10번 랜덤 선택 |
spendItem | string | 소비한 아이템 | 소비 카테고리별 하위 아이템 중 랜덤 선택 |
spendCost | int | 소비한 금액 | 1,000원에서 100,000원 사이 (원 단위, 일의 자리 0으로 고정) |
spendDate | datetime | 소비 일시 | 2025-07-02 기준 최근 18주 내 랜덤 날짜 및 시간 배정, 시간 순서대로 정렬 |
감정 카테고리 아이디 | 감정 | 소비 카테고리 아이디 | 소비 |
---|---|---|---|
1 | 행복 | 1 | 쇼핑 |
2 | 사랑 | 2 | 배달음식 |
3 | 기대감 | 3 | 외식 |
4 | 슬픔 | 4 | 카페 |
5 | 우울 | 5 | 취미 |
6 | 분노 | 6 | 뷰티 |
7 | 스트레스 | 7 | 건강 |
8 | 피로 | 8 | 자기계발 |
9 | 불안 | 9 | 선물 |
10 | 무료함 | 10 | 여행 |
11 | 외로움 | 11 | 모임 |
12 | 자기연민 | - | - |
유저 15명
유저 구성 : 신분(직장인/학생) + 수준(부자/중산층/거지) + 특정 소비 패턴
커피만 조지는 부자 직장인 | 2. 외식 조지는 부자 직장인 | 3. 쇼핑만 조지는 부자 직장인
커피만 조지는 중간 직장인 | 5. 외식만 조지는 중간 직장인 | 6. 쇼핑만 조지는 중간 직장인
커피만 조지는 거지 직장인 | 8. 쇼핑만 조지는 거지 직장인 | 9. 커피만 조지는 부자 학생
뷰티만 조지는 부자 학생 | 11. 쇼핑만 조지는 부자 학생 | 12. 커피만 조지는 중간 학생
뷰티만 조지는 중간 학생 | 14. 쇼핑만 조지는 중간 학생 | 15. 커피만 조지는 거지 학생
2025-05-01~2025-07-31 14주동안의 기록
주 당 소비 횟수는 신분/수준에 따라 다름
신분 ↓ / 수준 → | 부자 | 중산층 | 거지 |
---|---|---|---|
직장인 | 8 ~ 10회 | 6 ~ 8회 | 4 ~ 6회 |
학생 | 6 ~ 8회 | 4 ~ 6회 | 2 ~ 4회 |
주요 카테고리 중심 소비 (특정 카테고리 소비가 70~85%)
일부 주차는 애매한 소비 패턴으로 설정 (70~85% → 30%~50%)
소비 금액은 카테고리별 + 사용자 수준별 범위 다르게 설정 (예: 부자 > 거지)
일주일 치 소비 금액이 규칙성을 갖도록 샘플링
<aside> 💡
</aside>
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, mean_squared_error
from sklearn.ensemble import RandomForestRegressor
import joblib
# ===== 데이터 불러오기 및 기본 전처리 =====
df = pd.read_csv('/content/drive/MyDrive/PaEmotion/train_spending_prediction_feature1.csv')
df['spendDate'] = pd.to_datetime(df['spendDate']) # 날짜 타입 변환
df['week'] = df['spendDate'].dt.isocalendar().week # 연도 내 주차 계산
# ===== 주별 총 소비금액 및 평균 소비금액 계산 =====
weekly_spending = df.groupby(['userId', 'week'])['spendCost'].sum().reset_index()
weekly_spending['avg_spend'] = weekly_spending.groupby('userId')['spendCost'].transform('mean')
# ===== 반복 소비 카테고리별 빈도 및 총 금액 피처 생성 함수 =====
def add_category_repeat_features(df, window=3):
df = df.sort_values(['userId', 'spendDate'])
df['week'] = df['spendDate'].dt.isocalendar().week
# 주별, 카테고리별 소비 빈도 계산
weekly_freq = df.groupby(['userId', 'week', 'spendCategoryId'])['spendId'].count().reset_index(name='count')
# 주별, 카테고리별 소비 금액 합계 계산
weekly_amt = df.groupby(['userId', 'week', 'spendCategoryId'])['spendCost'].sum().reset_index(name='amount')
# 빈도와 금액을 하나의 데이터프레임으로 병합
weekly = pd.merge(weekly_freq, weekly_amt, on=['userId', 'week', 'spendCategoryId'])
feature_rows = []
for user in df['userId'].unique():
user_weeks = weekly[weekly['userId'] == user]['week'].unique()
for w in user_weeks:
recent_weeks = list(range(w - window, w)) # 최근 window주
recent = weekly[(weekly['userId'] == user) & (weekly['week'].isin(recent_weeks))]
row = {'userId': user, 'week': w}
for cat_id in range(1, 11):
cat_data = recent[recent['spendCategoryId'] == cat_id]
# 각 카테고리별 빈도, 금액 합계
row[f'cat{cat_id}_freq'] = cat_data['count'].sum()
row[f'cat{cat_id}_amount'] = cat_data['amount'].sum()
feature_rows.append(row)
return pd.DataFrame(feature_rows)
# ===== 카테고리별 피처 생성 후 기존 데이터와 병합 =====
cat_feat = add_category_repeat_features(df)
weekly = pd.merge(weekly_spending, cat_feat, on=['userId', 'week'], how='left').fillna(0)
# ===== 주력 루틴 소비 1회당 평균 금액 피처 추가 함수 =====
def add_main_routine_avg(df):
# 각 행에서 빈도가 가장 높은 카테고리 이름 추출
df['main_cat'] = df[[f'cat{i}_freq' for i in range(1, 11)]].idxmax(axis=1)
# 'catN_freq' 에서 N만 추출해 숫자로 변환
df['main_cat_id'] = df['main_cat'].str.extract(r'cat(\\d+)_freq').astype(int)
# 주력 카테고리 총 소비 금액, 빈도 추출
df['main_cat_total'] = df.apply(
lambda row: row.get(f"cat{int(row['main_cat_id'])}_amount", 0), axis=1
)
df['main_cat_freq'] = df.apply(
lambda row: row.get(f"cat{int(row['main_cat_id'])}_freq", 0), axis=1
)
# 1회당 평균 소비 금액 계산 (빈도가 0이면 0)
df['main_cat_avg_amount'] = df.apply(
lambda row: row['main_cat_total'] / row['main_cat_freq'] if row['main_cat_freq'] > 0 else 0, axis=1
)
# 예상 루틴 소비 (주기 3주 기준)
df['expected_routine_spend'] = (df['main_cat_freq'] / 3) * df['main_cat_avg_amount']
return df
weekly = add_main_routine_avg(weekly)
# ===== 학습용 Feature, Label, 정보 생성 함수 =====
def make_features_with_info(df, window=8):
X, y, info = [], [], []
for user in df['userId'].unique():
user_df = df[df['userId'] == user].sort_values('week')
spend = user_df['spendCost'].values # 소비금액
avg = user_df['avg_spend'].values # 평균 소비금액
cat_feats = user_df[[f'cat{i}_freq' for i in range(1, 11)]].values # 카테고리별 빈도
routine_avg = user_df['main_cat_avg_amount'].values # 주력 카테고리 1회당 평균 소비
routine_spend = user_df['expected_routine_spend'].values # 예상 루틴 소비
if len(spend) < window + 1:
continue
for i in range(len(spend) - window):
# window 길이만큼 과거 데이터 슬라이스
spend_slice = spend[i:i+window]
avg_slice = avg[i:i+window]
cat_slice = cat_feats[i:i+window]
routine_avg_slice = routine_avg[i:i+window]
routine_spend_slice = routine_spend[i:i+window]
# 여러 피처 배열을 1차원 벡터로 결합
features = np.concatenate([
spend_slice,
avg_slice,
cat_slice.flatten(),
routine_avg_slice,
routine_spend_slice
])
X.append(features) # feature 벡터
y.append(spend[i + window]) # 다음 주 소비금액 (타겟)
info.append((user, user_df['week'].iloc[i + window])) # userId, 예측 주차 정보
return np.array(X), np.log1p(np.array(y)), info # 로그 변환된 라벨 반환
# ===== Feature, Label, info 생성 =====
X, y, info = make_features_with_info(weekly)
# ===== 학습용, 검증용 데이터 분할 =====
X_train, X_val, y_train, y_val, info_train, info_val = train_test_split(
X, y, info, test_size=0.2, random_state=42
)
# ===== 모델 선언 및 학습 =====
model = RandomForestRegressor(n_estimators=200, random_state=42)
model.fit(X_train, y_train)
# ===== 검증 데이터 예측 =====
preds = model.predict(X_val)
# 로그 역변환
y_val_exp = np.expm1(y_val)
preds_exp = np.expm1(preds)
# ===== 평가 지표 계산 =====
mae = mean_absolute_error(y_val_exp, preds_exp)
rmse = np.sqrt(mean_squared_error(y_val_exp, preds_exp))
def asymmetric_loss(y_true, y_pred, under_weight=2.0, over_weight=1.0):
"""
과소 예측에 더 민감한 비대칭 손실 함수
under_weight: 실제값보다 작게 예측했을 때 가중치
over_weight: 실제값보다 크게 예측했을 때 가중치
"""
error = y_pred - y_true
loss = np.where(error < 0, under_weight * (error**2), over_weight * (error**2))
return np.mean(loss)
asym_loss = asymmetric_loss(y_val_exp, preds_exp)
print(f"✅ 모델 : 랜덤포레스트회귀")
print(f"✅ 로그 변환 후 MAE: {mae:,.0f}원")
print(f"✅ 로그 변환 후 RMSE: {rmse:,.0f}원")
print(f"⚠️ 과소 예측에 더 민감한 Asymmetric Loss: {asym_loss:,.0f}원")
# ===== 유저별 최근 주 예측값과 실제값 비교 =====
df_results = pd.DataFrame(info_val, columns=['userId', 'week'])
df_results['actual'] = y_val_exp
df_results['pred'] = preds_exp
latest_results = df_results.sort_values('week').groupby('userId').tail(1)
latest_results = latest_results.sort_values('userId')
for _, row in latest_results.iterrows():
print(f"userId:{int(row['userId'])} 최근 주({int(row['week'])}) 실제값: {int(row['actual']):,}원, 예측값: {int(row['pred']):,}원")
# ===== 모델 저장 (필요 시 주석 해제) =====
# joblib.dump((model, True), '/content/drive/MyDrive/PaEmotion/final_predict.pkl')
<aside> 💡
</aside>
✅ 모델 : 랜덤포레스트회귀
✅ 로그 변환 후 MAE: 31,945원
✅ 로그 변환 후 RMSE: 54,504원
⚠️ 과소 예측에 더 민감한 Asymmetric Loss: 5,408,831,436원
userId:1 최근 주(30) 실제값: 332,499원, 예측값: 297,549원
userId:2 최근 주(30) 실제값: 514,799원, 예측값: 346,987원
userId:3 최근 주(26) 실제값: 612,799원, 예측값: 516,129원
userId:4 최근 주(30) 실제값: 134,999원, 예측값: 130,384원
userId:5 최근 주(30) 실제값: 289,300원, 예측값: 302,950원
userId:6 최근 주(31) 실제값: 263,199원, 예측값: 256,584원
userId:7 최근 주(30) 실제값: 48,000원, 예측값: 33,126원
userId:8 최근 주(28) 실제값: 73,999원, 예측값: 75,630원
userId:10 최근 주(28) 실제값: 371,099원, 예측값: 358,632원
userId:11 최근 주(31) 실제값: 122,800원, 예측값: 127,215원
userId:12 최근 주(30) 실제값: 45,800원, 예측값: 57,107원
userId:14 최근 주(29) 실제값: 150,800원, 예측값: 83,420원
userId:15 최근 주(28) 실제값: 15,400원, 예측값: 17,439원
✅ 모델 : XGBoost 회귀
✅ 로그 변환 후 MAE: 40,051원
✅ 로그 변환 후 RMSE: 60,607원
⚠️ 과소 예측에 더 민감한 Asymmetric Loss: 5,758,373,768원
userId:1 최근 주(30) 실제값: 332,499원, 예측값: 263,356원
userId:2 최근 주(30) 실제값: 514,799원, 예측값: 384,896원
userId:3 최근 주(26) 실제값: 612,799원, 예측값: 516,670원
userId:4 최근 주(30) 실제값: 134,999원, 예측값: 154,309원
userId:5 최근 주(30) 실제값: 289,300원, 예측값: 280,975원
userId:6 최근 주(31) 실제값: 263,199원, 예측값: 241,178원
userId:7 최근 주(30) 실제값: 48,000원, 예측값: 33,656원
userId:8 최근 주(28) 실제값: 73,999원, 예측값: 79,732원
userId:10 최근 주(28) 실제값: 371,099원, 예측값: 377,063원
userId:11 최근 주(31) 실제값: 122,800원, 예측값: 107,803원
userId:12 최근 주(30) 실제값: 45,800원, 예측값: 63,365원
userId:14 최근 주(29) 실제값: 150,800원, 예측값: 75,658원
userId:15 최근 주(28) 실제값: 15,400원, 예측값: 15,287원
✅ 모델 : 랜덤포레스트회귀
✅ 로그 변환 후 MAE: 65,741원
✅ 로그 변환 후 RMSE: 91,894원
⚠️ 과소 예측에 더 민감한 Asymmetric Loss: 16,160,033,823원
userId:1 최근 주(30) 실제값: 364,000원, 예측값: 296,487원
userId:2 최근 주(30) 실제값: 773,999원, 예측값: 499,083원
userId:3 최근 주(26) 실제값: 539,600원, 예측값: 489,214원
userId:4 최근 주(30) 실제값: 54,799원, 예측값: 117,538원
userId:5 최근 주(30) 실제값: 402,500원, 예측값: 270,637원
userId:6 최근 주(31) 실제값: 236,900원, 예측값: 317,714원
userId:7 최근 주(30) 실제값: 34,899원, 예측값: 24,409원
userId:8 최근 주(28) 실제값: 71,899원, 예측값: 79,985원
userId:10 최근 주(28) 실제값: 385,400원, 예측값: 425,161원
userId:11 최근 주(31) 실제값: 172,899원, 예측값: 204,455원
userId:12 최근 주(30) 실제값: 78,900원, 예측값: 48,337원
userId:14 최근 주(29) 실제값: 126,500원, 예측값: 90,195원
userId:15 최근 주(28) 실제값: 18,999원, 예측값: 13,590원
✅ 모델 : XGBoost 회귀
✅ 로그 변환 후 MAE: 62,833원
✅ 로그 변환 후 RMSE: 80,028원
⚠️ 과소 예측에 더 민감한 Asymmetric Loss: 11,492,560,033원
userId:1 최근 주(30) 실제값: 364,000원, 예측값: 311,335원
userId:2 최근 주(30) 실제값: 773,999원, 예측값: 583,196원
userId:3 최근 주(26) 실제값: 539,600원, 예측값: 514,489원
userId:4 최근 주(30) 실제값: 54,799원, 예측값: 154,285원
userId:5 최근 주(30) 실제값: 402,500원, 예측값: 283,540원
userId:6 최근 주(31) 실제값: 236,900원, 예측값: 345,615원
userId:7 최근 주(30) 실제값: 34,899원, 예측값: 26,409원
userId:8 최근 주(28) 실제값: 71,899원, 예측값: 75,525원
userId:10 최근 주(28) 실제값: 385,400원, 예측값: 421,631원
userId:11 최근 주(31) 실제값: 172,899원, 예측값: 198,473원
userId:12 최근 주(30) 실제값: 78,900원, 예측값: 53,897원
userId:14 최근 주(29) 실제값: 126,500원, 예측값: 64,820원
userId:15 최근 주(28) 실제값: 18,999원, 예측값: 14,944원
✅ 모델 : 랜덤포레스트회귀
✅ 로그 변환 후 MAE: 60,095원
✅ 로그 변환 후 RMSE: 83,536원
⚠️ 과소 예측에 더 민감한 Asymmetric Loss: 7,931,867,785원
userId:1 최근 주(30) 실제값: 211,300원, 예측값: 363,618원
userId:2 최근 주(30) 실제값: 500,399원, 예측값: 704,054원
userId:3 최근 주(26) 실제값: 698,800원, 예측값: 615,421원
userId:4 최근 주(30) 실제값: 215,599원, 예측값: 174,753원
userId:5 최근 주(30) 실제값: 222,800원, 예측값: 260,245원
userId:6 최근 주(31) 실제값: 179,100원, 예측값: 263,914원
userId:7 최근 주(30) 실제값: 27,499원, 예측값: 32,349원
userId:8 최근 주(28) 실제값: 52,200원, 예측값: 63,426원
userId:10 최근 주(28) 실제값: 212,699원, 예측값: 258,749원
userId:11 최근 주(31) 실제값: 184,399원, 예측값: 201,304원
userId:12 최근 주(30) 실제값: 37,599원, 예측값: 35,637원
userId:14 최근 주(29) 실제값: 42,300원, 예측값: 64,177원
userId:15 최근 주(28) 실제값: 17,500원, 예측값: 17,364원
✅ 모델 : XGBoost 회귀
✅ 로그 변환 후 MAE: 63,110원
✅ 로그 변환 후 RMSE: 82,685원
⚠️ 과소 예측에 더 민감한 Asymmetric Loss: 8,087,501,137원
userId:1 최근 주(30) 실제값: 211,300원, 예측값: 364,546원
userId:2 최근 주(30) 실제값: 500,399원, 예측값: 687,674원
userId:3 최근 주(26) 실제값: 698,800원, 예측값: 656,204원
userId:4 최근 주(30) 실제값: 215,599원, 예측값: 115,626원
userId:5 최근 주(30) 실제값: 222,800원, 예측값: 248,420원
userId:6 최근 주(31) 실제값: 179,100원, 예측값: 284,155원
userId:7 최근 주(30) 실제값: 27,499원, 예측값: 29,198원
userId:8 최근 주(28) 실제값: 52,200원, 예측값: 77,887원
userId:10 최근 주(28) 실제값: 212,699원, 예측값: 280,218원
userId:11 최근 주(31) 실제값: 184,399원, 예측값: 209,258원
userId:12 최근 주(30) 실제값: 37,599원, 예측값: 15,814원
userId:14 최근 주(29) 실제값: 42,300원, 예측값: 69,728원
userId:15 최근 주(28) 실제값: 17,500원, 예측값: 10,061원
✅ 모델 : 랜덤포레스트회귀
✅ 로그 변환 후 MAE: 53,000원
✅ 로그 변환 후 RMSE: 84,265원
⚠️ 과소 예측에 더 민감한 Asymmetric Loss: 13,848,160,659원
userId:1 최근 주(30) 실제값: 180,099원, 예측값: 234,872원
userId:2 최근 주(30) 실제값: 567,700원, 예측값: 424,423원
userId:3 최근 주(26) 실제값: 683,699원, 예측값: 403,051원
userId:4 최근 주(30) 실제값: 140,200원, 예측값: 90,736원
userId:6 최근 주(29) 실제값: 232,900원, 예측값: 179,250원
userId:7 최근 주(29) 실제값: 30,899원, 예측값: 29,963원
userId:8 최근 주(28) 실제값: 103,100원, 예측값: 82,181원
userId:9 최근 주(31) 실제값: 111,200원, 예측값: 129,676원
userId:11 최근 주(30) 실제값: 183,199원, 예측값: 208,201원
userId:13 최근 주(31) 실제값: 129,900원, 예측값: 92,935원
userId:14 최근 주(29) 실제값: 90,699원, 예측값: 88,514원
✅ 모델 : XGBoost 회귀
✅ 로그 변환 후 MAE: 63,148원
✅ 로그 변환 후 RMSE: 97,278원
⚠️ 과소 예측에 더 민감한 Asymmetric Loss: 18,120,741,837원
userId:1 최근 주(30) 실제값: 180,099원, 예측값: 263,971원
userId:2 최근 주(30) 실제값: 567,700원, 예측값: 380,765원
userId:3 최근 주(26) 실제값: 683,699원, 예측값: 383,052원
userId:4 최근 주(30) 실제값: 140,200원, 예측값: 131,687원
userId:6 최근 주(29) 실제값: 232,900원, 예측값: 149,611원
userId:7 최근 주(29) 실제값: 30,899원, 예측값: 24,115원
userId:8 최근 주(28) 실제값: 103,100원, 예측값: 89,049원
userId:9 최근 주(31) 실제값: 111,200원, 예측값: 144,762원
userId:11 최근 주(30) 실제값: 183,199원, 예측값: 249,641원
userId:13 최근 주(31) 실제값: 129,900원, 예측값: 107,117원
userId:14 최근 주(29) 실제값: 90,699원, 예측값: 80,676원
✅ 모델 : 랜덤포레스트회귀
✅ 로그 변환 후 MAE: 48,642원
✅ 로그 변환 후 RMSE: 77,798원
⚠️ 과소 예측에 더 민감한 Asymmetric Loss: 11,853,409,098원
userId:1 최근 주(30) 실제값: 345,900원, 예측값: 190,467원
userId:2 최근 주(30) 실제값: 833,899원, 예측값: 599,049원
userId:3 최근 주(26) 실제값: 497,500원, 예측값: 503,070원
userId:4 최근 주(30) 실제값: 119,400원, 예측값: 101,581원
userId:6 최근 주(29) 실제값: 255,699원, 예측값: 193,081원
userId:7 최근 주(29) 실제값: 39,599원, 예측값: 30,782원
userId:8 최근 주(28) 실제값: 59,700원, 예측값: 77,562원
userId:9 최근 주(31) 실제값: 157,500원, 예측값: 137,752원
userId:11 최근 주(30) 실제값: 233,600원, 예측값: 198,533원
userId:13 최근 주(31) 실제값: 29,600원, 예측값: 82,182원
userId:14 최근 주(29) 실제값: 57,000원, 예측값: 82,077원
✅ 모델 : XGBoost 회귀
✅ 로그 변환 후 MAE: 50,331원
✅ 로그 변환 후 RMSE: 69,937원
⚠️ 과소 예측에 더 민감한 Asymmetric Loss: 9,346,729,398원
userId:1 최근 주(30) 실제값: 345,900원, 예측값: 288,657원
userId:2 최근 주(30) 실제값: 833,899원, 예측값: 647,145원
userId:3 최근 주(26) 실제값: 497,500원, 예측값: 386,356원
userId:4 최근 주(30) 실제값: 119,400원, 예측값: 104,900원
userId:6 최근 주(29) 실제값: 255,699원, 예측값: 168,304원
userId:7 최근 주(29) 실제값: 39,599원, 예측값: 26,989원
userId:8 최근 주(28) 실제값: 59,700원, 예측값: 77,488원
userId:9 최근 주(31) 실제값: 157,500원, 예측값: 132,191원
userId:11 최근 주(30) 실제값: 233,600원, 예측값: 229,774원
userId:13 최근 주(31) 실제값: 29,600원, 예측값: 81,852원
userId:14 최근 주(29) 실제값: 57,000원, 예측값: 78,154원
<aside> 💡
</aside>