"""
The class that holds the logic to compute ETAs predictability through 3 components ranging from 0 to 1:

- The RAW PREDICTABILITY: 0 when projections are outside the convergence cone, 1 when projections are spot on
- The ACCURACY: 0 when the projected range is wider than the cone, 1 when projection is spot (and not a range)
- The PREDICTABILITY: considering both RAW PREDICTABILITY and ACCURACY.

For further details on logic to compute these PREDICTABILITY COMPONENTS, please refer to the blueprint.
TODO: publish here a post to illustrate and explain.
"""

from enum import Enum, auto
from inspect import stack
from logging import Logger, getLogger
from typing import Self, TypedDict

import numpy as np
import pandas as pd
from numpy import where
from pandas import DataFrame, MultiIndex, Series

from genov.helpers.dataframe.df_typer import DfTyper


class DescribeStatisticsForSpotMeasures(TypedDict):
    """
    Statistics as a Typed Dictionary that is used to describe SPOT MEASUREs data:

    - int_nb_of_records: the total nb of records, across STR_KEYs and INT_LABELs
    - int_nb_of_keys: the nb of different STR_KEYs
    - int_minimal_label: the minimal INT_LABEL across STR_KEYs
    - int_maximal_label: the maximal INT_LABEL across STR_KEYs
    - int_minimal_measure: the minimal INT_MEASURE across STR_KEYs and INT_LABELs
    - int_maximal_measure: the maximal INT_MEASURE across STR_KEYs and INT_LABELs.

    These statistics are computed on DF_DATA, that contains the data after cleansing (eg. useless tails) and sampling
    up.
    """

    int_nb_of_records: int
    int_nb_of_keys: int
    int_minimal_label: np.int64
    int_maximal_label: np.int64
    int_minimal_measure: np.int64
    int_maximal_measure: np.int64


class DescribeStatisticsForRangeMeasures(TypedDict):
    """
    Statistics as a Typed Dictionary that is used to describe RANGE MEASUREs data:
    - int_nb_of_records: the total nb of records, across STR_KEYs and INT_LABELs
    - int_nb_of_keys: the nb of different STR_KEYs
    - int_minimal_label: the minimal INT_LABEL across STR_KEYs
    - int_maximal_label: the maximal INT_LABEL across STR_KEYs
    - int_minimal_measure_from: the minimal INT_MEASURE_FROM across STR_KEYs and INT_LABELs
    - int_minimal_measure_to: the minimal INT_MEASURE_TO across STR_KEYs and INT_LABELs
    - int_maximal_measure_from: the maximal INT_MEASURE_FROM across STR_KEYs and INT_LABELs
    - int_maximal_measure_to: the maximal INT_MEASURE_TO across STR_KEYs and INT_LABELs.

    These statistics are computed on DF_DATA, that contains the data after cleansing (eg. useless tails) and sampling
    up.
    """

    int_nb_of_records: int
    int_nb_of_keys: int
    int_minimal_label: np.int64
    int_maximal_label: np.int64
    int_minimal_measure_from: np.int64
    int_maximal_measure_from: np.int64
    int_minimal_measure_to: np.int64
    int_maximal_measure_to: np.int64


class DpDatePredictability:
    """
    The class that holds the logic to compute ETAs predictability through 3 components with values ranging from 0 to 1:

    - The RAW PREDICTABILITY: 0 when projections are outside the convergence cone, 1 when projections are spot on
    - The ACCURACY: 0 when the projected range is wider than the cone, 1 when projection is a spot value (and not a
      range)
    - The PREDICTABILITY: considering both components RAW PREDICTABILITY and ACCURACY.

    For further details on the logic to compute these PREDICTABILITY COMPONENTS, please refer to the blueprint.

    TODO: publish here a post to illustrate and explain.

    Below, a possible flow of operations:

    - Instantiate an object DpPredictability

        - Set the columns' labels, ie STR_KEY, INT_LABEL, INT_MEASURE/ INT_MEASURE_FROM/ INT_MEASURE_TO, STR_STATUS

    - Set data, using the function `set_data`

        - Class Property DF_DATA is set from Data, after some processing:

            - Data are checked (aka business rules are verified)
            - Data are cleansed (aka useless tails are deleted)
            - Data are sampled up (to make sure there are continuous observations up to the latest meaningful INT_LABEL)
            - Data are enriched (incl. INCREMENTAL DATA that do not rely on observations' labels)

        - Class properties DF_DATA_AS_OF_LABEL and INT_AS_OF_LABEL are reinitialized to NONE

    - Decompose the data, using the function `decompose`

        - Given that the function `as_of_label` has not been called yet, the class properties DF_DATA_AS_OF_LABEL and
          INT_AS_OF_LABEL are NONE, and need to be set:

            - INT_AS_OF_LABEL is set to the maximal INT_LABEL from DF_DATA
            - DF_DATA_AS_OF_LABEL is set as a copy from DF_DATA

        - Residual INCREMENTAL DATA (the data that consider INT_AS_OF_LABEL) are enriched into DF_DATA_AS_OF_LABEL,
          which is returned to the caller

    - Compute spot predictability, using the function `compute`

        - Given function `decompose` has been called, DF_DATA_AS_OF_LABEL contains all the INCREMENTAL DATA

            - Spot predictability components are aggregated from DF_DATA_AS_OF_LABEL

        - A DataFrame is returned:

            - STR_KEYs as rows`index
            - And with 3 columns:

                - "_spot_raw_predictability", aka DpDatePredictability.COL_LBL_SPOT_RAW_PREDICTABILITY
                - "_spot_accuracy", aka DpDatePredictability.COL_LBL_SPOT_ACCURACY
                - "_spot_predictability", aka DpDatePredictability.COL_LBL_SPOT_PREDICTABILITY

    - Modify as_of_label, using the function `as_of_label`

        - The function parameter is persisted under INT_AS_OF_LABEL
        - DF_DATA_AS_OF_LABEL is refreshed from df_data and truncated from INT_AS_OF_LABEL (aka only earlier labels
          are kept)
        - DF_DATA_AS_OF_LABEL is cleansed (aka useless tails are deleted)
        - Residual INCREMENTAL DATA are refreshed based on the parameter INT_AS_OF_LABEL

    - Compute historic predictabilities, using function `slide`

        - Spot predictability is computed for each and every labels in DF_DATA_AS_OF_LABEL
        - A DataFrame is returned:

            - As a copy from DF_DATA_AS_OF_LABEL
            - And with 3 columns:

                - "_spot_raw_predictability", aka DpDatePredictability.COL_LBL_SPOT_RAW_PREDICTABILITY
                - "_spot_accuracy", aka DpDatePredictability.COL_LBL_SPOT_ACCURACY
                - "_spot_predictability", aka DpDatePredictability.COL_LBL_SPOT_PREDICTABILITY.

    Suggestions to optimize this class:

    - For records which are eventually OPEN or CANCELLED, we repeat the last PREDICTABILITY (incl. RAW PREDICTABILITY
      and ACCURACY) until the latest MEASURE (the `ffill` call in both functions
      `_compute_incremental_predictability_by_label_for_a_given_key` and
      `_compute_incremental_accuracy_by_label_for_a_given_key`). This could be prevented modulo some adjustment
      in the function `_compute_spot_predictability_and_accuracy_for_a_given_key` to compute means slightly differently.
      TODO: is that still the case?

    - The algorithm relies on MEASUREs for all LABELs, leading to repetitions when MEASUREs do not evolve across
      LABELs. This completion by repeating the last known MEASURE across missing LABELs is handled by the function
      `_filter_and_resample_data_by_key`. This could be prevented by using polygamma function to compute PREDICTABILITY
      (incl. RAW PREDICTABILITY and ACCURACY)

    - Adjust function decompose, so we can compute across all keys at once (aka make str_key facultative)
      TODO: is that still the case?

    For sake of documentation, below the hierarchy of functions:

    - set_data

      - _copy_flatten_check_type_reduce_order_and_sort_data

      - _version_status_changes_by_key

      - _drop_useless_heads_and_tails__on_versioned_group

      - _sample_up_data__on_versioned_group

        - _version_status_changes_by_key

      - _prepend_first_observation_by_key

      - as_of_label

    """

    _obj_logger: Logger = getLogger(__name__)

    # ERROR 0xx: rose when instantiating class
    __ERROR_001__: str = "Key, Label, Measure (or From and To) and Status columns should not have the same label"

    # ERROR 1xx: to raise when computing predictability (step copying, checking and cleansing data)
    __ERROR_100__: str = (
        "BR_100: The data provided to compute date predictability does not comply to more than one business rules"
    )
    __ERROR_101__: str = (
        'BR_101: Columns labels should not start with an "_" ("_" is reserved for temporary columns...)'
    )
    __ERROR_102__: str = "BR_102: Key column should exist"
    __ERROR_103__: str = "BR_103: Label column should exist"
    __ERROR_104__: str = "BR_104: Measure column (or columns from and to) should exist"
    __ERROR_105__: str = "BR_105: Status column should exist"

    __ERROR_106__: str = "BR_106: Label column should not have any null value"
    __ERROR_107__: str = "BR_107: Label column should be typed as integers (or cast-able to integers)"
    __ERROR_108__: str = (
        "BR_108: Measure column (or columns from and to) should be typed as integers (or cast-able to integers)"
    )
    __ERROR_109__: str = (
        'BR_109: In case measure is a range (aka a tuple "from" - "to"), "from" should be less or equal to "to"'
    )
    __ERROR_110__: str = "BR_110: Status column should only contain valid status: OPEN, CLOSED, CANCELLED"

    __ERROR_111__: str = (
        "BR_111: Combination across key and label columns should be unique (aka one single measure"
        " (or from-to) for one given key and a given label)"
    )
    __ERROR_112__: str = (
        "BR_112: When status is OPEN, measure (aka eta) (or min across from and to) should be"
        " greater to label (aka date)"
    )
    __ERROR_113__: str = "BR_113: When status is CLOSED, in case measure is a range, both from and to should be equal"
    __ERROR_114__: str = (
        "BR_114: When status is CLOSED, measure (aka eta) (or from and to) should be lower or equal to label (aka date)"
    )

    # ERROR 2xx: other exceptions that can be raised
    __ERROR_201__: str = (
        "BR_201: When last status for a key is OPEN, the measure (aka eta) (or from and to) should be greater than the "
        "latest label across all keys in DF_DATA"
    )
    __ERROR_202__: str = "BR_202: When computing as of a given label, we cannot invent data beyond INT_LAST_AS_OF_LABEL"

    #: The OPEN status
    STATUS_OPEN: str = "open"
    #: The CLOSED status
    STATUS_CLOSED: str = "closed"
    #: The CANCELLED status
    STATUS_CANCELLED: str = "cancelled"

    #: The column label in the DataFrame where Spot Raw Predictability is stored
    COL_LBL_SPOT_RAW_PREDICTABILITY: str = "_spot_raw_predictability"
    #: The column label in the DataFrame where Spot Accuracy is stored
    COL_LBL_SPOT_ACCURACY: str = "_spot_accuracy"
    #: The column label in the DataFrame where Spot Predictability is stored
    COL_LBL_SPOT_PREDICTABILITY: str = "_spot_predictability"

    #: The column label in the DataFrame where Incremental Raw Predictability is stored
    COL_LBL_INCREMENTAL_RAW_PREDICTABILITY: str = "_incremental_raw_predictability"
    #: The column label in the DataFrame where Incremental Accuracy is stored
    COL_LBL_INCREMENTAL_ACCURACY: str = "_incremental_accuracy"
    #: The column label in the DataFrame where Incremental Predictability is stored
    COL_LBL_INCREMENTAL_PREDICTABILITY: str = "_incremental_predictability"

    @property
    def dict_columns_and_types(self) -> dict[str, type]:
        """
        A helper dictionary that is used whenever we need to type a dataframe, to ensure we return typed data.
        """
        return {
            # DF_DATA
            self.str_column_label_for_keys: str,
            self.str_column_label_for_labels: np.int64,
            self.str_column_label_for_measures: np.int64,
            self.str_column_label_for_measures_from: np.int64,
            self.str_column_label_for_measures_to: np.int64,
            self.str_column_label_for_status: str,
            # VERSIONS
            "_versioned_status": np.int64,
            "_versioned_status_version": np.int64,
            # PREPEND
            "_first_label": np.int64,
            "_first_measure": np.int64,
            "_first_measure_from": np.int64,
            "_first_measure_to": np.int64,
            "_first_status": str,
            # APPEND
            "_last_label": np.int64,
            "_last_measure": np.int64,
            "_last_measure_from": np.int64,
            "_last_measure_to": np.int64,
            "_last_status": str,
            "_last_versioned_status": np.int64,
            "_last_versioned_status_version": np.int64,
            "_last_measure_floor": np.int64,
            "_last_measure_ceil": np.int64,
            self.COL_LBL_INCREMENTAL_RAW_PREDICTABILITY: np.float64,
            self.COL_LBL_INCREMENTAL_ACCURACY: np.float64,
            self.COL_LBL_INCREMENTAL_PREDICTABILITY: np.float64,
            self.COL_LBL_SPOT_RAW_PREDICTABILITY: np.float64,
            self.COL_LBL_SPOT_ACCURACY: np.float64,
            self.COL_LBL_SPOT_PREDICTABILITY: np.float64,
        }

    @property
    def str_column_label_for_keys(self) -> str:
        """
        The column label for the keys.
        Keys are used to group observations belonging to the same group (e.g., user story, stream, project, etc.).
        """
        return self._str_column_label_for_keys

    @property
    def str_column_label_for_labels(self) -> str:
        """
        The column label for the labels. Labels are used to timestamp observations (aka. date).
        """
        return self._str_column_label_for_labels

    @property
    def str_column_label_for_measures(self) -> str:
        """
        The column label for the measures.
        Measures are the observations' values (aka. eta).

        Measures can be of two kinds:

        - Spot measures: the column label for the measures is the one provided when calling "init"
        - Range measures: the column label for the measures is "_spotified_measure". This column will be computed while
          "setting data" as the mean of the measures ranges. It is then used while compute incremental predictability
          to determine a floor and a ceil, and compute a convergence cone.
        """
        return self._str_column_label_for_measures

    @property
    def str_column_label_for_measures_from(self) -> str:
        """
        The column label for the lower-bound measure.
        Measures are the observations' values (aka. eta from).

        Measures can be of two kinds:

        - Spot measures: the column label for the measures is the one provided when calling "init" (same label for
          measures, measures_from and measures_to)
        - Range measures: the column label for the lower-bound measures is the one provided when calling "init"
        """
        return self._str_column_label_for_measures_from

    @property
    def str_column_label_for_measures_to(self) -> str:
        """
        The column label for the upper-bound measure.
        Measures are the observations' values (aka. eta to).

        Measures can be of two kinds:

        - Spot measures: the column label for the measures is the one provided when calling "init" (same label for
          measures, measures_from and measures_to)
        - Range measures: the column label for the upper-bound measures is the one provided when calling "init"
        """
        return self._str_column_label_for_measures_to

    @property
    def lst_columns_labels_for_measures(self) -> list[str]:
        """
        Helper to get all measures columns at once.

        - Either a list of one label for spot measures
        - Or a list of two labels for range measures.
        """
        if self._str_column_label_for_measures_from == self._str_column_label_for_measures_to:
            return [self._str_column_label_for_measures]

        return [self._str_column_label_for_measures_from, self._str_column_label_for_measures_to]

    @property
    def str_column_label_for_status(self) -> str:
        """
        The column label for the status. Statuses are used to distinguish observations that could be:

        - "Open": not yet a final status
        - "closed": aka delivered
        - "canceled": aka canceled.
        """
        return self._str_column_label_for_status

    @property
    def df_data(self) -> DataFrame | None:
        """
        The data after having been checked and cleansed.

        By checking, we mean all business rules "1xx" are validated.
        By cleansed, we mean multi indices are flattened; dataframe is sorted by label.
        """
        if self._df_data is None:
            raise AttributeError(name="df_date", obj="Before getting attribute, you need to call set_data.")
        return self._df_data

    @property
    def int_first_label(self) -> np.int64:
        """
        The minimal label across all keys. This value is the same for both DF_DATA and DF_DATA_AS_OF_LABEL (if set).
        """
        if self._df_data is None:
            raise AttributeError(name="int_first_label", obj="Before getting attribute, you need to call set_data.")
        return self._int_first_label

    @property
    def int_last_label(self) -> np.int64:
        """
        The maximal label across all keys in DF_DATA. Given DF_DATA_AS_OF_LABEL is a truncated copy of DF_DATA, its
        maximal label can be different.
        """
        if self._df_data is None:
            raise AttributeError(name="int_last_label", obj="Before getting attribute, you need to call set_data.")
        return self._int_last_label

    @property
    def df_data_as_of_label(self) -> DataFrame | None:
        """
        The data as of a given date. Could be None.
        """
        return self._df_data_as_of_label

    @property
    def int_as_of_label(self) -> np.int64 | None:
        """
        The label provided when truncating a copy of DF_DATA into DF_DATA_AS_OF_LABEL.
        """
        return self._int_as_of_label

    @property
    def int_last_as_of_label(self) -> np.int64 | None:
        """
        The last label that can be accepted when truncating a copy of DF_DATA into DF_DATA_AS_OF_LABEL.
        """
        return self._int_last_as_of_label

    def is_measure_spot(self) -> bool:
        """
        Returns whether measures are spot or range.
        """
        return self.str_column_label_for_measures_from == self.str_column_label_for_measures_to

    def __init__(
        self,
        str_column_label_for_keys: str = "key",
        str_column_label_for_labels: str = "label",
        x_column_label_for_measures: str | tuple[str, str] = "measure",
        str_column_label_for_status: str = "status",
    ):
        """
        At initialization, we provide labels:
        - For the column where KEYs are stored, a.k.a. IDs to distinguish the different elements under observation
        - For the column where LABELs are stored, a.k.a. dates of observations
        - For the column where MEASUREs are stored, a.k.a. ETAs of observations
        - For the column where STATUS-es are stored, a.k.a. Whether the elements are OPEN, CLOSED or CANCELLED.

        MEASUREs can be of two kinds:
        - Either SPOT MEASURES, with only one single ETA
        - Or RANGE MEASURES, as a tuple of ETAs FROM and TO.

        Exception ValueError is thrown in case the below Business Rule is broken:
        - BR_001: "Key, Label, Measure (or From and To), and Status columns should not have the same label"
        """

        # In case measures are spot, we only have one single column
        if isinstance(x_column_label_for_measures, str):
            self._str_column_label_for_measures_from = x_column_label_for_measures
            self._str_column_label_for_measures_to = x_column_label_for_measures
            self._str_column_label_for_measures: str = x_column_label_for_measures

        # In case measures are ranges, we have 2 columns (listed in a tuple)
        else:
            self._str_column_label_for_measures_from = x_column_label_for_measures[0]
            self._str_column_label_for_measures_to = x_column_label_for_measures[1]
            self._str_column_label_for_measures: str = "_spotified_measure"

        # ##############################################################################################################
        # BR_001: "Key, Label, Measure (or From and To), and Status columns should not have the same label"
        _lst_seen_column = set()
        _lst_duplicated_columns = [
            i_col
            for i_col in [
                str_column_label_for_keys,
                str_column_label_for_labels,
                str_column_label_for_status,
                *self.lst_columns_labels_for_measures,
            ]
            if i_col in _lst_seen_column or _lst_seen_column.add(i_col)
        ]
        if len(_lst_duplicated_columns) > 0:
            raise ValueError(
                "BR_001 breached due to columns [" + ", ".join(_lst_duplicated_columns) + "]: " + self.__ERROR_001__
            )

        self._str_column_label_for_keys: str = str_column_label_for_keys
        self._str_column_label_for_labels: str = str_column_label_for_labels
        self._str_column_label_for_status: str = str_column_label_for_status

        # Attributes that are set while "setting data"
        self._df_data: DataFrame | None = None
        self._int_first_label: np.int64 | None = None
        self._int_last_label: np.int64 | None = None

        # Attributes that are set while setting data "as of label"
        self._df_data_as_of_label: DataFrame | None = None
        self._int_as_of_label: np.int64 | None = None
        self._int_last_as_of_label: np.int64 | None = None

    def set_data(self, df_data: DataFrame) -> Self:
        """
        Function that sets the data to then compute predictability upon

        - Data is copied, flattened, checked, typed, reduced, ordered and sorted through the function

          _copy_flatten_check_type_reduce_order_and_sort_data:

            - Copied: the dataframe provided by the caller is duplicated to prevent side effects (eg. rows dropped)

            - Flattened: multi-indexes (rows and columns) are flattened

            - Checked: data is checked against business rules (listed below)

            - Typed: data is typed:

                - KEY as string

                - LABEL as np.int64

                - MEASURE/ MEASURE_FROM/ MEASURE_TO as np.int64

                - STATUS as string

            - Reduced:

                - KEY and LABEL are set as index

                - Useless columns are discarded to only keep MEASURE/ MEASURE_FROM/ MEASURE_TO and STATUS

            - Ordered: columns are ordered: MEASURE/ MEASURE_FROM/ MEASURE_TO then STATUS

            - Sorted: data is sorted as per the index

        - Useless tails are truncated

        - The first and the latest labels are persisted as class properties INT_FIRST_LABEL and INT_LAST_LABEL

        - Data is sampled-up: missing LABELs are added and filled with previous measures, up to INT_LATEST_LABEL, while
          not producing useless tails

        - We prepend into DF_DATA the incremental predictability data that do not rely on AS_OF_LABEL:

            - _versioned_status

            - _versioned_status_version

            - _first_label

            - _first_measure/ _first_measure_from/ _first_measure_to

            - _first_status

        Eventually, we end up with 2 distinct dataframes:

        - DF_DATA: contains all the data provided by the called (after cleansing, prepending, etc)

        - DF_DATA_AS_OF_LABEL: set to None for the time being;

            - This class property is set:

                - Whenever calling function as_of_label with a particular label

                - Or when calling computing functions: compute, decompose, slide

            - Note: when setting DF_DATA_AS_OF_LABEL, the residual incremental predictability data is computed. This
                processing is triggered just in time when caller is explicit on the AS_OF_LABEL to consider.

        Exception ValueError is thrown in case a Business Rule is broken, or ExceptionGroup is several errors
        are faced:

        - BR_101: Columns labels should not start with an "_" ("_" is reserved for temporary columns...)

        - BR_102: Key column should exist

        - BR_103: Label column should exist

        - BR_104: Measure column (or columns from and to) should exist

        - BR_105: Status column should exist

        - BR_106: Label column should not have any null value

        - BR_107: Label column should be typed as integers (or cast-able to integers)

        - BR_108: Measure column (or columns from and to) should be typed as integers (or cast-able to integers)

        - BR_109: In case measure is a range (aka a tuple "from" - "to"), "from" should be less or equal to "to"

        - BR_110: Status column should only contain valid status: OPEN, CLOSED, CANCELLED

        - BR_111: Combination across key and label columns should be unique (aka one single measure
          (or from-to) for one given key and a given label)

        - BR_112: When status is OPEN, measure (aka eta) (or min across from and to) should be
          greater to label (aka date)

        - BR_113: When status is CLOSED, in case measure is a range, both from and to should be equal

        - BR_114: When status is CLOSED, measure (aka eta) (or from and to) should be lower or equal to label (aka date)

        :param df_data: DataFrame instance containing the observation data

        :return: Itself, a.k.a. Date Predictability instance
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        # In case the parameter df_data is an empty dataframe
        if len(df_data.index) == 0:
            self._obj_logger.warning("The parameter df_data is empty. Business rules are not checked.")
            self._df_data = self._return_empty_dataframe(enum_function=DpDatePredictability.EnumFunction.SET_DATA)
            self._int_last_label = np.int64(0)
            self._int_first_label = np.int64(0)
            self.as_of_label(int_as_of_label=None)
            return self

        # We check the historical measures are correct,
        # Any data issue is escalated through Value Errors,
        # Data is indexed, and sorted,
        # Columns are typed and ordered.
        _df_the_data_being_processed: DataFrame = self._copy_flatten_check_type_reduce_order_and_sort_data(
            df_data=df_data
        )

        # We version the data before removing useless heads and tails, then sampling up
        _df_the_data_being_processed = self._version_status_changes_by_key(df_data=_df_the_data_being_processed)

        # We set the min and max meaningful labels

        # JSG 10-Mar-26
        # self._int_first_label = _df_the_data_being_processed[_df_the_data_being_processed[self.str_column_label_for_status] == self.STATUS_OPEN].index.get_level_values(self.str_column_label_for_labels).min()
        # self._int_last_label = _df_the_data_being_processed[_df_the_data_being_processed[self.str_column_label_for_status] == self.STATUS_OPEN].index.get_level_values(self.str_column_label_for_labels).max()
        #
        # if _df_the_data_being_processed.index.get_level_values(self.str_column_label_for_labels).max() > self._int_last_label:
        #    self._int_last_label = _df_the_data_being_processed[_df_the_data_being_processed.index.get_level_values(self.str_column_label_for_labels) > self._int_last_label].index.get_level_values(self.str_column_label_for_labels).min()

        self._int_first_label = _df_the_data_being_processed.index.get_level_values(
            self.str_column_label_for_labels
        ).min()
        self._int_last_label = _df_the_data_being_processed.index.get_level_values(
            self.str_column_label_for_labels
        ).max()

        # JSG 10-Mar-26

        # ##############################################################################################################
        # When last status for a key is OPEN, the measure (aka eta) (or from and to) should be greater than the latest
        # label across all keys in DF_DATA.
        # Illustration:
        # - For 2 keys 'a' and 'b'
        #   - Latest observation for key 'a' is as of label '10' with status 'open' measure (aka eta) '20'
        #   - Latest observation for key 'b' is as of label '30'
        # - We could compute predictability until label '19' without inventing data
        # - We would have to invent data for key 'a' starting from label '20' onwards, which would not be correct

        # We keep for each key the very last observation
        _df_last_row_per_key: DataFrame = (
            _df_the_data_being_processed.reset_index(level=1)
            .groupby(level=0, group_keys=False)
            .last()
            .set_index(self.str_column_label_for_labels, append=True, drop=False)
        )

        # We only keep the open keys among these latest observations
        _df_last_row_per_key = _df_last_row_per_key[
            _df_last_row_per_key[self.str_column_label_for_status] == DpDatePredictability.STATUS_OPEN
        ]

        # We eventually check no key has an eta prior to INT_LAST_LABEL
        _df_error_201: DataFrame = _df_last_row_per_key[
            _df_last_row_per_key[self.str_column_label_for_measures_from] <= self._int_last_label
        ]

        # We raise an error in case we are missing observations
        if len(_df_error_201.index) > 0:
            raise ValueError(
                "Business Rule breached due to keys '"
                + ",".join(_df_error_201.index.get_level_values(0))
                + "' missing observations until at least label '"
                + str(self._int_last_label)
                + "': "
                + self.__ERROR_201__
            )

        # ##############################################################################################################
        # We set the maximal INT_AS_OF_LABEL we can accept to compute predictability without inventing data
        # Illustration:
        # - For 2 keys 'a' and 'b', with measures '5' and '10' as of label '1', both with status 'open'
        # - We can compute predictability until label '4' without inventing data
        # - We would have to invent data for key 'a' starting from label '5' onwards, and for key 'b' from label '10' onwards
        if len(_df_last_row_per_key.index) == 0:
            self._int_last_as_of_label = max(
                _df_the_data_being_processed.reset_index(level=1)[self.str_column_label_for_labels]
            )
        else:
            self._int_last_as_of_label = max(_df_last_row_per_key[self.str_column_label_for_measures_from]) - 1

        # ##############################################################################################################
        # We now process each KEY, drop useless heads and tails and sample up to the last measure (while not generating
        # useless tails though)
        _df_the_data_being_processed = _df_the_data_being_processed.groupby(level=0, group_keys=False).apply(
            lambda df_group: self._sample_up_data__on_versioned_group(
                df_group=self._drop_useless_heads_and_tails__on_versioned_group(df_group=df_group),
                # int_expand_until_label= self._int_last_label
            )
        )

        # ##############################################################################################################
        # We compute all incremental data not relying on as_of_label into df_data, aka prepend:
        # - _versioned_status (once again !)
        # - _versioned_status_version (once again !)
        # - _first_label
        # - _first_measure/ _first_measure_from/ _first_measure_to
        # - _first_status
        self._df_data = self._prepend_first_observation_by_key(df_data=_df_the_data_being_processed)

        # ##############################################################################################################
        # Eventually, we reset the properties ..._as_of_label
        self.as_of_label(int_as_of_label=None)

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return self

    def _drop_useless_heads_and_tails__on_versioned_group(self, df_group: DataFrame) -> DataFrame:
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        # If there is no observation with a STATUS OPEN
        if len(df_group[df_group[self.str_column_label_for_status] == self.STATUS_OPEN].index) == 0:
            # We return an empty DataFrame
            return self._return_empty_dataframe(
                enum_function=DpDatePredictability.EnumFunction.DROP_USELESS_HEADS_AND_TAILS__ON_VERSIONED_GROUP
            )

        # We retrieve the earliest observation OPEN, so we can remove the useless head
        _int_the_label_from: np.int64 = (
            df_group[df_group[self.str_column_label_for_status] == self.STATUS_OPEN]
            .index.get_level_values(self.str_column_label_for_labels)
            .min()
        )

        # We retrieve the latest observation
        # - In case the last observation is OPEN, we return its LABEL
        if df_group[self.str_column_label_for_status].iloc[-1] == self.STATUS_OPEN:
            _int_the_label_to: np.int64 = df_group.index.get_level_values(self.str_column_label_for_labels).max()

        # - In case the last observation is not OPEN, we exclude the useless tail by returning the earliest LABEL for
        #   the latest version
        else:
            _int_the_label_to: np.int64 = (
                df_group[df_group["_versioned_status"] == df_group["_versioned_status"].max()]
                .index.get_level_values(self.str_column_label_for_labels)
                .min()
            )

        # We return the data minus both useless head and tail
        _df_the_return: DataFrame = df_group.loc[(slice(None), slice(_int_the_label_from, _int_the_label_to)), :]

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return _df_the_return

    def _sample_up_data__on_versioned_group(
        self, df_group: DataFrame, int_expand_until_label: np.int64 | None = None
    ) -> DataFrame:
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        _b_override_last_status_to_closed: bool

        # If the group is empty, we do not have to re-sample records. We return an empty DataFrame
        if len(df_group.index) == 0:
            return self._return_empty_dataframe(
                enum_function=DpDatePredictability.EnumFunction.SAMPLE_UP_DATA__ON_VERSIONED_GROUP
            )

        # If the last observation for the key is OPEN
        if df_group[self.str_column_label_for_status].iloc[-1] == self.STATUS_OPEN:
            # We sample data up to last measure, and override the very last measure to "closed"
            _int_the_last_measure: np.int64 = df_group[self.str_column_label_for_measures_to].iloc[-1]

            if int_expand_until_label is not None:
                if int_expand_until_label > _int_the_last_measure:
                    _int_the_last_measure = int_expand_until_label
                    _b_override_last_status_to_closed = False
                else:
                    _b_override_last_status_to_closed = True

            else:
                _b_override_last_status_to_closed = True

                # raise ValueError("JSG")

            _df_the_index: DataFrame = DataFrame(
                data={
                    self.str_column_label_for_labels: range(_int_the_last_measure + 1)[
                        df_group.index.get_level_values(self.str_column_label_for_labels).min() :
                    ]
                },
                dtype=np.int64,
            )

        # If the last observation for the key is CLOSED or CANCELLED
        else:
            # We then sample data up to the latest meaningful observation for the key
            _df_the_index: DataFrame = DataFrame(
                data={
                    self.str_column_label_for_labels: range(
                        df_group.index.get_level_values(self.str_column_label_for_labels).max() + 1
                    )[df_group.index.get_level_values(self.str_column_label_for_labels).min() :]
                },
                dtype=np.int64,
            )

            _b_override_last_status_to_closed = False

        # We construct the multiindex
        _df_the_index[self.str_column_label_for_keys] = df_group.index.get_level_values(self.str_column_label_for_keys)[
            0
        ]
        _df_the_index = _df_the_index[[self.str_column_label_for_keys, self.str_column_label_for_labels]]

        # We sample up data...
        _df_the_return: DataFrame = df_group.reindex(MultiIndex.from_frame(df=_df_the_index), method="ffill")

        # The very last status cannot be "open": defaulted to "closed"
        if _b_override_last_status_to_closed:
            _df_the_return.at[_df_the_return.index[-1], self.str_column_label_for_status] = self.STATUS_CLOSED

        # We have to recall the function to version status, to properly increment "_versioned_status_version"
        _df_the_return = self._version_status_changes_by_key(df_data=_df_the_return)

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return _df_the_return

    def as_of_label(self, int_as_of_label: np.int64 | int | None):
        """
        The function has 3 major steps:

        - Copy DF_DATA into DF_DATA_AS_OF_LABEL, and truncate data to keep observations with LABELs lower or equal to
          the provided parameter

        - Enrich the residual INCREMENTAL DATA that rely on the latest LABEL, aka append:

          - _last_label

          - _last_measure

          - _last_status

          - _last_versioned_status

          - _last_versioned_status_version

        - Eventually, compute the residual incremental predictability data, aka:

          - _incremental_raw_predictability

          - _incremental_accuracy

          - _incremental_predictability.

        Note: difference between function `_as_of_label` and `as_of_label`:

        - `as_of_label` is called by the end user. It relies on the instance property DF_DATA and persists the instance
          property DF_DATA_AS_OF_LABEL. This instance property is later consumed when end users call function such as
          `compute` or `decompose`.

        - `_as_of_label` is called either by `as_of_label` or by `_slide`. It does not rely on the instance property
          DF_DATA, but it receives the DataFrame to process as a parameter. It does not persist the instance property
          DF_DATA_AS_OF_LABEL, but it returns the DataFrame enriched with incremental predictability components.

        In case DF_DATA is an empty dataframe:

        - DF_DATA_AS_OF_LABEL is initialized to an empty DataFrame

        - INT_AS_OF_LABEL is set to 0

        :param int_as_of_label: LABEL to truncate posterior observations
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        # By default, we do not compute anything...
        _b_the_call_sub_function: bool = False

        # The as of label value to consider, derived from
        # - The parameter if provided
        # - The class properties otherwise
        _int_the_as_of_label: np.int64 = np.int64(0)

        # If we call with the same parameter INT_AS_OF_LABEL as previously, it means data is already available.
        # We don't do anything
        if (self.int_as_of_label == int_as_of_label) & (int_as_of_label is not None):
            self._obj_logger.info("Calling as_of_label with same label '%s'.", int_as_of_label)

        # If DF_DATA is an empty dataframe, we set DF_DATA_AS_OF_LABEL as an empty one
        elif len(self.df_data.index) == 0:
            _int_the_as_of_label = self.int_last_label if int_as_of_label is None else np.int64(int_as_of_label)

            self._df_data_as_of_label = self._return_empty_dataframe(
                enum_function=DpDatePredictability.EnumFunction.AS_OF_LABEL
            )

        # Else, if the parameter INT_AS_OF_LABEL is null, we consider as of the last label from DF_DATA
        elif int_as_of_label is None:
            _int_the_as_of_label = self.int_last_label
            _b_the_call_sub_function = True

        # Else, if the parameter INT_AS_OF_LABEL is as of past date, but before we get any observation, we set
        # DF_DATA_AS_OF_LABEL as an empty one
        elif int_as_of_label < self.int_first_label:
            _int_the_as_of_label = int_as_of_label

            self._df_data_as_of_label = self._return_empty_dataframe(
                enum_function=DpDatePredictability.EnumFunction.AS_OF_LABEL
            )

        elif int_as_of_label > self.int_last_as_of_label:
            raise ValueError("Business Rule breached: " + self.__ERROR_202__)

        # Else, we have df data and a parameter as of label which is greater or equal than the first label
        else:
            _int_the_as_of_label = np.int64(int_as_of_label)
            _b_the_call_sub_function = True

        # If we have to compute df data as of label
        if _b_the_call_sub_function:
            # Eventually, we set the dataframe as of label
            self._df_data_as_of_label = self._as_of_label(df_data=self.df_data, int_as_of_label=_int_the_as_of_label)

            self._int_as_of_label = _int_the_as_of_label

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

    def _as_of_label(self, df_data: DataFrame, int_as_of_label: np.int64) -> DataFrame:
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        # We truncate observations beyond the parameter int_as_of_label
        _df_the_return: DataFrame = df_data[
            df_data.index.get_level_values(self.str_column_label_for_labels) <= np.int64(int_as_of_label)
        ].copy()

        # We process each KEY:
        # - drop useless heads and tails, as some might have appeared by truncating data as of a label
        # - and sample up to the last label (while not generating useless tails though)
        _df_the_return = _df_the_return.groupby(level=0, group_keys=False).apply(
            lambda df_group: self._sample_up_data__on_versioned_group(
                df_group=self._drop_useless_heads_and_tails__on_versioned_group(df_group=df_group),
                int_expand_until_label=int_as_of_label,
            )
        )

        # We append the residual INCREMENTAL DATA that rely on the parameter int_as_of_label
        _df_the_return = self._append_last_observation_by_key(df_data=_df_the_return)

        # We compute the INCREMENTAL PREDICTABILITY DATA
        _df_the_return = self._compute_incremental_raw_predictability(df_data=_df_the_return)
        _df_the_return = self._compute_incremental_accuracy(df_data=_df_the_return)

        _df_the_return[self.COL_LBL_INCREMENTAL_PREDICTABILITY] = where(
            np.isnan(_df_the_return[self.COL_LBL_INCREMENTAL_ACCURACY]),
            _df_the_return[self.COL_LBL_INCREMENTAL_RAW_PREDICTABILITY],
            _df_the_return[self.COL_LBL_INCREMENTAL_RAW_PREDICTABILITY]
            * _df_the_return[self.COL_LBL_INCREMENTAL_ACCURACY],
        )

        # Eventually, we ensure all columns are properly typed
        _df_the_return = DfTyper(
            lst_map=[
                {DfTyper.NODE_LBL: i_col, DfTyper.NODE_TYP: self.dict_columns_and_types[i_col]}
                for i_col in _df_the_return.columns
            ]
        ).df_type(df_instance=_df_the_return)

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return _df_the_return

    def compute(self) -> DataFrame:
        """
        The function computes KEY by KEY the various spot predictability components:

        - RAW PREDICTABILITY, not considering the accuracy

        - ACCURACY

        - PREDICTABILITY.

        The function relies on the function DECOMPOSE that is called for all KEYs, with incremental values averaged to
        get spot values.

        Note: difference between function `_compute` and `compute`:

        - `compute` is called by the end user, and compute incremental predictability components on
          DF_DATA_AS_OF_LABEL (a subset of DF_DATA as of a given label which is set by `as_of_label`). Behind the
          scene, `compute` calls `_compute`, after having called `as_of_label` if DF_DATA_AS_OF_LABEL was not set yet.

        - `_compute` is called either by `_compute` or by `_slide`, and receives as a parameter the DataFrame to
          compute spot predictability components. It is expected this DataFrame does contain all the computed fields
          required, including those which depend on the `as_of_label` (aka. _last_label, _last_status, etc.).

        :return: A DataFrame containing 4 columns: KEY, RAW PREDICTABILITY, ACCURACY and PREDICTABILITY
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        if len(self.df_data_as_of_label.index) == 0:
            _df_the_return: DataFrame = self._return_empty_dataframe(
                enum_function=DpDatePredictability.EnumFunction.COMPUTE
            )

        else:
            _df_the_return: DataFrame = self._compute(df_data=self.df_data_as_of_label)

            _df_the_return = _df_the_return.reset_index(level=self.str_column_label_for_labels)
            _df_the_return[self.str_column_label_for_labels] = _df_the_return[self.str_column_label_for_labels].clip(
                upper=self.int_as_of_label
            )
            _df_the_return = _df_the_return.set_index(keys=self.str_column_label_for_labels, append=True)

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return _df_the_return

    def _compute(self, df_data: DataFrame) -> DataFrame:
        """
        The function is called either by `_compute` or by `_slide`, and receives as a parameter the DataFrame to
        compute spot predictability components. It is expected this DataFrame does contain all the computed fields
        required, including those which depend on the `as_of_label` (aka. _last_label, _last_status, etc.).

        :return: A DataFrame containing 4 columns: KEY, RAW PREDICTABILITY, ACCURACY and PREDICTABILITY
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        _df_the_return: DataFrame = (
            df_data.reset_index(level=1)
            .groupby(level=0)
            .agg(
                {
                    self.COL_LBL_INCREMENTAL_RAW_PREDICTABILITY: "mean",
                    self.COL_LBL_INCREMENTAL_ACCURACY: "mean",
                    self.COL_LBL_INCREMENTAL_PREDICTABILITY: "mean",
                    self.str_column_label_for_labels: "max",
                }
            )
            .set_index(keys=self.str_column_label_for_labels, append=True)
        )

        _df_the_return.columns = [
            self.COL_LBL_SPOT_RAW_PREDICTABILITY,
            self.COL_LBL_SPOT_ACCURACY,
            self.COL_LBL_SPOT_PREDICTABILITY,
        ]

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return _df_the_return

    def decompose(self, str_key: str | None = None) -> DataFrame:
        """
        The function computes LABEL by LABEL and KEY by KEY, the incremental contributions to the various
        predictability components:

        - RAW PREDICTABILITY, not considering the accuracy
        - ACCURACY
        - PREDICTABILITY.

        Setting STR_KEY can be used to filter on one single key. A KeyError is raised in case STR_KEY cannot be found
        among observations.

        :return: the DataFrame DF_DATA enriched with 3 columns: RAW PREDICTABILITY, ACCURACY and PREDICTABILITY
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        # In case a key is provided
        if str_key:
            # An exception is thrown in case it does not exist in DF_DATA (and thus in DF_DATA_AS_OF_LABEL)
            if str_key not in self.df_data.index.get_level_values(self.str_column_label_for_keys):
                raise KeyError("Key '" + str_key + "' does not exist.")

            _df_the_return: DataFrame = self.df_data_as_of_label.loc[[str_key]].copy()

        else:
            _df_the_return: DataFrame = self.df_data_as_of_label.copy()

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return _df_the_return

    def slide(self, str_key: str | None = None):
        """
        The function computes LABEL by LABEL and KEY by KEY, the spot predictability components as of the LABEL:

        - RAW PREDICTABILITY, not considering the accuracy
        - ACCURACY
        - PREDICTABILITY.

        Setting STR_KEY can be used to filter on one single key. A KeyError is raised in case STR_KEY cannot be found
        among observations.

        :return: the DataFrame DF_DATA enriched with 3 columns: RAW PREDICTABILITY, ACCURACY and PREDICTABILITY
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        # JSG as of 11-Mar-26, from
        ## In case data has not been set yet as of a label...
        ## NOT NEEDED FOR SLIDE...
        ## if self.df_data_as_of_label is None:
        ##     self.as_of_label(int_as_of_label=self.int_last_label)
        #
        ## In case the context does not have an as_of_label
        # if self._int_as_of_label is None:
        #    _df_the_data: DataFrame = self.df_data.copy()
        # else:
        #    _df_the_data: DataFrame = self.df_data[
        #        self.df_data.index.get_level_values(self.str_column_label_for_labels) <= np.int64(self._int_as_of_label)
        #    ].copy()
        #
        ## In case a key is provided
        # if str_key:
        #    # An exception is thrown in case it does not exist in DF_DATA (and thus in DF_DATA_AS_OF_LABEL)
        #    if str_key not in self.df_data.index.get_level_values(self.str_column_label_for_keys):
        #        raise KeyError("Key '" + str_key + "' does not exist.")
        #
        #    _df_the_data = _df_the_data.loc[[str_key]]

        # JSG as of 11-Mar-26, to

        # In case a key is provided
        if str_key:
            # An exception is thrown in case it does not exist in DF_DATA (and thus in DF_DATA_AS_OF_LABEL)
            if str_key not in self.df_data.index.get_level_values(self.str_column_label_for_keys):
                raise KeyError("Key '" + str_key + "' does not exist.")

            _df_the_data: DataFrame = self.df_data.loc[[str_key]].copy()

        else:
            _df_the_data: DataFrame = self.df_data.copy()

        # JSG as of 11-Mar-26, end

        # We will call _as_of_label and _compute as many times as there are different labels. Results are
        # aggregated into a list, and eventually merged into one DataFrame
        _lst_df_the_return: list[DataFrame] = []

        # For each and every LABEL, up to the last label
        for _int_i_label in [
            i
            for i in np.sort(_df_the_data.index.unique(level=self.str_column_label_for_labels))
            if i <= self.int_last_label
        ]:
            # We compute the df data as of that label
            _df_i_the_return: DataFrame = self._as_of_label(df_data=_df_the_data, int_as_of_label=_int_i_label)

            # We keep the keys with observations at the current label
            _list_the_keys_to_keep = _df_i_the_return[
                _df_i_the_return.index.get_level_values(self.str_column_label_for_labels) == _int_i_label
            ].index.unique(level=self.str_column_label_for_keys)
            _df_i_the_return = _df_i_the_return[
                _df_i_the_return.index.get_level_values(self.str_column_label_for_keys).isin(_list_the_keys_to_keep)
            ]

            # And we compute the predictability components
            _df_i_the_return = self._compute(df_data=_df_i_the_return)

            _df_i_the_return = _df_i_the_return.reset_index(level=self.str_column_label_for_labels, drop=False)
            _df_i_the_return[self.str_column_label_for_labels] = _int_i_label
            _df_i_the_return = _df_i_the_return.set_index(keys=self.str_column_label_for_labels, append=True)

            # We append the result
            _lst_df_the_return.append(_df_i_the_return)

        # We merge all dataframes
        _df_the_return: DataFrame = pd.concat(objs=_lst_df_the_return).sort_index()

        # We merge the predictability data within the original dataframe, so we have both the observation and the
        # computed predictability for a given label
        _df_the_return = _df_the_data.merge(right=_df_the_return, left_index=True, right_index=True)

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)
        return _df_the_return

    def describe(self) -> dict[str, DescribeStatisticsForSpotMeasures | DescribeStatisticsForRangeMeasures]:
        """
        The function describes the DF_DATA as of INT_AS_OF_LABEL that is under analysis, and returns a dictionary
        containing for each and every KEY an instance of DescribeStatisticsForSpotMeasures or
        DescribeStatisticsForRangeMeasures (depending on whether the data is SPOT or RANGE).

        TODO: as is the function return an empty dictionary. To be tested and finalized.

        :return: A dictionary KEY: DescribeStatisticsForSpotMeasures|DescribeStatisticsForRangeMeasures
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        # We get the data as of label to describe
        # We check _df_data_as_of_label (prefixed with a "_"), and not df_data_as_of_label (not prefixed with a "_")
        # to avoid computing unnecessary predictability components.
        if self._df_data_as_of_label is None:
            _df_the_data_to_describe: DataFrame = self.df_data
        else:
            _df_the_data_to_describe: DataFrame = self.df_data_as_of_label

        # We unstack the LABELs from the index
        _df_the_data_to_describe = _df_the_data_to_describe.reset_index(level=self.str_column_label_for_labels)

        # We compute statistics
        _df_the_stats: DataFrame = _df_the_data_to_describe.groupby(self.str_column_label_for_keys).agg(
            {
                self.str_column_label_for_labels: ["count", "min", "max"],
            }
            | {i_col: ["min", "max", "mean"] for i_col in self.lst_columns_labels_for_measures}
            | {self.str_column_label_for_status: ["count", "first", "last"]}
        )

        # We flatten the multilevel columns
        _df_the_stats.columns = _df_the_stats.columns.to_flat_index().str.join("_")

        _dict_the_return: dict[str, DescribeStatisticsForSpotMeasures | DescribeStatisticsForRangeMeasures] = {}

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return _dict_the_return

    def _copy_flatten_check_type_reduce_order_and_sort_data(self, df_data: DataFrame) -> DataFrame:
        """
        The data is:
        - Copied: to prevent side effects on the callers
        - Flattened: multi-indexes (rows and columns) are flattened
        - Checked: data is checked against business rules (listed below)
        - Typed: data is typed:
          - KEY as string
          - LABEL as np.int64
          - MEASURE/ MEASURE_FROM/ MEASURE_TO as np.int64
          - STATUS as string
        - Reduced:
          - KEY and LABEL are set as index
          - Useless columns are discarded to only keep MEASURE/ MEASURE_FROM/ MEASURE_TO and STATUS
        - Ordered: columns are ordered
        - Sorted: data is sorted as per the index.

        Notes:
        - Column KEY can contain null values or empty strings
        - Index is reset
        - Multi indices (if any) are flattened, both for rows and columns

        :param df_data: The raw historical measures to check and cleanse
        :return: the DataFrame instance with checked, cleansed and adjusted data.
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        _dict_errors: dict[str:str] = {}

        _df_the_return: DataFrame = df_data.copy()

        # ##############################################################################################################
        # Step 1: we flatten the indices
        _df_the_return = _df_the_return.reset_index()
        _df_the_return.columns = [
            _i_element if isinstance(_i_element, str) else "_".join(_i_element)
            for _i_element in _df_the_return.columns.to_flat_index()
        ]

        # ##############################################################################################################
        # BR_101: Columns labels should not start with an "_" ("_" is reserved for temporary columns...)
        _lst_columns_labels: list[str] = [i_col for i_col in _df_the_return.columns if i_col.startswith("_")]
        if len(_lst_columns_labels) > 0:
            _dict_errors["BR_101"] = (
                "Business Rule breached due to columns [" + ", ".join(_lst_columns_labels) + "]: " + self.__ERROR_101__
            )

        # ##############################################################################################################
        # BR_102: Key column should exist
        if self.str_column_label_for_keys not in _df_the_return.columns:
            _dict_errors["BR_102"] = (
                "Business Rule breached due to column '" + self.str_column_label_for_keys + "': " + self.__ERROR_102__
            )

        # ##############################################################################################################
        # BR_103: Label column should exist
        if self.str_column_label_for_labels not in _df_the_return.columns:
            _dict_errors["BR_103"] = (
                "Business Rule breached due to column '" + self.str_column_label_for_labels + "': " + self.__ERROR_103__
            )

        # ##############################################################################################################
        # BR_104: Measure column (or columns from and to) should exist
        _lst_columns_labels = [
            i_col for i_col in self.lst_columns_labels_for_measures if i_col not in _df_the_return.columns
        ]
        if len(_lst_columns_labels) > 0:
            _dict_errors["BR_104"] = (
                "Business Rule breached due to columns [" + ", ".join(_lst_columns_labels) + "]: " + self.__ERROR_104__
            )

        # ##############################################################################################################
        # BR_105: Status column should exist
        if self.str_column_label_for_status not in _df_the_return.columns:
            _dict_errors["BR_105"] = (
                "Business Rule breached due to column '" + self.str_column_label_for_status + "': " + self.__ERROR_105__
            )

        # ##############################################################################################################
        # BR_106: Label column should not have any null value
        if "BR_103" not in _dict_errors:  # Label column should exist
            _int_nb_rows: int = len(_df_the_return[_df_the_return[self.str_column_label_for_labels].isnull()].index)
            if _int_nb_rows > 0:
                _dict_errors["BR_106"] = (
                    "Business Rule breached due to '" + str(_int_nb_rows) + "' rows: " + self.__ERROR_106__
                )

        # ##############################################################################################################
        # BR_107: Label column should be typed as integers (or cast-able to integers)
        if all(
            i_error not in _dict_errors
            for i_error in [
                "BR_103",  # Label column should exist
                "BR_106",  # Label column should not have any null value
            ]
        ):
            try:
                _df_the_return = DfTyper(
                    lst_map=[{DfTyper.NODE_LBL: self.str_column_label_for_labels, DfTyper.NODE_TYP: DfTyper.TYPE_INT}]
                ).df_type(df_instance=_df_the_return)
            except TypeError as _an_exception:
                _dict_errors["BR_107"] = "Business Rule breached: " + self.__ERROR_107__, _an_exception

        # ##############################################################################################################
        # BR_108: Measure column (or columns from and to) should be typed as integers (or cast-able to integers)
        if "BR_104" not in _dict_errors:  # Measure column (or columns from and to) should exist
            try:
                _df_the_return = DfTyper(
                    lst_map=[
                        {DfTyper.NODE_LBL: i_col, DfTyper.NODE_TYP: DfTyper.TYPE_INT}
                        for i_col in self.lst_columns_labels_for_measures
                    ]
                ).df_type(df_instance=_df_the_return)
            except TypeError as _an_exception:
                _dict_errors["BR_108"] = "Business Rule breached: " + self.__ERROR_108__, _an_exception

        # ##############################################################################################################
        # BR_109: In case measure is a range (aka a tuple "from" - "to"), "from" should be less or equal to "to"
        if (
            all(
                i_error not in _dict_errors
                for i_error in [
                    "BR_104",  # Measure column (or columns from and to) should exist
                    "BR_108",  # Measure column (or columns from and to) should be typed as integers (or cast-able to integers)
                ]
            )
            and not self.is_measure_spot()
        ):
            _int_nb_rows = len(
                _df_the_return[
                    _df_the_return[self.str_column_label_for_measures_from]
                    > _df_the_return[self.str_column_label_for_measures_to]
                ].index
            )
            if _int_nb_rows > 0:
                _dict_errors["BR_109"] = (
                    "Business Rule breached due to '" + str(_int_nb_rows) + "' rows: " + self.__ERROR_109__
                )

        # ##############################################################################################################
        # BR_110: Status column should only contain valid status: OPEN, CLOSED, CANCELLED
        if "BR_105" not in _dict_errors:  # Status column should exist
            _int_nb_rows = len(
                _df_the_return[
                    ~_df_the_return[self.str_column_label_for_status].isin(
                        [
                            self.STATUS_OPEN,
                            self.STATUS_CLOSED,
                            self.STATUS_CANCELLED,
                        ]
                    )
                ].index
            )
            if _int_nb_rows > 0:
                _dict_errors["BR_110"] = (
                    "Business Rule breached due to '" + str(_int_nb_rows) + "' rows: " + self.__ERROR_110__
                )

        # ##############################################################################################################
        # BR_111:
        # Combination across key and label columns should be unique (aka one single measure (or from-to) for one given
        # key and a given label)
        if all(
            i_error not in _dict_errors
            for i_error in [
                "BR_102",  #  Key column should exist
                "BR_103",  # Label column should exist
            ]
        ):
            _int_nb_rows = (
                _df_the_return[[self.str_column_label_for_keys, self.str_column_label_for_labels]].duplicated().sum()
            )
            if _int_nb_rows > 0:
                _dict_errors["BR_111"] = (
                    "Business Rule breached due to '" + str(_int_nb_rows) + "' rows: " + self.__ERROR_111__
                )

        # ##############################################################################################################
        # BR_112:
        # When status is OPEN, measure (aka eta) (or min across from and to) should be greater to label (aka date)
        if all(
            i_error not in _dict_errors
            for i_error in [
                "BR_105",  # Status column should exist
                "BR_110",  # Status column should only contain valid status: OPEN, CLOSED, CANCELLED
                "BR_103",  # Label column should exist
                "BR_106",  # Label column should not have any null value
                "BR_107",  # Label column should be typed as integers (or cast-able to integers)
                "BR_104",  # Measure column (or columns from and to) should exist
                "BR_108",  # Measure column (or columns from and to) should be typed as integers (or cast-able to integers)
            ]
        ):
            if self.is_measure_spot():
                s_min_measure: Series = _df_the_return[self.str_column_label_for_measures]
            else:
                s_min_measure: Series = _df_the_return[self.lst_columns_labels_for_measures].min(axis=1)

            _df_rows_in_error: DataFrame = _df_the_return[
                (_df_the_return[self.str_column_label_for_status] == self.STATUS_OPEN)
                & (_df_the_return[self.str_column_label_for_labels] >= s_min_measure)
            ]

            _int_nb_rows = len(_df_rows_in_error.index)

            if _int_nb_rows > 0:
                _str_the_error_details: str = (
                    _df_rows_in_error[[self.str_column_label_for_keys, self.str_column_label_for_labels]]
                    .groupby([self.str_column_label_for_keys])
                    .agg({self.str_column_label_for_labels: lambda x: ", ".join(x.astype(str))})
                    .to_json(orient="index")
                )

                _dict_errors["BR_112"] = (
                    "Business Rule breached due to '"
                    + str(_int_nb_rows)
                    + "' rows: "
                    + self.__ERROR_112__
                    + "\n{"
                    + self.str_column_label_for_keys
                    + ": "
                    + self.str_column_label_for_labels
                    + "}\n"
                    + _str_the_error_details
                )

        # ##############################################################################################################
        # BR_113: When status is CLOSED, in case measure is a range, both from and to should be equal
        if (
            all(
                i_error not in _dict_errors
                for i_error in [
                    "BR_105",  # Status column should exist
                    "BR_110",  # Status column should only contain valid status: OPEN, CLOSED, CANCELLED
                    "BR_104",  # Measure column (or columns from and to) should exist
                    "BR_108",  # Measure column (or columns from and to) should be typed as integers (or cast-able to integers)
                ]
            )
            and not self.is_measure_spot()
        ):
            _int_nb_rows = len(
                _df_the_return[
                    (_df_the_return[self.str_column_label_for_status] == self.STATUS_CLOSED)
                    & (
                        _df_the_return[self.str_column_label_for_measures_from]
                        != _df_the_return[self.str_column_label_for_measures_to]
                    )
                ].index
            )

            if _int_nb_rows > 0:
                _dict_errors["BR_113"] = (
                    "Business Rule breached due to '" + str(_int_nb_rows) + "' rows: " + self.__ERROR_113__
                )

        # ##############################################################################################################
        # BR_114: When status is CLOSED, measure (aka eta) (or from and to) should be lower or equal to label (aka date)
        if all(
            i_error not in _dict_errors
            for i_error in [
                "BR_105",  # Status column should exist
                "BR_110",  # Status column should only contain valid status: OPEN, CLOSED, CANCELLED
                "BR_103",  # Label column should exist
                "BR_106",  # Label column should not have any null value
                "BR_107",  # Label column should be typed as integers (or cast-able to integers)
                "BR_104",  # Measure column (or columns from and to) should exist
                "BR_108",  # Measure column (or columns from and to) should be typed as integers (or cast-able to integers)
                "BR_113",  # When status is CLOSED, in case measure is a range, both from and to should be equal
            ]
        ):
            if self.is_measure_spot():
                str_measure_column: str = self.str_column_label_for_measures
            else:
                str_measure_column: str = self.str_column_label_for_measures_from

            _int_nb_rows = len(
                _df_the_return[
                    (_df_the_return[self.str_column_label_for_status] == self.STATUS_CLOSED)
                    & (_df_the_return[self.str_column_label_for_labels] < _df_the_return[str_measure_column])
                ].index
            )

            if _int_nb_rows > 0:
                _dict_errors["BR_114"] = (
                    "Business Rule breached due to '" + str(_int_nb_rows) + "' rows: " + self.__ERROR_114__
                )

        # ##############################################################################################################
        # Eventually, we raise an exception in case we found errors...
        if len(_dict_errors) == 1:
            raise ValueError(next(i_val for i_key, i_val in _dict_errors.items()))
        if len(_dict_errors) > 1:
            raise ExceptionGroup(
                "'" + str(len(_dict_errors)) + "' business rules breached: " + self.__ERROR_100__,
                [ValueError(i_val) for i_key, i_val in _dict_errors.items()],
            )

        _df_the_return = _df_the_return.set_index(
            [self.str_column_label_for_keys, self.str_column_label_for_labels]
        ).sort_index()
        if self.is_measure_spot():
            _df_the_return = _df_the_return[[self.str_column_label_for_measures, self.str_column_label_for_status]]
        else:
            _df_the_return = _df_the_return[
                [
                    self.str_column_label_for_measures_from,
                    self.str_column_label_for_measures_to,
                    self.str_column_label_for_status,
                ]
            ]

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return _df_the_return

    def _version_status_changes_by_key(self, df_data: DataFrame) -> DataFrame:
        """
        Compute versions by KEY:
        - _versioned_status: each time the status evolves, _versioned_status is incremented
        - _versioned_status_version: observations are versioned for a given status version.

        Illustration:
        ===  =========  =================  =========================
        KEY  STATUS     _versioned_status  _versioned_status_version
        ===  =========  =================  =========================
        a    OPEN       0                  0
        a    OPEN       0                  1
        a    OPEN       0                  2
        bb   OPEN       0                  0
        bb   CANCELLED  1                  0
        bb   CLOSED     2                  0
        ccc  OPEN       0                  0
        ccc  OPEN       0                  1
        ccc  CANCELLED  1                  0
        ccc  OPEN       2                  0
        ===  =========  =================  =========================

        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        if len(df_data.index) == 0:
            return self._return_empty_dataframe(DpDatePredictability.EnumFunction.VERSION_STATUS_CHANGES_CHANGES_BY_KEY)

        # We version when the status changes
        df_data["_versioned_status"] = df_data.groupby(level=0, group_keys=False)[
            self.str_column_label_for_status
        ].apply(lambda gr: ((gr.ne(gr.shift(1))).cumsum() - 1).astype(np.int64))

        # We version observation under the same status version
        df_data["_versioned_status_version"] = (
            df_data.set_index("_versioned_status", append=True)
            .groupby(level=[0, 2], group_keys=False)[self.str_column_label_for_status]
            .apply(lambda gr: (gr.expanding(1).count() - 1).astype(np.int64))
            .droplevel(level=[2])
        )

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)
        return df_data

    def _append_last_observation_by_key(self, df_data: DataFrame) -> DataFrame:
        """
        When INT_AS_OF_LABEL is set, the last observations as of that LABEL for each key are repeated and appended to
        rows.

        Compute additional columns:
        - _last_label
        - _last_measure or _last_measure_from/ _last_measure_to
        - _last_status
        - _last_versioned_status
        - _last_versioned_status_version
        - _last_measure_floor
        - _last_measure_ceil
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        # In case df_data is empty, we return an empty dataframe with all the expected columns, properly typed
        if len(df_data.index) == 0:
            return self._return_empty_dataframe(DpDatePredictability.EnumFunction.APPEND_LAST_OBSERVATION_BY_KEY)

        # We keep for each key the very last observation
        _df_last_row_per_key: DataFrame = (
            df_data.reset_index(level=1)
            .groupby(level=0, group_keys=False)
            .last()
            .set_index(self.str_column_label_for_labels, append=True, drop=False)
        )

        # we rename the columns, as they will be appended to the source data
        if self.is_measure_spot():
            _df_last_row_per_key = _df_last_row_per_key.rename(
                {
                    self.str_column_label_for_labels: "_last_label",
                    self.str_column_label_for_measures: "_last_measure",
                    self.str_column_label_for_status: "_last_status",
                    "_versioned_status": "_last_versioned_status",
                    "_versioned_status_version": "_last_versioned_status_version",
                },
                axis="columns",
            )
        else:
            _df_last_row_per_key = _df_last_row_per_key.rename(
                {
                    self.str_column_label_for_labels: "_last_label",
                    self.str_column_label_for_measures_from: "_last_measure_from",
                    self.str_column_label_for_measures_to: "_last_measure_to",
                    self.str_column_label_for_status: "_last_status",
                    "_versioned_status": "_last_versioned_status",
                    "_versioned_status_version": "_last_versioned_status_version",
                },
                axis="columns",
            )

        # We remove the first observation appended, not to append it again...
        _df_last_row_per_key = _df_last_row_per_key[
            [
                _i_str_column
                for _i_str_column in _df_last_row_per_key.columns
                if _i_str_column
                not in ["_first_label", "_first_measure", "_first_measure_from", "_first_measure_to", "_first_status"]
            ]
        ]

        # We duplicate the lines to have a dataframe containing similar index to source data
        _df_last_row_per_key = _df_last_row_per_key.reindex(df_data.index, method="bfill")

        # We can then merge the dataframes
        _df_the_return: DataFrame = pd.concat([df_data, _df_last_row_per_key], axis="columns")

        # Eventually, we compute:
        # - _last_measure_floor
        # - _last_measure_ceil
        if self.is_measure_spot():
            _df_the_return["_last_measure_floor"] = _df_the_return["_last_measure"]
            _df_the_return["_last_measure_ceil"] = _df_the_return["_last_measure"]
        else:
            _df_the_return["_last_measure_floor"] = (
                (_df_the_return["_last_measure_from"] + _df_the_return["_last_measure_to"]) / 2
            ).apply(np.floor)
            _df_the_return["_last_measure_ceil"] = (
                (_df_the_return["_last_measure_from"] + _df_the_return["_last_measure_to"]) / 2
            ).apply(np.ceil)

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)
        return _df_the_return

    def _prepend_first_observation_by_key(self, df_data: DataFrame) -> DataFrame:
        """
        When DF_DATA is set, the first "correct" observations for each key are repeated and prepended to
        rows.
        Called only once in set_data.

        Compute additional columns:
        - _first_label
        - _first_measure or _first_measure_from/ _first_measure_to
        - _first_status
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        # In case df_data is empty, we return an empty dataframe with all the expected columns, properly typed
        if len(df_data.index) == 0:
            return self._return_empty_dataframe(DpDatePredictability.EnumFunction.PREPEND_FIRST_OBSERVATION_BY_KEY)

        # We keep for each key the very last observation
        _df_first_row_per_key: DataFrame = (
            df_data.reset_index(level=1)
            .groupby(level=0, group_keys=False)
            .first()
            .set_index(self.str_column_label_for_labels, append=True, drop=False)
        )

        # we rename the columns, as they will be appended to the source data
        if self.is_measure_spot():
            _df_first_row_per_key = _df_first_row_per_key.rename(
                {
                    self.str_column_label_for_labels: "_first_label",
                    self.str_column_label_for_measures: "_first_measure",
                    self.str_column_label_for_status: "_first_status",
                },
                axis="columns",
            )
        else:
            _df_first_row_per_key = _df_first_row_per_key.rename(
                {
                    self.str_column_label_for_labels: "_first_label",
                    self.str_column_label_for_measures_from: "_first_measure_from",
                    self.str_column_label_for_measures_to: "_first_measure_to",
                    self.str_column_label_for_status: "_first_status",
                },
                axis="columns",
            )

        # We remove the versions (by definition 0)
        _df_first_row_per_key = _df_first_row_per_key[
            [
                _i_str_column
                for _i_str_column in _df_first_row_per_key.columns
                if _i_str_column not in ["_versioned_status", "_versioned_status_version"]
            ]
        ]

        # We duplicate the lines to have a dataframe containing similar index to source data
        _df_first_row_per_key = _df_first_row_per_key.reindex(df_data.index, method="ffill")

        # We can then merge the dataframes
        _df_the_return: DataFrame = pd.concat([df_data, _df_first_row_per_key], axis="columns")

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)
        return _df_the_return

    def _compute_incremental_raw_predictability(self, df_data: DataFrame) -> DataFrame:
        """
        Based on data:
        - versioned
        - With first observation
        - Selected
        - With last observation as of label.
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        # In case df_data is empty, we return an empty dataframe with all the expected columns, properly typed
        if len(df_data.index) == 0:
            return self._return_empty_dataframe(
                DpDatePredictability.EnumFunction.COMPUTE_INCREMENTAL_RAW_PREDICTABILITY
            )

        _df_the_return: DataFrame = df_data

        _df_the_return[self.COL_LBL_INCREMENTAL_RAW_PREDICTABILITY] = where(
            # ##########################################################################################################
            # A. The observation is EVENTUALLY CANCELLED
            _df_the_return["_last_status"] == self.STATUS_CANCELLED,
            where(
                # The observation eventually cancelled is ACTUALLY CANCELLED
                _df_the_return[self.str_column_label_for_status] == self.STATUS_CANCELLED,
                # The observation is the FINAL CANCELLED OBSERVATION
                where(
                    _df_the_return.index.get_level_values(self.str_column_label_for_labels)
                    == _df_the_return["_last_label"],
                    # FINAL CANCELLED does not account
                    np.float64(np.nan),
                    # INTERMEDIARY CANCELLED accounts for 1
                    np.float64(1),
                ),
                # Else, the current observation is NOT CANCELLED
                # We multiply cone * anticipation (see illustration in test)
                # cone = MIN(1;(eta-label)/(last_label-label)/2)
                (
                    (
                        _df_the_return[self.str_column_label_for_measures_from]
                        - _df_the_return.index.get_level_values(self.str_column_label_for_labels)
                    )
                    / (
                        _df_the_return["_last_label"]
                        - _df_the_return.index.get_level_values(self.str_column_label_for_labels)
                    )
                    / 2
                ).clip(0, 1)
                # anticipation = 1-(label-first_label)/(eta-first_label)
                * (
                    1
                    - (
                        _df_the_return.index.get_level_values(self.str_column_label_for_labels)
                        - _df_the_return["_first_label"]
                    )
                    / (_df_the_return[self.str_column_label_for_measures_from] - _df_the_return["_first_label"])
                ),
            ),
            # ##########################################################################################################
            # B. The observation is EVENTUALLY OPEN or CLOSED
            #    MAX(0;1-ABS((measure-last_measure)/(label-last_measure)))
            where(
                # The observation eventually OPEN or CLOSED is ACTUALLY CANCELLED
                _df_the_return[self.str_column_label_for_status] == self.STATUS_CANCELLED,
                # INTERMEDIARY CANCELLED accounts for 0
                np.float64(0),
                # The observation eventually OPEN or CLOSED is ACTUALLY NOT CANCELLED
                where(
                    # The observation eventually OPEN or CLOSED is actually the FINAL CLOSED OBSERVATION
                    _df_the_return[self.str_column_label_for_status] == self.STATUS_CLOSED,
                    # FINAL CLOSED does not account
                    np.float64(np.nan),
                    # The observation eventually OPEN or CLOSED, is actually OPEN
                    where(
                        _df_the_return[self.str_column_label_for_measures_from]
                        >= (
                            _df_the_return["_last_measure_ceil"]
                            + _df_the_return["_last_measure_ceil"]
                            - _df_the_return.index.get_level_values(self.str_column_label_for_labels)
                        ),
                        # Outside the cone account for 0
                        np.float64(0),
                        # if we are outside the cone - DOWN
                        where(
                            _df_the_return[self.str_column_label_for_measures_to]
                            <= _df_the_return.index.get_level_values(self.str_column_label_for_labels),
                            # Outside the cone account for 0
                            np.float64(0),
                            # if we are spot on
                            where(
                                (  # For range of 1, (from - to) should be within (floor - ceil)
                                    (
                                        _df_the_return[self.str_column_label_for_measures_from]
                                        == _df_the_return[self.str_column_label_for_measures_to]
                                    )
                                    & (
                                        _df_the_return[self.str_column_label_for_measures_from]
                                        <= _df_the_return["_last_measure_ceil"]
                                    )
                                    & (
                                        _df_the_return[self.str_column_label_for_measures_to]
                                        >= _df_the_return["_last_measure_floor"]
                                    )
                                )
                                | (  # For range wider than 1, (from - to) should be wider than (floor - ceil)
                                    (
                                        _df_the_return[self.str_column_label_for_measures_from]
                                        <= _df_the_return["_last_measure_floor"]
                                    )
                                    & (
                                        _df_the_return[self.str_column_label_for_measures_to]
                                        >= _df_the_return["_last_measure_ceil"]
                                    )
                                ),
                                # Spot on means predictability of 1
                                np.float64(1),
                                # if we are in the cone - UP
                                where(
                                    _df_the_return[self.str_column_label_for_measures_from]
                                    >= _df_the_return["_last_measure_ceil"],
                                    # If landing on 1: 1 - ( measure_from - last_measure_floor ) / ( last_measure_floor - label )
                                    # Elif landing on 2: 1 - ( measure_from - last_measure_floor ) / ( last_measure_floor - label + 2 )
                                    1
                                    - (
                                        (
                                            _df_the_return[self.str_column_label_for_measures_from]
                                            - _df_the_return["_last_measure_floor"]
                                        )
                                        / (
                                            _df_the_return["_last_measure_floor"]
                                            - _df_the_return.index.get_level_values(self.str_column_label_for_labels)
                                            + (
                                                _df_the_return["_last_measure_ceil"]
                                                - _df_the_return["_last_measure_floor"]
                                            )
                                            * 2
                                        )
                                    ),
                                    # if we are in the cone - DOWN
                                    # 1 - ( last_measure_ceil - measure_to ) / ( last_measure_ceil - label )
                                    1
                                    - (
                                        (
                                            _df_the_return["_last_measure_ceil"]
                                            - _df_the_return[self.str_column_label_for_measures_to]
                                        )
                                        / (
                                            _df_the_return["_last_measure_ceil"]
                                            - _df_the_return.index.get_level_values(self.str_column_label_for_labels)
                                        )
                                    ),
                                ),
                            ),
                        ),
                    ),
                ),
            ),
        )

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return _df_the_return

    def _compute_incremental_accuracy(self, df_data: DataFrame) -> DataFrame:
        """
        After extension to LAST MEASURE.

        Formula to compute accuracy is as below:
        - NaN for observations CLOSED or CANCELLED
        - 0 for LABEL beyond LAST_MEASURE_CEIL
        - Otherwise:
          MAX(
            0;
            1 - (
              MEASURE_TO
              - MEASURE_FROM
            ) / (
              LAST_MEASURE_CEIL
              - LABEL
            ) / 2
          )

        Computing incremental accuracy relies on the following columns:
        - label
        - measure_from
        - measure_to
        - status
        - last_measure_ceil

        :param df_data:
        :return:
        """
        self._obj_logger.debug("Function '%s - %s' is called", stack()[0].filename, stack()[0].function)

        # In case df_data is empty, we return an empty dataframe with all the expected columns, properly typed
        if len(df_data.index) == 0:
            return self._return_empty_dataframe(DpDatePredictability.EnumFunction.COMPUTE_INCREMENTAL_ACCURACY)

        _df_the_return: DataFrame = df_data

        _df_the_return[self.COL_LBL_INCREMENTAL_ACCURACY] = where(
            # ##########################################################################################################
            # A. The observation is CANCELLED or CLOSED
            (_df_the_return[self.str_column_label_for_status] == self.STATUS_CANCELLED)
            | (_df_the_return[self.str_column_label_for_status] == self.STATUS_CLOSED),
            # CANCELLED or CLOSED observations does not account
            np.float64(np.nan),
            # B. We handle SPOT MEASURES
            where(
                self.is_measure_spot(),
                # Accuracy is 1
                np.float64(1),
                # C. We handle projections beyond the ceil
                where(
                    _df_the_return.index.get_level_values(self.str_column_label_for_labels)
                    >= _df_the_return["_last_measure_ceil"],
                    np.float64(0),
                    # We compute accuracy: MAX( 0 ; 1 - ( MEASURE_TO - MEASURE_FROM ) / ( LAST_MEASURE_CEIL  - LABEL ) / 2 )
                    (
                        1
                        - (
                            _df_the_return[self.str_column_label_for_measures_to]
                            - _df_the_return[self.str_column_label_for_measures_from]
                        )
                        / (
                            _df_the_return["_last_measure_ceil"]
                            - _df_the_return.index.get_level_values(self.str_column_label_for_labels)
                        )
                        / 2
                    ).clip(0, 1),
                ),
            ),
        )

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return _df_the_return

    class EnumFunction(Enum):
        VERSION_STATUS_CHANGES_CHANGES_BY_KEY = auto()

        SET_DATA = auto()
        PREPEND_FIRST_OBSERVATION_BY_KEY = auto()
        DROP_USELESS_HEADS_AND_TAILS__ON_VERSIONED_GROUP = auto()
        SAMPLE_UP_DATA__ON_VERSIONED_GROUP = auto()
        EXPAND_DATA__ON_VERSIONED_GROUP = auto()

        APPEND_LAST_OBSERVATION_BY_KEY = auto()
        COMPUTE_INCREMENTAL_ACCURACY = auto()
        COMPUTE_INCREMENTAL_RAW_PREDICTABILITY = auto()
        AS_OF_LABEL = auto()
        COMPUTE = auto()

    def _return_empty_dataframe(self, enum_function: EnumFunction) -> DataFrame:
        _lst_the_columns: list[str] = []

        match enum_function:
            case DpDatePredictability.EnumFunction.VERSION_STATUS_CHANGES_CHANGES_BY_KEY:
                _lst_the_columns = [
                    self.str_column_label_for_keys,
                    self.str_column_label_for_labels,
                    *self.lst_columns_labels_for_measures,
                    self.str_column_label_for_status,
                    "_versioned_status",
                    "_versioned_status_version",
                ]

            case (
                DpDatePredictability.EnumFunction.SET_DATA
                | DpDatePredictability.EnumFunction.PREPEND_FIRST_OBSERVATION_BY_KEY
                | DpDatePredictability.EnumFunction.DROP_USELESS_HEADS_AND_TAILS__ON_VERSIONED_GROUP
                | DpDatePredictability.EnumFunction.SAMPLE_UP_DATA__ON_VERSIONED_GROUP
                | DpDatePredictability.EnumFunction.EXPAND_DATA__ON_VERSIONED_GROUP
            ):
                _lst_the_columns = [
                    self.str_column_label_for_keys,
                    self.str_column_label_for_labels,
                    *self.lst_columns_labels_for_measures,
                    self.str_column_label_for_status,
                    "_versioned_status",
                    "_versioned_status_version",
                    "_first_label",
                ] + [
                    k_col
                    for k_col, v_col in {
                        "_first_measure_from": False,
                        "_first_measure_to": False,
                        "_first_measure": True,
                    }.items()
                    if v_col == self.is_measure_spot()
                ]

            case DpDatePredictability.EnumFunction.APPEND_LAST_OBSERVATION_BY_KEY:
                _lst_the_columns = (
                    [
                        self.str_column_label_for_keys,
                        self.str_column_label_for_labels,
                        *self.lst_columns_labels_for_measures,
                        self.str_column_label_for_status,
                        "_versioned_status",
                        "_versioned_status_version",
                        "_first_label",
                    ]
                    + [
                        k_col
                        for k_col, v_col in {
                            "_first_measure_from": False,
                            "_first_measure_to": False,
                            "_first_measure": True,
                        }.items()
                        if v_col == self.is_measure_spot()
                    ]
                    + ["_last_label"]
                    + [
                        k_col
                        for k_col, v_col in {
                            "_last_measure_from": False,
                            "_last_measure_to": False,
                            "_last_measure": True,
                        }.items()
                        if v_col == self.is_measure_spot()
                    ]
                    + [
                        "_last_status",
                        "_last_versioned_status",
                        "_last_versioned_status_version",
                        "_last_measure_floor",
                        "_last_measure_ceil",
                    ]
                )

            case DpDatePredictability.EnumFunction.COMPUTE_INCREMENTAL_RAW_PREDICTABILITY:
                _lst_the_columns = (
                    [
                        self.str_column_label_for_keys,
                        self.str_column_label_for_labels,
                        *self.lst_columns_labels_for_measures,
                        self.str_column_label_for_status,
                        "_versioned_status",
                        "_versioned_status_version",
                        "_first_label",
                    ]
                    + [
                        k_col
                        for k_col, v_col in {
                            "_first_measure_from": False,
                            "_first_measure_to": False,
                            "_first_measure": True,
                        }.items()
                        if v_col == self.is_measure_spot()
                    ]
                    + ["_last_label"]
                    + [
                        k_col
                        for k_col, v_col in {
                            "_last_measure_from": False,
                            "_last_measure_to": False,
                            "_last_measure": True,
                        }.items()
                        if v_col == self.is_measure_spot()
                    ]
                    + [
                        "_last_status",
                        "_last_versioned_status",
                        "_last_versioned_status_version",
                        "_last_measure_floor",
                        "_last_measure_ceil",
                        self.COL_LBL_INCREMENTAL_RAW_PREDICTABILITY,
                    ]
                )

            case DpDatePredictability.EnumFunction.COMPUTE_INCREMENTAL_ACCURACY:
                _lst_the_columns = (
                    [
                        self.str_column_label_for_keys,
                        self.str_column_label_for_labels,
                        *self.lst_columns_labels_for_measures,
                        self.str_column_label_for_status,
                        "_versioned_status",
                        "_versioned_status_version",
                        "_first_label",
                    ]
                    + [
                        k_col
                        for k_col, v_col in {
                            "_first_measure_from": False,
                            "_first_measure_to": False,
                            "_first_measure": True,
                        }.items()
                        if v_col == self.is_measure_spot()
                    ]
                    + ["_last_label"]
                    + [
                        k_col
                        for k_col, v_col in {
                            "_last_measure_from": False,
                            "_last_measure_to": False,
                            "_last_measure": True,
                        }.items()
                        if v_col == self.is_measure_spot()
                    ]
                    + [
                        "_last_status",
                        "_last_versioned_status",
                        "_last_versioned_status_version",
                        "_last_measure_floor",
                        "_last_measure_ceil",
                        self.COL_LBL_INCREMENTAL_ACCURACY,
                    ]
                )

            case DpDatePredictability.EnumFunction.AS_OF_LABEL | DpDatePredictability.EnumFunction.COMPUTE:
                _lst_the_columns = (
                    [
                        self.str_column_label_for_keys,
                        self.str_column_label_for_labels,
                        *self.lst_columns_labels_for_measures,
                        self.str_column_label_for_status,
                        "_versioned_status",
                        "_versioned_status_version",
                        "_first_label",
                    ]
                    + [
                        k_col
                        for k_col, v_col in {
                            "_first_measure_from": False,
                            "_first_measure_to": False,
                            "_first_measure": True,
                        }.items()
                        if v_col == self.is_measure_spot()
                    ]
                    + ["_last_label"]
                    + [
                        k_col
                        for k_col, v_col in {
                            "_last_measure_from": False,
                            "_last_measure_to": False,
                            "_last_measure": True,
                        }.items()
                        if v_col == self.is_measure_spot()
                    ]
                    + [
                        "_last_status",
                        "_last_versioned_status",
                        "_last_versioned_status_version",
                        "_last_measure_floor",
                        "_last_measure_ceil",
                        self.COL_LBL_INCREMENTAL_RAW_PREDICTABILITY,
                        self.COL_LBL_INCREMENTAL_ACCURACY,
                        self.COL_LBL_INCREMENTAL_PREDICTABILITY,
                    ]
                )
            case _:
                msg = f"Function '{enum_function.value!s}' is not defined..."
                raise TypeError(msg)

        # Eventually, we instantiate the dataframe to return...
        _df_the_return: DataFrame = DataFrame(
            np.empty(
                0,
                dtype=np.dtype(
                    [
                        (i_str_col_label, self.dict_columns_and_types[i_str_col_label])
                        for i_str_col_label in _lst_the_columns
                    ]
                ),
            )
        ).set_index(keys=[self.str_column_label_for_keys, self._str_column_label_for_labels])

        self._obj_logger.debug("Function '%s - %s' is returning", stack()[0].filename, stack()[0].function)

        return _df_the_return
