"""
News for state space models

Author: Chad Fulton
License: BSD-3
"""
from statsmodels.compat.pandas import FUTURE_STACK

import numpy as np
import pandas as pd

from statsmodels.iolib.summary import Summary
from statsmodels.iolib.table import SimpleTable
from statsmodels.iolib.tableformatting import fmt_params


class NewsResults:
    """
    Impacts of data revisions and news on estimates of variables of interest

    Parameters
    ----------
    news_results : SimpleNamespace instance
        Results from `KalmanSmoother.news`.
    model : MLEResults
        The results object associated with the model from which the NewsResults
        was generated.
    updated : MLEResults
        The results object associated with the model containing the updated
        dataset.
    previous : MLEResults
        The results object associated with the model containing the previous
        dataset.
    impacted_variable : str, list, array, or slice, optional
        Observation variable label or slice of labels specifying particular
        impacted variables to display in output. The impacted variable(s)
        describe the variables that were *affected* by the news. If you do not
        know the labels for the variables, check the `endog_names` attribute of
        the model instance.
    tolerance : float, optional
        The numerical threshold for determining zero impact. Default is that
        any impact less than 1e-10 is assumed to be zero.
    row_labels : iterable
        Row labels (often dates) for the impacts of the revisions and news.

    Attributes
    ----------
    total_impacts : pd.DataFrame
        Updates to forecasts of impacted variables from both news and data
        revisions, E[y^i | post] - E[y^i | previous].
    update_impacts : pd.DataFrame
        Updates to forecasts of impacted variables from the news,
        E[y^i | post] - E[y^i | revisions] where y^i are the impacted variables
        of interest.
    revision_impacts : pd.DataFrame
        Updates to forecasts of impacted variables from all data revisions,
        E[y^i | revisions] - E[y^i | previous].
    news : pd.DataFrame
        The unexpected component of the updated data,
        E[y^u | post] - E[y^u | revisions] where y^u are the updated variables.
    weights : pd.DataFrame
        Weights describing the effect of news on variables of interest.
    revisions : pd.DataFrame
        The revisions between the current and previously observed data, for
        revisions for which detailed impacts were computed.
    revisions_all : pd.DataFrame
        The revisions between the current and previously observed data,
        y^r_{revised} - y^r_{previous} where y^r are the revised variables.
    revision_weights : pd.DataFrame
        Weights describing the effect of revisions on variables of interest,
        for revisions for which detailed impacts were computed.
    revision_weights_all : pd.DataFrame
        Weights describing the effect of revisions on variables of interest,
        with a new entry that includes NaNs for the revisions for which
        detailed impacts were not computed.
    update_forecasts : pd.DataFrame
        Forecasts based on the previous dataset of the variables that were
        updated, E[y^u | previous].
    update_realized : pd.DataFrame
        Actual observed data associated with the variables that were
        updated, y^u
    revisions_details_start : int
        Integer index of first period in which detailed revision impacts were
        computed.
    revision_detailed_impacts : pd.DataFrame
        Updates to forecasts of impacted variables from data revisions with
        detailed impacts, E[y^i | revisions] - E[y^i | grouped revisions].
    revision_grouped_impacts : pd.DataFrame
        Updates to forecasts of impacted variables from data revisions that
        were grouped together, E[y^i | grouped revisions] - E[y^i | previous].
    revised_prev : pd.DataFrame
        Previously observed data associated with the variables that were
        revised, for revisions for which detailed impacts were computed.
    revised_prev_all : pd.DataFrame
        Previously observed data associated with the variables that were
        revised, y^r_{previous}
    revised : pd.DataFrame
        Currently observed data associated with the variables that were
        revised, for revisions for which detailed impacts were computed.
    revised_all : pd.DataFrame
        Currently observed data associated with the variables that were
        revised, y^r_{revised}
    prev_impacted_forecasts : pd.DataFrame
        Previous forecast of the variables of interest, E[y^i | previous].
    post_impacted_forecasts : pd.DataFrame
        Forecast of the variables of interest after taking into account both
        revisions and updates, E[y^i | post].
    revisions_iloc : pd.DataFrame
        The integer locations of the data revisions in the dataset.
    revisions_ix : pd.DataFrame
        The label-based locations of the data revisions in the dataset.
    revisions_iloc_detailed : pd.DataFrame
        The integer locations of the data revisions in the dataset for which
        detailed impacts were computed.
    revisions_ix_detailed : pd.DataFrame
        The label-based locations of the data revisions in the dataset for
        which detailed impacts were computed.
    updates_iloc : pd.DataFrame
        The integer locations of the updated data points.
    updates_ix : pd.DataFrame
        The label-based locations of updated data points.
    state_index : array_like
        Index of state variables used to compute impacts.

    References
    ----------
    .. [1] Bańbura, Marta, and Michele Modugno.
           "Maximum likelihood estimation of factor models on datasets with
           arbitrary pattern of missing data."
           Journal of Applied Econometrics 29, no. 1 (2014): 133-160.
    .. [2] Bańbura, Marta, Domenico Giannone, and Lucrezia Reichlin.
           "Nowcasting."
           The Oxford Handbook of Economic Forecasting. July 8, 2011.
    .. [3] Bańbura, Marta, Domenico Giannone, Michele Modugno, and Lucrezia
           Reichlin.
           "Now-casting and the real-time data flow."
           In Handbook of economic forecasting, vol. 2, pp. 195-237.
           Elsevier, 2013.
    """
    def __init__(self, news_results, model, updated, previous,
                 impacted_variable=None, tolerance=1e-10, row_labels=None):
        # Note: `model` will be the same as one of `revised` or `previous`, but
        # we need to save it as self.model so that the `predict_dates`, which
        # were generated by the `_get_prediction_index` call, will be available
        # for use by the base wrapping code.
        self.model = model
        self.updated = updated
        self.previous = previous
        self.news_results = news_results
        self._impacted_variable = impacted_variable
        self._tolerance = tolerance
        self.row_labels = row_labels
        self.params = []  # required for `summary` to work

        self.endog_names = self.updated.model.endog_names
        self.k_endog = len(self.endog_names)

        self.n_revisions = len(self.news_results.revisions_ix)
        self.n_revisions_detailed = len(self.news_results.revisions_details)
        self.n_revisions_grouped = len(self.news_results.revisions_grouped)

        index = self.updated.model._index
        columns = np.atleast_1d(self.endog_names)

        # E[y^i | post]
        self.post_impacted_forecasts = pd.DataFrame(
            news_results.post_impacted_forecasts.T,
            index=self.row_labels, columns=columns).rename_axis(
                index="impact date", columns="impacted variable")
        # E[y^i | previous]
        self.prev_impacted_forecasts = pd.DataFrame(
            news_results.prev_impacted_forecasts.T,
            index=self.row_labels, columns=columns).rename_axis(
                index="impact date", columns="impacted variable")
        # E[y^i | post] - E[y^i | revisions]
        self.update_impacts = pd.DataFrame(
            news_results.update_impacts,
            index=self.row_labels, columns=columns).rename_axis(
                index="impact date", columns="impacted variable")
        # E[y^i | revisions] - E[y^i | grouped revisions]
        self.revision_detailed_impacts = pd.DataFrame(
            news_results.revision_detailed_impacts,
            index=self.row_labels,
            columns=columns,
            dtype=float,
        ).rename_axis(index="impact date", columns="impacted variable")
        # E[y^i | revisions] - E[y^i | previous]
        self.revision_impacts = pd.DataFrame(
            news_results.revision_impacts,
            index=self.row_labels,
            columns=columns,
            dtype=float,
        ).rename_axis(index="impact date", columns="impacted variable")
        # E[y^i | grouped revisions] - E[y^i | previous]
        self.revision_grouped_impacts = (
            self.revision_impacts
            - self.revision_detailed_impacts.fillna(0))
        if self.n_revisions_grouped == 0:
            self.revision_grouped_impacts.loc[:] = 0

        # E[y^i | post] - E[y^i | previous]
        self.total_impacts = (self.post_impacted_forecasts -
                              self.prev_impacted_forecasts)

        # Indices of revisions and updates
        self.revisions_details_start = news_results.revisions_details_start

        self.revisions_iloc = pd.DataFrame(
            list(zip(*news_results.revisions_ix)),
            index=["revision date", "revised variable"]).T
        iloc = self.revisions_iloc
        if len(iloc) > 0:
            self.revisions_ix = pd.DataFrame({
                "revision date": index[iloc["revision date"]],
                "revised variable": columns[iloc["revised variable"]]})
        else:
            self.revisions_ix = iloc.copy()

        mask = iloc["revision date"] >= self.revisions_details_start
        self.revisions_iloc_detailed = self.revisions_iloc[mask]
        self.revisions_ix_detailed = self.revisions_ix[mask]

        self.updates_iloc = pd.DataFrame(
            list(zip(*news_results.updates_ix)),
            index=["update date", "updated variable"]).T
        iloc = self.updates_iloc
        if len(iloc) > 0:
            self.updates_ix = pd.DataFrame({
                "update date": index[iloc["update date"]],
                "updated variable": columns[iloc["updated variable"]]})
        else:
            self.updates_ix = iloc.copy()

        # Index of the state variables used
        self.state_index = news_results.state_index

        # Wrap forecasts and forecasts errors
        r_ix_all = pd.MultiIndex.from_arrays([
            self.revisions_ix["revision date"],
            self.revisions_ix["revised variable"]])
        r_ix = pd.MultiIndex.from_arrays([
            self.revisions_ix_detailed["revision date"],
            self.revisions_ix_detailed["revised variable"]])
        u_ix = pd.MultiIndex.from_arrays([
            self.updates_ix["update date"],
            self.updates_ix["updated variable"]])

        # E[y^u | post] - E[y^u | revisions]
        if news_results.news is None:
            self.news = pd.Series([], index=u_ix, name="news",
                                  dtype=model.params.dtype)
        else:
            self.news = pd.Series(news_results.news, index=u_ix, name="news")
        # Revisions to data (y^r_{revised} - y^r_{previous})
        if news_results.revisions_all is None:
            self.revisions_all = pd.Series([], index=r_ix_all, name="revision",
                                           dtype=model.params.dtype)
        else:
            self.revisions_all = pd.Series(news_results.revisions_all,
                                           index=r_ix_all, name="revision")
        # Revisions to data (y^r_{revised} - y^r_{previous}) for which detailed
        # impacts were computed
        if news_results.revisions is None:
            self.revisions = pd.Series([], index=r_ix, name="revision",
                                       dtype=model.params.dtype)
        else:
            self.revisions = pd.Series(news_results.revisions,
                                       index=r_ix, name="revision")
        # E[y^u | revised]
        if news_results.update_forecasts is None:
            self.update_forecasts = pd.Series([], index=u_ix,
                                              dtype=model.params.dtype)
        else:
            self.update_forecasts = pd.Series(
                news_results.update_forecasts, index=u_ix)
        # y^r_{revised}
        if news_results.revised_all is None:
            self.revised_all = pd.Series([], index=r_ix_all,
                                         dtype=model.params.dtype,
                                         name="revised")
        else:
            self.revised_all = pd.Series(news_results.revised_all,
                                         index=r_ix_all, name="revised")
        # y^r_{revised} for which detailed impacts were computed
        if news_results.revised is None:
            self.revised = pd.Series([], index=r_ix, dtype=model.params.dtype,
                                     name="revised")
        else:
            self.revised = pd.Series(news_results.revised, index=r_ix,
                                     name="revised")
        # y^r_{previous}
        if news_results.revised_prev_all is None:
            self.revised_prev_all = pd.Series([], index=r_ix_all,
                                              dtype=model.params.dtype)
        else:
            self.revised_prev_all = pd.Series(
                news_results.revised_prev_all, index=r_ix_all)
        # y^r_{previous} for which detailed impacts were computed
        if news_results.revised_prev is None:
            self.revised_prev = pd.Series([], index=r_ix,
                                          dtype=model.params.dtype)
        else:
            self.revised_prev = pd.Series(
                news_results.revised_prev, index=r_ix)
        # y^u
        if news_results.update_realized is None:
            self.update_realized = pd.Series([], index=u_ix,
                                             dtype=model.params.dtype)
        else:
            self.update_realized = pd.Series(
                news_results.update_realized, index=u_ix)
        cols = pd.MultiIndex.from_product([self.row_labels, columns])
        # reshaped version of gain matrix E[y A'] E[A A']^{-1}
        if len(self.updates_iloc):
            weights = news_results.gain.reshape(
                len(cols), len(u_ix))
        else:
            weights = np.zeros((len(cols), len(u_ix)))
        self.weights = pd.DataFrame(weights, index=cols, columns=u_ix).T
        self.weights.columns.names = ["impact date", "impacted variable"]

        # reshaped version of revision_weights
        if self.n_revisions_detailed > 0:
            revision_weights = news_results.revision_weights.reshape(
                len(cols), len(r_ix))
        else:
            revision_weights = np.zeros((len(cols), len(r_ix)))
        self.revision_weights = pd.DataFrame(
            revision_weights, index=cols, columns=r_ix).T
        self.revision_weights.columns.names = [
            "impact date", "impacted variable"]

        self.revision_weights_all = self.revision_weights.reindex(
            self.revised_all.index)

    @property
    def impacted_variable(self):
        return self._impacted_variable

    @impacted_variable.setter
    def impacted_variable(self, value):
        self._impacted_variable = value

    @property
    def tolerance(self):
        return self._tolerance

    @tolerance.setter
    def tolerance(self, value):
        self._tolerance = value

    @property
    def data_revisions(self):
        """
        Revisions to data points that existed in the previous dataset

        Returns
        -------
        data_revisions : pd.DataFrame
            Index is as MultiIndex consisting of `revision date` and
            `revised variable`. The columns are:

            - `observed (prev)`: the value of the data as it was observed
              in the previous dataset.
            - `revised`: the revised value of the data, as it is observed
              in the new dataset
            - `detailed impacts computed`: whether or not detailed impacts have
              been computed in these NewsResults for this revision

        See also
        --------
        data_updates
        """
        # Save revisions data
        data = pd.concat([
            self.revised_all.rename("revised"),
            self.revised_prev_all.rename("observed (prev)")
        ], axis=1).sort_index()
        data["detailed impacts computed"] = (
            self.revised_all.index.isin(self.revised.index))
        return data

    @property
    def data_updates(self):
        """
        Updated data; new entries that did not exist in the previous dataset

        Returns
        -------
        data_updates : pd.DataFrame
            Index is as MultiIndex consisting of `update date` and
            `updated variable`. The columns are:

            - `forecast (prev)`: the previous forecast of the new entry,
              based on the information available in the previous dataset
              (recall that for these updated data points, the previous dataset
              had no observed value for them at all)
            - `observed`: the value of the new entry, as it is observed in the
              new dataset

        See also
        --------
        data_revisions
        """
        data = pd.concat([
            self.update_realized.rename("observed"),
            self.update_forecasts.rename("forecast (prev)")
        ], axis=1).sort_index()
        return data

    @property
    def details_by_impact(self):
        """
        Details of forecast revisions from news, organized by impacts first

        Returns
        -------
        details : pd.DataFrame
            Index is as MultiIndex consisting of:

            - `impact date`: the date of the impact on the variable of interest
            - `impacted variable`: the variable that is being impacted
            - `update date`: the date of the data update, that results in
              `news` that impacts the forecast of variables of interest
            - `updated variable`: the variable being updated, that results in
              `news` that impacts the forecast of variables of interest

            The columns are:

            - `forecast (prev)`: the previous forecast of the new entry,
              based on the information available in the previous dataset
            - `observed`: the value of the new entry, as it is observed in the
              new dataset
            - `news`: the news associated with the update (this is just the
              forecast error: `observed` - `forecast (prev)`)
            - `weight`: the weight describing how the `news` effects the
              forecast of the variable of interest
            - `impact`: the impact of the `news` on the forecast of the
              variable of interest

        Notes
        -----
        This table decomposes updated forecasts of variables of interest from
        the `news` associated with each updated datapoint from the new data
        release.

        This table does not summarize the impacts or show the effect of
        revisions. That information can be found in the `impacts` or
        `revision_details_by_impact` tables.

        This form of the details table is organized so that the impacted
        dates / variables are first in the index. This is convenient for
        slicing by impacted variables / dates to view the details of data
        updates for a particular variable or date.

        However, since the `forecast (prev)` and `observed` columns have a lot
        of duplication, printing the entire table gives a result that is less
        easy to parse than that produced by the `details_by_update` property.
        `details_by_update` contains the same information but is organized to
        be more convenient for displaying the entire table of detailed updates.
        At the same time, `details_by_update` is less convenient for
        subsetting.

        See Also
        --------
        details_by_update
        revision_details_by_update
        impacts
        """
        s = self.weights.stack(level=[0, 1], **FUTURE_STACK)
        df = s.rename("weight").to_frame()
        if len(self.updates_iloc):
            df["forecast (prev)"] = self.update_forecasts
            df["observed"] = self.update_realized
            df["news"] = self.news
            df["impact"] = df["news"] * df["weight"]
        else:
            df["forecast (prev)"] = []
            df["observed"] = []
            df["news"] = []
            df["impact"] = []
        df = df[["observed", "forecast (prev)", "news", "weight", "impact"]]
        df = df.reorder_levels([2, 3, 0, 1]).sort_index()

        if self.impacted_variable is not None and len(df) > 0:
            df = df.loc[np.s_[:, self.impacted_variable], :]

        mask = np.abs(df["impact"]) > self.tolerance
        return df[mask]

    @property
    def _revision_grouped_impacts(self):
        s = self.revision_grouped_impacts.stack(**FUTURE_STACK)
        df = s.rename("impact").to_frame()
        df = df.reindex(["revision date", "revised variable", "impact"],
                        axis=1)
        if self.revisions_details_start > 0:
            df["revision date"] = (
                self.updated.model._index[self.revisions_details_start - 1])
            df["revised variable"] = "all prior revisions"
        df = (df.set_index(["revision date", "revised variable"], append=True)
                .reorder_levels([2, 3, 0, 1]))
        return df

    @property
    def revision_details_by_impact(self):
        """
        Details of forecast revisions from revised data, organized by impacts

        Returns
        -------
        details : pd.DataFrame
            Index is as MultiIndex consisting of:

            - `impact date`: the date of the impact on the variable of interest
            - `impacted variable`: the variable that is being impacted
            - `revision date`: the date of the data revision, that results in
              `revision` that impacts the forecast of variables of interest
            - `revised variable`: the variable being revised, that results in
              `news` that impacts the forecast of variables of interest

            The columns are:

            - `observed (prev)`: the previous value of the observation, as it
              was given in the previous dataset
            - `revised`: the value of the revised entry, as it is observed in
              the new dataset
            - `revision`: the revision (this is `revised` - `observed (prev)`)
            - `weight`: the weight describing how the `revision` effects the
              forecast of the variable of interest
            - `impact`: the impact of the `revision` on the forecast of the
              variable of interest

        Notes
        -----
        This table decomposes updated forecasts of variables of interest from
        the `revision` associated with each revised datapoint from the new data
        release.

        This table does not summarize the impacts or show the effect of
        new datapoints. That information can be found in the
        `impacts` or `details_by_impact` tables.

        Grouped impacts are shown in this table, with a "revision date" equal
        to the last period prior to which detailed revisions were computed and
        with "revised variable" set to the string "all prior revisions". For
        these rows, all columns except "impact" will be set to NaNs.

        This form of the details table is organized so that the impacted
        dates / variables are first in the index. This is convenient for
        slicing by impacted variables / dates to view the details of data
        updates for a particular variable or date.

        However, since the `observed (prev)` and `revised` columns have a lot
        of duplication, printing the entire table gives a result that is less
        easy to parse than that produced by the `details_by_revision` property.
        `details_by_revision` contains the same information but is organized to
        be more convenient for displaying the entire table of detailed
        revisions. At the same time, `details_by_revision` is less convenient
        for subsetting.

        See Also
        --------
        details_by_revision
        details_by_impact
        impacts
        """
        weights = self.revision_weights.stack(level=[0, 1], **FUTURE_STACK)
        df = pd.concat([
            self.revised.reindex(weights.index),
            self.revised_prev.rename("observed (prev)").reindex(weights.index),
            self.revisions.reindex(weights.index),
            weights.rename("weight"),
            (self.revisions.reindex(weights.index) * weights).rename("impact"),
        ], axis=1)

        if self.n_revisions_grouped > 0:
            df = pd.concat([df, self._revision_grouped_impacts])
            # Explicitly set names for compatibility with pandas=1.2.5
            df.index = df.index.set_names(
                ["revision date", "revised variable",
                 "impact date", "impacted variable"])

        df = df.reorder_levels([2, 3, 0, 1]).sort_index()

        if self.impacted_variable is not None and len(df) > 0:
            df = df.loc[np.s_[:, self.impacted_variable], :]

        mask = np.abs(df["impact"]) > self.tolerance
        return df[mask]

    @property
    def details_by_update(self):
        """
        Details of forecast revisions from news, organized by updates first

        Returns
        -------
        details : pd.DataFrame
            Index is as MultiIndex consisting of:

            - `update date`: the date of the data update, that results in
              `news` that impacts the forecast of variables of interest
            - `updated variable`: the variable being updated, that results in
              `news` that impacts the forecast of variables of interest
            - `forecast (prev)`: the previous forecast of the new entry,
              based on the information available in the previous dataset
            - `observed`: the value of the new entry, as it is observed in the
              new dataset
            - `impact date`: the date of the impact on the variable of interest
            - `impacted variable`: the variable that is being impacted

            The columns are:

            - `news`: the news associated with the update (this is just the
              forecast error: `observed` - `forecast (prev)`)
            - `weight`: the weight describing how the `news` affects the
              forecast of the variable of interest
            - `impact`: the impact of the `news` on the forecast of the
              variable of interest

        Notes
        -----
        This table decomposes updated forecasts of variables of interest from
        the `news` associated with each updated datapoint from the new data
        release.

        This table does not summarize the impacts or show the effect of
        revisions. That information can be found in the `impacts` table.

        This form of the details table is organized so that the updated
        dates / variables are first in the index, and in this table the index
        also contains the forecasts and observed values of the updates. This is
        convenient for displaying the entire table of detailed updates because
        it allows sparsifying duplicate entries.

        However, since it includes forecasts and observed values in the index
        of the table, it is not convenient for subsetting by the variable of
        interest. Instead, the `details_by_impact` property is organized to
        make slicing by impacted variables / dates easy. This allows, for
        example, viewing the details of data updates on a particular variable
        or date of interest.

        See Also
        --------
        details_by_impact
        impacts
        """
        s = self.weights.stack(level=[0, 1], **FUTURE_STACK)
        df = s.rename("weight").to_frame()
        if len(self.updates_iloc):
            df["forecast (prev)"] = self.update_forecasts
            df["observed"] = self.update_realized
            df["news"] = self.news
            df["impact"] = df["news"] * df["weight"]
        else:
            df["forecast (prev)"] = []
            df["observed"] = []
            df["news"] = []
            df["impact"] = []
        df = df[["forecast (prev)", "observed", "news",
                 "weight", "impact"]]
        df = df.reset_index()
        keys = ["update date", "updated variable", "observed",
                "forecast (prev)", "impact date", "impacted variable"]
        df.index = pd.MultiIndex.from_arrays([df[key] for key in keys])
        details = df.drop(keys, axis=1).sort_index()

        if self.impacted_variable is not None and len(df) > 0:
            details = details.loc[
                np.s_[:, :, :, :, :, self.impacted_variable], :]

        mask = np.abs(details["impact"]) > self.tolerance
        return details[mask]

    @property
    def revision_details_by_update(self):
        """
        Details of forecast revisions from revisions, organized by updates

        Returns
        -------
        details : pd.DataFrame
            Index is as MultiIndex consisting of:

            - `revision date`: the date of the data revision, that results in
              `revision` that impacts the forecast of variables of interest
            - `revised variable`: the variable being revised, that results in
              `news` that impacts the forecast of variables of interest
            - `observed (prev)`: the previous value of the observation, as it
              was given in the previous dataset
            - `revised`: the value of the revised entry, as it is observed in
              the new dataset
            - `impact date`: the date of the impact on the variable of interest
            - `impacted variable`: the variable that is being impacted

            The columns are:

            - `revision`: the revision (this is `revised` - `observed (prev)`)
            - `weight`: the weight describing how the `revision` affects the
              forecast of the variable of interest
            - `impact`: the impact of the `revision` on the forecast of the
              variable of interest

        Notes
        -----
        This table decomposes updated forecasts of variables of interest from
        the `revision` associated with each revised datapoint from the new data
        release.

        This table does not summarize the impacts or show the effect of
        new datapoints, see `details_by_update` instead.

        Grouped impacts are shown in this table, with a "revision date" equal
        to the last period prior to which detailed revisions were computed and
        with "revised variable" set to the string "all prior revisions". For
        these rows, all columns except "impact" will be set to NaNs.

        This form of the details table is organized so that the revision
        dates / variables are first in the index, and in this table the index
        also contains the previously observed and revised values. This is
        convenient for displaying the entire table of detailed revisions
        because it allows sparsifying duplicate entries.

        However, since it includes previous observations and revisions in the
        index of the table, it is not convenient for subsetting by the variable
        of interest. Instead, the `revision_details_by_impact` property is
        organized to make slicing by impacted variables / dates easy. This
        allows, for example, viewing the details of data revisions on a
        particular variable or date of interest.

        See Also
        --------
        details_by_impact
        impacts
        """
        weights = self.revision_weights.stack(level=[0, 1], **FUTURE_STACK)

        df = pd.concat([
            self.revised_prev.rename("observed (prev)").reindex(weights.index),
            self.revised.reindex(weights.index),
            self.revisions.reindex(weights.index),
            weights.rename("weight"),
            (self.revisions.reindex(weights.index) * weights).rename("impact"),
        ], axis=1)

        if self.n_revisions_grouped > 0:
