핸즈온머신러닝&딥러닝

캘리포니아 주택 가격 예측

threegopark 2021. 5. 15. 17:01
728x90

1. 데이터

  • 주어진 데이터 : Sample + label -> 지도 학습
  • 주택 가격이라는 특정 값 예측 -> 회귀 문제
  • 예측에 사용할 특성 다수(인구 수, 중간 소득 등) -> 다중 회귀
  • 각 구역마다 하나의 값 예측(주택 가격 하나만을 예측) -> 단변량 회귀                                                                                                 <==>    구역마다 여러 값을 예측한다면 다변량 회귀
  • 데이터가 상대적으로 작음 -> 일반적인 배치 학습                                                                                                                                              <==>    데이터 매우 크면 맵리듀스 기술 활용

 

2. 성능 측정 지표 선택

  • 회귀 문제의 전형적인 성능 지표 -> 평균 제곱근 오차 (root mean square error, RMSE)
  • RMSE -> 오차가 커질수록 RMSE가 더욱 커지므로 예측에 대한 오류의 크기를 확인할 수 있다.
  • RMSE(X,h) -> 가설 h를 사용하여 일련의 샘플을 평가하는 비용함수 (loss function)

  • 이상치로 보이는 구간이 많은 경우 -> 평균 절대 오차 (MAE) 고려

 

3. 데이터 확인

import os
import tarfile
import urllib

DOWNLOAD_ROOT = "다운받고자 하는 파일의 URL주소/"
HOUSING_PATH = os.path.join("datasets", "housing")
HOUSING_URL = DOWNLOAD_ROOT + "datasets/housing/housing/tgz"

#자동화 --> 편리함
def fetch_housing_data(housing_url = HOUSING_URL, housing_path = HOUSING_PATH):
    os.makedirs(housing_path, exist_ok = True)
    tgz_path = os.path.join(housing_path,"housing.tgz")
    urllib.request.urlretrieve(housing_url, tgz_path)
    housing_tgz = tarfile.open(tgz_path)
    housing_tgz.extractall(path=housing_path)
    housing_tgz.close()
    
fetch_housing_data()

import pandas as pd

def load_housing_data(housing_path = HOUSING_PATH):
    csv_path = os.path.join(housing_path, "housing_csv")
    return pd.read_csv(csv_path)
    
housing = load_housing_data()
housing.head()

총 10개의 특성

 

  • info(), describe() 등을 활용하여 데이터 확인
housing.info()

 

  • object 형인 ocean_proximity에 어떤 종류의 객체가 존재하는지 확인
housing["ocean_proximity"].value_counts()

 

  • 히스토그램을 활용하여 데이터 확인 ( describe() 함수의 직관화)
import matplotlib.pyplot as plt

%matplotlib inline

housing.hist(bins=50, figsize(20,15))
plt.show()

 

 

hist()를 통해 알 수 있는 점

 

   1. 중간 소득(median income) 의 x축이 2,4,6,8,10 ... 의 형식 --> 2는 2만달러를 의미

   2. 중간 주택 연도(housing median age) & 중간 주택 가격(median house value) 최댓값 한정시킴 --> 해당 구역 제거 or 구역 이외의 데이터도 수집

   3. 특성들의 스케일이 다름

   4. 대부분의 히스토그램의 꼬리가 두꺼움 --> 패턴 찾기 어려우므로 정규화를 통해 종 모양으로 변경할 필요 있음

 

 

  • 테스트 세트 만들기

   1. 테스트 세트를 활용하게 되면 "데이터 스누핑"으로 인해 낮은 성능 발생

   2. 과대적합

   3. 위의 문제를 해결하기 위해 테스트 세트는 절대 들여다보지 않는다.

 

 

방법 1

import numpy as np

def split_train_test(data, test_ratio):

    shuffled_indices = np.random.permutation(len(data))
    test_set_size = int(len(data) * test_ratio)
    test_indicies = shuffled_indices[:test_set_size]
    train_indicies = shuffled_indices[test_set_size:]
    return data.iloc[train_indicies], data.iloc[test_indicies]
    
train_split, test_split = split_train_test(housing, 0.2)

데이터를 무작위 셔플 후 8:2 비율로 split

--> 문제점 : 코드 실행마다 무작위 셔플되므로 결국 테스트 데이터를 모두 훈련하게 되어 스누핑 편향 발생

 

 

 

해결 : 방법 2

np.random.seed(42)

np.random.permutation()
...

랜덤 시드 (시드 넘버는 사용자 마음) 적용, --> 항상 같은 난수 인덱스가 생성, (시드 넘버 42에 해당되는 난수에 의해 데이터 분리)

--> 문제점 : 다음번에 업데이트된 데이터셋을 사용하려면 문제가 됨

 

 

 

해결 : 방법 3

샘플의 식별자 사용 (고유 식별자, 변경 불가능한 식별자가 존재한다고 가정)

from zlib import crc32

def test_set_check(identifier, test_ratio):
	
    return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2 ** 32
    
def split_train_test_by_id(data, test_ratio, id_column):
	
    ids = data[id_column]
    in_test_set = ids.apply(lambda id_ : test_set_check(id_, test_ratio))
    return data.loc[~in_test_set], data.loc[in_test_set]
    
housing_with_id = housing.reset_index()
#인덱스를 특성으로 추가, (인덱스 열이 하나 생김)

train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")

#새로 생성된 index 열에 고유 식별자를 추가하는 방식

 

 

해결 : 방법 4

사이킷런의 함수 활용 (방법 3과 매우 유사한 방법)

from sklearn.model_selection import train_test_split

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

 

(+ 위의 방법들은 순수한 무작위 샘플링 방식이다. 만약 데이터셋이 충분히 크지 않다면 샘플링 편향이 발생할 수 있다. 예를 들어 10, 20, 30대 별로 설문 조사를 하기 위해서는 전화번호부에서 무작위로 1000개의 샘플을 뽑지 말아야한다. 10대에서 몇 명, 20대에서 몇 명, 30대에서 몇 명... 이렇게 계층적 샘플링이 이뤄져야한다. 뿐만 아니라 각 샘플들은 다른 샘플들을 잘 '대표'해야 한다.)

 

 

+ 계층적인 샘플링을 수행할 경우 (소득 카테고리 기반으로 각 계층을 cut함)

housing["income_cut"] = pd.cut(housing["median_income"], bins=[0.,1.5,3.0,4.5,6., np.inf], labels=[1,2,3,4,5])

housing["income_cut"].hist()

--> 각 카테고리에는 충분한 샘플의 수가 존재해야하므로 너무 세세하게 나누면 안된다.

 

위에서 만든 housing["income_cut"] 을 활용, 계층 샘플링을 한다.

from sklearn.model_selection import StratifiedShuffleSplit
#k-fold 계층 샘플링 + shuffledSplit 랜덤 샘플링을 합친 것

split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in split.split(housing, housing["income_cut"]):
	
    strat_train_set = housing.loc[train_index]
    strat_test_set = housing.loc[test_index]

(income_cut으로 계층을 나눈 후 --> 각 계층 별 train, test 샘플링 실행)

 

 

 

4. 시각화 & 데이터 탐색

 

  • 산점도를 활용한 데이터 시각화
housing = strat_train_set.copy()

housing.plot(kind="scatter", x="longitude", y="latitude")

housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)
#밀집도 확인 가능

 

(+ 특정 패턴을 얻기 위해 alpha 옵션 사용)

 

(+산점도의 활용 : 원의 반지름과 색상을 활용하여 구역의 인구 & 가격 시각화)

housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4, s=housing["population"]/100,
        label="poplulation", figsize=(10,7),
        c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True,)
#s = 원의 반지름, 인구
#c = 색상, 가격
#jet = 미리 정의된 컬러 맵, 낮은 가격(파랑)~높은 가격(빨강)
plt.legend()

(+ 바다와 가까운 곳 --> 인구 밀집 --> 군집성을 띄고 있음!!)

 

 

  • 상관관계 조사 (주택 가격 기준)
corr_matrix = housing.corr()

corr_matrix["median_house_value"].sort_values(ascending=False)

(+ -1에 가까우면 강한 음의 상관관계, +1에 가까우면 강한 양의 상관관계, 상관계수는 선형적인 상관관계만 측정하므로 비선형적인 관계는 확인할 수 없다.)

 

  • 산점도를 활용하여 상관관계 확인 (특성이 많으므로 몇 개의 특성만 확인함)
from pandas.plotting import scatter_matrix

attributes = ["median_house_value", "median_income", "total_rooms", "housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12,8))

(+ 각 변수들의 상관관계를 직관적으로 확인할 수 있다. ex. median_income와 median_house_value 은 선형적인 관계를 띄고 있다.)

 

  • 변수를 추가하여 상관관계를 파악하는 방법
housing["rooms_per_household"] = housing["total_rooms"] / housing["households"]
housing["bedrooms_per_room"] = housing["total_bedrooms"] / housing["total_rooms"]
housing["population_per_household"] = housing["population"] / housing["households"]

corr_matrix = housing.corr()

corr_matrix["median_house_value"].sort_values(ascending=False)

 

 

4. 머신러닝 알고리즘을 위한 데이터 준비

 

데이터 준비 자동화의 필요성

  • 어떤 데이터셋에 대해서도 데이터 변환이 쉬움
  • 향후 프로젝트에 사용할 수 있는 변환 라이브러리 점진적으로 구축
  • 실제 시스템에서도 사용 가능
  • 여러 가지 데이터 변환을 쉽게 시도할 수 있음 --> 최적의 조합 발견이 용이함
housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()

#훈련 데이터와 레이블 데이터 분리

 

 

dropna(), fillna(), drop() 를 활용한 이상치, 결측치 제거

 

housing.dropna(subset=["total_bedrooms"]) #옵션 1
housing.drop("total_bedrooms", axis=1) #옵션 2
median = housing["total_bedrooms"].median() #옵션 3
housing["total_bedrooms"].fillna(median, inplace=True)

 

 

사이킷런의 SimpleImputer을 활용한 누락치 제거

 

위의 옵션 3을 쉽게 구현 가능.

from sklearn.impute import SimpleImputer

imputer = SimpleImputer(strategy="median")

 

중간값이 수치형 특성에서만 계산 가능하므로 object형인 ocean_proximity를 제외한 복사본 생성

housing_num = housing.drop("ocean_proximity", axis=1)

imputer.fit(housing_num)

 

imputer는 각 특성의 중간 값을 계산해서 그 결과를 객체의 statistics_ 속성에 저장함

imputer.statistics_

학습된 imputer 객체 (중간값이 저장된) 를 사용해 훈련 세트에서 누락된 값을 학습한 중간값으로 변경

X = imputer.transform(housing_num)

housing_tr = pd.DataFrame(X, columns = housing_num.columns, index = housing_num.index)

housing_tr

 

 

텍스트와 범주형 데이터 핸들링

 

housing["ocean_proximity"] 는 object형 데이터

housing_cat = housing[["ocean_proximity"]]
housing_cat.head(10)

텍스트를 "벡터"의 형태로 변경하여 수치형 취급한다.

 

 

방법 1 : (5개의 범주를 각각 0,1,2,3,4 의 숫자형태로 변환)

from sklearn.preprocessing import OrdinalEncoder

ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:10]

--> 문제점 : 이 방법은 0 : 매우 안 좋음, 1,2,3, 4 : 매우 좋음  과도 같은 순서형 데이터에서 사용된다. 즉, 해당 예제는 명목형 데이터이므로 이러한 방법은 사용하면 안된다. (위의 방법은 벡터 간의 거리를 중요하게 생각하기 때문이다.)

 

 

방법 2 : (원-핫 인코딩, 더미변수 생성)

from sklearn.preprocessing import OneHotEncoder

onehot = OneHotEncoder()
housing_cat_onehot = onehot.fit_transform(housing_cat)

housing_cat_onehot.toarray()

--> 각 카테고리를 임베딩이라 부르는 학습 가능한 저차원 벡터로 만든다.

 

 

 

사용자 정의 변환기 제작

  • 사이킷 런 : 덕 타이핑 지원 (객체의 속성 or 메서드가 객체의 유형을 결정)
  • fit(), transform(), fit_transform() 등 사용자 정의 변환기 제작이 가능함
  • 파이썬에서 이름에 Maxin 이 존재하면 객체의 기능을 확장하려는 목적으로 만들어진 클래스를 나타낸다. 예를 들어 TransformerMaxin 은 fit_transform() 메서드를 상속받아 해당 기능을 수행할 수 있으며 TransformMaxin을 상속받는 모든 파이썬 클래스에 fit_transform()을 제공한다.
  • BaseEstimator를 상속하면 (동시에 생성자에 *args나 **kwargs를 사용하지 않으면) 하이퍼파라미터 튜닝에 필요한 두 메서드 (get_params()와 set_params()) 를 추가로 얻을 수 있다.

(+참고,    *args : 어떤 값을 넣을지는 모르겠는데 값을 넣으면 함수에 변수가 튜플 형태로 입력된다.

             **kwargs : 어떤 값을 넣을지는 모르겠는데 값을 넣으면 변수가 딕셔너리 형태로 입력된다.)

 

 

--다음 포스팅에 계속--