"""
# Importing libraries📚
"""
import torch
import torchvision
import numpy as np
import matplotlib.pyplot as plt
import torch.nn as nn
import torch.nn.functional as F
from torchvision.transforms import ToTensor
from torchvision.utils import make_grid
from torch.utils.data import random_split
import pandas as pd
import seaborn as sns
from sklearn.preprocessing import StandardScaler
from colorama import Fore, Back, Style
import warnings
warnings.filterwarnings("ignore")
%matplotlib inline
"""
# Getting data 💽
"""
train = pd.read_csv("../input/titanic/train.csv")
test = pd.read_csv("../input/titanic/test.csv")
submission = pd.read_csv("../input/titanic/gender_submission.csv")
data = train.append(test)
train.shape
test.shape
data.shape
train.head()
test.head()
train.isnull().sum()
test.isnull().sum()
"""
# EDA 📊
"""
red = Fore.RED
grn = Fore.GREEN
blu = Fore.BLUE
ylw = Fore.YELLOW
wht = Fore.WHITE
def plot_distribution(feature,color):
    plt.figure(dpi=125)
    sns.distplot(data[feature],color=color);
    print("{}Max value of {} is {}\n{}Min value of {} is {}\n{}Mean value of {} is {}\n{}Std value of {} is {}\n{}Median value of {} is {}".format(red,feature,train[feature].max(),blu,feature,train[feature].min(),grn,feature,train[feature].mean(),ylw,feature,train[feature].std(),wht,feature,train[feature].median()));
plot_distribution('Age','green')
"""
As we know from the disaster.. women and children were the first to be evacuated.. mean age is 29.6, median age is 28.. both of which are >18 which suggests the people with missing ages are adults ... standard deviation is 14.5 which is <18 which suggests they are chilren..
"""
sns.set(style = 'darkgrid')
plt.figure(dpi=125)
sns.countplot(x=train.Sex, hue=train.Survived, data=train,edgecolor = sns.color_palette('dark',2));
"""
As we can confirm females had a greater survival rate compared to males.
"""
plt.figure(dpi=125)
sns.countplot(x = train.Sex, hue = train.Pclass,data = train,edgecolor = sns.color_palette('dark',3));
"""
There were many people from both genders in 3rd class(as expected)... the second most filled class was 1st class ... this maybe suggests huge price difference between classes.. not sure though 🤷‍♂️🤷‍♂️
"""
plt.figure(dpi=125)
sns.countplot(x = train.Survived, hue = train.Pclass, data = train,edgecolor = sns.color_palette('dark',1));
"""
Most people survived were from 1st class.. so people in 1st class had a greater chance of survival
"""
plt.figure(dpi=125)
sns.countplot(x = train.Pclass, hue = train.Survived, data = train,edgecolor = sns.color_palette('dark',5));
"""
Only people from 1st class had a greater survival rate compared to classes 2 and 3... maybe there were fewer people in 1st class
"""
train.groupby('Pclass').Survived.value_counts()
"""
Well there weren't fewer people in 1st class.. so we can conclude that people in first class did indeed had a higher chance and rate of survival
"""
plt.figure(dpi=125)
sns.countplot(x = train.Embarked, hue = train.Sex, data = train,edgecolor = sns.color_palette('dark',6));
plt.figure(dpi=125)
sns.countplot(x = train.Embarked, hue = train.Survived, data = train);
"""
More people survived from Cherbourg compared to other two
"""
"""
### Family
"""
train['family'] = train['SibSp'] + train['Parch'] + 1
plt.figure(dpi=125)
sns.countplot(x = train.family, hue = train.Survived, data = train);
train.groupby('family').Survived.value_counts()
"""
This seems kinda random .. only people with family member size of 2,3,4 survived greater than the rest 🤔🤔.
"""
for i in range(len(train)):
    if(train['family'][i] > 1):
        train['family'][i] = 1
    else:
        train['family'][i] = 0
plt.figure(dpi=125)
sns.countplot(x = train.family, hue = train.Survived, data = train);
"""
people with family had greater rate of survival
"""
"""
## Name
"""
train['Name'] = train.Name.str.extract('([A-Za-z]+)\.',expand = False)
plt.figure(dpi=200)
plt.xticks(size=5)
sns.countplot(x = train.Name, hue = train.Survived, data = train);
top6 = train['Name'].value_counts()[:6].index.to_list()
top6
train['Name'] = train['Name'].apply(lambda x: x if x in top6 else 'Other')
train.groupby('Name').Survived.value_counts()
"""
### Cabin
"""
train['Cabin'].fillna(0,inplace = True)
train['Cabin'] = train['Cabin'].apply(lambda x: 1 if x != 0 else 0)
plt.figure(dpi = 125)
sns.countplot(x = train.Cabin,hue = train.Survived, data = train)
"""
people with cabins were more likely to survive
"""
train.groupby('Cabin').Survived.value_counts()
"""
### Fare
"""
plt.figure(dpi=125)
plot_distribution('Fare','orange')
"""
The minimum fare is 0.0 which means there was/were someone/some people with a free ride in titanic(probably in 1st class) 😅😅
"""
train['fare_val'] = pd.qcut(train['Fare'],5,labels=False)
train['fare_val'] = train['fare_val'].astype(int)
#train['fare_val'] = 0
#for i in range(len(train)):
#    if(train['Fare'][i] > 40.0):
#        train['fare_val'][i] = 1        
train.groupby('fare_val').Survived.value_counts()
plt.figure(dpi=125)
sns.countplot(x = train.fare_val, hue = train.Survived, data = train);
"""
People with greater fare had a higher rate of survival
"""
"""
### Ticket
"""
for i in train.Ticket:
    train['ticket'] = train['Ticket'].apply(lambda x: x.replace('/','').replace('.','').split()[0] if not x.isdigit() else 'N' )
train["ticket"].unique()
plt.figure(dpi=125)
plt.xticks(rotation = 'vertical')
sns.countplot(x = train.ticket, hue = train.Survived, data = train);
"""
people with PC,SWPP,SCAH,SS,PP ticket survived the most
"""
train['SibSp'] = pd.cut(train['SibSp'],bins=[0,1,100],labels=[0,1],right=False).astype(int)

train['Parch'] = pd.cut(train['Parch'], bins=[0,1,100], labels=[0,1], right=False).astype(int)
train.groupby('Parch').Survived.value_counts()
"""
#### Making same modifications to test dataset
"""
#family
test['family'] = test['SibSp'] + test['Parch'] + 1
for i in range(len(test)):
    if(test['family'][i] > 1):
        test['family'][i] = 1
    else:
        test['family'][i] = 0
    
#Name
test['Name'] = test['Name'].apply(lambda x: x if x in top6 else 'Other')
        
#Cabin
test['Cabin'].fillna(0,inplace = True)

test['Cabin'] = test['Cabin'].apply(lambda x: 1 if x != 0 else 0)

#ticket
for i in test.Ticket:
    test['ticket'] = test['Ticket'].apply(lambda x: x.replace('/','').replace('.','').split()[0] if not x.isdigit() else 'N' )

#Fare
test['fare_val'] = pd.qcut(test['Fare'],5,labels=False)
test['fare_val'] = test['fare_val'].fillna(value = test['fare_val'].mode()[0])
test['fare_val'] = test['fare_val'].astype(int)
#test['fare_val'] = 0
#for i in range(len(test)):
#    if(test['Fare'][i] > 40.0):
#        test['fare_val'][i] = 1

# SibSp and Parch
test['SibSp'] = pd.cut(test['SibSp'],bins = [0,1,200],labels=[0,1],right = False).astype(int)
test['Parch'] = pd.cut(test['Parch'],bins = [0,1,200],labels=[0,1],right = False).astype(int)
"""
# Data Preprocessing 🗄️
"""
features = [##'PassengerId',
            'Pclass',
            'Name',
            'Sex',
            'Age',
            'SibSp',
            'Parch',
            #'family',#derived from SibSp & Parch
            #'Ticket',
            #'ticket',
            ##'Fare',
            'fare_val',#derived from Fare
            #'Cabin',
            #'Embarked'
           ]

target = ['Survived']
train[features].isnull().sum()
test[features].info()
test[features].isnull().sum()
'''Age_mean = train['Age'].std()
#train['Age'] = train['Age'].fillna(value = Age_mean)

Age_mean_t = test['Age'].std()
#test['Age'] = test['Age'].fillna(value = Age_mean_t)
f"'train',{Age_mean}, 'test',{Age_mean_t}"''';
'''Age_std = train['Age'].std()
train['Age'] = train['Age'].fillna(value = Age_std)

Age_std_t = test['Age'].std()
test['Age'] = test['Age'].fillna(value = Age_std_t)
f"'train',{Age_std}, 'test',{Age_std_t}"''';
Age_mean = data['Age'].mean()
Age_std = data['Age'].std()

train['Age'] = train['Age'].fillna(np.random.randint(Age_mean-Age_std,Age_mean+Age_std))
test['Age'] = test['Age'].fillna(np.random.randint(Age_mean-Age_std,Age_mean+Age_std))
from sklearn.preprocessing import LabelEncoder

lbl = LabelEncoder()

train['Sex'] = lbl.fit_transform(train[['Sex']].values.ravel())
test['Sex'] = lbl.fit_transform(test[['Sex']].values.ravel())
lbl2 = LabelEncoder()
train['Name'] = lbl2.fit_transform(train[['Name']].values.ravel())
test['Name'] = lbl2.fit_transform(test[['Name']].values.ravel())
#lbl3 = LabelEncoder()
#train['Cabin'] = lbl3.fit_transform(train[['Cabin']].values.ravel())
#test['Cabin'] = lbl3.fit_transform(test[['Cabin']].values.ravel())
train['Embarked'] = train['Embarked'].fillna(value=train['Embarked'].mode()[0])
test['Embarked'] = test['Embarked'].fillna(value=test['Embarked'].mode()[0])

train_ds = train[features+target]
test_ds = test[features]
#train_ds = pd.get_dummies(columns = ['Embarked'],data=train_ds,drop_first = False)
#test_ds = pd.get_dummies(columns = ['Embarked'],data=test_ds,drop_first = False)
#train_ds = pd.get_dummies(columns = ['Pclass'],data=train_ds,drop_first = True)
#test_ds = pd.get_dummies(columns = ['Pclass'],data=test_ds,drop_first = True)
#train_remove_tickets = ['_{}'.format(x) for x in train['ticket'].unique() if x not in test['ticket'].unique()]
#test_remove_tickets = ['_{}'.format(x) for x in test['ticket'].unique() if x not in train['ticket'].unique()]
#print(f'train tickets to be removed:{train_remove_tickets}\ntest tickets to be removed:{test_remove_tickets}')
#train_ds = pd.get_dummies(columns = ['ticket'],data=train_ds,prefix = '')
#test_ds = pd.get_dummies(columns = ['ticket'],data=test_ds,prefix = '')
#train_ds = train_ds.drop(train_remove_tickets,axis = 1)
#test_ds = test_ds.drop(test_remove_tickets,axis = 1)
print(train_ds.head())
train_ds.shape
print(test_ds.head())
test_ds.shape
#train_ds = train_ds.drop(['Cabin_T'],axis = 1)
train_ds
"""
# Creating a PyTorch Model ❤️‍🔥
"""
from torch.nn import BCELoss
from torch.utils.data.dataloader import DataLoader
from tqdm import tqdm_notebook as tqdm
torch.manual_seed(7)
class Titanicnn(nn.Module):
    def __init__(self,in_size,hidden_size1,hidden_size2,hidden_size3,hidden_size4,num_classes):
        super().__init__()
        self.linear1 = nn.Linear(in_size, hidden_size1)
        self.linear2 = nn.Linear(hidden_size1, hidden_size2)
        self.linear3 = nn.Linear(hidden_size2,hidden_size3)
        self.linear4 = nn.Linear(hidden_size3,hidden_size4)
        self.linear5 = nn.Linear(hidden_size4,num_classes)
        self.dropout = nn.Dropout(0.2)
    
    def forward(self,xb):
        xb = xb.view(-1,xb.size(1)).float()
        out = self.linear1(xb)
        #out = self.dropout(out)
        out = F.relu(out)
        out = self.linear2(out)
        out = self.dropout(out)
        #out = F.relu(out)
        out = self.linear3(out)
        #out = self.dropout(out)
        out = F.relu(out)
        out = self.linear4(out)
        out = self.dropout(out)
        #out = F.relu(out)
        out = self.linear5(out)
        out = self.dropout(out)
        return out
    
    def training_step(self,batch):
        features, labels = batch
        out = self(features)
        loss = F.cross_entropy(out, labels)
        #loss_x = nn.CrossEntropyLoss()
        #loss = loss_x(out,labels)
        return loss
    
    def validation_step(self,batch):
        features,labels = batch
        out = self(features)
        loss = F.cross_entropy(out, labels)
        #loss_x = F.CrossEntropyLoss()
        #loss = loss_x(out,labels)
        acc = accuracy(out,labels)
        return {'valid_loss': loss, 'valid_acc': acc}

    def validation_epoch_end(self,outputs):
        batch_losses = [x['valid_loss'] for x in outputs]
        epoch_loss = torch.stack(batch_losses).mean()
        batch_accs = [x['valid_acc'] for x in outputs]
        epoch_acc = torch.stack(batch_accs).mean()
        return {'valid_loss': epoch_loss.item(), 'valid_acc': epoch_acc.item()}

    def epoch_end(self,epoch,result):
        print("Epoch [{}], valid_loss: {:.4f}, valid_acc: {:.4f}".format(epoch, result['valid_loss'], result['valid_acc']))
def accuracy(outputs, labels):
    
    _, preds = torch.max(outputs, dim=1)
    return torch.tensor(torch.sum(preds == labels).item() / len(preds))
        
    return preds
input_size = train_ds.shape[1] -1
hidden_size1 = 128
hidden_size2 = 1024
hidden_size3 = 512
hidden_size4 = 128
num_classes = 2
model = Titanicnn(input_size, hidden_size1=hidden_size1, hidden_size2= hidden_size2,hidden_size3 = hidden_size3,hidden_size4 = hidden_size4,num_classes = num_classes)
model
for t in model.parameters():
    print(t.shape)
def convert_to_tensors(ds, valid_size = 0.30,train_set=True):
    
    scaler = StandardScaler()
    batch_size = ds.shape[0]
    
    if(train_set == True):
        
        targets_t = ds.Survived.values
        features_t = ds.drop(labels = ['Survived'],axis = 1).values
        
        features_t = scaler.fit_transform(features_t)
        
        targetsTrain = torch.from_numpy(targets_t).type(torch.LongTensor)
        featuresTrain = torch.from_numpy(features_t)
        
        train_tensor = torch.utils.data.TensorDataset(featuresTrain,targetsTrain)
        
        valid_size = round(len(train_tensor) * (valid_size))
        train_size = len(train_tensor) - valid_size

        ttrain_ds, tvalid_ds = random_split(train_tensor, [train_size, valid_size])
        
        print("train_size:- ",len(ttrain_ds),"test_size:- ", len(tvalid_ds))
              
        train_loader = DataLoader(ttrain_ds, batch_size = batch_size, shuffle=True, num_workers=4, pin_memory=True)
        valid_loader = DataLoader(tvalid_ds, batch_size = batch_size*2, num_workers=4, pin_memory=True)
              
        return train_loader,valid_loader
    
    else:
        
        featuresTest = ds.values
        features_test = scaler.fit_transform(featuresTest)
        featuresTensor = torch.from_numpy(features_test)
        print(len(featuresTensor))
        test_loader = DataLoader(featuresTensor, batch_size*2, num_workers=4, pin_memory = True)
        
        return test_loader
        
train_ds.shape
train_loader,valid_loader = convert_to_tensors(train_ds)
for data, _ in train_loader:
    print('data.shape:', data.shape)
"""
## Initial Prediction 🌱
"""
for data, labels in train_loader:
    outputs = model(data)
    print(labels.shape)
    #loss_x = nn.CrossEntropyLoss()
    #loss = loss_x(outputs, labels)
    loss = F.cross_entropy(outputs, labels)
    acc = accuracy(outputs,labels)
    print('Loss:', loss.item())
    print('Initial Acc:',float(acc),"%")
    break

print('outputs.shape : ', outputs.shape)
print('Sample outputs :\n', outputs[:5].data)
print('Sample labels :\n', labels[:5].data)
"""
As we can see we get loss of about .63 and an accuracy of .67
"""
"""
# Using GPU for Training 💖🤯
"""
torch.cuda.is_available()
"""
Device is cuda means we are using GPU
"""
def get_default_device():
    if torch.cuda.is_available():
        return torch.device('cuda')
    else:
        return torch.device('cpu')
device = get_default_device()
device
"""
### Moving PyTorch Tensors to Device(GPU)
"""
def to_device(data,device):
    if isinstance(data, (list,tuple)):
        return [to_device(x,device) for x in data]
    return data.to(device,non_blocking = True)
for xd, yd in train_loader:
    print(xd.shape)
    xds = to_device(xd, device)
    print(xds.device)
    break
class DeviceDataLoader():
    def __init__(self, ds, device):
        self.ds = ds
        self.device = device
    
    def __iter__(self):
        for d in self.ds:
            yield to_device(d,self.device)
            
    def __len__(self):
        return len(self.ds)
train_loader = DeviceDataLoader(train_loader, device)
valid_loader = DeviceDataLoader(valid_loader, device)
"""
Loading DataLoaders into the GPU
"""
for xd, yd in valid_loader:
    print('xd.device:', xd.device)
    print('yd:', yd)
    break
def evaluate(model, valid_loader):
    outputs = [model.validation_step(batch) for batch in valid_loader]
    return model.validation_epoch_end(outputs)
"""
## Training Loop 🔁
"""
def fit(epochs, lr, model, train_loader, valid_loader, opt_func = torch.optim.SGD):
    history = []
    optimizer = opt_func(model.parameters(),lr)
    for epoch in tqdm(range(epochs)):
        # Training ==>
        for batch in train_loader:
            loss = model.training_step(batch)
            loss.backward()
            optimizer.step()
            optimizer.zero_grad()
            
        # Validation ==>
        result = evaluate(model,valid_loader)
        model.epoch_end(epoch, result)
        history.append(result)
    return history
model = Titanicnn(input_size,hidden_size1=hidden_size1, hidden_size2= hidden_size2, hidden_size3= hidden_size3,hidden_size4 = hidden_size4,num_classes = num_classes)
to_device(model, device) #Load model into the GPU
history = [evaluate(model,valid_loader)]
history
history += fit(20, 0.2, model, train_loader, valid_loader) #Learning Rate = 0.5
history += fit(40, 0.001, model, train_loader, valid_loader)
history += fit(100, 0.01, model, train_loader, valid_loader)
history += fit(300, 0.01, model, train_loader, valid_loader)
history += fit(30, 0.001, model, train_loader, valid_loader)
test_loader = convert_to_tensors(test_ds,train_set= False)
"""
## Prediction Function 🔮
"""
def predict(pred_x,model):
    for pred in pred_x:
        y = model(pred)
        _, y_  = torch.max(y, dim=1)
    return y_
test_loader_gpu = DeviceDataLoader(test_loader, device)
test_preds = predict(test_loader_gpu,model)
final_pred = test_preds.cpu()
print(final_pred.shape)
"""
# My Submission 🙋‍♂️
"""
passId = test[['PassengerId']].values
final_pred = final_pred.data.numpy()
final_pred
sub = {'PassengerId':passId.ravel(), 'Survived':final_pred}
submission_csv = pd.DataFrame(sub)
submission_csv.head()
submission_csv.to_csv('final_sub_titanic_pth.csv',index=False)
x = pd.read_csv("./final_sub_titanic_pth.csv")
x.head()
"""
## Work in Progress 🚧🚧
"""
"""
## Saving the Model 💾
"""
filename = "./titanic_pytorch.pth"
torch.save(model.state_dict(),filename)
"""
## Loading the Model 🔃
"""
loaded_model = Titanicnn(input_size,hidden_size1=hidden_size1, hidden_size2= hidden_size2, hidden_size3= hidden_size3,hidden_size4 = hidden_size4,num_classes = num_classes)
to_device(model, device)
loaded_model.load_state_dict(torch.load(filename))
loaded_model.eval()
"""
## Checkout my other [**Notebook**](https://www.kaggle.com/mdhamani/titanic-getting-better-eda-top-14) with randomforest and xgboost Classifiers

"""
"""
# To-Do📋
## Tuning parameters 🤷‍♂️🤷‍♂️
## Make the PyTorch Model more accurate
"""