Source code for p2p.models.reference_distribution

"""Stage 2 fitted around a fixed reference distribution.

A reference distribution ``a`` (one value per option row, summing to one within
each question) enters every prediction with weight one:

    prediction = a + X @ beta

The agents therefore explain each target's *deviation* from the reference
rather than its level. Internally the estimator is fitted to ``y - a`` with the
question-mass target lowered by one, so that each question's prediction still
sums to the configured total; when every agent answers every question this
makes the agents' weights sum to zero. The penalty, the cross-validation and
the scoring are those of the wrapped estimator: ``score`` and ``predict`` work
on the full prediction, so the error of ``y - a`` against ``X @ beta`` and the
error of ``y`` against ``a + X @ beta`` are the same number.

The drift refit of a survey across waves is the case where ``a`` is the earlier
wave's marginals.
"""
from __future__ import annotations

from collections.abc import Mapping

import numpy as np
import pandas as pd

TOLERANCE = 1e-9


[docs] def reference_vector( reference: Mapping[str, float], rows: pd.Index, ) -> np.ndarray: """The reference value of every option row, in row order.""" missing = [row for row in rows if row not in reference] if missing: raise ValueError( "reference distribution has no value for option rows " f"{missing[:5]}{' ...' if len(missing) > 5 else ''}" ) return np.asarray([float(reference[row]) for row in rows], dtype=float)
[docs] def check_reference_masses( reference: Mapping[str, float], constraint_groups: Mapping[str, list[str]], rows: pd.Index, ) -> None: """Every question's reference mass over its modelled rows must be one.""" present = set(rows) for question, members in constraint_groups.items(): modelled = [row for row in members if row in present] if not modelled: continue mass = sum(float(reference[row]) for row in modelled) if abs(mass - 1.0) > TOLERANCE: raise ValueError( f"reference distribution for {question!r} sums to {mass!r} over " "its modelled option rows; it must sum to one" )
[docs] def with_reference_distribution(model_cls: type, reference: Mapping[str, float]) -> type: """Return ``model_cls`` fitted around ``reference`` (see the module docstring). ``reference`` maps option-row ids to values. The returned class takes the same keyword arguments; ``group_total`` keeps its meaning as the total of each question's full prediction. """ frozen = dict(reference) class ReferenceDistributionModel(model_cls): reference_distribution_ = frozen def __init__(self, *args, group_total: float = 1.0, **kwargs): # The estimator only sees the deviation, whose mass target is the # full prediction's total minus the reference's mass of one. super().__init__(*args, group_total=group_total - 1.0, **kwargs) self.prediction_group_total_ = group_total def fit(self, A: pd.DataFrame, y: np.ndarray): offset = reference_vector(frozen, A.index) return super().fit(A, np.asarray(y, dtype=float) - offset) def predict(self, A: pd.DataFrame) -> np.ndarray: return super().predict(A) + reference_vector(frozen, A.index) ReferenceDistributionModel.__name__ = f"{model_cls.__name__}WithReference" ReferenceDistributionModel.__qualname__ = ReferenceDistributionModel.__name__ return ReferenceDistributionModel
__all__ = [ "check_reference_masses", "reference_vector", "with_reference_distribution", ]