Source code for match_stream.bandwidth
"""The two data-driven bandwidth rules of kernel and ridge matching, as objects.
``match_kernel(..., bandwidth=)`` takes a number (score units), the name of a
rule with its defaults (``"pair_distance"``, ``"cross_validation"``), or one of
these objects when a rule's settings are changed.
"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
PENALTIES = (None, "support_loss")
[docs]
@dataclass(frozen=True)
class PairDistance:
"""The bandwidth from the distances of 1:1 nearest-neighbor matching.
``factor`` times the ``quantile`` quantile of the nonzero distances
between each focal unit and its nearest other-arm unit (with replacement,
pooled over strata). The default, 1.5 times the 90th percentile, is narrow:
focal units with no other-arm unit within it are unmatched.
"""
factor: float = 1.5
quantile: float = 0.9
def __post_init__(self):
if not float(self.factor) > 0:
raise ValueError("PairDistance factor must be positive")
if not 0 < float(self.quantile) <= 1:
raise ValueError("PairDistance quantile must be in (0, 1]")
[docs]
@dataclass(frozen=True)
class CrossValidation:
"""The bandwidth chosen by leave-one-out cross-validation.
Parameters
----------
outcome : str, optional
Predict this outcome column of each other-arm unit from the rest and
choose the bandwidth with the smallest error. Without it the criterion
is the balance of the score: how little leaving out any one other-arm
unit moves the matched mean of the score.
grid : sequence of float, optional
The bandwidths tried. Default: the pair-distance bandwidth times
1.5^k for k = -4..4, extended and refined around the minimum.
weighted : bool, default False
Weight the outcome's errors by the units' matching weights. Needs
`outcome`.
penalty : None or "support_loss"
None judges every bandwidth on the same units. "support_loss" judges
each bandwidth on the units it can evaluate, times a penalty for the
units it loses to common support (and for bandwidths above the score's
standard deviation), searching outward from the pair-distance
bandwidth (or over `grid`).
"""
outcome: str | None = None
grid: Sequence[float] | None = None
weighted: bool = False
penalty: str | None = None
def __post_init__(self):
if self.penalty not in PENALTIES:
raise ValueError(f"CrossValidation penalty must be None or 'support_loss', "
f"not {self.penalty!r}")
if self.weighted and self.outcome is None:
raise ValueError("CrossValidation weighted=True weights the outcome's errors: "
"give outcome=")
if self.grid is not None:
object.__setattr__(self, "grid", tuple(float(x) for x in self.grid))