"""
# Conway's Reverse Game of Life 2020

The Game of Life is a cellular automaton created by mathematician John Conway in 1970. The game consists of a board of cells that are either on or off. One creates an initial configuration of these on/off states and observes how it evolves. There are **four simple rules** to determine the next state of the game board, given the current state:

- **Overpopulation:** if a living cell is surrounded by more than three living cells, it dies.

- **Stasis:** if a living cell is surrounded by two or three living cells, it survives.

- **Underpopulation:** if a living cell is surrounded by fewer than two living cells, it dies.

- **Reproduction:** if a dead cell is surrounded by exactly three cells, it becomes a live cell.
"""
# 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

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import matplotlib.pyplot as plt
import seaborn as sns
# Input data files are available in the read-only "../input/" directory
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory
print("hello world")
import os
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))

# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using "Save & Run All" 
# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session
"""
# Initial Exploration - Data Set

The data consists of 50.000 games on a 25x25 board (625 locations). Each line is a game with the first column as game id and the second the delta (time steps between start set up and final/stop board). The next 625 columns represent each position in the start matrix and the last 625 each position in the stop one.  
"""
train_ds = pd.read_csv("../input/conways-reverse-game-of-life-2020/train.csv")
train_ds.head()
train_ds.shape
train_ds.describe()
"""
A sample of how the board looks like
"""
fig= plt.figure()

start = train_ds.iloc[5,2:625+2]
mat_start = np.array(start).reshape(25,25)
fig.add_subplot(1, 2, 1)
plt.imshow(mat_start), plt.title('START MATRIX')


stop = train_ds.iloc[5,625+2:]
mat_stop= np.array(stop).reshape(25,25)
fig.add_subplot(1, 2, 2)
plt.imshow(mat_stop), plt.title('STOP MATRIX')
"""
Looking for differences in the start and stop matrixes. We sum each matrix to get the total value of cells/ones and see if there is any difference.
Both distributions are fairly similar, but the stop board has slighly less cells/ones.
"""
start = train_ds.iloc[:,:2]
start['Sum'] = train_ds.iloc[:,2:627].sum(axis=1)

stop = train_ds.iloc[:,:2]
stop['Sum'] = train_ds.iloc[:,627:].sum(axis=1)
print('START')
print('mean= ' + str(start['Sum'].mean()))
print('std= ' + str(start['Sum'].std()))
print('STOP')
print('mean= ' + str(stop['Sum'].mean()))
print('std= ' + str(stop['Sum'].std()))

fig = plt.figure(figsize=(15,15))

#START
fig.add_subplot(3, 2, 1)
#Histogram
sns.distplot( start['Sum'], bins=15)
plt.title('START')

#Boxplot
fig.add_subplot(3, 2, 3)
sns.boxplot( y=start['Sum'] )

#Violin
fig.add_subplot(3, 2, 5)
sns.violinplot( y=start['Sum'] )

#STOP
fig.add_subplot(3, 2, 2)
#Histogram
sns.distplot( stop['Sum'], bins=15)
plt.title('STOP')

fig.add_subplot(3, 2, 4)
#Boxplot
sns.boxplot( y=stop['Sum'] )

fig.add_subplot(3, 2, 6)
#Violin
sns.violinplot( y=stop['Sum'] )
"""
Now we look into the difference of cells between start and stop boards. 
We clearly see again, that stop boards have less cells alive. In general we see three patterns:

1) As we could expect, the start #cells highly influences the stop #cells

2) The more the delta the more the variance on the relation start/stop #cells

3) The more the start #cells the more the variance on the relation
"""
relation = start.copy()
relation = relation.rename(columns={'Sum':'Start_Sum'})
relation['Stop_Sum'] = stop['Sum']
relation['Diff'] = abs(relation['Start_Sum'] - relation['Stop_Sum'])

plt.figure(figsize=(15,7))
sns.violinplot( x= relation['delta'],y=relation['Diff']  )
cmap = plt.cm.Spectral
fig,axes = plt.subplots(nrows=1, ncols=5, figsize=(30, 10))
for i,delta in enumerate(np.sort(relation['delta'].unique())):
    delta_condition=relation['delta']==delta
    sns.regplot(x=relation['Start_Sum'][delta_condition], y=relation['Stop_Sum'][delta_condition],ax=axes[i], scatter=False)
    sns.scatterplot(data=relation[delta_condition],x=relation['Start_Sum'][delta_condition], y=relation['Stop_Sum'][delta_condition],ax=axes[i], hue=relation['Diff'][delta_condition])
    
"""
# Deep Learning model
Now we train a DL model to predict the start state given the stop board. (It is the reverse game of life)
"""
import tensorflow as tf
from sklearn.model_selection import train_test_split
from tensorflow.keras.layers import Dense, Flatten, Conv2D
from tensorflow.keras import Model

start = train_ds.iloc[:,2:625+2]
stop = train_ds.iloc[:,625+2:]

#For now I am only considering delta=1
y = np.array(start[train_ds['delta']==1])
x = np.array(stop[train_ds['delta']==1])

x= x.reshape(np.shape(x)[0],25,25,1)
#y= y.reshape(np.shape(y)[0],25,25,1)

x_train, x_test, y_train, y_test = train_test_split(x,y,test_size=0.3)

#Some sanity checks
print(np.shape(x_train))
print(np.shape(y_train))
print(np.shape(y_test))

print(x_train[:3].reshape(3,25,25))
#Simple model
model = tf.keras.models.Sequential([
    Conv2D(2, 3, activation='relu', dilation_rate=2, input_shape=(25,25,1), padding='same'),
    Conv2D(1, 3, activation='relu', dilation_rate=2, input_shape=(25,25,1), padding='same'),
    Flatten()#,
    #Dense(625, input_dim=625, activation='sigmoid')
  ])

model.compile(optimizer='adam',
              loss='binary_crossentropy'
             )

model.fit(x_train, y_train,validation_data=(x_test,y_test) , epochs=150, batch_size=32)
model.summary()
"""
# Results data analysis

We plot some samples to check the results. In this case we define the treshold as 0.5 but we will explore further to get the best value.
We get an **accuracy of 83.4%**.

We will differentiate between accuracy on predicting alive and dead cells. As there many more dead cells that alive it is easy to predict an output of all dead cells and get fairly good results.
"""
y_pred = model.predict(x_test)

pred = np.where(y_pred > 0.5, 1, 0)

fig = plt.figure(figsize=(15,15))

ax1=fig.add_subplot(1,3,1)
ax1.imshow(x_test[0].reshape(25,25))
plt.title('START')

ax2 = fig.add_subplot(1,3,2)
ax2.imshow(pred[0].reshape(25,25))
plt.title('PREDICTION')

ax3 = fig.add_subplot(1,3,3)
ax3.imshow(y_test[0].reshape(25,25))
plt.title('GROUND TRUTH')

print(np.shape(pred))
pred = np.where(y_pred > 0.5, 1, 0)

accuracy_1 = []
accuracy_0 = []
for i,prediction in enumerate(pred):
    accuracy_1.append(np.sum(prediction[y_test[i] == 1] == y_test[i][y_test[i] == 1])/len(y_test[i][y_test[i] == 1]))
    accuracy_0.append(np.sum(prediction[y_test[i] == 0] == y_test[i][y_test[i] == 0])/len(y_test[i][y_test[i] == 0]))
    acc = (np.sum(prediction[prediction == 1] == y_test[i][y_test[i] == 1])/len(y_test[i][y_test[i] == 1]))
    
    #print(str(len(y_test[i][y_test[i] == 1])) +' - ' + str(len(prediction[prediction == 1])) + ' - ' + str(acc))
#print(str(prediction[y_test[i] == 1]) + ' - ' + str(y_test[i][y_test[i] == 1]))
print(sum(accuracy_1)/len(accuracy_1))
print(sum(accuracy_0)/len(accuracy_0))

print( (sum(accuracy_1)/len(accuracy_1) + sum(accuracy_0)/len(accuracy_0) )/2.0)
"""
Our output is a probability of [0,1] of a cell being alive. We plot the difference between our probability and the ground truth for dead and alive cells.

With this, we see that the treshold value for a cell being considerd alive is close to 0.2 not to the "usual" 0.5. There are also many more cells dead than alive.
"""
diff = y_test-y_pred
#print(sum(diff)/len(diff))

sns.distplot( diff[y_test==1], label='Alive [1]')

sns.distplot( abs(diff[y_test==0]), label='Dead [0]')
plt.title('Diff of Ground truth - probability')
plt.legend()
plt.show()
"""
Now we will se how changing the treshold changes the accuracy
"""

tresh = np.arange(0,1,0.01)
acc1 = []
acc0 = []
acc = []
for treshold in tresh:
    pred = np.where(y_pred > treshold, 1, 0)
    
    accuracy_1 = []
    accuracy_0 = []
    for i,prediction in enumerate(pred):
        accuracy_1.append(np.sum(prediction[y_test[i] == 1] == y_test[i][y_test[i] == 1])/len(y_test[i][y_test[i] == 1]))
        accuracy_0.append(np.sum(prediction[y_test[i] == 0] == y_test[i][y_test[i] == 0])/len(y_test[i][y_test[i] == 0]))

    acc1.append(sum(accuracy_1)/len(accuracy_1))
    acc0.append(sum(accuracy_0)/len(accuracy_0))

    acc.append( (sum(accuracy_1)/len(accuracy_1) + sum(accuracy_0)/len(accuracy_0) )/2.0)
sns.scatterplot(x=tresh, y = acc1)
sns.scatterplot(x=tresh, y = acc0)
sns.lineplot(x=tresh, y = acc)
pred = np.where(y_pred > 0.3, 1, 0)

accuracy_1 = []
accuracy_0 = []
for i,prediction in enumerate(pred):
    accuracy_1.append(np.sum(prediction[y_test[i] == 1] == y_test[i][y_test[i] == 1])/len(y_test[i][y_test[i] == 1]))
    accuracy_0.append(np.sum(prediction[y_test[i] == 0] == y_test[i][y_test[i] == 0])/len(y_test[i][y_test[i] == 0]))
    acc = (np.sum(prediction[prediction == 1] == y_test[i][y_test[i] == 1])/len(y_test[i][y_test[i] == 1]))
    
    #print(str(len(y_test[i][y_test[i] == 1])) +' - ' + str(len(prediction[prediction == 1])) + ' - ' + str(acc))
#print(str(prediction[y_test[i] == 1]) + ' - ' + str(y_test[i][y_test[i] == 1]))
print(sum(accuracy_1)/len(accuracy_1))
print(sum(accuracy_0)/len(accuracy_0))

print( (sum(accuracy_1)/len(accuracy_1) + sum(accuracy_0)/len(accuracy_0) )/2.0)
"""
We have increased our global accuracy by 12pp only with the treshold setting. 
"""
test_ds = pd.read_csv('../input/conways-reverse-game-of-life-2020/test.csv')
test_ds.head()
test_ds.iloc[:,2:].head()
predictions = np.array([])
for delta in np.flip(np.arange(1,6)):
    print(delta)
    if np.shape(predictions)[0] >0: 
        a = np.concatenate((predictions,np.array(test_ds.iloc[:,2:][test_ds['delta']==delta])),axis=0)
    else:
        a=np.array(test_ds.iloc[:,2:][test_ds['delta']==delta])
    a = a.reshape(np.shape(a)[0],25,25,1)
    print(np.shape(a))
    predictions = model.predict(a)
    predictions = np.where(predictions > 0.3, 1, 0)
    

sample_submission = pd.read_csv('../input/conways-reverse-game-of-life-2020/sample_submission.csv', index_col='id')
sample_submission.iloc[:] = predictions
sample_submission.to_csv('submission.csv')
sample_submission.to_csv('submission3.csv')
sample_submission