"""
.. _example_getting_started:

======================
Skore: getting started
======================

This guide illustrates how to use skore through a complete
machine learning workflow for binary classification:

#. Set up a proper experiment with training and test data
#. Develop and evaluate multiple models using cross-validation
#. Compare models to select the best one
#. Validate the final model on held-out data
#. Track and organize your machine learning results

Throughout this guide, we will see how skore helps you:

* Avoid common pitfalls with smart diagnostics
* Quickly get rich insights into model performance
* Organize and track your experiments

Storing reports in Skore Hub
----------------------------

At the end of this example, we send the reports in Skore Hub
(https://skore.probabl.ai/) that is a platform for storing, sharing and exploring
your machine learning reports.

To run this example and push in your own Skore Hub workspace and project, you can run
this example with the following command:

.. code-block:: bash

    WORKSPACE=<workspace> PROJECT=<project> python plot_getting_started.py

In this gallery, we are going to push the different reports into a public
workspace.
"""

# %%
# Setting up our classification problem
# =====================================
#
# Let's start by loading the "toxicity" dataset, a classification
# problem where we classify tweets as "toxic" or "not toxic".

# %%
from skrub import TableReport, datasets

toxicity = datasets.fetch_toxicity()
X, y = toxicity.X, toxicity.y
TableReport(toxicity.toxicity)

# %%
# We create a held-out test set to evaluate our final model once we are done
# experimenting.

# %%
from sklearn.model_selection import train_test_split

X_experiment, X_holdout, y_experiment, y_holdout = train_test_split(
    X, y, random_state=0
)

# %%
# Model development with cross-validation
# =======================================
#
# We will investigate two different families of models using cross-validation.
#
# 1. A :class:`~sklearn.linear_model.LogisticRegression` which is a linear model
# 2. A :class:`~sklearn.ensemble.RandomForestClassifier` which is a more
#    powerful model.
#
# In both cases, we rely on :func:`skrub.tabular_pipeline` to choose the proper
# preprocessing depending on the kind of model.
#
# Cross-validation is necessary to get a more reliable estimate of model performance.
# skore makes it easy through :class:`skore.CrossValidationReport`.

# %%
# Model no. 1: logistic regression with preprocessing
# ---------------------------------------------------
#
# Our first model will be a linear model, with automatic preprocessing of the text feature.
# Under the hood, skrub's :class:`~skrub.TableVectorizer` will adapt the
# preprocessing based on our choice to use a linear model.

# %%
from sklearn.linear_model import LogisticRegression
from skrub import tabular_pipeline

logistic_regression = tabular_pipeline(LogisticRegression())
logistic_regression

# %%
# We now evaluate our model with cross-validation, using :func:`~skore.evaluate`
# with `splitter=5` to perform 5-fold cross-validation.
# This returns a :class:`~skore.CrossValidationReport` object, which can be used to
# access the performance metrics and other information about the model.

# %%
from skore import evaluate

logreg_cv_report = evaluate(
    logistic_regression, X_experiment, y_experiment, pos_label="Toxic", splitter=5
)
logreg_cv_report

# %%
# A report will quickly show important information regarding the performance of the
# model, the dataset used and the architecture of the model. This information is only
# a quick overview and one can dig deeper into the report to get more information.
#
# Indeed, Skore reports allow to structure the statistical information
# we look for when experimenting with predictive models. First, the
# :meth:`~skore.CrossValidationReport.help` method shows us all its available methods
# and attributes, with the knowledge that our model was trained for classification:

# %%
logreg_cv_report.help()

# %%
# For example, we can examine the training data, which excludes the held-out data:

# %%
logreg_cv_report.data.summarize()

# %%
# Additionally we can run automatic checks on the model and get a summary of the findings:

# %%
logreg_cv_report.checks.summarize()

# %%
# But we can also quickly get an overview of the performance of our model,
# using :meth:`~skore.CrossValidationReport.metrics.summarize`:

# %%
logreg_metrics = logreg_cv_report.metrics.summarize()
logreg_metrics.frame(favorability=True)

# %%
# .. note::
#
#     `favorability=True` adds a column showing whether higher or lower metric values
#     are better.

# %%
# In addition to the summary of metrics, skore provides more advanced statistical
# information such as the precision-recall curve:

# %%
precision_recall = logreg_cv_report.metrics.precision_recall()
precision_recall.help()

# %%
# .. note::
#
#     The output of :meth:`~skore.CrossValidationReport.metrics.precision_recall` is a
#     :class:`~skore.Display` object. This is a common pattern in skore which allows us
#     to access the information in several ways.

# %%
# We can visualize the critical information as a plot, with only a few lines of code:

# %%
_ = precision_recall.plot()

# %%
# Or we can access the raw information as a dataframe if additional analysis is needed:

# %%
precision_recall.frame()

# %%
# As another example, we can plot the confusion matrix with the same consistent API:

# %%
confusion_matrix = logreg_cv_report.metrics.confusion_matrix()
_ = confusion_matrix.plot()

# %%
# Skore also provides utilities to inspect models. Since our model is a linear
# model, we can study the importance that it gives to each feature:

# %%
coefficients = logreg_cv_report.inspection.coefficients()
coefficients.frame()

# %%
_ = coefficients.plot(select_k=15)

# %%
# Model no. 2: Random forest
# --------------------------
#
# Now, we cross-validate a more powerful model using
# :class:`~sklearn.ensemble.RandomForestClassifier`. Again, we rely on
# :func:`~skrub.tabular_pipeline` to perform the appropriate preprocessing to use with
# this model.

# %%
from sklearn.ensemble import RandomForestClassifier

random_forest = tabular_pipeline(RandomForestClassifier(random_state=0))
random_forest

# %%
rf_cv_report = evaluate(
    random_forest, X_experiment, y_experiment, pos_label="Toxic", splitter=5
)
rf_cv_report

# %%
rf_cv_report.checks.summarize()

# %%
# We will now compare this new model with the previous one.

# %%
# Comparing our models
# ====================
#
# Now that we have our two models, we need to decide which one should go into
# production. We can compare them with the :func:`~skore.compare` function that returns a
# :class:`~skore.ComparisonReport`:

# %%
from skore import compare

comparison = compare(
    {
        "logistic regression": logreg_cv_report,
        "random forest": rf_cv_report,
    },
)
comparison

# %%
# This report follows the same API as :class:`~skore.CrossValidationReport`:
comparison.help()

# %%
# We have access to the same tools to perform statistical analysis and compare both
# models:
comparison_metrics = comparison.metrics.summarize()
comparison_metrics.frame(favorability=True)

# %%
_ = comparison.metrics.precision_recall().plot()

# %%
# Based on the previous tables and plots, it seems that the
# :class:`~sklearn.ensemble.RandomForestClassifier` model has slightly worse
# performance due to overfitting on this small dataset. We make the choice
# to deploy the linear model to make a comparison with the coefficients study shown
# earlier.

# %%
# Final model evaluation on held-out data
# =======================================
#
# Now that we have chosen to deploy the linear model, we will train it on the full
# experiment set and evaluate it on our held-out data: training on more data should help
# performance and we can also validate that our model generalizes well to new data. This
# can be done in one step with :meth:`~skore.ComparisonReport.create_estimator_report`.

# %%

final_report = comparison.create_estimator_report(
    report_key="logistic regression", X_test=X_holdout, y_test=y_holdout
)
final_report

# %%
# This returns a :class:`~skore.EstimatorReport` which has a similar API to the other
# report classes:

# %%
final_metrics = final_report.metrics.summarize()
final_metrics.frame()

# %%
_ = final_report.metrics.confusion_matrix().plot()

# %%
# We can easily combine the results of the previous cross-validation together with
# the evaluation on the held-out dataset, since the two are accessible as tables. This
# way, we can check if our chosen model meets the expectations we set during the
# experiment phase.

# %%
final_frame = final_metrics.frame().to_frame()
cv_frame = logreg_cv_report.metrics.summarize().frame()
final_frame.merge(cv_frame, on="metric", how="outer")

# %%
# As expected, our final model gets better performance, likely thanks to the
# larger training set.

# %%
# Our final sanity check is to compare the features considered most impactful
# between our final model and the cross-validation:

# %%
final_coefficients = final_report.inspection.coefficients()
cv_coefficients = logreg_cv_report.inspection.coefficients()

features_final_coefficients = final_coefficients.frame(select_k=15)["feature"]
features_cv_coefficients = cv_coefficients.frame(select_k=15)["feature"]

print(
    f"Most important features available in both models: "
    f"{set(features_final_coefficients).intersection(set(features_cv_coefficients))}"
)

print(
    f"Most important features available in final model but not in cross-validation: "
    f"{set(features_final_coefficients).difference(set(features_cv_coefficients))}"
)

# %%
# We can further check if there is a drastic difference in the ordering by plotting
# those features with the largest absolute coefficients.

# %%
final_coefficients.plot(select_k=15, sorting_order="descending")
_ = cv_coefficients.plot(select_k=15, sorting_order="descending")

# %%
# They seem very similar, so we are done!

# %%
# Tracking our work with a skore Project
# ======================================
#
# Now that we have completed our modeling workflow, we should store our models in a
# safe place for future work. Indeed, if this research notebook were modified,
# we would no longer be able to relate the current production model to the code that
# generated it.
#
# We can use a :class:`skore.Project` to keep track of our experiments.
# This makes it easy to organize, retrieve, and compare models over time.
#
# Usually this would be done as you go along the model development, but
# in the interest of simplicity we kept this until the end.
#
# We are using Skore Hub (https://skore.probabl.ai/) to store and review our reports.
#
# .. note::
#    Here, we are using Skore Hub to store and analyze the reports that we computed.
#    Note that you can store reports as well locally using `mode="local"` when creating
#    or loading projects via `skore.Project`.

# sphinx_gallery_start_ignore
#
# Configure the context variables and ensure that the example is run with sufficient
# credentials. This is a useful consistency check for CI where you can't have
# interactive login.
import os

if os.environ.get("SPHINX_BUILD"):
    GITHUB = os.environ.get("GITHUB_ACTIONS")
    API_KEY = os.environ.get("SPHINX_EXAMPLE_API_KEY")
    WORKSPACE = os.environ.get("SPHINX_EXAMPLE_WORKSPACE")
    VERSION = os.environ.get("SPHINX_VERSION")

    if not (GITHUB and API_KEY and WORKSPACE and VERSION):
        raise RuntimeError("Required environment variables not set.")

    PROJECT = f"example-getting-started-{VERSION}"
    os.environ["SKORE_HUB_API_KEY"] = API_KEY
else:
    assert (WORKSPACE := os.environ.get("WORKSPACE")), "`WORKSPACE` must be defined."
    assert (PROJECT := os.environ.get("PROJECT")), "`PROJECT` must be defined."
# sphinx_gallery_end_ignore

from skore import login

login()

# sphinx_gallery_start_ignore
#
# Delete project before running the example.
from httpx import HTTPStatusError, codes
from skore import Project

try:
    Project.delete(name=PROJECT, mode="hub", workspace=WORKSPACE)
except HTTPStatusError as e:
    if e.response.status_code != codes.NOT_FOUND:
        raise
# sphinx_gallery_end_ignore

# %%
# We load or create a hub project:

project = Project(name=PROJECT, mode="hub", workspace=WORKSPACE)

# %%
# We store our reports with descriptive keys:

project.put("logreg_cv", logreg_cv_report)

# %%
project.put("rf_cv", rf_cv_report)

# %%
# In this example, we created a read-only Skore Hub project that you can visit by
# clicking on the link above and explore the reports.

# %%
# Now we can retrieve a summary of our stored reports:

# %%
summary = project.summarize()
summary

# %%
# .. note::
#     :meth:`~skore.Project.summarize` returns a :class:`~skore.Summary` object. In a
#     Jupyter environment it renders as an interactive table where you can filter rows
#     and pick reports across the different views; the selection produces a query string
#     ready to pass to :meth:`~skore.Summary.query` so you can recover exactly those
#     reports.

# %%
# Once you filtered the summary (e.g. to keep only the cross-validation reports), if
# you now call :meth:`~skore.Summary.compare`, you get only the
# :class:`~skore.CrossValidationReport` objects, which
# you can directly put in the form of a :class:`~skore.ComparisonReport`:

# %%
new_report = summary.query('report_type == "cross-validation"').compare(
    return_as="report"
)
new_report

# %%
# .. admonition:: Stay tuned!
#
#   This is only the beginning for skore. We welcome your feedback and ideas
#   to make it the best tool for end-to-end data science.
#
#   Key benefits of using skore in your ML workflow:
#
#   * Standardized evaluation and comparison of models
#   * Rich visualizations and diagnostics
#   * Organized experiment tracking
#   * Seamless integration with scikit-learn
#
#   Feel free to join our community on `Discord <https://discord.probabl.ai>`_
#   or `create an issue <https://github.com/probabl-ai/skore/issues>`_.
