로지스틱 회귀와 랜덤포레스트 모델을 활용해서 호텔 예약 취소 예측해보기

1. 실습 주제

이 실습은 호텔 예약 데이터로부터 예약 취소 여부를 예측하는 이진 분류 문제를 다룬다.

예측 대상은 다음과 같다.

is_canceled

즉, 각 예약 정보가 주어졌을 때 이 예약이 실제로 취소될지 유지될지를 분류하는 것이 목표이다.

입력 변수로는 다음과 같은 정보가 활용된다.

  • 호텔 유형
  • 예약 후 체크인까지 걸린 시간
  • 도착 연도, 월, 주차 정보
  • 주중 및 주말 숙박일 수
  • 성인, 어린이, 아기 수
  • 식사 유형
  • 국가
  • 유통 채널
  • 이전 예약 이력
  • 객실 예약 및 배정 정보
  • 예약 보증금 유형
  • 고객 유형
  • 대기 기간
  • 주차 공간 요청 수
  • 특별 요청 수
  • 예약 상태 등

전체 흐름은 다음과 같다.

데이터 로드
↓
데이터 구조 확인
↓
lead_time 이상치 탐색 및 제거
↓
범주형 변수와 취소율 관계 확인
↓
결측치 처리
↓
파생 변수 생성
↓
불필요 컬럼 제거
↓
원-핫 인코딩
↓
학습 / 테스트 데이터 분리
↓
로지스틱 회귀 학습
↓
스케일링 적용
↓
정확도, 정밀도, 재현율, F1 점수 확인
↓
예측 확률과 임계값 조정
↓
랜덤 포레스트 분류기 비교
↓
ROC AUC 및 ROC Curve 확인
↓
K-Fold 교차 검증

2. 데이터셋 컬럼 정리

이 데이터셋의 대표 컬럼은 다음과 같다.

컬럼명 의미
hotel 호텔 유형
is_canceled 예약 취소 여부
lead_time 예약 후 체크인까지의 기간
arrival_date_year 도착 연도
arrival_date_month 도착 월
arrival_date_week_number 도착 주차
arrival_date_day_of_month 도착 일
stays_in_weekend_nights 주말 숙박 수
stays_in_week_nights 주중 숙박 수
adults 성인 수
children 어린이 수
babies 유아 수
meal 식사 유형
country 국가
market_segment 시장 세그먼트
distribution_channel 예약 유통 채널
is_repeated_guest 재방문 고객 여부
previous_cancellations 이전 취소 횟수
previous_bookings_not_canceled 이전 비취소 예약 횟수
reserved_room_type 예약 객실 유형
assigned_room_type 실제 배정 객실 유형
booking_changes 예약 변경 횟수
deposit_type 보증금 유형
agent 예약 대행사 ID
company 회사 ID
days_in_waiting_list 대기 리스트 대기일
customer_type 고객 유형
adr 평균 일일 요금
required_car_parking_spaces 필요 주차 공간 수
total_of_special_requests 특별 요청 수
reservation_status_date 예약 상태 변경 날짜

이 실습의 핵심 타깃은 예약 취소 여부이므로,
이후 모든 전처리와 모델링은 is_canceled 예측에 맞춰 진행된다.


3. 라이브러리 불러오기

실습에서는 NumPy, Pandas, Seaborn, Matplotlib를 먼저 불러온다.

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

각 라이브러리의 역할은 다음과 같다.

  • NumPy: 수치 연산
  • Pandas: 데이터 처리와 전처리
  • Seaborn: 분포, 박스플롯, 범주형 평균 시각화
  • Matplotlib: 그래프 크기와 ROC Curve 시각화

4. pd.read_csv() 와 DataFrame.info()

개요

pd.read_csv()는 CSV 파일을 읽어 DataFrame으로 변환하는 함수이다.
info()는 불러온 데이터의 구조를 한 번에 점검하는 메서드이다.

실습에서는 데이터를 로드한 직후 info()를 함께 사용해 다음을 확인한다.

  • 전체 데이터 개수
  • 컬럼별 자료형
  • 결측치 존재 여부
  • 전처리 필요한 컬럼

실습 코드

hotel_df = pd.read_csv('/content/drive/MyDrive/.../hotel_bookings.csv')
hotel_df.info()

주요 역할

  • 원본 데이터 로드
  • 수치형 / 범주형 컬럼 구분
  • 결측치 존재 여부 확인
  • 이후 전처리 우선순위 결정

반환값

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 119390 entries, 0 to 119389
Data columns (total 32 columns):
 #   Column                          Non-Null Count   Dtype  
---  ------                          --------------   -----  
 0   hotel                           119390 non-null  object 
 1   is_canceled                     119390 non-null  int64  
 2   lead_time                       119390 non-null  int64  
 3   arrival_date_year               119390 non-null  int64  
 4   arrival_date_month              119390 non-null  object 
 5   arrival_date_week_number        119390 non-null  int64  
 6   arrival_date_day_of_month       119390 non-null  int64  
 7   stays_in_weekend_nights         119390 non-null  int64  
 8   stays_in_week_nights            119390 non-null  int64  
 9   adults                          119390 non-null  int64  
 10  children                        119386 non-null  float64
 11  babies                          119390 non-null  int64  
 12  meal                            119390 non-null  object 
 13  country                         118902 non-null  object 
 14  market_segment                  119390 non-null  object 
 15  distribution_channel            119390 non-null  object 
 16  is_repeated_guest               119390 non-null  int64  
 17  previous_cancellations          119390 non-null  int64  
 18  previous_bookings_not_canceled  119390 non-null  int64  
 19  reserved_room_type              119390 non-null  object 
 20  assigned_room_type              119390 non-null  object 
 21  booking_changes                 119390 non-null  int64  
 22  deposit_type                    119390 non-null  object 
 23  agent                           103050 non-null  float64
 24  company                         6797 non-null    float64
 25  days_in_waiting_list            119390 non-null  int64  
 26  customer_type                   119390 non-null  object 
 27  adr                             119390 non-null  float64
 28  required_car_parking_spaces     119390 non-null  int64  
 29  total_of_special_requests       119390 non-null  int64  
 30  reservation_status              119390 non-null  object 
 31  reservation_status_date         119390 non-null  object 
dtypes: float64(4), int64(16), object(12)
memory usage: 29.1+ MB

ai가 추천하는 심화 예제

hotel_df = pd.read_csv("hotel_bookings.csv")

print(hotel_df.shape)
print(hotel_df.dtypes)

# 결과
# 데이터 크기와 각 컬럼 자료형 출력

5. DataFrame.describe()

개요

describe()는 수치형 컬럼의 기초 통계량을 요약하는 메서드이다.

다음 항목을 한 번에 확인할 수 있다.

  • count
  • mean
  • std
  • min
  • 25%
  • 50%
  • 75%
  • max

이 실습에서는 수치형 변수의 범위와 이상치 가능성을 빠르게 확인하는 데 사용된다.


실습 코드

hotel_df.describe()

주요 역할

  • 수치형 변수의 분포 개요 파악
  • 극단값 존재 여부 확인
  • 이상치 탐색의 출발점 제공

반환값

is_canceled	lead_time	arrival_date_year	arrival_date_week_number	arrival_date_day_of_month	stays_in_weekend_nights	stays_in_week_nights	adults	children	babies	is_repeated_guest	previous_cancellations	previous_bookings_not_canceled	booking_changes	agent	company	days_in_waiting_list	adr	required_car_parking_spaces	total_of_special_requests
count	119390.000000	119390.000000	119390.000000	119390.000000	119390.000000	119390.000000	119390.000000	119390.000000	119386.000000	119390.000000	119390.000000	119390.000000	119390.000000	119390.000000	103050.000000	6797.000000	119390.000000	119390.000000	119390.000000	119390.000000
mean	0.370416	104.011416	2016.156554	27.165173	15.798241	0.927599	2.500302	1.856403	0.103890	0.007949	0.031912	0.087118	0.137097	0.221124	86.693382	189.266735	2.321149	101.831122	0.062518	0.571363
std	0.482918	106.863097	0.707476	13.605138	8.780829	0.998613	1.908286	0.579261	0.398561	0.097436	0.175767	0.844336	1.497437	0.652306	110.774548	131.655015	17.594721	50.535790	0.245291	0.792798
min	0.000000	0.000000	2015.000000	1.000000	1.000000	0.000000	0.000000	0.000000	0.000000	0.000000	0.000000	0.000000	0.000000	0.000000	1.000000	6.000000	0.000000	-6.380000	0.000000	0.000000
25%	0.000000	18.000000	2016.000000	16.000000	8.000000	0.000000	1.000000	2.000000	0.000000	0.000000	0.000000	0.000000	0.000000	0.000000	9.000000	62.000000	0.000000	69.290000	0.000000	0.000000
50%	0.000000	69.000000	2016.000000	28.000000	16.000000	1.000000	2.000000	2.000000	0.000000	0.000000	0.000000	0.000000	0.000000	0.000000	14.000000	179.000000	0.000000	94.575000	0.000000	0.000000
75%	1.000000	160.000000	2017.000000	38.000000	23.000000	2.000000	3.000000	2.000000	0.000000	0.000000	0.000000	0.000000	0.000000	0.000000	229.000000	270.000000	0.000000	126.000000	0.000000	1.000000
max	1.000000	737.000000	2017.000000	53.000000	31.000000	19.000000	50.000000	55.000000	10.000000	10.000000	1.000000	26.000000	72.000000	21.000000	535.000000	543.000000	391.000000	5400.000000	8.000000	5.000000

ai가 추천하는 심화 예제

print(hotel_df["lead_time"].describe())

# 결과
# lead_time 컬럼의 통계량 출력

6. sns.displot()

개요

displot()은 특정 변수의 분포를 시각화하는 함수이다.
실습에서는 lead_time 분포를 먼저 확인한다.

lead_time은 예약 시점과 실제 체크인까지의 간격을 뜻하므로,
이 값이 지나치게 큰 예약이 존재하는지 확인하는 것이 중요하다.


실습 코드

sns.displot(hotel_df['lead_time'])

주요 역할

  • lead_time 분포 확인
  • 오른쪽 꼬리가 긴 분포인지 확인
  • 이상치 탐색 필요성 판단

반환값


ai가 추천하는 심화 예제

sns.displot(hotel_df["adr"])

# 결과
# 평균 일일 요금의 분포 시각화

7. sns.boxplot(), quantile(), IQR 기반 이상치 제거

개요

박스플롯은 중앙값, 사분위수, 이상치를 시각적으로 확인하는 그래프이다.
실습에서는 lead_time에 대해 박스플롯을 먼저 확인한 후, 사분위수와 IQR을 계산해서 이상치를 제거한다.

이상치 기준은 다음과 같다.

[
IQR = Q3 - Q1
]

[
Lower\ Bound = Q1 - 1.5 \times IQR
]

[
Upper\ Bound = Q3 + 1.5 \times IQR
]

이 범위를 벗어나는 값은 일반적으로 이상치로 본다.


실습 코드

sns.boxplot(hotel_df['lead_time'])
Q1 = hotel_df['lead_time'].quantile(0.25)
Q3 = hotel_df['lead_time'].quantile(0.75)
print(Q1)
print(Q3)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

print(lower_bound)
print(upper_bound)

hotel_df = hotel_df[(hotel_df['lead_time'] >= lower_bound) & (hotel_df['lead_time'] <= upper_bound)]
sns.boxplot(hotel_df['lead_time'])

개별 메서드 설명

quantile(0.25), quantile(0.75)

1사분위수와 3사분위수를 계산한다.

boxplot()

사분위수 기반으로 분포와 이상치를 시각화한다.

불리언 인덱싱

조건을 만족하는 행만 남기고 이상치 행을 제거한다.


주요 역할

  • lead_time 이상치 탐색
  • 극단값 제거
  • 모델이 비정상적인 예약 길이에 과도하게 흔들리지 않도록 조정

반환값

제거 전
제거 후


ai가 추천하는 심화 예제

Q1 = hotel_df["adr"].quantile(0.25)
Q3 = hotel_df["adr"].quantile(0.75)
IQR = Q3 - Q1

lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR

filtered_df = hotel_df[(hotel_df["adr"] >= lower) & (hotel_df["adr"] <= upper)]

print(filtered_df.shape)

# 결과
# adr 이상치 제거 후 데이터 크기 출력

8. sns.barplot() 와 범주형 변수별 취소율 확인

개요

barplot()은 범주형 변수별 수치형 값의 평균을 보여주는 시각화 함수이다.
실습에서는 distribution_channel과 arrival_date_month에 따라 평균 취소율이 어떻게 달라지는지 확인한다.

is_canceled는 0 또는 1의 값을 가지므로,
barplot에서 y축으로 is_canceled를 두면 각 그룹의 평균값은 곧 취소율로 해석할 수 있다.

예를 들어 어떤 채널의 평균 is_canceled 값이 0.35라면,
해당 채널 예약의 약 35%가 취소되었다고 해석할 수 있다.


실습 코드

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

주요 역할

  • 유통 채널별 취소율 비교
  • 월별 취소율 패턴 확인
  • 계절성과 예약 채널 특성 탐색

반환값


ai가 추천하는 심화 예제

plt.figure(figsize=(12, 4))
sns.barplot(x=hotel_df["customer_type"], y=hotel_df["is_canceled"])

# 결과
# 고객 유형별 평균 취소율 비교

9. calendar.month_name 과 월 순서 정렬

개요

arrival_date_month는 월 이름이 문자열로 저장되어 있기 때문에,
그대로 시각화하면 알파벳 순서로 정렬될 수 있다.

이를 막기 위해 calendar.month_name을 사용해 1월부터 12월까지의 순서를 직접 만든 뒤,
barplot의 order 옵션으로 전달한다.


실습 코드

import calendar
print(calendar.month_name[1])
print(calendar.month_name[2])
months = []
for i in range(1, 13):
    months.append(calendar.month_name[i])
months
plt.figure(figsize=(15, 5))
sns.barplot(x=hotel_df['arrival_date_month'], y=hotel_df['is_canceled'], order=months)

주요 역할

  • 월 이름을 시간 순서대로 정렬
  • 월별 취소율 패턴을 자연스럽게 해석 가능하게 만듦

반환값

  • calendar.month_name[i] → 문자열
  • order=months → 범주 정렬 기준 제공

ai가 추천하는 심화 예제

months = [calendar.month_name[i] for i in range(1, 13)]
print(months)

# 결과
# January부터 December까지 순서대로 출력

10. isna().sum(), value_counts(dropna=False), fillna()

개요

실습에서는 결측치가 있는지 확인한 뒤, children 컬럼의 결측치를 0으로 채운다.

children은 동반 어린이 수를 의미하므로,
결측치를 실제로 값이 없다고 보기보다는 어린이가 없는 경우로 처리하는 것이 자연스러울 수 있다.

value_counts(dropna=False)는 결측치까지 포함해서 각 값의 빈도를 확인하는 메서드이다.


실습 코드

hotel_df.isna().sum()
hotel_df['children'].value_counts(dropna=False)
hotel_df['children'] = hotel_df['children'].fillna(0)
hotel_df['children'].value_counts(dropna=False)

개별 메서드 설명

isna().sum()

컬럼별 결측치 개수를 계산한다.

value_counts(dropna=False)

결측치를 포함해 각 값의 등장 횟수를 계산한다.

fillna(0)

결측치를 0으로 대체한다.


주요 역할

  • 결측치 확인
  • children 결측치 처리
  • 결측치가 실제로 의미하는 바를 반영한 전처리

반환값

  • isna().sum() → pandas.Series
  • value_counts() → pandas.Series
  • fillna() → Series 또는 DataFrame

ai가 추천하는 심화 예제

hotel_df["agent"] = hotel_df["agent"].fillna(-1)
print(hotel_df["agent"].value_counts(dropna=False).head())

# 결과
# 결측치가 -1로 대체된 뒤 agent 분포 확인

11. 조건 필터링과 people 파생 변수 생성

개요

성인, 어린이, 아기 수를 각각 따로 두는 대신,
총 인원수를 나타내는 파생 변수 people을 만든다.

먼저 adults가 0인 경우를 확인하고,
그 다음 adults + children + babies로 people을 만든다.

people이 0이라는 것은 예약에 사람이 한 명도 없는 비정상 데이터이므로 제거한다.


실습 코드

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

주요 역할

  • 실제 투숙 인원 수를 하나의 변수로 통합
  • 비정상 예약 데이터 제거
  • 입력 변수 단순화

ai가 추천하는 심화 예제

hotel_df["people"] = hotel_df["adults"] + hotel_df["children"] + hotel_df["babies"]
print(hotel_df["people"].describe())

# 결과
# 총 인원 수 분포 확인

12. drop() 으로 기존 인원 컬럼 제거

개요

people 파생 변수를 만든 뒤에는 adults, children, babies를 제거한다.
이렇게 하면 중복 정보가 줄어들고 입력 공간이 더 단순해진다.


실습 코드

hotel_df.drop(['adults', 'children', 'babies'], axis=1, inplace=True)

주요 역할

  • 파생 변수로 대체된 기존 컬럼 제거
  • 중복 정보 감소
  • 모델 입력 단순화

ai가 추천하는 심화 예제

reduced_df = hotel_df.drop(["adults", "children", "babies"], axis=1)

print(reduced_df.head())

# 결과
# 기존 인원 컬럼이 제거된 데이터프레임 출력

13. total_nights 파생 변수 생성

개요

숙박일은 주중 숙박일과 주말 숙박일로 나뉘어 있다.
실습에서는 이를 합쳐 총 숙박일 수를 나타내는 total_nights를 만든다.


실습 코드

hotel_df['total_nights'] = hotel_df['stays_in_week_nights'] + hotel_df['stays_in_weekend_nights']
hotel_df.head()
hotel_df.drop(['stays_in_week_nights', 'stays_in_weekend_nights'], axis=1, inplace=True)
hotel_df.info()

주요 역할

  • 주중 / 주말 숙박일 통합
  • 총 체류 기간 정보 생성
  • 입력 변수 단순화

ai가 추천하는 심화 예제

print(hotel_df["total_nights"].describe())

# 결과
# 총 숙박일 분포 확인

14. map() 과 계절 파생 변수 생성

개요

arrival_date_month는 월 이름으로 저장되어 있지만,
모델링과 해석을 위해 이를 계절 범주로 바꾸는 것이 더 유용할 수 있다.

실습에서는 봄, 여름, 가을, 겨울 기준으로 월을 다시 매핑하여 season 변수를 만든다.

  • 3, 4, 5 → spring
  • 6, 7, 8 → summer
  • 9, 10, 11 → fall
  • 12, 1, 2 → winter

이 과정은 월 자체보다 더 큰 주기적 패턴을 반영할 수 있게 해준다.


실습 코드

# season 파생변수
# arrival_date_month에 따라 아래와 같이 값을 저장
# 12, 1, 2: winter
# 3, 4, 5: spring
# 6, 7, 8: summer
# 9, 10, 11: fall
hotel_df['season'] = hotel_df['arrival_date_month'].map(new_season_dic)
hotel_df.head()
hotel_df.drop(['arrival_date_month'], axis=1, inplace=True)

map() 의 역할

map()은 기존 값과 새로운 값을 대응시켜 변환하는 메서드이다.
여기서는 월 정보를 계절 정보로 바꾸는 데 사용된다.


주요 역할

  • 월 정보를 계절 범주로 변환
  • 계절성 반영
  • 월 컬럼보다 더 압축된 범주형 정보 생성

ai가 추천하는 심화 예제

season_map = {
    "January": "winter", "February": "winter", "March": "spring",
    "April": "spring", "May": "spring", "June": "summer",
    "July": "summer", "August": "summer", "September": "fall",
    "October": "fall", "November": "fall", "December": "winter"
}

hotel_df["season"] = hotel_df["arrival_date_month"].map(season_map)

print(hotel_df[["arrival_date_month", "season"]].head())

# 결과
# 월 이름과 대응되는 계절 출력

15. 객실 일치 여부 파생 변수와 astype(int)

개요

예약한 객실 유형과 실제 배정된 객실 유형이 같은지 여부는 고객 경험과 취소 여부에 영향을 줄 수 있다.

실습에서는 reserved_room_type과 assigned_room_type이 같은지 비교해서
expected_room_type이라는 이진 변수로 만든다.

비교 결과는 True 또는 False가 나오므로,
astype(int)를 사용해 1과 0으로 바꾼다.


실습 코드

hotel_df['expected_room_type'] = (hotel_df['reserved_room_type'] == hotel_df['assigned_room_type']).astype(int)
hotel_df.head()
hotel_df.drop(['reserved_room_type', 'assigned_room_type'], axis=1, inplace=True)

astype(int) 의 역할

불리언 값을 정수형으로 바꾼다.

  • True → 1
  • False → 0

주요 역할

  • 예약 객실과 실제 객실의 일치 여부를 하나의 feature로 압축
  • 문자열 비교 결과를 숫자로 변환
  • 모델 입력 가능한 형태로 변환

ai가 추천하는 심화 예제

match_col = (hotel_df["reserved_room_type"] == hotel_df["assigned_room_type"]).astype(int)
print(match_col.value_counts())

# 결과
# 객실 유형이 일치하는 예약과 그렇지 않은 예약 개수 출력

16. cancel_rate 파생 변수와 fillna(-1)

개요

이전 취소 횟수와 이전 비취소 예약 횟수를 따로 쓰는 대신,
그 비율을 하나의 변수로 압축해서 예약 취소 이력을 표현할 수 있다.

실습에서는 다음과 같은 방식으로 cancel_rate를 계산한다.

[
cancel_rate = \frac{previous_cancellations}{previous_cancellations + previous_bookings_not_canceled}
]

분모가 0인 경우에는 결과가 NaN이 되므로,
이 값은 fillna(-1)로 채운다.

여기서 -1은 실제 비율이 아니라,
이전 예약 이력이 전혀 없는 경우를 구분하기 위한 특별한 표식으로 볼 수 있다.


실습 코드

hotel_df['cancel_rate'] = hotel_df['previous_cancellations'] / (hotel_df['previous_cancellations'] + hotel_df['previous_bookings_not_canceled'])
hotel_df.head()
hotel_df['cancel_rate']
hotel_df[hotel_df['cancel_rate'].isna()]
hotel_df['cancel_rate'] = hotel_df['cancel_rate'].fillna(-1)
hotel_df.drop(['previous_cancellations', 'previous_bookings_not_canceled'], axis=1, inplace=True)

주요 역할

  • 이전 예약 이력을 하나의 비율 변수로 요약
  • 과거 취소 성향을 수치화
  • 분모가 0인 경우를 특수값으로 구분

ai가 추천하는 심화 예제

cancel_rate = hotel_df["previous_cancellations"] / (
    hotel_df["previous_cancellations"] + hotel_df["previous_bookings_not_canceled"]
)
cancel_rate = cancel_rate.fillna(-1)

print(cancel_rate.head())

# 결과
# 이전 예약 이력을 반영한 취소율 변수 출력

17. value_counts(), fillna() 로 agent 와 company 처리

개요

agent와 company 컬럼에는 결측치가 존재한다.
이 실습에서는 결측치를 0이나 평균값으로 채우지 않고, -1로 채운다.

이 방식은 결측을 단순 누락이 아니라
예약 대행사나 회사 정보가 없음을 나타내는 별도 범주처럼 다루겠다는 의미에 가깝다.


실습 코드

hotel_df['agent'].value_counts(dropna=False).sort_index()
hotel_df['agent'] = hotel_df['agent'].fillna(-1)
hotel_df['company'].value_counts(dropna=False).sort_index()
hotel_df['company'] = hotel_df['company'].fillna(-1)
hotel_df.isna().sum()

주요 역할

  • agent, company 결측치 처리
  • 결측을 별도 범주처럼 반영
  • 이후 수치형 또는 범주형 처리 안정화

ai가 추천하는 심화 예제

hotel_df["company"] = hotel_df["company"].fillna(-1)
print(hotel_df["company"].value_counts(dropna=False).head())

# 결과
# 결측치가 -1로 대체된 뒤 company 분포 확인

18. select_dtypes(), nunique(), drop() 으로 고유값 많은 컬럼 정리

개요

실습에서는 숫자가 아닌 컬럼을 자동으로 찾아 각 컬럼의 고유값 개수를 확인한다.

고유값이 지나치게 많은 컬럼은 원-핫 인코딩 시 차원을 크게 늘릴 수 있다.
이 때문에 meal, country, reservation_status_date 같은 컬럼은 제거한다.

특히 reservation_status_date는 날짜 문자열이고,
country는 국가 종류가 많으며,
meal도 경우에 따라 직접적인 예측 기여보다 차원 증가 부담이 더 클 수 있다.


실습 코드

hotel_df.select_dtypes(exclude=['number']).columns.tolist()
for i in hotel_df.select_dtypes(exclude=['number']).columns.tolist():
    print(i, hotel_df[i].nunique())
hotel_df.drop(['meal', 'country', 'reservation_status_date'], axis=1, inplace=True)

주요 역할

  • 범주형 컬럼 자동 탐색
  • 고유값 개수 점검
  • 차원 증가 위험이 큰 컬럼 제거

ai가 추천하는 심화 예제

cat_cols = hotel_df.select_dtypes(exclude=['number']).columns.tolist()

for col in cat_cols:
    print(col, hotel_df[col].nunique())

# 결과
# 범주형 컬럼별 고유값 개수 출력

19. pd.get_dummies()

개요

문자열 범주형 변수는 대부분의 머신러닝 모델에 그대로 넣을 수 없다.
따라서 각 범주를 0과 1의 더미 변수로 바꾸는 원-핫 인코딩이 필요하다.

실습에서는 숫자가 아닌 컬럼 전체를 한 번에 인코딩하고,
drop_first=True를 사용해 기준 범주 하나를 제거한다.

이렇게 하면 다중공선성 문제를 다소 줄일 수 있다.


실습 코드

hotel_df = pd.get_dummies(hotel_df, columns=hotel_df.select_dtypes(exclude=['number']).columns.tolist(), drop_first=True)
hotel_df.head()

주요 역할

  • 범주형 데이터를 수치형으로 변환
  • 분류 모델 입력 가능 상태로 변경
  • 기준 범주 제거로 변수 수를 조금 줄임

반환값


is_canceled	lead_time	arrival_date_year	arrival_date_week_number	arrival_date_day_of_month	is_repeated_guest	booking_changes	agent	company	days_in_waiting_list	...	deposit_type_Non Refund	deposit_type_Refundable	customer_type_Group	customer_type_Transient	customer_type_Transient-Party	reservation_status_Check-Out	reservation_status_No-Show	season_spring	season_summer	season_winter
0	0	342	2015	27	1	0	3	-1.0	-1.0	0	...	False	False	False	True	False	True	False	False	True	False
2	0	7	2015	27	1	0	0	-1.0	-1.0	0	...	False	False	False	True	False	True	False	False	True	False
3	0	13	2015	27	1	0	0	304.0	-1.0	0	...	False	False	False	True	False	True	False	False	True	False
4	0	14	2015	27	1	0	0	240.0	-1.0	0	...	False	False	False	True	False	True	False	False	True	False
5	0	14	2015	27	1	0	0	240.0	-1.0	0	...	False	False	False	True	False	True	False	False	True	False
5 rows × 39 columns

 


ai가 추천하는 심화 예제

encoded_df = pd.get_dummies(
    hotel_df,
    columns=["hotel", "season"],
    drop_first=True
)

print(encoded_df.head())

# 결과
# 지정한 범주형 컬럼이 더미 변수로 변환됨

20. train_test_split()

개요

train_test_split()은 데이터를 학습용과 테스트용으로 나누는 함수이다.

분류 모델은 학습 데이터로 패턴을 배우고,
훈련에 사용하지 않은 테스트 데이터로 일반화 성능을 확인해야 한다.

실습에서는 test_size를 0.3으로 두고, random_state를 고정해 실험을 재현 가능하게 만든다.


실습 코드

from sklearn.model_selection import train_test_split

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=10
)
X_train.shape, y_train.shape
X_test.shape, y_test.shape

주요 역할

  • 학습셋과 테스트셋 분리
  • 모델 일반화 성능 평가
  • 재현 가능한 실험 조건 설정

ai가 추천하는 심화 예제

print(X_train.shape)
print(X_test.shape)
print(y_train.mean(), y_test.mean())

# 결과
# 데이터 크기와 취소 비율 확인

21. 로지스틱 회귀 이론

개요

로지스틱 회귀는 이진 분류와 다중 분류 문제를 해결하기 위한 선형 분류 모델이다.

입력 변수의 선형 결합 값을 그대로 쓰지 않고,
시그모이드 함수를 적용해 0과 1 사이의 확률로 변환한다.

시그모이드 함수는 다음과 같다.

[
\sigma(z) = \frac{1}{1 + e^{-z}}
]

이 값은 특정 샘플이 양성 클래스에 속할 확률로 해석할 수 있다.

예를 들어 예약 취소 확률이 0.8이라면,
해당 예약이 취소될 가능성이 높다고 볼 수 있다.


22. 규제와 max_iter

개요

로지스틱 회귀는 규제를 함께 사용해 과적합을 방지하는 경우가 많다.

규제는 모델의 가중치가 지나치게 커지는 것을 막아
복잡도를 줄이고 새로운 데이터에도 잘 일반화하도록 돕는다.

또한 최적화를 반복적으로 수행하는 모델이므로,
반복 횟수가 부족하면 ConvergenceWarning이 발생할 수 있다.

이 경고는 최적해를 충분히 찾지 못했다는 뜻이다.
이를 줄이기 위해 max_iter 값을 늘린다.

실습에서는 기본 LogisticRegression()도 사용하고,
스케일링 후에는 max_iter=1000으로 늘려 안정적으로 학습시킨다.


23. StandardScaler 와 데이터 스케일링

개요

로지스틱 회귀는 입력 변수의 스케일 차이에 영향을 많이 받는 모델이다.
어떤 변수는 수천 단위이고 어떤 변수는 0~1 범위라면,
최적화 과정에서 특정 변수에 지나치게 끌릴 수 있다.

이 문제를 줄이기 위해 표준화를 사용한다.

표준화는 각 변수를 평균 0, 표준편차 1로 변환한다.

[
z = \frac{x - \mu}{\sigma}
]

실습에서는 StandardScaler를 이용해 입력 데이터를 스케일링한다.

주의할 점은 테스트 데이터에는 fit_transform이 아니라 transform만 적용하는 것이 일반적으로 맞다.
실무에서는 학습 데이터에서 구한 평균과 표준편차를 그대로 테스트 데이터에 적용해야 데이터 누수를 막을 수 있다.


실습 코드

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
x_train_scaled = scaler.fit_transform(X_train)
x_test_scaled = scaler.fit_transform(X_test)

x_train_scaled

주요 역할

  • 입력 변수 스케일 맞춤
  • 로지스틱 회귀 최적화 안정화
  • 수렴 속도 개선 가능

ai가 추천하는 심화 예제

scaler = StandardScaler()
x_train_scaled = scaler.fit_transform(X_train)
x_test_scaled = scaler.transform(X_test)

print(x_train_scaled[:3])

# 결과
# 표준화된 학습 데이터 일부 출력

24. LogisticRegression(), fit(), predict()

개요

LogisticRegression()은 로지스틱 회귀 분류기 객체를 생성하고,
fit()은 학습 데이터를 사용해 모델을 학습시키며,
predict()는 최종 클래스 예측값을 반환한다.


실습 코드

from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()
lr.fit(X_train, y_train)

스케일링 후:

model = LogisticRegression(max_iter=1000)
model.fit(x_train_scaled, y_train)
pred = model.predict(x_test_scaled)

각 메서드의 역할

LogisticRegression()

이진 분류 모델 생성

fit(X, y)

입력과 정답을 이용해 가중치 학습

predict(X)

각 샘플의 최종 클래스 0 또는 1 예측


ai가 추천하는 심화 예제

model = LogisticRegression(max_iter=1000)
model.fit(x_train_scaled, y_train)

print(model.coef_)
print(model.intercept_)

# 결과
# 가중치와 절편 출력

25. accuracy_score()

개요

accuracy_score()는 전체 예측 중 몇 개를 맞췄는지를 비율로 나타내는 지표이다.

[
Accuracy = \frac{정확히\ 맞춘\ 샘플\ 수}{전체\ 샘플\ 수}
]

직관적이고 해석하기 쉽지만,
클래스 불균형이 심할 때는 Accuracy만으로 모델을 평가하면 오해가 생길 수 있다.


실습 코드

from sklearn.metrics import accuracy_score
accuracy_score(y_test, pred)
hotel_df['is_canceled'].value_counts()

주요 역할

  • 전체적인 정답률 확인
  • 클래스 불균형 여부와 함께 해석

ai가 추천하는 심화 예제

acc = accuracy_score(y_test, pred)
print(acc)

# 결과
# 정확도 출력

26. 혼동 행렬과 confusion_matrix()

개요

혼동 행렬은 실제값과 예측값의 관계를 네 가지 경우로 나눠 보여준다.

  • TP: 실제 양성, 예측 양성
  • TN: 실제 음성, 예측 음성
  • FP: 실제 음성, 예측 양성
  • FN: 실제 양성, 예측 음성

이진 분류에서는 단순 정확도보다 혼동 행렬을 함께 보는 것이 훨씬 중요하다.


실습 코드

from sklearn.metrics import confusion_matrix

confusion_matrix(y_test, pred)

주요 역할

  • 어떤 오류가 많이 발생하는지 확인
  • FP와 FN의 균형 확인
  • 정밀도, 재현율, F1 해석의 기반 제공

반환값

array([[22155,     0],
       [    0, 12707]])

ai가 추천하는 심화 예제

cm = confusion_matrix(y_test, pred)
print(cm)

# 결과
# [[TN FP]
#  [FN TP]]

27. precision_score(), recall_score(), f1_score()

개요

정밀도, 재현율, F1 점수는 Accuracy가 놓칠 수 있는 부분을 보완하는 대표적인 분류 지표이다.

정밀도

모델이 양성이라고 예측한 것 중 실제 양성의 비율

[
Precision = \frac{TP}{TP + FP}
]

재현율

실제 양성 중 모델이 양성으로 맞춘 비율

[
Recall = \frac{TP}{TP + FN}
]

F1 점수

정밀도와 재현율의 조화평균

[
F1 = 2 \cdot \frac{Precision \cdot Recall}{Precision + Recall}
]

취소 예측 문제에서는 어떤 오류가 더 중요한지에 따라
정밀도를 우선할지, 재현율을 우선할지 달라질 수 있다.


실습 코드

from sklearn.metrics import precision_score, recall_score, f1_score

print(precision_score(y_test, pred))
print(recall_score(y_test, pred))
print(f1_score(y_test, pred))

주요 역할

  • 양성 클래스 예측 품질 점검
  • 정확도 외의 세부 성능 확인
  • 비즈니스 목적에 맞는 임계값 조정 방향 판단

반환값

1.0
1.0
1.0

ai가 추천하는 심화 예제

precision = precision_score(y_test, pred)
recall = recall_score(y_test, pred)
f1 = f1_score(y_test, pred)

print(precision, recall, f1)

# 결과
# 정밀도, 재현율, F1 점수 출력

28. coef_, intercept_

개요

로지스틱 회귀는 선형 결합을 기반으로 작동하므로,
학습 후 각 변수의 가중치와 절편을 확인할 수 있다.

  • coef_ 는 각 feature의 기울기 역할
  • intercept_ 는 절편 역할

가중치가 양수이면 해당 변수가 커질수록 양성 클래스 쪽으로 기울고,
음수이면 반대로 음성 클래스 쪽으로 기울 가능성이 있다.


실습 코드

model.coef_
model.intercept_

주요 역할

  • 변수 영향 방향 확인
  • 모델 해석 가능성 제공

ai가 추천하는 심화 예제

print(model.coef_.shape)
print(model.intercept_)

# 결과
# 계수 배열 형태와 절편 출력

29. predict_proba() 와 확률 기반 예측

개요

predict()는 최종 클래스만 반환하지만,
predict_proba()는 각 클래스에 속할 확률을 반환한다.

이진 분류에서는 다음과 같은 형태가 나온다.

  • 첫 번째 값: 클래스 0일 확률
  • 두 번째 값: 클래스 1일 확률

실습에서는 취소할 확률만 따로 뽑아 확인한다.


실습 코드

proba = model.predict_proba(x_test_scaled)
proba
proba = model.predict_proba(x_test_scaled)[:, 1]
proba

주요 역할

  • 단순 클래스 예측을 넘어 확률적 판단 가능
  • 임계값 조정의 기반 제공
  • ROC AUC 계산에 활용 가능

반환값

array([9.99730467e-01, 1.42358244e-04, 2.03425814e-04, ...,
       1.97182673e-04, 4.97684677e-05, 9.99982951e-01])

ai가 추천하는 심화 예제

proba = model.predict_proba(x_test_scaled)[:5]
print(proba)

# 결과
# 앞 5개 샘플에 대한 각 클래스 확률 출력

30. 임계값 조정과 astype(int)

개요

기본적으로 이진 분류 모델은 양성 확률이 0.5 이상이면 1로 예측한다.
하지만 상황에 따라 이 기준을 높이거나 낮출 수 있다.

실습에서는 threshold를 매우 높게 설정해서
취소라고 판단하는 기준을 더 엄격하게 만든다.

threshold = 0.9999
pred = (proba >= threshold).astype(int)

여기서 astype(int)는 True, False를 1, 0으로 바꾸는 역할을 한다.

임계값을 높이면 일반적으로 다음 경향이 생긴다.

  • 정밀도는 높아질 수 있음
  • 재현율은 낮아질 수 있음

즉, 정말 확실한 경우만 취소라고 예측하겠다는 전략이다.


실습 코드

threshold = 0.9999
pred = (proba >= threshold).astype(int)
pred

주요 역할

  • 양성 판정 기준 직접 제어
  • 정밀도 / 재현율 trade-off 조정
  • 비즈니스 목적에 맞는 예측 전략 설계

ai가 추천하는 심화 예제

threshold = 0.7
pred_custom = (proba >= threshold).astype(int)

print(pred_custom[:10])

# 결과
# 임계값 0.7 기준 예측 결과 출력

31. RandomForestClassifier()

개요

랜덤 포레스트는 여러 개의 결정트리를 결합한 앙상블 분류 모델이다.
각 트리는 무작위 데이터 샘플과 일부 특성을 사용해 학습되고,
최종 예측은 다수결로 결정된다.

장점은 다음과 같다.

  • 비선형 패턴 학습 가능
  • 과적합 완화
  • 특성 중요도 제공
  • 스케일링 민감도가 상대적으로 낮음

실습 코드

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(random_state=10)
rf.fit(X_train, y_train)
pred1 = rf.predict(X_test)
pred1

주요 역할

  • 로지스틱 회귀와 다른 구조의 분류 모델 비교
  • 비선형 관계 반영
  • 확률 기반 출력 가능

ai가 추천하는 심화 예제

rf = RandomForestClassifier(n_estimators=200, random_state=10)
rf.fit(X_train, y_train)

pred_rf = rf.predict(X_test)
print(accuracy_score(y_test, pred_rf))

# 결과
# 랜덤 포레스트 정확도 출력

32. RandomForestClassifier 의 predict_proba()

개요

랜덤 포레스트도 predict_proba()를 통해 각 클래스 확률을 반환할 수 있다.

각 트리가 특정 클래스를 얼마나 지지하는지를 종합해
최종 확률 형태로 제공한다.


실습 코드

proba1 = rf.predict_proba(X_test)
proba1
proba1[0]
proba1[:, 1]

주요 역할

  • 각 예약이 취소될 확률 계산
  • 확률 기반 임계값 조정 가능
  • ROC AUC 분석에 활용 가능

ai가 추천하는 심화 예제

proba_rf = rf.predict_proba(X_test)[:, 1]
print(proba_rf[:10])

# 결과
# 취소 클래스 확률 상위 10개 출력

33. classification_report()

개요

classification_report()는 정밀도, 재현율, F1 점수, support를 한 번에 요약해 보여주는 함수이다.

클래스별 성능을 종합적으로 보고 싶을 때 매우 편리하다.


실습 코드

from sklearn.metrics import classification_report, roc_auc_score

print(classification_report(y_test, pred))

주요 역할

  • 클래스별 정밀도, 재현율, F1 한 번에 확인
  • macro average, weighted average 확인
  • 모델 성능 요약

반환값

문자열 형태의 보고서가 출력된다.


ai가 추천하는 심화 예제

report = classification_report(y_test, pred1)
print(report)

# 결과
# 랜덤 포레스트 분류 성능 요약 출력

34. ROC AUC Score 와 roc_auc_score()

개요

ROC AUC는 이진 분류 모델의 확률 예측 능력을 평가하는 대표 지표이다.

ROC Curve는 다양한 임계값에서의 TPR과 FPR 관계를 그린 곡선이고,
AUC는 그 곡선 아래 면적을 의미한다.

  • AUC = 1 이면 완벽한 분류기
  • AUC = 0.5 이면 랜덤 추측 수준
  • AUC < 0.5 이면 랜덤보다 나쁜 수준

ROC AUC는 양성과 음성 샘플을 얼마나 잘 구분하는지를 나타내는 지표로 이해할 수 있다.


실습 코드

roc_auc_score(y_test, pred)

주요 역할

  • 단순 정확도를 넘는 분류 성능 평가
  • 임계값 변화 전체를 고려한 평가
  • 클래스 구분 능력 수치화

반환값

np.float64(0.6928071141890297)

ai가 추천하는 심화 예제

auc = roc_auc_score(y_test, pred1)
print(auc)

# 결과
# 랜덤 포레스트 기준 ROC AUC 출력

35. roc_curve() 와 ROC Curve 시각화

개요

roc_curve()는 여러 임계값에 대해 FPR, TPR, threshold를 계산하는 함수이다.

  • FPR: 거짓 양성 비율
  • TPR: 참 양성 비율

실습에서는 이를 이용해 ROC Curve를 직접 그린다.

다만 ROC Curve는 일반적으로 확률값을 넣는 것이 더 적절하다.
클래스 예측값만 넣으면 곡선 정보가 크게 줄어들 수 있다.


실습 코드

import matplotlib.pyplot as plt
from sklearn.metrics._plot.roc_curve import roc_curve

fpr, tpr, thr = roc_curve(y_test, pred)
print(fpr, tpr, thr)

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

주요 역할

  • 임계값에 따른 분류 성능 변화 확인
  • 무작위 추측 기준선과 비교
  • ROC AUC 해석 보조

반환값


ai가 추천하는 심화 예제

proba_rf = rf.predict_proba(X_test)[:, 1]
fpr, tpr, thr = roc_curve(y_test, proba_rf)

plt.plot(fpr, tpr)
plt.plot([0, 1], [0, 1], linestyle="--")
plt.show()

# 결과
# 확률값 기반 ROC Curve 출력

36. KFold 와 교차 검증

개요

교차 검증은 모델 성능을 더 안정적으로 평가하기 위해 데이터를 여러 번 나누어 검증하는 기법이다.

K-Fold 교차 검증은 데이터를 k개로 나누고,
각 폴드를 한 번씩 검증셋으로 사용하면서 나머지를 학습셋으로 사용한다.

이렇게 하면 특정 한 번의 train/test 분할에 덜 의존하는 평가가 가능하다.


실습 코드

from sklearn.model_selection import KFold

kf = KFold(n_splits=5)
kf
for train_index, test_index in kf.split(range(len(hotel_df))):
    print(train_index, test_index, len(train_index), len(test_index))

주요 역할

  • 데이터 분할을 여러 번 반복
  • 단일 split보다 안정적인 성능 추정
  • 검증 편향 완화

주요 파라미터

파라미터 설명
n_splits 폴드 개수
shuffle 섞을지 여부
random_state 셔플 시 난수 고정

ai가 추천하는 심화 예제

kf = KFold(n_splits=5, shuffle=True, random_state=2025)

for fold, (train_idx, test_idx) in enumerate(kf.split(range(len(hotel_df))), start=1):
    print(fold, len(train_idx), len(test_idx))

# 결과
# 각 폴드별 학습/검증 크기 출력

37. shuffle=True 와 random_state

개요

기본 KFold는 데이터를 순서대로 나눈다.
하지만 데이터가 특정 순서로 정렬되어 있으면 분할 편향이 생길 수 있다.

이를 줄이기 위해 shuffle=True를 설정하고,
random_state로 셔플 결과를 고정한다.


실습 코드

kf = KFold(n_splits=5, random_state=2025, shuffle=True)
kf
for train_index, test_index in kf.split(range(len(hotel_df))):
    print(train_index, test_index, len(train_index), len(test_index))

주요 역할

  • 데이터 순서 편향 완화
  • 더 무작위적인 폴드 구성
  • 재현 가능한 교차 검증 환경 구축

38. iloc 를 이용한 교차 검증용 데이터 분리

개요

KFold가 반환하는 것은 인덱스 배열이므로,
실제 데이터프레임과 시리즈에서 해당 행을 선택하려면 iloc가 필요하다.

iloc는 정수 위치 기반 인덱싱 메서드이다.


실습 코드

acc_list = []

for train_index, test_index in kf.split(range(len(hotel_df))):
    X = hotel_df.drop('is_canceled', axis=1)
    y = hotel_df['is_canceled']

    X_train = X.iloc[train_index]
    X_test = X.iloc[test_index]
    y_train = y.iloc[train_index]
    y_test = y.iloc[test_index]

주요 역할

  • 정수 인덱스 배열 기반으로 행 선택
  • 각 폴드별 학습/검증 데이터 분리
  • 교차 검증 루프 구성

ai가 추천하는 심화 예제

X = hotel_df.drop("is_canceled", axis=1)
y = hotel_df["is_canceled"]

for train_index, test_index in kf.split(range(len(hotel_df))):
    X_train = X.iloc[train_index]
    X_test = X.iloc[test_index]
    print(X_train.shape, X_test.shape)
    break

# 결과
# 첫 번째 폴드 기준 학습/검증 데이터 크기 출력

39. 실습 전체 흐름 요약

pd.read_csv()
↓
info() / describe()
↓
sns.displot()
↓
sns.boxplot()
↓
quantile() / IQR 계산
↓
이상치 제거
↓
sns.barplot()
↓
calendar.month_name
↓
isna().sum() / value_counts() / fillna()
↓
people 파생 변수 생성
↓
drop()
↓
total_nights 파생 변수 생성
↓
map() 으로 season 생성
↓
expected_room_type 생성
↓
cancel_rate 생성
↓
select_dtypes() / nunique()
↓
pd.get_dummies()
↓
train_test_split()
↓
StandardScaler()
↓
LogisticRegression()
↓
fit() / predict() / predict_proba()
↓
accuracy_score()
↓
confusion_matrix()
↓
precision_score() / recall_score() / f1_score()
↓
임계값 조정
↓
RandomForestClassifier()
↓
classification_report()
↓
roc_auc_score()
↓
roc_curve()
↓
KFold()
↓
iloc 기반 교차 검증

40. 최종 정리

이 실습은 호텔 예약 취소 예측 문제를 다루는 이진 분류문제로,
단순히 모델 하나를 학습하는 수준을 넘어 분류 문제의 전형적인 전체 흐름을 잘 담고 있다.

핵심적으로 포함된 내용은 다음과 같다.

데이터 이해

  • 예약 취소 여부를 예측하기 위해 호텔 유형, 예약 채널, 체류 기간, 인원 수, 고객 이력, 객실 정보 등을 사용한다.

EDA

  • lead_time 분포와 이상치를 확인하고
  • 유통 채널과 월별 취소율을 시각화한다.

전처리

  • children, agent, company 결측치 처리
  • people, total_nights, season, expected_room_type, cancel_rate 같은 파생 변수 생성
  • 불필요 컬럼 제거
  • 범주형 변수 원-핫 인코딩

모델링

  • 로지스틱 회귀
  • 표준화 적용 로지스틱 회귀
  • 랜덤 포레스트 분류기

평가

  • Accuracy
  • 혼동 행렬
  • Precision, Recall, F1
  • ROC AUC
  • ROC Curve

추가 실험

  • 확률 기반 임계값 조정
  • K-Fold 교차 검증