특정 기후에 따른 자전거 수요 예측¶
딥러닝 기반 자전거 대여량 예측 프로젝트
개요¶
학부 AI+X: 딥러닝 수업에서 진행한 프로젝트로, Kaggle Bike Sharing Demand 데이터를 활용하여 기후 조건에 따른 자전거 대여량을 예측하는 다층 퍼셉트론(MLP) 모델 개발.
- 수행 기간: 학부 과정 (AI+X: 딥러닝)
- 데이터 출처: Kaggle Bike Sharing Demand
- 분석 도구: Python, PyTorch
- 블로그: 특정 기후에 따른 자전거 수요 예측
- 성과: Kaggle 리더보드 상위 5% 이내
문제정의¶
비즈니스 맥락¶
마이크로 모빌리티는 친환경 교통 수단으로 주목받고 있으며, 자전거는 전통적인 마이크로 모빌리티 수단으로서 환경 보호와 도시 교통 관리에 중요한 역할을 한다.
- 탄소 배출 감소
- 교통 혼잡 완화
- 에너지 절약
해결 과제¶
자전거 대여 시스템의 효율성을 높이기 위해 정확한 수요 예측 필요.
목표¶
자전거 수요 예측 모델을 개발하여: 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)
배운 점¶
- EDA의 중요성: 시간대, 요일, 기후 패턴 분석으로 특징 선택 근거 확보
- 로그 변환: Target 분포 정규화로 모델 성능 향상
- MLP 구조 설계: 은닉층 깊이와 노드 수의 균형
- 과적합 모니터링: Train/Valid Loss 비교로 일반화 성능 확인
- 추가 특징 가능성: 지역 특성 등 추가 시 성능 향상 기대
기술 스택¶
| 분류 | 도구 |
|---|---|
| 언어 | 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)