# ## Download data from the web

import os
import tarfile
import urllib

DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml2/master/"
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 data from the web and extract .csv file
fetch_housing_data()

# ## Analyze 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)


# peak top 5 samples from the dataset
housing = load_housing_data()
housing.head()

# see type informations of dataset
housing.info()

# check how many values that the feature 'ocean_proximity' can take
housing['ocean_proximity'].value_counts()

# see statistic characteristics of each property (or feature maybe)
housing.describe()

# draw histograms describing each features
# %matplotlib inline
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
plt.show()

# 위의 히스토그램을 관찰하다보니 이상한 점들을 몇 가지 발견할 수 있었다:  
#
# 1. 데이터들의 median income (중간 소득) 값이 미국 달러 단위로 표기되어 있지 않다. 확인해본 결과 0.5에서 1.5 사이의 값을 갖도록 scaling이 된 값이라고 한다. 예를 들어, 3은 30000으로 변환된다.
# 2. housing median age, median house value 역시 특정 구간 내의 값만 갖도록 조정이 되었다. 그 결과 히스토그램의 오른쪽 끝에 많은 수의 데이터가 몰려 있는 현상이 나타났다.
# 3. 각 특성들의 scaling 정도가 다르다.
# 4. 많은 데이터들의 오른쪽 꼬리가 길다 (영어로는 right-skewed). 이 비대칭성은 학습에 안 좋은 영향을 미칠 수 있으므로 학습 이전에 데이터를 정규 분포와 유사하게 만들어주는 과정이 필요할 것으로 보인다. 

# ## Split train/test sets

import numpy as np


# can you guarantee that this function will always generate consistent train/test sets? -> NO
def split_train_set(data, test_ratio):
    assert test_ratio < 1, "test_ratio must be smaller than 1"
    shuffled_indices = np.random.permutation(len(data))
    test_set_size = int(len(data) * test_ratio)
    test_indices = shuffled_indices[:test_set_size]
    train_indices = shuffled_indices[test_set_size:]
    return data.iloc[train_indices], data.iloc[test_indices]


# 하지만 이런 식으로 학습과 테스트 셋을 분리하는 방식은 그리 좋지 않다.  
# 만약 데이터가 업데이트 된다면? 막연히 무작위로 둘을 분리한다면 새로운 데이터가 들어왔을 때 기존의 데이터를 유지하면서  
# 새로운 데이터만 각 셋에 추가시키는 것은 불가능해진다.  
#
# **때문에 우리는 학습 데이터의 index와 같이 각 샘플마다 유일한 값을 갖는 일종의 해시(hash)를 분류 기준으로 데이터를 나누는 방식을 택할 것이다!** 

train_set, test_set = split_train_set(housing, 0.2)

from zlib import crc32


def test_set_check(id, test_ratio):
    return crc32(np.int64(id)) & 0xffffffff < test_ratio * 2 ** 32


def split_train_set_by_id(data, test_ratio, id_col):
    ids = data[id_col]
    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_set_by_id(housing_with_id, 0.2, "index")
# -

# 이로써 학습과 테스트 셋을 잘 나누었고, 향후 데이터가 업데이트 되더라도 기존의 인덱스가 바뀌지 않는 이상 두 셋엔 새로운 데이터만 추가된다고 보장할 수 있다. 이걸로 충분할까?
#
# 그렇지 않다! 학습 셋과 테스트 셋은 모집단의 통계적 특성을 명확히 대표해야 할 필요가 있다. 가령 이런 상황을 생각해보면:
#
# 학습 데이터들은 사람들의 경제적 소득 분위에 따라 고루 나뉜 반면 테스트에 쓰이는 데이터들에 (어떠한 이유로) 중산층의 데이터가 많이 모여있다면 
# 이 테스트 셋은 원래 모집단의 성격을 잘 대변하고 있다고 보기는 어려울 것이다.
#
# 따라서 **우리는 소득 분위를 적절히 나누고, 각 소득 분위가 전체 인구에서 차지하는 비율을 바탕으로, 학습 세트와 테스트 세트 모두에서 동일한 분포를 가지게끔 표본 추출(sampling)을 수행**해야 한다.

# add additional metric to categorize each data according to their income
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"].hist()

housing.head()

# Not only train set, but also test set should well-represent the entire dataset.
#
# However, random sampling sometimes generates wrongly splitted train/test sets.
#
# To prevent us from getting unintended result, we first need to analyze how data is distributed, then use sophiscated sampling methods which creates samples according to their distributions.

# +
from sklearn.model_selection import StratifiedShuffleSplit

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

strat_test_set["income_cat"].value_counts() / len(strat_test_set)

# after generating correct(?) train/test sets, drop the column "income_cat" we created
for set_ in (strat_train_set, strat_test_set):  # 위에서도 쓰였지만 특정 변수명 뒤에 '_'를 붙임으로써 파이썬 키워드와의 충돌을 방지한다
    set_.drop("income_cat", axis=1, inplace=True)

# ## Data Visualization & Exploration

# # copy train set just in case we corrupt it
housing = strat_train_set.copy()

# plot data with respect to their geographical attributes
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)

# plot additional data that might help (population, price of houses)
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4,
        s=housing["population"]/100, label="population", figsize=(10,7),
        c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True,
        sharex=False)
plt.legend()

# investiage standard correlation coeffcient between each features
corr_matrix = housing.corr()
corr_matrix

# however, we cannot fully understand what's going on with just a number    
corr_matrix["median_house_value"].sort_values(ascending=False)

# +
from pandas.plotting import scatter_matrix

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

housing.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.1)

# create useful attributes from the attributes we already have
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)

# ### Refine data to use it for ML

housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()

# +
from sklearn.impute import SimpleImputer

imputer = SimpleImputer(strategy="median")
# -

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

housing_numeric.info()

imputer.fit(housing_numeric)

X = imputer.transform(housing_numeric)
X.shape

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

# examine the only one string type attribute in house dataset
housing_cat = housing[["ocean_proximity"]]
housing_cat[:10]  # you can easily notice that values of each sample is consistent in some ways

from sklearn.preprocessing import OrdinalEncoder

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

# 이러한 표현 방식의 문제는 무엇일까? 
#
# 학습 알고리즘이 인접한 값을 갖는 두 데이터가 연관성이 있다고 볼 가능성이 남아있게 된다.  
# 만약 **bad, normal, good, great 처럼 변량에 순서가 있는 경우**라면 상관이 없지만, 우리의 데이터셋은 이런 경향성을 가지고 있지 않다.
#
# 따라서 이를 대신할 다른 방법이 필요하다.

print(ordinal_encoder.categories_)
print(len(ordinal_encoder.categories_[0]))

from sklearn.preprocessing import OneHotEncoder

cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot

# 사이킷런에 기본적으로 제공되지 않는 추정기/변환기는 어떻게 만들까?
#
# 사이킷런은 duck-typing을 지원하므로 어떤 클래스를 상속할 필요 없이 추정기/변환기라면 갖춰야 하는 메서드 fit, transform을 구현하기만 하면
# 사이킷런에서 natively 지원되는 다른 추정기/변환기와 함께 사용할 수 있다

# +
from sklearn.base import BaseEstimator, TransformerMixin

rooms_idx, bedrooms_idx, population_idx, households_idx = 3, 4, 5, 6

class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
    def __init__(self, add_bedrooms_per_room = True):
        self.add_bedrooms_per_room = add_bedrooms_per_room

    def fit(self, X, y=None):
        return self # Nothing to do
    
    def transform(self, X):
        """
        Input : Pandas Dataframe
        Output : Numpy array
        """
        rooms_per_household = X[:, rooms_idx] / X[:, households_idx]    # broadcasting이 될 거라고 가정한건가...?
        population_per_household = X[:, population_idx] / X[:, households_idx]
        if self.add_bedrooms_per_room:
            bedrooms_per_room = X[:, bedrooms_idx] / X[:, rooms_idx]
            return np.c_[X, rooms_per_household, population_per_household,
                        bedrooms_per_room]
        
        else:
            return np.c_[X, rooms_per_household, population_per_household]


# -

# **Scikit-learn Pipelining**
#
# 앞서 살펴본 바와 같이 데이터들은 일련의 추정기/변환기를 순서대로 거쳐 우리가 알고리즘에 집어넣을 최종 형태로 가공된다.  
# 만약 이들을 매 변환마다 순서대로 호출해야 한다면 코드가 복잡해지는 것은 물론 하이퍼파라미터의 튜닝 역시 어려워질 것이다.  
# 따라서, 이런 수고스러움을 덜어주기 위해 사이킷런은 추정기/변환기 sequence를 하나의 파이프라인으로 묶어서 관리하는 기능을 제공한다.

# +
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

num_pipeline = Pipeline([
    ('median_imputer', SimpleImputer(strategy="median")),
    ('attribs_adder', CombinedAttributesAdder()),
    ('std_scaler', StandardScaler())
])
# -

housing_num_tr = num_pipeline.fit_transform(housing_numeric)

# 하지만 우리가 가지고 있는 데이터 세트에는 숫자형(numeric) 데이터만 있었던 것이 아니다.   
# 어떤 데이터는 문자형인데, 어떻게 하면 서로 다른 타입을 가지고 있는 column들을 한꺼번에 가공해낼 수 있을까?
#
# **사이킷런 0.20에 새로 추가된 ColumnTransformer**를 이용하면 된다! 특정 조건을 만족하는 Column마다 각기 다른 Pipeline을 적용할 수 있다.

from sklearn.compose import ColumnTransformer

# +
num_attribs = list(housing_numeric)
cat_attribs = ['ocean_proximity']

full_pipeline = ColumnTransformer([
    ("numeric", num_pipeline, num_attribs),
    ("categorical", OneHotEncoder(), cat_attribs)
])
# -

housing_prepared = full_pipeline.fit_transform(housing)

# ### Model Selection & Training

from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)

# +
# apply trained model on some data in train set
some_data = housing.iloc[:5]
some_labels = housing_labels.iloc[:5]
some_data_prepared = full_pipeline.transform(some_data)

print("Prediction:", lin_reg.predict(some_data_prepared))

print("Label:", list(some_labels))

# +
# measure error
from sklearn.metrics import mean_squared_error

housing_predictions = lin_reg.predict(housing_prepared)
lin_mse = mean_squared_error(housing_labels, housing_predictions)
lin_rmse = np.sqrt(lin_mse)
lin_rmse # 68629...
# -

# 안 되는 것보다 낫지만 주택 가격이 120,000 달러 ~ 265,000 달러 사이라는 점을 감안하면 터무니 없는 오차이다.

from sklearn.tree import DecisionTreeRegressor

tree_reg = DecisionTreeRegressor()
tree_reg.fit(housing_prepared, housing_labels)

housing_predictions = tree_reg.predict(housing_prepared)
tree_mse = mean_squared_error(housing_labels, housing_predictions)
tree_rmse = np.sqrt(tree_mse)
tree_rmse # 0...???!!!

# **어떻게 오차가 0이 나올 수 있단 말인가??!!**  
#
# 한 가지 가능한 추측은 이 tree 모델이 훈련 세트에 과대 적합되었을 수 있다는 것이다.   
# 이를 검증하기 위해서 앞서 소개했던 train-validation-test 세트 중 validation, 즉 검증 세트를 활용할 것이다.  
#
# 검증 세트는 기본적으로 전체 데이터에서 테스팅에 사용할 데이터를 떼어낸 다음, 남아있는 데이터 세트에서 추출해낸다.  
# 이를 위해 앞서 훈련 세트와 테스트 세트를 분리할 때 사용했던 함수들을 사용하는 방법도 고려해볼 수 있다. **그러나,**  
# 이는 굉장히 많은 코드를 작성해야 하기에 귀찮다. 따라서 우리는 사이킷런이 제공하는 **k-Fold Cross Validation**이라는 방법을 사용할 텐데,
#
# 이 방법은 전체 훈련 세트를 $k$개의 부분 집합으로 나눈 후, 그 중 하나를 검증 세트로 사용하고 나머지 $k-1$개의 세트를 훈련 세트로 사용하며  
# 반복적으로 모델 학습 & 검증을 하는 것이다. 이 과정은 $k$개의 부분 집합 모두에 대해 이루어지므로 총 $k$번의 검증이 이루어진다.~~~~

from sklearn.model_selection import cross_val_score

scores = cross_val_score(tree_reg, housing_prepared, housing_labels, 
                        scoring="neg_mean_squared_error", cv=10)
tree_rmse_scores = np.sqrt(-scores)


def display_scores(scores):
    print("Score:", scores)
    print("Mean:", np.mean(scores))
    print("Standard Deviation:", np.std(scores))


display_scores(tree_rmse_scores)

from sklearn.ensemble import RandomForestRegressor

forest_reg = RandomForestRegressor()
forest_reg.fit(housing_prepared)

StarcoderdataJupyterScriptsDedupFiltered