본문 바로가기
데브코스

데이터 엔지니어링 70일차 TIL

by 우솨 2024. 7. 6.

학습 내용

배달시간 예측 해보기

import pandas as pd
import numpy as np

# 전체 데이터프레임 보기
# pd.set_option('display.max_rows', None)
# pd.set_option('display.max_columns', None)
# pd.set_option('display.width', None)
# pd.set_option('display.max_colwidth', None)

# 되돌리기
# pd.reset_option('all')

raw = pd.read_csv('delivery_raw.csv', sep='\t')

# "actual_delivery_time"이 없는 값 제외
raw_dropna_label = raw.dropna(subset=["actual_delivery_time"])

# "created_at" 과 "actual_delivery_time" 컬럼을 데이트타임 속성으로 변경
raw_dropna_label['created_at'] = pd.to_datetime(raw_dropna_label['created_at'])
raw_dropna_label['actual_delivery_time'] = pd.to_datetime(raw_dropna_label['actual_delivery_time'])

#predict 속성컬럼 생성, 초 단위로 변경
raw_dropna_label["predict"] = raw_dropna_label['actual_delivery_time'] - raw_dropna_label['created_at']
raw_dropna_label['predict'] = raw_dropna_label['predict'].dt.total_seconds()

# 'created_at' 주문 일자의 시간만 추출하여 hour 생성
# 'created_at' 주문 일자의 요일만 추출하여 day 생성 (0=월요일, 6= 일요일)
raw_dropna_label['hour'] = raw_dropna_label['created_at'].dt.hour
raw_dropna_label['day'] = raw_dropna_label['created_at'].dt.dayofweek

# 학습데이터, 테스트 데이터 1:9로 나누기
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(raw_dropna_label, test_size=0.1, random_state=42)
# 인덱스 초기화
train_set = train_set.reset_index(drop=True)
test_set = test_set.reset_index(drop=True)



# 오브젝트값, 숫자 값
objects = ["store_primary_category", "created_at", "actual_delivery_time",  "predict"]
nums = ['market_id', 'store_id', 'order_protocol', 'total_items', 'subtotal',
       'num_distinct_items', 'min_item_price', 'max_item_price',
       'total_onshift', 'total_busy', 'total_outstanding_orders',
       'estimated_order_place_duration',
       'estimated_store_to_consumer_driving_duration', 'hour', 'day']



# null인 모든 행 제거(데이터가 충분히 크다고 판단)
train_set = raw_dropna_label.dropna().reset_index(drop=True)

# 마이너스인 값 확인
negative_values = train_set[nums][(train_set[nums] < 0).any(axis=1)]
# 이상치 마이너스 값 제거
train_set = train_set.drop(negative_values.index).reset_index(drop=True)

# 목표: 음식배달에 걸리는 시간 예측하기
# • 왜 필요한가?
# • 배달시간을 정확하게 예측하는 것은 사용자의 경험에 많은 영향을 미치게 됨
# • 예측된 배달시간보다 실제 배달시간이 더 걸린 경우(under-prediction)가 반대의 경우(over-prediction)보다 두 배로
# 사용자의 경험에 안 좋은 영향을 줌
# • 가능한 실제 배달시간과 가까운 값을 예측하되 동시에 under-prediction을 최소화하는 것이 좋은 예측모델 (예, 모델이
# 30분을 예측하고 실제 배달은 29분에 도착)
# • 배달시간에 영향을 주는 요소들
# • 주문시간 (오후 7시 vs. 오후 4시)
# • 지역 (번화가 vs. 외곽지역)
# • 식당의 속성 (음식의 카테고리)
# • 주문의 속성 (아이템 개수)

# 실습문제 소개
# Copyright 2023. Grepp inc. All rights reserved.
# • 이 문제를 해결하기 위해 머신러닝이 필요한 이유?
# • 머신러닝: 과거에 관찰했던 데이터에서 패턴을 학습해서 현재(미래)의 데이터에 적용시키는 것
# • (주문시간, 지역, 식당속성, 주문속성) => 실제 배달시간
# • x => y
# • f(x) ~ y

# 6
# 음식배달 예측문제 데이터 살펴보기
# Copyright 2023. Grepp inc. All rights reserved.
# • 위 속성들 중 actual_delivery_time을 제외한 모든 속성들을 모델의 입력으로 사용할 수 있다. 모델이 예측해야하는 값은
# actual_delivery_time과 created_at을 사용해서 생성하면 된다 (초단위로 표현된 두 속성의 차이값).
# • 주어진 이 속성들 외에 이것들로부터 파생될 수 있는 속성들을 추가로 만들어서 사용할 수도 있다
# • 학습/테스트 데이터 구분
# • 위 데이터(delivery_raw.csv)에서 랜덤하게 10%를 추출해서 테스트 데이터로 사용하고 나머지는 학습데이터로
# 사용할 것
# 7
# 음식배달 예측문제 데이터 살펴보기
# Copyright 2023. Grepp inc. All rights reserved.
# • 최종 결과물
# • 간단한 요약문 (3페이지 이내)
# • 데이터 전처리와 속성 생성에 대한 간단한 설명
# • 학습을 위해서 어떤 모델을 사용했는지 그리고 어떠한 손실함수를 사용했는지를 간단히 설명
# • 테스트 데이터에 대한 평가지표들 (아래 두가지를 반드시 포함할 것)
# • Root Mean Square Error (RMSE)
# • Under-prediction의 비율 (under-prediction 개수 / 테스트 데이터의 샘플수)
# • 모델 학습에 사용한 Jupyter notebook 파일

train_set.info()

#  0   market_id                                     52591 non-null  float64     지역(배달이 이루어지는 도시) 아이디
#  1   created_at                                    52869 non-null  object      주문이 생성된 시간의 Timestamp(UTC) --- 예측 값에 사용 - created_at
#  2   actual_delivery_time                          52866 non-null  object      주문자가 배달을 받은 시간의 Timestamp(UTC) --- 예측 값에 사용
#  3   store_id                                      52868 non-null  float64     식당 아이디
#  4   store_primary_category                        51595 non-null  object      식당의 카테고리(italian, asian 등)
#  5   order_protocol                                52597 non-null  float64     주문을 받을 수 있는 방식을 나타내는 아이디
#  6   total_items                                   52868 non-null  float64
#  7   subtotal                                      52868 non-null  float64
#  8   num_distinct_items                            52868 non-null  float64
#  9   min_item_price                                52868 non-null  float64
#  10  max_item_price                                52868 non-null  float64
#  11  total_onshift                                 48373 non-null  float64     주문이 생성되었을 때 가게로부터 10마일 이내에 있는 배달원들의 수
#  12  total_busy                                    48373 non-null  float64     위 배달원들 중 주문에 관여하고 있는 사람들의 수
#  13  total_outstanding_orders                      48373 non-null  float64     주문한 가게로부터 10마일 이내에 있는 다른 주문들의 수
#  14  estimated_order_place_duration                52868 non-null  float64     식당이 주문을 받을 때까지 걸릴 것으로 예상되는 시간 (초단위)
#  15  estimated_store_to_consumer_driving_duration  52733 non-null  float64     식당에서 출발해 주문자에 도착할 때까지 걸릴 것으로 예측되는 시간 (초단위)



print(len(nums), len(objects), len(train_set.columns))

train_set.isnull().sum()

print(train_set)

## 데이터 값, 종류 확인
for i in objects:
  print(train_set[i].value_counts())
  print('-'*20)

for i in nums:
  print(train_set[i].value_counts())
  print('-'*20)

# "actual_delivery_time", "created_at"는 데이트타임이면서 상관관계가 적기때문에 삭제
# 요일과 시간대 속성은 이미 초기에 생성했음
train_set.drop(["actual_delivery_time", "created_at"], axis=1, inplace=True)
test_set.drop(["actual_delivery_time", "created_at"], axis=1, inplace=True)

cols = ['market_id', 'store_id', 'order_protocol', 'total_items', 'subtotal',
       'num_distinct_items', 'min_item_price', 'max_item_price',
       'total_onshift', 'total_busy', 'total_outstanding_orders',
       'estimated_order_place_duration',
       'estimated_store_to_consumer_driving_duration', 'hour', 'day', "predict"]

# 상관관계 확인
train_set[cols].corr()['predict'].sort_values(ascending=False)

train_set[cols].corr()['predict'].abs().sort_values(ascending=False)



train_set

# LabelEncoder: 주어진 순서가 있는(ordinal) 데이터를 인코딩할 때 사용.
# OrdinalEncoder: 크기에 상관있는 텍스트 데이터를 인코딩할 때 사용.
# OnehotEncdoer : 순서, 크기에 상관이 없는 텍스트 데이터를 인코딩할 때 사용

from sklearn.preprocessing import OrdinalEncoder

oe = OrdinalEncoder()
ob = ["store_primary_category"]

combined_data = pd.concat([train_set, test_set], ignore_index=True)

for i in ob :
  oe.fit(combined_data[[i]])
  train_set[[i]] = oe.transform(train_set[[i]])
  test_set[[i]] = oe.transform(test_set[[i]])

# #원핫 인코더
# import pandas as pd

# # 합쳐진 데이터 프레임을 준비합니다.
# combined_data = pd.concat([train_set, test_set], ignore_index=True)

# # store_primary_category 열을 원핫 인코딩합니다.
# combined_data = pd.get_dummies(combined_data, columns=["store_primary_category"])

# # 다시 train_set과 test_set으로 분리합니다.
# train_set = combined_data.iloc[:len(train_set)]
# test_set = combined_data.iloc[len(train_set):]

test_set





# StandardScaler사용할 컬럼
# : [total_items, subtotal, num_distinct_items, min_item_price, max_item_price, total_onshift, total_busy, total_outstanding_orders, estimated_order_place_duration, estimated_store_to_consumer_driving_duration]
s = ["total_items", "subtotal", "num_distinct_items", "min_item_price", "max_item_price", "total_onshift", "total_busy", "total_outstanding_orders", "estimated_order_place_duration", "estimated_store_to_consumer_driving_duration"
]

from sklearn.preprocessing import StandardScaler

ss = StandardScaler()

for i in s:
  ss.fit(train_set[[i]])
  train_set[[i]] = ss.transform(train_set[[i]])
  test_set[[i]] = ss.transform(test_set[[i]])

# 훈련/정답 데이터 나누기
# y_tr 이 예측할 값 - "predict"
x_tr = train_set.drop("predict", axis=1)
y_tr = train_set['predict']

x_val = test_set.drop("predict", axis=1)
y_val = test_set['predict']



# 모델훈련 (xgboost 사용)
from xgboost import XGBRegressor

model = XGBRegressor()
model.fit(x_tr,y_tr)

# 예측 값
pred_tr = model.predict(x_tr)

# 변수의 중요도 확인
cols_importance = pd.DataFrame({'feature':x_tr.columns, 'xgb':model.feature_importances_})
cols_importance

# 회귀 모델의 예측 성능을 평가
# MSE, MAE, r2_score, RMSE, MAPE
from sklearn.metrics import mean_squared_error
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import r2_score
from sklearn.metrics import mean_squared_error

# 파라미터 설정전 기본 모델 평가 값
mse = mean_squared_error(y_tr, pred_tr)
mae = mean_absolute_error(y_tr, pred_tr)
r2 = r2_score(y_tr, pred_tr)
rmse = np.sqrt(mean_squared_error(y_tr, pred_tr))


print(f'Mean Squared Error (MSE): {mse}\n'
      f'Mean Absolute Error (MAE): {mae}\n'
      f'R^2 Score: {r2}\n'
      f'Root Mean Squared Error (RMSE): {rmse}\n'
)

# #GridSearchCV를 사용한 파라미터 설정

# from sklearn.model_selection import GridSearchCV
# param_grid = {
#     'learning_rate': [0.1, 0.15, 0.2],  # 학습률
#     'max_depth': [5, 7, 9],             # 트리의 최대 깊이
#     'n_estimators': [100, 200, 300],    # 트리의 개수
#     # 'min_child_weight': [1, 3, 5],       # 각 잎사귀 노드에서 추가적으로 필요한 가중치
#     # 'gamma': [0, 0.1, 0.2],             # 트리의 가지치기에 필요한 최소 손실 감소
#     # 'subsample': [0.6, 0.8, 1.0],        # 각 트리에서 사용할 행 샘플링 비율
#     # 'colsample_bytree': [0.6, 0.8, 1.0], # 각 트리에서 사용할 열 샘플링 비율
#     # 'reg_alpha': [0, 0.1, 0.5],          # L1 정규화 텀의 파라미터
#     # 'reg_lambda': [0, 0.1, 0.5]          # L2 정규화 텀의 파라미터
# }

# grid_search = GridSearchCV(estimator=model, param_grid=param_grid, scoring='neg_root_mean_squared_error', n_jobs=-1, verbose=1)
# grid_search.fit(x_tr, y_tr)

# print("최적 하이퍼파라미터:", grid_search.best_params_)
# print("최고 성능 (RMSE):", -grid_search.best_score_)



#파라미터 수정후 final_model
#손실 함수로 mse 사용
# optimal_params = grid_search.best_params_

optimal_params = {'learning_rate': 0.2, 'max_depth': 9, 'n_estimators': 300}
final_model = XGBRegressor(objective='reg:squarederror', **optimal_params)
final_model.fit(x_tr,y_tr)

# 하이퍼파라미터 설정 전과 성능 비교
final_pred_tr = final_model.predict(x_tr)

final_mse = mean_squared_error(y_tr, final_pred_tr)
final_mae = mean_absolute_error(y_tr, final_pred_tr)
final_r2 = r2_score(y_tr, final_pred_tr)
final_rmse = np.sqrt(mean_squared_error(y_tr, final_pred_tr))


print(f'Mean Squared Error (MSE): {mse}, {final_mse} \n'
      f'Mean Absolute Error (MAE): {mae}, {final_mae} \n'
      f'R^2 Score: {r2}, {final_r2} \n'
      f'Root Mean Squared Error (RMSE): {rmse}, {final_rmse} \n'
)

# 테스트 데이터셋 비교

final_pred_val = final_model.predict(x_val)

final_mse = mean_squared_error(y_val, final_pred_val)
final_mae = mean_absolute_error(y_val, final_pred_val)
final_r2 = r2_score(y_val, final_pred_val)
final_rmse = np.sqrt(mean_squared_error(y_val, final_pred_val))


print(f'Mean Squared Error (MSE): {final_mse} \n'
      f'Mean Absolute Error (MAE): {final_mae} \n'
      f'R^2 Score: {final_r2} \n'
      f'Root Mean Squared Error (RMSE): {final_rmse} \n'
)

# 테스트 데이터 예측치 비교
# 초단위/ 절대값 초단위/ 분 단위(반 올림)
df = pd.DataFrame({
    'y_val': y_val,
    'final_pred_val': final_pred_val
})
df['diff_second'] = round(df['y_val'] - df['final_pred_val'],0)
df['diff_abs_second'] = abs(df['diff_second'])
df['diff_abs_minute'] = round(df['diff_abs_second']/60,0)

# Under-prediction 개수 계산
df['under_prediction'] = df['y_val']>df['final_pred_val']
under_prediction_count = df['under_prediction'].sum()
total_samples = len(df['y_val'])
under_prediction_ratio = under_prediction_count/total_samples
print(under_prediction_ratio)
print(f"Under-prediction 비율: {under_prediction_ratio:.2f}")

print(df)