"""Load the Boston dataset and examine its target (label) distribution."""
# (c) 2015 Patrick Martin and Udacity
# MIT License

# Load libraries
import numpy as np
import pylab as pl
import sklearn as skl
from sklearn import datasets
from sklearn.tree import DecisionTreeRegressor

################################
### ADD EXTRA LIBRARIES HERE ###
################################
from sklearn.grid_search import GridSearchCV

def load_data():
    """Load the Boston dataset."""

    boston = datasets.load_boston()
    return boston


def explore_city_data(city_data):
    """Calculate the Boston housing statistics."""

    # Get the labels and features from the housing data
    housing_prices = city_data.target
    housing_features = city_data.data

    ###################################
    ### Step 1. YOUR CODE GOES HERE ###
    ###################################

    # Please calculate the following values using the Numpy library
    # Size of data (number of houses)?
    num_houses = housing_prices.size
    print "# houses = " + str(num_houses)
    # Number of features?
    num_features = housing_features[1].size
    print "# house features = " + str(num_features)
    # Minimum price?
    min_house_price = np.amin(housing_prices)
    print "min home price = " + str(min_house_price)
    # Maximum price?
    max_house_price = np.amax(housing_prices)
    print "max home price = " + str(max_house_price)
    # Calculate mean price?
    mean_home_price = np.mean(housing_prices)
    print "mean home price = " + str(mean_home_price)
    # Calculate median price?
    median_home_price = np.median(housing_prices)
    print "median home price = " + str(median_home_price)
    # Calculate standard deviation?
    std_dev = np.std(housing_prices)
    print "std dev prices = " + str(std_dev)

def performance_metric(label, prediction):
    """Calculate and return the appropriate error performance metric."""

    ###################################
    ### Step 2. YOUR CODE GOES HERE ###
    ###################################
    # Perform a mean square error metric - want to penalize outliers.
    return skl.metrics.mean_squared_error(label, prediction)


def split_data(city_data):
    """Randomly shuffle the sample set. Divide it into 70 percent training and 30 percent testing data."""

    # Get the features and labels from the Boston housing data
    X, y = city_data.data, city_data.target

    ###################################
    ### Step 3. YOUR CODE GOES HERE ###
    ###################################
    X_train, X_test, y_train, y_test = skl.cross_validation.train_test_split(X, y, test_size=0.3, random_state=42)

    return X_train, X_test, y_train, y_test


def learning_curve(depth, X_train, y_train, X_test, y_test):
    """Calculate the performance of the model after a set of training data."""

    # We will vary the training set size so that we have 50 different sizes
    sizes = np.linspace(1, len(X_train), 50)
    train_err = np.zeros(len(sizes))
    test_err = np.zeros(len(sizes))

    print "Decision Tree with Max Depth: "
    print depth

    for i, s in enumerate(sizes):
        # Create and fit the decision tree regressor model
        regressor = DecisionTreeRegressor(max_depth=depth)
        regressor.fit(X_train[:s], y_train[:s])
        # Find the performance on the training and testing set
        train_err[i] = performance_metric(y_train[:s], regressor.predict(X_train[:s]))
        test_err[i] = performance_metric(y_test, regressor.predict(X_test))

    # Plot learning curve graph
    learning_curve_graph(sizes, train_err, test_err)


def learning_curve_graph(sizes, train_err, test_err):
    """Plot training and test error as a function of the training size."""

    pl.figure()
    pl.title('Decision Trees: Performance vs Training Size')
    pl.plot(sizes, test_err, lw=2, label = 'test error')
    pl.plot(sizes, train_err, lw=2, label = 'training error')
    pl.legend()
    pl.xlabel('Training Size')
    pl.ylabel('Error')
    pl.show()


def model_complexity(X_train, y_train, X_test, y_test):
    """Calculate the performance of the model as model complexity increases."""

    print "Model Complexity: "

    # We will vary the depth of decision trees from 2 to 25
    max_depth = np.arange(1, 25)
    train_err = np.zeros(len(max_depth))
    test_err = np.zeros(len(max_depth))

    for i, d in enumerate(max_depth):
        # Setup a Decision Tree Regressor so that it learns a tree with depth d
        regressor = DecisionTreeRegressor(max_depth=d)

        # Fit the learner to the training data
        regressor.fit(X_train, y_train)

        # Find the performance on the training set
        train_err[i] = performance_metric(y_train, regressor.predict(X_train))

        # Find the performance on the testing set
        test_err[i] = performance_metric(y_test, regressor.predict(X_test))

    # Plot the model complexity graph
    model_complexity_graph(max_depth, train_err, test_err)


def model_complexity_graph(max_depth, train_err, test_err):
    """Plot training and test error as a function of the depth of the decision tree learn."""

    pl.figure()
    pl.title('Decision Trees: Performance vs Max Depth')
    pl.plot(max_depth, test_err, lw=2, label = 'test error')
    pl.plot(max_depth, train_err, lw=2, label = 'training error')
    pl.legend()
    pl.xlabel('Max Depth')
    pl.ylabel('Error')
    pl.show()


def fit_predict_model(city_data):
    """Find and tune the optimal model. Make a prediction on housing data."""

    # Get the features and labels from the Boston housing data
    X, y = city_data.data, city_data.target

    # Setup a Decision Tree Regressor
    dtr = DecisionTreeRegressor()
    # print "DTR parameters"
    # print dtr.get_params()

    dtr_parameters = {'max_depth':(1,2,3,4,5,6,7,8,9,10)}

    ###################################
    ### Step 4. YOUR CODE GOES HERE ###
    ###################################

    # 1. Find the best performance metric
    # Referenced users guide for scoring.
    mse_scorer = skl.metrics.make_scorer(skl.metrics.mean_squared_error, greater_is_better=False)

    # 2. Use gridsearch to fine tune the Decision Tree Regressor and find the best model
    # Referenced lecture and the GridSearch Users Guide docs.
    tuned_dtr = GridSearchCV(dtr, dtr_parameters, mse_scorer)

    # Fit the learner to the training data
    print "Final Model: "
    print tuned_dtr.fit(X, y)
    print "Tuned DTR depth = " + str(tuned_dtr.best_estimator_.max_depth)

    # Use the model to predict the output of a particular sample
    x = [11.95, 0.00, 18.100, 0, 0.6590, 5.6090, 90.00, 1.385, 24, 680.0, 20.20, 332.09, 12.13]
    y = tuned_dtr.predict(x)
    print "House: " + str(x)
    print "Prediction: " + str(y)


def main():
    """Analyze the Boston housing data. Evaluate and validate the
    performanance of a Decision Tree regressor on the housing data.
    Fine tune the model to make prediction on unseen data."""

    print "Starting!"
    # Load data
    city_data = load_data()

    # Explore the data
    explore_city_data(city_data)

    # Training/Test dataset split
    X_train, X_test, y_train, y_test = split_data(city_data)
    # print "X train shape = " + str(X_train.shape)
    # print "X test shape = " + str(X_test.shape)
    # print "y train shape = " + str(y_train.shape)
    # print "y test shape = " + str(y_test.shape)

    # Learning Curve Graphs
    # max_depths = [1,2,3,4,5,6,7,8,9,10]
    # for max_depth in max_depths:
    #     learning_curve(max_depth, X_train, y_train, X_test, y_test)

    # Model Complexity Graph
    # model_complexity(X_train, y_train, X_test, y_test)

    # Tune and predict Model
    # For report writing...
    # idxs = [1,2,3,4,5]
    # for i in idxs:
    fit_predict_model(city_data)


if __name__ == "__main__":
    main()
