그룹핑 :: 시계열 분석 - mindscale
Skip to content

그룹핑

자전거 대여소 데이터

이번에는 미국 시애틀에 위치한 Fremont 브리지의 자전거 대여소 데이터를 이용해 시계열 데이터를 Pandas에서 어떻게 다루는지 살펴보겠습니다.

이 데이터는 자전거 대여 횟수와 대여소의 위치 등의 정보를 담고 있습니다. 이 데이터는 'bicycle.csv'라는 파일에 저장되어 있습니다. 여기서 'bicycle'은 자전거를, 'csv'는 데이터 포맷을 나타냅니다. CSV는 Comma Separated Values의 약자로, 간단히 말해 쉼표로 구분된 텍스트 데이터를 말합니다. 이러한 포맷은 특정 프로그램에 종속되지 않게 데이터를 저장하고 싶을 때 자주 사용됩니다.

pandas에서는 read_csv 함수를 이용해 CSV 포맷의 데이터를 읽을 수 있습니다. read_csv 함수의 옵션 중 index_col은 데이터를 불러올 때 index로 설정할 칼럼을 지정할 때 사용하며, parse_dates=True 옵션은 날짜 형식으로 들어올 수 있는 데이터를 자동으로 날짜로 변환합니다.

data = pd.read_csv(
    'bicycle.csv', 
    index_col='Date',  # Date 컬럼을 인덱스로 지정
    parse_dates=True)  # 자동으로 날짜 처리

데이터를 보면, 11월 1일 0시부터 시작하여 1시간 간격으로 데이터가 기록되어 있습니다. 그리고 Fremont 브리지의 자전거 대여량 합계와 동쪽 대여소, 서쪽 대여소의 각각의 대여량이 기록되어 있습니다. 마지막으로 이 두 대여소의 대여량을 합한 전체 대여량이 있습니다.

data.head()
Fremont Bridge Total Fremont Bridge East Sidewalk Fremont Bridge West Sidewalk
Date
2019-11-01 00:00:00 12.0 7.0 5.0
2019-11-01 01:00:00 7.0 0.0 7.0
2019-11-01 02:00:00 1.0 0.0 1.0
2019-11-01 03:00:00 6.0 6.0 0.0
2019-11-01 04:00:00 6.0 5.0 1.0

이제 이 컬럼 이름이 너무 길기 때문에, 우리가 분석을 좀 간단하게 하기 위해서 이 컬럼 이름을 좀 짧게, 'Total', 'East', 'West', 이렇게 변환을 해주도록 하겠습니다.

data.columns = ['Total', 'East', 'West']

그 다음에 전체 데이터를 플롯으로 그려보겠습니다.

data.plot()
<Axes: xlabel='Date'>

주별 합계

따라서, 리샘플 함수를 이용하여 특정 구간의 통계를 계산할 수 있습니다. 'W' 옵션을 이용하면 주 단위로 데이터를 잘라서, 'sum'을 이용하여 합계를 구할 수 있습니다. 주 단위로 합계를 구하는 이유는 일주일 동안 총 몇 대가 나가는지 확인하기 위함입니다.

만약 한 달에 얼마나 많은 대가 나가는지 알고 싶다면, 'M' 옵션을 사용합니다. 하루에 얼마나 나가는지 확인하려면, 'D' 옵션을 사용하면 됩니다.

자전거 사용량은 주기적으로 올라갔다 내려갔다하며, 특히 여름에 증가하는 경향을 보입니다. 반면에 겨울에는 날씨가 추워 자전거 이용이 줄어듭니다.

우리나라의 경우 여름에는 고온으로 인해 오히려 자전거를 타기 어려울 수 있지만, 미국 시애틀은 날씨가 선선하므로 여름에도 자전거 이용이 가능해 보입니다.

이 밖에도 두 지역인 이스트와 웨스트의 자전거 사용량을 비교하였는데, 초기에는 두 지역의 사용량이 비슷하다가 점차 서쪽에서는 사용량이 늘어나고, 동쪽에서는 줄어든 것을 확인할 수 있습니다.

그러나 2020년이 지나면서 자전거 이용량이 감소했는데, 이는 코로나19로 인해 사람들이 외부 활동을 줄이면서 생긴 현상으로 보입니다.

data.resample('W').sum().plot()
<Axes: xlabel='Date'>

일별 합계의 이동 평균

이는 주 단위 통계이며, 'D'의 사용으로 일 단위 통계를 구할 수 있습니다.

일 단위 통계를 구하고 모든 결과를 그래프로 표현하면 너무 복잡할 수 있기 때문에, 전체 일 단위 통계의 합계를 구하고, 이를 이용해 30일 이동 평균을 계산해 그래프로 표현했습니다.

따라서 리샘플에선 시간 단위를 일 단위로 변경하고 롤링을 적용했습니다. 원래 그래프의 형태는 불규칙한 오르내림을 보였지만, 30일 단위의 이동 평균 구간을 적용함으로써 그래프는 매끄럽게 변화하는 모습을 보였습니다.

이는 월별 합계 계산과 30일 이동 평균을 구한 결과로, 그래프가 상대적으로 매끄러운 변화를 보여줍니다. 특히 이동 평균인 만큼, 날짜를 늘릴수록 그래프는 더욱 매끄러워집니다.

가령, 특정 날에 가격이 급상승하면 이는 전체 데이터에 크게 영향을 미칠 수 있습니다. 하지만 30일 중 하루만 점프하는 경우, 이동 평균은 30분의 1만 증가합니다.

daily = data.resample('D').sum()
daily.rolling(30, center=True).mean().plot()
<Axes: xlabel='Date'>

또한 이동 평균 구간을 50일로 늘리면, 하루 동안의 점프는 전체 이동 평균에 50분의 1만큼만 영향을 미칩니다. 따라서 이동 평균 구간을 더 길게 설정하면 그래프의 변동 폭이 줄어들어 전반적으로 더욱 매끄러워집니다.

win_type="gaussian"은 중심이 되는 날짜에 대해 더 높은 가중치를 주고 그 외의 날짜에는 점차 가중치를 줄여 분포를 매끄럽게 조정하는 것이 가능하게 됩니다.

이러한 가중치는 가우시안 또는 정규분포 함수의 형태에 따라 결정되며, 이 함수의 표준편차(std)에 따라서 그 형태가 달라집니다. 표준편차가 작으면 정규분포의 중심이 뾰족하게 되어 중심에 가까운 날짜에 대해 큰 영향을 주게 되고, 표준편차가 크면 완만하게 되어 중심에서 멀리 떨어진 날짜에 대해서도 그 영향력이 유지됩니다. 따라서 표준편차를 조절해서 그래프의 매끄러움을 결정할 수 있습니다.

만약에 표준편차를 더 작게 가져가게 되면 분포의 뾰족함이 증가하여 멀리 떨어진 날짜의 데이터가 거의 영향을 주지 않게 됩니다. 따라서, 분포의 뾰족함, 즉 이동 평균의 매끄러움을 조절하는 것은 중요한 고려사항입니다.

roll = daily.rolling(50, center=True, win_type='gaussian')
roll.sum(std=10).plot()
<Axes: xlabel='Date'>

그룹핑

다음으로, 우리가 사용할 것은 groupby 기능입니다. 이 기능은 일정한 기준에 따라 데이터를 그룹화합니다.

지금까지 우리가 해왔던 것은 연속된 시간을 구간으로 나누고, 이 구간에 따라 데이터를 그룹화하는 것이었습니다. 그러나 이 대신, 만약 매일 같은 시간대를 하나의 그룹으로 묶고 싶다면 groupby를 사용하면 됩니다.

예를 들어, 매일 1시, 2시의 데이터를 각각 하나의 그룹으로 모을 수 있습니다. groupby 기능과 '리샘플(resample)'의 차이점은, '리샘플'은 연속된 간격으로 데이터를 분할하는 방식이고, groupby는 지정된 날짜나 시간의 데이터를 다른 날짜나 시간에서 추출하는 방식입니다.

이해를 돕기 위해, groupby의 활용 예를 들어보겠습니다. 데이터 인덱스에는 날짜와 시간이 포함되어 있습니다. 만약 시간만을 이용하여 데이터를 분석하고 싶다면, 시간 정보만 추출할 수 있습니다. 이렇게 추출된 시간대별로 평균을 내면, 시간대별 평균 데이터를 얻을 수 있습니다.

예를 들어, 0시부터 23시 59분까지 데이터가 1시간 단위로 존재한다면, 이를 1시간 단위로 그룹화하여 평균을 낼 수 있습니다. 이제 날짜는 무시하고, 어떤 시간대에 자전거 대여가 가장 많이 이루어지는지 확인할 수 있습니다.

이런 방식으로 분석한 데이터를 보면, 대체로 아침과 저녁에 자전거 대여가 많이 이루어지는 것을 확인할 수 있습니다. 이는 출퇴근 시간에 자전거를 이용하는 사람들이 많기 때문일 것입니다. 아침에는 동쪽 지역에서, 저녁에는 서쪽 지역에서 대여가 많이 이루어지는 경향이 있습니다.

즉, 어떤 시간대에, 어떤 지역에서 자전거 대여가 활발하게 이루어지는지를 groupby를 이용해 분석할 수 있습니다.

by_time = data.groupby(data.index.time).mean()
by_time.plot()
<Axes: xlabel='time'>

다음으로, 가로축 눈금이 정확하게 맞지 않는다면 좀 더 깔끔한 보기를 위해, 플롯 작성 시 xicks라는 옵션을 사용하세요. 여기서 'x'는 가로축을 의미하고, 'tick'는 눈금을 의미합니다.

그리고 numpy를 사용하여 hourly_ticks라는 변수를 생성합니다. numpy는 파이썬에서 다양한 수치 계산을 수행할 때 사용됩니다.

이 경우, 4시간 간격의 눈금을 설정하려고 합니다. 그러려면, 초 단위의 시간을 4시간 (3600초 x 4 = 14400초) 표현으로 변환하면 됩니다.

numpy.arange(6)을 사용하면, 0부터 5까지의 6개 숫자를 생성할 수 있습니다. 이 값을 14400에 곱하면, 4시간 간격을 초 단위로 변환한 눈금 값을 얻을 수 있습니다.

이를 통해 4시간 간격의 눈금을 표시할 수 있습니다. 2시간이나 6시간 등 원하는 시간 간격으로 변경하려면, 곱셈 값을 그에 맞게 조정하면 됩니다.

확인해보면, 8시와 5시에 자전거 이용이 주로 발생한다는 것을 알 수 있습니다.

import numpy as np
hourly_ticks = 4 * 60 * 60 * np.arange(6)
by_time.plot(xticks=hourly_ticks, style=[':', '--', '-'])
<Axes: xlabel='time'>

요일별 평균

동일하게 요일별 평균을 확인할 수 있습니다. 여기서 dayofweek은 요일을 의미합니다. 요일별로 정렬하여 표시하면 됩니다.

다음으로, 인덱스를 지정할 때 지정한 순서대로 그래프가 출력됩니다.

결과를 확인해보면, 월요일부터 금요일까지는 자전거 이용량이 많지만, 토요일과 일요일은 그렇지 않습니다. 이를 통해 사람들이 자전거를 주로 출퇴근에 이용한다는 사실을 알 수 있습니다.

by_weekday = data.groupby(data.index.dayofweek).mean()
by_weekday.index = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
by_weekday.plot()
<Axes: >

주중과 주말

다음으로 주중과 주말을 나눠 분석할 수 있습니다. 하지만 현재 데이터에는 주중, 주말을 구분하는 컬럼이 없고, 단지 날짜 정보만 있습니다. 이를 위해 날짜 데이터를 바탕으로 주중, 주말을 구분해야 합니다.

데이터의 인덱스에 weekday 함수를 적용하면, 요일 정보를 얻을 수 있습니다. 이때, 요일은 숫자로 표현되며 월요일부터 금요일까지가 0부터 4, 토요일과 일요일은 5부터 6까지입니다. 숫자가 5 이하면 주중을, 5 혹은 6이면 주말을 나타냅니다. 이 기준에 따라 np.where 함수를 사용하여 기존 데이터에 조건을 일괄적으로 적용하면, 새로운 데이터를 생성할 수 있습니다.

이렇게 만든 주중과 주말을 구분하는 변수와 시간 변수를 기준으로 그룹핑을 하고, 각 그룹의 평균을 계산합니다. 이 결과로 각 시간대별 주중과 주말의 평균이 계산됩니다.

그리고 나서 주중 데이터만 추려 시각화하고, 같은 방법으로 주말 데이터를 추려 따로 시각화합니다. 이 작업은 별도로 수행해야 합니다.

weekend = np.where(data.index.weekday < 5, 'Weekday', 'Weekend')
by_time = data.groupby([weekend, data.index.time]).mean()

두 그래프를 한 장의 그림으로 그리기

subplots이라는 함수를 이용하면 두 그래프를 하나로 합칠 수 있습니다. 이를 위해 matplotlib라는 라이브러리에서 pyplot이라는 모듈을 사용합니다. pyplot의 별칭으로 plt를 사용하며, 이 plt에서 제공하는 subplots 함수를 사용합니다.

subplots 함수를 사용하여 그림의 틀을 먼저 생성합니다. 이때, 그리고자하는 차트의 수를 파라미터로 지정합니다. 예를 들어, 줄 하나에 두 개의 그래프를 그리려면 1,2를 파라미터로 지정하며, 두 줄에 그래프 하나씩을 그리려면 2,1을 파라미터로 지정하면 됩니다.

그림의 크기를 지정하기 위해서는 figsize를 사용합니다. 이때, 크기는 가로, 세로 인치로 지정하며, 원하는 크기가 나올 때까지 실험적으로 진행하며 정할 수 있습니다.

그림을 그릴 때에는 plot 함수의 ax 옵션에 0번 또는 1번을 지정하여 그릴 위치를 결정합니다. 이를 통해 하나의 그림 안에 두 개의 그래프를 그릴 수 있습니다. 그림의 제목은 title 함수를 통해 설정합니다.

이 세 줄을 한 셀에서 실행하면 하나의 그림에 두 개의 서브 플롯이 그려집니다.

import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, figsize=(14, 5))
by_time.loc['Weekday'].plot(ax=ax[0], title='Weekdays', xticks=hourly_ticks)
by_time.loc['Weekend'].plot(ax=ax[1], title='Weekends', xticks=hourly_ticks)
<Axes: title={'center': 'Weekends'}, xlabel='time'>

주중과 주말, 그리고 시간대별로 분석한 자전거 이용량 결과를 보면, 주중에는 아침과 저녁의 출퇴근 시간에 주로 자전거를 이용하며, 그때 약 400대에서 500대의 자전거가 이용됩니다. 반면 주말에는 점심 후, 즉 12시에서 4시 사이에 주로 이용되며 이때는 약 140대의 자전거가 이용됩니다. 따라서 주말의 자전거 이용량이 주중보다 상당히 적음을 알 수 있습니다.