"""
@brief Script that vizualize all experiments, by default all graphs will be created
Private methods noted with _ servers as helper methods for public methods.
"""

from src.constants import *
from src.stock import Stock
from scipy.special import expit
from matplotlib.ticker import FormatStrFormatter
from sklearn.preprocessing import StandardScaler

import scipy.stats as stats
import cairosvg
import os
import glob
import seaborn as sns

# define default style
sns.set(style='whitegrid', palette='tab10')
height = 6
aspect = 12 / height
col_wrap = 2
title_font_size = 16
font_size = 14
bbox_to_anchor = (0.95, 0.92)


def _model_to_data(row) -> tuple:
    """
    Create stock, model_type, model_units from model name.
    :param row:
    :return:
    """

    model_split = row['model'].split('-')
    stock = model_split[1]

    # split could break stocks with name like brk-a
    if model_split[2] == 'a':
        stock = model_split[1] + '-' + model_split[2]
        del model_split[2]

    model_type = 'jedno-premenný' if model_split[2] == 'single' else 'viac-premenný'
    model_units = model_split[3]

    return stock.upper(), model_type, model_units


def _model_mse(row) -> tuple:
    """
    Split model name into model and mse
    :param row:
    :return:
    """

    model_split = row['model'].split('-')

    model = '-'.join(model_split[:-1])
    mse = model_split[-1]

    return model, mse


def _create_validity_summary(data: pd.DataFrame) -> pd.DataFrame:
    """
    Create validity summary of models.
    :param data:
    :return:
    """
    validity_summary = data \
        .groupby(['model', 'stock', 'stock_type', 'model_units', 'model_type',
                  'validity']) \
        .size() \
        .reset_index() \
        .rename(columns={0: 'validity_count'})

    # drop invalid combinations of (model, stock)
    validity_summary = validity_summary[(validity_summary['validity'] == 1) & (
            validity_summary['validity_count'] > 0)]

    validity_summary['validity_string'] = validity_summary['validity'].apply(
        lambda x: 'valid' if x == 1 else 'invalid')

    # model with units
    validity_summary['summary_name'] = "Model " + validity_summary[
        'model_units'].astype(str)

    # sort
    return validity_summary.sort_values(['validity_count'], ascending=[False])


def _determine_validity(row) -> int:
    """
    Determine validity of prediction.
    :param row:
    :return: prediction validity (1 - valid, 0 - invalid)
    """
    # check validity of prediction -> directional change should be preserved
    validity = 0
    real_change = row['today_price'] - row['previous_day_price']
    predicted_change = row['fake_prediction'] - row['previous_day_price']
    if real_change >= 0 and predicted_change >= 0:
        validity = 1
    elif real_change < 0 and predicted_change < 0:
        validity = 1

    return validity


def _create_descriptors(row):
    info = row['model'].split('-')
    row['type'] = info[0]
    row['type_layer'] = ('Jedno' if row['type'] == 'single' else 'Viac') + '-premenný ' + ('jedno' if info[3] == '1' else 'dvoj') + '-vrstvový model'
    row['epochs'] = info[1]
    row['alpha'] = info[2]
    row['layers'] = info[3]
    row['early_stopping'] = "early-stop" in row['model']

    if info[5] == '10y':
        row['dataset_size'] = '10 ročná'
    elif info[5] == '20y':
        row['dataset_size'] = '20 ročná'

    if info[-1] == 'A':
        row['stock'] = 'BRK-A'
    else:
        row['stock'] = info[-1]

    row['stock_type_layer'] = row['type_layer'] + ' akcie ' + row['stock']

    if row['early_stopping']:
        row['dataset_size_early_stopping'] = row['dataset_size'] + '\n(najlepšie epochy)'
    else:
        row['dataset_size_early_stopping'] = row['dataset_size']

    return row


def _split_model(row):
    split = row['model'].split('_')
    row['stock'] = split[0]
    row['model_units'] = split[1]
    row['period'] = split[2]
    return row


def _split_model_exp_3(row):
    split = row['model'].split('-')

    for idx, info in enumerate(split):
        if 'e' == info[-1] and info[-2].isdigit():
            split[idx] = float(info + split[idx + 1])
            del split[idx + 1]

    # fix for BRK-A
    if split[1] == 'a':
        split[0] = split[0] + '-' + split[1]
        del split[1]

    row['stock'] = split[0]
    row['training_no'] = split[1]
    row['model_type'] = split[2]
    row['primary_variable'] = split[3]
    row['packages'] = split[4:-2]
    row['t_mse'] = split[-2]
    row['v_mse'] = split[-1]

    return row


def _determine_quality(row, group_mean):
    best_val_loss = float(row['best_val_loss'])

    if best_val_loss <= group_mean:
        return 'Lepší ako priemer'
    else:
        return 'Horší ako priemer'


# end of helper methods


def load_experiment_1_2_data():
    data = pd.read_csv('experiment-results/exp-1-2/experiment-1-2.csv',
                       names=['model', 'prediction', 'scaled_prediction', 'reference_price',
                              'reference_normalized_price', 'normalization_fixed', 't_mse', 'v_mse', 'test_mse'])

    # create index of 200 values three times
    data['index'] = np.repeat([np.arange(1, 201)], 15, axis=0).flatten()
    # create stock, model_units and period columns
    data = data.apply(lambda row: _split_model(row), axis=1)
    # put reference_price and scaled_prediction into separate rows
    data = data.melt(id_vars=['index', 'model', 'model_units', 'period', 'stock', 'normalization_fixed'],
                     value_vars=['reference_price', 'scaled_prediction'],
                     var_name='type', value_name='price_prediction')
    # create stock unit
    data['stock_unit'] = data['stock'] + ' ' + data['model_units'] + ' LSTM jednotiek'

    return data


def plot_experiment_1_2(data: pd.DataFrame, file_name: str, graph_width: float = 0.85, exp_no: int = 1):
    # override graph width
    graph_box_width = graph_width
    # plot data
    facet_grid = sns.relplot(data=data,
                             x="index",
                             y="price_prediction", hue="type",
                             kind="line", col_wrap=(1 if exp_no == 2 else 1),
                             col=("stock" if exp_no == 2 else "stock"),
                             facet_kws=dict(sharex=False, sharey=False),
                             legend=False, height=height, aspect=aspect)
    # set labels, titles and legend
    facet_grid.set_axis_labels("Predikované dni", "Cena akcie ($)", labelpad=15, size=16)
    facet_grid.set_titles("Akcia {col_name}", pad=15, size=16)
    facet_grid.figure.legend(title='Cena akcie',
                             labels=['Reálna', 'Predikovaná'],
                             title_fontsize=title_font_size,
                             fontsize=16, loc='upper right',
                             bbox_to_anchor=bbox_to_anchor, borderaxespad=1)

    # keep xlabel for all graphs
    for ax in facet_grid.axes:
        ax.set_xlabel('Predikované dni', fontsize=16, labelpad=15)

    # save graph
    plt.suptitle('Predikcie 200 dní z ' + (
        'validačného obdobia (2018≈2019)' if exp_no == 1 else 'testovacieho obdobia (2020≈2021)'), fontsize=24)
    plt.tight_layout(pad=5, rect=[0, 0, graph_box_width - 0.025, 1])
    full_file_name = 'experiment-results/exp-1-2/' + file_name
    plt.savefig(full_file_name + '.svg', format='svg')
    cairosvg.svg2pdf(url=full_file_name + '.svg', write_to=full_file_name + '.pdf')
    plt.show()


def plot_testing_msft(data):
    # override graph width
    graph_box_width = 1

    # plot data
    facet_grid = sns.relplot(data=data, x=np.arange(1, 201),
                             y="price_prediction",
                             kind="line",
                             facet_kws=dict(sharex=False, sharey=False),
                             legend=False, height=height, aspect=aspect)

    # set labels, titles and legend
    facet_grid.set_axis_labels("Predikované dni", "Cena akcie ($)", labelpad=15, size=font_size + 4)

    # save graph
    plt.suptitle('Vývoj predikcií akcie MSFT počas 200 dní testovacieho obdobia', fontsize=24)
    plt.tight_layout(pad=3, rect=[0, 0, graph_box_width, 1])
    file_name = 'experiment-results/exp-1-2/experiment-2-msft-bad'
    plt.savefig(file_name + '.svg', format='svg')
    cairosvg.svg2pdf(url=file_name + '.svg', write_to=file_name + '.pdf')
    plt.show()


def experiment_3():
    data = pd.read_csv('experiment-results/exp-3/experiment-3.csv',
                       names=['model', 'prediction', 'scaled_prediction', 'reference_price',
                              'reference_normalized_price'])
    # create index of 200 values three times
    data['index'] = np.repeat([np.arange(1, 201)], 72, axis=0).flatten()  # was 48 for normal feature selection
    # create stock, model_units and period columns
    data = data.apply(lambda row: _split_model_exp_3(row), axis=1)
    # put reference_price and scaled_prediction into separate rows
    data = data.melt(
        id_vars=['index', 'model', 'stock', 'training_no', 'model_type', 'primary_variable', 'packages', 't_mse', 'v_mse'],
        value_vars=['reference_price', 'scaled_prediction'],
        var_name='type', value_name='price_prediction')
    # create stock unit
    data['stock_unit'] = data['stock'] + ' 512 LSTM jednotiek'

    # keep only primary_variable == 'price'
    data = data[data['primary_variable'] == 'price']
    # drop duplicates
    data = data.drop_duplicates(subset=['model'], keep='first')
    # order by v_mse
    data = data.sort_values(by=['stock', 'v_mse'], ascending=[True, True])
    # create order column
    data['order'] = data.groupby('stock').cumcount() + 1
    data['packages_str'] = data['packages'].astype(str)
    data['v_mse'] = data['v_mse'].astype(float)
    # group data by training_no and calculate mean of v_mse
    grouped_stocks_with_errors = data.groupby(['stock', 'packages_str'])['v_mse'].mean().reset_index()
    print('Data grouped by stock and package with validation mse mean')
    print(grouped_stocks_with_errors)
    print('Data grouped only by package with validation mse mean')
    print(grouped_stocks_with_errors.groupby('packages_str')['v_mse'].mean())


def load_exp_4_data() -> pd.DataFrame:
    """
    Load test data from csv file.
    :return: csv data with correct dtypes
    """
    test_columns = ['id', 'model', 'stock', 'date', 'predicted_value',
                    'denormalized_prediction', 'previous_day_price',
                    'today_price', 'validity']
    # load data from experiment
    data = pd.read_csv('experiment-results/exp-4/experiment-4-predictions.csv',
                       names=test_columns)
    # id is redundant when using numbers as index
    data.drop('id', axis=1, inplace=True)

    dtypes = {
        'model': 'str',
        'stock': 'category',
        'date': 'datetime64',
        'predicted_value': 'float',
        'denormalized_prediction': 'float',
        'previous_day_price': 'float',
        'today_price': 'float',
        'validity': 'int'
    }

    # convert data types
    return data.astype(dtypes)


def clean_exp_4_data(data: pd.DataFrame) -> tuple[pd.DataFrame, np.ndarray]:
    """
    Keep only best performing 24 models.
    :param data:
    :return: cleaned data
    """
    # keep only valid predictions
    valid_predictions = data[data['validity'] == 1] \
        .groupby(['model', 'stock']) \
        .size() \
        .reset_index() \
        .rename(columns={0: 'valid_predictions'})

    # drop invalid combinations of (model, stock)
    valid_predictions = valid_predictions[
        valid_predictions['valid_predictions'] > 0]

    # from model create columns model_type (single_variable, multi_variable) and model_units
    valid_predictions['model_type'] = valid_predictions['model'].apply(
        lambda x: x.split('-')[-2])
    valid_predictions['model_units'] = valid_predictions['model'].apply(
        lambda x: x.split('-')[-1])

    data['model_type'] = data['model'].apply(lambda x: x.split('-')[-3])
    data['model_units'] = data['model'].apply(lambda x: x.split('-')[-2])

    # sort data into sensible order
    valid_predictions = valid_predictions.sort_values(
        ['stock', 'model_type', 'model_units', 'valid_predictions'],
        ascending=[True, False, True, False])
    print('Summary of trend predictions by model')
    print(valid_predictions)
    # from combination (stock, model_type, model_units) -> each training group, keep one record with the highest valid_predictions
    valid_predictions = valid_predictions.drop_duplicates(
        subset=['stock', 'model_type', 'model_units'], keep='first')

    chosen_models = valid_predictions['model'].values

    # keep only best models
    data = data[data['model'].isin(chosen_models)]

    # create combination of stock and type
    data['stock_type'] = data[['stock', 'model_type']].apply("-".join, axis=1)

    # create month and year columns
    data['month'] = data['date'].dt.month
    data['year'] = data['date'].dt.year

    validity_by_month_year = data.groupby(['month', 'year', 'stock'])['validity'].apply(lambda validity: (validity == 1).sum()) \
        .reset_index().sort_values('validity', ascending=False).dropna()
    print('Summary of trend prediction by month and year')
    print(validity_by_month_year)

    return data, chosen_models


def _prepare_test_data(create_from_file_names: bool = False) -> pd.DataFrame:
    """
    Prepare test data for plotting and analysis. Data source could be .csv file of file names
    :return: test data
    """
    # load data either from model-stats.csv (containing testing mse) or load mse from file names
    if not create_from_file_names:
        data = pd.read_csv('experiment-results/exp-4/experiment-4-model-stats.csv',
                           names=['model', 'mse'])
    else:
        # load all file names from models/best-models and create dataframe with model and mse
        file_names = glob.glob('models/exp-4/*')
        # remove path and extension
        file_names = [os.path.splitext(os.path.basename(file_name))[0] for file_name in file_names]
        # create df
        data = pd.DataFrame(file_names, columns=['model'])
        # split model and mse
        data = data.apply(
            lambda row: _model_mse(row), axis='columns', result_type='expand'
        ).rename(columns={0: 'model', 1: 'mse'})

    # create stock, model_type, model_units columns and stock_type column
    applied_df = data.apply(lambda row: _model_to_data(row), axis='columns',
                            result_type='expand')
    data = pd.concat([data, applied_df], axis='columns').rename(
        columns={0: 'stock', 1: 'model_type', 2: 'model_units'})

    dtypes = {
        'model': 'str',
        'mse': 'float',
        'stock': 'str',
        'model_type': 'category',
        'model_units': 'int'
    }

    # apply dtypes
    data = data.astype(dtypes)

    # create stock_type column
    data['stock_type'] = data['stock'] + '-' + data['model_type'].astype(str)

    # sort data
    return data.sort_values(['stock', 'model_type', 'model_units'],
                            ascending=True)


def plot_month_predictions(data: pd.DataFrame) -> None:
    """
    Plot lineplots for month with the best validity performance and show barplot of validity summary for given month.
    :param data:
    :return:
    """

    # prepare data
    validity_summary = _create_validity_summary(data)
    validity_summary['model_units'] = 'Model ' + validity_summary['model_units'].astype(str)

    # plot predictions from set month for each model
    december_data = data[
        (data['date'].dt.month == 12) & (data['date'].dt.year == 2021)]

    # count number of valid predictions
    validity_in_month = december_data.groupby(['stock'])['validity'].apply(lambda validity: (validity == 1).sum()) \
        .reset_index().sort_values('validity', ascending=False)

    # override graph box width
    graph_box_width = 0.9

    # create three subplots composed of two subplots each
    fig, axes = plt.subplots(3, 2, figsize=(12, 12), gridspec_kw={'width_ratios': [10, 1]})
    facet_grid = []

    # plot data
    for ax, model_name in zip(axes, ['4-brk-a-single-512-', '10-goog-multi-512-', '3-msft-multi-512-']):
        selected_model = december_data[december_data['model'] == model_name]
        stock_name = selected_model['stock'].iloc[0]
        model_type = 'Jedno-premenný' if selected_model['model_type'].iloc[0] == 'single' else 'Viac-premenný'
        identifier = model_type + ' model akcie ' + stock_name
        # plot predictions
        sns.lineplot(data=selected_model, x="date",
                     y="denormalized_prediction",
                     legend=False, ax=ax[0])
        # plot reference line
        facet_grid = sns.lineplot(data=selected_model, x="date",
                                  y="today_price",
                                  legend=False, ax=ax[0], label='reference', alpha=0.7, color='tab:red',
                                  linestyle='dashed')

        # adjust lineplot params
        ax[0].set_xticklabels(selected_model["date"].dt.date, rotation=45)
        ax[0].set(xticks=selected_model["date"].dt.date, xlabel=None, ylabel="Cena akcie ($)")
        ax[0].yaxis.labelpad = 15

        # select data for bar plot
        selected_validity = validity_summary[validity_summary['model'] == model_name]
        selected_validity['vc'] = validity_in_month[validity_in_month['stock'] == stock_name]['validity'].iloc[0]
        # plot bar plot, set properties and vertical line at half of the bar
        bar_ax = sns.barplot(x='model_units', y='vc', data=selected_validity, ax=ax[1])
        ax[1].set(ylim=(0, 22), xlabel=stock_name, ylabel='Počet správnych predikcií trendu', xticklabels=[])
        ax[1].axhline(y=11, color='tab:orange', linestyle='dashed')
        # increase font sizes
        ax[0].tick_params(labelsize=font_size - 1)
        ax[0].set_ylabel('Cena akcie ($)', fontsize=font_size)
        ax[1].set_ylabel('Počet správnych predikcií trendu', fontsize=font_size)

        # annotation taken from https://www.tutorialspoint.com/how-to-add-percentages-on-top-of-bars-in-seaborn-using-matplotlib
        patches = bar_ax.patches
        for i in range(len(patches)):
            x = patches[i].get_x() + patches[i].get_width() / 2
            y = patches[i].get_height() + .05
            bar_ax.annotate('{:.1f}%'.format(selected_validity['vc'].iloc[0] * 100 / 22), (x, y), ha='center')

        # set labels, titles and legend
        ax[0].set_title(identifier, pad=15, size=font_size + 2)

    facet_grid.figure.legend(title='Prezentované krivky',
                             labels=['predikcia modelu',
                                     'reálna cena akcie'],
                             title_fontsize=14,
                             fontsize=12, loc='upper right',
                             bbox_to_anchor=(0.9725, 0.96), borderaxespad=1)

    # save graph
    plt.suptitle('Vyhodnotenie predikcií modelov za obdobie december 2021', fontsize=22, y=0.98)
    plt.tight_layout(pad=3, rect=[0, 0, graph_box_width, 0.95])
    file_name = 'experiment-results/exp-4/experiment-4-month-predictions'
    plt.savefig(file_name + '.svg', format='svg')
    cairosvg.svg2pdf(url=file_name + '.svg', write_to=file_name + '.pdf')


def plot_training_testing_stats() -> None:
    """
    Plot training and testing mse values (box plot).
    :return:
    """
    # override default styles
    graph_box_width = 0.94
    col_wrap = 2
    # test data should be loaded from model-stats.csv
    data_test = _prepare_test_data()
    data_test['source'] = ' z testovacieho obdobia'
    # training data should be loaded from file names of models
    data_train = _prepare_test_data(True)
    data_train['source'] = ' z validačného obdobia'
    # combine data
    data = pd.concat([data_test, data_train])
    # prepare and sort values
    data['source_stock'] = 'Modely akcie ' + data['stock'] + data['source']
    data = data.sort_values(['source', 'stock', 'model_type'], ascending=False)
    # describe for table 5.5
    print('Table 5.5')
    print(data.groupby(['model_type', 'source', 'stock'])['mse'].describe())

    data = data.sort_values(['stock', 'model_type'], ascending=[True, False])

    # plot data
    facet_grid = sns.catplot(data=data, x="model_type", y="mse", kind="box",
                             col="source_stock", col_wrap=col_wrap,
                             hue="stock", height=height,
                             aspect=aspect,
                             dodge=False, sharex=False, sharey=False, legend=False,
                             )

    # set labels and titles
    facet_grid.set_axis_labels("Typ modelu", "Hodnota odchylky (MSE)", labelpad=15,
                               size=30)

    # keep xlabel on all graphs
    for ax in facet_grid.axes:
        ax.set_xlabel('Typ modelu', fontsize=30, labelpad=15)

    facet_grid.set_titles("{col_name}", pad=15, size=28)
    facet_grid.set_xticklabels(fontsize=28)
    facet_grid.set_yticklabels(fontsize=28)
    # create legend without duplicates
    handles, labels = facet_grid.figure.axes[0].get_legend_handles_labels()
    facet_grid.figure.legend(handles=handles, labels=labels,
                             title='Akcia',
                             title_fontsize=28,
                             fontsize=24, loc='upper right',
                             bbox_to_anchor=(1.01, 0.98), borderaxespad=1)
    # save graph
    plt.suptitle('Porovnanie kvality predikcií podľa odchýlok MSE', fontsize=30, y=1)
    plt.tight_layout(pad=3.5, rect=[0, 0, graph_box_width, 1])
    file_name = 'experiment-results/exp-4/experiment-4-validation-vs-testing'
    plt.savefig(file_name + '.svg', format='svg')
    cairosvg.svg2pdf(url=file_name + '.svg', write_to=file_name + '.pdf')


def plot_brk_a_november(data: pd.DataFrame) -> None:
    # plot brk-a on november 2022
    november_data = data[(data['date'].dt.year == 2022) & (data['date'].dt.month == 11)]

    november_data = november_data[(november_data['model'] == '4-brk-a-single-512-')]
    graph_box_width = 1
    # plot data
    facet_grid = sns.relplot(data=november_data, x="date",
                             y="today_price",
                             kind="line",
                             facet_kws=dict(sharex=False, sharey=False),
                             legend=False, height=height, aspect=aspect)
    # set labels, titles and legend
    facet_grid.set_axis_labels("Dátum", "Cena", labelpad=15, size=font_size + 4)

    reference_data = november_data[
        (november_data['stock_type'] == 'BRK-A-single') &
        (november_data['model_units'] == '512')]
    # set xticks
    facet_grid.set_xticklabels(reference_data['date'].dt.date, rotation=45, size=font_size)
    facet_grid.set(xticks=reference_data['date'].dt.date)
    # increase yticks size
    facet_grid.set_yticklabels(fontsize=font_size)

    # save graph
    plt.suptitle('Vývoj ceny akcie BRK-A v novembri 2022', fontsize=24)
    plt.tight_layout(pad=3, rect=[0, 0, graph_box_width, 0.95])
    plt.savefig('experiment-results/exp-4/brk-a-november-2022.svg', format='svg')
    cairosvg.svg2pdf(url='experiment-results/exp-4/brk-a-november-2022.svg', write_to='experiment-results/exp-4/brk-a-november-2022.pdf')
    plt.show()


def load_experiment_5_data():
    # load data from cross-model-val-loss.csv
    data = pd.read_csv('experiment-results/exp-5/experiment-5.csv',  # was selected-improver / improver-with-extra-feature-all.csv
                       names=['mse', 'mae', 'loss', 'val_mse', 'val_mae', 'val_loss', 'epoch', 'model'])
    data = data.drop(columns=['mse', 'mae', 'loss', 'val_mse', 'val_mae', 'epoch'])
    data['val_loss'] = data['val_loss']  # .clip(0, 0.005)  # turned off clip for now

    return data.apply(lambda row: _create_descriptors(row), axis=1)


def plot_experiment_5(data: pd.DataFrame):
    # experiment 5 statistics
    print('Experiment 5 statistics')
    print('-----------------------')
    print('Data grouped by model stock type layer with dataset and early stopping')
    print(data.groupby(['stock_type_layer', 'dataset_size_early_stopping'])['val_loss'].mean())
    print('Data grouped by model type and layers')
    print(data.groupby(['type_layer'])['val_loss'].mean())
    print('Data grouped by dataset with / without early stopping')
    print(data.groupby(['dataset_size_early_stopping'])['val_loss'].mean())
    print('Data grouped by model type')
    print(data.groupby(['type'])['val_loss'].mean())

    # override default styles
    graph_box_width = 0.97
    col_wrap = 2

    data_copy = data.copy()
    for stocks in [('BRK-A', 'GOOG'), ('MSFT', 'filter')]:
        data = data_copy
        data['stock_type_layer_filtered'] = data['stock_type_layer']
        height_buffer = 0
        font_buffer = 4

        if stocks[0] == 'BRK-A':
            data = data[(data['stock'] == stocks[0]) | (data['stock'] == stocks[1])]
        else:
            data['stock_type_layer_filtered'] += ' s 20 roč. sadou'
            data = data[(data['stock'] == stocks[0])]

            if stocks[1] == 'filter':
                # remove 20y data
                msft_filtered = data[(data['dataset_size'] != '20 ročná') | (data['early_stopping'] == True)]
                msft_filtered['stock_type_layer_filtered'] = data['stock_type_layer'] + ' bez 20 roč. sady'
                # join data
                data = pd.concat([data, msft_filtered])

        # plot data
        facet_grid = sns.catplot(data=data, x="dataset_size_early_stopping", y="val_loss", kind="box",
                                 col="stock_type_layer_filtered", col_wrap=col_wrap,
                                 hue="dataset_size_early_stopping", height=height + height_buffer,
                                 aspect=aspect,
                                 dodge=False, sharey=False, sharex=False, legend=False)

        # set labels and titles
        facet_grid.set_axis_labels("Veľkosť sady", "Validačná odchylka (MSE)", labelpad=15, size=22 + font_buffer)
        facet_grid.set_titles("{col_name}", pad=30, size=22 + font_buffer)
        # set xtick and ytick sizes
        facet_grid.set_xticklabels(fontsize=20 + font_buffer)
        facet_grid.set_yticklabels(fontsize=20 + font_buffer)
        # save graph
        plt.suptitle('Porovnanie výsledkov modelov podľa konfigurácií z experimentu 5', fontsize=28, y=1)
        plt.tight_layout(pad=3, rect=[0, 0, graph_box_width, 1])
        file_name = 'experiment-results/exp-5/experiment-5-general-box-plot-' + ('brk-a_goog' if stocks[0] == 'BRK-A' else 'msft-' + stocks[1])
        plt.savefig(file_name + '.svg', format='svg')
        cairosvg.svg2pdf(url=file_name + '.svg', write_to=file_name + '.pdf')
        plt.show()


def epochs_lr():
    # load data from csv
    data = pd.read_csv('experiment-results/exp-5/experiment-5-full-lr-test.csv',
                       names=['stock', 'model', 'best_val_loss', 'best_epoch', 'lr_at_epoch', 'val_loss', 'config', 'length'])
    # replace config names
    data['config'] = data['config'].replace({'config-1': 'Jedno-premenný jedno-vrstvový model',
                                             'config-2': 'Viac-premenný jedno-vrstvový model',
                                             'config-3': 'Jedno-premenný viac-vrstvový model',
                                             'config-4': 'Viac-premenný viac-vrstvový model'})
    data['group_id'] = data['stock'] + '-' + data['config'] + '-' + data['length']

    data['Kvalita výsledku'] = data.apply(lambda row: _determine_quality(row, data[(data['config'] == row['config'])]['best_val_loss'].mean()), axis=1)

    # reduce groups stock to 3 stocks with average best_epoch and lr_at_epoch
    # data = data.groupby(['config', 'length', 'group_id']).mean().reset_index()
    for graph in [{'x_axis': 'best_epoch', 'x_label': 'Trénované epochy', 'title': 'trénovacej epochy'},
                  {'x_axis': 'lr_at_epoch', 'x_label': 'Miera učenia', 'title': 'miery učenia'}]:
        fg = sns.lmplot(data=data, x=graph['x_axis'], y='best_val_loss', hue='Kvalita výsledku', col="config", col_wrap=2, fit_reg=False, scatter_kws={'clip_on': False},
                        facet_kws={'sharex': False, 'sharey': False})
        fg.set(xlabel=graph['x_label'], ylabel='Najnižšia validačná chyba (MSE)')
        # set label font sizes
        for ax in fg.axes:
            ax.tick_params(labelsize=font_size + 4)
            ax.set_title(ax.get_title(), fontsize=font_size + 4)
            ax.set_xlabel(ax.get_xlabel(), fontsize=font_size + 4)
            ax.set_ylabel(ax.get_ylabel(), fontsize=font_size + 4)

        fg.set_titles("{col_name}", pad=15, size=font_size + 4)
        sns.move_legend(fg, "upper left", bbox_to_anchor=(0.79, 1.005))

        plt.setp(fg._legend.get_texts(), fontsize=font_size + 2)  # for legend text
        plt.setp(fg._legend.get_title(), fontsize=title_font_size + 2)  # for legend title
        plt.tight_layout(pad=3, rect=(0, 0, 1, 0.95))
        plt.suptitle('Vzťah {} k najnižšej validačnej chybe'.format(graph['title']), fontsize=title_font_size + 4)
        file_name = 'experiment-results/exp-5/experiment-5-{}'.format(graph['x_axis'])
        plt.savefig(file_name + '.svg', format='svg')
        cairosvg.svg2pdf(url=file_name + '.svg', write_to=file_name + '.pdf')
        plt.show()


def plot_activation_functions():
    # x range
    x = np.linspace(-5, 5, 1000)
    # compute tanh
    y = (2 / (1 + np.exp(-2 * x))) - 1
    # plot tanh
    plt.figure(figsize=(12, height))
    plt.plot(x, y)
    # emphasis on -1 and 1 range
    plt.axvline(x=1, color='tab:orange')
    plt.axvline(x=-1, color='tab:orange')
    # more detailed x axis
    plt.xticks(np.arange(-5, 5.1, 1), fontsize=title_font_size)
    plt.yticks(np.arange(-1, 1.1, 0.2), fontsize=title_font_size)
    # add x and y labels
    plt.xlabel('x', fontsize=font_size + 4)
    plt.ylabel('tanh(x)', fontsize=font_size + 4)
    plt.legend(['hyperbolický tangens'], fontsize=title_font_size)
    plt.savefig('experiment-results/general/tanh.svg', format='svg')
    cairosvg.svg2pdf(url='experiment-results/general/tanh.svg', write_to='experiment-results/general/tanh.pdf')
    # plot sigmoid
    y = expit(x)
    plt.figure(figsize=(12, height))
    plt.plot(x, y)
    plt.legend(['sigmoidná funkcia'], fontsize=(title_font_size + 4))
    # change size of xticks and yticks
    plt.xticks(np.arange(-4, 4.1, 1), fontsize=(title_font_size + 2))
    plt.yticks(np.arange(0, 1.1, 0.2), fontsize=(title_font_size + 2))
    # add x and y labels
    plt.xlabel('x', fontsize=font_size + 14)
    plt.ylabel(r"$y\,=\,\frac{1}{1+e^{-x}}$", fontsize=font_size + 14, labelpad=10)
    plt.savefig('experiment-results/general/sigmoid.svg', format='svg')
    cairosvg.svg2pdf(url='experiment-results/general/sigmoid.svg', write_to='experiment-results/general/sigmoid.pdf')


def plot_volatility():
    returns = pd.DataFrame([], columns=['stock', 'return'])
    stocks = ['BRK-A', 'GOOG', 'MSFT', 'SPY']
    for ref_stock in stocks:
        # get data
        stock = Stock(ref_stock, START, END, FORCE_FETCH_STOCK)
        stock.fetch()
        prices = stock.df['Adj Close'].pct_change(1)[1:]
        # append to returns df
        returns = returns.append(pd.DataFrame({'stock': ref_stock, 'return': prices}), ignore_index=True)

    # remove outliers
    returns = returns[np.abs(returns['return']) < 3 * returns['return'].std()]
    # plot data
    facet_grid = sns.displot(returns, x="return", hue="stock", kind="kde", fill=True, col="stock", col_wrap=2,
                             height=height, aspect=aspect, facet_kws=dict(sharex=False, sharey=False), legend=False)
    # set title and legend
    facet_grid.set_titles("{col_name}", size=26)

    facet_grid.figure.legend(
        labels=['BRK-A', 'GOOG', 'MSFT', 'SPY'],
        loc='upper right',
        bbox_to_anchor=(0.96, 0.95),
        title='Akcia',
        title_fontsize=24,
        fontsize=20, borderaxespad=1)

    # add std lines
    for idx, ax in enumerate(facet_grid.axes):
        # get stock
        stock = ax.get_title()
        stock_returns = returns[returns['stock'] == stock]['return']
        # calculate values
        std = stock_returns.std()
        print(stock, std)
        print('mean', stock_returns.mean())
        val_percentage_between_std = np.round(stock_returns.between(-std, std).sum() / len(stock_returns) * 100, 2)
        # print vertical std lines
        ax.axvline(std, color='r', linestyle='--', label='std' if idx == 0 else None)
        ax.axvline(-std, color='r', linestyle='--')
        # get maximum y value
        graph_top = np.floor(ax.get_ylim()[1])
        # print horizontal line between -std and std values
        ax.hlines(y=graph_top, xmin=-std, xmax=std, colors='r', linestyles='--')
        ax.text(0, graph_top + 0.5, str(val_percentage_between_std) + '%', ha='center', va='center', fontsize=24)
        # set ticks font size
        ax.tick_params(axis='both', which='major', labelsize=24, pad=10)
        # add xtick on std value and format them
        xticks = np.round([-0.05, -0.025, -std, 0, std, 0.025, 0.05], 4)
        ax.set_xticks(xticks)
        ax.xaxis.set_major_formatter(FormatStrFormatter('%g'))
        ax.set_title(stock, size=24)

    # set labels
    facet_grid.set_axis_labels("Medzi dňové výnosy", "Hustota", size=26, labelpad=15)
    plt.tight_layout(pad=3, rect=[0, 0, 0.9, 1])
    plt.savefig('experiment-results/general/returns_dist.svg', format='svg')
    cairosvg.svg2pdf(url='experiment-results/general/returns_dist.svg', write_to='experiment-results/general/returns_dist.pdf')
    plt.show()


def gradient_descent(W, x, y):
    y_hat = x.dot(W).flatten()
    error = (y - y_hat)
    mse = (1.0 / len(x)) * np.sum(np.square(error))
    gradient = -(1.0 / len(x)) * error.dot(x)
    return gradient, mse


# Overtaken, fixed and modified from original source:
# https://www.adeveloperdiary.com/data-science/how-to-visualize-gradient-descent-using-contour-plot-in-python/
def plot_gd_vizualization():
    # fetch and prepare data
    data = pd.read_csv(
        'https://raw.githubusercontent.com/Columbia-Intro-Data-Science/python-introduction-caitlinwang/master/www-bcf.usc.edu/~gareth/ISL/Advertising.csv')
    y = data['Sales']
    x = np.column_stack((data['TV'], data['Radio']))

    # scale inputs
    scaler = StandardScaler()
    scaled_x = scaler.fit_transform(x)
    # init weights and params
    w = np.array((-40, -40))
    alpha = .1
    tolerance = 1e-3

    old_w = []
    errors = []

    # perform upto 200 GD iterations
    for iteration in range(1, 201):
        # perform GD and calculate new weights
        gradient, error = gradient_descent(w, scaled_x, y)
        new_w = w - alpha * gradient

        # every 10 iters print error and append weights, errors
        if iteration % 10 == 0:
            print("Iteration: %d - Error: %.4f" % (iteration, error))
            old_w.append(new_w)
            errors.append(error)

        # check if convergence criteria is met
        if np.sum(abs(new_w - w)) < tolerance:
            print('Gradient Descent has converged')
            break

        w = new_w

    print('Final weights:')
    print('w =', w)
    # weights to numpy array
    all_ws = np.array(old_w)

    # selected values for visualization
    errors.append(600)
    errors.append(500)
    errors.append(400)
    errors.append(300)
    errors.append(225)

    # create more values around weights for contour plot
    w0 = np.linspace(-w[0] * 5, w[0] * 5, 100)
    w1 = np.linspace(-w[1] * 5, w[1] * 5, 100)
    # create space for 3d values
    mse_z = np.zeros(shape=(w0.size, w1.size))
    # calculate 3d values
    for xi, v1 in enumerate(w0):
        for yi, v2 in enumerate(w1):
            mse_z[xi, yi] = gradient_descent(np.array((v1, v2)), scaled_x, y)[1]

    # setup shared figure
    fig = plt.figure(figsize=(9, 4.5))
    fig.suptitle("Vizualizácia vzostupu gradientu", fontsize=20)
    # plot 3d function
    mesh_x, mesh_y = np.meshgrid(w0, w1)
    ax = fig.add_subplot(1, 2, 1, projection='3d')
    ax.plot_surface(mesh_x, mesh_y, mse_z, cmap=plt.cm.get_cmap('viridis'), linewidth=0, antialiased=True, rstride=2, cstride=2)
    ax.set_xlabel('w0', labelpad=10)
    ax.set_ylabel('w1', labelpad=10)
    ax.set_zlabel('MSE', labelpad=10)
    # rotate
    ax.view_init(30, 30)

    # plot contours based on weights, 3d values and levels from errors
    levels = np.sort(np.array(errors))
    ax = fig.add_subplot(1, 2, 2)
    ax.contourf(w0, w1, mse_z, levels, alpha=.7, cmap=plt.cm.get_cmap('viridis'))
    # plot center lines
    ax.axhline(0, color='black', alpha=.5, dashes=[2, 4], linewidth=1)
    ax.axvline(0, color='black', alpha=0.5, dashes=[2, 4], linewidth=1)
    # plot arrows
    for i in range(len(old_w) - 1):
        ax.annotate('', xy=all_ws[i + 1, :], xytext=all_ws[i, :],
                    arrowprops={'arrowstyle': '->', 'color': 'r', 'lw': 1},
                    va='center', ha='center')

    # add contour lines
    CS = ax.contour(w0, w1, mse_z, levels, linewidths=1, colors='black')
    ax.clabel(CS, inline=1, fontsize=8)
    # add labels and plot
    ax.set_xlabel("w0")
    ax.set_ylabel("w1")
    plt.subplots_adjust(left=0.05, bottom=0.1, right=0.95, top=0.90, wspace=0.15, hspace=0)
    plt.savefig('experiment-results/general/gradient-vizualization.png')
    plt.show()


def describe_stock_data():
    # describe for data characteristics
    for loop_stock in ['BRK-A', 'GOOG', 'MSFT', 'SPY']:
        stock = Stock(loop_stock, START, END, FORCE_FETCH_STOCK)
        stock.fetch()
        prices = stock.df['Adj Close']
        training_range = prices[prices.index < datetime.datetime(2018, 1, 1)]
        print('Statistics for stock: ' + loop_stock + ' from whole dataset')
        print(prices.describe())
        print('Statistics for stock: ' + loop_stock + ' from training range')
        print(training_range.describe())


def experiment_1_2():
    # experiment 1 and 2
    exp_1_2_data = load_experiment_1_2_data()
    # exp_1 data
    exp_1 = exp_1_2_data[exp_1_2_data['period'] == 'validation']
    # plot exp 1
    plot_experiment_1_2(exp_1, 'experiment-1')

    # prepare exp 2 data
    exp_2 = exp_1_2_data[exp_1_2_data['period'] == 'testing']
    # try using 20 LSTMs on testing period
    plot_experiment_1_2(exp_2[(exp_2['model_units'] == '20')], 'experiment-2-initial', 0.85, 2)
    plot_experiment_1_2(exp_2[(exp_2['model_units'] == '512')], 'experiment-2-512-LSTMS', 0.85, 2)
    # plot standalone broken MSFT
    plot_testing_msft(exp_2[(exp_2['model'] == 'MSFT_20_testing_period') & (exp_2['type'] == 'scaled_prediction')])
    # print MSFT with 512 units without fixed normalization
    plot_experiment_1_2(exp_2[(exp_2['stock'] == 'MSFT') & (exp_2['model_units'] == '512')], 'experiment-2-512-todo-normalization', 0.85, 2)
    # plot full exp 2 stock graph with 512 units and fixed normalization
    plot_experiment_1_2(exp_1_2_data[exp_1_2_data['period'] == 'testing-fixed'], 'experiment-2-full-result', 0.85, 3)


def experiment_4():
    # experiment 4
    # generate and prepare data"""
    generated_data = load_exp_4_data()
    test_data, best_models = clean_exp_4_data(generated_data)
    # visualize data
    # plot month predictions for december 2021 with alternative evaluation
    plot_month_predictions(test_data)
    # plot box plot comparison of all models
    plot_training_testing_stats()
    # first line plot in thesis
    plot_brk_a_november(test_data)


def experiment_5():
    generated_data = load_experiment_5_data()
    # plot visualizations for experiment 5 (480 models)
    plot_experiment_5(generated_data)
    # plot learning rate and epochs of 72 models
    epochs_lr()


def general_vizualization():
    # plot tanh and sigmoid activation functions
    plot_activation_functions()
    # plot graph with SPY and volatility
    plot_volatility()
    # plot gradient descent visualization
    plot_gd_vizualization()
    # describe data stock data
    describe_stock_data()


if __name__ == "__main__":
    # generate experiment 1 and 2 plots
    experiment_1_2()

    # load experiment 3 data and print evaluation, graphs used in thesis were generated in experiment-3.py
    experiment_3()

    # create graphs for experiment 4 and print evaluation
    experiment_4()

    # experiment 5
    experiment_5()

    # collection of various graphs that were required in thesis
    general_vizualization()
