Source code for pair_match.match_result
"""The matched pairs a net-effects analysis is computed on.
This package does the *estimation* half of a matched study -- it takes pairs as given
and draws inference from their binary outcomes. How the pairs were formed is out of
scope: propensity-score matching, exact matching on a few keys, or a pairing that
already exists in the data all work equally well.
:class:`MatchResult` is the hand-off point. It is a plain record of which treated unit
was paired with which control, by index label, and is consumed by
:meth:`pair_match.PairedOutcomeTable.from_match_result`. Anything else carrying
``treated_index`` and ``control_index`` attributes of equal length works there too --
the method reads only those two.
If you are starting from two aligned 0/1 outcome vectors rather than from index labels,
skip this module entirely and use :meth:`pair_match.PairedOutcomeTable.from_outcomes`.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Protocol
import numpy as np
import numpy.typing as npt
if TYPE_CHECKING:
from collections.abc import Sequence
[docs]
class Pairing(Protocol):
"""Anything :meth:`pair_match.PairedOutcomeTable.from_match_result` accepts.
Declared as read-only properties so that a frozen dataclass, a plain class
attribute, or a property all satisfy it.
"""
@property
def treated_index(self) -> Sequence[object]:
"""Index labels of the treated units, one per pair."""
...
@property
def control_index(self) -> Sequence[object]:
"""Index labels of the matched controls, aligned with ``treated_index``."""
...
[docs]
@dataclass(frozen=True)
class MatchResult:
"""One treated unit paired with one control, repeated ``n_pairs`` times.
Attributes
----------
treated_index : list
Index labels of the treated units, one per pair.
control_index : list
Index labels of the matched controls, aligned element-wise with
``treated_index``: ``control_index[i]`` is the control matched to
``treated_index[i]``.
distances : ndarray, optional
Covariate distance within each pair, in the same order. Carried for reporting
only -- nothing in the net-effects inference reads it -- so it defaults to an
empty array when the pairing came from somewhere that does not compute
distances.
"""
treated_index: list[object]
control_index: list[object]
distances: npt.NDArray[np.float64] = field(
default_factory=lambda: np.empty(0, dtype=np.float64)
)
def __post_init__(self) -> None:
if len(self.treated_index) != len(self.control_index):
raise ValueError(
"`treated_index` and `control_index` must have the same "
f"length (got {len(self.treated_index)} and "
f"{len(self.control_index)})."
)
@property
def n_pairs(self) -> int:
"""Number of matched pairs."""
return len(self.treated_index)