◆ 프로젝트: 캘리포니아 주택 가격 데이터셋을 이용한 머신러닝 프로젝트
이 데이터셋은 1990년 캘리포니아 인구 조사 데이터를 기반으로 하며, 진행할 주요 단계는 아래와 같다.
- 데이터 준비
- 데이터로부터 인사이트를 얻기 위해 탐색하고 시각화
- 모델 선택하고 훈련
- 모델 미세 튜닝
- 솔루션 제시
- 시스템 론칭, 모니터링, 유지보수
데이터 준비 및 탐색하기
모든 데이터가 들어 있는 CSV 파일인 housing.csv를 압축한 housing.tgz 파일을 내려받는데, 데이터를 수동으로 내려받아 압축을 푸는 대신 이를 위한 함수를 작성하는 것이 일반적으로 낫다. 특히 데이터가 정기적으로 바뀌는 경우에 유용하며, 최근 데이터를 내려받기 위해 이 함수를 사욯아는 짧은 스크립트를 작성할 수 있다.
데이터를 내려받는 일을 자동화하면 여러 기기에 데이터셋을 설치해야 할 때도 편리하다.
from pathlib import Path
import pandas as pd
import tarfile
import urllib.request
def load_housing_data():
tarball_path = Path("datasets/housing.tgz")
if not tarball_path.is_file():
Path("datasets").mkdir(parents=True, exist_ok=True)
url = "https://github.com/ageron/data/raw/main/housing.tgz"
urllib.request.urlretrieve(url, tarball_path)
with tarfile.open(tarball_path) as housing_tarball:
housing_tarball.extractall(path="datasets")
return pd.read_csv(Path("datasets/housing/housing.csv"))
housing = load_housing_data()
housing.head()
info() 메서드는 데이터에 관한 간략한 설명을 보여준다. (전체 행 수, 각 특성의 데이터 타입, 널이 아닌 값의 개수 등)
housing.info()
describe() 메서드는 숫자형 특성의 요약 정보(개수, 평균, 최대값, 최솟값, 분위수 등)를 보여준다.
housing.describe()
히스토그램을 그려 데이터의 형태롤 빠르게 검토해볼 수 있다.
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(12,8))
plt.show()
<확인 사항>
- 중간 소득(median_income) 특성은 US 달러로 표현되어 있지 않고 상한이 15, 하한이 0.5로가 되도록 만듦.
- 중간 주택 연도(housing_median_age)와 중간 주택 가격(median_house_value) 최댓값과 최솟값을 한정함.
- 특성들의 스케일이 서로 많이 다름.
- 많은 히스토그램에서 오른쪽 꼬리가 더 길다. 이런 형태는 일부 머신러닝 알고리즘에서 패턴을 찾기 어렵게 만드므로, 이런 특성들을 좀 더 종 모양의 분포가 되도록 변형 시킬 예정.
테스트 세트 만들기
데이터를 더 깊게 들여다보기 전에 테스트 세트를 따로 떼어놓고, 테스트 세트를 절대 들여다보면 안 된다. 만약 테스트 세트를 들여다본다면 테스트 세트에서 겉으로 드러난 어떤 패턴에 속아 특정 머신러닝 모델을 선택하게 될 수 있으며, 이 테스트 세트로 일반화 오차를 추정하면 매우 낙관적인 추정이 되며 시스템을 론칭했을 때 기대한 성능이 나오지 않을 것이다. 이를 데이터 스누핑 편향이라고 한다.
테스트 세트는 랜덤으로 어떤 샘플을 선택해서 데이터셋의 20% 정도를 떼어놓으면 된다.
또한, 계층적 샘플링을 위해 테스트 세트가 전체 데이터셋에 있는 여러 소득 카테고리를 잘 대표하도록 소득에 대한 카테고리 특성을 pd.cut() 함수를 사용해 카테고리 5개를 가진 소득 카테고리 특성을 만들고 시각화 해본다.
from sklearn.model_selection import train_test_split
import numpy as np
# matplotlib 한글 깨지는 문제 해결
import platform
path = "c:/Windows/Fonts/malgun.ttf"
from matplotlib import font_manager, rc
if platform.system() == 'Windows':
font_name = font_manager.FontProperties(fname=path).get_name()
rc('font', family=font_name)
else:
print('Unknon system')
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
# pd.cout() 함수를 이용해 카테고리 5개를 가진 소득 카테고리 특성 생성
housing['income_cat'] = pd.cut(housing['median_income'],
bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
labels=[1, 2, 3, 4, 5])
housing['income_cat'].value_counts().sort_index().plot.bar(rot=0, grid=True)
plt.xlabel('소득 카테고리')
plt.ylabel('구역 개수')
plt.show()
이제 소득 카테고리를 기반으로 계층적 샘플링을 수행할 준비를 마쳤다.
※ 계층적 샘플링: 모집단의 데이터 분포 비율을 유지하면서 데이터를 샘플링(취득)하는 것
사이킷런은 sklearn.model_selection 패키지 안에 여러 가지 분할기 클래스를 제공하는데, 분할기(spliter)란 데이터셋을 훈련 세트와 테스트 세트로 분할하는 다양한 전략을 구현한 것을 말하며, 모든 분할기는 훈련과 테스트 분할에 대한 반복자를 반환하는 split() 메서드를 가지고 있다.
만약 한 데이터셋으로 각각 다른 10개의 계층 분할을 생성하고자 한다면
from sklearn.model_selection import StratifiedShuffleSplit
splitter = StratifiedShuffleSplit(n_splits=10, test_size=0.2, random_state=42)
strat_splits = []
for train_index, test_index in splitter.split(housing, housing['income_cat']):
strat_train_set_n = housing.iloc[train_index]
strat_test_set_n = housing.iloc[test_index]
strat_splits.append([strat_train_set_n, strat_test_set_n])
# 첫번째 분할 사용법
strat_train_set, strat_test_set = strat_splits[0]
# 테스트 세트에서 소득 카테고리의 비율 살펴보기
strat_test_set['income_cat'].value_counts() / len(strat_test_set)
계층적 샘플링은 자주 사용되기 때문에 하나의 분할이 필요한 경우 train_test_split() 함수와 startify 매개변수를 사용하여 간편하게 만들 수 있다.
strat_train_set, strat_test_set = train_test_split(
housing, test_size=0.2, stratify=housing['income_cat'], random_state=42)
income_cat 특성은 계층 샘플링의 예시 기준 중 하나였으므로 이 열을 삭제하고 데이터를 원래 상태로 되돌린다.
for set_ in (strat_train_set, strat_test_set):
set_.drop('income_cat', axis=1, inplace=True)
데이터 이해를 위한 탐색과 시각화
먼저 전체 훈련 세트에 대해서 다양한 변환을 실험하기 때문에 나중에 되돌릴 수 있도록 원본 데이터의 복사본을 만든다.
housing = strat_train_set.copy()
[지리적 데이터 시각화하기]
데이터셋에 포함된 지리 정보를 가지고 모든 구역을 산점도로 만들어 데이터를 시각화 해본다.
housing.plot(kind='scatter', x='longitude', y='latitude', grid=True)
plt.xlabel('경도')
plt.ylabel('위도')
# 마이너스 기호 문제 해결하기
plt.rcParams['axes.unicode_minus'] = False
plt.show()
데이터가 밀집된 영역을 보여주기 위해 alpha 옵션 적용
housing.plot(kind='scatter', x='longitude', y='latitude', grid=True, alpha=0.2)
plt.xlabel('경도')
plt.ylabel('위도')
# 마이너스 기호 문제 해결하기
plt.rcParams['axes.unicode_minus'] = False
plt.show()
여기에 주택 가격을 표시하기 위한 코드를 작성해본다.
housing.plot(
kind='scatter',
x='longitude',
y='latitude',
grid=True,
s=housing['population'] / 100, # 원의 크기: 인구 수를 나타냄
c='median_house_value',
cmap='jet',
colorbar=True,
figsize=(10, 7),
sharex=False,
label='population' # 레이블 추가
)
# 색상 막대 레이블 설정
cbar = plt.gcf().get_axes()[1]
cbar.set_ylabel('중간 주택 가격')
# 축 레이블 설정
plt.xlabel('경도')
plt.ylabel('위도')
# 마이너스 기호 문제 해결
plt.rcParams['axes.unicode_minus'] = False
# 레이블 표시
plt.legend()
plt.show()
▶ 빨간색은 높은 가격, 파란색은 낮은 가격, 큰 원은 인구가 밀집된 지역을 나타낸다.
[상관관계 조사하기]
표준 상관계수는 corr() 메서드를 이용해 쉽게 계산할 수 있다.
corr_matrix = housing.corr(numeric_only=True)
# 중간 주택 가격과 다른 특성 사이의 상관관계 보기
corr_matrix['median_house_value'].sort_values(ascending=False)
▶ 중간 주택 가격은 중간 소득(median_income)이 올라갈 때 증가하는 경향 (양의 상관관계)이 있다.
[특성 사이의 상관관계를 확인하는 다른 방법 - 판다스의 scatter_matrix 함수]
scatter_matrix 함수를 사용하면 숫자형 특성 간 산점도가 그려진다. (특성의 제곱 수 만큼. 예 - 특성 11개: 121개의 그래프)
from pandas.plotting import scatter_matrix
attributes = ['median_house_value', 'median_income', 'total_rooms',
'housing_median_age']
scatter_matrix(housing[attributes], figsize=(12,8))
plt.show()
[특성 조합으로 실험하기]
머신러닝 알고리즘용 데이터를 준비하기 전에 마지막으로 할 수 있는 일은 특성을 여러 가지로 조합해보는 것이다.
예. 가구당 방 개수, 가구당 인원 등
housing['rooms_per_house'] = housing['total_rooms'] / housing['households']
housing['bedrooms_ratio'] = housing['total_bedrooms'] / housing['total_rooms']
housing['popluation_per_house'] = housing['population'] / housing['households']
housing.head()
새로 만든 컬럼들을 포함해 상관관계 행렬을 다시 확인해 본다.
corr_matrix = housing.corr(numeric_only=True)
corr_matrix['median_house_value'].sort_values(ascending=False)
▶ 새로 추가한 칼럼 중 'bedrooms_ratio' 특성은 전체 방 개수나 침실 개수보다 중간 주택 가격과의 상관관계가 훨씬 높으며, 이를 통해 침실/방의 비율이 낮은 집이 더 비싼 경향이 있으며, 가구당 방 개수도 구역 내 전체 방 개수보다 더 유용하다는 것을 알 수 있다.
머신러닝 알고리즘을 위한 데이터 준비
[머신러닝 알고리즘을 위한 데이터 준비 작업을 함수를 만들어 자동화해야 하는 이유]
- 어떤 데이터셋에서도 데이터 변환을 손쉽게 반복할 수 있다 (새로운 데이터셋 사용 시 활용 가능)
- 향후 프로젝트에 재사용 가능한 변환 라이브러리를 점진적으로 구축할 수 있다
- 실제 시스템에서 알고리즘에 새 데이터를 주입하기 전에 이 함수를 사용해 변환할 수 있다
- 여러 가지 데이터 변환을 쉽게 시도해볼 수 있고 어떤 조합이 가장 좋은지 확인하는 데 편리하다
원래 훈련 세트로 복원하고, 예측 변수와 타깃값에 같은 변형을 적용하지 않기 위해 예측 변수와 레이블 분리
housing = strat_train_set.drop('median_house_value', axis=1)
housing_labels = strat_train_set['median_house_value'].copy()
[데이터 정제]
Null 값 등의 누락된 특성을 다룰 수 있는 몇 가지 방법
- 해당 구역 제거: dropna() 메서드 이용
- 전체 특성 삭제: drop() 메서드 이용
- 누락된 값 대체 (0, 평균, 중간값 등): fillna() 메서드
# 옵션 1
housing.dropna(subset=['total_bedrooms'], inplace=True)
# 옵션 2
housing.drop('total_bedromms', axis=1, inplace=True)
# 옵션 3
median = housing['total_bedromms'].meidan()
housing['total_bedrooms'].fillna(median, inplace=True)
누락된 값을 대체(옵션 3)하는 게 데이터를 최대한 유지하므로 이를 선택하는 대신, 사이킷런에 있는 편리한 SimpleImputer 클래스를 사용한다. 이 클래스는 각 특성의 중간값을 저장하고 있어 유용하며, 훈련 세트뿐만 아니라 검증 세트와 테스트 그리고 모델에 주입될 새로운 데이터에 있는 누락된 값을 대체할 수 있다.
이를 사용하려면 먼저 누락된 값을 특성의 중간값으로 대체하도록 지정하여 SimpleImputer 객체를 생성한다.
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy='median')
# 중간값은 수치형 특성에서만 계산될 수 있으므로 수치 특성만 가진 데이터 복사본 생성
housing_num = housing.select_dtypes(include=[np.number])
# imputer 객체의 fit() 메서드를 사용해 훈련 데이터에 적용
imputer.fit(housing_num)
# imputer는 각 특성의 중간값을 계산해서 그 결과를 객체의 statistics_ 속성에 저장
imputer.statistics_
# 새로운 데이터에 어떤 값이 누락될지 모르므로 모든 수치형 특성에 imputer 적용
housing_num.median().values
# 학습된 imputer 객체를 사용해 훈련 세트에서 누락된 값을 학습한 중간값으로 변환
X = imputer.transform(housing_num)
누락된 값을 평균(strategy='mean') 이나 자주 등장하는 값(='most_frequent') 또는 상수(='constant', fill_value=..) 로 바꿀 수 있으며, 마지막 두 방법은 수치가 아닌 데이터를 지원한다.
[sklearn.imputer 패키지에서 제공하는 다른 클래스들 (수치 특성에만 적용)]
- KNNImputer: 누락된 값을 이 특성에 대한 k-최근접 이웃의 평균으로 대체하며, 거리는 모든 특성을 바탕으로 계산
- IterativeImputer: 특성마다 회귀 모델을 훈련하여 다른 모든 특성을 기반으로 누락된 값을 예측하고 업데이트된 데이터로 모델을 다시 훈련하고 이 과정을 몇 번 반복하여 모델과 대체 값을 향상
사이킷런 변환기는 판다스 DataFrame이 입력되더라도 넘파이 배열(또는 사이파이 희소 행렬)을 출력하며, X에는 열 이름도 없고 인덱스도 없다. 다행히 X를 DataFrame으로 감싸서 housing_num으로부터 열 이름과 인덱스를 복원하는 것은 어렵지 않다.
housing_tr = pd.DataFrame(X, columns=housing_num.columns,
index = housing_num.index)
housing_tr.head()
[텍스트와 범주형 특성 다루기]
이 데이터셋에는 'ocean_proximity' 특성만 텍스트 형인데,
housing_cat = housing[['ocean_proximity']]
housing_cat.head(8)
이는 임의의 텍스트가 아닌 가능한 값을 제한된 개수로 나열하고 각 값은 카테고리를 나타내므로 범주형 특성에 해당한다. 대부분의 머신러닝 알고리즘은 숫자를 다루므로 이 카테고리를 텍스트에서 숫자로 변환하기 위해 사이킷런의 OrdinalEncoder 클래스를 사용한다.
from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
# 인코딩된 값 확인
housing_cat_encoded[:8]
categories_ 인스턴스 변수를 사용해 카테고리 리스트를 얻을 수 있으며, 범주형 특성마다 ID 카테고리 배열을 담은 리스트가 반환된다.
# categories_ 인스턴스 변수를 사용해 카테고리 리스트 확인
ordinal_encoder.categories_
ocen_proximity 열은 한 특성만 1이고 나머지는 0이므로 사이킷런의 OneHotEncoder 클래스를 이용해 범주의 값을 원-핫 벡터로 변환해야 한다. (예. 카테고리가 1H OCEAN일 때 한 특성이 1이고 나머지는 0, 카테고리가 INLAND 일 때 다른 한 특성이 1이고 나머지가 0이 됨)
from sklearn.preprocessing import OneHotEncoder
cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot
기본적으로 OneHotEncoder의 출력은 사이파이 희소 행렬이다.
희소 행렬은 0이 대부분인 행렬을 매우 효율적으로 표현하는데, 내부적으로 0이 아닌 값과 그 위치만 저장한다.
하나의 범주형 특성에 수백, 수천 개의 카테고리가 있을 때 원-핫 인코딩은 행마다 하나만 1이고 나머지는 0으로 채워진 매우 큰 행렬을 만들고 이런 경우 희소 행렬이 필요하다.
희소 행렬은 많은 메모리를 절약하고 계산 속도를 높여준다. 희소 행렬을 보통의 2D 배열처럼 사용할 수 있지만 밀집된 넘파이 배열로 바꾸려면 toarray() 메서드를 호출해야 한다.
housing_cat_1hot.toarray()
또는 OneHotEncoder를 만들 때 sparse=False 또는 saprse_output=False로 설정하여 transform() 메서드가 일반적인 넘파이 배열을 변환하게 할 수 있다.
# 사이킷런 1.2버전에서 `sparse_output` 매개변수가 추가되었고 `sparse` 매개변수는 1.4버전에서 삭제.
판다스에는 범주형 특성을 원-핫 표현으로 바꿔서 카테고리마다 하나의 이진 특성을 만드는 get_dummies() 함수도 있다.
df_test = pd.DataFrame({'ocean_proximity': ['INLAND', 'NEAR BAY']})
pd.get_dummies(df_test)
get_dummies()는 두 개의 카테고리만 보았기 때문에 두 개열만 출력하는 반면, OneHotEncoder는 학습된 카테고리마다 하나의 열을 순서대로 출력한다. 또한 알 수 없는 카테고리를 담은 데이터프레임을 get_dummies()에 주입하면 아무런 문제없이 변환된 결과를 출력한다.
하지만 OneHotEncoder는 알 수 없는 카테고리를 감지하고 예외를 발생시키며, 원한다면 handle_unknown 매개변수를 'ignore'로 지정하여 알 수 없는 카테고리를 그냥 0으로 나타낼 수 있다.
데이터 프레임을 사용하여 사이킷런 추정기를 훈련할 때 추정기는 열 이름을 feature_names_in_ 속성에 저장하고, 사이킷런은 transform()이나 predict()를 통해 이 추정기에 입력된 모든 데이터프레임이 동일한 열 이름을 갖는지 확인한다.
변환기는 출력으로 DataFrame을 만들 수 있도록 get_feature_names_out() 메서드도 제공한다.
df_output = pd.DataFrame(cat_encoder.transform(df_test_unknown),
columns=cat_encoder.get_feature_names_out(),
index=df_test_unknown.index)
df_output
다음 내용
[머신러닝] 캘리포니아 주택 가격 프로젝트-2
◆ 프로젝트: 캘리포니아 주택 가격 데이터셋을 이용한 머신러닝 프로젝트이 데이터셋은 1990년 캘리포니아 인구 조사 데이터를 기반으로 하며, 진행할 주요 단계는 아래와 같다.데이터 준비데
puppy-foot-it.tistory.com
[출처]
핸즈온 머신러닝
아리엘의 블로그 코딩시작반:티스토리 https://boringariel.tistory.com/30
'[파이썬 Projects] > <파이썬 머신러닝>' 카테고리의 다른 글
[머신러닝] 캘리포니아 주택 가격 프로젝트-2 (0) | 2024.11.08 |
---|---|
[머신러닝] 데이터셋을 구하기 좋은 사이트 모음 (1) | 2024.11.08 |
[머신러닝] 머신러닝의 주요 도전 과제 (2) | 2024.11.07 |
[머신러닝] 추천 시스템: Surprise를 이용한 영화 시스템 구축 (8) | 2024.11.06 |
[머신러닝] 추천 시스템: 파이썬 패키지 Surprise (0) | 2024.11.04 |