본문 바로가기
프로젝트/완료

제주도 버스 운행시간 예측

by 가장따뜻한로봇 2023. 5. 11.

https://dacon.io/competitions/official/229611/overview/description

 


 

import pandas as pd
import datetime
data = pd.read_csv('./229611_버스 운행시간 예측 경진대회_data/train.csv')

 

데이터 EDA

# 컬럼 확인
data.columns

# info()
data.info()

# 결측치 확인
data.isnull().sum()

 

# 분석에 필요 없을 것 같은 컬럼 삭제
df = data.drop(['id','date','vh_id','route_nm','now_station','next_station'],axis=1)
df.head()

# now_arrive_time 이 문자열(str) 이므로 '시'를 삭제 하고 Dtype을 정수로 바꿈
df['now_arrive_time'] = df['now_arrive_time'].str.rstrip('시')
df = df.astype({'now_arrive_time':'int'})
# 각 컬럼별 연관도 확인
df.corr()

# 위도, 경도를 좌표로 보아(위도,경도) 정류장의 갯수 확인 (중복 제거)
print(len(df[['now_latitude','now_longitude']].drop_duplicates()))
print(len(df[['next_latitude','next_longitude']].drop_duplicates()))
# 현재 정류장과 다음 정류장의 갯수가 일치하지 않음

# 현재 정류장의 이름 개수 확인
print(len(data['now_station'].unique()))

# 정류장을 좌표로 나타냈을때와 차이가 큼.
# => 정류장 이름이 중복이 많다고 할 수 있음.

 

# 버스 노선 이름과 id, 차량 번호 개수 확인
print(len(data['route_nm'].unique()))
print(len(data['route_id'].unique()))
print(len(data['vh_id'].unique()))

# 버스노선과 id 는 1:1 매칭, vh_id를 보면 같은 노선을 가져도 차량번호가 다를수 있음

# 데이터의 날짜 개수 확인
len(data['date'].drop_duplicates())
# 14일. 2주간의 버스 데이터

 

데이터 전처리

 

# date를 이용하여 요일을 0~6로 나타냄
df['week'] = pd.to_datetime(data['date']).dt.weekday
df.head(1)

# 현재 정류장 569개, 다음 정류장 571개
#(위도,경도)를 하나의 좌표으로 보고 
# 현재 정류장 좌표와 다음 정류장 좌표를 합치고 중복 제거
position = set([str(a) for a in zip(data['now_latitude'],data['now_longitude'])] + [str(a) for a in zip(data['next_latitude'],data['next_longitude'])])
print(len(position))

# 전체 정류장의 위치를 Label encoding
from sklearn.preprocessing import LabelEncoder
label_enc = LabelEncoder()
label_enc.fit([str(t) for t in position])

# Labeling 된 현재 정류장, 다음 정류장 새로운 컬럼 생성
df['now_position'] = label_enc.transform([str(a) for a in zip(data['now_latitude'],data['now_longitude'])])
df['next_position'] = label_enc.transform([str(a) for a in zip(data['next_latitude'],data['next_longitude'])])

# route_id 도 Label encoding
label_route = LabelEncoder()
df['route_id'] = label_route.fit_transform(df['route_id'])

# 필요 없는 컬럼 삭제
# 위도 경도를 position으로 나타냈으므로 삭제.
df_train = df.drop(['now_latitude','now_longitude','next_latitude','next_longitude'],axis=1)
df_train.head(1)

# 분석 모델을 선정하기 위해 pycaret 사용.
from sklearn.model_selection import train_test_split
from pycaret.regression import *
# pycaret 을 돌리기 위해 train,validation 분리
x_train , x_val, _, _ = train_test_split(df_train,df_train['next_arrive_time'],test_size = 0.2,random_state = 43)

# pycaret 은 data 하나에 target 컬럼으로 선택
ixi_model = setup(session_id = 43, data = x_train, target = 'next_arrive_time',
                  test_data = x_val, 
                  normalize = False,
                  transformation = False, 
                  fold_strategy = 'stratifiedkfold', 
                  use_gpu = True)

# 회귀 모델 중 비교하려는 모델 선택
# 회귀 모델이므로 RMSE가 낮은 순서로 정렬
pycaret_regression_models = compare_models(n_select = 20, sort ='RMSE',
                                           include =['lr', 'lasso', 'ridge', 'en', 'lar',
                                                     'llar', 'omp', 'br', 'ard', 'par',
                                                     'ransac', 'kr',  'knn', 'dt', 'ada',
                                                     'xgboost', 'lightgbm', 'catboost', 'dummy'] )

# xgboost로 모델 선정.
# 모델 선정시 데이터의 이상치를 확인 안함.
# 도착시간은 거리에 영향이 큼
data[['distance','next_arrive_time']].describe()
# 거리의 최대가 7461, 평균이 490 으로 차이가 큼.
# 도착시간이 최대가 2996, 평균이 85로 차이가 매우 큼.

# 정류장 사이의 거리는 길수도 있음.
# 평균을 가지고 max 거리를 변환했을때 시간이 1200 가량 나와야 함.
# 2996 이므로 이상치라고 추측.
#거리와 도착시간에 이상치가 있는것 같음

 

거리별 도착시간을 태블로로 시각화.
 x축 거리, y 축 도착 시간
 거리 200~ 600 사이 도착시간이 이상한 수치가 많음.

 

# 도착시간을 기준으로 700 이상을 이상치로 보고 제외하면 대부분 걸러질 것 같음.
# 도착시간 700 이하를 선택
df_train = df_train[df_train['next_arrive_time'] <= 700]
from xgboost import XGBRegressor
from sklearn.metrics import mean_squared_error
from sklearn.metrics import r2_score

y = df_train['next_arrive_time']
df_train = df_train.drop(['next_arrive_time'],axis=1)
x_train , x_test, y_train, y_test = train_test_split(df_train,y,test_size = 0.2,random_state = 41)

xgb = XGBRegressor(n_jobs=-1,eval_metric = 'rmse',
                   random_state=43, tree_method='gpu_hist', gpu_id=0)
xgb.fit(x_train,y_train)
pre = xgb.predict(x_test)
print(mean_squared_error(y_test,pre,squared = False))
r2_score(y_test,pre)

#GridSearchCV를 사용하여 최적의 파라미터 찾기
from sklearn.model_selection import GridSearchCV

# GridSearchCV 파라미터 
params = {'max_depth':[4,5,6],
          'n_estimators':[600,700,800],
          'eta':[0.1,0.2,0.3]
         }

grid = GridSearchCV(
                    xgb,
                    params,
                    n_jobs=-1,
                    refit=True,
                    cv=5,
                    verbose=1,
                    return_train_score=False,
                    )
grid.fit(x_train, y_train)
grid.score(x_test, y_test)

#최적 파라미터 : max_depth=6, n_estimators=700, eta=0.1
grid.best_params_

pre2 = grid.predict(x_test)
print(mean_squared_error(y_test,pre2,squared = False))
print(r2_score(y_test,pre2))

# 테스트 전저리
test = pd.read_csv('./229611_버스 운행시간 예측 경진대회_data/test.csv')
test = test.drop(['id','vh_id','route_nm','now_station','next_station'],axis=1)
test['week'] = pd.to_datetime(test['date']).dt.weekday
test = test.drop(['date'],axis=1)
test['now_arrive_time'] = test['now_arrive_time'].str.rstrip('시')
test = test.astype({'now_arrive_time':'int'})
label_enc.fit([str(t) for t in position])
test['now_position'] = label_enc.transform([str(a) for a in zip(test['now_latitude'],test['now_longitude'])])
test['next_position'] = label_enc.transform([str(a) for a in zip(test['next_latitude'],test['next_longitude'])])
test['route_id'] = label_route.transform(test['route_id'])
test = test.drop(['now_latitude','next_latitude','now_longitude','next_longitude'],axis=1)

predict = grid.predict(test)
dt = pd.read_csv('./229611_버스 운행시간 예측 경진대회_data/submission_제출양식.csv')
dt['next_arrive_time'] = predict
dt.to_csv('./버스운행시간_예측.csv',index=False)