In [1]:
import numpy as np
import pandas as pd
from IPython.core.display import HTML
import matplotlib as mtp
from pylab import *

from datetime import datetime, timedelta
from StringIO import StringIO

In [2]:
parse = lambda x: datetime.strptime(x, '%Y%m%d %H%M')

In [73]:
april = pd.read_csv('data/round2-4.csv', names=['date', 'time', 'direction', 'index', 'source', 'destination', 'distance', 'speed'], parse_dates=[[0, 1]], date_parser=parse, header=None)
may = pd.read_csv('data/round2-5.csv', names=['date', 'time', 'direction', 'index', 'source', 'destination', 'distance', 'speed'], parse_dates=[[0, 1]], date_parser=parse, header=None)
june = pd.read_csv('data/round2-6.csv', names=['date', 'time', 'direction', 'index', 'source', 'destination', 'distance', 'speed'], parse_dates=[[0, 1]], date_parser=parse, header=None)

간단한 검증을 거쳐서 모델을 선택하기로 한다. 4, 5월을 모델 학습을 위한 데이터로 삼고 6월을 이를 검증하는 데이터로 삼는다.


In [149]:
train = pd.concat([april, may])
test = pd.concat([june])

train = train.sort(['direction', 'index', 'date_time'])
test = test.sort(['direction', 'index', 'date_time'])

Data analysis에서 평일과 주말을 분리하여 보기로 하였는데, 검증을 해보자. 일단 전체 (평일과 주말) 데이터를 사용하여 median을 구해보자.


In [150]:
whole_week = train.copy()
whole_week['time'] = whole_week.date_time.apply(lambda x: "{:02d}{:02d}".format(x.hour, x.minute))
group = whole_week.groupby(['direction', 'index', 'time'])
df = group.median()
median_model = df.reset_index()

In [52]:
print median_model
display(HTML(median_model[:10].to_html()))


<class 'pandas.core.frame.DataFrame'>
Int64Index: 72576 entries, 0 to 72575
Data columns (total 5 columns):
direction    72576  non-null values
index        72576  non-null values
time         72576  non-null values
distance     72576  non-null values
speed        72576  non-null values
dtypes: float64(1), int64(2), object(2)
direction index time distance speed
0 D 1 0000 1194 89.55
1 D 1 0005 1194 91.46
2 D 1 0010 1194 91.46
3 D 1 0015 1194 93.44
4 D 1 0020 1194 95.52
5 D 1 0025 1194 95.52
6 D 1 0030 1194 95.52
7 D 1 0035 1194 95.52
8 D 1 0040 1194 97.69
9 D 1 0045 1194 97.69

Test를 어떻게 하느냐도 문제가 되지만 일단 화요일에 대한 검증만 해보도록 하자. 2013년 6월의 화요일은 6/4, 6/11, 6/18, 6/25일이다.


In [176]:
def test_june(prediction, dow='tue'):
    week = ['sat', 'sun', 'mon', 'tue', 'wed', 'thu', 'fri']
    i = week.index(dow.lower()) 
    testing_days = range(i+1, 31, 7)

    result = []
    for k in testing_days:
        test_data = june.copy()
        test_data['day'] = test_data.date_time.apply(lambda x: int(x.day))
        test_data['time'] = test_data.date_time.apply(lambda x: "{:02d}{:02d}".format(x.hour, x.minute))
        test_data = test_data[test_data['day'] == k]
        assert(len(test_data) == 2*126*288)
        test_data = test_data.sort(['direction', 'index', 'time'])
        prediction = prediction.sort(['direction', 'index', 'time'])
        
        result.append(np.mean(np.abs(prediction.speed.values - test_data.speed.values)))
        
    return result

In [177]:
median_res = test_june(median_model, 'tue')
print np.mean(median_res)
print median_res


5.86801621748
[5.0517788249559956, 5.8417775848766516, 7.1962039792769419, 5.3823044808202543]

주중의 데이터만 활용한 모델을 만들어보자.


In [154]:
weekdays = train.copy()
weekdays['weekday'] = weekdays['date_time'].apply(lambda x: x.weekday())
weekdays = weekdays[weekdays['weekday'] < 5]
del weekdays['weekday']
weekdays['time'] = weekdays.date_time.apply(lambda x: "{:02d}{:02d}".format(x.hour, x.minute))
group = weekdays.groupby(['direction', 'index', 'time'])
df = group.median()
weekday_median_model = df.reset_index()

In [169]:
weekday_median_res = test_june(weekday_median_model, 'tue')
print np.mean(weekday_median_res)
print weekday_median_res


5.85363191689
[4.9729312720459697, 5.8052837026015318, 7.2075788139330363, 5.4287338789683348]

일단 화요일에 대해서는 주중 데이터만 활용하는 것이 더 좋다. 다만 데이터를 자세히 살펴보면 2:2의 결과이며 하루는 값이 좀 튀는 경향이 있다. 일단 데이터 포인트가 4개 밖에 안 되기 때문에 통계적으로 안정적인 결과라 할 수는 없다.


In [170]:
for i in range(7):
    days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
    print days[i]
    res1 = test_june(median_model, days[i])
    res2 = test_june(weekday_median_model, days[i])
    print np.mean(res1), np.mean(res2)


Sun
5.21588125551 5.53193945657
Mon
5.04345772018 5.06026437803
Tue
5.86801621748 5.85363191689
Wed
5.47827828759 5.43223538084
Thu
5.25132619599 5.26708498677
Fri
5.1378397473 5.17411468667
Sat
5.33010967813 5.5877789903

주말의 결과는 전체 데이터를 사용한 것이 월등하다. 평일에는 조금 갈리는 경향을 보인다. 월, 목, 금에는 전체 데이터를 사용한 편이 좋고 화, 수에는 평일 데이터만 활용하는 것이 좋다.

조금 더 나은 분석을 위해 일종의 cross validation을 해보자.


In [414]:
whole_data = pd.concat([april, may, june])

In [415]:
whole_data['date'] = whole_data.date_time.apply(lambda x: x.date())
whole_data['time'] = whole_data.date_time.apply(lambda x: "{:02d}{:02d}".format(x.hour, x.minute))
whole_data['weekday'] = whole_data['date_time'].apply(lambda x: x.weekday())
whole_data = whole_data.sort(['date', 'direction', 'index', 'time'])

In [442]:
import random

def crossvalidate():
    # 91 days
    days = range(91)
    random.shuffle(days)

    STRIDE = 2 * 126 * 288
    
    test_range = days[:10]
    train_range = days[10:]
    
    train_data = []
    for x in train_range:
        train_data.append(whole_data[x * STRIDE:(x + 1) * STRIDE])
        
    test_data = []
    for x in test_range:
        test_data.append(whole_data[x * STRIDE:(x + 1) * STRIDE])
        
    cv_train = pd.concat(train_data)
    cv_test = pd.concat(test_data)

    return cv_train, cv_test

Crossvalidate 함수는 말 그대로 k-fold cross validation을 하기 위한 함수이다. 데이터를 10:81으로 나누도록 하드코딩 되어 있으니 9-fold CV라 할 수 있겠다. 이런식으로 사용하기 위해 몇 가지 가정이 뒷받침되어야 하지만 이는 된다고 가정하고 분석을 해보자.


In [443]:
def test_cv(prediction, test_data, dow='tue'):
    week = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
    i = week.index(dow.lower())
    
    test_data = test_data[test_data['weekday'] == i]
    
    STRIDE = 2 * 126 * 288
    stepsize = len(test_data) / STRIDE
    
    result = []
    for k in range(stepsize):
        temp_data = test_data[k * STRIDE:(k + 1) * STRIDE]
        temp_data = temp_data.sort(['direction', 'index', 'time'])
        prediction = prediction.sort(['direction', 'index', 'time'])
        
        result.append(np.mean(np.abs(prediction.speed.values - temp_data.speed.values)))
        
    return result

In [444]:
for x in range(10):
    train, test = crossvalidate()

    group = train.groupby(['direction', 'index', 'time'])
    df = group.median()
    cv_median_model = df.reset_index()
    
    weekdays = train[train['weekday'] < 5]
    group = weekdays.groupby(['direction', 'index', 'time'])
    df = group.median()
    cv_weekday_median_model = df.reset_index()
    
    cv_median_model_res = test_cv(cv_median_model, test, 'tue')
    cv_weekday_median_model_res = test_cv(cv_weekday_median_model, test, 'tue')
    
    print np.mean(cv_median_model_res), np.mean(cv_weekday_median_model_res)
    print np.mean(cv_median_model_res) - np.mean(cv_weekday_median_model_res)


5.7308320014 5.64618053259
0.0846514688052
5.01917396936 4.91857790454
0.100596064815
nan nan
nan
4.9950061315 4.91634886188
0.0786572696209
nan nan
nan
5.83914413856 5.84164896109
-0.00250482253078
4.8562787974 4.75965394896
0.0966248484348
5.46667066248 5.37352775022
0.0931429122575
nan nan
nan
5.68120648699 5.59890454145
0.0823019455468

화요일 기준으로는 평일 데이터를 사용한 것이 거의 항상 우월하다.


In [445]:
for y in ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']:
    print y
    result = []
    for x in range(10):
        train, test = crossvalidate()
    
        group = train.groupby(['direction', 'index', 'time'])
        df = group.median()
        cv_median_model = df.reset_index()
        
        weekdays = train[train['weekday'] < 5]
        group = weekdays.groupby(['direction', 'index', 'time'])
        df = group.median()
        cv_weekday_median_model = df.reset_index()
        
        cv_median_model_res = test_cv(cv_median_model, test, y)
        cv_weekday_median_model_res = test_cv(cv_weekday_median_model, test, y)
        
        result.append(np.mean(cv_median_model_res) - np.mean(cv_weekday_median_model_res))
    print result


Mon
[nan, -0.037003692680729117, 0.017944223985988828, -0.053671461640165674, -0.018101438492005073, 0.0025154320987752499, 0.014292603615570165, -0.030870673500841939, -0.018506485155723951, -0.047370067239898894]
Tue
[0.071944168871228342, -0.016789572310431211, nan, nan, 0.086851300705427903, 0.013157862103265749, 0.025897197420687412, 0.10321139219580466, 0.019178516313936633, 0.083144979056429591]
Wed
[0.057922660383598057, 0.035372046773933796, nan, nan, nan, nan, nan, 0.029073178461231919, nan, -0.0030846974205269717]
Thu
[nan, 0.087597415123473965, 0.053271604938280426, 0.060279775683460102, 0.012752700617274293, 0.077648258377418955, 0.1007045028659963, 0.073843763778683957, 0.095754657187016257, 0.012541290049999709]
Fri
[nan, 0.014818259479712381, -0.0028717482363536107, 0.0054254850088319984, 0.033432815255778792, -0.084203317901193842, -0.2465878527336649, 0.039594631834271254, 0.037854111552076297, 0.10066647376547699]
Sat
[nan, nan, -0.27002645502641798, -0.29335358796295807, -0.17482170414464626, -0.30133694334208982, nan, -0.28994846781294115, -0.25838031856255217, -0.29131627535269988]
Sun
[nan, -0.29084077380947715, nan, -0.36893862985006987, -0.36974316578470656, -0.40952746086856973, -0.33498546351409075, -0.41576030643733652, nan, -0.2834543099646929]

전체 요일에 대해 비슷하게 cross validation 분석을 해보면 전체 데이터를 사용하는 편이 주말은 물론이고 월요일에도 더 우월한 전략이다. 화요일과 수요일, 목요일 그리고 금요일에는 평일 데이터만 사용하는 편이 더 우월하다. 이는 따로 cross validation을 하지 않은 결과와 비슷해 보인다. 비록 10회 밖에 반복을 하지 않아 통계적인 안정성을 말할 수는 없지만, 적어도 화요일에는 평일 데이터만 사용하는 편이 더 나은 것으로 보인다.

요일별로 양상이 다른 것을 고려한다면 목표 예측 요일별 데이터를 뽑아내는 모집단도 더 세밀하게 나눠보는 것을 고려할 수 있을 것이다.

최종 loss function이 MAE (mean absolute error) 이므로 평균값 (mean) 보다는 중앙값 (median) 을 사용하는 편이 더 성능이 좋을 것이라고 생각할 수 있다. 이를 검증하는 것은 쉬운 문제이다.


In [446]:
whole_week = pd.concat([april, may])
whole_week['time'] = whole_week.date_time.apply(lambda x: "{:02d}{:02d}".format(x.hour, x.minute))
group = whole_week.groupby(['direction', 'index', 'time'])
df = group.mean()
mean_model = df.reset_index()

mean_res = test_june(mean_model, 'tue')
print np.mean(mean_res)
print mean_res


6.04020650371
[5.5189650961704766, 6.0169240497694316, 6.9158139121997166, 5.7091229566925117]

Median을 사용한 모델의 에러는 5.86801621748 였는데, mean을 사용한 모델은 6.04020650371 로 크게 차이난다.


In [447]:
weekdays = pd.concat([april, may])
weekdays['weekday'] = weekdays['date_time'].apply(lambda x: x.weekday())
weekdays = weekdays[weekdays['weekday'] < 5]
del weekdays['weekday']
weekdays['time'] = weekdays.date_time.apply(lambda x: "{:02d}{:02d}".format(x.hour, x.minute))
group = weekdays.groupby(['direction', 'index', 'time'])
df = group.mean()
weekday_mean_model = df.reset_index()

weekday_mean_res = test_june(weekday_mean_model, 'tue')
print np.mean(weekday_mean_res)
print weekday_mean_res


5.9240332326
[5.2767339187732576, 5.8565075109004887, 6.9358504066235662, 5.6270410940868256]

주중 데이터만 사용한 경우에도 마찬가지의 결과를 얻을 수 있다. Median 기반은 5.85363191689 인데 mean 기반은 5.9240332326 이다.


In [ ]: