"""
# Starting Off...
Importing the data, and necessary libraries. Welcome to my notebook!
"""
import numpy as np 
import pandas as pd

full_train_data = pd.read_csv('/kaggle/input/titanic/train.csv')
full_test_data = pd.read_csv('/kaggle/input/titanic/test.csv')

train_data = pd.read_csv('/kaggle/input/titanic/train.csv')
test_data = pd.read_csv('/kaggle/input/titanic/test.csv')
"""
# Introduction:

As said before, welcome to my notebook! Today we'll be analyzing one's likelihood for survival on the Titanic, based on features of one's travel plan. Despite the grim nature of this project, considering the tragedy it is based around, this will be an attempt at a thorough and professional look at the subject at hand.

So, let's start with the basics. What's our *question*?

**Question: Based on the features in the provided dataset, can we accurately predict one's survival on the Titanic? If so, how accurately?**
**MOREOVER: How can we do this?**

So... it's clear then that we will be using a *machine learning* approach in answering this question, as we will be attempting to build an algorithm to learn from data for this particular example. Yet we must first understand the data we are using! 

So! Let's move on to some interactive data analysis, shall we?
"""
"""
# Interactive Data Analysis:

This is going to be a large and broad section, so let's start by laying out a *table of contents*:

**1. Features of the datasets**

**2. Missing Value Analysis**

**3. Categorical Variable Handling**

"""
"""
**Features of the Dataset:**

So let's start by training to view and understand the features of the dataset. We have the following features. 
"""
train_data.info()
print("\n\n --- \n\n")
test_data.info()
train_data.head()
test_data.head()
"""
**Passenger-Id:** A simple number to keep track of each passenger, starting at 1. It seems the split from training to testing data occurs between passenger 891-892. 

**Pclass:** What class the passenger was a part of, ranging from 1st to 3rd (according to research on the Titanic's classes). It should be noted that 3rd class enjoyed the least preferable locations on the ship... towards the bottom and front/back of the ship. As such, it would be unsurprising if the third class had the highest moratlity rate, though that trend will be looked for later.

**Name:** Fairly self-describing... Last name, first name. Sometimes, for women, the women's full name (and maiden name) is given in parantheses after the married name. Ex: Mrs. Alexander (Helga E Lindqvist). 

**Sex:** Again, self-describing... 'male' or 'female', specified as such. 

**Age:** Number denoting the person's age. *Surprisingly*, this number is a float, and seems to denote fractions of ages as well.

**SibSp:** Detailed in the dictionary, number of siblings + spouses aboard.

**Parch:** Also detailed in the dictionary, number of parents + children aboard.

**Ticket:** These seem to be most likely ticket numbers for each passenger. Essentially another way of keeping track of each passenger individually.

**Fare:** Amount of money payed to embark. These numbers likley directly correspond to class, and other factors (perhaps number of parents, children, siblings, spouses... where he/she embarked from... etc.). 

**Cabin:** Most of these values seem to be null... As an example, in the training data, there are 891 total values (as evidenced by the passenger IDs), and only 204 cabin values. These values, where they exist, seem to consist of the form \[Letter\]\[Set of three numbers\]. There are some passengers with multiple cabins listed. 

**Embarked:** Single letters listing the locations in which the passenger embarked the ship from. The ship, according to research, departed from Southhampton, stopped at Cherbourg, and Queenstown (now Cobh), and then departed for New York. Q likely corresponds to Queenstown, S=Southhampton, C=Cherbourg. 
"""
"""
**Missing Value Analysis:**

Let's move on to checking for missing values, and understanding how we might deal with these issues. 
"""
train_data.isnull().sum()
test_data.isnull().sum()
"""
Let's remember a few major pieces of information:

Size of the training set: 891 passengers

Size of the test set: 418 passengers

Thus the missing values can be broken down as follows...
"""
"""
**Training:**

Age: 177/891 = approx 20%

Cabin: 687/891 = approx 77.1%

Embarked: 2/891 = approx 0.22%
"""
"""
**Test:**

Age: 86/418 = approx 20.6%

Fare: 1/418 = approx 0.24%

Cabin: 327/418 = approx 78.2%
"""
"""
There's an issue here. The cabin value is obviously missing almost all of the time (upwards of 80% of the values are missing). **Yet!** ... the cabin value is ***extremely important***! The number itself, without an extremely in-depth knowledge of the Titanic's layout, is fairly useless for us to analyze... but the first letter indicates the *deck* that the cabin was located on, as presented by the image below.

![Titanic_cutaway.png](attachment:Titanic_cutaway.png)

It is a shame then, that the cabin value seems to be extremely useful, but not really accessible en masse. Yet! *PClass* will serve our purposes here, as the class one traveled in directly corresponds to the deck one would be assigned to. To increase the accuracy of this model, using cabin values may be added as an option, but seeing as so many values are missing, we will drop the cabin column. 
"""
del train_data["Cabin"]
del test_data["Cabin"]

print("Cabin columns deleted")
"""
Let's also try and figure out how to handle the *age*, *embarked*, and *fare* missing values in both the training and testing data. Age is a little difficult as, by the cacluations done above, approximately 20% of the training data's rows are missing the age values. Yet, if we think about the time, it's likely that the elderly would be evacuated first (and, though grim, it's likely that the elderly in, say, 3rd class, would've had a hard time reaching the lifeboats). Thus, it's clear that age is an important feature... or at least one that should be included in an inital model for testing purposes. So! We must figure out how to deal with the missing values.

In this case... for an inital model, and seeing as the age category is numeric, it's likely most fair to say that we can probably get by with assigning the average age... so let's calculate and assign that.
"""
# Calculate the average age
print(" - Calculating average ages - \n\n")
average_train_age = 0
average_test_age = 0
count = 0
for age in train_data["Age"]:
    if not pd.isnull(age):
        count += age
average_train_age = count/train_data["Age"].count()

count = 0

for age in test_data["Age"]:
    if not pd.isnull(age):
        count += age
average_test_age = count/test_data["Age"].count()
print("Average training-set age: %s" % average_train_age)
print("Average test-set age: %s" % average_test_age)

print("\n\n - Rounding up - \n\n")

# Rounding up (to fit the format of the data, one decimal)...
average_train_age = round(average_train_age, 1)
average_test_age = round(average_test_age, 1)

print("Average training-set age: %s" % average_train_age)
print("Average test-set age: %s" % average_test_age)

# Assign it to the missing values

print("\n\n - Assigning average values - \n\n")
for i in range(0, train_data["PassengerId"].count()):
    if pd.isnull(train_data.iloc[i]["Age"]):
        train_data.at[i, "Age"] = average_train_age
for i in range(0, test_data["PassengerId"].count()):
    if pd.isnull(test_data.iloc[i]["Age"]):
        test_data.at[i, "Age"] = average_test_age
print("Done")
"""
Next up is **embarked**, which is only missing in a few of the values. 

... Hmm. This one is a little difficult. It's a categorical variable, which we're gonna have to deal with later, but for now we have to deal with the fact that it's got a few missing values. Any sort of logical guessing is definitely not going to work... based on the information we have there's no way to logically deduce where a person embarked from. It's *very* unlikely that this feature matters at all... it's just the location someone boarded after all. But certain factors, such as more familiarity with the ship's layouts, or common sounds, might've helped a passenger survive in an odd incident. 

Let's d a little digging on the *embarked* feature, see if we can understand it a little better.

First off, number of unique values.

(We'll only concern ourselves with the training data, as no values are missing for embarked from the testing data). 
"""
print("Embarked unique values count: %s" % train_data["Embarked"].nunique())

# Next, let's try list those values...
embarked_unique_vals = []

for loc in train_data["Embarked"]:
    if not loc in embarked_unique_vals:
       embarked_unique_vals.append(loc) 
print("Embarked unique values: %s" % embarked_unique_vals)

# This fits exactly the prediction we had earlier!
"""
Great! So it's exactly as we predicted earlier, in the feature description. Let's see if we can get an "average" value... or which value, of S, C, Q, occurs the most.
"""
print("Training data: \n%s\n\n" % train_data["Embarked"].value_counts())
"""
Great, so we see it's the same result for both. S (Southampton) is the most common embarked location. Little issue here, it seems that the data is almost completely Southampton in the training set, and only just over half Southampton for the testing. We'll talk about such things in the skewness chapter though, for now let's focus on the "average" value.

It's quite clear that we'll be selecting S, or Southampton, for our "average" value in this case. Let's going ahead and replace all NaN values for embarked with S. 
"""
print("Before fix, number of null values: %s" % train_data["Embarked"].isnull().sum())
train_data["Embarked"].fillna("S", inplace=True)  
print("After fix, number of null values: %s" % train_data["Embarked"].isnull().sum())
"""
It's only a small fix, in this case, but it's all that's needed. 

Alright, final issue! Let's fix the missing value for fare in the test data! Let's just average the fares, and assign that. It's a simple fix, but it's only a single missing value.

But! Let's try and do it with a little intelligence. Not only the machines should do the learning, huh? Let's figure out the person's assigned class first.
"""
fare_missing_value_row = 99999
for i in range(0, test_data["PassengerId"].count()):
    if pd.isnull(test_data.at[i, "Fare"]):
        fare_missing_value_row = i
print("Person: \n%s" % test_data.loc[fare_missing_value_row])
print("\n\n - Determining class - \n\n")
print("Class: %s" % test_data.at[fare_missing_value_row, "Pclass"])
"""
We did it! We got his class, 3. So let's average the fares (from the test data) from his *class* only. 
"""
test_fare_average = 0 
for i in range(0, test_data["PassengerId"].count()):
    if not pd.isnull(test_data.at[i, "Fare"]) and test_data.at[i, "Pclass"] == 3:
        test_fare_average += test_data.at[i, "Fare"]
test_fare_average /= test_data["PassengerId"].count()
print("Unrounded fare average: %s" % test_fare_average)
# Round to 4 decimals to fit the data's format
test_fare_average = round(test_fare_average, 4)
print("Rounded fare average: %s" % test_fare_average)
# Let's assign it now...
test_data.at[fare_missing_value_row, "Fare"] = test_fare_average
print("After fix, number of missing values in Fare (test data): %s" % test_data["Fare"].isnull().sum())

"""
Great! We've fixed all the missing values! Observe...
"""
train_data.isnull().sum()
test_data.isnull().sum()
"""
**Handling Categorical Variables:**
"""
"""
Another large challenge in a machine learning approach is figuring out how to deal with the issue of *categorical* variables, or non-numerical variables. As an example, we can't provide someone's name just as a string of characters to the machine learning algortitms we're going to apply today, but we can provide numbers. So! Let's lay out our data again, determine which variables are categorical, and how we'll deal with this.
"""
train_data.info()
test_data.info()
"""
Great, so we can see right away that Name, Sex, Ticket, and Embarked, are categorical variables. Cabin is as well, but we've already removed it from consideration.
Let's start off easy. For an inital model, patterns in name, and survival, aren't likely to exist... so to save time and effort, we're just going to remove Name as a column.

Sex is definitely going to be a different story though, as it can be shown to play a much larger role in who survived, and who didn't (as women and children would be evacuated first). As just an example of that...
"""
# Before the example, remove name as a column.
del train_data["Name"]
del test_data["Name"]

# So, how important is the gender feature? How many people from each gender survived (at least in the training data)? Let's see.
female_passengers = [train_data.iloc[i] for i in range(0, len(train_data.index)) if train_data.iloc[i][3] == "female"]
male_passengers = [train_data.iloc[i] for i in range(0, len(train_data.index)) if train_data.iloc[i][3] == "male"]

female_survivors = [female_passengers[i] for i in range(0, len(female_passengers)) if female_passengers[i][1]==1]
male_survivors = [male_passengers[i] for i in range(0, len(male_passengers)) if male_passengers[i][1]==1]

print("Number of male survivors (from training data): %s" % len(male_survivors))
print("Number of female survivors (from training data): %s" % len(female_survivors))

# Let's remember the total number of people in the training set.

print("Total number of passengers on the Titanic (from the training data): %s" % len(train_data.index))
print("Total number of female passengers on the Titanic (from the training data): %s" % len(female_passengers))
print("Total number of male passengers on the Titanic (from the training data): %s" % len(male_passengers))

# Thus... survival rate. (Rounded to 1 decimal)
print("Rounded female survival rate: %s%%" % (100 * round(len(female_survivors) / len(female_passengers), 3)))
print("Rounded male survival rate: %s%%" % (100 * round(len(male_survivors) / len(male_passengers), 3)))
"""
Okay, so gender is needed. We won't one-hot encode that, as it seems to be a little too much trouble for a variable that can simply be encoded as binary, so we'll assign 1=male, 0=female. 

Ticket has too many possible values, and many aren't very understandable. Perhaps this could be revisisted to improve model performance, but for now we're just going to drop Ticket.

Embarked might be important. It will be one-hot encoded, but, for now, let's just handle the above statements, and then .info() the data, and then in the following box we'll take a look at how much "embarked" had an effect.
"""
# Let's drop Ticket first
del train_data["Ticket"]
del test_data["Ticket"]

# Now let's label encode sex
from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()

train_data["Sex"] = label_encoder.fit_transform(train_data["Sex"])
test_data["Sex"] = label_encoder.transform(test_data["Sex"])

# Let's take a look at how important "embarked" is
S_passengers = [train_data.iloc[i] for i in range(0, len(train_data.index)) if train_data.iloc[i][8] == "S"]
C_passengers = [train_data.iloc[i] for i in range(0, len(train_data.index)) if train_data.iloc[i][8] == "C"]
Q_passengers = [train_data.iloc[i] for i in range(0, len(train_data.index)) if train_data.iloc[i][8] == "Q"]

S_survivors = [S_passengers[i] for i in range(0, len(S_passengers)) if S_passengers[i][1]==1]
C_survivors = [C_passengers[i] for i in range(0, len(C_passengers)) if C_passengers[i][1]==1]
Q_survivors = [Q_passengers[i] for i in range(0, len(Q_passengers)) if Q_passengers[i][1]==1]

print("Number of Southampton survivors (from training data): %s" % len(S_survivors))
print("Number of Cherbourg survivors (from training data): %s" % len(C_survivors))
print("Number of Queenstown survivors (from training data): %s" % len(Q_survivors))

# Let's remember the total number of people in the training set.

print("Total number of passengers on the Titanic (from the training data): %s" % len(train_data.index))
print("Total number of Southampton passengers on the Titanic (from the training data): %s" % len(S_passengers))
print("Total number of Cherbourg passengers on the Titanic (from the training data): %s" % len(C_passengers))
print("Total number of Queenstown passengers on the Titanic (from the training data): %s" % len(Q_passengers))

# Thus... survival rate. (Rounded to 1 decimal)
print("Rounded Southampton survival rate: %s%%" % (100 * round(len(S_survivors) / len(S_passengers), 3)))
print("Rounded Cherbourg survival rate: %s%%" % (100 * round(len(C_survivors) / len(C_passengers), 3)))
print("Rounded Queenstown survival rate: %s%%" % (100 * round(len(Q_survivors) / len(Q_passengers), 3)))
"""
Hmm. Embarked didn't seem to have that much of a noticeable effect. It is true though, that there is far more data for Southampton's survival chances (as there are a far more Southamtpon passengers than any other city) than there are for the other cities.

Either way though, it definitely seems like a useful variable. At any rate, there's nothing stopping us from including it, and our machine learning approach may find something with it we miss.

So, to make it useable, let's one-hot encode it. 
"""
# One-hot encode the "Embarked" value
from sklearn.preprocessing import OneHotEncoder

one_hot_enc = OneHotEncoder(categories='auto', handle_unknown='ignore', sparse=False) # ignore values that aren't present in the training data, and return values as a numpy array. Though the first parameter is unlikely to be needed.

OH_cols_train = pd.DataFrame(one_hot_enc.fit_transform(train_data.Embarked.values.reshape(-1, 1))) 
OH_cols_test = pd.DataFrame(one_hot_enc.transform(test_data.Embarked.values.reshape(-1, 1)))

OH_cols_train.index = train_data.index
OH_cols_test.index = test_data.index

del train_data["Embarked"]
del test_data["Embarked"]

train_data = pd.concat([train_data, OH_cols_train], axis=1)
test_data = pd.concat([test_data, OH_cols_test], axis=1)

"""
Great! Column 0 = C, 1 = Q, 2 = S. 

Oneeeee final change, PassengerID won't be useful in learning, as it's just a counter for each passenger, and not actual data. This isn't technically a categorical variable, but seeing as this is our final bit of preprocessing, I've included it here. 
"""
del train_data["PassengerId"]
del test_data["PassengerId"]
train_data.head()
test_data.head()
"""
# Machine Learning (XGBoost):

We're here! Finally, time to start making predictions. We've analyzed (a little) our data, and set up our dataset for usage. The only step in between us and predictions now is the fact that we haven't seperated the target, survived, from our training data. We'll do that, and start training!
"""
# Seperate the target, survived, from our training data.

y = train_data["Survived"]
train_data = train_data.drop(['Survived'], axis=1)

# Import XGRegressor
from xgboost import XGBRegressor

# Create model and fit
model = XGBRegressor()
model.fit(pd.get_dummies(train_data), y)

# Get predictions
predictions = model.predict(pd.get_dummies(test_data))

# Round each prediction up or down to get a whole 0, or 1.
predictions = [round(x) for x in predictions]
int_predictions = [int(x) for x in predictions]

# Save and submit
output = pd.DataFrame({'PassengerId': full_test_data.PassengerId, 'Survived': int_predictions})
output.to_csv('my_submission.csv', index=False)
output.head() #  Output first few predictions
"""
# Thank you!

I hope this notebook is useful in some way! This is my first published notebook, so if you have any comments at all, please feel free to leave them down in the comments section.
Thank you again!

Sources:
https://en.wikipedia.org/wiki/RMS_Titanic#:~:text=After%20leaving%20Southampton%20on%2010,11%3A40%20p.m.%20ship's%20time.
https://www.kaggle.com/c/titanic/discussion/4693
"""