본문 바로가기
데이터 분석 및 시각화

[ML] 랜덤 포레스트를 이용한 호텔 데이터 다루기

by 바다의 공간 2024. 7. 14.

1.hotel 데이터셋

-이번에는 호텔정보를 가지고 취소를 할 것 같은 고객인지 분류해보는 프로젝트를 진행해보려고 합니다.

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

hotel_df = pd.read_csv('/content/drive/MyDrive/컴퓨터 비전 시즌2/3. 데이터 분석/data/hotel.csv')
hotel_df.head()

hotel_df.info()

여기서 is_canceled는 종속변수로 둘거고 

필요하지 않은 데이터들은 나누면서 데이터 전처리를 해보도록 하겠습니다.

여기서 디타입을 보면 오브젝트가 보이는데 이제는 오브젝트를 보면 데이터 전처리 해야할것들이 있다고 

생각이 어느정도는 듭니다. 

즉 필요하지않은데이터들을 나누고 오브젝트디타입은 원핫인코딩으로 데이터전처리를 

끝내면 되겠죠?

 

인포는 보았으니 간단한 분석도 해보겠습니다.

hotel_df.describe()

를해보면 컬럼이 27개로 좀 많아서 다 보이진 않겠지만 

위 사진처럼 나옵니다.

 

각각 어떤 관계가 있는지 데이터들이 얼마나 신뢰성이 있는지 sns로 확인을 해보겠습니다.

일단 lead_time 남은기간 값을 보겠습니다.

sns.displot(hotel_df['lead_time'])

근데 700일전에 누가 예약을 할까 해서 이상치인가 해서 보도록 하겠습니다.

sns.boxplot(hotel_df['lead_time'])

sns.barplot(x=hotel_df['distribution_channel'], y=hotel_df['is_canceled'])

어디를 통해서 예약했는지를 확인본 것입니다.

심지가 긴것은 데이터가 좀 적다는것은 이제는 압니다!

hotel_df['distribution_channel'].value_counts()
distribution_channel
TA/TO        97870
Direct       14645
Corporate     6677
GDS            193
Undefined        5
Name: count, dtype: int64

여기서 보면 언디파인드가 확연하게 적다는게 느껴지죠.

 

sns.barplot(x=hotel_df['hotel'], y=hotel_df['is_canceled'])

가족끼리 가는 리조트호텔은 취소없이 가는편이고 

출장으로 가는 시티호텔은 취소가 조금 더 있는 편이라는것을 알 수 있습니다

sns.barplot(x=hotel_df['arrival_date_year'], y=hotel_df['is_canceled'])

 

plt.figure(figsize=(15, 5))
sns.barplot(x=hotel_df['arrival_date_month'], y=hotel_df['is_canceled'])

월도 제각각이고 한 눈에 보이지않아서 calendar모듈을 사용해서 보도록 하겠습니다.

사용방법은 print(calender.month_name[숫자])

숫자안에 넣어주면 월 이름이 나옵니다.

import calendar

print(calendar.month_name[1])
print(calendar.month_name[2])
print(calendar.month_name[3])

#January
#February
#March
month = []
for i in range(1,13):
  month.append(calendar.month_name[i])

month

 

['January',
 'February',
 'March',
 'April',
 'May',
 'June',
 'July',
 'August',
 'September',
 'October',
 'November',
 'December']

이렇게 반복문으로 돌리면 들어가게 됩니다.

plt.figure(figsize=(15,5))
sns.barplot(x=hotel_df['arrival_date_month'],y=hotel_df['is_canceled'], order=month)

위 순서대로 데이터 정렬해주었습니다.

데이터들을 살짝 보니 겨울, 여름에는 취소율이 조금 높는것 같으니 독립변수로 가져갈 수 있을 것 같습니다.

sns.barplot(x=hotel_df['is_repeated_guest'],y=hotel_df['is_canceled'])

 

sns.barplot(x=hotel_df['deposit_type'],y=hotel_df['is_canceled'])

hotel_df['deposit_type'].value_counts()
deposit_type
No Deposit    104641
Non Refund     14587
Refundable       162
Name: count, dtype: int64

여태 일부로 하나하나 상관관계를 확인했지만 데이터가 더 많아지면 하나하나 확인할 수 없으니

corr함수를 사용하면 쉽게 확인할 수 있습니다.

corr():열들간의 상관관계를 계산하는 함수(피어슨 상관계수)
# -1 ~ 1 까지의 범위를 가지고 0에 가까울수록 두 변수의 상관관계가 없거나 매우 약한것을 뜻합니다.
hotel_df.corr(numeric_only=True)

sns.heatmap(hotel_df.corr(numeric_only=True), cmap='coolwarm', vmax=1, vmin=-1, annot=True)

vmax = 최대값  vmin= 최소값

annot = 숫자를 체크해달라고 하는 의미

figsize= (15, 15)

0일때는 관련이 없다는 뜻입니다.

1일때는 관련이 많다 라는뜻이니 독립변수를 넣을 수 있겠쬬

hotel_df.isna().mean()

 

결측치를 보고있습니다.

hotel_df = hotel_df.dropna()

적은수이기때문에 날려버리도록 하겠습니다.

hotel_df.head()

아까 의심스러웠던 자료를 보려고 합니다

hotel_df[hotel_df['adults'] == 0]

어른들이 없는것이 몇개냐면 393개입니다.

어른이 없더라도 아이들만있더라도 인정하려고합니다.

캠프 이런걸수도있으니까요!

그래서 people이라는 파생변수를 만들어보도록 하겠습니다.

hotel_df['people'] = hotel_df['adults'] + hotel_df['children'] + hotel_df['babies']
hotel_df.head()

다 더했더니 (표가 너무 길어서 생략) 파생변수가 만들어졌고

그러면 people파생변수가 0인 사람들을 보니

hotel_df[hotel_df['people'] == 0]

170명이나 됩니다.

 

이 사람들을 제외해야겠죠?

hotel_df = hotel_df[hotel_df['people'] != 0]
hotel_df

0이 아닌사람만 넣었더고 170명이 날라갔습니다.

 

hotel_df['total_nights'] = hotel_df['stays_in_weekend_nights'] + hotel_df['stays_in_week_nights']
hotel_df.head()

파생변수를 또 만들었습니다.

만든 이유는 몇박 며칠을 묵는지 확인해보도록 하기 위함입니다.

 

토탈나이츠가 잘 만들어졌습니다.

여기서 월을 보면 달 이름으로 나와있는데 이것을 봄여름가을 겨울로 묶어주려고 하고

season이라는 파생변수를 만들려고 합니다.

#봄 여름 가을 겨울로 데이터그룹짓기
#season 파생변수
#arrival_date_month에 따라 아래와 같이 값을 저장
#12, 1, 2 : winter
#3, 4, 5 : spring
#6, 7 ,8 : summer
#9, 10 ,11 : Fall
season_dic = {'spring':[3,4,5], 'summer':[6,7,8], 'fall':[9,10,11], 'winter':[12,1,2]}

new_season_dic = {}

for i in season_dic:
    for j in season_dic[i]:
        new_season_dic[calendar.month_name[j]] = i

new_season_dic

결과가 잘 나온다는것을 확인할 수 있습니다.

hotel_df['season'] = hotel_df['arrival_date_month'].map(new_season_dic)
hotel_df.head()

hotel_df.info()

이 중에서 18번19번을 보면 내가 실제 룸타임, 배정된 룸타입이있습니다.

보통 다를때 취소률과 관련이 있을것같아서 확인해보겠습니다.

hotel_df['expected_room_type'] = (hotel_df['reserved_room_type'] == hotel_df['assigned_room_type']).astype(int)
hotel_df.head()

두개가 일치하는지 안하는지에 대한 값을 확인해보려고합니다.

hotel_df['cancel_rate'] = hotel_df['previous_cancellations'] / hotel_df['previous_bookings_not_canceled']
hotel_df.head()

취소율이 NaN이 왜 나오는지 보니

0으로 나눠버리면 값이 되지않습니다.

hotel_df[hotel_df['cancel_rate'].isna()]

NaN값을 보니 10만이나 있습니다.

아예 안왔다고 한 사람들은 파생변수로 신규생성합니다.

hotel_df['cancel_rate'] = hotel_df['cancel_rate'].fillna(-1)

이제 -1로 바뀐값을 볼 수 있습니다.

hotel_df.info()

이 값들을 보면 오브젝트가 있고 이 부분을 바꾸어줘야합니다. 원핫인코딩으로 하면 좋은데

너무 많아서 다른방법으로 진행해보겠습니다,

hotel_df['hotel'].dtype

다른건 다 int64, float64로 잘 나오는데 

hotel만

dtype('O')

 

로 디타입이 정의됩니다.

obj_list = []

for i in hotel_df.columns:
    if hotel_df[i].dtype == 'O':
        obj_list.append(i)

오브젝트만 거르는 과정입니다 리스트에 담는거죠

obj_list

for i in obj_list:
    print(i, hotel_df[i].nunique())

 

오브젝트에 있는 카테고리의 갯수를 확인할 수 있습니다.

hotel_df.drop(['country', 'arrival_date_month'], axis=1, inplace=True)

obj_list.remove('country')
obj_list.remove('arrival_date_month')

리스트와 df에서도 지워주기로 합니다.

두개 다 지워야 데이터를 사용할 수 있습니다.

 

그 다음에 이제 원핫인코딩을 하겠습니다.

hotel_df = pd.get_dummies(hotel_df, columns=obj_list)
hotel_df.head()

원핫인코딩이 잘 된것을 확인할 수 있습니다.

 

학습을 시켜주도록 하겠습니다.

X_train, X_test, y_train, y_test = train_test_split(hotel_df.drop('is_canceled', axis=1), hotel_df['is_canceled'], test_size=0.3, random_state=2024)

test_size는 데이터가 많아서 0.3정도로 주었습니다.

X_train.shape, y_train.shape
#((83109, 64), (83109,))

X_test.shape, y_test.shape
#((35619, 64), (35619,))

학습데이터와 테스트 데이터를 두개 나누는것까지 완료 했습니다.


2. 앙상블(ensemble)모델

  • 여러개의 머신러닝 모델을 이용해 최적의 답을 찾아내는 기법을 사용하는 모델
  • 보팅(Voting)
    • 서로 다른 알고리즘 model을 조합해서 사용
    • 모델에 대해 투표로 결과를 도출
  • 배깅(Bagging)
    • 같은 알고리즘 내에서 다른 sample 조합을 사용
    • 샘플 중복 생성을 통해 결과를 도축
  • 부스팅(Boosting)
    • 약한 학습기들을 순차적으로 학습시켜 강력한 학습기를 만듦
    • 이전 오차를 보완해가면서 가중치를 부여
    • 성능이 우수하지만 잘못된 레이블이나 아웃라이어에 대해 필요이상으로 민감
    • AdaBoost, Gradient Boosting, XGBoost, LightBGM
  • 스태킹(Stacking)
    • 다양한 개별 모델들을 조합하여 새로운 모델을 생성
    • 다양한 모델들을 학습시켜 예측 결과를 얻은 다음, 다양한 모델들의 예측 결과를 입력으로 새로운 메타 모델을 학습

 

 

3. 랜덤 포레스트(Random Forest)

  • 머신러닝에서 많이 사용되는 앙상블 기법 중 하나이며, 결정 나무를 기반으로 함
  • 학습을 통해 구성해 놓은 결정나무로부터 분류 결과를 취합해서 결론을 얻는 방식
  • 랜덤포레스트의 트리는 원본 데이터에서 무작위로 선택된 샘플을 기반으로 학습하게 됨.
  • 각 트리가 서로 다른 데이터셋으로 학습되어 다양한 트리가 생성되며 모델의 다양성이 증가함
  • 각각의 트리가 예측한 결과를 기반으로 다수결 또는 평균을 이용하여 최종 예측을 수행함
  • 분류와 회귀 문제에 모두 사용할 수 있으며, 특히 데이터가 많고 복잡한 경우에 매우 효과적인 모델입니다.
  • 성능은 꽤 우수한 편이나, 오버피팅이 되는 경우가 많습니다.(단점)
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(random_state=2024)
rf.fit(X_train, y_train)
pred1 = rf.predict(X_test)
pred1
#array([1, 0, 0, ..., 1, 1, 1])

preba1 = rf.predict_proba(X_test)
preba1
#array([[0.06      , 0.94      ],
#       [0.54833333, 0.45166667],
#       [0.98      , 0.02      ],
#       ...,
#       [0.49      , 0.51      ],
#       [0.06      , 0.94      ],
#       [0.        , 1.        ]])

# 첫번째 테스트 데이터에 대한 예측 결과
preba1[0]
#array([0.06, 0.94])

# 모든 테스트 데이터에 대한 호텔 예약을 취소할 확률만 출력
preba1[:, 1]
#array([0.94      , 0.45166667, 0.02      , ..., 0.51      , 0.94      ,
#       1.        ])

4. 머신러닝/딥러닝에서 모델의 성능을 평가하는데 시용하는 측정값

  • Accuracy: 올바른 예측의 비율
  • Precision : 모델에서 수행한 총 긍정 예측 수에 대한 참 긍정 예측의 비율
  • Recall : 실제 긍정 사례의 총 수에 대한 참 긍정 예측의 비율
  • F1 Score : 정밀도와 재현율의 조화평균이며, 정밀도와 재현율 간의 균형을 맞추기 위한 단일 메트릭으로 사용
  • AUC-ROC Curve: 참양성률(TPR)과 가양성률(FPR)간의 균형을 측정
      *예) TPR : 실제로 질병이 있을때, 검사 결과가 양성인 경우
      *예) FPR : 실제로 질병이 없을 때 검사 결과가 음성인 경우
      *ROC : 이진 분류의 성능을 보여주는 그래프 민감도(TPR)
      *AUC : ROC 커브와 직선 사이의 면적의 의미. 범위는 0.5~1이며 값의 클 수록 예측의 정확도 높음.

  • AUC = 1
    • 두 개의 곡선이 전혀 겹치지 않는 경우 모델은 가장 이상적인 분류 성능을 보임
    • 양성 클래스와 음성 클래스를 완벽하게 구별할 수 있음
  • AUC = 0.75
    • 설정한 threshold에 따라 오류값들을 최소화 또는 최대화할 수 있음
    • 해당 분류 모델이 양성 클래스와 음성 클래스를 구별할 수 있는 확률이 75%임을 의미
  • AUC = 0.5
    • 분류 모델의 성능이 최악인 상황
    • 해당 분류 모델은 양성 클래스와 음성 클래스를 구분할 수 있는 능력이 없음

이것을 계산할 수 있는 모듈을 사용할 수 있습니다.

from sklearn.metrics import accuracy_score, confusion_matrix, classification_report, roc_auc_score
accuracy_score(y_test, pred1)
#0.8643420646284287

confusion_matrix(y_test, pred1)
#array([[20709,  1659],
#       [ 3173, 10078]])

confusion_matrix을 사용하는 이유는 학습이 잘못됐을지 확인하기위해서 사용하게됩니다.

한쪽으로 쏠린 데이터가 아닌것을 확인할 수 있습니다. 

예) 암진단 확률이 5%라서 아니라고 해도 95% 맞춘다는 예시를 생각하기.

 

print(classification_report(y_test, pred1))

roc_auc_score(y_test, proba1[:, 1])
#0.9315576511541386

거의 1에 가까우니 어느정도 잘 맞추고 있다는것을 확인할 수 있습니다.

 

번외

하이퍼 파라미터 max_depth=30 적용해서 수정해보기

 

rf2 = RandomForestClassifier(max_depth=30, random_state=2024)
rf2.fit(X_train, y_train)
proba2 = rf2.predict_proba(X_test)
roc_auc_score(y_test, proba2[:,1])
#0.9319781899069026


#하이퍼 파라미터 수정 후
0.9319781899069026 - 0.9315576511541386
#0.0004205387527640436

# 하이퍼 파라미터 수정(max_depth=30, min_samples_split=5, n_estimators=120을 적용)
rf3 = RandomForestClassifier(max_depth=30, min_samples_split=5, n_estimators=120, random_state=2024)

rf3.fit(X_train, y_train)
proba3 = rf3.predict_proba(X_test)
roc_auc_score(y_test, proba3[:,1])
#0.931059217235636

#하이퍼 파라미터 추가 수정 후
0.931059217235636 -0.9319781899069026
#-0.0009189726712666157

하이퍼 파라미터 추가 수정후에는 성능이 더 안 좋아지는 경우도 볼 수 있습니다.

즉 하이퍼 파라미터는 조합을 해봐야 알 수 있습니다.

5. 하이퍼 파라미터 최적의 값 찾기

  • GridSearchCV: 원하는 모든 하이퍼 파라미터를 적용하여 최적의 값을 찾음
  • RandomizedSearchCV: 원하는 모든 하이퍼 파라미터를 지정하고 n_iter 값을 설정하여 해당 수 만큼 random하게 조합하여 최적의 값을 찾음
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV

params = { 'max_depth' : [30,40],
          'min_samples_split' : [2, 3],
          'n_estimators' : [100, 120]
}

 

학습시키기

rf4 = RandomForestClassifier(random_state=2024)
grid_df = GridSearchCV(rf4, params)
grid_df.fit(X_train, y_train)

* cv = 교차검증이라서 cv를 넣어도 됩니다. 단 검증시간은 더욱 길어지겠죠

grid_df.cv_results_

맨 마지막 rank_Test_score을 보면 각각 결과에 대한 rank를 확인시켜줍니다

즉 1이라고 쓰여있는 위치에 있는 값이 가장 베스트 파라미터인거죠.

 

최적의 파라미터 찾기

rand_df.best_params_

#{'n_estimators': 120, 'min_samples_split': 2, 'max_depth': 30}

 

랜덤으로 돌려보기(학습)

rf5 = RandomForestClassifier(random_state=2024)
rand_df = RandomizedSearchCV(rf5, params, n_iter=3, random_state=2024)
rand_df.fit(X_train, y_train)

랜덤으로 모든조합으로 3개를 뽑아달라는 의미입니다.

 

결과보기

rand_df.cv_results_

최적의 파라미터 찾기

rand_df.best_params_
#{'n_estimators': 120, 'min_samples_split': 2, 'max_depth': 30}

위에서 나온 랭크테스트스코어랑 같은 값을 확인할 수 있습니다.


위에 나온 값들을 그래프로 만들어보기

import matplotlib.pyplot as plt
from sklearn.metrics._plot.roc_curve import roc_curve
proba5 = rand_df.predict_proba(X_test)

fpr, tpr, thr = roc_curve(y_test, proba5[:, 1])
print(fpr, tpr, thr)

 

그래프를찍는 점들에 대한 값

[0.00000000e+00 4.47067239e-05 4.47067239e-05 ... 9.43445994e-01 9.44071888e-01 1.00000000e+00] [0. 0.36374613 0.36427439 ... 0.99901894 0.99901894 1. ] [2.00000000e+00 1.00000000e+00 9.99806202e-01 ... 5.14403292e-05 2.62881178e-05 0.00000000e+00]

 

plt.plot(fpr, tpr, label='ROC Curve')
plt.plot([0,1],[0,1])
plt.show()

 


6. 피처 중요도(Feature Importances)

  • 결정 나무에서 노드를 분기할 때 해당 피처가 클래스를 나누는데 얼마나 영향을 미쳤는지 표기하는 척도
  • 0에 가까우면 클래스를 구분하는데 해당 피처의 영향이 거의 없다는 것이며, 1에 가까우면 해당 피처가 클래스를 나누는데 영향을 무척이나 많이 주었다는 이야기가 됩니다.

# proba5 = rand_df.predict_proba(X_test)
roc_auc_score(y_test, proba5[:,1])
#0.9321130101499074

 

# {'n_estimators': 120, 'min_samples_split': 2, 'max_depth': 30}
rf6 = RandomForestClassifier(random_state=2024, max_depth=30,
                             min_samples_split=2, n_estimators=120)

rf6.fit(X_train, y_train)
proba6 = rf6.predict_proba(X_test)
roc_auc_score(y_test, proba6[:,1])

#0.9321130101499074

random, max_depth, min_sample_split, n_estimators을 넣은 값은 최적의 값이라서 넣은것입니다.

 

그 뒤로 rf6를 학습시키고 roc_auc_score의 값을 내보면 

0.92~~~가 나오게 됩니다.

proba6

rf6.feature_importances_

피처중요도를 뽑아보면 이렇게 나옵니다.맨 앞에껏을 따로 보면 e가 숫자로 나오게됩니다.

 

랜덤포레스트를 하면서 호텔 관련한 피처중요도입니다. 각각 하나가 첫번쨰 컬럼의 중요도로 볼 수 있습니다.

그래서 중요도를 한 번에 확인할 수 있습니다.

그런데 이렇게보면 어떤게중요한지 한 눈에 볼 수 없습니다.

그래서 판다스를 이용해서 시각화해보도록 하려고 합니다.

feature_imp = pd.DataFrame({
    'features': X_train.columns,
    'importances': rf6.feature_importances_
})
feature_imp

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

ascending=false로 해주어야 내림차순으로 됩니다.

오래기다리면 기다릴수록 취소학 확률이 적다라는것을 확인할 수 있습니다.

 

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

paltette는 당연히 옵션이고 굉장히 많기떄문에 더 예쁘게 시각화시킬 수 있습니다. 

Set1,2 등 다양하게 있지만 저는 Blues_r로 했습니다.