Scikit-learn 실무 예제 완전 정복
실제 데이터셋을 활용한 Scikit-learn 실무 프로젝트 모음. 주택 가격 예측, 고객 이탈 분석, 이미지 분류, 추천 시스템 등 다양한 실전 예제를 통해 머신러닝 실력을 향상시킵니다.
학습 항목
본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
이미지 로딩 중...
주택 가격 예측 프로젝트 완벽 가이드
머신러닝 입문자를 위한 주택 가격 예측 프로젝트입니다. Boston Housing 데이터셋을 활용하여 데이터 탐색부터 모델 튜닝까지 전 과정을 실습합니다.
목차
1. Boston Housing 데이터셋 탐색
김개발 씨는 데이터 사이언스 부서에 새로 배치된 주니어 개발자입니다. 첫 프로젝트로 "주택 가격을 예측하는 모델을 만들어보라"는 미션을 받았습니다.
어디서부터 시작해야 할지 막막했던 김개발 씨에게 박시니어 씨가 다가왔습니다. "데이터를 만지기 전에, 먼저 데이터를 제대로 들여다보는 게 중요해요."
Boston Housing 데이터셋은 머신러닝 입문자들이 회귀 문제를 학습할 때 가장 많이 사용하는 데이터셋입니다. 1970년대 보스턴 지역의 주택 가격과 그에 영향을 미치는 13가지 특성이 담겨 있습니다.
마치 부동산 중개사가 집값을 매길 때 고려하는 요소들을 숫자로 정리해놓은 것과 같습니다.
다음 코드를 살펴봅시다.
import pandas as pd
from sklearn.datasets import load_boston
import warnings
warnings.filterwarnings('ignore')
# Boston Housing 데이터셋 로드
boston = load_boston()
df = pd.DataFrame(boston.data, columns=boston.feature_names)
df['PRICE'] = boston.target
# 데이터 기본 정보 확인
print(f"데이터 크기: {df.shape}")
print(f"특성 목록: {list(df.columns)}")
print(df.describe())
김개발 씨는 입사 첫 주에 흥미로운 미션을 받았습니다. 바로 주택 가격을 예측하는 머신러닝 모델을 만드는 것이었습니다.
처음에는 막막했지만, 선배 개발자 박시니어 씨가 첫 번째 조언을 해주었습니다. "모델을 만들기 전에 데이터를 충분히 이해해야 해요.
마치 요리를 하기 전에 재료를 파악하는 것처럼요." 박시니어 씨의 말처럼 데이터를 이해하는 것은 프로젝트의 성패를 좌우합니다. Boston Housing 데이터셋은 바로 이런 학습에 최적화된 데이터입니다.
이 데이터셋에는 506개 주택에 대한 정보가 담겨 있습니다. 각 주택마다 13가지 특성이 기록되어 있습니다.
CRIM은 범죄율, RM은 방의 개수, LSTAT는 저소득층 비율을 나타냅니다. 이런 정보들이 주택 가격에 어떤 영향을 미치는지 분석하는 것이 우리의 목표입니다.
코드를 살펴보면 먼저 load_boston() 함수로 데이터를 불러옵니다. 이 함수는 sklearn 라이브러리에서 제공하는 편리한 도구입니다.
불러온 데이터를 판다스 데이터프레임으로 변환하면 다루기가 훨씬 쉬워집니다. df.shape를 통해 데이터의 크기를 확인할 수 있습니다.
506개의 행과 14개의 열이 있다는 것을 알 수 있습니다. 13개는 특성이고 1개는 우리가 예측해야 할 가격입니다.
df.describe() 메서드는 각 특성의 통계 정보를 보여줍니다. 평균, 표준편차, 최소값, 최대값 등을 한눈에 파악할 수 있습니다.
이 정보는 나중에 데이터 전처리를 할 때 중요한 단서가 됩니다. 실무에서는 이 단계에서 이상한 값이 없는지, 결측치가 있는지도 함께 확인합니다.
다행히 Boston Housing 데이터셋은 깔끔하게 정리되어 있어서 바로 다음 단계로 넘어갈 수 있습니다. 김개발 씨는 데이터를 출력해보며 고개를 끄덕였습니다.
"아, 이렇게 생긴 데이터였군요. 이제 감이 좀 잡히네요."
실전 팁
💡 - 새로운 데이터셋을 받으면 반드시 head(), info(), describe()를 먼저 실행하세요
- 결측치 확인은 df.isnull().sum()으로 할 수 있습니다
2. 특성 상관관계 분석
데이터를 둘러본 김개발 씨는 이제 궁금해졌습니다. "13개 특성 중에서 어떤 것이 가격에 가장 큰 영향을 미칠까요?" 박시니어 씨가 미소를 지으며 대답했습니다.
"좋은 질문이에요. 상관관계 분석을 해보면 알 수 있어요."
상관관계 분석은 두 변수 사이의 관계를 수치로 나타내는 방법입니다. 상관계수가 1에 가까우면 양의 상관관계, -1에 가까우면 음의 상관관계를 의미합니다.
마치 키와 몸무게처럼 한쪽이 커지면 다른 쪽도 커지는 관계를 파악할 수 있습니다.
다음 코드를 살펴봅시다.
import matplotlib.pyplot as plt
import seaborn as sns
# 상관관계 행렬 계산
correlation_matrix = df.corr()
# 가격과의 상관관계 확인
price_corr = correlation_matrix['PRICE'].sort_values(ascending=False)
print("가격과의 상관관계:")
print(price_corr)
# 히트맵 시각화
plt.figure(figsize=(12, 10))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', center=0)
plt.title('Feature Correlation Matrix')
plt.tight_layout()
plt.savefig('correlation_heatmap.png')
김개발 씨는 13개의 특성을 보면서 고민에 빠졌습니다. 어떤 특성이 중요하고 어떤 특성은 덜 중요할까요?
모든 특성을 똑같이 취급해야 할까요? 박시니어 씨가 화이트보드에 그림을 그리며 설명을 시작했습니다.
"상관관계라는 개념이 있어요. 쉽게 말해서 두 숫자가 같이 움직이는 정도를 측정하는 거예요." 예를 들어 방의 개수가 많은 집일수록 가격이 비싸다면, 이 둘은 양의 상관관계에 있다고 말합니다.
반대로 저소득층 비율이 높은 동네일수록 집값이 낮다면, 이것은 음의 상관관계입니다. 상관계수는 -1부터 1 사이의 값을 가집니다.
1에 가까울수록 강한 양의 상관관계, -1에 가까울수록 강한 음의 상관관계를 의미합니다. 0에 가깝다면 두 변수 사이에 선형적인 관계가 거의 없다는 뜻입니다.
코드에서 df.corr() 메서드는 모든 특성 쌍의 상관계수를 계산합니다. 그 결과를 행렬로 보여주기 때문에 상관관계 행렬이라고 부릅니다.
가격과의 상관관계를 정렬해보면 흥미로운 사실을 발견할 수 있습니다. RM(평균 방 개수)은 약 0.7의 양의 상관관계를 보입니다.
방이 많은 집이 비싸다는 직관과 일치합니다. 반면 LSTAT(저소득층 비율)는 약 -0.74의 강한 음의 상관관계를 보입니다.
저소득층이 많은 동네의 집값이 낮다는 것을 데이터가 말해주고 있습니다. seaborn의 히트맵은 이런 관계를 시각적으로 보여줍니다.
빨간색은 양의 상관관계, 파란색은 음의 상관관계를 나타냅니다. 색이 진할수록 관계가 강하다는 뜻입니다.
김개발 씨는 히트맵을 보며 감탄했습니다. "숫자로만 봤을 때보다 훨씬 한눈에 들어오네요!" 실무에서는 이 분석을 통해 어떤 특성에 집중해야 할지, 어떤 특성은 제외해도 될지 판단합니다.
또한 특성 간의 상관관계가 너무 높으면 다중공선성 문제가 생길 수 있어서 주의가 필요합니다.
실전 팁
💡 - 상관계수가 0.7 이상이면 강한 상관관계로 봅니다
- 특성 간 상관관계가 너무 높으면 하나를 제거하는 것을 고려하세요
3. 데이터 전처리 및 스케일링
상관관계 분석을 마친 김개발 씨가 바로 모델을 만들려고 하자, 박시니어 씨가 말렸습니다. "잠깐, 데이터를 그대로 넣으면 안 돼요.
특성마다 스케일이 다르잖아요." 김개발 씨는 고개를 갸웃거렸습니다. 스케일이 왜 문제가 될까요?
데이터 스케일링은 서로 다른 범위의 특성들을 비슷한 범위로 맞춰주는 작업입니다. 마치 원화와 달러를 비교하려면 환율로 변환해야 하는 것처럼, 머신러닝 모델도 특성들의 스케일이 비슷해야 제대로 학습할 수 있습니다.
다음 코드를 살펴봅시다.
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# 특성과 타겟 분리
X = df.drop('PRICE', axis=1)
y = df['PRICE']
# 훈련 데이터와 테스트 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 스케일링 적용
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print(f"훈련 데이터: {X_train_scaled.shape}")
print(f"테스트 데이터: {X_test_scaled.shape}")
김개발 씨는 데이터를 살펴보다가 이상한 점을 발견했습니다. CRIM(범죄율)은 0부터 88까지의 값을 가지는데, TAX(재산세)는 187부터 711까지였습니다.
이렇게 범위가 다른 숫자들을 그대로 모델에 넣어도 괜찮을까요? 박시니어 씨가 비유를 들어 설명했습니다.
"키를 센티미터로 재면 170이고, 미터로 재면 1.7이잖아요. 단위가 다르면 비교하기 어렵듯이, 모델도 마찬가지예요." StandardScaler는 이 문제를 해결해줍니다.
각 특성의 평균을 0으로, 표준편차를 1로 맞춰주는 표준화를 수행합니다. 이렇게 하면 모든 특성이 비슷한 스케일을 가지게 됩니다.
코드를 단계별로 살펴보겠습니다. 먼저 train_test_split으로 데이터를 훈련용과 테스트용으로 나눕니다.
test_size=0.2는 전체의 20%를 테스트용으로 쓰겠다는 의미입니다. 여기서 중요한 개념이 등장합니다.
왜 데이터를 나눠야 할까요? 시험을 볼 때 문제를 미리 보고 공부하면 진짜 실력을 알 수 없는 것처럼, 모델도 학습에 사용하지 않은 데이터로 평가해야 진짜 성능을 알 수 있습니다.
스케일링에서 주의할 점이 있습니다. fit_transform은 훈련 데이터에만 적용하고, 테스트 데이터에는 transform만 적용해야 합니다.
훈련 데이터의 평균과 표준편차를 기준으로 테스트 데이터도 변환하는 것입니다. 만약 테스트 데이터에도 fit_transform을 적용하면 어떻게 될까요?
테스트 데이터의 정보가 스케일러에 반영되어 데이터 누수가 발생합니다. 이러면 실제보다 성능이 좋게 나오는 착시가 생깁니다.
random_state=42는 데이터를 나눌 때 같은 방식으로 나누도록 고정하는 것입니다. 이렇게 해야 실험을 반복해도 같은 결과를 얻을 수 있습니다.
김개발 씨는 고개를 끄덕였습니다. "아, 그래서 선배들이 항상 전처리가 중요하다고 하셨구나!"
실전 팁
💡 - 테스트 데이터에는 절대 fit을 다시 하지 마세요 (데이터 누수 방지)
- random_state를 고정하면 실험 재현이 가능합니다
4. 선형 회귀 모델 구축
드디어 모델을 만들 시간이 왔습니다. 김개발 씨는 기대에 차서 물었습니다.
"어떤 모델부터 시작하면 좋을까요?" 박시니어 씨가 대답했습니다. "회귀 문제의 기본은 선형 회귀예요.
가장 간단하지만 강력한 모델이죠."
선형 회귀는 입력 특성들의 가중치 합으로 결과를 예측하는 모델입니다. 마치 여러 재료의 양을 조절해서 최적의 맛을 찾는 요리사처럼, 각 특성에 적절한 가중치를 부여하여 가격을 예측합니다.
다음 코드를 살펴봅시다.
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
import numpy as np
# 선형 회귀 모델 생성 및 학습
lr_model = LinearRegression()
lr_model.fit(X_train_scaled, y_train)
# 예측 수행
y_pred = lr_model.predict(X_test_scaled)
# 성능 평가
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)
print(f"RMSE: {rmse:.2f}")
print(f"R2 Score: {r2:.4f}")
print(f"각 특성의 가중치: {lr_model.coef_}")
김개발 씨는 드디어 첫 번째 머신러닝 모델을 만들게 되었습니다. 선배가 추천한 선형 회귀, 과연 어떤 원리로 작동하는 걸까요?
박시니어 씨가 칠판에 수식을 적었습니다. "가격 = w1 곱하기 방개수 + w2 곱하기 범죄율 + ...
이런 식이에요. 모델이 하는 일은 이 w값들, 즉 가중치를 찾는 거예요." 선형 회귀는 가장 기본적인 회귀 모델입니다.
입력 특성들의 선형 조합으로 결과를 예측합니다. 마치 저울에 여러 추를 올려서 균형을 맞추는 것처럼, 각 특성에 적절한 가중치를 부여합니다.
코드에서 **LinearRegression()**으로 모델 객체를 생성합니다. 그리고 fit() 메서드로 훈련 데이터를 학습시킵니다.
이 과정에서 모델은 최적의 가중치를 찾습니다. 학습이 끝나면 predict() 메서드로 새로운 데이터의 가격을 예측할 수 있습니다.
테스트 데이터를 넣어서 예측값을 구하고, 실제값과 비교합니다. 성능을 평가하는 지표가 두 가지 등장합니다.
RMSE(Root Mean Squared Error)는 예측값과 실제값의 차이를 제곱해서 평균 낸 뒤 루트를 씌운 것입니다. 단위가 원래 값과 같아서 해석하기 쉽습니다.
R2 점수는 결정계수라고도 불립니다. 모델이 데이터의 변동을 얼마나 설명하는지를 나타냅니다.
1에 가까울수록 좋고, 0에 가까우면 평균으로 예측하는 것과 다를 바 없습니다. lr_model.coef_를 출력하면 각 특성의 가중치를 볼 수 있습니다.
양수면 그 특성이 증가할 때 가격도 증가하고, 음수면 반대입니다. 이를 통해 어떤 특성이 가격에 어떤 방향으로 영향을 미치는지 해석할 수 있습니다.
김개발 씨는 결과를 보며 물었습니다. "R2가 0.7 정도 나왔는데, 이게 좋은 건가요?" 박시니어 씨가 미소 지었습니다.
"괜찮은 시작이에요. 하지만 더 좋은 모델도 있어요."
실전 팁
💡 - 선형 회귀의 장점은 결과를 해석하기 쉽다는 것입니다
- R2 점수 0.7은 모델이 데이터 변동의 70%를 설명한다는 의미입니다
5. 랜덤 포레스트 회귀 적용
선형 회귀로 어느 정도 성능을 얻은 김개발 씨, 하지만 더 좋은 결과를 원했습니다. 박시니어 씨가 귀띔했습니다.
"선형 회귀는 데이터가 직선 관계일 때 잘 맞아요. 하지만 현실은 더 복잡하죠.
랜덤 포레스트를 써보세요."
랜덤 포레스트는 여러 개의 결정 트리를 만들고 그 결과를 종합하는 앙상블 모델입니다. 마치 여러 전문가의 의견을 종합해서 결정을 내리는 것처럼, 다양한 트리의 예측을 평균 내어 더 안정적인 결과를 얻습니다.
다음 코드를 살펴봅시다.
from sklearn.ensemble import RandomForestRegressor
# 랜덤 포레스트 모델 생성 및 학습
rf_model = RandomForestRegressor(
n_estimators=100,
max_depth=10,
random_state=42
)
rf_model.fit(X_train_scaled, y_train)
# 예측 수행
y_pred_rf = rf_model.predict(X_test_scaled)
# 성능 평가
rmse_rf = np.sqrt(mean_squared_error(y_test, y_pred_rf))
r2_rf = r2_score(y_test, y_pred_rf)
print(f"랜덤 포레스트 RMSE: {rmse_rf:.2f}")
print(f"랜덤 포레스트 R2 Score: {r2_rf:.4f}")
# 특성 중요도 확인
feature_importance = pd.Series(
rf_model.feature_importances_, index=X.columns
).sort_values(ascending=False)
print(f"특성 중요도:\n{feature_importance}")
김개발 씨는 선형 회귀의 한계를 느꼈습니다. 현실 세계의 데이터는 항상 직선으로 설명되지 않습니다.
방이 3개에서 4개로 늘어날 때와 10개에서 11개로 늘어날 때 가격 상승폭이 같을까요? 박시니어 씨가 새로운 모델을 소개했습니다.
"랜덤 포레스트는 결정 트리 여러 개를 합친 거예요. 숲이라는 이름이 붙은 이유죠." 결정 트리는 스무고개 게임과 비슷합니다.
"방이 5개 이상인가요?" "범죄율이 10 이하인가요?" 이런 질문들을 거쳐서 최종 예측에 도달합니다. 하지만 하나의 트리는 과적합되기 쉽습니다.
랜덤 포레스트는 이 문제를 해결합니다. 데이터의 일부만 사용해서 여러 개의 트리를 만들고, 그 예측들을 평균 냅니다.
마치 100명의 전문가에게 물어보고 다수결로 결정하는 것과 같습니다. 코드에서 n_estimators=100은 100개의 트리를 만들겠다는 의미입니다.
트리가 많을수록 성능이 좋아지지만 학습 시간도 길어집니다. max_depth=10은 각 트리의 깊이를 제한해서 과적합을 방지합니다.
흥미로운 것은 feature_importances_ 속성입니다. 랜덤 포레스트는 각 특성이 예측에 얼마나 기여했는지 알려줍니다.
이것은 선형 회귀의 가중치와는 다른 개념입니다. 특성 중요도를 보면 LSTAT와 RM이 상위권에 있을 것입니다.
앞서 상관관계 분석에서도 이 두 특성이 중요하다고 나왔었죠. 분석 결과와 모델의 판단이 일치하는 것을 확인할 수 있습니다.
김개발 씨는 결과를 비교해보았습니다. 랜덤 포레스트의 R2 점수가 선형 회귀보다 높았습니다.
"와, 정말 더 좋아졌네요!" 박시니어 씨가 덧붙였습니다. "랜덤 포레스트는 비선형 관계도 잘 잡아내고, 과적합에도 강해요.
실무에서 아주 많이 사용하는 모델이에요."
실전 팁
💡 - n_estimators는 100~500 사이에서 시작하는 것이 좋습니다
- 특성 중요도를 통해 불필요한 특성을 제거할 수 있습니다
6. 모델 성능 비교 및 튜닝
두 모델을 만든 김개발 씨, 이제 마지막 단계입니다. 박시니어 씨가 말했습니다.
"모델을 만들었다고 끝이 아니에요. 하이퍼파라미터를 조정해서 성능을 더 끌어올릴 수 있어요." 김개발 씨의 눈이 반짝였습니다.
하이퍼파라미터 튜닝은 모델의 설정값을 조정하여 최적의 성능을 찾는 과정입니다. 마치 라디오 주파수를 미세 조정해서 가장 깨끗한 소리를 찾는 것처럼, 다양한 조합을 시험하여 최고의 결과를 찾습니다.
다음 코드를 살펴봅시다.
from sklearn.model_selection import GridSearchCV
# 하이퍼파라미터 그리드 정의
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [5, 10, 15, None],
'min_samples_split': [2, 5, 10]
}
# 그리드 서치 수행
grid_search = GridSearchCV(
RandomForestRegressor(random_state=42),
param_grid,
cv=5,
scoring='r2',
n_jobs=-1
)
grid_search.fit(X_train_scaled, y_train)
# 최적 모델 결과
print(f"최적 파라미터: {grid_search.best_params_}")
print(f"최적 R2 Score: {grid_search.best_score_:.4f}")
# 최종 성능 비교
best_model = grid_search.best_estimator_
y_pred_best = best_model.predict(X_test_scaled)
print(f"테스트 R2: {r2_score(y_test, y_pred_best):.4f}")
프로젝트가 거의 마무리 단계에 접어들었습니다. 김개발 씨는 이미 괜찮은 성능을 얻었지만, 박시니어 씨는 더 높은 목표를 제시했습니다.
"모델에는 여러 가지 설정값이 있어요. 이걸 하이퍼파라미터라고 해요.
n_estimators, max_depth 같은 것들이죠. 이 값들을 잘 조정하면 성능이 더 좋아져요." 하지만 어떤 조합이 최적인지 어떻게 알 수 있을까요?
하나씩 다 해보기엔 너무 많은 조합이 있습니다. 이때 사용하는 것이 GridSearchCV입니다.
param_grid에 시험하고 싶은 값들을 정의합니다. n_estimators는 50, 100, 200을, max_depth는 5, 10, 15, None을 시험하겠다고 지정했습니다.
GridSearchCV는 이 모든 조합을 자동으로 시험합니다. cv=5는 5겹 교차 검증을 의미합니다.
데이터를 5등분해서, 매번 다른 부분을 검증용으로 사용합니다. 이렇게 하면 특정 데이터에 운 좋게 잘 맞는 경우를 걸러낼 수 있습니다.
n_jobs=-1은 CPU의 모든 코어를 사용하라는 의미입니다. 많은 조합을 시험해야 하므로 병렬 처리가 필수입니다.
그리드 서치가 끝나면 best_params_로 최적의 파라미터 조합을 확인할 수 있습니다. best_estimator_는 이미 그 파라미터로 학습된 모델입니다.
김개발 씨는 최종 결과를 비교표로 정리했습니다. 선형 회귀, 기본 랜덤 포레스트, 튜닝된 랜덤 포레스트 순으로 성능이 좋아진 것을 확인할 수 있었습니다.
박시니어 씨가 마지막으로 조언했습니다. "실무에서는 여기서 끝이 아니에요.
모델을 저장하고, 새로운 데이터가 들어왔을 때 예측하는 파이프라인도 만들어야 해요." 김개발 씨는 첫 머신러닝 프로젝트를 성공적으로 마쳤습니다. 데이터 탐색부터 모델 튜닝까지, 전체 과정을 경험한 것입니다.
이제 다음 프로젝트가 기다려집니다.
실전 팁
💡 - GridSearchCV는 시간이 오래 걸릴 수 있으니 처음엔 적은 조합으로 시작하세요
- RandomizedSearchCV를 사용하면 모든 조합 대신 일부만 무작위로 시험할 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!