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.
- mergeron/__init__.py +103 -48
- mergeron/core/__init__.py +105 -4
- mergeron/core/empirical_margin_distribution.py +100 -78
- mergeron/core/ftc_merger_investigations_data.py +309 -316
- mergeron/core/guidelines_boundaries.py +67 -138
- mergeron/core/guidelines_boundary_functions.py +202 -379
- mergeron/core/guidelines_boundary_functions_extra.py +264 -106
- mergeron/core/pseudorandom_numbers.py +73 -64
- mergeron/data/damodaran_margin_data_serialized.zip +0 -0
- mergeron/data/ftc_invdata.zip +0 -0
- mergeron/demo/visualize_empirical_margin_distribution.py +9 -7
- mergeron/gen/__init__.py +138 -161
- mergeron/gen/data_generation.py +181 -149
- mergeron/gen/data_generation_functions.py +220 -237
- mergeron/gen/enforcement_stats.py +78 -109
- mergeron/gen/upp_tests.py +119 -194
- {mergeron-2025.739290.3.dist-info → mergeron-2025.739290.5.dist-info}/METADATA +2 -3
- mergeron-2025.739290.5.dist-info/RECORD +24 -0
- {mergeron-2025.739290.3.dist-info → mergeron-2025.739290.5.dist-info}/WHEEL +1 -1
- mergeron/data/damodaran_margin_data_dict.msgpack +0 -0
- mergeron-2025.739290.3.dist-info/RECORD +0 -23
|
@@ -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
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
_fc := int(np.ceil(1 /
|
|
129
|
-
_r :=
|
|
130
|
-
_g :=
|
|
131
|
-
_dr :=
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
194
|
-
"
|
|
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
|
-
"
|
|
205
|
-
"
|
|
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
|
-
|
|
194
|
+
conc_fn = gbfn.hhi_delta_boundary
|
|
232
195
|
case "Combined share":
|
|
233
|
-
|
|
234
|
-
case "
|
|
235
|
-
|
|
236
|
-
case "
|
|
237
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
318
|
+
share_ratio = critical_share_ratio(
|
|
377
319
|
self.diversion_ratio, r_bar=self.recapture_ratio
|
|
378
320
|
)
|
|
379
|
-
|
|
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
|
-
|
|
387
|
-
|
|
328
|
+
upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
|
|
329
|
+
upp_agg_kwargs |= {"agg_method": "distance", "weighting": None}
|
|
388
330
|
case UPPAggrSelector.AVG:
|
|
389
|
-
|
|
331
|
+
upp_agg_fn = gbfn.shrratio_boundary_xact_avg # type: ignore
|
|
390
332
|
case UPPAggrSelector.MAX:
|
|
391
|
-
|
|
392
|
-
|
|
333
|
+
upp_agg_fn = gbfn.shrratio_boundary_max # type: ignore
|
|
334
|
+
upp_agg_kwargs = {"dps": 10} # replace here
|
|
393
335
|
case UPPAggrSelector.MIN:
|
|
394
|
-
|
|
395
|
-
|
|
336
|
+
upp_agg_fn = gbfn.shrratio_boundary_min # type: ignore
|
|
337
|
+
upp_agg_kwargs |= {"dps": 10} # update here
|
|
396
338
|
case _:
|
|
397
|
-
|
|
339
|
+
upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
|
|
398
340
|
|
|
399
|
-
|
|
400
|
-
if self.agg_method.value.endswith("
|
|
401
|
-
|
|
402
|
-
elif self.agg_method.value.endswith("
|
|
403
|
-
|
|
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
|
-
|
|
347
|
+
aggregator_ = "distance"
|
|
406
348
|
|
|
407
|
-
|
|
349
|
+
wgt_type: Literal["cross-product-share", "own-share", None]
|
|
408
350
|
if self.agg_method.value.startswith("cross-product-share"):
|
|
409
|
-
|
|
351
|
+
wgt_type = "cross-product-share"
|
|
410
352
|
elif self.agg_method.value.startswith("own-share"):
|
|
411
|
-
|
|
353
|
+
wgt_type = "own-share"
|
|
412
354
|
else:
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
) ->
|
|
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
|
|
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
|
-
) ->
|
|
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
|
|
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
|
-
) ->
|
|
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)
|