mergeron 2025.739290.2__py3-none-any.whl → 2025.739290.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mergeron might be problematic. Click here for more details.

@@ -7,12 +7,11 @@ with a canvas on which to draw boundaries for Guidelines standards.
7
7
  from __future__ import annotations
8
8
 
9
9
  import decimal
10
- from dataclasses import dataclass
11
10
  from typing import Literal
12
11
 
13
12
  import numpy as np
14
13
  from attrs import Attribute, field, frozen, validators
15
- from mpmath import mp, mpf # type: ignore
14
+ from mpmath import mp # type: ignore
16
15
  from ruamel import yaml
17
16
 
18
17
  from .. import ( # noqa: TID252
@@ -23,6 +22,8 @@ from .. import ( # noqa: TID252
23
22
  RECForm,
24
23
  UPPAggrSelector,
25
24
  this_yaml,
25
+ yamelize_attrs,
26
+ yaml_rt_mapper,
26
27
  )
27
28
  from . import guidelines_boundary_functions as gbfn
28
29
 
@@ -33,7 +34,7 @@ mp.dps = 32
33
34
  mp.trap_complex = True
34
35
 
35
36
 
36
- @dataclass(frozen=True)
37
+ @frozen
37
38
  class HMGThresholds:
38
39
  delta: float
39
40
  fc: float
@@ -43,21 +44,6 @@ class HMGThresholds:
43
44
  cmcr: float
44
45
  ipr: float
45
46
 
46
- @classmethod
47
- def to_yaml(
48
- cls, _r: yaml.representer.SafeRepresenter, _d: HMGThresholds
49
- ) -> yaml.MappingNode:
50
- _ret: yaml.MappingNode = _r.represent_mapping(
51
- f"!{cls.__name__}", {_a: getattr(_d, _a) for _a in _d.__dataclass_fields__}
52
- )
53
- return _ret
54
-
55
- @classmethod
56
- def from_yaml(
57
- cls, _c: yaml.constructor.SafeConstructor, _n: yaml.MappingNode
58
- ) -> HMGThresholds:
59
- return cls(**_c.construct_mapping(_n))
60
-
61
47
 
62
48
  @this_yaml.register_class
63
49
  @frozen
@@ -112,7 +98,7 @@ class GuidelinesThresholds:
112
98
  # that staff only investigates mergers that meet the presumption;
113
99
  # thus, here, the tentative delta safeharbor under
114
100
  # the 2023 Guidelines is 100 points
115
- _hhi_p, _dh_s, _dh_p = {
101
+ hhi_p, dh_s, dh_p = {
116
102
  1982: (_s1982 := (0.18, 0.005, 0.01)),
117
103
  1984: _s1982,
118
104
  1992: _s1982,
@@ -124,18 +110,18 @@ class GuidelinesThresholds:
124
110
  self,
125
111
  "safeharbor",
126
112
  HMGThresholds(
127
- _dh_s,
128
- _fc := int(np.ceil(1 / _hhi_p)),
129
- _r := float(_r_s := gbfn.round_cust(_fc / (_fc + 1), frac=0.05)),
130
- _g := float(guppi_from_delta(_dh_s, m_star=1.0, r_bar=_r)),
131
- _dr := float(1 - _r_s),
113
+ dh_s,
114
+ _fc := int(np.ceil(1 / hhi_p)),
115
+ _r := gbfn.round_cust(_fc / (_fc + 1), frac=0.05),
116
+ _g := guppi_from_delta(dh_s, m_star=1.0, r_bar=_r),
117
+ _dr := 1 - _r,
132
118
  _cmcr := 0.03, # Not strictly a Guidelines standard
133
119
  _ipr := _g, # Not strictly a Guidelines standard
134
120
  ),
135
121
  )
136
122
 
137
123
  object.__setattr__(
138
- self, "presumption", HMGThresholds(_dh_p, _fc, _r, _g, _dr, _cmcr, _ipr)
124
+ self, "presumption", HMGThresholds(dh_p, _fc, _r, _g, _dr, _cmcr, _ipr)
139
125
  )
140
126
 
141
127
  # imputed_presumption is relevant for presumptions implicating
@@ -148,13 +134,9 @@ class GuidelinesThresholds:
148
134
  HMGThresholds(
149
135
  2 * (0.5 / _fc) ** 2,
150
136
  _fc,
151
- float(
152
- _r_i := gbfn.round_cust(
153
- (_fc - 1 / 2) / (_fc + 1 / 2), frac=0.05
154
- )
155
- ),
137
+ _r_i := gbfn.round_cust((_fc - 1 / 2) / (_fc + 1 / 2), frac=0.05),
156
138
  _g,
157
- float((1 - _r_i) / 2),
139
+ (1 - _r_i) / 2,
158
140
  _cmcr,
159
141
  _ipr := _g,
160
142
  )
@@ -167,22 +149,20 @@ class GuidelinesThresholds:
167
149
 
168
150
  @classmethod
169
151
  def to_yaml(
170
- cls, _r: yaml.representer.SafeRepresenter, _d: GuidelinesThresholds
152
+ cls, _r: yaml.representer.RoundTripRepresenter, _d: GuidelinesThresholds
171
153
  ) -> yaml.MappingNode:
172
- _ret: yaml.MappingNode = _r.represent_mapping(
173
- f"!{cls.__name__}",
174
- {_a.name: getattr(_d, _a.name) for _a in _d.__attrs_attrs__},
154
+ ret: yaml.MappingNode = _r.represent_mapping(
155
+ f"!{cls.__name__}", {"pub_year": _d.pub_year}
175
156
  )
176
- return _ret
157
+ return ret
177
158
 
178
159
  @classmethod
179
160
  def from_yaml(
180
- cls, _c: yaml.constructor.SafeConstructor, _n: yaml.MappingNode
161
+ cls, _c: yaml.constructor.RoundTripConstructor, _n: yaml.MappingNode
181
162
  ) -> GuidelinesThresholds:
182
- return cls(**_c.construct_mapping(_n))
163
+ return cls(**yaml_rt_mapper(_c, _n))
183
164
 
184
165
 
185
- @this_yaml.register_class
186
166
  @frozen
187
167
  class ConcentrationBoundary:
188
168
  """Concentration parameters, boundary coordinates, and area under concentration boundary."""
@@ -198,12 +178,12 @@ class ConcentrationBoundary:
198
178
  def _mnv(
199
179
  _instance: ConcentrationBoundary, _attribute: Attribute[str], _value: str, /
200
180
  ) -> None:
201
- if _value not in (
181
+ if _value not in {
202
182
  "ΔHHI",
203
183
  "Combined share",
204
184
  "Pre-merger HHI Contribution",
205
185
  "Post-merger HHI Contribution",
206
- ):
186
+ }:
207
187
  raise ValueError(f"Invalid name for a concentration measure, {_value!r}.")
208
188
 
209
189
  threshold: float = field(kw_only=False, default=0.01)
@@ -228,40 +208,19 @@ class ConcentrationBoundary:
228
208
  def __attrs_post_init__(self, /) -> None:
229
209
  match self.measure_name:
230
210
  case "ΔHHI":
231
- _conc_fn = gbfn.hhi_delta_boundary
211
+ conc_fn = gbfn.hhi_delta_boundary
232
212
  case "Combined share":
233
- _conc_fn = gbfn.combined_share_boundary
213
+ conc_fn = gbfn.combined_share_boundary
234
214
  case "Pre-merger HHI Contribution":
235
- _conc_fn = gbfn.hhi_pre_contrib_boundary
215
+ conc_fn = gbfn.hhi_pre_contrib_boundary
236
216
  case "Post-merger HHI Contribution":
237
- _conc_fn = gbfn.hhi_post_contrib_boundary
217
+ conc_fn = gbfn.hhi_post_contrib_boundary
238
218
 
239
- _boundary = _conc_fn(self.threshold, dps=self.precision)
240
- object.__setattr__(self, "area", _boundary.area)
241
- object.__setattr__(self, "coordinates", _boundary.coordinates)
219
+ boundary_ = conc_fn(self.threshold, dps=self.precision)
220
+ object.__setattr__(self, "area", boundary_.area)
221
+ object.__setattr__(self, "coordinates", boundary_.coordinates)
242
222
 
243
- @classmethod
244
- def to_yaml(
245
- cls, _r: yaml.representer.SafeRepresenter, _d: ConcentrationBoundary
246
- ) -> yaml.MappingNode:
247
- _ret: yaml.MappingNode = _r.represent_mapping(
248
- f"!{cls.__name__}",
249
- {
250
- _a.name: getattr(_d, _a.name)
251
- for _a in _d.__attrs_attrs__
252
- if _a.name not in ("area", "coordinates")
253
- },
254
- )
255
- return _ret
256
223
 
257
- @classmethod
258
- def from_yaml(
259
- cls, _c: yaml.constructor.SafeConstructor, _n: yaml.MappingNode
260
- ) -> ConcentrationBoundary:
261
- return cls(**_c.construct_mapping(_n))
262
-
263
-
264
- @this_yaml.register_class
265
224
  @frozen
266
225
  class DiversionRatioBoundary:
267
226
  """
@@ -373,70 +332,50 @@ class DiversionRatioBoundary:
373
332
  """Market-share pairs as Cartesian coordinates of points on the diversion ratio boundary."""
374
333
 
375
334
  def __attrs_post_init__(self, /) -> None:
376
- _share_ratio = critical_share_ratio(
335
+ share_ratio = critical_share_ratio(
377
336
  self.diversion_ratio, r_bar=self.recapture_ratio
378
337
  )
379
- _upp_agg_kwargs: gbfn.ShareRatioBoundaryKeywords = {
338
+ upp_agg_kwargs: gbfn.ShareRatioBoundaryKeywords = {
380
339
  "recapture_form": getattr(self.recapture_form, "value", "inside-out"),
381
340
  "dps": self.precision,
382
341
  }
383
342
 
384
343
  match self.agg_method:
385
344
  case UPPAggrSelector.DIS:
386
- _upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
387
- _upp_agg_kwargs |= {"agg_method": "distance", "weighting": None}
345
+ upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
346
+ upp_agg_kwargs |= {"agg_method": "distance", "weighting": None}
388
347
  case UPPAggrSelector.AVG:
389
- _upp_agg_fn = gbfn.shrratio_boundary_xact_avg # type: ignore
348
+ upp_agg_fn = gbfn.shrratio_boundary_xact_avg # type: ignore
390
349
  case UPPAggrSelector.MAX:
391
- _upp_agg_fn = gbfn.shrratio_boundary_max # type: ignore
392
- _upp_agg_kwargs = {"dps": 10} # replace here
350
+ upp_agg_fn = gbfn.shrratio_boundary_max # type: ignore
351
+ upp_agg_kwargs = {"dps": 10} # replace here
393
352
  case UPPAggrSelector.MIN:
394
- _upp_agg_fn = gbfn.shrratio_boundary_min # type: ignore
395
- _upp_agg_kwargs |= {"dps": 10} # update here
353
+ upp_agg_fn = gbfn.shrratio_boundary_min # type: ignore
354
+ upp_agg_kwargs |= {"dps": 10} # update here
396
355
  case _:
397
- _upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
356
+ upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
398
357
 
399
- _aggregator: Literal["arithmetic mean", "geometric mean", "distance"]
400
- if self.agg_method.value.endswith("average"):
401
- _aggregator = "arithmetic mean"
402
- elif self.agg_method.value.endswith("geometric mean"):
403
- _aggregator = "geometric mean"
358
+ aggregator_: Literal["arithmetic mean", "geometric mean", "distance"]
359
+ if self.agg_method.value.endswith("geometric mean"):
360
+ aggregator_ = "geometric mean"
361
+ elif self.agg_method.value.endswith("average"):
362
+ aggregator_ = "arithmetic mean"
404
363
  else:
405
- _aggregator = "distance"
364
+ aggregator_ = "distance"
406
365
 
407
- _wgt_type: Literal["cross-product-share", "own-share", None]
366
+ wgt_type: Literal["cross-product-share", "own-share", None]
408
367
  if self.agg_method.value.startswith("cross-product-share"):
409
- _wgt_type = "cross-product-share"
368
+ wgt_type = "cross-product-share"
410
369
  elif self.agg_method.value.startswith("own-share"):
411
- _wgt_type = "own-share"
370
+ wgt_type = "own-share"
412
371
  else:
413
- _wgt_type = None
372
+ wgt_type = None
414
373
 
415
- _upp_agg_kwargs |= {"agg_method": _aggregator, "weighting": _wgt_type}
374
+ upp_agg_kwargs |= {"agg_method": aggregator_, "weighting": wgt_type}
416
375
 
417
- _boundary = _upp_agg_fn(_share_ratio, self.recapture_ratio, **_upp_agg_kwargs)
418
- object.__setattr__(self, "area", _boundary.area)
419
- object.__setattr__(self, "coordinates", _boundary.coordinates)
420
-
421
- @classmethod
422
- def to_yaml(
423
- cls, _r: yaml.representer.SafeRepresenter, _d: DiversionRatioBoundary
424
- ) -> yaml.MappingNode:
425
- _ret: yaml.MappingNode = _r.represent_mapping(
426
- f"!{cls.__name__}",
427
- {
428
- _a.name: getattr(_d, _a.name)
429
- for _a in _d.__attrs_attrs__
430
- if _a.name not in ("area", "coordinates")
431
- },
432
- )
433
- return _ret
434
-
435
- @classmethod
436
- def from_yaml(
437
- cls, _c: yaml.constructor.SafeConstructor, _n: yaml.MappingNode
438
- ) -> DiversionRatioBoundary:
439
- return cls(**_c.construct_mapping(_n))
376
+ boundary_ = upp_agg_fn(share_ratio, self.recapture_ratio, **upp_agg_kwargs)
377
+ object.__setattr__(self, "area", boundary_.area)
378
+ object.__setattr__(self, "coordinates", boundary_.coordinates)
440
379
 
441
380
 
442
381
  def guppi_from_delta(
@@ -445,7 +384,7 @@ def guppi_from_delta(
445
384
  *,
446
385
  m_star: float = 1.00,
447
386
  r_bar: float = DEFAULT_REC_RATIO,
448
- ) -> decimal.Decimal:
387
+ ) -> float:
449
388
  """
450
389
  Translate ∆HHI bound to GUPPI bound.
451
390
 
@@ -471,13 +410,13 @@ def guppi_from_delta(
471
410
 
472
411
 
473
412
  def critical_share_ratio(
474
- _guppi_bound: float | decimal.Decimal = 0.075,
413
+ _guppi_bound: float = 0.075,
475
414
  /,
476
415
  *,
477
416
  m_star: float = 1.00,
478
417
  r_bar: float = 1.00,
479
418
  frac: float = 1e-16,
480
- ) -> decimal.Decimal:
419
+ ) -> float:
481
420
  """
482
421
  Corollary to GUPPI bound.
483
422
 
@@ -496,18 +435,16 @@ def critical_share_ratio(
496
435
  for given margin and recapture ratio.
497
436
 
498
437
  """
499
- return gbfn.round_cust(
500
- mpf(f"{_guppi_bound}") / mp.fmul(f"{m_star}", f"{r_bar}"), frac=frac
501
- )
438
+ return gbfn.round_cust(_guppi_bound / (m_star * r_bar), frac=frac)
502
439
 
503
440
 
504
441
  def share_from_guppi(
505
- _guppi_bound: float | decimal.Decimal = 0.065,
442
+ _guppi_bound: float = 0.065,
506
443
  /,
507
444
  *,
508
445
  m_star: float = 1.00,
509
446
  r_bar: float = DEFAULT_REC_RATIO,
510
- ) -> decimal.Decimal:
447
+ ) -> float:
511
448
  """
512
449
  Symmetric-firm share for given GUPPI, margin, and recapture ratio.
513
450
 
@@ -538,3 +475,7 @@ if __name__ == "__main__":
538
475
  print(
539
476
  "This module defines classes with methods for generating boundaries for concentration and diversion-ratio screens."
540
477
  )
478
+
479
+
480
+ for _typ in (HMGThresholds, ConcentrationBoundary, DiversionRatioBoundary):
481
+ yamelize_attrs(_typ, {"coordinates", "area"})