"""
# 변수 설명
1. ID : 집을 구분하는 번호
2. date : 집을 구매한(거래한) 날짜
3. price : 집의 가격(Target variable)
4. bedrooms : 침실의 수
5. bathrooms : 침실 개수 당 화장실의 수(화장실의 수 / 침실의 수 )
    화장실, 세면대, 샤워실, 욕조 : 1
    화장실, 세면대, 샤워실, : 0.75
    화장실, 세면대, : 0.5
6. sqft_living : 주거 공간의 평방 피트(면적) 
    sqft_living = sqft_above + sqftbasement 임.
7. sqft_lot : 부지의 평방 피트(면적)
8. floors : 집의 층 수
    층수에 0.5는 다락방의 의미. 
9. waterfront : 집의 전방에 강이 흐르는지 유무 (a.k.a. 리버뷰)
10. view : 집이 얼마나 좋아 보이는지의 정도 (0~4)
11. condition : 집의 전반적인 상태 (1~5)
12. grade : King County grading 시스템 기준으로 매긴 집의 등급 (1~13)
13. sqft_above : 지하실을 제외한 평방 피트(면적)
14. sqft_basement : 지하실의 평방 피트(면적)
15. yr_built : 지어진 년도
16. yr_renovated : 집을 재건축한 년도
17. zipcode : 우편번호
18. lat : 위도
19. long : 경도
20. sqft_living15 : 2015년 기준 주거 공간의 평방 피트(면적, 집을 재건축했다면, 변화가 있을 수 있음)
    또는 가장 가까운 주변 15개의 평균 주거 공간의 평방 피트
21. sqft_lot15 : 2015년 기준 부지의 평방 피트(면적, 집을 재건축했다면, 변화가 있을 수 있음)
    또는 가장 가까운 주변 15개의 부지의 평균 평방 피트
"""
# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load in 

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

# Input data files are available in the "../input/" directory.
# For example, running this (by clicking run or pressing Shift+Enter) will list the files in the input directory

import os
print(os.listdir("../input"))

# Any results you write to the current directory are saved as output.
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from keras import models
from keras import layers
from keras import optimizers
from keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau, TensorBoard
from keras import backend as K
#%%
debug = False
eda_view = True

num_rows = 2000 if debug else None
train = pd.read_csv('../input/train.csv', nrows = num_rows)
test = pd.read_csv('../input/test.csv', nrows = num_rows)

train.info()
test.info()
"""
다른 분들의 EDA를 많이 참고 했습니다. null값은 없는 걸로 확인하였습니다.
제일 먼저 target feature인 "price"를 살펴보면
"""
if eda_view == True:
    sns.distplot(train['price'])
    plt.show()
    print(train['price'].skew())
    print(train['price'].kurt())
"""
# skew(왜도) 
![skew](https://drive.google.com/file/d/1I-m_9d2TaN-qRZb9NQS7OnEsuEIOH6YH/view?usp=sharing)
왜도는 데이터의 좌우 대칭 정도를 나타내는 수치로 0이면 좌우 대칭임.
왜도 값이 0보다 크면 오른쪽 꼬리 즉 price feature처럼 생김.
왜도 값이 0보다 작으면 왼쪽 꼬리 형태임

모델을 훈련시킬때 왜도가 0에 가까울 수록 좋은 결과를 가져 오기도 합니다.
해서 최대한 정규 분포 형태가 되도록 이렇게 한쪽으로 치우진 feature들은 작업을 하여 줍니다.

# kurt(첨도)
첨도는 분포의 꼬리(양쪽 끝)이 정규 분포대비 어떻게 다른가를 나타냄.
첨도가 양수이면 정규분포보다 꼬리가 두껍다.
첨도가 음수이면 정규분포보다 꼬리가 얇다.
"""
"""
train.info에서 확인한 date feature가 object 형태임을 확인했습니다.
kdeplot을 그려보기 위해서 object 타입을 가공합니다.
"""
#%%
for df in [train, test]:
	df['sale_yr'] = pd.to_numeric(df.date.str.slice(0, 4))
	df['sale_month'] = pd.to_numeric(df.date.str.slice(4, 6))
	df['sale_day'] = pd.to_numeric(df.date.str.slice(6, 8))
	df.drop(['date'], axis=1, inplace=True)
if eda_view == True:
    for c in train:
        sns.kdeplot(train[c])
        plt.show()
"""
연속형 변수 중 왜도가 0에서 많이 벗어나 보이는 feature들은
"""
features = ['sqft_living', 'sqft_lot', 'sqft_above', 'sqft_basement', 'sqft_living15', 'sqft_lot15']
"""
price를 포함하여 왜도 feature들을 왜도가 0에 가까워 지도록 변환하여 줍니다.
"""
for df in [train, test]:
    for c in features:
        df[c] = np.log1p(df[c])

train['price'] = np.log1p(train['price'])
"""
다시 kdeplot으로 분포를 살펴보면 변화된 형태를 확인 했습니다.
"""
if eda_view == True:
    sns.kdeplot(train['price'])
    plt.show()
    for c in features:
        sns.kdeplot(train[c])
        plt.show()
"""
이제 boxplot과 pairplot은 다른 분들의 커널을 참고하여 확인만 하였고
이상치들은 제거 하지 않을 예정입니다.
"""
for df in [train, test]:
	df['total_rooms'] = df['bathrooms'] + df['bedrooms']
	df['total_sqft'] = df['sqft_living'] + df['sqft_lot']
	df['total_sqft15'] = df['sqft_living15'] + df['sqft_lot15']
	df['grade_multi_cond'] = df['grade'] * df['condition']
	# 용적률 주거 공간 / 대지 면적
	df['far1'] = df['sqft_living'] / df['sqft_lot']
	df['far2'] = df['sqft_living15'] / df['sqft_lot15']
    # 용적률이 늘어 났는가?
	df['is_far_incre'] = df['far2'] - df['far1']
	df['is_far_incre'] = df['is_far_incre'].apply(lambda x : 0 if x < 0 else 1)
	
    #건폐률 (토지 면적 대비 1층이 차지하는 비)
	df['far3'] = (df['sqft_above'] / df['floors']) / df['sqft_lot']
	
    # 층당 화장실 개수
	df['bath_per_floors'] = df['bathrooms'] / df['floors']
    
    # 층당 방 수
	df['bed_per_floors'] = df['bedrooms'] / df['floors']
    
    # 지상 거주 공간 중 1층이 차지하는 비
	df['1st_living'] = df['sqft_above'] / df['floors']
	df['is_renovated'] = df['yr_renovated'] - df['yr_built']
	df['is_renovated'] = df['is_renovated'].apply(lambda x: 0 if x < 0 else 1)
	df['age_built'] = df['yr_built'] / 2015
	df['age_reno'] = df['yr_renovated'] / 2015
	# 지하가 있냐 없냐?
	df['is_basement'] = df['sqft_basement'].apply(lambda x: 0 if x == 0 else 1)
    #전체 방수 + 지하실
	df['total_rooms'] = df['total_rooms'] + df['is_basement']
    # 다락이 있냐 없냐?
	df['is_top'] = df['floors'].apply(lambda x: 0 if int(x) == x else 1)
    # 2015년에 주거 공간이 커졌는가?
	df['is_living_wider'] =  df['sqft_living15'] / df['sqft_living']
	df['is_living_wider'] = df['is_living_wider'].apply(lambda x:0 if x < 1 else 1)
    # 2015년 토지 면적이 커녔는가?
	df['is_lot_wider'] =  df['sqft_lot15'] / df['sqft_lot']
	df['is_lot_wider'] = df['is_lot_wider'].apply(lambda x: 0 if x < 1 else 1)
	df['total_grade'] = df['waterfront'] + df['view'] + df['condition'] + df['grade']
"""
feature들을 추가 합니다. 몇가지 다른 커널에서 보지 못한 것들만 설명드리면
1. 용적률, 건폐률 추가 하였고요.
2. 용적률이 2015년 측정후에 늘었났는지 여부를 0, 1로 추가
3. 층당 화장실 수 개념을 추가 했습니다. (이건 개인적인 생각임 1층에 최소 1개의 화장실은 있어야.....급똥을 피하는...)
4. 층당 방수
5. 지상 거주 공간 중 1층이 차지하는 비
6. 2015년 기준 지어진지 얼마나 오래 지났나를 나타내려고 yr_built / 2015 추가함.
7. 리모델링 한 기간 얼마나 되었는지 추가.
8. 지하실이 있냐 없냐를 0, 1로 추가
9. 다락이 있냐 없냐를 0, 1로 추가
10. 거주 공간과 부지면적이 2015년 재 측정후에 커졌는지 작아졌는지 추가.

"""
train['per_price_living'] = train['price']/train['sqft_living']
train['per_price_above'] = train['price']/train['sqft_above']
train['per_price_lot'] = train['price']/train['sqft_lot']

train['per_price_living_grade'] = train['per_price_living'] * train['grade']
train['per_price_above_grade'] = train['per_price_above'] * train['grade']
train['per_price_lot_grade'] = train['per_price_lot'] / train['grade']

zipcode_price_living = train.groupby(['zipcode'])['per_price_living'].agg({'mean','var'}).reset_index()
train = pd.merge(train,zipcode_price_living, how='left',on='zipcode')
test = pd.merge(test,zipcode_price_living, how='left',on='zipcode')

zipcode_price_above = train.groupby(['zipcode'])['per_price_above'].agg({'mean','var'}).reset_index()
train = pd.merge(train,zipcode_price_above,how='left',on='zipcode')
test = pd.merge(test,zipcode_price_above,how='left',on='zipcode')

zipcode_price_lot = train.groupby(['zipcode'])['per_price_lot_grade'].agg({'mean','var'}).reset_index()
train = pd.merge(train,zipcode_price_lot,how='left',on='zipcode')
test = pd.merge(test,zipcode_price_lot,how='left',on='zipcode')

zipcode_price_living_grade = train.groupby(['zipcode'])['per_price_living_grade'].agg({'mean','var'}).reset_index()
train = pd.merge(train,zipcode_price_living_grade, how='left',on='zipcode')
test = pd.merge(test,zipcode_price_living_grade, how='left',on='zipcode')

zipcode_price_above_grade = train.groupby(['zipcode'])['per_price_above_grade'].agg({'mean','var'}).reset_index()
train = pd.merge(train,zipcode_price_above_grade,how='left',on='zipcode')
test = pd.merge(test,zipcode_price_above_grade,how='left',on='zipcode')

zipcode_price_lot_grade = train.groupby(['zipcode'])['per_price_lot_grade'].agg({'mean','var'}).reset_index()
train = pd.merge(train,zipcode_price_lot_grade,how='left',on='zipcode')
test = pd.merge(test,zipcode_price_lot_grade,how='left',on='zipcode')

train.drop(['per_price_living', 'per_price_above', 'per_price_lot', 'per_price_living_grade', 'per_price_above_grade', 'per_price_lot_grade'], axis=1, inplace=True)
"""
한국의 평당 단가의 개념을 추가.
정확한 평당 단가를 구하려면 

주택 가격 = 토지가격 + 건물 가격
토지 가격 = 토지시세 * 토지면적
건물 가격 = 표준건축비 * 건물 연면적 * 감가상각률

이런 것들을 계산 하여야 하는데 주어진 feature들만 가지고는 정확하게 산출이 어려움.
해서 대략적인 계산을 하고, 이렇게 추가된 feature들을 zipcoe feature와 묶어서 추가함.

위, 경도에 따른 특정 지역의 집값이 높다는 건 결국 zipcode중 특정 지역의 집값이 높다로 봐도 될 듯 함.
"""
train.head(10)
train.info()
corrmat = train.corr()
top_corr_features = corrmat.index[abs(corrmat['price']) >= 0.45]
plt.figure(figsize=(12, 12))
hm = sns.heatmap(train[top_corr_features].corr(), annot=True)
plt.show()
"""
feature들을 추가하고 price와 상관관계 0.45 이상인 feature들을 출력해 보았습니다.
"""
x_train = train.drop(['price', 'id'], axis=1)
x_test = test.drop(['id'], axis=1)
y_train = train['price']
y_train = y_train.values.reshape(-1,1)
"""
훈련에 사용할 x_train, y_train. 예측에 사용할 x_test 데이터 분류합니다.

지금은 id를 제외한 모든 feature들은 모델 훈련시 사용할 계획이고, 향후 관련도 적은 feature들은 제외하여 보면서 테스트 예정임.
"""
x_scaler = StandardScaler().fit(x_train)
y_scaler = StandardScaler().fit(y_train)

x_train = x_scaler.transform(x_train)
x_test = x_scaler.transform(x_test)
y_train = y_scaler.transform(y_train)
"""
훈련하기전 feature들을 정규화 하여 줍니다.
"""
def coeff_determination(y_true, y_pred):
    SS_res =  K.sum(K.square( y_true-y_pred ))
    SS_tot = K.sum(K.square( y_true - K.mean(y_true) ) )
    return ( 1 - SS_res/(SS_tot + K.epsilon()) )
"""
r2 공분산 함수를 추가 합니다. keras에서는 없어서.... 하지만 확인하여 보니 꼭 이 함수를 추가해서 볼 필요는 없습니다.
mse와 비례하기 때문에 r2의 수치를 확인하기 위한 용도 입니다.
"""
np.random.seed(2019)
def make_model1():
    model = models.Sequential()
    model.add(layers.Dense(8, activation='elu',	input_dim=x_train.shape[1]))
    model.add(layers.Dense(8, activation='elu'))
    model.add(layers.Dense(1))

    optimizer = optimizers.Adam(lr=0.001)
    model.compile(optimizer= optimizer, loss = 'mse', metrics=['mae', coeff_determination])
    return model
def make_model2():
    model = models.Sequential()
    model.add(layers.Dense(8, activation='elu',	input_dim=x_train.shape[1]))
    model.add(layers.Dense(8, activation='elu'))
    model.add(layers.Dense(1))

    optimizer = optimizers.RMSprop(lr=0.001)
    model.compile(optimizer= optimizer, loss = 'mse', metrics=['mae', coeff_determination])
    return model
"""
훈련 모델을 2개 만들었고 optimizer를 하나는 Adam, 다른 하나는 RMSprop를 사용합니다.
"""
epoch = 200
patient = 20
k = 4
num_val_samples = len(x_train) // k
"""
훈련시 사용할 epochs 수와 훈련 중 종료조건 함수에서 사용할 기준 수를 정합니다.
4폴드 검정에 2개 모델, 각 훈련 별 patient는 20
실제 돌려 보면 대략 각 폴드 마다 100 epoch 정도에서 학습이 종료 됩니다.
"""
model_path = '../model/'
if not os.path.exists(model_path):
    os.mkdir(model_path)

model_path1 = model_path + 'best_model1.hdf5'
model_path2 = model_path + 'best_model2.hdf5'
"""
훈련중 모델 결과를 저장할 디렉터리, 파일명 
"""
callbacks1 = [
    EarlyStopping(monitor='mse', patience=patient, mode='min', verbose=1),
    ModelCheckpoint(filepath=model_path1, monitor='mse', verbose=1, save_best_only=True, mode='min'),
    ReduceLROnPlateau(monitor = 'val_loss', factor = 0.5, patience = patient / 3, min_lr=0.000001, verbose=1, mode='min')
    ]
callbacks2 = [
    EarlyStopping(monitor='mse', patience=patient, mode='min', verbose=1),
    ModelCheckpoint(filepath=model_path2, monitor='min', verbose=1, save_best_only=True, mode='max'),
    ReduceLROnPlateau(monitor = 'val_loss', factor = 0.5, patience = patient / 3, min_lr=0.000001, verbose=1, mode='min')
    ]
"""
훈련에서 사용할 callback 함수들
EarlyStopping은 공분상이 20번 동안 커지지 않으면 학습 종료의 의미.
ModelCheckpoint는 훈련 중 가장 높은 공분산 값이 나온 모델을 따로 저장.
ReduceLROnPlateau는 훈련중 loss가 줄지 않으면(여기서 loss는 mse임) learning rate를 줄여가면서 학습 하도록 설정. 이는 loss값 널뛰기를 막아줍니다.
"""
for i in range(k):
    print('Fold num #', i+1)
    val_data = x_train[i * num_val_samples : (i+1) * num_val_samples]
    val_targets = y_train[i * num_val_samples : (i+1) * num_val_samples]

    partial_train_data = np.concatenate(
        [x_train[:i*num_val_samples],
        x_train[(i+1) * num_val_samples:]],
        axis=0
    )
    partial_train_targets = np.concatenate(
        [y_train[:i*num_val_samples],
        y_train[(i+1) * num_val_samples:]],
        axis=0
    )
    
    model1 = make_model1()
    model2 = make_model2()
    
    history1 = model1.fit(
        partial_train_data, 
	    partial_train_targets,
        validation_data=(val_data, val_targets), 
	    epochs=epoch, 
	    batch_size=16, 
	    callbacks=callbacks1)

    history2 = model2.fit(
        partial_train_data, 
	    partial_train_targets,
        validation_data=(val_data, val_targets), 
	    epochs=epoch, 
	    batch_size=16, 
	    callbacks=callbacks2)
#%%
plt.plot(history1.history['coeff_determination'])
plt.title('1-coeff_determination')
plt.ylabel('coeff_determination')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
# summarize history for loss
plt.plot(history1.history['val_coeff_determination'])
plt.title('1-val_coeff_determination')
plt.ylabel('val_coeff_determination')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

#%%
plt.plot(history2.history['coeff_determination'])
plt.title('2-coeff_determination')
plt.ylabel('coeff_determination')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
# summarize history for loss
plt.plot(history2.history['val_coeff_determination'])
plt.title('2-val_coeff_determination')
plt.ylabel('val_coeff_determination')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
"""
훈련 결과를 그래프로 요약하여 봅니다.
훈련 데이터와 검증 데이터 모두에서 안정적인 결과치를 보여 주는 것으로 판단했습니다.
"""
best_model1 = make_model1()
best_model1.load_weights('../model/best_model1.hdf5')
best_model1.fit(
   	x_train, 
	y_train, 
	epochs=epoch, 
	batch_size=16, 
	shuffle=True, 
	validation_split=0.2,
    callbacks=callbacks1
    )

best_model2 = make_model2()
best_model2.load_weights('../model/best_model2.hdf5')
best_model2.fit(
   	x_train, 
	y_train, 
	epochs=epoch, 
	batch_size=16, 
	shuffle=True, 
	validation_split=0.2,
    callbacks=callbacks2
    )
"""
이제 전체 데이터를 가지고 훈련 시켜봅니다.
훈련시 4 fold로 검증하였던 결과를 불러들여 적용합니다.

전체 데이터로 훈련시 자동으로 0.2 는 다시 검증용으로 하고, 훈련때마다 자동 shuffle을 적용합니다.
(이런건 keras가 참 편합니다.)
"""
y_preds_best1 = best_model1.predict(x_test)
inv_y_preds_best1 =  np.expm1(y_scaler.inverse_transform(y_preds_best1))

y_preds_best2 = best_model2.predict(x_test)
inv_y_preds_best2 =  np.expm1(y_scaler.inverse_transform(y_preds_best2))

avg_pred = ( inv_y_preds_best1 + inv_y_preds_best2) / 2
avg_pred = avg_pred.astype(int)
"""
이제 test 데이터로 집값을 예측하여 봅니다.
여기서 주의 하실 건
앞에서 log1p, standscale 하였으니 역으로 다시 환원하여야 합니다.
np.expm1과 inverse_transform 이 그에 해당 합니다.

두 모델의 평균이니 앙상블입니다.

제가 안해 봤지만 여기다가 xgb등을 사용하면 점수가 많이 올라 갈 것으로 보입니다.
"""
if debug == False:
    sub = pd.read_csv('../input/sample_submission.csv')
    sub['price'] = avg_pred
    sub.to_csv('./neural_avg.csv', index=False)