Source code for mergeron.core.guidelines_boundaries

"""
Methods for defining and analyzing boundaries for Guidelines standards.

Includes function to create a canvas on which to draw boundaries for
Guidelines standards.

"""

from __future__ import annotations

import decimal
from typing import Literal

import numpy as np
from attrs import Attribute, field, frozen, validators
from mpmath import mp  # type: ignore

from .. import (  # noqa: TID252
    DEFAULT_REC,
    VERSION,
    ArrayDouble,
    PubYear,
    RECForm,
    UPPAggrSelector,
    this_yaml,
    yamelize_attrs,
)
from . import guidelines_boundary_functions as gbfn

__version__ = VERSION


mp.dps = 32
mp.trap_complex = True


@frozen
[docs] class MGThresholds: """Thresholds for Guidelines standards."""
[docs] delta: float
[docs] fc: float
[docs] rec: float
[docs] guppi: float
[docs] dr: float
[docs] cmcr: float
[docs] ipr: float
@this_yaml.register_class @frozen
[docs] class GuidelinesThresholds: """ Guidelines thresholds by Guidelines publication year. ΔHHI, Recapture Rate, GUPPI, Diversion ratio, CMCR, and IPR thresholds constructed from concentration standards in Guidelines published in 1992, 2010, and 2023. """
[docs] pub_year: PubYear = field( kw_only=False, default=2023, validator=validators.in_([1992, 2010, 2023]) )
""" Year of publication of the Guidelines. """
[docs] safeharbor: MGThresholds = field(kw_only=True, default=None, init=False)
""" Negative presumption quantified on various measures. ΔHHI safeharbor bound, default recapture rate, GUPPI bound, diversion ratio limit, CMCR, and IPR. """
[docs] presumption: MGThresholds = field(kw_only=True, default=None, init=False)
""" Presumption of harm defined in HMG. ΔHHI bound and corresponding default recapture rate, GUPPI bound, diversion ratio limit, CMCR, and IPR. """
[docs] imputed_presumption: MGThresholds = field(kw_only=True, default=None, init=False)
""" Presumption of harm imputed from Guidelines. ΔHHI bound inferred from strict numbers-equivalent of (post-merger) HHI presumption, and corresponding default recapture rate, GUPPI bound, diversion ratio limit, CMCR, and IPR. """ def __attrs_post_init__(self, /) -> None: """ Initialize Guidelines thresholds, based on Guidelines publication year. In the 2023 Guidelines, the agencies do not define a negative presumption, or safeharbor. Practically speaking, given resource constraints and loss aversion, it is likely that staff only investigates mergers that meet the presumption; thus, here, the tentative delta safeharbor under the 2023 Guidelines is 100 points. """ hhi_p, dh_s, dh_p = { 1992: (0.18, 0.005, 0.01), 2010: (0.25, 0.01, 0.02), 2023: (0.18, 0.01, 0.01), }[self.pub_year] object.__setattr__( self, "safeharbor", MGThresholds( dh_s, _fc := int(np.ceil(1 / hhi_p)), _r := gbfn.round_cust(_fc / (_fc + 1), frac=0.05), _g := guppi_from_delta(dh_s, m_star=1.0, r_bar=_r), _dr := 1 - _r, _cmcr := _g, # Not strictly a Guidelines standard _ipr := _g, # Not strictly a Guidelines standard ), ) object.__setattr__( self, "presumption", MGThresholds(dh_p, _fc, _r, _g, _dr, _cmcr, _ipr) ) # imputed_presumption is relevant for presumptions implicating # mergers *to* symmetry in numbers-equivalent of post-merger HHI, # as in 2010 U.S.Guidelines. object.__setattr__( self, "imputed_presumption", ( MGThresholds( 2 * (0.5 / _fc) ** 2, _fc, _r_i := gbfn.round_cust((_fc - 1 / 2) / (_fc + 1 / 2), frac=0.05), _g, (1 - _r_i) / 2, _cmcr, _ipr, ) if self.pub_year == 2010 else MGThresholds( 2 * (1 / (_fc + 1)) ** 2, _fc, _r, _g, _dr, _cmcr, _ipr ) ), )
@frozen
[docs] class ConcentrationBoundary: """Concentration parameters, boundary coordinates, and area under concentration boundary."""
[docs] threshold: float = field(kw_only=False, default=0.01)
@threshold.validator def _tv( _instance: ConcentrationBoundary, _attribute: Attribute[float], _value: float, / ) -> None: if not 0 <= _value <= 1: raise ValueError("Concentration threshold must lie between 0 and 1.")
[docs] measure_name: Literal[ "ΔHHI", "Combined share", "HHI contribution, pre-merger", "HHI contribution, post-merger", ] = field(kw_only=False, default="ΔHHI")
@measure_name.validator def _mnv( _instance: ConcentrationBoundary, _attribute: Attribute[str], _value: str, / ) -> None: if _value not in { "ΔHHI", "Combined share", "HHI contribution, pre-merger", "HHI contribution, post-merger", }: raise ValueError(f"Invalid name for a concentration measure, {_value!r}.")
[docs] precision: int = field( kw_only=True, default=5, validator=validators.instance_of(int) )
[docs] area: float = field(init=False, kw_only=True)
"""Area under the concentration boundary."""
[docs] coordinates: ArrayDouble = field(init=False, kw_only=True)
"""Market-share pairs as Cartesian coordinates of points on the concentration boundary.""" def __attrs_post_init__(self, /) -> None: """Initialize boundary and area based on other attributes.""" match self.measure_name: case "ΔHHI": conc_fn = gbfn.hhi_delta_boundary case "Combined share": conc_fn = gbfn.combined_share_boundary case "HHI contribution, pre-merger": conc_fn = gbfn.hhi_pre_contrib_boundary case "HHI contribution, post-merger": conc_fn = gbfn.hhi_post_contrib_boundary boundary_ = conc_fn(self.threshold, dps=self.precision) object.__setattr__(self, "area", boundary_.area) object.__setattr__(self, "coordinates", boundary_.coordinates)
@frozen
[docs] class DiversionBoundary: """ Diversion ratio specification, boundary coordinates, and area under boundary. Along with the default diversion ratio and recapture rate, a diversion ratio boundary specification includes the recapture form -- whether fixed for both merging firms' products ("fixed") or consistent with share-proportionality, i.e., "inside-out"; the method of aggregating diversion ratios for the two products, and the precision for the estimate of area under the divertion ratio boundary (also defines the number of points on the boundary). """
[docs] diversion_ratio: float = field(kw_only=False, default=0.065)
@diversion_ratio.validator def _dvv( _instance: DiversionBoundary, _attribute: Attribute[float], _value: float, / ) -> None: if not (isinstance(_value, decimal.Decimal | float) and 0 <= _value <= 1): raise ValueError( "Margin-adjusted benchmark diversion share must lie between 0 and 1." )
[docs] recapture_rate: float = field( kw_only=False, default=DEFAULT_REC, validator=validators.instance_of(float) )
[docs] recapture_form: RECForm | None = field(kw_only=True, default=RECForm.INOUT)
R""" The form of the recapture rate. When :attr:`mergeron.RECForm.INOUT`, the recapture rate for he product having the smaller market-share is assumed to equal the default, and the recapture rate for the product with the larger market-share is computed assuming MNL demand. Fixed recapture rates are specified as :attr:`mergeron.RECForm.FIXED`. (To specify that recapture rates be constructed from the generated purchase-probabilities for products in the market and for the outside good, specify :attr:`mergeron.RECForm.OUTIN`.) The GUPPI boundary is a continuum of conditional diversion ratio boundaries, .. math:: d_{ij} \vert_{p_i, p_j, m_j} \triangleq \frac{g_i p_i}{m_j p_j} = \overline{d} with :math:`d_{ij}` the diversion ratio from product :math:`i` to product :math:`j`; :math:`g_i` the GUPPI for product :math:`i`; :math:`m_j` the price-cost margin on product :math:`j`; :math:`p_i, p_j` the prices of goods :math:`i, j`, respectively; and :math:`\overline{d}` the diversion ratio threshold (i.e., bound). """ @recapture_form.validator def _rsv( _instance: DiversionBoundary, _attribute: Attribute[RECForm], _value: RECForm, / ) -> None: if _value and not (isinstance(_value, RECForm)): raise ValueError(f"Invalid recapture specification, {_value!r}.") if _value == RECForm.OUTIN and _instance.recapture_rate: raise ValueError( f"Invalid recapture specification, {_value!r}. " "You may consider specifying `mergeron.RECForm.INOUT` here, and " 'assigning the default recapture rate as attribute, "recapture_rate" of ' "this `DiversionBoundarySpec` object." ) if _value is None and _instance.agg_method != UPPAggrSelector.MAX: raise ValueError( f"Specified aggregation method, {_instance.agg_method} requires a recapture specification." )
[docs] agg_method: UPPAggrSelector = field( kw_only=True, default=UPPAggrSelector.MIN, validator=validators.instance_of(UPPAggrSelector), )
""" Method for aggregating the distinct diversion ratio measures for the two products. Distinct diversion ratio or GUPPI measures for the two merging-firms' products are aggregated using the method specified by the `agg_method` attribute, which is specified using the enum :class:`mergeron.UPPAggrSelector`. """
[docs] precision: int = field( kw_only=False, default=5, validator=validators.instance_of(int) )
""" The number of decimal places of precision for the estimated area under the UPP boundary. Leaving this attribute unspecified will result in the default precision, which varies based on the `agg_method` attribute, reflecting the limit of precision available from the underlying functions. The number of boundary points generated is also defined based on this attribute. """
[docs] area: float = field(init=False, kw_only=True)
"""Area under the diversion ratio boundary."""
[docs] coordinates: ArrayDouble = field(init=False, kw_only=True)
"""Market-share pairs as Cartesian coordinates of points on the diversion ratio boundary.""" def __attrs_post_init__(self, /) -> None: """Initialize boundary and area based on other attributes.""" share_ratio = critical_diversion_share( self.diversion_ratio, r_bar=self.recapture_rate ) upp_agg_kwargs: gbfn.DiversionShareBoundaryKeywords = {"dps": self.precision} if self.agg_method != UPPAggrSelector.MAX: upp_agg_kwargs |= { "recapture_form": getattr(self.recapture_form, "value", "inside-out") } match self.agg_method: case UPPAggrSelector.DIS: upp_agg_fn = gbfn.diversion_share_boundary_wtd_avg upp_agg_kwargs |= {"agg_method": "distance", "weighting": None} case UPPAggrSelector.AVG: upp_agg_fn = gbfn.diversion_share_boundary_xact_avg case UPPAggrSelector.MAX: upp_agg_fn = gbfn.diversion_share_boundary_max upp_agg_kwargs |= {"dps": 10} case UPPAggrSelector.MIN: upp_agg_fn = gbfn.diversion_share_boundary_min upp_agg_kwargs |= {"dps": 10} case _: upp_agg_fn = gbfn.diversion_share_boundary_wtd_avg aggregator_: Literal["arithmetic mean", "geometric mean", "distance"] if self.agg_method.value.endswith("geometric mean"): aggregator_ = "geometric mean" elif self.agg_method.value.endswith("average"): aggregator_ = "arithmetic mean" else: aggregator_ = "distance" wgt_type: Literal["cross-product-share", "own-share", None] if self.agg_method.value.startswith("cross-product-share"): wgt_type = "cross-product-share" elif self.agg_method.value.startswith("own-share"): wgt_type = "own-share" else: wgt_type = None upp_agg_kwargs |= {"agg_method": aggregator_, "weighting": wgt_type} boundary_ = upp_agg_fn(share_ratio, self.recapture_rate, **upp_agg_kwargs) # type: ignore object.__setattr__(self, "area", boundary_.area) object.__setattr__(self, "coordinates", boundary_.coordinates)
[docs] def guppi_from_delta( _delta_bound: float = 0.01, /, *, m_star: float = 1.00, r_bar: float = DEFAULT_REC ) -> float: """ Translate ∆HHI bound to GUPPI bound. Parameters ---------- _delta_bound Specified ∆HHI bound. m_star Parametric price-cost margin. r_bar Default recapture rate. Returns ------- GUPPI bound corresponding to ∆HHI bound, at given margin and recapture rate. """ return gbfn.round_cust( m_star * r_bar * (_s_m := np.sqrt(_delta_bound / 2)) / (1 - _s_m), frac=0.005, rounding_mode="ROUND_HALF_DOWN", )
[docs] def critical_diversion_share( _guppi_bound: float = 0.075, /, *, m_star: float = 1.00, r_bar: float = 1.00, frac: float = 1e-16, ) -> float: """ Corollary to GUPPI bound. Parameters ---------- _guppi_bound Specified GUPPI bound. m_star Parametric price-cost margin. r_bar Default recapture rate. Returns ------- Critical diversion share (diversion share bound) corresponding to the GUPPI bound for given margin and recapture rate. """ return gbfn.round_cust(_guppi_bound / (m_star * r_bar), frac=frac)
[docs] def share_from_guppi( _guppi_bound: float = 0.065, /, *, m_star: float = 1.00, r_bar: float = DEFAULT_REC ) -> float: """ Symmetric-firm share for given GUPPI, margin, and recapture rate. Parameters ---------- _guppi_bound GUPPI bound. m_star Parametric price-cost margin. r_bar Default recapture rate. Returns ------- float Symmetric firm market share on GUPPI boundary, for given margin and recapture rate. """ return gbfn.round_cust( (_d0 := critical_diversion_share(_guppi_bound, m_star=m_star, r_bar=r_bar)) / (1 + _d0) )
if __name__ == "__main__": print( "This module defines classes with methods for generating boundaries for concentration and diversion-ratio screens." ) for _typ in ( ConcentrationBoundary, DiversionBoundary, GuidelinesThresholds, MGThresholds, ): yamelize_attrs(_typ)