오늘 강의는 "선형대수 기초"이지만, 이미 대학교에서 강의를 수강하기도 했고 수식이 많아 모두 정리하기에 무리가 있어 학습은 영상으로만 진행하였다. 대신에 머신러닝 과제인 "음식 배달에 걸리는 시간 예측하기"를 진행한 내용을 TIL에 적어보려고 한다.
음식 배달에 걸리는 시간 예측하기
문제 이해
실습 문제 소개
- 목표 : 음식 배달에 걸리는 시간 예측하기
- 이유 : 배달 시간을 정확히 예측하는 것은 사용자의 경험에 많은 영향을 미침
- 사용자의 경험
- under-prediction : 예측된 배달 시간보다 실제 배달 시간이 더 걸린 경우
- over-prediction : 예측된 배달 시간보다 실제 배달 시간이 덜 걸린 경우
- under-prediction이 over-prediction보다 두 배로 사용자의 경험에 안 좋은 영향을 줌
- 따라서 실제 배달 시간과 가깝게 예측하되 under-prediction을 최소화하는 것이 좋은 예측 모델
- 배달 시간에 영향을 주는 요소
- 주문 시간 (오후 7시 vs 오후 4시)
- 지역 (번화가 vs 외곽지역)
- 식당의 속성 (음식의 카테고리)
- 주문의 속성 (아이템 개수)
데이터 소개
- 지역 속성
- market_id : 배달이 이루어지는 도시의 지역 ID
- 시간 속성
- created_at : 주문이 생성된 시간의 timestamp (UTC)
- actual_delivery_time : 주문자가 배달을 받은 시간의 timestamp (UTC)
- 식당 속성
- store_id : 식당 ID
- store_primary_category : 식당의 카테고리
- order_protocol : 주문을 받을 수 있는 방식을 나타내는 ID
- 지역 상황 속성
- total_onshift : 주문이 생성되었을 때 가게로부터 10 마일 이내에 있는 배달원의 수
- total_busy : total_onshift 중 주문에 관여하고 있는 사람의 수
- total_outstanding_orders : 주문한 가게로부터 10 마일 이내에 있는 다른 주문의 수
- 다른 예측 값
- estimated_order_place_duration : 식당이 주문을 받을 때까지 걸리는 예상 시간 (sec)
- estimated_store_to_consumer_driving_duration : 식당에서 주문자에게 도착까지 걸리는 예측 시간 (sec)
데이터 레이블
- 음식 배달에 걸리는 시간 : (actual_delivery_time - created_at)을 초 단위로 변경해 생성
- 레이블 생성 후 actual_delivery_time 컬럼은 삭제
- 데이터를 랜덤으로 10%를 추출해 테스트 데이터로 사용
진행 과정
데이터 가져오기
import pandas as pd
raw = pd.read_csv('delivery_raw.csv', sep='\t')
전처리
- label 생성 및 actual_actual_delivery_time, created_at 제거
data = raw.dropna(subset=["actual_delivery_time"])
data['created_at'] = pd.to_datetime(data['created_at'])
data['actual_delivery_time'] = pd.to_datetime(data['actual_delivery_time'])
data['label'] = (data['actual_delivery_time'] - data['created_at']).dt.total_seconds().astype(int)
data = data.drop(['actual_delivery_time'], axis=1)
data = data.drop(['created_at'], axis=1)
- NULL 전처리 (제거)
- market_id, store_primary_category, order_protocol
- estimated_order_place_duration, estimated_store_to_consumer_driving_duration
# market_id가 null 인 경우 제거
data.dropna(subset=['market_id'], inplace=True)
data.dropna(subset=['store_primary_category'], inplace=True)
data.dropna(subset=['order_protocol'], inplace=True)
data.dropna(subset=['estimated_order_place_duration'], inplace=True)
data.dropna(subset=['estimated_store_to_consumer_driving_duration'], inplace=True)
- NULL 전처리 (대체)
- total_onshift, total_busy, total_outstanding_orders
- martket_id (지역 위치)와 관계가 있을 것이라 생각해 groupby를 활용해 평균으로 대체
mean_onshift_per_market = data.groupby('market_id')['total_onshift'].mean()
# total_onshift의 null 값을 대체하는 함수 정의
def fill_na_with_mean(row):
if pd.isnull(row['total_onshift']):
return mean_onshift_per_market[row['market_id']]
else:
return row['total_onshift']
# apply 함수를 사용하여 null 값 대체
data['total_onshift'] = data.apply(fill_na_with_mean, axis=1)
mean_busy_per_market = data.groupby('market_id')['total_busy'].mean()
def fill_na_with_mean(row):
if pd.isnull(row['total_busy']):
return mean_busy_per_market[row['market_id']]
else:
return row['total_busy']
data['total_busy'] = data.apply(fill_na_with_mean, axis=1)
mean_outstanding_orders_per_market = data.groupby('market_id')['total_outstanding_orders'].mean()
def fill_na_with_mean(row):
if pd.isnull(row['total_outstanding_orders']):
return mean_outstanding_orders_per_market[row['market_id']]
else:
return row['total_outstanding_orders']
data['total_outstanding_orders'] = data.apply(fill_na_with_mean, axis=1)
- store_primary_category 인코딩
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
label_encoder = LabelEncoder()
data['store_primary_category'] = label_encoder.fit_transform(data['store_primary_category'])
- label 이상치 제거 : 비정상적으로 큰 수치나 작은 수치 제거 (30만 sec 이상, 102 sec)
data = data.drop([2690, 185550, 27189, 63294])
학습 데이터, 테스트 데이터 분리
from sklearn.model_selection import train_test_split
train, test = train_test_split(data, test_size=0.1, random_state=42)
하이퍼 파라미터 튜닝 (랜덤 포레스트)
- RandomForestRegressor 회귀 모델 사용
- KFold를 사용해 교차 검증
- optuna로 하이퍼 파라미터 튜닝
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import accuracy_score
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import KFold
import numpy as np
import optuna
X = train.drop(['label'], axis=1)
y = train['label']
# Objective function 정의
def objective(trial):
# 하이퍼파라미터 설정
n_estimators = trial.suggest_int('n_estimators', 50, 200)
max_depth = trial.suggest_int('max_depth', 5, 30)
min_samples_split = trial.suggest_int('min_samples_split', 2, 20)
min_samples_leaf = trial.suggest_int('min_samples_leaf', 1, 20)
# 랜덤 포레스트 회귀 모델 생성
model = RandomForestRegressor(
n_estimators=n_estimators,
max_depth=max_depth,
min_samples_split=min_samples_split,
min_samples_leaf=min_samples_leaf,
random_state=42
)
# K-Fold 교차 검증 설정
kf = KFold(n_splits=5, shuffle=True, random_state=42)
rmse_list = []
# K-Fold 교차 검증 수행
for train_index, test_index in kf.split(X):
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y.iloc[train_index], y.iloc[test_index]
# 모델 학습
model.fit(X_train, y_train)
# 예측
y_pred = model.predict(X_test)
# RMSE 계산
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
rmse_list.append(rmse)
return np.mean(rmse_list)
# Callback 함수 정의
def print_callback(study, trial):
print(f'Trial {trial.number}: RMSE={trial.value:.2f}, Params={trial.params}')
# Optuna 스터디 생성 및 최적화 실행
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=10, callbacks=[print_callback])
# 최적의 하이퍼파라미터 출력
print(f'Best hyperparameters: {study.best_params}')
print(f'Best RMSE: {study.best_value:.2f}')
'[프로그래머스] 데이터 엔지니어링 데브코스 3기 > TIL(Today I Learn)' 카테고리의 다른 글
[TIL - 76일 차] Spark, SparkML 실습 (1) (0) | 2024.07.08 |
---|---|
[TIL - 73일 차] 음식 배달에 걸리는 시간 예측하기 (2) (0) | 2024.07.03 |
[TIL - 71일 차] 머신러닝 기초 (0) | 2024.07.01 |
[TIL - 70일 차] Kafka와 Spark Streaming 기반 스트리밍 처리 (5) (0) | 2024.06.28 |
[TIL - 69일 차] Kafka와 Spark Streaming 기반 스트리밍 처리 (4) (0) | 2024.06.28 |