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.
- mergeron/__init__.py +74 -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 +62 -121
- mergeron/core/guidelines_boundary_functions.py +207 -384
- mergeron/core/guidelines_boundary_functions_extra.py +264 -104
- mergeron/core/pseudorandom_numbers.py +76 -67
- 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 +123 -161
- mergeron/gen/data_generation.py +183 -149
- mergeron/gen/data_generation_functions.py +220 -237
- mergeron/gen/enforcement_stats.py +83 -115
- mergeron/gen/upp_tests.py +118 -193
- {mergeron-2025.739290.2.dist-info → mergeron-2025.739290.4.dist-info}/METADATA +2 -3
- mergeron-2025.739290.4.dist-info/RECORD +24 -0
- {mergeron-2025.739290.2.dist-info → mergeron-2025.739290.4.dist-info}/WHEEL +1 -1
- mergeron/data/damodaran_margin_data_dict.msgpack +0 -0
- mergeron-2025.739290.2.dist-info/RECORD +0 -23
|
@@ -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
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
_fc := int(np.ceil(1 /
|
|
129
|
-
_r :=
|
|
130
|
-
_g :=
|
|
131
|
-
_dr :=
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
152
|
+
cls, _r: yaml.representer.RoundTripRepresenter, _d: GuidelinesThresholds
|
|
171
153
|
) -> yaml.MappingNode:
|
|
172
|
-
|
|
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
|
|
157
|
+
return ret
|
|
177
158
|
|
|
178
159
|
@classmethod
|
|
179
160
|
def from_yaml(
|
|
180
|
-
cls, _c: yaml.constructor.
|
|
161
|
+
cls, _c: yaml.constructor.RoundTripConstructor, _n: yaml.MappingNode
|
|
181
162
|
) -> GuidelinesThresholds:
|
|
182
|
-
return cls(**_c
|
|
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
|
-
|
|
211
|
+
conc_fn = gbfn.hhi_delta_boundary
|
|
232
212
|
case "Combined share":
|
|
233
|
-
|
|
213
|
+
conc_fn = gbfn.combined_share_boundary
|
|
234
214
|
case "Pre-merger HHI Contribution":
|
|
235
|
-
|
|
215
|
+
conc_fn = gbfn.hhi_pre_contrib_boundary
|
|
236
216
|
case "Post-merger HHI Contribution":
|
|
237
|
-
|
|
217
|
+
conc_fn = gbfn.hhi_post_contrib_boundary
|
|
238
218
|
|
|
239
|
-
|
|
240
|
-
object.__setattr__(self, "area",
|
|
241
|
-
object.__setattr__(self, "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
|
-
|
|
335
|
+
share_ratio = critical_share_ratio(
|
|
377
336
|
self.diversion_ratio, r_bar=self.recapture_ratio
|
|
378
337
|
)
|
|
379
|
-
|
|
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
|
-
|
|
387
|
-
|
|
345
|
+
upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
|
|
346
|
+
upp_agg_kwargs |= {"agg_method": "distance", "weighting": None}
|
|
388
347
|
case UPPAggrSelector.AVG:
|
|
389
|
-
|
|
348
|
+
upp_agg_fn = gbfn.shrratio_boundary_xact_avg # type: ignore
|
|
390
349
|
case UPPAggrSelector.MAX:
|
|
391
|
-
|
|
392
|
-
|
|
350
|
+
upp_agg_fn = gbfn.shrratio_boundary_max # type: ignore
|
|
351
|
+
upp_agg_kwargs = {"dps": 10} # replace here
|
|
393
352
|
case UPPAggrSelector.MIN:
|
|
394
|
-
|
|
395
|
-
|
|
353
|
+
upp_agg_fn = gbfn.shrratio_boundary_min # type: ignore
|
|
354
|
+
upp_agg_kwargs |= {"dps": 10} # update here
|
|
396
355
|
case _:
|
|
397
|
-
|
|
356
|
+
upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
|
|
398
357
|
|
|
399
|
-
|
|
400
|
-
if self.agg_method.value.endswith("
|
|
401
|
-
|
|
402
|
-
elif self.agg_method.value.endswith("
|
|
403
|
-
|
|
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
|
-
|
|
364
|
+
aggregator_ = "distance"
|
|
406
365
|
|
|
407
|
-
|
|
366
|
+
wgt_type: Literal["cross-product-share", "own-share", None]
|
|
408
367
|
if self.agg_method.value.startswith("cross-product-share"):
|
|
409
|
-
|
|
368
|
+
wgt_type = "cross-product-share"
|
|
410
369
|
elif self.agg_method.value.startswith("own-share"):
|
|
411
|
-
|
|
370
|
+
wgt_type = "own-share"
|
|
412
371
|
else:
|
|
413
|
-
|
|
372
|
+
wgt_type = None
|
|
414
373
|
|
|
415
|
-
|
|
374
|
+
upp_agg_kwargs |= {"agg_method": aggregator_, "weighting": wgt_type}
|
|
416
375
|
|
|
417
|
-
|
|
418
|
-
object.__setattr__(self, "area",
|
|
419
|
-
object.__setattr__(self, "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
|
-
) ->
|
|
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
|
|
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
|
-
) ->
|
|
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
|
|
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
|
-
) ->
|
|
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"})
|