Programming Study/Python & AI

서울 자전거 공유 수요 데이터셋을 활용한 대여 수요 예측 실습

Solbi Lee 2025. 7. 26. 15:10

서울시의 공공 자전거 대여 서비스인 "따릉이"의 대여 수요를 예측하는 문제에 사용되는 데이터셋.

특정 시간대와 날씨, 요일, 공휴일 여부, 기온, 습도 등 다양한 데이터를 활용해 자전거 대여 수요를 예측 

 

데이터셋 컬럼 

Date : 연월일
Rented Bike count - 매 시간마다 대여한 자전거 수
Hour - 하루 중 시간
Temperature - 온도
Humidity - 습도 %
Windspeed - 풍속 m/s
Visibility - 가시거리 m
Dew point temperature - 이슬점 온도
Solar radiation - 태양 복사 MJ/m2
Rainfall - 강우량 mm
Snowfall - 적설량 cm
Seasons - 겨울, 봄, 여름, 가을
Holiday - 휴일/휴일 없음
Functional Day - 운영되지 않았던 날, 정상적으로 운영된 날

 

데이터 전처리 및 탐색적 데이터 분석 (EDA)

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

bike_df = pd.read_csv('/SeoulBikeData.csv', encoding='CP949')
bike_df
※ CP949

Microsoft Windows의 한국어 문자 인코딩입니다.
EUC-KR을 확장한 형태로, 더 많은 한국어 문자(한자, 확장 문자 등)를 지원합니다.
주로 Windows 환경에서 저장된 한글 파일에서 사용됩니다.
bike_df.info()

===================

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8760 entries, 0 to 8759
Data columns (total 14 columns):
 #   Column                    Non-Null Count  Dtype  
---  ------                    --------------  -----  
 0   Date                      8760 non-null   object 
 1   Rented Bike Count         8760 non-null   int64  
 2   Hour                      8760 non-null   int64  
 3   Temperature(캜)            8760 non-null   float64
 4   Humidity(%)               8760 non-null   int64  
 5   Wind speed (m/s)          8760 non-null   float64
 6   Visibility (10m)          8760 non-null   int64  
 7   Dew point temperature(캜)  8760 non-null   float64
 8   Solar Radiation (MJ/m2)   8760 non-null   float64
 9   Rainfall(mm)              8760 non-null   float64
 10  Snowfall (cm)             8760 non-null   float64
 11  Seasons                   8760 non-null   object 
 12  Holiday                   8760 non-null   object 
 13  Functioning Day           8760 non-null   object 
dtypes: float64(6), int64(4), object(4)
memory usage: 958.3+ KB

 

bike_df.describe()

bike_df.columns

=======================

Index(['Date', 'Rented Bike Count', 'Hour', 'Temperature(캜)', 'Humidity(%)',
       'Wind speed (m/s)', 'Visibility (10m)', 'Dew point temperature(캜)',
       'Solar Radiation (MJ/m2)', 'Rainfall(mm)', 'Snowfall (cm)', 'Seasons',
       'Holiday', 'Functioning Day'],
      dtype='object')
      
# 컬럼명 재정의 
bike_df.columns = ['Date', 'Rented Bike Count', 'Hour', 'Temperature', 'Humidity',
       'Wind speed', 'Visibility', 'Dew point temperature',
       'Solar Radiation', 'Rainfall', 'Snowfall', 'Seasons',
       'Holiday', 'Functioning Day']

 

# scatterplot으로 상관관계 시각화

# 온도와 자전거 대여 수 
sns.scatterplot(x='Temperature', y='Rented Bike Count', data=bike_df, alpha=0.3, color='darkviolet')

# 풍속과 자전거 대여 수 
sns.scatterplot(x='Wind speed', y='Rented Bike Count', data=bike_df, alpha=0.3, color='gray')

# 가시거리와 자전거 대여 수 (숫자가 클수록 멀리서도 보임)
sns.scatterplot(x='Visibility', y='Rented Bike Count', data=bike_df, alpha=0.3, color='green')

# 날짜 Object type으로 되어있어서 datetime으로 바꾸고, format 맞춰주기

bike_df['Date'] = pd.to_datetime(bike_df['Date'], format='%d/%m/%Y')
bike_df.info()
# 파생 변수 생성 

bike_df['year'] = bike_df['Date'].dt.year
bike_df['month'] = bike_df['Date'].dt.month
bike_df['day'] = bike_df['Date'].dt.day
bike_df.head()

 

# 시계열 대여 수 시각화

plt.figure(figsize=(14, 4))
sns.lineplot(x='Date', y='Rented Bike Count', data=bike_df)
plt.xticks(rotation=45)
plt.show()

 # Hour(0~23) 값을 네 개의 구간으로 나눠서, 각 시간대에 대응하는 이름(‘Dawn’, ‘Morning’, ‘Afternoon’, ‘Evening’)을 새 컬럼 TimeOfDay 에 지정

bike_df['TimeOfDay'] = pd.cut(bike_df['Hour'],
                            bins=[0, 5, 11, 17, 23],
                            labels=['Dawn', 'Morning', 'Afternoon', 'Evening'],
                            include_lowest=True) # 처음 시작 값은 포함시켜라

* pd.cut()

pd.cut()은 숫자 데이터를 구간(bins)으로 나눠 범주형 데이터로 변환하는 데 사용된다. 

주로 연속형 데이터를 특정 범주로 분류할 때 사용된다. 

bins : 숫자 데이터를 나눌 경계값(구간)

sns.barplot(x='Functioning Day', y='Rented Bike Count', data=bike_df)

* barplot의 bar 역할 

- 신뢰구간(CI) : 평균값이 속할 것으로 예상되는 값의 범위 (막대 위의 검은 선)

- 바 그래프에서 신뢰구간 : 검은색 심지로 나타낸다

- 신뢰구간이 좁다 : 평균값에 대한 확신이 높다 

- 신뢰구간이 넓다 : 평균값에 대한 확신이 낮고 데이터가 흩어져 있다. 

 

=> 위 그래프는 평균 대여량 추정치가 꽤 정확하다 라는 뜻으로 해석할 수 있다. 

즉 운행일 데이터가 충분히 많고, 분산도 크지 않아서, 평균이 이 정도 범위 안에 잇을 것이다. 라고 확신할 수 있을 것이다! 

라고 확신할 수 있는 폭이 좁게 계산된 것 

 

bike_df['Functioning Day'].value_counts()

bike_df = pd.get_dummies(bike_df, columns=bike_df.select_dtypes(exclude=['number']).columns.tolist(), drop_first=True)
# drop_first=True : 원핫인코딩된 첫 번째 컬럼 수를 drop해서 피쳐 수를 하나 줄이겠다. 이렇게 해도 모델이 이해하는 데 전혀 문제 되지 않음
bike_df.head() #첫번째인 Dawn이 사라짐

 

# 모든 컬럼 간 상관관계 분석
correlation_matrix = bike_df.corr()

# 목표 변수와의 상관관계만 확인
target_corr = correlation_matrix['Rented Bike Count'].sort_values(ascending=False) # 내림차순
print(target_corr)

==========================


Rented Bike Count        1.000000
Temperature              0.538558
Hour                     0.410257
Dew point temperature    0.379788
TimeOfDay_Evening        0.322978
Seasons_Summer           0.296549
Solar Radiation          0.261837
year                     0.215162
Functioning Day_Yes      0.203943
Visibility               0.199280
month                    0.133514
TimeOfDay_Afternoon      0.128639
Wind speed               0.121108
Holiday_No Holiday       0.072338
Seasons_Spring           0.022888
day                      0.022291
TimeOfDay_Morning       -0.081115
Rainfall                -0.123074
Snowfall                -0.141804
Humidity                -0.199780
Seasons_Winter          -0.424925
Name: Rented Bike Count, dtype: float64

 

* corr() 함수 : 데이터프레임의 숫자형 열 간의 상관관계를 계산하는 데 사용된다. 

 

# 히트맵으로 확인해보기 

plt.figure(figsize=(16, 12))
sns.heatmap(correlation_matrix, annot=True, fmt='.2f', cmap='Purples')
plt.title('Feature Correlation Heatmap')
plt.show()

본격 예측해보기 

# 훈련/테스트 데이터 분할 

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(bike_df.drop('Rented Bike Count', axis=1), bike_df['Rented Bike Count'], test_size=0.3, random_state=2025)

X_train.shape, y_train.shape # ((6132, 19), (6132,))
X_test.shape, y_test.shape # ((2628, 19), (2628,))

사용 모델 : 결정 트리 (Decision Tree)

데이터를 기반으로 의사결정을 수행하는 트리 구조의 예측 모델 

루트노드에서 시작해 각 노드는 특정 특성의 조건에 따라 가지로 분기됨

최종적으로 리프 노드에 도달해 예측 결과를 도출한다. 

주로 분류와 회귀문제에 사용되며, 데이터의 패턴을 직관적으로 시각화할 수 있어 해석이 용이하다. 

트리가 너무 깊어지면 과적합 문제가 발생할 수 있으므로 가지치기나 최대 깊이 설정 등으로 제어해야한다. 

동작 원리 

전체 데이터셋을 하나의 노드로 시작 

최적의 특성과 분할 기준을 찾아 첫 번째 분할을 수행 

분류 : 

- 지니 불순도 : 노드 안에 있는 샘플들이 얼마나 섞여 있는지 

- 엔트로피 : 노드 안의 정보(무질서도)가 얼마나 혼란스러운지 

회귀 :

- 평균제곱오차 : MSE

- 절대평균오차 : MAE

각 하위 노드에 대해 위 단계를 반복 

이 과정을 통해 트리는 여러 깊이로 성장한다. 모든 노드가 더 이상 나눌 수 없거나 특정 조건을 만족할 때까지 반복된다. 

더 이상 분할이 불가능할 때 리프 노드가 생성된다.

분류 : 가장 많은 클래스가 있는 클래스를 예측값으로 사용 

회귀 : 평균값을 예측값으로 사용 

 

지니 불순도 

한 노드에 있는 데이터의 순수도를 측정하는 지표. 

한 노드에 있는 샘플들이 동일한 클래스에 속할 확률이 높을수록 지니 불순도는 낮아진다. 즉 노드가 얼마나 섞여 있는지를 나타낸다. 

엔트로피 

정보의 불확실성을 측정한다. 엔트로피가 높을수록 해당 노드에 있는 데이터는 더 섞여 있으며 예측하기 어렵다. 엔트로피는 데이터가 균등하게 분포될 때 최대값을 가짐 

 

둘 다 노드가 얼마나 섞였나 측정하는 도구 

실제 나무 만드는 과정에서 쓰기 편한 걸 고르면 된다. 

- 섞여있을수록 값이 커진다 -> 더 분할할 필요가 있다. 

- 한 쪽으로 치우칠수록 값이 작아진다 -> 이미 순수하니 멈춰도 된다. 

from sklearn.tree import DecisionTreeRegressor

dtr = DecisionTreeRegressor(random_state=2025)
dtr.fit(X_train, y_train)
pred1 = dtr.predict(X_test)

sns.scatterplot(x=y_test, y=pred1)

from sklearn.metrics import root_mean_squared_error

root_mean_squared_error(y_test, pred1)

# 313.7826116872321
from sklearn.linear_model import LinearRegression

lr = LinearRegression()
lr.fit(X_train, y_train)
pred2 = lr.predict(X_test)

sns.scatterplot(x=y_test, y=pred2)

root_mean_squared_error(y_test, pred2)

# 420.7921510115953
# 하이퍼 파라미터 적용
dtr = DecisionTreeRegressor(random_state=2025, max_depth=50, min_samples_leaf=30)
dtr.fit(X_train, y_train)
pred3 = dtr.predict(X_test)

root_mean_squared_error(y_test, pred3) # 293.44393572577195
from sklearn.tree import plot_tree

plt.figure(figsize=(24, 12))
plot_tree(dtr, max_depth=5, fontsize=10, feature_names=X_train.columns)
plt.show()

from sklearn.ensemble import RandomForestRegressor

rf = RandomForestRegressor(random_state=2025)
rf.fit(X_train, y_train)
pred4 = rf.predict(X_test)
root_mean_squared_error(y_test, pred4) #234.43573060707698
print("설정된 트리 개수 (n_estimators):", rf.n_estimators)
print("실제로 학습된 트리 수 (len(estimators_)):", len(rf.estimators_))

=====================

설정된 트리 개수 (n_estimators): 100
실제로 학습된 트리 수 (len(estimators_)): 100
rf.feature_importances_

array([0.2711775 , 0.32433855, 0.08286032, 0.01611289, 0.01597226,
       0.09191418, 0.0396052 , 0.00075289, 0.00041153, 0.02197947,
       0.01706343, 0.00169124, 0.00168268, 0.01549781, 0.0024369 ,
       0.08600034, 0.00221501, 0.00480898, 0.00347884])
feature_imp = pd.DataFrame({
    'features': X_train.columns,
    'importances': rf.feature_importances_
})
feature_imp

top10 = feature_imp.sort_values('importances', ascending=False).head(10)
top10

plt.figure(figsize=(5, 10))
sns.barplot(x='importances', y='features', data=top10)