mergeron 2025.739290.3__py3-none-any.whl → 2025.739290.5__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,13 +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
16
- from ruamel import yaml
14
+ from mpmath import mp # type: ignore
17
15
 
18
16
  from .. import ( # noqa: TID252
19
17
  DEFAULT_REC_RATIO,
@@ -23,6 +21,7 @@ from .. import ( # noqa: TID252
23
21
  RECForm,
24
22
  UPPAggrSelector,
25
23
  this_yaml,
24
+ yamelize_attrs,
26
25
  )
27
26
  from . import guidelines_boundary_functions as gbfn
28
27
 
@@ -33,7 +32,7 @@ mp.dps = 32
33
32
  mp.trap_complex = True
34
33
 
35
34
 
36
- @dataclass(frozen=True)
35
+ @frozen
37
36
  class HMGThresholds:
38
37
  delta: float
39
38
  fc: float
@@ -43,21 +42,6 @@ class HMGThresholds:
43
42
  cmcr: float
44
43
  ipr: float
45
44
 
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
45
 
62
46
  @this_yaml.register_class
63
47
  @frozen
@@ -112,7 +96,7 @@ class GuidelinesThresholds:
112
96
  # that staff only investigates mergers that meet the presumption;
113
97
  # thus, here, the tentative delta safeharbor under
114
98
  # the 2023 Guidelines is 100 points
115
- _hhi_p, _dh_s, _dh_p = {
99
+ hhi_p, dh_s, dh_p = {
116
100
  1982: (_s1982 := (0.18, 0.005, 0.01)),
117
101
  1984: _s1982,
118
102
  1992: _s1982,
@@ -124,18 +108,18 @@ class GuidelinesThresholds:
124
108
  self,
125
109
  "safeharbor",
126
110
  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),
111
+ dh_s,
112
+ _fc := int(np.ceil(1 / hhi_p)),
113
+ _r := gbfn.round_cust(_fc / (_fc + 1), frac=0.05),
114
+ _g := guppi_from_delta(dh_s, m_star=1.0, r_bar=_r),
115
+ _dr := 1 - _r,
132
116
  _cmcr := 0.03, # Not strictly a Guidelines standard
133
117
  _ipr := _g, # Not strictly a Guidelines standard
134
118
  ),
135
119
  )
136
120
 
137
121
  object.__setattr__(
138
- self, "presumption", HMGThresholds(_dh_p, _fc, _r, _g, _dr, _cmcr, _ipr)
122
+ self, "presumption", HMGThresholds(dh_p, _fc, _r, _g, _dr, _cmcr, _ipr)
139
123
  )
140
124
 
141
125
  # imputed_presumption is relevant for presumptions implicating
@@ -148,13 +132,9 @@ class GuidelinesThresholds:
148
132
  HMGThresholds(
149
133
  2 * (0.5 / _fc) ** 2,
150
134
  _fc,
151
- float(
152
- _r_i := gbfn.round_cust(
153
- (_fc - 1 / 2) / (_fc + 1 / 2), frac=0.05
154
- )
155
- ),
135
+ _r_i := gbfn.round_cust((_fc - 1 / 2) / (_fc + 1 / 2), frac=0.05),
156
136
  _g,
157
- float((1 - _r_i) / 2),
137
+ (1 - _r_i) / 2,
158
138
  _cmcr,
159
139
  _ipr := _g,
160
140
  )
@@ -165,24 +145,7 @@ class GuidelinesThresholds:
165
145
  ),
166
146
  )
167
147
 
168
- @classmethod
169
- def to_yaml(
170
- cls, _r: yaml.representer.SafeRepresenter, _d: GuidelinesThresholds
171
- ) -> 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__},
175
- )
176
- return _ret
177
-
178
- @classmethod
179
- def from_yaml(
180
- cls, _c: yaml.constructor.SafeConstructor, _n: yaml.MappingNode
181
- ) -> GuidelinesThresholds:
182
- return cls(**_c.construct_mapping(_n))
183
148
 
184
-
185
- @this_yaml.register_class
186
149
  @frozen
187
150
  class ConcentrationBoundary:
188
151
  """Concentration parameters, boundary coordinates, and area under concentration boundary."""
@@ -190,20 +153,20 @@ class ConcentrationBoundary:
190
153
  measure_name: Literal[
191
154
  "ΔHHI",
192
155
  "Combined share",
193
- "Pre-merger HHI Contribution",
194
- "Post-merger HHI Contribution",
156
+ "HHI contribution, pre-merger",
157
+ "HHI contribution, post-merger",
195
158
  ] = field(kw_only=False, default="ΔHHI")
196
159
 
197
160
  @measure_name.validator
198
161
  def _mnv(
199
162
  _instance: ConcentrationBoundary, _attribute: Attribute[str], _value: str, /
200
163
  ) -> None:
201
- if _value not in (
164
+ if _value not in {
202
165
  "ΔHHI",
203
166
  "Combined share",
204
- "Pre-merger HHI Contribution",
205
- "Post-merger HHI Contribution",
206
- ):
167
+ "HHI contribution, pre-merger",
168
+ "HHI contribution, post-merger",
169
+ }:
207
170
  raise ValueError(f"Invalid name for a concentration measure, {_value!r}.")
208
171
 
209
172
  threshold: float = field(kw_only=False, default=0.01)
@@ -228,40 +191,19 @@ class ConcentrationBoundary:
228
191
  def __attrs_post_init__(self, /) -> None:
229
192
  match self.measure_name:
230
193
  case "ΔHHI":
231
- _conc_fn = gbfn.hhi_delta_boundary
194
+ conc_fn = gbfn.hhi_delta_boundary
232
195
  case "Combined share":
233
- _conc_fn = gbfn.combined_share_boundary
234
- case "Pre-merger HHI Contribution":
235
- _conc_fn = gbfn.hhi_pre_contrib_boundary
236
- case "Post-merger HHI Contribution":
237
- _conc_fn = gbfn.hhi_post_contrib_boundary
238
-
239
- _boundary = _conc_fn(self.threshold, dps=self.precision)
240
- object.__setattr__(self, "area", _boundary.area)
241
- object.__setattr__(self, "coordinates", _boundary.coordinates)
242
-
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
196
+ conc_fn = gbfn.combined_share_boundary
197
+ case "HHI contribution, pre-merger":
198
+ conc_fn = gbfn.hhi_pre_contrib_boundary
199
+ case "HHI contribution, post-merger":
200
+ conc_fn = gbfn.hhi_post_contrib_boundary
256
201
 
257
- @classmethod
258
- def from_yaml(
259
- cls, _c: yaml.constructor.SafeConstructor, _n: yaml.MappingNode
260
- ) -> ConcentrationBoundary:
261
- return cls(**_c.construct_mapping(_n))
202
+ boundary_ = conc_fn(self.threshold, dps=self.precision)
203
+ object.__setattr__(self, "area", boundary_.area)
204
+ object.__setattr__(self, "coordinates", boundary_.coordinates)
262
205
 
263
206
 
264
- @this_yaml.register_class
265
207
  @frozen
266
208
  class DiversionRatioBoundary:
267
209
  """
@@ -373,70 +315,50 @@ class DiversionRatioBoundary:
373
315
  """Market-share pairs as Cartesian coordinates of points on the diversion ratio boundary."""
374
316
 
375
317
  def __attrs_post_init__(self, /) -> None:
376
- _share_ratio = critical_share_ratio(
318
+ share_ratio = critical_share_ratio(
377
319
  self.diversion_ratio, r_bar=self.recapture_ratio
378
320
  )
379
- _upp_agg_kwargs: gbfn.ShareRatioBoundaryKeywords = {
321
+ upp_agg_kwargs: gbfn.ShareRatioBoundaryKeywords = {
380
322
  "recapture_form": getattr(self.recapture_form, "value", "inside-out"),
381
323
  "dps": self.precision,
382
324
  }
383
325
 
384
326
  match self.agg_method:
385
327
  case UPPAggrSelector.DIS:
386
- _upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
387
- _upp_agg_kwargs |= {"agg_method": "distance", "weighting": None}
328
+ upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
329
+ upp_agg_kwargs |= {"agg_method": "distance", "weighting": None}
388
330
  case UPPAggrSelector.AVG:
389
- _upp_agg_fn = gbfn.shrratio_boundary_xact_avg # type: ignore
331
+ upp_agg_fn = gbfn.shrratio_boundary_xact_avg # type: ignore
390
332
  case UPPAggrSelector.MAX:
391
- _upp_agg_fn = gbfn.shrratio_boundary_max # type: ignore
392
- _upp_agg_kwargs = {"dps": 10} # replace here
333
+ upp_agg_fn = gbfn.shrratio_boundary_max # type: ignore
334
+ upp_agg_kwargs = {"dps": 10} # replace here
393
335
  case UPPAggrSelector.MIN:
394
- _upp_agg_fn = gbfn.shrratio_boundary_min # type: ignore
395
- _upp_agg_kwargs |= {"dps": 10} # update here
336
+ upp_agg_fn = gbfn.shrratio_boundary_min # type: ignore
337
+ upp_agg_kwargs |= {"dps": 10} # update here
396
338
  case _:
397
- _upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
339
+ upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
398
340
 
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"
341
+ aggregator_: Literal["arithmetic mean", "geometric mean", "distance"]
342
+ if self.agg_method.value.endswith("geometric mean"):
343
+ aggregator_ = "geometric mean"
344
+ elif self.agg_method.value.endswith("average"):
345
+ aggregator_ = "arithmetic mean"
404
346
  else:
405
- _aggregator = "distance"
347
+ aggregator_ = "distance"
406
348
 
407
- _wgt_type: Literal["cross-product-share", "own-share", None]
349
+ wgt_type: Literal["cross-product-share", "own-share", None]
408
350
  if self.agg_method.value.startswith("cross-product-share"):
409
- _wgt_type = "cross-product-share"
351
+ wgt_type = "cross-product-share"
410
352
  elif self.agg_method.value.startswith("own-share"):
411
- _wgt_type = "own-share"
353
+ wgt_type = "own-share"
412
354
  else:
413
- _wgt_type = None
414
-
415
- _upp_agg_kwargs |= {"agg_method": _aggregator, "weighting": _wgt_type}
416
-
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
355
+ wgt_type = None
356
+
357
+ upp_agg_kwargs |= {"agg_method": aggregator_, "weighting": wgt_type}
434
358
 
435
- @classmethod
436
- def from_yaml(
437
- cls, _c: yaml.constructor.SafeConstructor, _n: yaml.MappingNode
438
- ) -> DiversionRatioBoundary:
439
- return cls(**_c.construct_mapping(_n))
359
+ boundary_ = upp_agg_fn(share_ratio, self.recapture_ratio, **upp_agg_kwargs)
360
+ object.__setattr__(self, "area", boundary_.area)
361
+ object.__setattr__(self, "coordinates", boundary_.coordinates)
440
362
 
441
363
 
442
364
  def guppi_from_delta(
@@ -445,7 +367,7 @@ def guppi_from_delta(
445
367
  *,
446
368
  m_star: float = 1.00,
447
369
  r_bar: float = DEFAULT_REC_RATIO,
448
- ) -> decimal.Decimal:
370
+ ) -> float:
449
371
  """
450
372
  Translate ∆HHI bound to GUPPI bound.
451
373
 
@@ -471,13 +393,13 @@ def guppi_from_delta(
471
393
 
472
394
 
473
395
  def critical_share_ratio(
474
- _guppi_bound: float | decimal.Decimal = 0.075,
396
+ _guppi_bound: float = 0.075,
475
397
  /,
476
398
  *,
477
399
  m_star: float = 1.00,
478
400
  r_bar: float = 1.00,
479
401
  frac: float = 1e-16,
480
- ) -> decimal.Decimal:
402
+ ) -> float:
481
403
  """
482
404
  Corollary to GUPPI bound.
483
405
 
@@ -496,18 +418,16 @@ def critical_share_ratio(
496
418
  for given margin and recapture ratio.
497
419
 
498
420
  """
499
- return gbfn.round_cust(
500
- mpf(f"{_guppi_bound}") / mp.fmul(f"{m_star}", f"{r_bar}"), frac=frac
501
- )
421
+ return gbfn.round_cust(_guppi_bound / (m_star * r_bar), frac=frac)
502
422
 
503
423
 
504
424
  def share_from_guppi(
505
- _guppi_bound: float | decimal.Decimal = 0.065,
425
+ _guppi_bound: float = 0.065,
506
426
  /,
507
427
  *,
508
428
  m_star: float = 1.00,
509
429
  r_bar: float = DEFAULT_REC_RATIO,
510
- ) -> decimal.Decimal:
430
+ ) -> float:
511
431
  """
512
432
  Symmetric-firm share for given GUPPI, margin, and recapture ratio.
513
433
 
@@ -538,3 +458,12 @@ if __name__ == "__main__":
538
458
  print(
539
459
  "This module defines classes with methods for generating boundaries for concentration and diversion-ratio screens."
540
460
  )
461
+
462
+
463
+ for _typ in (
464
+ ConcentrationBoundary,
465
+ DiversionRatioBoundary,
466
+ GuidelinesThresholds,
467
+ HMGThresholds,
468
+ ):
469
+ yamelize_attrs(_typ)