수달이네 기술 블로그

15. 서울 따릉이 수요 예측2 본문

AI공부/머신러닝

15. 서울 따릉이 수요 예측2

슬픈 수달이 2026. 1. 9. 21:32

컬럼간 상관관계 분석(다중공선성 확인)

위의 전처리로 인해 object컬럼은 사라졌다. 이제 컬럼간의 상관관계를 확인하거나, 학습해보자

correlation_matrix = bike_df.corr()
bike_df

판다스에서 제공하는 correlation_matrix를 살펴보자면]

Rented Bike CountHourTemperatureHumidityWind speedVisibilityDew point temperature

  Rented Bike Count Hour Temperature Humidity
Rented Bike Count 1.000000 4.102573e-01 0.538558 -0.199780
Hour 0.410257 1.000000e+00 0.124114 -0.241644
Temperature 0.538558 1.241145e-01 1.000000 0.159371
Humidity -0.199780 -2.416438e-01 0.159371 1.000000

컬럼간의 상관관계를 소수점으로 나타내 보여준다(피어슨 상관계수)

  • 1.0은 관계가 있는 것이 아닌 아예 같은 컬럼
  • 0.5 이상: 양의 상관관계가 존재
  • -0.5이하: 음의 상관관계가 존재
  • 0에 가까움 : 상관관계 없음

우리가 알아야할 타깃값인 Rented Bike Count와의 상관관계를 확인하여 정렬하면

target_corr = correlation_matrix['Rented Bike Count'].sort_values(ascending=False)
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

위 상관관계를 보면 온도가 매우 큰 상관관계를 가짐을 알 수 있다.

  • 종속변수와의 상관관계는 좋음.

그러나 높은 상관관계를 가진 컬럼의 경우 제거해야할 경우도 있는데, 다중공선성 문제가 발생할 수 있기 때문이다.

다중 공선성(Multicollinearity)

회귀 분석에서 독립 변수들(설명 변수)간에 강한 상관관관계(종속 변수 X!!) 가 존재하는 현상

  • 각 독립변수가 종속변수에 미치는 개별적 영향을 추정하기 힘들어짐
  • 추정치가 불안정해져 작은 데이터 변화에도 크게 영향을 줌
  • 위 두이유로 예측 성능 저하, 해석의 신뢰성이 감소할 수 있음.

따라서 각각의 컬럼들의 상관계수를 파악해서 제거해주어야함.

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

위의 히트맵에서 보면 온도와 이슬점의 관계. 온도와 겨울과의 상관관계가 매우 높음을 알 수 있다.

  • 그런데 제거해야할 기준은 뭘까?

VIF(Variance Inflation Factor)

한 독립변수가 다른 독립변수들에 의해 얼마나 잘 설명되는지 나타내는 값.

  • 해당 변수가 사실상 다른 변수들의 조합은 아닌가를 수치로 보여줌.

기준

  • VIF < 5: 문제 없음
  • 5 ≤ VIF < 10: 주의해야함
  • 10 ≤ VIF: 다중공선성 의심(조치 고려해야 함)
from statsmodels.stats.outliers_influence import variance_inflation_factor
x = bike_df.drop('Rented Bike Count', axis=1)# 타겟 변수 제거
x = x.select_dtypes(include=['number']) # VIF는 수치형 데이터에 대해서만 계산 가능
x = x.astype(float) # VIF 계산을 위해 float 타입으로 변환
vif_df = pd.DataFrame({ 
    'Feature': x.columns,
    'VIF': [variance_inflation_factor(x.values, i) for i in range(x.shape[1])]# 각 피처에 대한 VIF 계산
}).sort_values(by='VIF', ascending=False) # VIF 수치가 높은 순서대로 정렬
vif_df
/ Feature VIF
9 year 407.025112
1 Temperature 188.666573
2 Humidity 187.533688
5 Dew point temperature 126.954261
4 Visibility 10.788995
10 month 5.108772
3 Wind speed 4.890096
0 Hour 4.458880
11 day 4.379818
6 Solar Radiation 2.904971
8 Snowfall 1.155412
7 Rainfall 1.103386

각 컬럼에 대해 VIF를 계산한다. (statsmodels의 variance_inflation_factor를 사용

  • year: date에서 파생변수를 만들었음(동일한 애가 있음)
  • 온도, 이슬점, 습도, 시야 등은 VIF가 높아 조치를 취해주어야한다.
bike_df = bike_df.drop(['Dew point temperature', 'Hour', 'Humidity'], ['year'], axis=1)
bike_df.head()
  • 위처럼 제거해 주고 난 뒤 VIF를 확인하면
    / Feature VIF
    2 Visibility 5.247050
    6 month 4.211105
    1 Wind speed 3.807370
    7 day 3.292291
    0 Temperature 2.758183
    3 Solar Radiation 1.914448
    5 Snowfall 1.116980
    4 Rainfall 1.046290
  • 위와 같이 10이상의 VIF는 없는것을 확인할 수 있다.

학습

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.2, random_state=42)
  • 해당 컬럼들을 이용해서 모델을 나눠준다. (train과 test를 나눠줌)

결정트리(Decision Tree)

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

  • 각 노드는 특정 특성의 조건에 따라 가지로 분기되어 최종으로 리프노드에 도달해 예측 결과를 도출
  • 분류, 회귀 문제에 사용되어 데이터의 패턴을 직관적으로 시각화할 수 있다.
  • 너무 깊어지면 과적합 문제가 발생할 수 있어 가지치기나 최대깊이 설정등으로 제어한다.
  • 알고리즘의 트리구조임

원리

  1. 전체 데이터셋을 하나의 노드로 시작
  2. 최적의 특성, 분할기준을 찾아 첫번째 분할을 실행한다.
    1. 분류문제: Gini 불순도(Gini Impurity), 엔트로피(entropy)사용
    2. 회귀문제: MSE(평균 제곱 오차), MAE(절대 평균 오차)사용
  3. 각 하위 노드에 대해 위 과정 반봅
  4. 이 과정을 통해 트리는 여러 깊이로 성장
  5. 노드가 더 나눌 수 없거나 특정 조건(max_depth, min_samples, split)을 만족하면 종료(리프노드)
  6. 분류: 가장 많은 클래스가 있는 클래스가 예측값 회귀: 평균값이 예측값

지니 불순도

한 노드의 데이터 순수도를 측정,

  • 한 노드에 있는 샘플이 동일한 클래스에 속할 확률이 높으면 불순도 저하.(노드의 엔트로피)
  • 엔트로피는 데이터가 균등하게 분포되면 최대값.

DecisionTreeRegressor

주어진 데이터를 반복적으로 분할하여 예측을 수행

  • 각 분할은 예측하는데 가장 적합한 값을 찾기 위해 이루어진다.
  • 이를 통해 데이터를 점차 더 작은 부분으로 나누고, 각 부분에서 평균값을 예측값으로 사용한다.

목표

  • 두 그룹의 MSE가 능한 한 낮도록 만드는것. 즉, MSE가 최소화 되도록 분할점을 찾는다.

출처:https://ryuzyproject.tistory.com/72

from sklearn.tree import DecisionTreeRegressor
dt_reg = DecisionTreeRegressor(random_state=42)
dt_reg.fit(X_train, y_train)
y_pred = dt_reg.predict(X_test)
sns.scatterplot(x=y_test, y=y_pred, alpha=0.3)

예측하고 결과값을 scatterplot으로 찍으면 위와 같이 출력되는데.

이게 과연 잘 맞춘걸까?

  • 대충보면 어느정도 값이 선형을 그리므로 잘 맞은것 같아보이긴한다.

원래 분류문제는 accuracy가 있어서 눈에띄게 확인이 가능하다. 하지만 회귀 문제의 경우 지표가 MSE나 RMSE를 확인한다.

from sklearn.metrics import root_mean_squared_error
root_mean_squared_error(y_test, y_pred)

#393.74930649113867

결정트리는 위와 같은 RMSE를 나타낸다.

비교를 위해 LinearRegression을 사용하면

from sklearn.linear_model import LinearRegression
lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)
pred2 = lr_reg.fit(X_train, y_train).predict(X_test)
sns.scatterplot(x=y_test, y=pred2, alpha=0.3)

root_mean_squared_error(y_test, pred2)
# 433.9205216180732

위와 같은 결과가 나온다.

decision트리가 rmse값이 더 작으므로 오차가 적다 즉, 더 잘 맞췄다.

하이퍼파라미터 튜닝

dt_reg = DecisionTreeRegressor(max_depth=50, min_samples_leaf=30, random_state=42)

decision트리의 하이퍼파라미터는 위와 같이 구성된다.

max_depth : 최대 깊이, 나누어져 내려가는 트리의 최대 깊이를 나타낸다.

min_samples_leaf: 최소 리프 크기, 트리를 나눌때 리프가 정해진 크기가 나오면 정지.

하이퍼 파라미터를 계속 수정하면서 오차를 고쳐나가보면

dt_reg = DecisionTreeRegressor(max_depth=10, min_samples_leaf=13, random_state=42)
dt_reg.fit(X_train, y_train)   
y_pred = dt_reg.predict(X_test)
sns.scatterplot(x=y_test, y=y_pred, alpha=0.3)
root_mean_squared_error(y_test, y_pred)

# 319.2356988155453

위와 같이 성능이 좋아진 것을 확인할 수 있다.

decision트리가 어떤 결과를 거쳤는지 확인하기

from sklearn.tree import plot_tree
plt.figure(figsize=(24,12))
plot_tree(dt_reg, fontsize=10, feature_names=X_train.columns, max_depth=5)
plt.show()

sklearn에서 제공하는 plot_tree메서드를 이용하면, decision트리의 과정을 볼 수 있다.

랜덤포레스트(강한 학습기)

from sklearn.ensemble import RandomForestRegressor
rf_reg = RandomForestRegressor(random_state=42)
rf_reg.fit(X_train, y_train)
y_pred = rf_reg.predict(X_test)
sns.scatterplot(x=y_test, y=y_pred, alpha=0.3)
root_mean_squared_error(y_test, y_pred)

# 283.90004524006025

  • 랜덤포레스트는 결정트리(약한학습기)를 여러개 만들어서 사용하는 것. 즉 성능이 좋을 수 밖에 없다.
  • 그러나 항상 좋은 모델을 쓰는 것보다 적절한 모델을 쓰는것이 더 중요하다.
    • 알뜰하게 최적의 값을 뽑는것.

피처 중요도(랜덤포레스트의 특성)

rf_reg.feature_importances_
# array([0.33120064, 0.03555859, 0.04532136, 0.18913849, 0.06638298,
#        0.00104838, 0.03108891, 0.03576222, 0.00366982, 0.00175583,
#        0.01641125, 0.00208089, 0.08394038, 0.00671751, 0.00638788,
#        0.14353488])

위는 각 피처의 중요도를 나타낸 값이다. (컬럼 순서대로)

이걸 데이터프레임으로 표시하면

feature_imp = pd.DataFrame({
    'Feature': X_train.columns,
    'Importance': rf_reg.feature_importances_
})

feature_imp.sort_values(by='Importance', ascending=False)

 

/ Feature Importance
0 Temperature 0.331201
3 Solar Radiation 0.189138
15 TimeOfDay_Evening 0.143535
12 Functioning Day_Yes 0.083940
4 Rainfall 0.066383
2 Visibility 0.045321
7 day 0.035762
1 Wind speed 0.035559
6 month 0.031089
10 Seasons_Winter 0.016411
13 TimeOfDay_Morning 0.006718
14 TimeOfDay_Afternoon 0.006388
8 Seasons_Spring 0.003670
11 Holiday_No Holiday 0.002081
9 Seasons_Summer 0.001756
5 Snowfall 0.001048

위와 같이 컬럼 중요도를 뽑아낼 수 있다.