콘텐츠로 이동

특정 기후에 따른 자전거 수요 예측

딥러닝 기반 자전거 대여량 예측 프로젝트


개요

학부 AI+X: 딥러닝 수업에서 진행한 프로젝트로, Kaggle Bike Sharing Demand 데이터를 활용하여 기후 조건에 따른 자전거 대여량을 예측하는 다층 퍼셉트론(MLP) 모델 개발.


문제정의

비즈니스 맥락

마이크로 모빌리티는 친환경 교통 수단으로 주목받고 있으며, 자전거는 전통적인 마이크로 모빌리티 수단으로서 환경 보호와 도시 교통 관리에 중요한 역할을 한다.

  • 탄소 배출 감소
  • 교통 혼잡 완화
  • 에너지 절약

해결 과제

자전거 대여 시스템의 효율성을 높이기 위해 정확한 수요 예측 필요.

목표

자전거 수요 예측 모델을 개발하여: 1. 자전거가 필요한 위치에 적시 배치 2. 자원의 효율적 배분 3. 사용자 경험 개선


가설설정

가설 1: 시간대별 패턴 존재

  • 출퇴근 시간대에 대여량 증가

가설 2: 기후 조건 영향

  • 온도, 습도, 풍속 등이 대여량에 영향

가설 3: 요일별 패턴 차이

  • 평일과 주말의 대여 패턴이 다름

데이터

데이터셋 구조

컬럼 설명
datetime 대여 기록 일시
season 계절 (1: 겨울, 2: 봄, 3: 여름, 4: 가을)
holiday 공휴일 여부
workingday 평일 여부
weather 날씨 상황 (1: 맑음 ~ 4: 심한 비)
temp 실제 기온 (섭씨)
atemp 체감 기온 (섭씨)
humidity 습도 (%)
windspeed 풍속 (mph)
casual 비등록 사용자 대여 수
registered 등록 사용자 대여 수
count 총 대여 수 (target)

전처리

import pandas as pd

train = pd.read_csv('train.csv', parse_dates=["datetime"])
test = pd.read_csv('test.csv', parse_dates=["datetime"])

# datetime에서 특징 추출
train["year"] = train["datetime"].dt.year
train["month"] = train["datetime"].dt.month
train["day"] = train["datetime"].dt.day
train["hour"] = train["datetime"].dt.hour
train["minute"] = train["datetime"].dt.minute
train["second"] = train["datetime"].dt.second

# day_of_week 추가 (분석용)
train['day_of_week'] = train['datetime'].dt.dayofweek

분석/모델링

탐색적 데이터 분석 (EDA)

연/월/시간별 대여량

import seaborn as sns
import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 3, figsize=(18, 8))
ax1, ax2, ax3, ax4, ax5, ax6 = axes.flatten()

sns.barplot(data=train, x="year", y="count", ax=ax1)
sns.barplot(data=train, x="month", y="count", ax=ax2)
sns.barplot(data=train, x="day", y="count", ax=ax3)
sns.barplot(data=train, x="hour", y="count", ax=ax4)

ax1.set(ylabel='Count', title="Rental volume by year")
ax2.set(xlabel='month', title="Rental volume by month")
ax3.set(xlabel='day', title="Rental volume by day")
ax4.set(xlabel='hour', title="Rental volume by hour")

발견: - 2012년 대여량 > 2011년 (서비스 인기 상승) - 6월 대여량 최대, 1월 최소 - 출퇴근 시간(8시, 17-18시) 피크

요일별 시간대 패턴

plt.figure(figsize=(12, 6))
sns.pointplot(data=train, x='hour', y='count', hue='day_of_week')
plt.title('Hourly Rental Count by Day of the Week')
plt.xlabel('Hour of the Day')
plt.ylabel('Rental Count')

발견: - 평일: 출퇴근 시간대 두드러진 피크 - 주말: 하루 종일 균일 분포

온도/체감온도 vs 대여량

train['temp_rounded'] = train['temp'].round()
train['atemp_rounded'] = train['atemp'].round()

temp_atemp_mean = train.groupby(['temp_rounded', 'atemp_rounded'])['count'].mean().unstack()

plt.figure(figsize=(10, 8))
sns.heatmap(temp_atemp_mean, cmap="YlGnBu", annot=True, fmt=".1f")
plt.title('Heatmap: Temperature vs Rental Count')

발견: - 온도가 높을수록 대여량 증가 - 겨울보다 여름에 대여량 많음

풍속 vs 대여량

import numpy as np

train['windspeed_interval'] = pd.cut(
    train['windspeed'], 
    bins=np.arange(0, train['windspeed'].max() + 5, 10), 
    right=False
)

windspeed_count_mean = train.groupby('windspeed_interval')['count'].mean()
windspeed_count_mean.plot(kind='bar', color='skyblue')
plt.title('Average Rental Count by Windspeed Interval')

발견: - 적당한 풍속에서 대여량 높음 - 너무 강하거나 약한 바람은 대여량 감소

모델 구현

데이터 준비

import torch
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 특징 선택
cols = ['season', 'holiday', 'workingday', 'weather', 
        'temp', 'atemp', 'humidity', 'windspeed',
        'year', 'month', 'hour']

# 표준화
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(train[cols])
X_test_scaled = scaler.transform(test[cols])

# 데이터 분할
X_train, X_valid, y_train, y_valid = train_test_split(
    X_train_scaled, 
    np.log1p(train["count"]),  # 로그 변환
    test_size=0.2, 
    random_state=42
)

# 텐서 변환
X_train = torch.FloatTensor(X_train)
y_train = torch.FloatTensor(y_train.values.reshape(-1, 1))
X_valid = torch.FloatTensor(X_valid)
y_valid = torch.FloatTensor(y_valid.values.reshape(-1, 1))
X_test = torch.FloatTensor(X_test_scaled)

MLP 모델 정의

import torch.nn as nn

class MultivariateLinearRegression(nn.Module):
    def __init__(self, input_size, output_size):
        super().__init__()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(input_size, 256),
            nn.ReLU(),
            nn.Linear(256, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Linear(128, output_size),
        )

    def forward(self, x):
        return self.linear_relu_stack(x)

model = MultivariateLinearRegression(input_size=X_train.shape[1], output_size=1)

학습

import torch.optim as optim

loss_fn = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

num_epochs = 2000
for epoch in range(num_epochs):
    model.train()
    y_pred = model(X_train)

    loss = loss_fn(y_pred, y_train)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if (epoch + 1) % 100 == 0:
        print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')

평가

def rmsle(y_true, y_pred):
    return np.sqrt(np.mean((np.log1p(y_true) - np.log1p(y_pred)) ** 2))

with torch.no_grad():
    model.eval()
    y_pred_valid = model(X_valid)

y_true = y_valid.numpy().squeeze()
y_pred = y_pred_valid.numpy().squeeze()
rmsle_score = rmsle(np.expm1(y_true), np.expm1(y_pred))
print(f'RMSLE: {rmsle_score:.4f}')

결과

학습 과정

Epoch [100/2000], Loss: 1.2554
Epoch [200/2000], Loss: 0.5998
Epoch [300/2000], Loss: 0.2701
...
Epoch [1900/2000], Loss: 0.0402
Epoch [2000/2000], Loss: 0.0405

손실 값이 점진적으로 감소하며 모델 성능 향상 확인.

최종 성능

지표
Test Loss 0.1311
RMSLE 0.3621
Kaggle 순위 상위 5% 이내

예측 결과 제출

with torch.no_grad():
    outputs = model(X_test)
    y_predict = outputs.squeeze().numpy()

submission = pd.read_csv('sampleSubmission.csv')
submission["count"] = np.expm1(y_predict)
submission.to_csv(f"submit_{rmsle_score:.5f}.csv", index=False)

배운 점

  1. EDA의 중요성: 시간대, 요일, 기후 패턴 분석으로 특징 선택 근거 확보
  2. 로그 변환: Target 분포 정규화로 모델 성능 향상
  3. MLP 구조 설계: 은닉층 깊이와 노드 수의 균형
  4. 과적합 모니터링: Train/Valid Loss 비교로 일반화 성능 확인
  5. 추가 특징 가능성: 지역 특성 등 추가 시 성능 향상 기대

기술 스택

분류 도구
언어 Python
딥러닝 PyTorch
분석 pandas, numpy, scikit-learn
시각화 matplotlib, seaborn
환경 Jupyter Notebook

모델 아키텍처

Input (11 features)
Linear(11, 256) → ReLU
Linear(256, 256) → ReLU
Linear(256, 128) → ReLU
Linear(128, 1)
Output (count prediction)

관련 문서


참고자료