"""
#  Classification using Decision Tree
## Decision trees on Spotify Song Attributes dataset 
"""
"""
## 0. Importing packages
"""
import pandas as pd

# Dummy model and Decision Tree Classifier
from sklearn.dummy import DummyClassifier
from sklearn.model_selection import(
    cross_validate,
    train_test_split
)
from sklearn.tree import (
    DecisionTreeClassifier,
    export_graphviz
)

# Visualization in the EDA section
import altair as alt
alt.renderers.enable('kaggle')
import seaborn as sns

# Visualizing the final tree diagram
import re
import graphviz
from IPython.display import Image
"""
## 1. Reading the data CSV
"""
# Will be required to download spotify dataset in the machine
spotify_df = pd.read_csv('/kaggle/input/spotifyclassification/data.csv', index_col=0)
spotify_df
"""
## 2. Data splitting 
"""
train_df, test_df = train_test_split(spotify_df,
                                     train_size=0.8,
                                     random_state=2021)
spotify_df.shape
train_df.shape[0]
"""
Total data frame size has 2017 examples.  
The test size data frame has 20% of the examples = 404  
The training size data frame has 80% of the examples = 1613
"""
"""
## 3. Preliminary EDA
"""
train_df.info()
"""
Luckily, we do not have any `NAN` values in the dataset. We have a combination of numeric and text features.
"""
train_df.describe()
"""
There are not much outliers in the dataset as can be seen from the ranges above.
"""
"""
## 4. EDA 
"""
"""
### 4.1 Relationship among the features
"""
corr_df = train_df.corr('spearman').stack().reset_index(name='corr')
corr_df.loc[corr_df['corr'] == 1, 'corr'] = 0  # Remove diagonal
# Use abs so that we can visualize the impact of negative correaltion  
corr_df['abs'] = corr_df['corr'].abs()
corr_df.sort_values('abs', ascending=False)
alt.Chart(corr_df).mark_circle().encode(
    x='level_0',
    y='level_1',
    size='abs',
    color=alt.Color('corr',
                    scale=alt.Scale(scheme='blueorange',
                                    domain=(-1, 1))))
"""
The three standout correlation relationships which can be observed from the graph above are:
1. `loudness` and `energy`
2. `acousticness` and `energy`
3. `danceability` and `valence`

The good thing is that the relationship is not too strong, and the rest of the features seem fairly unrelated to each other.
"""
"""
### 4.2 Relationship between the features and the target
"""
sns.violinplot(x="energy",  y="target", data=train_df, orient="h")
sns.violinplot(x="acousticness",  y="target", data=train_df, orient="h")
sns.violinplot(x="tempo",  y="target", data=train_df, orient="h")
"""
After trying multiple relations between the target and features, we observe that:
1. There is some bimodality for not liking the song for features `tempo` and `energy`.
2. `acousticness` is unimodal but it is more dense when the user likes the song.
"""
"""
## 5. Data splitting 
"""
"""
For our basic analysis, we are avoiding vectorizing the text columns. This can be incorporated using CountVectorzier or TD-IDF.
"""
X = spotify_df.drop(columns=["song_title", "artist", "target"])
y = spotify_df["target"]

X_train, X_test, y_train, y_test = train_test_split(X, y,
        train_size=0.8, test_size=0.2, random_state=123)
"""
## 6. Hyperparameter optimization of max_depth
"""
"""
Below we define a function to calculate the validation and train score at a specific depth.
"""
def cross_validate_return(X_train, y_train, depth):
    """
    Fits a Decision tree basis the depth provided and 
    returns the accuracies and depth as list

    Parameters
    ----------
    X_train: numpy.ndarray
        The X_train part of the data
    y_train: numpy.ndarray
        The y_train part of the data
    depth: int
        The depth of the Decision Tree

    Returns
    -------
        list of train_score, test_score, depth
    """
    model = DecisionTreeClassifier(max_depth=depth, random_state=123)
    scores = cross_validate(model, X_train, y_train, 
                        return_train_score=True, cv=10)
    return_df = pd.DataFrame(pd.DataFrame(scores).mean()).iloc[2:].T
    return [round(return_df.train_score[0], 3),
            round(return_df.test_score[0],3), depth]

data = []
i = 1
while i < 26:
    data.append(cross_validate_return(X_train, y_train, i))
    i += 1

df = pd.DataFrame(data, columns=['train_score', 'validation_score',
                  'max_depth'])
df.head(n=5)
"""
We now melt the df so that we can plot train and validation score separately.
"""
cv_df = df.melt(value_vars=(["train_score", "validation_score"]), id_vars=["max_depth"])
cv_df.head(n=5)
alt.Chart(cv_df).mark_line().encode(
    x=alt.X('max_depth', title='Max depth of tree'),
    y=alt.Y('value', title='Score value',
           scale=alt.Scale(domain=[0.6, 1.05])), 
    color=alt.Color('variable', title='Score'))
"""
- We can see in the plot that we get maximum validation score at max_depth = 4 and the difference between train and validation score is the least at this point as well. This will be our optimal maximum depth. 
- We assume that since it has delivered the best validation score at this depth, the test score will be best at this point as well.
"""
"""
#### **`max_depth` and the Fundamental Tradeoff**
"""
"""
- Increasing the max_depth increases the training accuracy of the Decision Tree as we add more branches/leaves to the tree and we end up adding all the random quirks of the training set.  
- Increasing the max_depth increases the validation accuracy till a point and then it starts decreasing as the overfitting on the training set fails to generalize well on the validation set.
"""
"""
## 7. Picking the best value for `max_depth`
"""
optimal_depth = df.max_depth[df.validation_score.argmax()]

print("We pick the value where the cross-validation error is "
      "minimum (or the cross-validation score is the highest)"
      f" at max_depth = {optimal_depth}")
"""
## 8. Model performance on the test set
"""
print(f"Validation score at optimal depth: {df.validation_score[3]}")
optimal_model = DecisionTreeClassifier(max_depth=optimal_depth)
optimal_model.fit(X_train, y_train)
print(f"Score on test set: { optimal_model.score(X_test, y_test):.3f}")
"""
Test score is less than our cross-validation score. Our optimal depth ends up giving a fair estimate of how much accuracy we can expect from our best model.
"""
"""
## 9. Visualizing our optimal Decision Tree!
"""
# adapted from https://stackoverflow.com/questions/44821349/python-graphviz-remove-legend-on-nodes-of-decisiontreeclassifier

def display_tree(feature_names, tree, counts=False):
    """For binary classification only"""
    dot = export_graphviz(
        tree,
        out_file=None,
        feature_names=feature_names,
        class_names=tree.classes_.astype(str),
        impurity=False,
    )
    # dot = re.sub('(\\\\nsamples = [0-9]+)(\\\\nvalue = \[[0-9]+, [0-9]+\])(\\\\nclass = [A-Za-z0-9]+)', '', dot)
    if counts:
        dot = re.sub("(samples = [0-9]+)\\\\n", "", dot)
        dot = re.sub("value", "counts", dot)
    else:
        dot = re.sub(
            "(\\\\nsamples = [0-9]+)(\\\\nvalue = \[[0-9]+, [0-9]+\])", "", dot
        )
        dot = re.sub(
            "(samples = [0-9]+)(\\\\nvalue = \[[0-9]+, [0-9]+\])\\\\n", "", dot
        )
    return graphviz.Source(dot)
Image(display_tree(X_train.columns, optimal_model).pipe("png"))
"""
- `instrumentalness` seems the most important feature as it sits at the root and best divides the tree. In our exploratory analysis, we found `danceability` to be a good feature but it is not the best.
- **Scaling not required:** While using Decision Tree Classification the scale doesn't matter as it does not depend on the variance in the data.
- **More features can be added:** We can use the Bag of Words technique which throws away the order information of the word and counts the frequencies of each word in it, which is done by assigning a unique number to each word. Alternatively, we can use one-hot encoding to divide the categorical feature into multiple features with labels 0 and 1(when a particular artist is present). This could increase our data size by a big margin if the number of artists is large.
"""