머신러닝 수요 예측 응용
서울시 공공자전거 ‘따릉이’와 호텔 예약 데이터를 활용하면, 실제 서비스에서 활용할 수 있는 수요 예측 모델을 만들어 볼 수 있다. 이번 실습에서는 회귀 문제(자전거 대여 수 예측)와 이진 분류 문제(호텔 예약 취소 여부 예측)를 함께 다루면서, 결정 트리, 랜덤 포레스트, 로지스틱 회귀, 교차 검증까지 한 번에 정리해본다.
정말 간단하게 설명하자면, “언제 자전거를 얼마나 빌릴지”, “이 예약이 취소될 확률이 얼마나 될지”를 데이터로 예측해보는 하루치 실습이다.
(서울 자전거 공유 수요 데이터셋 정리, 호텔 예약 수요 데이터셋 정리를 기반으로 재구성)
1. 서울 자전거 공유 수요 예측 (회귀)
1-1. 데이터셋 소개
서울시 공공자전거 ‘따릉이’의 대여 수요를 예측하는 Seoul Bike Sharing Demand 데이터셋은 시간대, 기온, 습도, 가시거리, 눈/비, 계절, 공휴일 여부 등 다양한 정보를 바탕으로 Rented Bike Count(시간당 대여 자전거 수)를 예측하는 회귀 문제에 사용된다.
원본 설명은 블로그 글에 잘 정리되어 있다.
1-2. 데이터 로드 및 기본 정보
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
bike_df = pd.read_csv(
'/content/drive/MyDrive/랭체인 AI 영상객체탐지분석 플랫폼 구축/10. 머신러닝 딥러닝/data/SeoulBikeData.csv',
encoding='CP949'
)
bike_df.info()
bike_df.describe()
※ CP949 인코딩
- Microsoft Windows의 한국어 문자 인코딩
- EUC-KR을 확장한 형태로 더 많은 한글·한자를 지원
- 윈도우에서 저장한 한글 CSV 파일에서 자주 사용
컬럼명을 분석하기 좋게 정리해준다.
bike_df.columns = [
'Date', 'Rented Bike Count', 'Hour', 'Temperature', 'Humidity',
'Wind speed', 'Visibility', 'Dew point temperature',
'Solar Radiation', 'Rainfall', 'Snowfall', 'Seasons',
'Holiday', 'Functioning Day'
]
bike_df.head()
1-3. 기초 EDA
주요 연속형 변수와 대여 수의 관계를 산점도로 본다.
sns.scatterplot(x='Temperature', y='Rented Bike Count', data=bike_df, alpha=0.3)
sns.scatterplot(x='Wind speed', y='Rented Bike Count', data=bike_df, alpha=0.3)
sns.scatterplot(x='Visibility', y='Rented Bike Count', data=bike_df, alpha=0.3)
sns.scatterplot(x='Hour', y='Rented Bike Count', data=bike_df, alpha=0.3)
결측치 확인:
bike_df.isna().sum()
날짜를 datetime으로 변환하고 연·월·일 파생변수를 만든다.
bike_df['Date'] = pd.to_datetime(bike_df['Date'], format='%d/%m/%Y')
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.info()
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()
연도·월별 평균 수요:
bike_df[bike_df['year'] == 2017].groupby('month')['Rented Bike Count'].mean()
bike_df[bike_df['year'] == 2018].groupby('month')['Rented Bike Count'].mean()
1-4. 시간대 구간화 (pd.cut)
Hour를 범주형 시간대(새벽/아침/오후/저녁)로 나눈다.
bike_df['TimeOfDay'] = pd.cut(
bike_df['Hour'],
bins=[0, 5, 11, 17, 23],
labels=['Dawn', 'Morning', 'Afternoon', 'Evening'],
include_lowest=True
)
bike_df.head()
※ pd.cut() 요약
pd.cut()은 연속형 숫자 데이터를 구간(bins) 으로 나누어 범주형 데이터로 바꿔주는 함수다.
bins: 숫자를 나눌 구간 경계값- 기본적으로 오른쪽 경계값을 포함 (예:
(5, 11])
| 시간 범위 | 레이블 |
|---|---|
| 0 ≤ Hour ≤ 5 | Dawn |
| 5 < Hour ≤ 11 | Morning |
| 11 < Hour ≤ 17 | Afternoon |
| 17 < Hour ≤ 23 | Evening |
1-5. 범주형 처리 및 상관분석
운영일 여부에 따른 평균 대여 수:
sns.barplot(x='Functioning Day', y='Rented Bike Count', data=bike_df)
날짜는 제거하고 범주형은 원-핫 인코딩으로 변환한다.
bike_df = bike_df.drop('Date', axis=1)
cat_cols = bike_df.select_dtypes(exclude=['number']).columns.tolist()
for c in cat_cols:
print(c, bike_df[c].nunique())
bike_df = pd.get_dummies(
bike_df,
columns=cat_cols,
drop_first=True
)
bike_df.head()
상관계수와 히트맵:
correlation_matrix = bike_df.corr()
target_corr = correlation_matrix['Rented Bike Count'].sort_values(ascending=False)
print(target_corr)
plt.figure(figsize=(16, 12))
sns.heatmap(correlation_matrix, annot=True, fmt='.2f', cmap='coolwarm')
plt.title('Feature Correlation Heatmap')
plt.show()
※ corr()과 다중공선성
corr()는 피어슨 상관계수를 사용해 숫자형 컬럼 간 선형 상관관계를 계산한다.- |상관계수|가 0.5 이상이면 강한 상관, 0.2 이하이면 약한 상관으로 보는 경우가 많다.
- 서로 상관계수가 너무 높은 독립변수들끼리는 다중공선성(Multicollinearity) 를 일으켜 회귀 계수 해석과 예측을 불안정하게 만들 수 있다.
다중공선성이 의심되면, 상관이 높은 컬럼 중 일부를 제거하거나 차원 축소를 고려한다.
1-6. VIF로 다중공선성 점검
from statsmodels.stats.outliers_influence import variance_inflation_factor
X = bike_df.drop(columns=['Rented Bike Count'], errors='ignore')
X = X.select_dtypes(include='number')
X = X.loc[:, X.nunique() > 1] # 분산 0인 컬럼 제거
X = X.astype(float)
vif_df = pd.DataFrame({
'Feature': X.columns,
'VIF': [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
}).sort_values(by='VIF', ascending=False)
vif_df
VIF 값이 너무 큰(Dew point temperature, Hour, Humidity, year 등) 컬럼은 제거 후 다시 계산한다.
bike_df = bike_df.drop(['Dew point temperature', 'Hour', 'Humidity', 'year'], axis=1)
X = bike_df.drop(columns=['Rented Bike Count'], errors='ignore')
X = X.select_dtypes(include='number')
X = X.loc[:, X.nunique() > 1].astype(float)
vif_df = pd.DataFrame({
'Feature': X.columns,
'VIF': [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
}).sort_values(by='VIF', ascending=False)
vif_df
※ VIF(Variance Inflation Factor)
- 한 독립변수가 다른 독립변수들의 선형 결합으로 얼마나 잘 설명되는지를 나타내는 수치
- 해석 기준
- VIF < 5 : 큰 문제 없음
- 5 ~ 10 : 주의
- ≥ 10 : 다중공선성 강하게 의심
1-7. 결정 트리/선형 회귀/랜덤 포레스트 비교
먼저 학습·테스트 데이터를 나눈다.
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=2026
)
X_train.shape, y_train.shape
X_test.shape, y_test.shape
(1) 결정 트리 회귀
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import root_mean_squared_error
dtr = DecisionTreeRegressor(random_state=2025)
dtr.fit(X_train, y_train)
pred1 = dtr.predict(X_test)
sns.scatterplot(x=y_test, y=pred1)
root_mean_squared_error(y_test, pred1)
(2) 선형 회귀
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)
(3) 하이퍼파라미터 적용한 결정 트리
dtr = DecisionTreeRegressor(
random_state=2026,
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)
(4) 트리 구조 시각화
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()
(5) 랜덤 포레스트 회귀
from sklearn.ensemble import RandomForestRegressor
rf = RandomForestRegressor(random_state=2026)
rf.fit(X_train, y_train)
pred4 = rf.predict(X_test)
root_mean_squared_error(y_test, pred4)
RMSE 비교(예시):
- 결정 트리 : 약 384.74
- 선형 회귀 : 약 446.47
- 튜닝된 결정 트리 : 약 334.13
- 랜덤 포레스트 : 약 288.82
랜덤 포레스트가 가장 낮은 오차를 기록하며, 단일 트리나 선형 회귀보다 안정적인 성능을 보여준다.
중요 특성 확인
feature_imp = pd.DataFrame({
'features': X_train.columns,
'importances': rf.feature_importances_
})
top10 = feature_imp.sort_values('importances', ascending=False).head(10)
plt.figure(figsize=(5, 10))
sns.barplot(x='importances', y='features', data=top10)
2. 호텔 예약 취소 예측 (이진 분류)
2-1. 데이터셋 소개
호텔 예약 수요 데이터셋은 호텔 예약 취소 여부(is_canceled) 를 예측하는 데 널리 사용되는 데이터셋이다.
호텔 타입, 도착 날짜, 숙박 일수, 인원 수, 예약 채널, 고객 유형, 과거 취소 이력, 요금(adr) 등 다양한 정보가 포함되어 있어, 실제 비즈니스에서 활용 가능한 분류 문제를 구성할 수 있다. (자세한 설명)
2-2. 데이터 로드 및 기초 탐색
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/랭체인 AI 영상객체탐지분석 플랫폼 구축/10. 머신러닝 딥러닝/data/hotel_bookings.csv'
)
hotel_df.info()
hotel_df.describe()
lead_time 분포와 이상치를 확인한다.
sns.displot(hotel_df['lead_time'])
sns.boxplot(hotel_df['lead_time'])
IQR 기반 이상치 제거:
Q1 = hotel_df['lead_time'].quantile(0.25)
Q3 = hotel_df['lead_time'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
hotel_df = hotel_df[
(hotel_df['lead_time'] >= lower_bound) &
(hotel_df['lead_time'] <= upper_bound)
]
len(hotel_df)
sns.boxplot(hotel_df['lead_time'])
분포와 채널별 취소율:
sns.barplot(x=hotel_df['distribution_channel'], y=hotel_df['is_canceled'])
hotel_df['distribution_channel'].value_counts()
sns.barplot(x=hotel_df['hotel'], y=hotel_df['is_canceled'])
월별 취소율:
plt.figure(figsize=(15, 5))
sns.barplot(x=hotel_df['arrival_date_month'], y=hotel_df['is_canceled'])
실제 달 순서대로 정렬:
import calendar
months = [calendar.month_name[i] for i in range(1, 13)]
plt.figure(figsize=(15, 5))
sns.barplot(
x=hotel_df['arrival_date_month'],
y=hotel_df['is_canceled'],
order=months
)
2-3. 결측치 처리 및 파생변수 만들기
hotel_df.isna().sum()
hotel_df['children'].value_counts(dropna=False)
(1) 인원 관련 처리
hotel_df['children'] = hotel_df['children'].fillna(0)
hotel_df['people'] = (
hotel_df['adults'] +
hotel_df['children'] +
hotel_df['babies']
)
hotel_df = hotel_df[hotel_df['people'] != 0]
hotel_df.drop(['adults', 'children', 'babies'], axis=1, inplace=True)
(2) 총 숙박일 파생변수
hotel_df['total_nights'] = (
hotel_df['stays_in_week_nights'] +
hotel_df['stays_in_weekend_nights']
)
hotel_df.drop(
['stays_in_week_nights', 'stays_in_weekend_nights'],
axis=1,
inplace=True
)
(3) 시즌(season) 파생변수
season_dic = {
'spring': [3, 4, 5],
'summer': [6, 7, 8],
'fall': [9, 10, 11],
'winter': [12, 1, 2]
}
new_season_dic = {}
for season, months_ in season_dic.items():
for m in months_:
new_season_dic[calendar.month_name[m]] = season
hotel_df['season'] = hotel_df['arrival_date_month'].map(new_season_dic)
hotel_df.drop(['arrival_date_month'], axis=1, inplace=True)
(4) 기대 객실 타입一致 여부
hotel_df['expected_room_type'] = (
hotel_df['reserved_room_type'] == hotel_df['assigned_room_type']
).astype(int)
hotel_df.drop(
['reserved_room_type', 'assigned_room_type'],
axis=1,
inplace=True
)
(5) 과거 취소율(cancel_rate)
hotel_df['cancel_rate'] = (
hotel_df['previous_cancellations'] /
(hotel_df['previous_cancellations'] + hotel_df['previous_bookings_not_canceled'])
)
hotel_df['cancel_rate'] = hotel_df['cancel_rate'].fillna(-1)
hotel_df.drop(
['previous_cancellations', 'previous_bookings_not_canceled'],
axis=1,
inplace=True
)
(6) agent/company 빈도 인코딩
hotel_df['agent'] = hotel_df['agent'].fillna(-1)
agent_freq = hotel_df['agent'].value_counts()
hotel_df['agent_freq'] = hotel_df['agent'].map(agent_freq)
hotel_df.drop(['agent'], axis=1, inplace=True)
hotel_df['company'] = hotel_df['company'].fillna(-1)
company_freq = hotel_df['company'].value_counts()
hotel_df['company_freq'] = hotel_df['company'].map(company_freq)
hotel_df.drop(['company'], axis=1, inplace=True)
(7) 불필요한 범주형 제거 + 원-핫 인코딩
hotel_df.drop(
['meal', 'country', 'reservation_status_date'],
axis=1,
inplace=True
)
cat_cols = hotel_df.select_dtypes(exclude=['number']).columns.tolist()
for c in cat_cols:
print(c, hotel_df[c].nunique())
hotel_df = pd.get_dummies(
hotel_df,
columns=cat_cols,
drop_first=True
)
hotel_df.info()
2-4. 로지스틱 회귀 (LogisticRegression)
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=2026
)
X_train.shape, y_train.shape
X_test.shape, y_test.shape
스케일링 후 로지스틱 회귀를 학습한다.
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
model = LogisticRegression(max_iter=1000)
model.fit(X_train_scaled, y_train)
pred = model.predict(X_test_scaled)
정확도 확인:
from sklearn.metrics import accuracy_score
accuracy_score(y_test, pred)
hotel_df['is_canceled'].value_counts()
※ 규제(Regularization)와 스케일링
- 로지스틱 회귀는 기본적으로 L2 규제를 사용해 과적합을 막는다.
- 규제항이 가중치의 크기를 제한하기 때문에, 특성 스케일이 크게 다르면 특정 특성에 과도하게 페널티가 가해질 수 있다.
- 그래서
StandardScaler로 평균 0, 표준편차 1로 맞춰주는 표준화가 많이 사용된다.
2-5. 혼동 행렬과 분류 지표
from sklearn.metrics import (
confusion_matrix,
precision_score,
recall_score,
f1_score
)
cm = confusion_matrix(y_test, pred)
print(cm)
print(precision_score(y_test, pred))
print(recall_score(y_test, pred))
print(f1_score(y_test, pred))
혼동 행렬 용어 정리
- TP (True Positive): 실제 1, 예측 1 (실제 취소, 취소로 예측)
- TN (True Negative): 실제 0, 예측 0 (실제 유지, 유지로 예측)
- FP (False Positive): 실제 0, 예측 1 (유지인데 취소라고 잘못 예측)
- FN (False Negative): 실제 1, 예측 0 (취소인데 유지라고 잘못 예측)
혼동 행렬로부터 Accuracy, Precision, Recall, F1 등 다양한 지표를 계산할 수 있다.
예측 확률과 임계값 조정:
proba = model.predict_proba(X_test_scaled)[:, 1] # 취소할 확률
threshold = 0.5
pred = (proba >= threshold).astype(int)
2-6. 랜덤 포레스트 분류와 ROC AUC
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(random_state=2026)
rf.fit(X_train, y_train)
pred1 = rf.predict(X_test)
proba1 = rf.predict_proba(X_test)
성능 평가:
from sklearn.metrics import classification_report, roc_auc_score
print(classification_report(y_test, pred1))
roc_auc_score(y_test, proba1[:, 1])
ROC 곡선 그리기:
from sklearn.metrics import roc_curve
import matplotlib.pyplot as plt
fpr, tpr, thr = roc_curve(y_test, proba1[:, 1])
plt.plot(fpr, tpr, label='ROC Curve')
plt.plot([0, 1], [0, 1])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.legend()
plt.show()
※ ROC Curve & AUC
- ROC 곡선: x축 FPR, y축 TPR(Recall)로 그린 곡선
- AUC: ROC 곡선 아래 면적
- 1에 가까울수록 좋고, 0.5면 랜덤 추측 수준
2-7. K-Fold 교차 검증
from sklearn.model_selection import KFold
from sklearn.metrics import accuracy_score
kf = KFold(n_splits=5, random_state=2026, shuffle=True)
acc_list = []
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]
y_train = y.iloc[train_index]
y_test = y.iloc[test_index]
rf = RandomForestClassifier()
rf.fit(X_train, y_train)
pred = rf.predict(X_test)
acc_list.append(accuracy_score(y_test, pred))
acc_list
np.array(acc_list).mean()
교차 검증 정리
- 데이터를 여러 조각으로 나누어 번갈아가며 학습/검증에 사용
- 데이터 분할 운에 따른 편향을 줄이고, 모델의 일반화 성능을 더 안정적으로 측정
- 데이터가 많지 않은 경우 특히 유용
3. 실습 요약
서울 자전거 수요 예측 (회귀)
- 데이터 로드 및 인코딩: CP949 인코딩 CSV 로드, 컬럼명 정리
- EDA: 온도, 풍속, 가시거리, 시간과 대여 수 관계 시각화, 연·월별 수요 추세 확인
- 전처리:
TimeOfDay구간화, 날짜 제거, 원-핫 인코딩, 상관분석 및 VIF로 다중공선성 점검 - 모델링: 결정 트리, 선형 회귀, 튜닝된 결정 트리, 랜덤 포레스트 회귀 비교
- 결과: 랜덤 포레스트가 가장 낮은 RMSE로 가장 좋은 성능을 보였으며, 중요 특성을 통해 어떤 요인이 수요에 큰 영향을 주는지 해석 가능
호텔 예약 취소 예측 (이진 분류)
- 데이터 로드 및 이상치 처리:
lead_time의 IQR 기반 이상치 제거 - 파생변수:
people,total_nights,season,expected_room_type,cancel_rate,agent_freq,company_freq등 도메인 의미를 담은 피처 생성 - 전처리: 결측치 처리, 불필요한 컬럼 제거, 원-핫 인코딩
- 로지스틱 회귀: 스케일링 + 규제 기반 베이스라인 모델, 혼동 행렬과 Precision/Recall/F1로 성능 분석
- 랜덤 포레스트 분류: 더 복잡한 모델로 정확도·ROC AUC 개선, 확률 기반 의사결정 가능
- 교차 검증: K-Fold로 교차 검증을 수행해, 단일 train/test split에 의존하지 않는 안정적인 성능 평가 수행
결론
- 서울 자전거 수요 예측에서는 회귀 문제를 통해 결정 트리와 랜덤 포레스트의 차이, VIF·상관계수를 활용한 피처 선택의 중요성을 경험했다.
- 호텔 예약 취소 예측에서는 로지스틱 회귀와 랜덤 포레스트를 비교하면서, 분류 문제에서의 정밀도·재현율·ROC AUC, 임계값 조정, 교차 검증의 개념까지 한 번에 다뤘다.
- 두 실습을 통해 실제 데이터 전처리 → 파생변수 생성 → 모델 학습 → 평가 지표 해석 → 교차 검증까지 머신러닝 프로젝트의 전 과정을 “수요 예측”이라는 공통 주제로 경험할 수 있다.
참고 자료
'AI·머신러닝 > 딥러닝·비전' 카테고리의 다른 글
| Multi-class Weather 이미지 분류 실습 - ImageFolder, DataLoader, 완전연결 신경망 (0) | 2025.12.22 |
|---|---|
| 논리 회귀부터 손글씨 숫자·퍼셉트론까지 - 분류, DataLoader, 데이터 증강, MLP 실습 (0) | 2025.12.20 |
| 파이토치로 시작하는 딥러닝 - 텐서, 선형 회귀, 논리 회귀 실습 (0) | 2025.12.20 |
| 슈퍼스토어 마케팅 캠페인과 K-Means 클러스터링 - 고객 세그먼트 실습 (0) | 2025.12.17 |
| 머신러닝 완전 정복 - 기초부터 실전까지, 사이킷런으로 시작하기 (0) | 2025.12.12 |