# %% [markdown]
# # Evaluation of your predictive model

# %% [markdown]
# ## Introduction
# Machine-learning models rely on optimizing an objective function, by seeking
# its minimum or maximum. It is important to understand that this objective
# function is usually decoupled from the evaluation metric that we want to
# optimize in practice. The objective function serves as a proxy to the
# evaluation metric.
# FIXME: add information about a loss function depending of the notebooks
# presented before the notebook about metrics.
#
# While other notebooks will give insights regarding algorithms and their
# associated objective functions, in this notebook we will focus on the
# metrics used to evaluate the performance of a predictive model.
#
# Selecting an evaluation metric will mainly depend on the model chosen to
# solve our datascience problem.

# %% [markdown]
# ## Classification
# We can recall that in a classification setting, the target `y` is categorical
# rather than continuous. We will use the blood transfusion dataset that will
# be fetched from OpenML.

# %%
import pandas as pd
from sklearn.datasets import fetch_openml

X, y = fetch_openml(
    name="blood-transfusion-service-center",
    as_frame=True, return_X_y=True,
)
# Make columns and classes more human-readable
X.columns = ["Recency", "Frequency", "Monetary", "Time"]
y = y.apply(
    lambda x: "donated" if x == "2" else "not donated"
).astype("category")
y.cat.categories

# %% [markdown]
# We can see that the target `y` contains 2 categories corresponding to whether
# or not a subject gave blood or not. We will use a logistic regression
# classifier to predict this outcome.
#
# First, we split the data into a training and a testing set.

# %%
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, shuffle=True, random_state=0, test_size=0.5
)

# %% [markdown]
# Once our data are split, we can learn a logistic regression classifier solely
# on the training data, keeping the testing data for the evaluation of the
# model.

# %%
from sklearn.linear_model import LogisticRegression

classifier = LogisticRegression()
classifier.fit(X_train, y_train)

# %% [markdown]
# Now, that our classifier is trained, we can provide some information about a
# subject and the classifier can predict whether or not the subject will donate
# blood.
#
# Let's create a synthetic sample corresponding to the following potential new
# donor: he/she donated blood 6 month ago and gave twice blood in the past for
# a total of 1000 c.c. He/she gave blood for the first time 20 months ago.

# %%
new_donor = [[6, 2, 1000, 20]]
classifier.predict(new_donor)

# %% [markdown]
# With these information, our classifier predicted that this synthetic subject
# is more likely to not donate blood. However, we have no possibility to ensure
# if the prediction is correct or not. That's why, we can now use the testing
# set for this purpose. First, we can predict whether or not a subject will
# give blood with the help of the trained classifier.

# %%
y_pred = classifier.predict(X_test)
y_pred[:5]

# %% [markdown]
# ### Accuracy as a baseline
# Now that we have these predictions, we could compare them with the true
# predictions (sometimes called ground-truth) which we did not use up to now.

# %%
y_test == y_pred

# %% [markdown]
# In the comparison above, a `True` value means that the value predicted by our
# classifier is identical to the real `prediction` while a `False` means that
# our classifier made a mistake. One way to get an overall statistic telling us
# how good the performance of our classifier are is to compute the number of
# time our classifier was right and divide it by the number of samples in our
# set (i.e. taking the mean of correct predictions)

# %%
import numpy as np

np.mean(y_test == y_pred)

# %% [markdown]
# This measure is also known as the accuracy. Here, our classifier is 78%
# accurate at classifying if subject will give blood. `scikit-learn` provides a
# function to compute this metric in the module `sklearn.metrics`.

# %%
from sklearn.metrics import accuracy_score

accuracy_score(y_test, y_pred)

# %% [markdown]
# Scikit-learn also have a build-in method named `score` which compute by
# default the accuracy score.

# %%
classifier.score(X_test, y_test)

# %% [markdown]
# ### Confusion matrix and derived metrics
# The comparison that we did above and the accuracy that we deducted did not
# take into account which type of error our classifier was doing. The accuracy
# is an aggregate of the error. However, we might be interested in a lower
# granularity level to know separately the error for the two following case:
# - we predicted that a person will give blood but she/he is not;
# - we predicted that a person will not give blood but she/he is.

# %%
from sklearn.metrics import plot_confusion_matrix

plot_confusion_matrix(classifier, X_test, y_test)

# %% [markdown]
# The in-diagonal numbers are related to predictions that agree
# with the true labels while off-diagonal numbers are related to
# misclassification. Besides, we now know the type of true or erroneous
# predictions the classifier did:
#
# * the top left corner is called true positive (TP) and correspond to a person
#   who gave blood and was predicted as such by the classifier;
# * the bottom right corner is called the true negative (TN) and correspond to
#   a person who did not gave blood and was predicted as such by the
#   classifier;
# * the top right corner is called false negative (FN) and correspond to a
#   person who gave blood but was predicted as not giving blood;
# * the bottom left corner is called false positive (FP) and correspond to a
#   person who did not give blood but was predicted as giving blood.
#
# Once we have split these information, we can compute statistics for
# highlighting the performance of our classifier in a particular setting. For
# instance, one could be interested in the fraction of persons who really gave
# blood when the classifier predicted so or the fraction of people predicted as
# giving blood among the total population that actually did so.
#
# The former statistic is known as the precision defined as TP / (TP + FP)
# while the latter statistic is known as the recall defined as TP / (TP + FN)
# We could, similarly than with the accuracy, manually compute these values.
# But scikit-learn provides functions to compute these statistics.

# %%
from sklearn.metrics import precision_score, recall_score

print(
    f"Precision score: {precision_score(y_test, y_pred, pos_label='donated')}"
    f"\nRecall score: {recall_score(y_test, y_pred, pos_label='donated')}"
)

# %% [markdown]
# These results are in line with what we could see in the confusion matrix.
# In the left column, more than half of the predictions were corrected leading
# to a precision above 0.5. However, our classifier mislabeled a lot of persons
# who gave blood as "not donated" leading to a very low recall of around 0.1.
#
# The precision and recall can be combined in a single score called the F1
# score (which is the harmonic mean of precision and recall)

# %%
from sklearn.metrics import f1_score

f1_score(y_test, y_pred, pos_label='donated')

# %% [markdown]
# ### The issue of class imbalance
# At this stage, we could ask ourself a reasonable question. While the accuracy
# did not look bad (i.e. 77%), the F1 score is relatively low (i.e. 21%).
#
# As we mentioned, precision and recall only focus on the positive label while
# the accuracy is taking both aspects into account. In addition,
# we omit to look at the ratio class
# occurrence. We could check this ratio in the training set.

# %%
from collections import Counter

class_counts = pd.Series(Counter(y_train))
class_counts /= class_counts.sum()
class_counts

# %% [markdown]
# So we can observed that the positive class `'donated'` is only 24% of the
# total number of instances. The good accuracy of our classifier is then linked
# to its capability of predicting correctly the negative class `'not donated'`
# which could be relevant or not depending of the application. We can
# illustrate the issue using a dummy classifier as a baseline.

# %%
from sklearn.dummy import DummyClassifier

dummy_classifier = DummyClassifier(
    strategy="constant", constant="not donated"
)
dummy_classifier.fit(X_train, y_train).score(X_test, y_test)

# %% [markdown]
# This dummy classifier will always predict the negative class `'not donated'`.
# We obtain an accuracy score of 76%. Therefore, it means that this classifier,
# without learning anything from the data `X` is capable of predicting as
# accurately than our logistic regression. 76% represents the baseline that
# any classifier should overperform to not be a random classifier.
#
# The problem illustrated above is also known as the class imbalance problem
# where the accuracy should not be used. In this case, one should either use
# the precision, recall, or F1 score as presented above or the balanced
# accuracy score instead of the accuracy.

# %%
from sklearn.metrics import balanced_accuracy_score

balanced_accuracy_score(y_test, y_pred)
# %% [markdown]
# The balanced accuracy is equivalent to the accuracy in the context of
# balanced classes. It is defined as the average recall obtained on each class.
#
# ### Evaluation with different probability threshold
#
# All statistics that we presented up to now rely on `classifier.predict` which
# provide the most likely label. However, we don't use the probability
# associated with this prediction or in other words how sure are the classifier
# confident about this prediction. By default, the prediction of a classifier
# correspons to a thresholding at a 0.5 probability, in a binary classification
# problem. We can quickly check this relationship with the classifier that
# we trained.

# %%
y_proba = pd.DataFrame(
    classifier.predict_proba(X_test),
    columns=classifier.classes_
)
y_proba[:5]

# %%
y_pred = classifier.predict(X_test)
y_pred[:5]

# %%
# Since probabilities sum to 1 we can get the class with the highest
# probability without using the threshold 0.5
equivalence_pred_proba = (
    y_proba.idxmax(axis=1).to_numpy() == y_pred
)
np.all(equivalence_pred_proba)

# %% [markdown]
# The default decision threshold (0.5) might not be the best threshold leading
# to optimal performance of our classifier. In this case, one can vary the
# decision threshold and therefore the underlying prediction and compute the
# same statistic than presented earlier. Usually, two metrics are computed and
# reported as a curve. Each metric is belonging to a graph axis and a point on
# the graph corresponds to a specific decision threshold. Let's start by
# computing the precision-recall curve.

# %%
import matplotlib.pyplot as plt
from sklearn.metrics import precision_recall_curve
from sklearn.metrics import average_precision_score

y_pred = classifier.predict_proba(X_test)
pos_label = "donated"
precision, recall, threshold = precision_recall_curve(
    y_test, y_pred[:, 0], pos_label=pos_label,
)
average_precision = average_precision_score(
    y_test, y_pred[:, 0], pos_label=pos_label,
)
plt.plot(
    recall, precision,
    color="tab:orange", linewidth=3,
    marker=".", markerfacecolor="tab:blue", markeredgecolor="tab:blue",
    label=f"Average Precision: {average_precision:.2f}",
)
plt.xlabel(f"Recall\n (Positive label: {pos_label})")
plt.ylabel(f"Precision\n (Positive label: {pos_label})")
plt.legend()

# # FIXME: to be used when solved in scikit-learn
# from sklearn.metrics import plot_precision_recall_curve

# disp = plot_precision_recall_curve(
#     classifier, X_test, y_test, pos_label='donated',
# )

# %% [markdown]
# On this curve, each blue dot correspond to a certain level of probability
# which we used as a decision threshold. We can see that by varying this
# decision threshold, we get different compromise precision vs. recall.
#
# A perfect classifier is expected to have a precision at 1 even when varying
# the recall. A metric characterizing the curve is linked to the area under the
# curve (AUC), named averaged precision. With a ideal classifier, the
# average precision will be 1.
#
# While the precision and recall metric focuses on the positive class, one
# might be interested into the compromise between performance to discriminate
# positive and negative classes. The statistics used in this case are the
# sensitivity and specificity. The sensitivity is just another denomination for
# recall. However, the specificity measures the proportion of well classified
# samples from the negative class defined as TN / (TN + FP). Similarly to the
# precision-recall curve, sensitivity and specificity are reported with a curve
# called the receiver operating characteristic (ROC) curve. We will show such
# curve:

# %%
from sklearn.metrics import roc_curve
from sklearn.metrics import roc_auc_score

fpr, tpr, threshold = roc_curve(y_test, y_pred[:, 0], pos_label=pos_label)
# FIXME: roc_auc_score has a bug and we need to give the inverse probability
# vector. Should be changed when the following is merged and released:
# https://github.com/scikit-learn/scikit-learn/pull/17594
roc_auc = roc_auc_score(y_test, y_pred[:, 1])
plt.plot(
    fpr, tpr,
    color="tab:orange", linewidth=3,
    marker=".", markerfacecolor="tab:blue", markeredgecolor="tab:blue",
    label=f"ROC-AUC: {roc_auc:.2f}"
)
plt.plot([0, 1], [0, 1], "--", color="tab:green", label="Chance")
plt.xlabel(f"1 - Specificity\n (Positive label: {pos_label})")
plt.ylabel(f"Sensitivity\n (Positive label: {pos_label})")
plt.legend()

# # FIXME: to be used when solved in scikit-learn
# from sklearn.metrics import plot_roc_curve

# plot_roc_curve(classifier, X_test, y_test, pos_label='donated')

# %% [markdown]
# This curve is built with the same principle than with the precision-recall
# curve: we vary the probability threshold to compute "hard" prediction and
# compute the metrics. As with the precision-recall curve as well, we can
# compute the area under the ROC (ROC-AUC) to characterize the performance of
# our classifier. However, this is important to observer that the lower bound
# of the ROC-AUC is 0.5. Indeed, we represented the performance of a dummy
# classifier (i.e. green dashed line) to show that the worse performance
# obtained will always be above this line.
#
# ### Link between confusion matrix, precision-recall curve and ROC curve
#
# TODO: ipywidgets to play with interactive curve

# %%
def plot_pr_curve(classifier, X_test, y_test, pos_label,
                  probability_threshold, ax):
    y_pred = classifier.predict_proba(X_test)
    precision, recall, threshold = precision_recall_curve(
        y_test, y_pred[:, 0], pos_label=pos_label,
    )
    average_precision = average_precision_score(
        y_test, y_pred[:, 0], pos_label=pos_label,
    )
    ax.plot(
        recall, precision,
        color="tab:orange", linewidth=3,
        label=f"Average Precision: {average_precision:.2f}",
    )
    threshold_idx = np.searchsorted(
        threshold, probability_threshold,
    )
    ax.plot(
        recall[threshold_idx], precision[threshold_idx],
        color="tab:blue", marker=".", markersize=10,
    )
    ax.plot(
        [recall[threshold_idx], recall[threshold_idx]],
        [0, precision[threshold_idx]],
        '--', color="tab:blue",
    )
    ax.plot(
        [0, recall[threshold_idx]],
        [precision[threshold_idx], precision[threshold_idx]],
        '--', color="tab:blue",
    )
    ax.set_xlabel(f"Recall")
    ax.set_ylabel(f"Precision")
    ax.set_xlim([0, 1])
    ax.set_ylim([0, 1])
    ax.legend()
    return ax

# %%
def plot_roc_curve(classifier, X_test, y_test, pos_label,
                   probability_threshold, ax):
    y_pred = classifier.predict_proba(X_test)
    fpr, tpr, threshold = roc_curve(y_test, y_pred[:, 0], pos_label=pos_label)
    roc_auc = roc_auc_score(y_test, y_pred[:, 1])
    ax.plot(
        fpr, tpr,
        color="tab:orange", linewidth=3,
        label=f"ROC-AUC: {roc_auc:.2f}"
    )
    ax.plot([0, 1], [0, 1], "--", color="tab:green", label="Chance")
    threshold_idx = np.searchsorted(
        threshold[::-1], probability_threshold,
    )
    threshold_idx = len(threshold) - threshold_idx - 1
    ax.plot(
        fpr[threshold_idx], tpr[threshold_idx],
        color="tab:blue", marker=".", markersize=10,
    )
    ax.plot(
        [fpr[threshold_idx], fpr[threshold_idx]],
        [0, tpr[threshold_idx]],
        '--', color="tab:blue",
    )
    ax.plot(
        [0, fpr[threshold_idx]],
        [tpr[threshold_idx], tpr[threshold_idx]],
        '--', color="tab:blue",
    )
    ax.set_xlabel(f"1 - Specificity")
    ax.set_ylabel(f"Sensitivity")
    ax.set_xlim([0, 1])
    ax.set_ylim([0, 1])
    ax.legend()
    return ax

# %%
def plot_confusion_matrix_with_threshold(classifier, X_test, y_test, pos_label,
                                         probability_threshold, ax):
    from itertools import product
    from sklearn.metrics import confusion_matrix

    class_idx = np.where(classifier.classes_ == pos_label)[0][0]
    n_classes = len(classifier.classes_)

    y_pred = classifier.predict_proba(X_test)
    y_pred = (y_pred[:, class_idx] > probability_threshold).astype(int)

    cm = confusion_matrix(
        (y_test == pos_label).astype(int), y_pred,
    )
    im_ = ax.imshow(cm, interpolation='nearest')

    text_ = None
    cmap_min, cmap_max = im_.cmap(0), im_.cmap(256)

    text_ = np.empty_like(cm, dtype=object)

    # print text with appropriate color depending on background
    thresh = (cm.max() + cm.min()) / 2.0

    for i, j in product(range(n_classes), range(n_classes)):
        color = cmap_max if cm[i, j] < thresh else cmap_min

        text_cm = format(cm[i, j], '.2g')
        if cm.dtype.kind != 'f':
            text_d = format(cm[i, j], 'd')
            if len(text_d) < len(text_cm):
                text_cm = text_d

        text_[i, j] = ax.text(
            j, i, text_cm, ha="center", va="center", color=color
        )

    ax.set(
        xticks=np.arange(n_classes),
        yticks=np.arange(n_classes),
        xticklabels=classifier.classes_[[int(not bool(class_idx)), class_idx]],
        yticklabels=classifier.classes_[[int(not bool(class_idx)), class_idx]],
        ylabel="True label",
        xlabel="Predicted label"
    )

# %%
def plot_pr_roc(threshold):
    # FIXME: we could optimize the plotting by only updating the the
    fig, axs = plt.subplots(ncols=3, figsize=(21, 6))
    plot_pr_curve(
        classifier, X_test, y_test, pos_label="donated",
        probability_threshold=threshold, ax=axs[0],
    )
    plot_roc_curve(
        classifier, X_test, y_test, pos_label="donated",
        probability_threshold=threshold, ax=axs[1]
    )
    plot_confusion_matrix_with_threshold(
        classifier, X_test, y_test, pos_label="donated",
        probability_threshold=threshold, ax=axs[2]
    )
    fig.suptitle("Overall performance with positive class 'donated'")

# %%
def plot_pr_roc_interactive():
    from ipywidgets import interactive, FloatSlider
    slider = FloatSlider(min=0, max=1, step=0.01, value=0.5)
    return interactive(plot_pr_roc, threshold=slider)

# %%
plot_pr_roc_interactive()

# %% [markdown]
# ## Regression
# Unlike in the classification problem, the target `y` is a continuous
# variable in regression problem. Therefore, the classification metrics can be
# used to evaluate the performance of a model. Instead, there exists a set of
# metric dedicated to regression.

# %%
data = pd.read_csv(
    ("https://raw.githubusercontent.com/christophM/interpretable-ml-book/"
     "master/data/bike.csv"),
)
# rename the columns with human-readable names
data = data.rename(columns={
    "yr": "year", "mnth": "month", "temp": "temperature", "hum": "humidity",
    "cnt": "count", "days_since_2011": "days since 2011"
})
# convert the categorical columns with a proper category data type
for col in data.columns:
    if data[col].dtype.kind == "O":
        data[col] = data[col].astype("category")

# separate the target from the original data
X = data.drop(columns=["count"])
y = data["count"]

# %%
X.head()

# %%
plt.hist(y, bins=50, density=True)
plt.xlabel("Number of bike rentals")
plt.ylabel("Probability")
plt.title("Target distribution")

# %% [markdown]
# Our problem can be formulated as follow: we would like to infer the number of
# bike rentals from data related to the current day. The number of bike rentals
# is a number that can vary in the interval [0, infinity) (if the number of
# bike available is infinite). As in the previous section, we will train a
# model and we will evaluate its performance by introducing the different
# regression metrics.
#
# First, we split the data into a training and a testing set.

# %%
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, shuffle=True, random_state=0
)

# %% [markdown]
# ### Baseline model
# We will use a random forest as a model. However, we first need to check the
# type of data that we are dealing with:

# %%
X_train.info()

# %% [markdown]
# While some features are numeric, some have been tagged as `category`. These
# features need to be encoded in a proper way such that our random forest can
# deal with them. The simplest solution is to use an `OrdinalEncoder`.
# Regarding, the numerical features, we don't need to do anything. Thus, we
# will create a preprocessing steps to take care about this encoding.

# %%
from sklearn.compose import make_column_transformer
from sklearn.compose import make_column_selector as selector
from sklearn.preprocessing import OrdinalEncoder

categorical_selector = selector(dtype_include="category")
preprocessor = make_column_transformer(
    (OrdinalEncoder(), categorical_selector),
    remainder="passthrough",
)

X_train_preprocessed = pd.DataFrame(
    preprocessor.fit_transform(X_train),
    columns=(
        categorical_selector(X_train) +
        [col for col in X_train.columns
         if col not in categorical_selector(X_train)]
    )
)
X_train_preprocessed.head()

# %% [markdown]
# Just to have some insights about the preprocessing, we manually preprocessed
# the training data and we can observe that the original strings were encoded
# with numbers. We can now create our model.

# %%
from sklearn.pipeline import make_pipeline
from sklearn.ensemble import RandomForestRegressor

regressor = make_pipeline(preprocessor, RandomForestRegressor())
regressor.fit(X_train, y_train)

# %% [markdown]
# As for classifiers, regressors have a `score` method which will compute the
# :math:`R^2` score (also known as the coefficient of determination) by
# default:

# %%
regressor.score(X_test, y_test)

# %% [markdown]
# The :math:`R^2` score represents the proportion of variance of the target
# explained by the independent variables in the model. The best score possible
# is 1 but there is no lower bound. However, a model which would predict the
# expected value of the target would get a score of 0.

# %%
from sklearn.dummy import DummyRegressor

dummy_regressor = DummyRegressor(strategy="mean")
dummy_regressor.fit(X_train, y_train).score(X_test, y_test)

# %% [markdown]
# The :math:`R^2` score gives insights regarding the goodness of fit of the
# model. However, this score cannot be compared from one dataset to another and
# the value obtained does not have a meaningful interpretation regarding the
# original unit of the target. If we want to get such interpretable score, we
# will be interested into the median or mean absolute error.

# %%
from sklearn.metrics import mean_absolute_error

y_pred = regressor.predict(X_test)
print(
    f"Mean absolute error: {mean_absolute_error(y_test, y_pred):.0f}"
)

# %% [markdown]
# By computing the mean absolute error, we can interpret that our model is
# predicting in average 507 bike rentals away from the truth. The mean can be
# impacted by large error while for some application, we would like to discard
# them and we can in this case opt for the median absolute error.

# %%
from sklearn.metrics import median_absolute_error

print(
    f"Median absolute error: {median_absolute_error(y_test, y_pred):.0f}"
)

# %% [markdown]
# In this case, our model make an error of 405 bikes.
# FIXME: **not sure how to introduce the `mean_squared_error`.**

# %% [markdown]
# In addition of metrics, we can visually represent the results by plotting
# the predicted values versus the true values.

# %%
def plot_predicted_vs_actual(y_true, y_pred, title=None):
    plt.scatter(y_true, y_pred)

    max_value = np.max([y_true.max(), y_pred.max()])
    plt.plot(
        [0, max_value],
        [0, max_value],
        color="tab:orange",
        linewidth=3,
        label="Perfect fit",
    )

    plt.xlabel("True values")
    plt.ylabel("Predicted values")
    plt.axis("square")
    plt.legend()
    if title is not None:
        plt.title(title)

plot_predicted_vs_actual(y_test, y_pred)

# %% [markdown]
# On this plot, the perfect prediction will lay on the diagonal line. This plot
# allows to detect if the model have a specific regime where our model does not
# work as expected or has some kinda of bias.
#
# Let's take an example using the house prices in Ames.

# %%
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import RidgeCV

X, y = fetch_openml(name="house_prices", as_frame=True, return_X_y=True)
X = X.select_dtypes(np.number).drop(
    columns=["LotFrontage", "GarageYrBlt", "MasVnrArea"]
)

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1)

# %% [markdown]
# We will fit a ridge regressor on the data and plot the prediction versus the
# actual values.

# %%
model = make_pipeline(StandardScaler(), RidgeCV())
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

plot_predicted_vs_actual(y_test, y_pred, title="House prices in Ames")

# %% [markdown]
# On this plot, we see that for the large "True values", our model tend to
# under-estimate the price of the house. Typically, this issue arises when
# the target to predict does not follow a normal distribution and the model
# could benefit of an intermediate target transformation.

# %%
from sklearn.preprocessing import QuantileTransformer
from sklearn.compose import TransformedTargetRegressor

model_transformed_target = TransformedTargetRegressor(
    regressor=model,
    transformer=QuantileTransformer(
        n_quantiles=900, output_distribution="normal"
    ),
)
model_transformed_target.fit(X_train, y_train)
y_pred = model_transformed_target.predict(X_test)

plot_predicted_vs_actual(y_test, y_pred, title="House prices in Ames")

# %% [markdown]
# Thus, once we transformed the target, we see that we corrected some of the
# high values.
#
# ## Summary
# In this notebook, we presented the metrics and plots useful to evaluate and
# get insights about models. We both focus on regression and classification
# problems.


# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: percent
#       format_version: '1.3'
#       jupytext_version: 1.11.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# %% [raw] raw_mimetype="text/restructuredtext"
# .. _ug_cartesian:
#
# Cartesian axes
# ==============
#
# This section documents features used for modifying Cartesian *x* and *y*
# axes, including axis scales, tick locations, tick label formatting, and
# several twin and dual axes commands.

# %% [raw] raw_mimetype="text/restructuredtext"
# .. _ug_locators:
#
# Tick locations
# --------------
#
# Matplotlib `tick locators
# <https://matplotlib.org/stable/gallery/ticks_and_spines/tick-locators.html>`__
# select sensible tick locations based on the axis data limits. In proplot, you can
# change the tick locator using the `~proplot.axes.CartesianAxes.format` keyword
# arguments `xlocator`, `ylocator`, `xminorlocator`, and `yminorlocator` (or their
# aliases, `xticks`, `yticks`, `xminorticks`, and `yminorticks`). This is powered by
# the `~proplot.constructor.Locator` :ref:`constructor function <why_constructor>`.
#
# You can use these keyword arguments to apply built-in matplotlib
# `~matplotlib.ticker.Locator`\ s by their "registered" names
# (e.g., ``xlocator='log'``), to draw ticks every ``N`` data values with
# `~matplotlib.ticker.MultipleLocator` (e.g., ``xlocator=2``), or to tick the
# specific locations in a list using `~matplotlib.ticker.FixedLocator` (just
# like `~matplotlib.axes.Axes.set_xticks` and `~matplotlib.axes.Axes.set_yticks`).
# If you want to work with the locator classes directly, they are available in the
# top-level namespace (e.g., ``xlocator=pplt.MultipleLocator(...)`` is allowed).
#
# To generate lists of tick locations, we recommend using proplot's
# `~proplot.utils.arange` function -- it’s basically an endpoint-inclusive
# version of `numpy.arange`, which is usually what you'll want in this context.

# %%
import proplot as pplt
import numpy as np
state = np.random.RandomState(51423)
pplt.rc.update(
    metawidth=1, fontsize=10,
    metacolor='dark blue', suptitlecolor='dark blue',
    titleloc='upper center', titlecolor='dark blue', titleborder=False,
    axesfacecolor=pplt.scale_luminance('powderblue', 1.15),
)
fig = pplt.figure(share=False, refwidth=5, refaspect=(8, 1))
fig.format(suptitle='Tick locators demo')

# Step size for tick locations
ax = fig.subplot(711, title='MultipleLocator')
ax.format(xlim=(0, 200), xminorlocator=10, xlocator=30)

# Specific list of locations
ax = fig.subplot(712, title='FixedLocator')
ax.format(xlim=(0, 10), xminorlocator=0.1, xlocator=[0, 0.3, 0.8, 1.6, 4.4, 8, 8.8])

# Ticks at numpy.linspace(xmin, xmax, N)
ax = fig.subplot(713, title='LinearLocator')
ax.format(xlim=(0, 10), xlocator=('linear', 21))

# Logarithmic locator, used automatically for log scale plots
ax = fig.subplot(714, title='LogLocator')
ax.format(xlim=(1, 100), xlocator='log', xminorlocator='logminor')

# Maximum number of ticks, but at "nice" locations
ax = fig.subplot(715, title='MaxNLocator')
ax.format(xlim=(1, 7), xlocator=('maxn', 11))

# Hide all ticks
ax = fig.subplot(716, title='NullLocator')
ax.format(xlim=(-10, 10), xlocator='null')

# Tick locations that cleanly divide 60 minute/60 second intervals
ax = fig.subplot(717, title='Degree-Minute-Second Locator (requires cartopy)')
ax.format(xlim=(0, 2), xlocator='dms', xformatter='dms')

pplt.rc.reset()

# %% [raw] raw_mimetype="text/restructuredtext"
# .. _ug_formatters:
#
# Tick formatting
# ---------------
#
# Matplotlib `tick formatters
# <https://matplotlib.org/stable/gallery/ticks_and_spines/tick-formatters.html>`__
# convert floating point numbers to nicely-formatted tick labels. In proplot, you can
# change the tick formatter using the `~proplot.axes.CartesianAxes.format` keyword
# arguments `xformatter` and `yformatter` (or their aliases, `xticklabels` and
# `yticklabels`). This is powered by the `~proplot.constructor.Formatter`
# :ref:`constructor function <why_constructor>`.
#
# You can use these keyword arguments to apply built-in matplotlib
# `~matplotlib.ticker.Formatter`\ s by their "registered" names
# (e.g., ``xformatter='log'``), to apply a ``%``-style format directive with
# `~matplotlib.ticker.FormatStrFormatter` (e.g., ``xformatter='%.0f'``), or
# to apply custom tick labels with `~matplotlib.ticker.FixedFormatter` (just
# like `~matplotlib.axes.Axes.set_xticklabels`). You can also apply one of proplot's
# new tick formatters -- for example, ``xformatter='deglat'`` to label ticks
# as geographic latitude coordinates, ``xformatter='pi'`` to label ticks as
# fractions of :math:`\pi`, or ``xformatter='sci'`` to label ticks with
# scientific notation. If you want to work with the formatter classes
# directly, they are available in the top-level namespace
# (e.g., ``xformatter=pplt.SciFormatter(...)`` is allowed).
#
# Proplot also changes the default tick formatter to
# `~proplot.ticker.AutoFormatter`. This class trims trailing zeros by
# default, can optionally omit or wrap tick values within particular
# number ranges, and can add prefixes and suffixes to each label. See
# `~proplot.ticker.AutoFormatter` for details. To disable the trailing
# zero-trimming feature, set :rcraw:`formatter.zerotrim` to ``False``.

# %%
import proplot as pplt
pplt.rc.fontsize = 11
pplt.rc.metawidth = 1.5
pplt.rc.gridwidth = 1

# Create the figure
fig, axs = pplt.subplots(ncols=2, nrows=2, refwidth=1.5, share=False)
axs.format(
    ytickloc='both', yticklabelloc='both',
    titlepad='0.5em', suptitle='Default formatters demo'
)

# Formatter comparison
locator = [0, 0.25, 0.5, 0.75, 1]
axs[0].format(xformatter='scalar', yformatter='scalar', title='Matplotlib formatter')
axs[1].format(title='Proplot formatter')
axs[:2].format(xlocator=locator, ylocator=locator)

# Limiting the tick range
axs[2].format(
    title='Omitting tick labels', ticklen=5, xlim=(0, 5), ylim=(0, 5),
    xtickrange=(0, 2), ytickrange=(0, 2), xlocator=1, ylocator=1
)

# Setting the wrap range
axs[3].format(
    title='Wrapping the tick range', ticklen=5, xlim=(0, 7), ylim=(0, 6),
    xwraprange=(0, 5), ywraprange=(0, 3), xlocator=1, ylocator=1
)
pplt.rc.reset()

# %%
import proplot as pplt
import numpy as np
pplt.rc.update(
    metawidth=1.2, fontsize=10, axesfacecolor='gray0', figurefacecolor='gray2',
    metacolor='gray8', gridcolor='gray8', titlecolor='gray8', suptitlecolor='gray8',
    titleloc='upper center', titleborder=False,
)
fig = pplt.figure(refwidth=5, refaspect=(8, 1), share=False)

# Scientific notation
ax = fig.subplot(911, title='SciFormatter')
ax.format(xlim=(0, 1e20), xformatter='sci')

# N significant figures for ticks at specific values
ax = fig.subplot(912, title='SigFigFormatter')
ax.format(
    xlim=(0, 20), xlocator=(0.0034, 3.233, 9.2, 15.2344, 7.2343, 19.58),
    xformatter=('sigfig', 2),  # 2 significant digits
)

# Fraction formatters
ax = fig.subplot(913, title='FracFormatter')
ax.format(xlim=(0, 3 * np.pi), xlocator=np.pi / 4, xformatter='pi')
ax = fig.subplot(914, title='FracFormatter')
ax.format(xlim=(0, 2 * np.e), xlocator=np.e / 2, xticklabels='e')

# Geographic formatters
ax = fig.subplot(915, title='Latitude Formatter')
ax.format(xlim=(-90, 90), xlocator=30, xformatter='deglat')
ax = fig.subplot(916, title='Longitude Formatter')
ax.format(xlim=(0, 360), xlocator=60, xformatter='deglon')

# User input labels
ax = fig.subplot(917, title='FixedFormatter')
ax.format(
    xlim=(0, 5), xlocator=np.arange(5),
    xticklabels=['a', 'b', 'c', 'd', 'e'],
)

# Custom style labels
ax = fig.subplot(918, title='FormatStrFormatter')
ax.format(xlim=(0, 0.001), xlocator=0.0001, xformatter='%.E')
ax = fig.subplot(919, title='StrMethodFormatter')
ax.format(xlim=(0, 100), xtickminor=False, xlocator=20, xformatter='{x:.1f}')
fig.format(ylocator='null', suptitle='Tick formatters demo')
pplt.rc.reset()

# %% [raw] raw_mimetype="text/restructuredtext"
# .. _pandas: https://pandas.pydata.org
#
# .. _ug_datetime:
#
# Datetime ticks
# --------------
#
# The above examples all assumed typical "numeric" axes. However
# `~proplot.axes.CartesianAxes.format` can also modify the tick locations and tick
# labels for "datetime" axes. To draw ticks on each occurence of some particular time
# unit, use a unit string (e.g., ``xlocator='month'``). To draw ticks every ``N`` time
# units, use a (unit, N) tuple (e.g., ``xlocator=('day', 5)``). For `% style formatting
# <https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior>`__
# of datetime tick labels with `~datetime.datetime.strftime`, you can use a string
# containing ``'%'`` (e.g. ``xformatter='%Y-%m-%d'``). By default, *x* axis datetime
# axis labels are rotated 90 degrees, like in `pandas`_. This can be disabled by
# passing ``xrotation=0`` to `~proplot.axes.CartesianAxes.format` or by setting
# :rcraw:`formatter.timerotation` to ``0``. See `~proplot.constructor.Locator`
# and `~proplot.constructor.Formatter` for details.

# %%
import proplot as pplt
import numpy as np
pplt.rc.update(
    metawidth=1.2, fontsize=10, ticklenratio=0.7,
    figurefacecolor='w', axesfacecolor='pastel blue',
    titleloc='upper center', titleborder=False,
)
fig, axs = pplt.subplots(nrows=5, refwidth=6, refaspect=(8, 1), share=False)

# Default date locator
# This is enabled if you plot datetime data or set datetime limits
ax = axs[0]
ax.format(
    xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-02')),
    title='Auto date locator and formatter'
)

# Concise date formatter introduced in matplotlib 3.1
ax = axs[1]
ax.format(
    xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-01')),
    xformatter='concise', title='Concise date formatter',
)

# Minor ticks every year, major every 10 years
ax = axs[2]
ax.format(
    xlim=(np.datetime64('2000-01-01'), np.datetime64('2050-01-01')),
    xlocator=('year', 10), xformatter='\'%y', title='Ticks every N units',
)

# Minor ticks every 10 minutes, major every 2 minutes
ax = axs[3]
ax.format(
    xlim=(np.datetime64('2000-01-01T00:00:00'), np.datetime64('2000-01-01T12:00:00')),
    xlocator=('hour', range(0, 24, 2)), xminorlocator=('minute', range(0, 60, 10)),
    xformatter='T%H:%M:%S', title='Ticks at specific intervals',
)

# Month and year labels, with default tick label rotation
ax = axs[4]
ax.format(
    xlim=(np.datetime64('2000-01-01'), np.datetime64('2008-01-01')),
    xlocator='year', xminorlocator='month',  # minor ticks every month
    xformatter='%b %Y', title='Ticks with default rotation',
)
axs[:4].format(xrotation=0)  # no rotation for the first four examples
fig.format(ylocator='null', suptitle='Datetime locators and formatters demo')
pplt.rc.reset()

# %% [raw] raw_mimetype="text/restructuredtext"
# .. _ug_loc:
#
# Axis positions
# --------------
#
# The locations of `axis spines
# <https://matplotlib.org/stable/gallery/ticks_and_spines/spines.html>`__,
# tick marks, tick labels, and axis labels can be controlled with
# `proplot.axes.CartesianAxes.format` keyword arguments like `xspineloc`
# (shorthand `xloc`), `xtickloc`, `xticklabelloc`, and `xlabelloc`. Valid
# locations include ``'left'``, ``'right'``, ``'top'``, ``'bottom'``, ``'neither'``,
# ``'none'``, or ``'both'``. Spine locations can also be set to a valid
# `~matplotlib.spines.Spine.set_position` value, e.g. ``'zero'`` or
# ``('axes', 1.5)``. The top or right spine is used when the coordinate is
# more than halfway across the axes. This is often convenient when passing
# e.g. `loc` to :ref:`"alternate" axes commands <ug_alt>`. These keywords
# provide the functionality of matplotlib's `~matplotlib.axis.YAxis.tick_left`,
# `~matplotlib.axis.YAxis.tick_right`, `~matplotlib.axis.XAxis.tick_top`, and
# `~matplotlib.axis.XAxis.tick_bottom`, and `~matplotlib.spines.Spine.set_position`,
# but with additional flexibility.

# %%
import proplot as pplt
pplt.rc.update(
    metawidth=1.2, fontsize=10, gridcolor='coral',
    axesedgecolor='deep orange', figurefacecolor='white',
)
fig = pplt.figure(share=False, refwidth=2, suptitle='Axis locations demo')

# Spine location demonstration
ax = fig.subplot(121, title='Various locations')
ax.format(xloc='top', xlabel='original axis')
ax.twiny(xloc='bottom', xcolor='black', xlabel='locked twin')
ax.twiny(xloc=('axes', 1.25), xcolor='black', xlabel='offset twin')
ax.twiny(xloc=('axes', -0.25), xcolor='black', xlabel='offset twin')
ax.format(ytickloc='both', yticklabelloc='both')
ax.format(ylabel='labels on both sides')

# Other locations locations
ax = fig.subplot(122, title='Zero-centered spines', titlepad='1em')
ax.format(xlim=(-10, 10), ylim=(-3, 3), yticks=1)
ax.format(xloc='zero', yloc='zero')
pplt.rc.reset()

# %% [raw] raw_mimetype="text/restructuredtext"
# .. _ug_scales:
#
# Axis scales
# -----------
#
# "Axis scales" like ``'linear'`` and ``'log'`` control the *x* and *y* axis
# coordinate system. To change the axis scale, pass e.g. ``xscale='log'`` or
# ``yscale='log'`` to `~proplot.axes.Axes.format`. This is powered by the
# `~proplot.constructor.Scale` :ref:`constructor function <why_constructor>`.
# Proplot makes several changes to the axis scale API:
#
# * The `~proplot.ticker.AutoFormatter` formatter is now used for all axis scales
#   by default, including ``'log'`` and ``'symlog'``. Matplotlib's behavior can
#   be restored by passing e.g. ``xformatter='log'`` or ``yformatter='log'`` to
#   `~proplot.axes.CartesianAxes.format`.
# * To make its behavior consistent with `~proplot.constructor.Locator` and
#   `~proplot.constructor.Formatter`, the `~proplot.constructor.Scale`
#   constructor function returns instances of `~matplotlib.scale.ScaleBase`,
#   and `~matplotlib.axes.Axes.set_xscale` and
#   `~matplotlib.axes.Axes.set_yscale` now accept these class instances in
#   addition to "registered" names like ``'log'``.
# * While matplotlib axis scales must be instantiated with an
#   `~matplotlib.axis.Axis` instance (for backwards compatibility reasons),
#   proplot axis scales can be instantiated without the axis instance
#   (e.g., ``pplt.LogScale()`` instead of ``pplt.LogScale(ax.xaxis)``).
# * The default `subs` for the ``'symlog'`` axis scale is now ``np.arange(1, 10)``,
#   and the default `linthresh` is now ``1``. Also the ``'log'`` and ``'symlog'``
#   axis scales now accept the keywords `base`, `linthresh`, `linscale`, and
#   `subs` rather than keywords with trailing ``x`` or ``y``.
#
# Proplot also includes a few new axis scales. The ``'cutoff'`` scale (see
# `~proplot.scale.CutoffScale`) is useful when the statistical distribution
# of your data is very unusual. The ``'sine'`` scale `~proplot.scale.SineLatitudeScale`
# scales the axis with a sine function (resulting in an area-weighted spherical latitude
# coordinate) and the ``'mercator'`` scale `~proplot.scale.MercatorLatitudeScale`
# scales the axis with the Mercator projection latitude coordinate. The
# ``'inverse'`` scale `~proplot.scale.InverseScale` can be useful when
# working with spectral data, especially with :ref:`"dual" unit axes <ug_dual>`.
# If you want to work with the axis scale classes directly, they are available
# in the top-level namespace (e.g., ``xscale=pplt.CutoffScale(...)`` is allowed).

# %%
import proplot as pplt
import numpy as np
N = 200
lw = 3
pplt.rc.update({'meta.width': 1, 'label.weight': 'bold', 'tick.labelweight': 'bold'})
fig = pplt.figure(refwidth=1.8, share=False)

# Linear and log scales
ax1 = fig.subplot(221)
ax1.format(yscale='linear', ylabel='linear scale')
ax2 = fig.subplot(222)
ax2.format(ylim=(1e-3, 1e3), yscale='log', ylabel='log scale')
for ax in (ax1, ax2):
    ax.plot(np.linspace(0, 1, N), np.linspace(0, 1000, N), lw=lw)

# Symlog scale
ax = fig.subplot(223)
ax.format(yscale='symlog', ylabel='symlog scale')
ax.plot(np.linspace(0, 1, N), np.linspace(-1000, 1000, N), lw=lw)

# Logit scale
ax = fig.subplot(224)
ax.format(yscale='logit', ylabel='logit scale')
ax.plot(np.linspace(0, 1, N), np.linspace(0.01, 0.99, N), lw=lw)

fig.format(suptitle='Axis scales demo', ytickminor=True)
pplt.rc.reset()

# %%
import proplot as pplt
import numpy as np

# Create figure
x = np.linspace(0, 4 * np.pi, 100)
dy = np.linspace(-1, 1, 5)
ys = (np.sin(x), np.cos(x))
state = np.random.RandomState(51423)
data = state.rand(len(dy) - 1, len(x) - 1)
colors = ('coral', 'sky blue')
cmap = pplt.Colormap('grays', right=0.8)
fig, axs = pplt.subplots(nrows=4, refaspect=(5, 1), figwidth=5.5, sharex=False)

# Loop through various cutoff scale options
titles = ('Zoom out of left', 'Zoom into left', 'Discrete jump', 'Fast jump')
args = (
    (np.pi, 3),  # speed up
    (3 * np.pi, 1 / 3),  # slow down
    (np.pi, np.inf, 3 * np.pi),  # discrete jump
    (np.pi, 5, 3 * np.pi)  # fast jump
)
locators = (
    np.pi / 3,
    np.pi / 3,
    np.pi * np.append(np.linspace(0, 1, 4), np.linspace(3, 4, 4)),
    np.pi * np.append(np.linspace(0, 1, 4), np.linspace(3, 4, 4)),
)
for ax, iargs, title, locator in zip(axs, args, titles, locators):
    ax.pcolormesh(x, dy, data, cmap=cmap)
    for y, color in zip(ys, colors):
        ax.plot(x, y, lw=4, color=color)
    ax.format(
        xscale=('cutoff', *iargs), xlim=(0, 4 * np.pi),
        xlocator=locator, xformatter='pi', xtickminor=False,
        ygrid=False, ylabel='wave amplitude',
        title=title, suptitle='Cutoff axis scales demo'
    )

# %%
import proplot as pplt
import numpy as np

# Create figure
n = 30
state = np.random.RandomState(51423)
data = state.rand(n - 1, n - 1)
colors = ('coral', 'sky blue')
cmap = pplt.Colormap('grays', right=0.8)
gs = pplt.GridSpec(nrows=2, ncols=2)
fig = pplt.figure(refwidth=2.3, share=False)
fig.format(grid=False, suptitle='Other axis scales demo')

# Geographic scales
x = np.linspace(-180, 180, n)
y = np.linspace(-85, 85, n)
for i, scale in enumerate(('sine', 'mercator')):
    ax = fig.subplot(gs[i, 0])
    ax.plot(x, y, '-', color=colors[i], lw=4)
    ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8})
    ax.format(
        yscale=scale, title=scale.title() + ' scale',
        ylim=(-85, 85), ylocator=20, yformatter='deg',
    )

# Exponential scale
n = 50
x = np.linspace(0, 1, n)
y = 3 * np.linspace(0, 1, n)
data = state.rand(len(y) - 1, len(x) - 1)
ax = fig.subplot(gs[0, 1])
title = 'Exponential $e^x$ scale'
ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8})
ax.plot(x, y, lw=4, color=colors[0])
ax.format(ymin=0.05, yscale=('exp', np.e), title=title)

# Power scale
ax = fig.subplot(gs[1, 1])
title = 'Power $x^{0.5}$ scale'
ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8})
ax.plot(x, y, lw=4, color=colors[1])
ax.format(ymin=0.05, yscale=('power', 0.5), title=title)

# %% [raw] raw_mimetype="text/restructuredtext"
# .. _ug_alt:
#
# Alternate axes
# --------------
#
# The `matplotlib.axes.Axes` class includes `~matplotlib.axes.Axes.twinx`
# and `~matplotlib.axes.Axes.twiny` commands for drawing "twin" *x* and
# *y* axes in the same subplot. Proplot expands on these commands and adds
# the arguably more intuitive `~proplot.axes.CartesianAxes.altx` and
# `~proplot.axes.CartesianAxes.alty` options. Here `~proplot.axes.CartesianAxes.altx`
# is equivalent to `~proplot.axes.CartesianAxes.twiny` (makes an alternate *x*
# axes and an identical twin *y* axes) and `~proplot.axes.CartesianAxes.alty`
# is equivalent to `~proplot.axes.CartesianAxes.twinx` (makes an alternate *y*
# axes and an identical twin *x* axes). The proplot versions can be quickly
# formatted by passing `proplot.axes.CartesianAxes.format` keyword arguments
# to the commands (e.g., ``ax.alty(ycolor='red')`` or, since the ``y`` prefix in
# this context is redundant, just ``ax.alty(color='red')``). They also enforce
# sensible default locations for the spines, ticks, and labels, and disable
# the twin axes background patch and gridlines by default.
#
# .. note::
#
#    Unlike matplotlib, proplot adds alternate axes as `children
#    <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.add_child_axes.html>`__
#    of the original axes. This helps simplify the :ref:`tight layout algorithm
#    <ug_tight>` but means that the drawing order is controlled by the difference
#    between the zorders of the alternate axes and the content *inside* the original
#    axes rather than the zorder of the original axes itself (see `this issue page
#    <https://github.com/lukelbd/proplot/issues/303>`__ for details).

# %%
import proplot as pplt
import numpy as np
state = np.random.RandomState(51423)
c0 = 'gray5'
c1 = 'red8'
c2 = 'blue8'
N, M = 50, 10

# Alternate y axis
data = state.rand(M) + (state.rand(N, M) - 0.48).cumsum(axis=0)
altdata = 5 * (state.rand(N) - 0.45).cumsum(axis=0)
fig = pplt.figure(share=False)
ax = fig.subplot(121, title='Alternate y twin x')
ax.line(data, color=c0, ls='--')
ox = ax.alty(color=c2, label='alternate ylabel', linewidth=1)
ox.line(altdata, color=c2)

# Alternate x axis
data = state.rand(M) + (state.rand(N, M) - 0.48).cumsum(axis=0)
altdata = 5 * (state.rand(N) - 0.45).cumsum(axis=0)
ax = fig.subplot(122, title='Alternate x twin y')
ax.linex(data, color=c0, ls='--')
ox = ax.altx(color=c1, label='alternate xlabel', linewidth=1)
ox.linex(altdata, color=c1)
fig.format(xlabel='xlabel', ylabel='ylabel', suptitle='Alternate axes demo')

# %% [raw] raw_mimetype="text/restructuredtext"
# .. _ug_dual:
#
# Dual unit axes
# --------------
#
# The `~proplot.axes.CartesianAxes.dualx` and
# `~proplot.axes.CartesianAxes.dualy` methods can be used to draw duplicate *x* and
# *y* axes meant to represent *alternate units* in the same coordinate range as the
# "parent" axis. This feature is powered by the `~proplot.scale.FuncScale` class.
# `~proplot.axes.CartesianAxes.dualx` and `~proplot.axes.CartesianAxes.dualy` accept
# the same axis formatting keyword arguments as `~proplot.axes.CartesianAxes.altx`
# and `~proplot.axes.CartesianAxes.alty`. The alternate units are specified with
# either of the following three positional arguments:
#
# #. A single linear forward function.
# #. A 2-tuple of arbitrary forward and inverse functions.
# #. An :ref:`axis scale <ug_scales>` name or class instance.
#
# In the third case, the axis scale transforms are used for the forward and
# inverse functions, and the default axis scale locators and formatters are used
# for the default dual axis locators and formatters. In the below examples,
# we generate dual axes with each of these three methods. Note that the
# "parent" axis scale is arbitrary -- in the first example, we create
# a `~proplot.axes.CartesianAxes.dualx` axis for a `symlog-scaled
# <https://matplotlib.org/stable/gallery/scales/symlog_demo.html>`__ axis.

# %%
import proplot as pplt
pplt.rc.update({'grid.alpha': 0.4, 'meta.width': 1, 'grid.linewidth': 1})
c1 = pplt.scale_luminance('cerulean', 0.5)
c2 = pplt.scale_luminance('red', 0.5)
fig = pplt.figure(refaspect=2.2, refwidth=3, share=False)
axs = fig.subplots(
    [[1, 1, 2, 2], [0, 3, 3, 0]],
    suptitle='Duplicate axes with simple transformations',
    ylocator=[], yformatter=[], xcolor=c1, gridcolor=c1,
)

# Meters and kilometers
ax = axs[0]
ax.format(xlim=(0, 5000), xlabel='meters')
ax.dualx(
    lambda x: x * 1e-3,
    label='kilometers', grid=True, color=c2, gridcolor=c2
)

# Kelvin and Celsius
ax = axs[1]
ax.format(xlim=(200, 300), xlabel='temperature (K)')
ax.dualx(
    lambda x: x - 273.15,
    label='temperature (\N{DEGREE SIGN}C)', grid=True, color=c2, gridcolor=c2
)

# With symlog parent
ax = axs[2]
ax.format(xlim=(-100, 100), xscale='symlog', xlabel='MegaJoules')
ax.dualx(
    lambda x: x * 1e6,
    label='Joules', formatter='log', grid=True, color=c2, gridcolor=c2
)
pplt.rc.reset()

# %%
import proplot as pplt
pplt.rc.update({'grid.alpha': 0.4, 'meta.width': 1, 'grid.linewidth': 1})
c1 = pplt.scale_luminance('cerulean', 0.5)
c2 = pplt.scale_luminance('red', 0.5)
fig = pplt.figure(
    share=False, refaspect=0.4, refwidth=1.8,
    suptitle='Duplicate axes with pressure and height'
)

# Pressure as the linear scale, height on opposite axis (scale height 7km)
ax = fig.subplot(121)
ax.format(
    xformatter='null', ylabel='pressure (hPa)',
    ylim=(1000, 10), xlocator=[], ycolor=c1, gridcolor=c1
)
ax.dualy(
    'height', label='height (km)', ticks=2.5, color=c2, gridcolor=c2, grid=True
)

# Height as the linear scale, pressure on opposite axis (scale height 7km)
ax = fig.subplot(122)
ax.format(
    xformatter='null', ylabel='height (km)', ylim=(0, 20), xlocator='null',
    grid=True, gridcolor=c2, ycolor=c2
)
ax.dualy(
    'pressure', label='pressure (hPa)', locator=100, color=c1, gridcolor=c1, grid=True
)
pplt.rc.reset()

# %%
import proplot as pplt
import numpy as np
pplt.rc.margin = 0
c1 = pplt.scale_luminance('cerulean', 0.5)
c2 = pplt.scale_luminance('red', 0.5)
fig, ax = pplt.subplots(refaspect=(3, 1), figwidth=6)

# Sample data
cutoff = 1 / 5
x = np.linspace(0.01, 0.5, 1000)  # in wavenumber days
response = (np.tanh(-((x - cutoff) / 0.03)) + 1) / 2  # response func
ax.axvline(cutoff, lw=2, ls='-', color=c2)
ax.fill_between([cutoff - 0.03, cutoff + 0.03], 0, 1, color=c2, alpha=0.3)
ax.plot(x, response, color=c1, lw=2)

# Add inverse scale to top
ax.format(
    title='Imaginary response function',
    suptitle='Duplicate axes with wavenumber and period',
    xlabel='wavenumber (days$^{-1}$)', ylabel='response', grid=False,
)
ax = ax.dualx(
    'inverse', locator='log', locator_kw={'subs': (1, 2, 5)}, label='period (days)'
)
pplt.rc.reset()


# -*- coding: utf-8 -*-
"""
Created on Thu Nov 14 17:39:48 2019

@author: PC
"""
import numpy as np

liste=[1,2,3,4]

array=np.array(liste)

liste2=list(array)

a=np.array([1,2,3,4])
b=a
c=a

c[0]=89#böyle yaparsak hepsi değişir çünkü hafızaya değer olarak depolanırlar

##nasıl düzeltilir
d=np.array([1,2,3,4])
e=d.copy()
f=d.copy()

f[0]=23
e[1]=25


import skimage.io as io
import skimage.transform as skt
import numpy as np
from PIL import Image
from src.models.class_patcher import patcher
from src.utils.imgproc import *

class patcher(patcher):
    def __init__(self, body='./body/body_inabikini.png', **options):
        super().__init__('ビキニ(いな屋さん)', body=body, pantie_position=[-18, 456], **options)
        self.mask = io.imread('./mask/mask_inabikini.png')
        self.bra_mask = io.imread('./mask/mask_inabikini_bra.png')

    def pick_color(self, arr):
        return np.mean(np.mean(arr, axis=0), axis=0)

    def gen_texture(self, image, size):
        pantie = np.array(image)
        front = pantie[20:100, 30:80, :3]
        front_color = self.pick_color(front)
        front_color = np.append(front_color, 255).astype(np.uint8)
        texture = np.ones((size[0], size[1], 4),  dtype=np.uint8) * front_color
        return Image.fromarray(texture)

    def convert(self, image):
        pantie = np.array(image)
        patch = np.copy(pantie[-100:-5, 546:, :])
        pantie[-100:, 546:, :] = 0
        patch = skt.resize(patch[::-1, ::-1, :], (patch.shape[0] + 30, patch.shape[1]), anti_aliasing=True, mode='reflect')
        [pr, pc, d] = patch.shape
        pantie[127 - 5:127 - 5 + pr, :pc, :] = np.uint8(patch * 255)

        front = pantie[:350, :300]
        arrx = (np.linspace(0, 1, 25)**2) * 80
        arrx[4:16] += np.sin(np.linspace(0, np.pi, 12)) * 15
        arrx -= 50
        arry = np.zeros(25)
        front = affine_transform_by_arr(front, arrx, arry)
        front = np.uint8(front[:, 7:] * 255)

        back = pantie[:350, 270:-10][::-1, ::-1]
        arrx = (np.linspace(0, 1, 25)**2) * -65
        arrx += 70
        arry = np.zeros(25)
        back = affine_transform_by_arr(back, arrx, arry)
        back = np.uint8(back[:-60, 7:] * 255)

        front = np.pad(front, [(0, 0), (0, back.shape[1] - front.shape[1]), (0, 0)], mode='constant')
        pantie = np.concatenate((front[:-68], back), axis=0)
        pantie = np.bitwise_and(pantie, self.mask)
        pantie = np.concatenate((pantie[:, ::-1], pantie), axis=1)
        return Image.fromarray(pantie)

    def gen_bra(self, image, fliplr=False):
        pantie = np.array(image)
        bra = pantie[15:170, -200:][::-1, :]
        bra = np.uint8(resize(bra, [1.73, 1.73]) * 255)
        bra = np.bitwise_and(bra, self.bra_mask)
        return Image.fromarray(bra[:, ::-1] if fliplr else bra)

    def patch(self, image, transparent=False):
        pantie = self.convert(image)
        if transparent:
            patched = Image.new("RGBA", self.body_size)
        else:
            patched = self.body.copy()
        patched = self.paste(patched, self.gen_texture(image, (self.body_size[0], self.body_size[1])), [0, 0])
        patched = self.paste(patched, self.gen_bra(image, fliplr=True), [676, 691])
        patched = self.paste(patched, self.gen_bra(image), [372, 573])
        patched = self.paste(patched, pantie, self.pantie_position)
        return patched


#!/usr/bin/env python
# coding: utf-8

# In[ ]:

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
import os
print(os.listdir("../../../input/kingburrito666_cannabis-strains"))
plt.style.use('ggplot') # to plot graphs with gggplot2 style
# Any results you write to the current directory are saved as output.

# In[ ]:

#Reading the dataset on pandas
strains = pd.read_csv("../../../input/kingburrito666_cannabis-strains/cannabis.csv")

# In[ ]:

strains.shape

# In[ ]:

strains.info()

# In[ ]:

# check the null value
strains.isnull().sum()

# Flavor and Description have nan value

# In[ ]:

strains.head()

# In[ ]:

strains['Type']= strains['Type'].astype(str)

# In[ ]:

print(strains.nunique())

# ### EDA

# In[ ]:

print("Numerical describe of distribuition Type")
print(strains.groupby("Type")["Strain"].count())
print("Percentage of distribuition Type ")
print((strains.groupby("Type")["Strain"].count() / len(strains.Type) * 100).round(decimals=2))

# In[ ]:

plt.figure(figsize=(10,6))
sns.countplot(x="Type", data=strains, palette='hls')
plt.xlabel('Species', fontsize=15)
plt.ylabel('Count', fontsize=20)
plt.title("Cannabis Species Count ", fontsize=20)
print()

# ### Looking the distribuition of Rating and type by Rating

# In[ ]:

print("Top 10 Rating by consumers")
print(strains["Rating"].value_counts().head(10))

plt.figure(figsize=(8,6))

#Total rating distribuition
g = sns.distplot(strains["Rating"], bins=50)
g.set_title("Rating distribuition", size = 20)
g.set_xlabel('Rating', fontsize=15)

#  Almost all species have rating higher than 4
#
# Now I will Look the distribuition separted by Type
#

# In[ ]:

print("Rating Distribuition by Species Type")
print(pd.crosstab(strains[strains.Rating > 4.0]['Rating'], strains.Type))

plt.figure(figsize=(10,14))

#Let's look the Rating distribuition by Type.
g = plt.subplot(311)
g = sns.distplot(strains[(strains.Type == 'hybrid') & (strains.Rating > 0)]["Rating"], color='y')
g.set_xlabel("Rating", fontsize=15)
g.set_ylabel("Distribuition", fontsize=15)
g.set_title("Rating Distribuition Hybrids", fontsize=20)

g1 = plt.subplot(312)
g1 = sns.distplot(strains[(strains.Type == 'sativa') & (strains.Rating > 0)]["Rating"], color='g')
g1.set_xlabel("Rating", fontsize=15)
g1.set_ylabel("Distribuition", fontsize=15)
g1.set_title("Rating Distribuition Sativas", fontsize=20)

g2 = plt.subplot(313)
g2 = sns.distplot(strains[(strains.Type == 'indica') & (strains.Rating > 0)]["Rating"], color='r')
g2.set_xlabel("Rating", fontsize=15)
g2.set_ylabel("Distribuition", fontsize=15)
g2.set_title("Rating Distribuition Indicas", fontsize=20)

plt.subplots_adjust(wspace = 0.1, hspace = 0.6,top = 0.9)

print()

# Sativa and Indica have a similar rating distribuition, and we can see that almost of all species in dataset have rating higher than 4

# ### Lets try a better look of the Rating Distribuition by species considering just the values higher than 2

# In[ ]:

plt.figure(figsize=(10,6))
#I will now explore the Rating distribuition by Type
g = sns.boxplot(x="Type",y="Rating",data=strains[strains["Rating"] > 2],palette="hls")
g.set_title("Rating distribuition by Species Type", fontsize=20)
g.set_xlabel("Species", fontsize=15)
g.set_ylabel("Rating", fontsize=15)
print()

# WE can see that the Sativa have a less median than Hybrids and indicas

# In[ ]:

#Looking the Rating distribuition description
print("Rating less than 4: ")
print(strains[strains.Rating <= 4].groupby("Type")["Strain"].count())
print("Rating between 4 and 4.5: ")
print(strains[(strains.Rating > 4) & (strains.Rating <= 4.5)].groupby("Type")["Strain"].count())
print("Top Strains - Rating > 4.5: ")
print(strains[strains["Rating"] > 4.5].groupby("Type")["Strain"].count())
print("Distribuition by type of Ratings equal 5: ")
print(strains[strains["Rating"] == 5].groupby("Type")["Strain"].count())
print("Total of: 2350 different Strains")

# In[ ]:

#I will extract the values in Effects and Flavor and pass to a new column
df_effect = pd.DataFrame(strains.Effects.str.split(',',4).tolist(), columns = ['Effect_1','Effect_2','Effect_3','Effect_4','Effect_5'])

df_flavors = pd.DataFrame(strains.Flavor.str.split(',',n=2,expand=True).values.tolist(), columns = ['Flavor_1','Flavor_2','Flavor_3'])

# In[ ]:

#Concatenating the new variables with strains
strains = pd.concat([strains, df_effect], axis=1)
strains = pd.concat([strains, df_flavors], axis=1)

#Looking the result
strains.head()

# In[ ]:

strains.columns

# We can se the Effects and Flavors are in separated columns... Now I will explore the main related effects

# In[ ]:

print("The top 5 First Effects related")
print(strains['Effect_1'].value_counts()[:5])

plt.figure(figsize=(13,6))

g = sns.boxplot(x = 'Effect_1', y="Rating", hue="Type", data=strains[strains["Rating"] > 3], palette="hls")
g.set_xlabel("Related Effect", fontsize=15)
g.set_ylabel("Rating Distribuition", fontsize=15)
g.set_title("First Effect Related x Rating by Species Type",fontsize=20)

print()

# ### The second most related effects and respective Rating

# In[ ]:

print("The top 5 Second related Effects")
print(strains['Effect_2'].value_counts()[:5])

plt.figure(figsize=(13,6))

g = sns.boxplot(x = 'Effect_2', y="Rating", hue="Type", data=strains[strains["Rating"] > 3], palette="hls")
g.set_xlabel("Related Effect", fontsize=15)
g.set_ylabel("Rating Distribuition", fontsize=15)
g.set_title("Second Effect Related x Rating by Species Type",fontsize=20)

print()

# ### Now let's see the first Flavor related
#
# We have 33 flavors in total

# In[ ]:

strains.head()

# In[ ]:

strains.shape

# ### Exploring Flavors

# In[ ]:

print("TOP 10 Flavors related")
print(strains.Flavor_1.value_counts()[:10])

plt.figure(figsize=(14,6))
sns.countplot('Flavor_1', data=strains)
plt.xticks(rotation=90)
plt.xlabel('Flavors', fontsize=15)
plt.ylabel('Frequency', fontsize=15)
plt.title("First flavors described ", fontsize=20)
print()

# ### Let's explore the Strains with Rating equal 5

# In[ ]:

#Whats the type with most strains with rating 5?
print("Percentual of Species with Rating equal 5")
five_rating = (strains[strains["Rating"] == 5].groupby("Type")["Strain"].count()                / len(strains[strains["Rating"] == 5]) *100).round(decimals=2)
print(five_rating)
plt.figure(figsize=(10,6))
g = sns.countplot(x="Type",data=strains[strains["Rating"] == 5])
g.set_xlabel('Species', fontsize=15)
g.set_ylabel('Frequency', fontsize=15)
g.set_title("Distribuition of Types by Rating 5.0  ", fontsize=20)

print()

# ### Exploring the principal effects and Flavors Related in Rating five strains

# In[ ]:

strains_top = strains[strains["Rating"] == 5]

fig, ax = plt.subplots(2,1, figsize=(12,10))

sns.countplot(x ='Effect_1',data = strains_top,hue="Type",ax=ax[0], palette='hls')

sns.countplot(x ='Flavor_1',data = strains_top,hue="Type",ax=ax[1], palette='hls')

for ax in fig.axes:
    plt.sca(ax)
    plt.xticks(rotation=45)

# ###
#
# Curious!
# We can see that in all types, the most related flavors are Sweet and Earthly.
# Is important to remember that we have alot of another flavors that are related with this Sweet and Earthly tastes.
#
# We can also remember that the first cannabis strain was Skunk #1, that have a high earthly and pungent taste.
#
# The distribuition total of data set is almost nearly of this values:
#
#     hybrid 51.55
#     indica 29.73
#     sativa 18.72
#
# Now I will Explore the total Effects and Flavors related to each strain
#

# In[ ]:

#Let's create subsets by each type and explore their Flavors and Effects
hibridas = strains[strains.Type == 'hybrid']
indicas = strains[strains.Type == 'indica']
sativas = strains[strains.Type == 'sativa']

# In[ ]:

#Now we can delete some columns that will not be useful
del strains["Effects"]
del strains["Flavor"]

# In[ ]:

#Creating the spliter -- copied by LiamLarsen --
def get_effects(dataframe):
    ret_dict = {}
    for list_ef in dataframe.Effects:
        effects_list = list_ef.split(',')
        for effect in effects_list:
            if not effect in ret_dict:
                ret_dict[effect] = 1
            else:
                ret_dict[effect] += 1
    return ret_dict

# ### Sativas effects

# In[ ]:

#Creating the counting of effects
sativa_effects = get_effects(sativas)

#Let see the distribuition of effects by types
plt.figure(figsize=(10,8))
sns.barplot(list(sativa_effects.values()), list(sativa_effects.keys()), orient='h')
plt.xlabel("Count", fontsize=12)
plt.ylabel("Related effects", fontsize=12)
plt.title("Sativas strain effects distribution", fontsize=16)
print()

# ### Indicas effects

# In[ ]:

# Couting effects of indicas
indica_effects = get_effects(indicas)

# Ploting Indica Effects
plt.figure(figsize=(10,8))
sns.barplot(list(indica_effects.values()),list(indica_effects.keys()), orient='h')
plt.xlabel("Count", fontsize=15)
plt.ylabel("Related effects", fontsize=15)
plt.title("Indica strain effects distribution", fontsize=20)
print()

# ### Hibrids flavors

# In[ ]:

hibridas_effects = get_effects(hibridas)

# Ploting Hybrid effects
plt.figure(figsize=(10,8))
sns.barplot(list(hibridas_effects.values()),list(hibridas_effects.keys()), orient='h')
plt.xlabel("Count", fontsize=15)
plt.ylabel("Related effects", fontsize=15)
plt.title("Hibrids strain effects distribution", fontsize=20)
print()

# 5. Some observations:
#
# Some observations:
#
# We can clearly see that Happy, Uplified, Relaxed, Euphoric have a high ranking at all 3 types
#
# Its interesting that almost 350 people of 440 in Sativas related Happy and Uplifted Effects
#
#     'Happy': 342
#     'Uplifted': 328
#     'Euphoric': 276
#     'Energetic': 268
#
# 78% has described Happy to Sativas strains
#
# Indicas we have 699 votes and Relaxed with most frequency at distribuition:
#
#     'Relaxed': 628
#     'Happy': 562
#     'Euphoric': 516
#
# 90% has described Relaxed to Indica strains
#
# Hybrids We have 1212 votes and distribuition of effects is
#
#     'Happy': 967
#     'Relaxed': 896
#     'Uplifted': 848
#     'Euphoric': 843
#
# 80% has described Happy and 74% related Relaxed to Hybrids strains
# Very Interesting!
#
# We also need to remember that's possible to vote in more than 1 effect or flavor in each vote.
#

# ### 6. Exploring general Flavors and Effects:
#
# Now, let's check the flavors
# I will use the same loop to known the most related flavors

# In[ ]:

#Creating flavors to cut each flavor by row -- inspired in LiamLarsen --
def flavors(df):
    ret_dict = {}
    for list_ef in df.Flavor.dropna():
        flavors_list = list_ef.split(',')
        for flavor in flavors_list:
            if not flavor in ret_dict:
                ret_dict[flavor] = 1
            else:
                ret_dict[flavor] += 1
    return ret_dict

# ### Sativas Flavors

# In[ ]:

#Runing flavors counts to sativas
sativa_flavors = flavors(sativas)

plt.figure(figsize=(10,12))
sns.barplot(list(sativa_flavors.values()),list(sativa_flavors.keys()), orient='h')
plt.xlabel("Count", fontsize=12)
plt.ylabel("Most related flavors", fontsize=12)
plt.title("Sativa flavors distribution", fontsize=16)
print()

# ### Most frequent flavors in Sativas:
# Sweet: 207

# Earthy: 178

# Citrus: 143

# ### Indicas Flavors
#

# In[ ]:

indica_flavors = flavors(indicas)

plt.figure(figsize=(10,12))
sns.barplot(list(indica_flavors.values()),list(indica_flavors.keys()), orient='h')
plt.xlabel("Count", fontsize=12)
plt.ylabel("Most related flavors",fontsize=12)
plt.title("Indica flavors distribution", fontsize=16)
print()

# Most frequent values in indicas
# Earthy: 378

# Sweet: 312

# Pungent: 157

# Berry: 145

# ### Hibrids flavors

# In[ ]:

#Getting hibridas flavors
hibridas_flavors = flavors(hibridas)

plt.figure(figsize=(10,12))
sns.barplot(list(hibridas_flavors.values()),list(hibridas_flavors.keys()), alpha=0.8,orient='h')
plt.xlabel("Count", fontsize=15)
plt.ylabel("Most related flavors", fontsize=15)
plt.title("Hibrids flavors distribution", fontsize=20)
print()

# The most frequent values in Hybrid type is:

# Earthy: 549

# Sweet: 534

# Citrus: 301

# ### Librarys to WordCloud
#

# In[ ]:

from wordcloud import WordCloud, STOPWORDS
import nltk.tokenize as word_tokenize
import re
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
import string
import re
from nltk.stem.porter import *
from nltk.tokenize import sent_tokenize
from sklearn.feature_extraction import stop_words

# ### Word Clouds

# In[ ]:

stopwords = set(STOPWORDS)
newStopWords = ['strain','effect', 'genetic', 'effects','flavor', 'dominant','known','cross']
stopwords.update(newStopWords)

wordcloud = WordCloud( background_color='black', stopwords=stopwords, max_words=1500, max_font_size=200, width=1000, height=600, random_state=42, ).generate(" ".join(strains['Description'].astype(str)))

fig = plt.figure(figsize = (12,12))
plt.imshow(wordcloud)
plt.title("WORD CLOUD - DESCRIPTION", fontsize=25)
plt.axis('off')
print()

# ### Word Cloud Sativas

# In[ ]:

stopwords = set(STOPWORDS)
newStopWords = ['strain','effect', 'genetic', 'sativa', 'effects', 'aroma','flavor','dominant','known','cross','genetics']
stopwords.update(newStopWords)

wordcloud = WordCloud( background_color='white', stopwords=stopwords, max_words=1500, max_font_size=200, width=1000, height=600, random_state=42, ).generate(" ".join(strains[strains.Type == 'sativa']['Description'].astype(str)))

fig = plt.figure(figsize = (12,12))
plt.imshow(wordcloud)
plt.title("WORD CLOUD - SATIVAS", fontsize=25)
plt.axis('off')
print()

# ### Word Cloud Indicas

# In[ ]:

stopwords = set(STOPWORDS)
newStopWords = ['strain','effect', 'genetic', 'indica', 'effects','aroma', 'genetics','flavor','dominant','known','cross']
stopwords.update(newStopWords)

wordcloud = WordCloud( background_color='black', stopwords=stopwords, max_words=1500, max_font_size=150, width=1000, height=600, random_state=42, ).generate(" ".join(strains[strains.Type == 'indica']['Description'].astype(str)))

fig = plt.figure(figsize = (12,12))
plt.imshow(wordcloud)
plt.title("WORD CLOUD - INDICAS", fontsize=25)
plt.axis('off')
print()

# ### Word Cloud Hybrids

# In[ ]:

stopwords = set(STOPWORDS)
newStopWords = ['strain','effect', 'genetic', 'hybrid', 'effects', 'aroma', 'genetics', 'flavor', 'genetics','cross','dominant','known']
stopwords.update(newStopWords)

wordcloud = WordCloud( background_color='white', stopwords=stopwords, max_words=1500, max_font_size=150, width=1000, height=600, random_state=42, ).generate(" ".join(strains[strains.Type == 'hybrid']['Description'].astype(str)))

fig = plt.figure(figsize = (12,12))
plt.imshow(wordcloud)
plt.title("WORD CLOUD - HYBRIDS", fontsize=25)
plt.axis('off')
print()

# ### Word Cloud Rating 5 Strains

# In[ ]:

stopwords = set(STOPWORDS)
newStopWords = ['strain','effect', 'genetic','effects','cross','genetics', 'aroma','consumer','known','dominant']
stopwords.update(newStopWords)

wordcloud = WordCloud( background_color='black', stopwords=stopwords, max_words=1500, max_font_size=150, width=1000, height=600, random_state=42, ).generate(" ".join(strains[strains.Rating == 5]['Description'].astype(str)))

fig = plt.figure(figsize = (12,12))
plt.imshow(wordcloud)
plt.title("WORD CLOUD - RATING 5", fontsize=25)
plt.axis('off')
print()

# ### 7. Preprocessing dataset:
#
# Knowing all this...Let's get high!
#
# and try to predict the type of strain using flavors, effects and rating?

# In[ ]:

# Lets do some transformation in data

print(strains.head())

# In[ ]:

#Transformin the Type in numerical
strains["Type"] = pd.factorize(strains["Type"])[0]
del strains["Description"]
# Now we have 3 numerical Types
# 0 - Hybrid
# 1 - Sativa
# 2 - Indica

# In[ ]:

# Creating the dummies variable of Effects and Flavors
#effect_dummy = strains['Effects'].str.get_dummies(sep=',',)
#flavor_dummy = strains['Flavor'].str.get_dummies(sep=',')

dummy = pd.get_dummies(strains[['Effect_1','Effect_2','Effect_3','Effect_4','Effect_5','Flavor_1','Flavor_2','Flavor_3']])

# In[ ]:

#Concatenating the result and droping the used variables
strains = pd.concat([strains, dummy], axis=1)

strains = strains.drop(['Strain','Effect_1','Effect_2','Effect_3','Effect_4','Effect_5','Flavor_1','Flavor_2','Flavor_3'], axis=1)

strains.shape

# ### 8. Importing Sklearn and Modeling:

# In[ ]:

#Importing the auxiliar and preprocessing librarys
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
from sklearn.cross_validation import cross_val_score
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.pipeline import Pipeline

from sklearn.model_selection import train_test_split, KFold, cross_validate
from sklearn.metrics import accuracy_score

#Models
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.svm import SVC
from sklearn.linear_model import RidgeClassifier, SGDClassifier, LogisticRegression
from sklearn.svm import SVC, LinearSVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from xgboost import XGBClassifier
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier, ExtraTreesClassifier, BaggingClassifier, VotingClassifier, RandomTreesEmbedding

# In[ ]:

strains.head(2)

# In[ ]:

# setting X and y
X = strains.drop("Type",1)
y = strains["Type"]
feature_name = X.columns.tolist()
X = X.astype(np.float64, copy=False)
y = y.astype(np.float64, copy=False)

# In[ ]:

#Spliting the variables in train and test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state=42)

print("X_train Shape: ", X_train.shape)
print("X_test Shape: ", X_test.shape)

# ### Feature Selection

# In[ ]:

thresh = 5 * 10**(-3)
model = GradientBoostingClassifier()
model.fit(X_train, y_train)
#select features using threshold
selection = SelectFromModel(model, threshold=thresh, prefit=True)

X_important_train = selection.transform(X_train)
X_important_test = selection.transform(X_test)

# In[ ]:

print("X_important_train Shape: ", X_important_train.shape)
print("X_important_test Shape: ", X_important_test.shape)

# ### Let's take a look at some models and compare their score

# In[ ]:

clfs = []
seed = 3

clfs.append(("LogReg", Pipeline([("Scaler", StandardScaler()), ("LogReg", LogisticRegression())])))

clfs.append(("XGBClassifier", Pipeline([("Scaler", StandardScaler()), ("XGB", XGBClassifier())])))
clfs.append(("KNN", Pipeline([("Scaler", StandardScaler()), ("KNN", KNeighborsClassifier())])))

clfs.append(("DecisionTreeClassifier", Pipeline([("Scaler", StandardScaler()), ("DecisionTrees", DecisionTreeClassifier())])))

clfs.append(("RandomForestClassifier", Pipeline([("Scaler", StandardScaler()), ("RandomForest", RandomForestClassifier())])))

clfs.append(("GradientBoostingClassifier", Pipeline([("Scaler", StandardScaler()), ("GradientBoosting", GradientBoostingClassifier(max_features=15, n_estimators=150))])))

clfs.append(("RidgeClassifier", Pipeline([("Scaler", StandardScaler()), ("RidgeClassifier", RidgeClassifier())])))

clfs.append(("BaggingRidgeClassifier", Pipeline([("Scaler", StandardScaler()), ("BaggingClassifier", BaggingClassifier())])))

clfs.append(("ExtraTreesClassifier", Pipeline([("Scaler", StandardScaler()), ("ExtraTrees", ExtraTreesClassifier())])))

#'neg_mean_absolute_error', 'neg_mean_squared_error','r2'
scoring = 'accuracy'
n_folds = 7

results, names  = [], []

for name, model  in clfs:
    kfold = KFold(n_splits=n_folds, random_state=seed)
    cv_results = cross_val_score(model, X_important_train, y_train, cv= 5, scoring=scoring, n_jobs=-1)
    names.append(name)
    results.append(cv_results)
    msg = "%s: %f (+/- %f)" % (name, cv_results.mean(),  cv_results.std())
    print(msg)

# boxplot algorithm comparison
fig = plt.figure(figsize=(15,6))
fig.suptitle('Classifier Algorithm Comparison', fontsize=22)
ax = fig.add_subplot(111)
sns.boxplot(x=names, y=results)
ax.set_xticklabels(names)
ax.set_xlabel("Algorithmn", fontsize=20)
ax.set_ylabel("Accuracy of Models", fontsize=18)
ax.set_xticklabels(ax.get_xticklabels(),rotation=45)
print()

# ### I will select the top 3 models and set some hyperParameters to try increase their prediction power.
# - The top 3 will be:
#     - GradientBoostingClassifier
#     - XGBClassifier
#     - RidgeClassifier
#
#

# In[ ]:

from sklearn.grid_search import GridSearchCV

params_ridge = {'alpha':[0.001, 0.1, 1.0], 'tol':[0.1, 0.01, 0.001], 'solver':['auto', 'svd', 'cholesky','lsqr', 'sparse_cg', 'sag', 'saga']}

ridge = RidgeClassifier()

Ridge_model = GridSearchCV(estimator = ridge, param_grid=params_ridge, verbose=2, n_jobs = -1)

# Fit the random search model
Ridge_model.fit(X_important_train, y_train)

# In[ ]:

# Printing the Training Score
print("Training score data: ")
print(Ridge_model.score(X_important_train, y_train) )
print("Ridge Best Parameters: ")
print(Ridge_model.best_params_ )

# - We got a nice improvement in our model compared with the first model without HyperParameters.

# Now, let's Predict with this model

# In[ ]:

# Predicting with X_test
Ridge_model = RidgeClassifier(solver='sparse_cg', tol=0.001, alpha=1.0)
Ridge_model.fit(X_important_train, y_train)
y_pred = Ridge_model.predict(X_important_test)

# Print the results
print(accuracy_score(y_test,y_pred))
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred))

# ### Now, I will evaluate the best params to XGBoost model

# In[ ]:

param_xgb = { 'n_estimators':[100,150,200], 'max_depth':[3,4,5,6], 'min_child_weight':[2,3,4,5], 'colsample_bytree':[.1, 0.2, 0.3,0.6,0.7,0.8], 'colsample_bylevel':[0.2,0.6,0.8] }

# In[ ]:

xgb = XGBClassifier()

xgb_model = GridSearchCV(estimator = xgb, param_grid = param_xgb, scoring='accuracy', cv=2, verbose = 1)

xgb_model.fit(X_important_train, y_train)

# In[ ]:

print("Results of the GridSearchCV of XGB: ")
print(xgb_model.best_params_)
print(xgb_model.score(X_important_train, y_train))

# In[ ]:

# let's set the best parameters to our model and fit again
xgb = XGBClassifier(colsample_bylevel=0.6, colsample_bytree=0.1, objective='multi', max_depth= 4, min_child_weight= 2, n_estimators= 200)
xgb.fit(X_important_train, y_train)

# Predicting with X_test
y_pred = xgb.predict(X_important_test)

# Print the results
print("METRICS \nAccuracy Score: ", accuracy_score(y_test,y_pred))
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred))

# ### Now let's fit and predict with Gradient Boosting Classifier model

# In[ ]:

param_gb = { 'n_estimators':[50, 125, 150], 'max_depth':[2,3,4], 'max_features':[3,4,5,6], 'learning_rate':[0.0001, 0.001, 0.01,0.1,1]  }

gb = GradientBoostingClassifier()

gb_model = GridSearchCV(estimator = gb, param_grid = param_gb, scoring='accuracy', cv=5, verbose = 1)

gb_model.fit(X_important_train, y_train)

# In[ ]:

print("Results of the GridSearchCV of Gradient Boosting Classifier: ")
print(gb_model.best_params_)
print(gb_model.score(X_important_train, y_train))

# In[ ]:

gb = GradientBoostingClassifier(learning_rate=.1, max_depth= 3, max_features=6, n_estimators= 150)
gb.fit(X_important_train, y_train)

# Predicting with X_test
y_pred = gb.predict(X_important_test)

# Print the results
print("METRICS \nAccuracy Score: ", accuracy_score(y_test,y_pred))
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred))

# In[ ]:


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

def split_train_test(data, test_ratio):
    np.random.seed(42)
    shuffled_indices = np.random.permutation(len(data))
    test_set_size = int(len(data) * test_ratio)
    test_indices = shuffled_indices[:test_set_size]
    train_indices = shuffled_indices[test_set_size:]
    return data.iloc[train_indices], data.iloc[test_indices]

def display_scores(scores):
    print("mean:", scores.mean())
    print("standard deviation:", scores.std())


import cv2
import numpy as np
from keras.models import load_model
from keras.applications import mobilenet
from glob import glob

class DogBreedClassifier:

    def __init__(self):
        # load start net (the model runs at start of program)
        self.start_model = load_model('start_net.h5', custom_objects={
            'relu6': mobilenet.relu6,
            'DepthwiseConv2D': mobilenet.DepthwiseConv2D})
        # load saved trained model
        self.model = load_model('mobilenet.h5', custom_objects={
            'relu6': mobilenet.relu6,
            'DepthwiseConv2D': mobilenet.DepthwiseConv2D})
        # load dog classes
        self.breeds = [item[20:-1] for item in sorted(glob("dogImages/train/*/"))]
        # load pre_classify classes for dog, cat, human, neither
        self.classes = [name[21:-1] for name in sorted(glob("custom_images/images/*/"))]

    def run(self, image_path: str):
        """
        First check to see what type of object is in image.
        If human, dog, or cat detected the predict dog breed.
        Print messages.
        :param image_path: String
        :return: None
        """
        img = self.get_image(image_path)
        preclass, proceed = self.pre_classify(img)
        print(preclass)
        if proceed:
            pred_dict = self.predict(img)
            print(self.decide_breed(pred_dict))
        return None

    @staticmethod
    def get_image(image_path: str):
        img = cv2.imread(image_path)
        assert img is not None, "Check image path, no image found."
        img = img / 1.  # Convert to float to avoid error, keras bug
        return img

    def pre_classify(self, img) -> (str, bool):
        """
        Take location of image and return if it's a human, cat, dog, or neither.
        Example output:
        This looks like a human, I'll pretend it's a dog.
        This looks like a cat, I'll pretend it's a dog.
        I don't see a dog, cat, or a human in this photo... are you trying to trick me?
        :param img:
        :return:
        """
        # Convert image to 224 * 224
        img = cv2.resize(img, dsize=(224, 224))
        # Preprocess data
        img = np.expand_dims(img, axis=0)
        # Preprocess input for mobilenet
        img = mobilenet.preprocess_input(img)
        # generate predictions
        prediction = self.start_model.predict(img)
        # Class
        pred_class = self.classes[np.argmax(prediction)]
        # Response dictionary
        response_dict = dict([("dog", ("This looks like a dog, let me guess the breed.", True)),
                              ("cat", ("This looks like a cat, I'll pretend it's a dog.", True)),
                              ("person", ("This looks like a person, I'll pretend it's a dog.", True)),
                              ("other", ("I don't see a dog, cat, or a human in this photo... "
                                         "are you trying to trick me?", False))])
        return response_dict[pred_class]

    def predict(self, img) -> dict:
        """
        Generate prediction dictionary with top 3 predicted dog breeds
        :param img:
        :return:
        """
        # Convert image to 224 * 224
        img = cv2.resize(img, dsize=(224, 224))
        # Preprocess data
        img = np.expand_dims(img, axis=0)
        # Preprocess input for mobilenet
        img = mobilenet.preprocess_input(img)
        # generate top 3 predictions
        prediction = self.model.predict(img)[0]
        # print(np.argmax(prediction))
        top_3 = prediction.argsort()[-3:][::-1]
        top_3_pred_values = [prediction[i] for i in top_3]
        # List top 3 breeds in order
        prediction_dict = dict(zip([self.breeds[i] for i in top_3], top_3_pred_values))

        return prediction_dict

    @staticmethod
    def decide_breed(pred: dict) -> str:
        """
        Allow for mixed breed predictions.
        if 2nd highest > .25 and 1st highest less than .6 then mixed breed (2)
        if top 2 < .6 combined and top 3 > .75 then 3 breed mix
        # I think that is a Yorkie purebred
        # I think that is a Yorkie and Japanese Chin mix
        # I think that is a Yorkie, Japanese Chin, and Lab mix
        :param pred: Prediction dict from predict method
        :return: String to display as prediction output
        """
        for k, v in pred.items():
            pred[k.replace("_", " ")] = pred.pop(k)

        top1 = sorted(pred, key=lambda k: pred[k])[-1]
        top2 = sorted(pred, key=lambda k: pred[k])[-2]
        top3 = sorted(pred, key=lambda k: pred[k])[-3]

        if pred[top1] >= .6:
            breed = "I think this is a %s purebred." % top1
        elif (pred[top2] > .25) & (pred[top1] < .6):
            breed = "I think this is a %s and %s mix." % (top1, top2)
        elif (pred[top1] + pred[top2] < .6) & (sum(pred.values()) > .75):
            breed = "I think this is a %s, %s, and %s mix." % (top1, top2, top3)
        else:
            breed = "I'm honestly not that sure. \n" \
                    "Could be a %s, could be a %s, could be a %s. \n" \
                    "Or maybe it's a mix of all those and something else." \
                    % (top1, top2, top3)
        return breed


# coding: utf-8

# ## AI for Medicine Course 1 Week 1 lecture exercises

# # Data Exploration
# In the first assignment of this course, you will work with chest x-ray images taken from the public [ChestX-ray8 dataset](https://arxiv.org/abs/1705.02315). In this notebook, you'll get a chance to explore this dataset and familiarize yourself with some of the techniques you'll use in the first graded assignment.
#
#
#
# The first step before jumping into writing code for any machine learning project is to explore your data. A standard Python package for analyzing and manipulating data is [pandas](https://pandas.pydata.org/docs/#).
#
# With the next two code cells, you'll import `pandas` and a package called `numpy` for numerical manipulation, then use `pandas` to read a csv file into a dataframe and print out the first few rows of data.

# In[1]:

# Import necessary packages
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
get_ipython().run_line_magic('matplotlib', 'inline')
import os
import seaborn as sns
sns.set()

# In[2]:

# Read csv file containing training datadata
train_df = pd.read_csv("nih/train-small.csv")
# Print first 5 rows
print(f'There are {train_df.shape[0]} rows and {train_df.shape[1]} columns in this data frame')
train_df.head()

# Have a look at the various columns in this csv file. The file contains the names of chest x-ray images ("Image" column) and the columns filled with ones and zeros identify which diagnoses were given based on each x-ray image.

# ### Data types and null values check
# Run the next cell to explore the data types present in each column and whether any null values exist in the data.

# In[3]:

# Look at the data type of each column and whether null values are present
train_df.info()

# ### Unique IDs check
# "PatientId" has an identification number for each patient. One thing you'd like to know about a medical dataset like this is if you're looking at repeated data for certain patients or whether each image represents a different person.

# In[ ]:

print(f"The total patient ids are {train_df['PatientId'].count()}, from those the unique ids are {train_df['PatientId'].value_counts().shape[0]} ")

# As you can see, the number of unique patients in the dataset is less than the total number so there must be some overlap. For patients with multiple records, you'll want to make sure they do not show up in both training and test sets in order to avoid data leakage (covered later in this week's lectures).
#
# ### Explore data labels
# Run the next two code cells to create a list of the names of each patient condition or disease.

# In[4]:

columns = train_df.keys()
columns = list(columns)
print(columns)

# In[5]:

# Remove unnecesary elements
columns.remove('Image')
columns.remove('PatientId')
# Get the total classes
print(f"There are {len(columns)} columns of labels for these conditions: {columns}")

# Run the next cell to print out the number of positive labels (1's) for each condition

# In[6]:

# Print out the number of positive labels for each class
for column in columns:
    print(f"The class {column} has {train_df[column].sum()} samples")

# Have a look at the counts for the labels in each class above. Does this look like a balanced dataset?

# ### Data Visualization
# Using the image names listed in the csv file, you can retrieve the image associated with each row of data in your dataframe.
#
# Run the cell below to visualize a random selection of images from the dataset.

# In[7]:

# Extract numpy values from Image column in data frame
images = train_df['Image'].values

# Extract 9 random images from it
random_images = [np.random.choice(images) for i in range(9)]

# Location of the image dir
img_dir = 'nih/images-small/'

print('Display Random Images')

# Adjust the size of your images
plt.figure(figsize=(20,10))

# Iterate and plot random images
for i in range(9):
    plt.subplot(3, 3, i + 1)
    img = plt.imread(os.path.join(img_dir, random_images[i]))
    plt.imshow(img, cmap='gray')
    plt.axis('off')

# Adjust subplot parameters to give specified padding
plt.tight_layout()

# ### Investigate a single image
# Run the cell below to look at the first image in the dataset and print out some details of the image contents.

# In[8]:

# Get the first image that was listed in the train_df dataframe
sample_img = train_df.Image[0]
raw_image = plt.imread(os.path.join(img_dir, sample_img))
plt.imshow(raw_image, cmap='gray')
plt.colorbar()
plt.title('Raw Chest X Ray Image')
print(f"The dimensions of the image are {raw_image.shape[0]} pixels width and {raw_image.shape[1]} pixels height, one single color channel")
print(f"The maximum pixel value is {raw_image.max():.4f} and the minimum is {raw_image.min():.4f}")
print(f"The mean value of the pixels is {raw_image.mean():.4f} and the standard deviation is {raw_image.std():.4f}")

# ### Investigate pixel value distribution
# Run the cell below to plot up the distribution of pixel values in the image shown above.

# In[9]:

# Plot a histogram of the distribution of the pixels
sns.distplot(raw_image.ravel(),
             label=f'Pixel Mean {np.mean(raw_image):.4f} & Standard Deviation {np.std(raw_image):.4f}', kde=False)
plt.legend(loc='upper center')
plt.title('Distribution of Pixel Intensities in the Image')
plt.xlabel('Pixel Intensity')
plt.ylabel('# Pixels in Image')

#
#
# # Image Preprocessing in Keras
#
# Before training, you'll first modify your images to be better suited for training a convolutional neural network. For this task you'll use the Keras [ImageDataGenerator](https://keras.io/preprocessing/image/) function to perform data preprocessing and data augmentation.
#
# Run the next two cells to import this function and create an image generator for preprocessing.

# In[10]:

# Import data generator from keras
from keras.preprocessing.image import ImageDataGenerator

# In[11]:

# Normalize images
image_generator = ImageDataGenerator(
    samplewise_center=True, #Set each sample mean to 0.
    samplewise_std_normalization= True # Divide each input by its standard deviation
)

# ### Standardization
#
# The `image_generator` you created above will act to adjust your image data such that the new mean of the data will be zero, and the standard deviation of the data will be 1.
#
# In other words, the generator will replace each pixel value in the image with a new value calculated by subtracting the mean and dividing by the standard deviation.
#
# $$\frac{x_i - \mu}{\sigma}$$
#
# Run the next cell to pre-process your data using the `image_generator`. In this step you will also be reducing the image size down to 320x320 pixels.

# In[12]:

# Flow from directory with specified batch size and target image size
generator = image_generator.flow_from_dataframe(
        dataframe=train_df,
        directory="nih/images-small/",
        x_col="Image", # features
        y_col= ['Mass'], # labels
        class_mode="raw", # 'Mass' column should be in train_df
        batch_size= 1, # images per batch
        shuffle=False, # shuffle the rows or not
        target_size=(320,320) # width and height of output image
)

# Run the next cell to plot up an example of a pre-processed image

# In[13]:

# Plot a processed image
sns.set_style("white")
generated_image, label = generator.__getitem__(0)
plt.imshow(generated_image[0], cmap='gray')
plt.colorbar()
plt.title('Raw Chest X Ray Image')
print(f"The dimensions of the image are {generated_image.shape[1]} pixels width and {generated_image.shape[2]} pixels height")
print(f"The maximum pixel value is {generated_image.max():.4f} and the minimum is {generated_image.min():.4f}")
print(f"The mean value of the pixels is {generated_image.mean():.4f} and the standard deviation is {generated_image.std():.4f}")

# Run the cell below to see a comparison of the distribution of pixel values in the new pre-processed image versus the raw image.

# In[14]:

# Include a histogram of the distribution of the pixels
sns.set()
plt.figure(figsize=(10, 7))

# Plot histogram for original iamge
sns.distplot(raw_image.ravel(),
             label=f'Original Image: mean {np.mean(raw_image):.4f} - Standard Deviation {np.std(raw_image):.4f} \n '
             f'Min pixel value {np.min(raw_image):.4} - Max pixel value {np.max(raw_image):.4}',
             color='blue',
             kde=False)

# Plot histogram for generated image
sns.distplot(generated_image[0].ravel(),
             label=f'Generated Image: mean {np.mean(generated_image[0]):.4f} - Standard Deviation {np.std(generated_image[0]):.4f} \n'
             f'Min pixel value {np.min(generated_image[0]):.4} - Max pixel value {np.max(generated_image[0]):.4}',
             color='red',
             kde=False)

# Place legends
plt.legend()
plt.title('Distribution of Pixel Intensities in the Image')
plt.xlabel('Pixel Intensity')
plt.ylabel('# Pixel')

# #### That's it for this exercise, you should now be a bit more familiar with the dataset you'll be using in this week's assignment!