# data analysis and wrangling
import pandas as pd
import numpy as np
import scipy

# visualization
import matplotlib.pyplot as plt
import seaborn as sns

# machine learning
from sklearn.svm import SVC
from sklearn import preprocessing
import fancyimpute
from sklearn.model_selection import train_test_split
from sklearn.model_selection import RandomizedSearchCV
from sklearn.metrics import classification_report
from sklearn.model_selection import cross_val_score

%matplotlib inline

"""
Explanation: Titanic: Machine Learning from Disaster
An Exploration into the Data using Python
Data Science on the Hill (Michael Hoffman and Charlies Bonfield)
Table of Contents:

Introduction <br/>
Loading/Examining the Data <br/>
All the Features! <br/>
    3a. Extracting Titles from Names <br/>
    3b. Treating Missing Ports of Departure <br/>
    3c. Handling Missing Fares <br/>
    3d. Cabin Number: Relevant or Not? <br/>
    3e. Quick Fixes <br/>
    3f. Imputing Missing Ages <br/>
Prediction <br/>

1. Introduction <a class="anchor" id="first-bullet"></a>
To get familiar with Kaggle competitions we worked on the initial tutorial project. The goal is to predict who onboard the Titanic survived the accident. In our initial analysis, we wanted to see how much the predictions would change when the input data was scaled properly as opposed to unscaled (violating the assumptions of the underlying SVM model). We saw an approximately five percent improvement in accuracy by preprocessing the data properly.
End of explanation
"""


# Load the data. 
training_data = pd.read_csv('train.csv')
test_data = pd.read_csv('test.csv')

# Examine the first few rows of data in the training set. 
training_data.head()

"""
Explanation: 2. Loading/Examining the Data <a class="anchor" id="second-bullet"></a>
End of explanation
"""


# Extract title from names, then assign to one of five classes.
# Function based on code from: https://www.kaggle.com/startupsci/titanic/titanic-data-science-solutions 
def add_title(data):
    data['Title'] = data.Name.str.extract(' ([A-Za-z]+)\.', expand=False)
    data.Title = data.Title.replace(['Lady', 'Countess','Capt', 'Col','Don', 'Dr', 'Major', 
                                     'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare')
    data.Title = data.Title.replace('Mlle', 'Miss')
    data.Title = data.Title.replace('Ms', 'Miss')
    data.Title = data.Title.replace('Mme', 'Mrs')
    
    # Map from strings to numerical variables.
    title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5}
    
    data.Title = data.Title.map(title_mapping)
    data.Title = data.Title.fillna(0)

    return data

"""
Explanation: 3. All the Features! <a class="anchor" id="third-bullet"></a>
We will be extracting the features with custom functions. This isn't necessary for all the features for this project, but we want to leave the possibilty for further development open for the future. 
3a. Extracting Titles from Names <a class="anchor" id="third-first"></a>
While the Name feature itself may not appear to be useful at first glance, we can tease out additional features that may be useful for predicting survival on the Titanic. We will extract a Title from each name, as that carries information about social and marital status (which in turn may relate to survival).
End of explanation
"""


missing_emb_training = training_data[pd.isnull(training_data.Embarked) == True]
missing_emb_test = test_data[pd.isnull(test_data.Embarked) == True]

missing_emb_training.head()

missing_emb_test.head()

"""
Explanation: 3b. Treating Missing Ports of Embarkation <a class="anchor" id="third-second"></a>
Next, let's see if there are any rows that are missing ports of embarkation.
End of explanation
"""


grid = sns.FacetGrid(training_data[training_data.Pclass == 1], col='Embarked', size=2.2, aspect=1.6)
grid.map(plt.hist, 'Fare', alpha=.5, bins=20)
grid.map(plt.axvline, x=80.0, color='red', ls='dashed')
grid.add_legend();

"""
Explanation: We have two passengers in the training set that are missing ports of embarkation, while we are not missing any in the test set. <br>
The features which may allow us to assign a port of embarkation based on the data that we do have are Pclass, Fare, and Cabin. However, since we are missing so much of the Cabin column (more on that later), let's focus in on the other two.
End of explanation
"""


# Recast port of departure as numerical feature. 
def simplify_embark(data):
    # Two missing values, assign Cherbourg as port of departure.
    data.Embarked = data.Embarked.fillna('C')
    
    le = preprocessing.LabelEncoder().fit(data.Embarked)
    data.Embarked = le.transform(data.Embarked)
    
    return data

"""
Explanation: Although Southampton was the most popular port of embarkation, there was a greater fraction of passengers in the first ticket class from Cherbourg who paid 80.00 for their tickets. Therefore, we will assign 'C' to the missing values for Embarked. We will also recast Embarked as a numerical feature.
End of explanation
"""


missing_fare_training = training_data[np.isnan(training_data['Fare'])]
missing_fare_test = test_data[np.isnan(test_data['Fare'])]

missing_fare_training.head()

missing_fare_test.head()

"""
Explanation: 3c. Handling Missing Fares <a class="anchor" id="third-third"></a>
We will perform a similar analysis to see if there are any missing fares.
End of explanation
"""


restricted_training = training_data[(training_data.Pclass == 3) & (training_data.Embarked == 'S')]
restricted_test = test_data[(test_data.Pclass == 3) & (test_data.Embarked == 'S')]
restricted_test = restricted_test[~np.isnan(restricted_test.Fare)] # Leave out poor Mr. Storey
combine = [restricted_training, restricted_test]
combine = pd.concat(combine)

# Find median fare, plot over resulting distribution. 
fare_med = np.median(combine.Fare)

sns.kdeplot(combine.Fare, shade=True)
plt.axvline(fare_med, color='r', ls='dashed', lw='1', label='Median')
plt.legend();

"""
Explanation: This time, the Fare column in the training set is complete, but we are missing that information for one passenger in the test set. Since we do have PClass and Embarked, however, we will assign a fare based on the distribution of fares for those particular values of PClass and Embarked.
End of explanation
"""


test_data['Fare'] = test_data['Fare'].fillna(fare_med)

"""
Explanation: After examining the distribution of Fare restricted to the specified values of Pclass and Fare, we will use the median for the missing fare (as it falls very close the fare corresponding to the peak of the distribution).
End of explanation
"""


missing_cabin_training = np.size(training_data.Cabin[pd.isnull(training_data.Cabin) == True]) / np.size(training_data.Cabin) * 100.0
missing_cabin_test = np.size(test_data.Cabin[pd.isnull(test_data.Cabin) == True]) / np.size(test_data.Cabin) * 100.0

print('Percentage of Missing Cabin Numbers (Training): %0.1f' % missing_cabin_training)
print('Percentage of Missing Cabin Numbers (Test): %0.1f' % missing_cabin_test)

"""
Explanation: 3d. Cabin Number: Relevant or Not? <a class="anchor" id="third-fourth"></a>
When we first encountered the data, we figured that Cabin would be one of the most important features in predicting survival, as it would not be unreasonable to think of it as a proxy for a passenger's position on the Titanic relative to the lifeboats (distance to deck, distance to nearest stairwell, social class, etc.). 
Unfortunately, much of this data is missing:
End of explanation
"""


## Set of functions to transform features into more convenient format.
#
# Code performs three separate tasks:
#   (1). Pull out the first letter of the cabin feature. 
#          Code taken from: https://www.kaggle.com/jeffd23/titanic/scikit-learn-ml-from-start-to-finish
#   (2). Recasts cabin feature as number.
def simplify_cabins(data):
    data.Cabin = data.Cabin.fillna('N')
    data.Cabin = data.Cabin.apply(lambda x: x[0])
    
    #cabin_mapping = {'N': 0, 'A': 1, 'B': 1, 'C': 1, 'D': 1, 'E': 1, 
    #                 'F': 1, 'G': 1, 'T': 1}
    #data['Cabin_Known'] = data.Cabin.map(cabin_mapping)
    
    le = preprocessing.LabelEncoder().fit(data.Cabin)
    data.Cabin = le.transform(data.Cabin)
    
    return data

"""
Explanation: What can we do with this data (rather, the lack thereof)? 
For now, let's just pull out the first letter of each cabin number (including NaNs), cast them as numbers, and hope they improve the performance of our classifier.
End of explanation
"""


# Recast sex as numerical feature. 
def simplify_sex(data):
    sex_mapping = {'male': 0, 'female': 1}
    data.Sex = data.Sex.map(sex_mapping).astype(int)
    
    return data

# Drop all unwanted features (name, ticket). 
def drop_features(data):
    return data.drop(['Name','Ticket'], axis=1)

# Perform all feature transformations. 
def transform_all(data):
    data = add_title(data)
    data = simplify_embark(data)
    data = simplify_cabins(data)
    data = simplify_sex(data)
    data = drop_features(data)
    
    return data

training_data = transform_all(training_data)
test_data = transform_all(test_data)

all_data = [training_data, test_data]
combined_data = pd.concat(all_data)

# Inspect data.
combined_data.head()

"""
Explanation: 3e. Quick Fixes  <a class="anchor" id="third-fifth"></a>
Prior to the last step (which is arguably the largest one), we need to tie up a few remaining loose ends:
- Recast Sex as numerical feature.
- Drop unwanted features. 
    - Name: We've taken out the information that we need (Title).
    - Ticket: There appears to be no rhyme or reason to the data in this column, so we remove it from our analysis.
- Combine training/test data prior to age imputation.
End of explanation
"""


null_ages = pd.isnull(combined_data.Age)
known_ages = pd.notnull(combined_data.Age)
initial_dist = combined_data.Age[known_ages]

# Examine distribution of ages prior to imputation (for comparison). 
sns.distplot(initial_dist)

"""
Explanation: 3f. Imputing Missing Ages  <a class="anchor" id="third-sixth"></a>
It is expected that age will be an important feature; however, a number of ages are missing. Attempting to predict the ages with a simple model was not very successful. We decided to follow the recommendations for imputation from this article.
End of explanation
"""


def impute_ages(data):
    drop_survived = data.drop(['Survived'], axis=1)
    column_titles = list(drop_survived)
    mice_results = fancyimpute.MICE().complete(np.array(drop_survived))
    results = pd.DataFrame(mice_results, columns=column_titles)
    results['Survived'] = list(data['Survived'])
    return results

complete_data = impute_ages(combined_data)
complete_data.Age = complete_data.Age[~(complete_data.Age).index.duplicated(keep='first')]

# Examine distribution of ages after imputation (for comparison). 
sns.distplot(initial_dist, label='Initial Distribution')
sns.distplot(complete_data.Age, label='After Imputation')
plt.title('Distribution of Ages')
plt.legend()

"""
Explanation: This paper provides an introduction to the MICE method with a focus on practical aspects and challenges in using this method. We have chosen to use the MICE implementation from fancyimpute.
End of explanation
"""


# Transform age and fare data to have mean zero and variance 1.0.
scaler = preprocessing.StandardScaler()
select = 'Age Fare'.split()
complete_data[select] = scaler.fit_transform(complete_data[select])

training_data = complete_data[:891]
test_data = complete_data[891:].drop('Survived', axis=1)

# ----------------------------------
# Support Vector Machines
droplist = 'Survived PassengerId'.split()
data = training_data.drop(droplist, axis=1)
# Define features and target values
X, y = data, training_data['Survived']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=0)

#
# # Set the parameters by cross-validation
# param_dist = {'C': scipy.stats.uniform(0.1, 1000), 'gamma': scipy.stats.uniform(.001, 1.0),
#   'kernel': ['rbf'], 'class_weight':['balanced', None]}
#
# clf = SVC()
#
# # run randomized search
# n_iter_search = 10000
# random_search = RandomizedSearchCV(clf, param_distributions=param_dist,
#                                    n_iter=n_iter_search, n_jobs=-1, cv=4)
#
# start = time()
# random_search.fit(X, y)
# print("RandomizedSearchCV took %.2f seconds for %d candidates"
#       " parameter settings." % ((time() - start), n_iter_search))
# report(random_search.cv_results_)
# exit()

"""
RandomizedSearchCV took 4851.48 seconds for 10000 candidates parameter settings.
Model with rank: 1
Mean validation score: 0.833 (std: 0.013)
Parameters: {'kernel': 'rbf', 'C': 107.54222939713921, 'gamma': 0.013379109762586716, 'class_weight': None}

Model with rank: 2
Mean validation score: 0.832 (std: 0.012)
Parameters: {'kernel': 'rbf', 'C': 154.85033872208422, 'gamma': 0.010852578446979289, 'class_weight': None}

Model with rank: 2
Mean validation score: 0.832 (std: 0.012)
Parameters: {'kernel': 'rbf', 'C': 142.60506747360913, 'gamma': 0.011625955252680842, 'class_weight': None}
"""

params = {'kernel': 'rbf', 'C': 107.54222939713921, 'gamma': 0.013379109762586716, 'class_weight': None}
clf = SVC(**params)
scores = cross_val_score(clf, X, y, cv=4, n_jobs=-1)
print("Accuracy: %0.2f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))

droplist = 'PassengerId'.split()
clf.fit(X,y)
predictions = clf.predict(test_data.drop(droplist, axis=1))
#print(predictions)
print('Predicted Number of Survivors: %d' % int(np.sum(predictions)))

# output .csv for upload
# submission = pd.DataFrame({
#         "PassengerId": test_data['PassengerId'].astype(int),
#         "Survived": predictions.astype(int)
#     })
#
# submission.to_csv('../submission.csv', index=False)

"""
Explanation: From the output above it is easy to see that distribution is not dramatically altered by the imputation. Additionally, there are no non-sensical ages (negative or extremely old) produced in the process. For experimenting with the tutorial this step is sufficent. In a more serious project this may be one of the most critical steps. 
4. Prediction <a class="anchor" id="fourth-bullet"></a>
The model was chosen to be a support vector machine (SVM) model. The reason for this choice:
    1. the method is for supervised classification. 
    2. effective for high dimensional systems (we intend to expand the features in the next post)
In this model we choose to use the rbf kernel which is essentially an expansion to an "infinite" hyperspace of Gaussians. Therefore, if there is a why to seperate the data points by a single boundary this method has a chance to find it.
Below is the code that impliments the model. The commented code below was used to perform a parameter search to find an optimal fit to the data. The highest ranked outcome was used. There is no particular reason for using a random search -we were just experimenting with the tools availiable natively in sci-kit learn. In fact, it is likely faster to use a straigt-forward grid search for a problem with so few features; however, this random search will scale more favorable as the number of features grows.
End of explanation
"""


