mergeron 2024.738963.0__py3-none-any.whl → 2025.739265.0__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 +26 -6
- mergeron/core/__init__.py +5 -65
- mergeron/core/{damodaran_margin_data.py → empirical_margin_distribution.py} +74 -58
- mergeron/core/ftc_merger_investigations_data.py +142 -93
- mergeron/core/guidelines_boundaries.py +289 -1077
- mergeron/core/guidelines_boundary_functions.py +1128 -0
- mergeron/core/{guidelines_boundaries_specialized_functions.py → guidelines_boundary_functions_extra.py} +76 -42
- mergeron/core/pseudorandom_numbers.py +16 -22
- mergeron/data/__init__.py +3 -0
- mergeron/data/damodaran_margin_data.xls +0 -0
- mergeron/data/damodaran_margin_data_dict.msgpack +0 -0
- mergeron/demo/__init__.py +3 -0
- mergeron/demo/visualize_empirical_margin_distribution.py +86 -0
- mergeron/gen/__init__.py +257 -245
- mergeron/gen/data_generation.py +473 -221
- mergeron/gen/data_generation_functions.py +876 -0
- mergeron/gen/enforcement_stats.py +355 -0
- mergeron/gen/upp_tests.py +159 -259
- mergeron-2025.739265.0.dist-info/METADATA +115 -0
- mergeron-2025.739265.0.dist-info/RECORD +23 -0
- {mergeron-2024.738963.0.dist-info → mergeron-2025.739265.0.dist-info}/WHEEL +1 -1
- mergeron/License.txt +0 -16
- mergeron/core/InCommon RSA Server CA cert chain.pem +0 -68
- mergeron/core/excel_helper.py +0 -259
- mergeron/core/proportions_tests.py +0 -520
- mergeron/ext/__init__.py +0 -5
- mergeron/ext/tol_colors.py +0 -851
- mergeron/gen/_data_generation_functions_nonpublic.py +0 -621
- mergeron/gen/investigations_stats.py +0 -709
- mergeron/jinja_LaTex_templates/clrrate_cis_summary_table_template.tex.jinja2 +0 -121
- mergeron/jinja_LaTex_templates/ftcinvdata_byhhianddelta_table_template.tex.jinja2 +0 -82
- mergeron/jinja_LaTex_templates/ftcinvdata_summary_table_template.tex.jinja2 +0 -57
- mergeron/jinja_LaTex_templates/ftcinvdata_summarypaired_table_template.tex.jinja2 +0 -104
- mergeron/jinja_LaTex_templates/mergeron.cls +0 -161
- mergeron/jinja_LaTex_templates/mergeron_table_collection_template.tex.jinja2 +0 -90
- mergeron/jinja_LaTex_templates/setup_tikz_tables.tex.jinja2 +0 -84
- mergeron-2024.738963.0.dist-info/METADATA +0 -108
- mergeron-2024.738963.0.dist-info/RECORD +0 -30
- /mergeron/{core → data}/ftc_invdata.msgpack +0 -0
|
@@ -4,32 +4,37 @@ with a canvas on which to draw boundaries for Guidelines standards.
|
|
|
4
4
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
7
9
|
import decimal
|
|
8
|
-
from collections.abc import Callable
|
|
9
10
|
from dataclasses import dataclass
|
|
10
|
-
from
|
|
11
|
-
from typing import Any, Literal, TypeAlias
|
|
11
|
+
from typing import Literal
|
|
12
12
|
|
|
13
13
|
import numpy as np
|
|
14
|
-
from attrs import
|
|
14
|
+
from attrs import Attribute, field, frozen, validators
|
|
15
15
|
from mpmath import mp, mpf # type: ignore
|
|
16
|
-
from numpy.typing import NDArray
|
|
17
16
|
|
|
18
|
-
from .. import
|
|
19
|
-
|
|
17
|
+
from .. import ( # noqa: TID252
|
|
18
|
+
DEFAULT_REC_RATIO,
|
|
19
|
+
VERSION,
|
|
20
|
+
ArrayDouble,
|
|
21
|
+
HMGPubYear,
|
|
22
|
+
RECForm,
|
|
23
|
+
UPPAggrSelector,
|
|
24
|
+
)
|
|
25
|
+
from . import guidelines_boundary_functions as gbfn
|
|
20
26
|
|
|
21
|
-
__version__ =
|
|
27
|
+
__version__ = VERSION
|
|
22
28
|
|
|
23
29
|
|
|
24
|
-
mp.
|
|
30
|
+
mp.dps = 32
|
|
25
31
|
mp.trap_complex = True
|
|
26
32
|
|
|
27
|
-
HMGPubYear: TypeAlias = Literal[1992, 2010, 2023]
|
|
28
|
-
|
|
29
33
|
|
|
30
|
-
@dataclass(
|
|
34
|
+
@dataclass(frozen=True)
|
|
31
35
|
class HMGThresholds:
|
|
32
36
|
delta: float
|
|
37
|
+
fc: float
|
|
33
38
|
rec: float
|
|
34
39
|
guppi: float
|
|
35
40
|
divr: float
|
|
@@ -37,67 +42,62 @@ class HMGThresholds:
|
|
|
37
42
|
ipr: float
|
|
38
43
|
|
|
39
44
|
|
|
40
|
-
@
|
|
41
|
-
class GuidelinesBoundary:
|
|
42
|
-
coordinates: NDArray[np.float64]
|
|
43
|
-
area: float
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@dataclass(slots=True, frozen=True)
|
|
47
|
-
class GuidelinesBoundaryCallable:
|
|
48
|
-
boundary_function: Callable[[NDArray[np.float64]], NDArray[np.float64]]
|
|
49
|
-
area: float
|
|
50
|
-
s_naught: float = 0
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@define(slots=True, frozen=True)
|
|
45
|
+
@frozen
|
|
54
46
|
class GuidelinesThresholds:
|
|
55
47
|
"""
|
|
56
48
|
Guidelines threholds by Guidelines publication year
|
|
57
49
|
|
|
58
|
-
ΔHHI, Recapture
|
|
59
|
-
constructed from concentration standards
|
|
50
|
+
ΔHHI, Recapture Ratio, GUPPI, Diversion ratio, CMCR, and IPR thresholds
|
|
51
|
+
constructed from concentration standards in Guidelines published in
|
|
52
|
+
1982, 1984, 1992, 2010, and 2023.
|
|
53
|
+
|
|
60
54
|
"""
|
|
61
55
|
|
|
62
|
-
pub_year: HMGPubYear
|
|
56
|
+
pub_year: HMGPubYear = field(
|
|
57
|
+
kw_only=False,
|
|
58
|
+
default=2023,
|
|
59
|
+
validator=validators.in_([1982, 1984, 1992, 2010, 2023]),
|
|
60
|
+
)
|
|
63
61
|
"""
|
|
64
|
-
Year of publication of the
|
|
62
|
+
Year of publication of the Guidelines
|
|
65
63
|
"""
|
|
66
64
|
|
|
67
65
|
safeharbor: HMGThresholds = field(kw_only=True, default=None)
|
|
68
66
|
"""
|
|
69
67
|
Negative presumption quantified on various measures
|
|
70
68
|
|
|
71
|
-
ΔHHI safeharbor bound, default recapture
|
|
69
|
+
ΔHHI safeharbor bound, default recapture ratio, GUPPI bound,
|
|
72
70
|
diversion ratio limit, CMCR, and IPR
|
|
73
71
|
"""
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
presumption: HMGThresholds = field(kw_only=True, default=None)
|
|
76
74
|
"""
|
|
77
|
-
Presumption of harm
|
|
75
|
+
Presumption of harm defined in HMG
|
|
78
76
|
|
|
79
|
-
ΔHHI bound
|
|
80
|
-
|
|
81
|
-
GUPPI bound, diversion ratio limit, CMCR, and IPR
|
|
77
|
+
ΔHHI bound and corresponding default recapture ratio, GUPPI bound,
|
|
78
|
+
diversion ratio limit, CMCR, and IPR
|
|
82
79
|
"""
|
|
83
80
|
|
|
84
|
-
|
|
81
|
+
imputed_presumption: HMGThresholds = field(kw_only=True, default=None)
|
|
85
82
|
"""
|
|
86
|
-
Presumption of harm
|
|
83
|
+
Presumption of harm imputed from guidelines
|
|
87
84
|
|
|
88
|
-
ΔHHI bound
|
|
89
|
-
|
|
85
|
+
ΔHHI bound inferred from strict numbers-equivalent
|
|
86
|
+
of (post-merger) HHI presumption, and corresponding default recapture ratio,
|
|
87
|
+
GUPPI bound, diversion ratio limit, CMCR, and IPR
|
|
90
88
|
"""
|
|
91
89
|
|
|
92
90
|
def __attrs_post_init__(self, /) -> None:
|
|
93
|
-
# In the 2023
|
|
91
|
+
# In the 2023 Guidelines, the agencies do not define a
|
|
94
92
|
# negative presumption, or safeharbor. Practically speaking,
|
|
95
93
|
# given resource constraints and loss aversion, it is likely
|
|
96
94
|
# that staff only investigates mergers that meet the presumption;
|
|
97
95
|
# thus, here, the tentative delta safeharbor under
|
|
98
96
|
# the 2023 Guidelines is 100 points
|
|
99
97
|
_hhi_p, _dh_s, _dh_p = {
|
|
100
|
-
|
|
98
|
+
1982: (_s1982 := (0.18, 0.005, 0.01)),
|
|
99
|
+
1984: _s1982,
|
|
100
|
+
1992: _s1982,
|
|
101
101
|
2010: (0.25, 0.01, 0.02),
|
|
102
102
|
2023: (0.18, 0.01, 0.01),
|
|
103
103
|
}[self.pub_year]
|
|
@@ -107,1143 +107,355 @@ class GuidelinesThresholds:
|
|
|
107
107
|
"safeharbor",
|
|
108
108
|
HMGThresholds(
|
|
109
109
|
_dh_s,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
_fc := int(np.ceil(1 / _hhi_p)),
|
|
111
|
+
_r := float(_r_s := gbfn.round_cust(_fc / (_fc + 1), frac=0.05)),
|
|
112
|
+
_g := float(guppi_from_delta(_dh_s, m_star=1.0, r_bar=_r)),
|
|
113
|
+
_dr := float(1 - _r_s),
|
|
113
114
|
_cmcr := 0.03, # Not strictly a Guidelines standard
|
|
114
|
-
_ipr :=
|
|
115
|
+
_ipr := _g, # Not strictly a Guidelines standard
|
|
115
116
|
),
|
|
116
117
|
)
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
object.__setattr__(
|
|
120
|
+
self, "presumption", HMGThresholds(_dh_p, _fc, _r, _g, _dr, _cmcr, _ipr)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# imputed_presumption is relevant for presumptions implicating
|
|
124
|
+
# mergers *to* symmetry in numbers-equivalent of post-merger HHI
|
|
125
|
+
# as in 2010 U.S.Guidelines
|
|
119
126
|
object.__setattr__(
|
|
120
127
|
self,
|
|
121
128
|
"imputed_presumption",
|
|
122
129
|
(
|
|
123
130
|
HMGThresholds(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
2 * (0.5 / _fc) ** 2,
|
|
132
|
+
_fc,
|
|
133
|
+
float(
|
|
134
|
+
_r_i := gbfn.round_cust(
|
|
135
|
+
(_fc - 1 / 2) / (_fc + 1 / 2), frac=0.05
|
|
136
|
+
)
|
|
137
|
+
),
|
|
138
|
+
_g,
|
|
139
|
+
float((1 - _r_i) / 2),
|
|
128
140
|
_cmcr,
|
|
129
|
-
|
|
141
|
+
_ipr := _g,
|
|
130
142
|
)
|
|
131
143
|
if self.pub_year == 2010
|
|
132
144
|
else HMGThresholds(
|
|
133
|
-
|
|
134
|
-
_r,
|
|
135
|
-
_g_i := gbd_from_dsf(_dh_i, m_star=1.0, r_bar=_r),
|
|
136
|
-
_dr,
|
|
137
|
-
_cmcr,
|
|
138
|
-
_g_i,
|
|
145
|
+
2 * (1 / (_fc + 1)) ** 2, _fc, _r, _g, _dr, _cmcr, _ipr
|
|
139
146
|
)
|
|
140
147
|
),
|
|
141
148
|
)
|
|
142
149
|
|
|
143
|
-
object.__setattr__(
|
|
144
|
-
self,
|
|
145
|
-
"presumption",
|
|
146
|
-
HMGThresholds(
|
|
147
|
-
_dh_p,
|
|
148
|
-
_r,
|
|
149
|
-
_g_p := gbd_from_dsf(_dh_p, m_star=1.0, r_bar=_r),
|
|
150
|
-
_dr,
|
|
151
|
-
_cmcr,
|
|
152
|
-
_ipr := _g_p,
|
|
153
|
-
),
|
|
154
|
-
)
|
|
155
150
|
|
|
151
|
+
@frozen
|
|
152
|
+
class ConcentrationBoundary:
|
|
153
|
+
"""Concentration parameters, boundary coordinates, and area under concentration boundary."""
|
|
156
154
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
*,
|
|
161
|
-
frac: float = 0.005,
|
|
162
|
-
rounding_mode: str = "ROUND_HALF_UP",
|
|
163
|
-
) -> float:
|
|
164
|
-
"""
|
|
165
|
-
Custom rounding, to the nearest 0.5% by default.
|
|
155
|
+
measure_name: Literal[
|
|
156
|
+
"ΔHHI", "Combined share", "Pre-merger HHI", "Post-merger HHI"
|
|
157
|
+
] = field(kw_only=False, default="ΔHHI")
|
|
166
158
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
The given number, rounded as specified.
|
|
179
|
-
|
|
180
|
-
Raises
|
|
181
|
-
------
|
|
182
|
-
ValueError
|
|
183
|
-
If rounding mode is not defined in the :code:`decimal` package.
|
|
184
|
-
|
|
185
|
-
Notes
|
|
186
|
-
-----
|
|
187
|
-
Integer-round the quotient, :code:`(_num / frac)` using the specified
|
|
188
|
-
rounding mode. Return the product of the rounded quotient times
|
|
189
|
-
the specified precision, :code:`frac`.
|
|
190
|
-
|
|
191
|
-
"""
|
|
192
|
-
|
|
193
|
-
if rounding_mode not in (
|
|
194
|
-
decimal.ROUND_05UP,
|
|
195
|
-
decimal.ROUND_CEILING,
|
|
196
|
-
decimal.ROUND_DOWN,
|
|
197
|
-
decimal.ROUND_FLOOR,
|
|
198
|
-
decimal.ROUND_HALF_DOWN,
|
|
199
|
-
decimal.ROUND_HALF_EVEN,
|
|
200
|
-
decimal.ROUND_HALF_UP,
|
|
201
|
-
decimal.ROUND_UP,
|
|
202
|
-
):
|
|
203
|
-
raise ValueError(
|
|
204
|
-
f"Value, {f'"{rounding_mode}"'} is invalid for rounding_mode."
|
|
205
|
-
"Documentation for the, \"decimal\" built-in lists valid rounding modes."
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
_n, _f, _e = (decimal.Decimal(f"{_g}") for _g in [_num, frac, 1])
|
|
209
|
-
|
|
210
|
-
return float(_f * (_n / _f).quantize(_e, rounding=rounding_mode))
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def lerp(
|
|
214
|
-
_x1: int | float | mpf | NDArray[np.float64 | np.int64] = 3,
|
|
215
|
-
_x2: int | float | mpf | NDArray[np.float64 | np.int64] = 1,
|
|
216
|
-
_r: float | mpf = 0.25,
|
|
217
|
-
/,
|
|
218
|
-
) -> float | mpf | NDArray[np.float64]:
|
|
219
|
-
"""
|
|
220
|
-
From the function of the same name in the C++ standard [2]_
|
|
221
|
-
|
|
222
|
-
Constructs the weighted average, :math:`w_1 x_1 + w_2 x_2`, where
|
|
223
|
-
:math:`w_1 = 1 - r` and :math:`w_2 = r`.
|
|
224
|
-
|
|
225
|
-
Parameters
|
|
226
|
-
----------
|
|
227
|
-
_x1, _x2
|
|
228
|
-
bounds :math:`x_1, x_2` to interpolate between.
|
|
229
|
-
_r
|
|
230
|
-
interpolation weight :math:`r` assigned to :math:`x_2`
|
|
231
|
-
|
|
232
|
-
Returns
|
|
233
|
-
-------
|
|
234
|
-
The linear interpolation, or weighted average,
|
|
235
|
-
:math:`x_1 + r \\cdot (x_1 - x_2) \\equiv (1 - r) \\cdot x_1 + r \\cdot x_2`.
|
|
236
|
-
|
|
237
|
-
Raises
|
|
238
|
-
------
|
|
239
|
-
ValueError
|
|
240
|
-
If the interpolation weight is not in the interval, :math:`[0, 1]`.
|
|
241
|
-
|
|
242
|
-
References
|
|
243
|
-
----------
|
|
159
|
+
@measure_name.validator # pyright: ignore
|
|
160
|
+
def __mnv(
|
|
161
|
+
_instance: ConcentrationBoundary, _attribute: Attribute[str], _value: str, /
|
|
162
|
+
) -> None:
|
|
163
|
+
if _value not in (
|
|
164
|
+
"ΔHHI",
|
|
165
|
+
"Combined share",
|
|
166
|
+
"Pre-merger HHI",
|
|
167
|
+
"Post-merger HHI",
|
|
168
|
+
):
|
|
169
|
+
raise ValueError(f"Invalid name for a concentration measure, {_value!r}.")
|
|
244
170
|
|
|
245
|
-
|
|
171
|
+
threshold: float = field(kw_only=False, default=0.01)
|
|
246
172
|
|
|
247
|
-
|
|
173
|
+
@threshold.validator # pyright: ignore
|
|
174
|
+
def __tv(
|
|
175
|
+
_instance: ConcentrationBoundary, _attribute: Attribute[float], _value: float, /
|
|
176
|
+
) -> None:
|
|
177
|
+
if not 0 <= _value <= 1:
|
|
178
|
+
raise ValueError("Concentration threshold must lie between 0 and 1.")
|
|
248
179
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
elif _r == 0:
|
|
252
|
-
return _x1
|
|
253
|
-
elif _r == 1:
|
|
254
|
-
return _x2
|
|
255
|
-
elif _r == 0.5:
|
|
256
|
-
return 1 / 2 * (_x1 + _x2)
|
|
257
|
-
else:
|
|
258
|
-
return _r * _x2 + (1 - _r) * _x1
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def gbd_from_dsf(
|
|
262
|
-
_deltasf: float = 0.01, /, *, m_star: float = 1.00, r_bar: float = 0.80
|
|
263
|
-
) -> float:
|
|
264
|
-
"""
|
|
265
|
-
Translate ∆HHI bound to GUPPI bound.
|
|
266
|
-
|
|
267
|
-
Parameters
|
|
268
|
-
----------
|
|
269
|
-
_deltasf
|
|
270
|
-
Specified ∆HHI bound.
|
|
271
|
-
m_star
|
|
272
|
-
Parametric price-cost margin.
|
|
273
|
-
r_bar
|
|
274
|
-
Default recapture rate.
|
|
275
|
-
|
|
276
|
-
Returns
|
|
277
|
-
-------
|
|
278
|
-
GUPPI bound corresponding to ∆HHI bound, at given margin and recapture rate.
|
|
279
|
-
|
|
280
|
-
"""
|
|
281
|
-
return round_cust(
|
|
282
|
-
m_star * r_bar * (_s_m := np.sqrt(_deltasf / 2)) / (1 - _s_m),
|
|
283
|
-
frac=0.005,
|
|
284
|
-
rounding_mode="ROUND_HALF_DOWN",
|
|
180
|
+
precision: int = field(
|
|
181
|
+
kw_only=False, default=5, validator=validators.instance_of(int)
|
|
285
182
|
)
|
|
286
183
|
|
|
184
|
+
coordinates: ArrayDouble = field(init=False, kw_only=True)
|
|
185
|
+
"""Market-share pairs as Cartesian coordinates of points on the concentration boundary."""
|
|
287
186
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
/,
|
|
291
|
-
*,
|
|
292
|
-
m_star: float = 1.00,
|
|
293
|
-
r_bar: float = 0.80,
|
|
294
|
-
frac: float = 1e-16,
|
|
295
|
-
) -> mpf:
|
|
296
|
-
"""
|
|
297
|
-
Corollary to GUPPI bound.
|
|
187
|
+
area: float = field(init=False, kw_only=True)
|
|
188
|
+
"""Area under the concentration boundary."""
|
|
298
189
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
-------
|
|
310
|
-
Critical share ratio (share ratio bound) corresponding to the GUPPI bound
|
|
311
|
-
for given margin and recapture rate.
|
|
190
|
+
def __attrs_post_init__(self, /) -> None:
|
|
191
|
+
match self.measure_name:
|
|
192
|
+
case "ΔHHI":
|
|
193
|
+
_conc_fn = gbfn.hhi_delta_boundary
|
|
194
|
+
case "Combined share":
|
|
195
|
+
_conc_fn = gbfn.combined_share_boundary
|
|
196
|
+
case "Pre-merger HHI":
|
|
197
|
+
_conc_fn = gbfn.hhi_pre_contrib_boundary
|
|
198
|
+
case "Post-merger HHI":
|
|
199
|
+
_conc_fn = gbfn.hhi_post_contrib_boundary
|
|
312
200
|
|
|
313
|
-
|
|
314
|
-
|
|
201
|
+
_boundary = _conc_fn(self.threshold, dps=self.precision)
|
|
202
|
+
object.__setattr__(self, "coordinates", _boundary.coordinates)
|
|
203
|
+
object.__setattr__(self, "area", _boundary.area)
|
|
315
204
|
|
|
316
205
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
) -> float:
|
|
206
|
+
@frozen
|
|
207
|
+
class DiversionRatioBoundary:
|
|
320
208
|
"""
|
|
321
|
-
|
|
209
|
+
Diversion ratio specification, boundary coordinates, and area under boundary.
|
|
322
210
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
Default recapture rate.
|
|
331
|
-
|
|
332
|
-
Returns
|
|
333
|
-
-------
|
|
334
|
-
float
|
|
335
|
-
Symmetric firm market share on GUPPI boundary, for given margin and
|
|
336
|
-
recapture rate.
|
|
211
|
+
Along with the default diversion ratio and recapture ratio,
|
|
212
|
+
a diversion ratio boundary specification includes the recapture form --
|
|
213
|
+
whether fixed for both merging firms' products ("proportional") or
|
|
214
|
+
consistent with share-proportionality, i.e., "inside-out";
|
|
215
|
+
the method of aggregating diversion ratios for the two products, and
|
|
216
|
+
the precision for the estimate of area under the divertion ratio boundary
|
|
217
|
+
(also defines the number of points on the boundary).
|
|
337
218
|
|
|
338
219
|
"""
|
|
339
220
|
|
|
340
|
-
|
|
341
|
-
(_d0 := critical_shrratio(_gbd, m_star=m_star, r_bar=r_bar)) / (1 + _d0)
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def boundary_plot(*, mktshares_plot_flag: bool = True) -> tuple[Any, ...]:
|
|
346
|
-
"""Setup basic figure and axes for plots of safe harbor boundaries.
|
|
221
|
+
diversion_ratio: float = field(kw_only=False, default=0.065)
|
|
347
222
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
import matplotlib.patches as mpp
|
|
354
|
-
import matplotlib.ticker as mpt
|
|
355
|
-
|
|
356
|
-
mpl.use("pgf")
|
|
357
|
-
import matplotlib.pyplot as plt
|
|
358
|
-
# from matplotlib.backends.backend_pgf import FigureCanvasPgf
|
|
359
|
-
|
|
360
|
-
# from matplotlib.backends.backend_pgf import FigureCanvasPgf
|
|
361
|
-
# mpl.backend_bases.register_backend("pdf", FigureCanvasPgf)
|
|
362
|
-
# import matplotlib.pyplot as plt
|
|
363
|
-
|
|
364
|
-
plt.rcParams.update({
|
|
365
|
-
"pgf.rcfonts": False,
|
|
366
|
-
"pgf.texsystem": "lualatex",
|
|
367
|
-
"pgf.preamble": "\n".join([
|
|
368
|
-
R"\pdfvariable minorversion=7",
|
|
369
|
-
R"\usepackage{fontspec}",
|
|
370
|
-
R"\usepackage{luacode}",
|
|
371
|
-
R"\begin{luacode}",
|
|
372
|
-
R"local function embedfull(tfmdata)",
|
|
373
|
-
R' tfmdata.embedding = "full"',
|
|
374
|
-
R"end",
|
|
375
|
-
R"",
|
|
376
|
-
R"luatexbase.add_to_callback("
|
|
377
|
-
R' "luaotfload.patch_font", embedfull, "embedfull"'
|
|
378
|
-
R")",
|
|
379
|
-
R"\end{luacode}",
|
|
380
|
-
R"\usepackage{mathtools}",
|
|
381
|
-
R"\usepackage{unicode-math}",
|
|
382
|
-
R"\setmathfont[math-style=ISO]{STIX Two Math}",
|
|
383
|
-
R"\setmainfont{STIX Two Text}",
|
|
384
|
-
r"\setsansfont{Fira Sans Light}",
|
|
385
|
-
R"\setmonofont[Scale=MatchLowercase,]{Fira Mono}",
|
|
386
|
-
R"\defaultfontfeatures[\rmfamily]{",
|
|
387
|
-
R" Ligatures={TeX, Common},",
|
|
388
|
-
R" Numbers={Proportional, Lining},",
|
|
389
|
-
R" }",
|
|
390
|
-
R"\defaultfontfeatures[\sffamily]{",
|
|
391
|
-
R" Ligatures={TeX, Common},",
|
|
392
|
-
R" Numbers={Monospaced, Lining},",
|
|
393
|
-
R" LetterSpace=0.50,",
|
|
394
|
-
R" }",
|
|
395
|
-
R"\usepackage[",
|
|
396
|
-
R" activate={true, nocompatibility},",
|
|
397
|
-
R" tracking=true,",
|
|
398
|
-
R" ]{microtype}",
|
|
399
|
-
]),
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
# Initialize a canvas with a single figure (set of axes)
|
|
403
|
-
_fig = plt.figure(figsize=(5, 5), dpi=600)
|
|
404
|
-
_ax_out = _fig.add_subplot()
|
|
405
|
-
|
|
406
|
-
def _set_axis_def(
|
|
407
|
-
_ax1: mpa.Axes,
|
|
223
|
+
@diversion_ratio.validator
|
|
224
|
+
def __dvv(
|
|
225
|
+
_instance: DiversionRatioBoundary,
|
|
226
|
+
_attribute: Attribute[float],
|
|
227
|
+
_value: float,
|
|
408
228
|
/,
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
# Set the width of axis gridlines, and tick marks:
|
|
414
|
-
# both axes, both major and minor ticks
|
|
415
|
-
# Frame, grid, and facecolor
|
|
416
|
-
for _spos0 in "left", "bottom":
|
|
417
|
-
_ax1.spines[_spos0].set_linewidth(0.5)
|
|
418
|
-
_ax1.spines[_spos0].set_zorder(5)
|
|
419
|
-
for _spos1 in "top", "right":
|
|
420
|
-
_ax1.spines[_spos1].set_linewidth(0.0)
|
|
421
|
-
_ax1.spines[_spos1].set_zorder(0)
|
|
422
|
-
_ax1.spines[_spos1].set_visible(False)
|
|
423
|
-
_ax1.set_facecolor("#E6E6E6")
|
|
424
|
-
|
|
425
|
-
_ax1.grid(linewidth=0.5, linestyle=":", color="grey", zorder=1)
|
|
426
|
-
_ax1.tick_params(axis="x", which="both", width=0.5)
|
|
427
|
-
_ax1.tick_params(axis="y", which="both", width=0.5)
|
|
428
|
-
|
|
429
|
-
# Tick marks skip, size, and rotation
|
|
430
|
-
# x-axis
|
|
431
|
-
plt.setp(
|
|
432
|
-
_ax1.xaxis.get_majorticklabels(),
|
|
433
|
-
horizontalalignment="right",
|
|
434
|
-
fontsize=6,
|
|
435
|
-
rotation=45,
|
|
436
|
-
)
|
|
437
|
-
# y-axis
|
|
438
|
-
plt.setp(
|
|
439
|
-
_ax1.yaxis.get_majorticklabels(), horizontalalignment="right", fontsize=6
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
if mktshares_plot_flag:
|
|
443
|
-
# Axis labels
|
|
444
|
-
if mktshares_axlbls_flag:
|
|
445
|
-
# x-axis
|
|
446
|
-
_ax1.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
|
|
447
|
-
_ax1.xaxis.set_label_coords(0.75, -0.1)
|
|
448
|
-
# y-axis
|
|
449
|
-
_ax1.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
|
|
450
|
-
_ax1.yaxis.set_label_coords(-0.1, 0.75)
|
|
451
|
-
|
|
452
|
-
# Plot the ray of symmetry
|
|
453
|
-
_ax1.plot(
|
|
454
|
-
[0, 1], [0, 1], linewidth=0.5, linestyle=":", color="grey", zorder=1
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
# Axis scale
|
|
458
|
-
_ax1.set_xlim(0, 1)
|
|
459
|
-
_ax1.set_ylim(0, 1)
|
|
460
|
-
_ax1.set_aspect(1.0)
|
|
461
|
-
|
|
462
|
-
# Truncate the axis frame to a triangle:
|
|
463
|
-
_ax1.add_patch(
|
|
464
|
-
mpp.Rectangle(
|
|
465
|
-
xy=(1.0025, 0.00),
|
|
466
|
-
width=1.1 * mp.sqrt(2),
|
|
467
|
-
height=1.1 * mp.sqrt(2),
|
|
468
|
-
angle=45,
|
|
469
|
-
color="white",
|
|
470
|
-
edgecolor=None,
|
|
471
|
-
fill=True,
|
|
472
|
-
clip_on=True,
|
|
473
|
-
zorder=5,
|
|
474
|
-
)
|
|
475
|
-
)
|
|
476
|
-
# Feasible space is bounded by the other diagonal:
|
|
477
|
-
_ax1.plot(
|
|
478
|
-
[0, 1], [1, 0], linestyle="-", linewidth=0.5, color="black", zorder=1
|
|
229
|
+
) -> None:
|
|
230
|
+
if not (isinstance(_value, float) and 0 <= _value <= 1):
|
|
231
|
+
raise ValueError(
|
|
232
|
+
"Margin-adjusted benchmark share ratio must lie between 0 and 1."
|
|
479
233
|
)
|
|
480
234
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
# would lead to a single minor tick between major ticks.
|
|
486
|
-
_minorLocator = mpt.AutoMinorLocator(5)
|
|
487
|
-
_majorLocator = mpt.MultipleLocator(0.05)
|
|
488
|
-
for _axs in _ax1.xaxis, _ax1.yaxis:
|
|
489
|
-
if _axs == _ax1.xaxis:
|
|
490
|
-
_majorticklabels_rot = 45
|
|
491
|
-
elif _axs == _ax1.yaxis:
|
|
492
|
-
_majorticklabels_rot = 0
|
|
493
|
-
# x-axis
|
|
494
|
-
_axs.set_major_locator(_majorLocator)
|
|
495
|
-
_axs.set_minor_locator(_minorLocator)
|
|
496
|
-
# It"s always x when specifying the format
|
|
497
|
-
_axs.set_major_formatter(mpt.StrMethodFormatter("{x:>3.0%}"))
|
|
498
|
-
|
|
499
|
-
# Hide every other tick-label
|
|
500
|
-
for _axl in _ax1.get_xticklabels(), _ax1.get_yticklabels():
|
|
501
|
-
plt.setp(_axl[::2], visible=False)
|
|
502
|
-
|
|
503
|
-
return _ax1
|
|
504
|
-
|
|
505
|
-
_ax_out = _set_axis_def(_ax_out, mktshares_plot_flag=mktshares_plot_flag)
|
|
506
|
-
|
|
507
|
-
return plt, _fig, _ax_out, _set_axis_def
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
def dh_area(_dh_val: float = 0.01, /, *, prec: int = 9) -> float:
|
|
511
|
-
R"""
|
|
512
|
-
Area under the ΔHHI boundary.
|
|
513
|
-
|
|
514
|
-
When the given ΔHHI bound matches a Guidelines standard,
|
|
515
|
-
the area under the boundary is half the intrinsic clearance rate
|
|
516
|
-
for the ΔHHI safeharbor.
|
|
517
|
-
|
|
518
|
-
Notes
|
|
519
|
-
-----
|
|
520
|
-
To derive the knots, :math:`(s^0_1, s^1_1), (s^1_1, s^0_1)`
|
|
521
|
-
of the ΔHHI boundary, i.e., the points where it intersects
|
|
522
|
-
the merger-to-monopoly boundary, solve
|
|
523
|
-
|
|
524
|
-
.. math::
|
|
525
|
-
|
|
526
|
-
2 s1 s_2 &= ΔHHI\\
|
|
527
|
-
s_1 + s_2 &= 1
|
|
528
|
-
|
|
529
|
-
Parameters
|
|
530
|
-
----------
|
|
531
|
-
_dh_val
|
|
532
|
-
Change in concentration.
|
|
533
|
-
prec
|
|
534
|
-
Specified precision in decimal places.
|
|
535
|
-
|
|
536
|
-
Returns
|
|
537
|
-
-------
|
|
538
|
-
Area under ΔHHI boundary.
|
|
539
|
-
|
|
540
|
-
"""
|
|
541
|
-
|
|
542
|
-
_dh_val = mpf(f"{_dh_val}")
|
|
543
|
-
_s_naught = (1 - mp.sqrt(1 - 2 * _dh_val)) / 2
|
|
544
|
-
|
|
545
|
-
return round(
|
|
546
|
-
float(_s_naught + (_dh_val / 2) * (mp.ln(1 - _s_naught) - mp.ln(_s_naught))),
|
|
547
|
-
prec,
|
|
235
|
+
recapture_ratio: float = field(
|
|
236
|
+
kw_only=False,
|
|
237
|
+
default=DEFAULT_REC_RATIO,
|
|
238
|
+
validator=validators.instance_of(float),
|
|
548
239
|
)
|
|
549
240
|
|
|
550
|
-
|
|
551
|
-
def dh_area_quad(_dh_val: float = 0.01, /, *, prec: int = 9) -> float:
|
|
241
|
+
recapture_form: RECForm | None = field(kw_only=True, default=RECForm.INOUT)
|
|
552
242
|
"""
|
|
553
|
-
|
|
243
|
+
The form of the recapture ratio.
|
|
554
244
|
|
|
555
|
-
When the
|
|
556
|
-
|
|
557
|
-
for the
|
|
245
|
+
When :attr:`mergeron.RECForm.INOUT`, the recapture ratio for
|
|
246
|
+
he product having the smaller market-share is assumed to equal the default,
|
|
247
|
+
and the recapture ratio for the product with the larger market-share is
|
|
248
|
+
computed assuming MNL demand. Fixed recapture ratios are specified as
|
|
249
|
+
:attr:`mergeron.RECForm.FIXED`. (To specify that recapture ratios be
|
|
250
|
+
constructed from the generated purchase-probabilities for products in
|
|
251
|
+
the market and for the outside good, specify :attr:`mergeron.RECForm.OUTIN`.)
|
|
558
252
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
Returns
|
|
567
|
-
-------
|
|
568
|
-
Area under ΔHHI boundary.
|
|
253
|
+
The GUPPI boundary is a continuum of diversion ratio boundaries conditional on
|
|
254
|
+
price-cost margins, :math:`d_{ij} = g_i * p_i / (m_j * p_j)`,
|
|
255
|
+
with :math:`d_{ij}` the diverion ratio from product :math:`i` to product :math:`j`;
|
|
256
|
+
:math:`g_i` the GUPPI for product :math:`i`;
|
|
257
|
+
:math:`m_j` the margin for product :math:`j`; and
|
|
258
|
+
:math:`p_i, p_j` the prices of goods :math:`i, j`, respectively.
|
|
569
259
|
|
|
570
260
|
"""
|
|
571
261
|
|
|
572
|
-
|
|
573
|
-
|
|
262
|
+
@recapture_form.validator
|
|
263
|
+
def __rsv(
|
|
264
|
+
_instance: DiversionRatioBoundary,
|
|
265
|
+
_attribute: Attribute[RECForm],
|
|
266
|
+
_value: RECForm,
|
|
267
|
+
/,
|
|
268
|
+
) -> None:
|
|
269
|
+
if _value and not (isinstance(_value, RECForm)):
|
|
270
|
+
raise ValueError(f"Invalid recapture specification, {_value!r}.")
|
|
271
|
+
if _value == RECForm.OUTIN and _instance.recapture_ratio:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
f"Invalid recapture specification, {_value!r}. "
|
|
274
|
+
"You may consider specifying `mergeron.RECForm.INOUT` here, and "
|
|
275
|
+
'assigning the default recapture ratio as attribute, "recapture_ratio" of '
|
|
276
|
+
"this `DiversionRatioBoundarySpec` object."
|
|
277
|
+
)
|
|
278
|
+
if _value is None and _instance.agg_method != UPPAggrSelector.MAX:
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f"Specified aggregation method, {_instance.agg_method} requires a recapture specification."
|
|
281
|
+
)
|
|
574
282
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
),
|
|
579
|
-
prec,
|
|
283
|
+
agg_method: UPPAggrSelector = field(
|
|
284
|
+
kw_only=True,
|
|
285
|
+
default=UPPAggrSelector.MAX,
|
|
286
|
+
validator=validators.instance_of(UPPAggrSelector),
|
|
580
287
|
)
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
def delta_hhi_boundary(
|
|
584
|
-
_dh_val: float = 0.01, /, *, prec: int = 5
|
|
585
|
-
) -> GuidelinesBoundary:
|
|
586
288
|
"""
|
|
587
|
-
|
|
289
|
+
Method for aggregating the distinct diversion ratio measures for the two products.
|
|
588
290
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
Merging-firms' ΔHHI bound.
|
|
593
|
-
prec
|
|
594
|
-
Number of decimal places for rounding reported shares.
|
|
595
|
-
|
|
596
|
-
Returns
|
|
597
|
-
-------
|
|
598
|
-
Array of share-pairs, area under boundary.
|
|
291
|
+
Distinct diversion ratio or GUPPI measures for the two merging-firms' products are
|
|
292
|
+
aggregated using the method specified by the `agg_method` attribute, which is specified
|
|
293
|
+
using the enum :class:`mergeron.UPPAggrSelector`.
|
|
599
294
|
|
|
600
295
|
"""
|
|
601
296
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
_s_mid = mp.sqrt(_dh_val / 2)
|
|
605
|
-
|
|
606
|
-
_dh_step_sz = mp.power(10, -6)
|
|
607
|
-
_s_1 = np.array(mp.arange(_s_mid, _s_naught - mp.eps, -_dh_step_sz))
|
|
608
|
-
_s_2 = _dh_val / (2 * _s_1)
|
|
609
|
-
|
|
610
|
-
# Boundary points
|
|
611
|
-
_dh_half = np.row_stack((
|
|
612
|
-
np.column_stack((_s_1, _s_2)),
|
|
613
|
-
np.array([(mpf("0.0"), mpf("1.0"))]),
|
|
614
|
-
))
|
|
615
|
-
_dh_bdry_pts = np.row_stack((np.flip(_dh_half, 0), np.flip(_dh_half[1:], 1)))
|
|
616
|
-
|
|
617
|
-
_s_1_pts, _s_2_pts = np.split(_dh_bdry_pts, 2, axis=1)
|
|
618
|
-
return GuidelinesBoundary(
|
|
619
|
-
np.column_stack((
|
|
620
|
-
np.array(_s_1_pts, np.float64),
|
|
621
|
-
np.array(_s_2_pts, np.float64),
|
|
622
|
-
)),
|
|
623
|
-
dh_area(_dh_val, prec=prec),
|
|
297
|
+
precision: int = field(
|
|
298
|
+
kw_only=False, default=5, validator=validators.instance_of(int)
|
|
624
299
|
)
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
def combined_share_boundary(
|
|
628
|
-
_s_intcpt: float = 0.0625, /, *, bdry_dps: int = 10
|
|
629
|
-
) -> GuidelinesBoundary:
|
|
630
|
-
"""
|
|
631
|
-
Share combinations on the merging-firms' combined share boundary.
|
|
632
|
-
|
|
633
|
-
Assumes symmetric merging-firm margins. The combined-share is
|
|
634
|
-
congruent to the post-merger HHI contribution boundary, as the
|
|
635
|
-
post-merger HHI bound is the square of the combined-share bound.
|
|
636
|
-
|
|
637
|
-
Parameters
|
|
638
|
-
----------
|
|
639
|
-
_s_intcpt:
|
|
640
|
-
Merging-firms' combined share.
|
|
641
|
-
bdry_dps
|
|
642
|
-
Number of decimal places for rounding reported shares.
|
|
643
|
-
|
|
644
|
-
Returns
|
|
645
|
-
-------
|
|
646
|
-
Array of share-pairs, area under boundary.
|
|
647
|
-
|
|
648
300
|
"""
|
|
649
|
-
|
|
650
|
-
_s_mid = _s_intcpt / 2
|
|
651
|
-
|
|
652
|
-
_s1_pts = (0, _s_mid, _s_intcpt)
|
|
653
|
-
return GuidelinesBoundary(
|
|
654
|
-
np.column_stack((
|
|
655
|
-
np.array(_s1_pts, np.float64),
|
|
656
|
-
np.array(_s1_pts[::-1], np.float64),
|
|
657
|
-
)),
|
|
658
|
-
round(float(_s_intcpt * _s_mid), bdry_dps),
|
|
659
|
-
)
|
|
301
|
+
The number of decimal places of precision for the estimated area under the UPP boundary.
|
|
660
302
|
|
|
303
|
+
Leaving this attribute unspecified will result in the default precision,
|
|
304
|
+
which varies based on the `agg_method` attribute, reflecting
|
|
305
|
+
the limit of precision available from the underlying functions. The number of
|
|
306
|
+
boundary points generated is also defined based on this attribute.
|
|
661
307
|
|
|
662
|
-
def hhi_pre_contrib_boundary(
|
|
663
|
-
_hhi_contrib: float = 0.03125, /, *, bdry_dps: int = 5
|
|
664
|
-
) -> GuidelinesBoundary:
|
|
665
308
|
"""
|
|
666
|
-
Share combinations on the premerger HHI contribution boundary.
|
|
667
309
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
_hhi_contrib:
|
|
671
|
-
Merging-firms' pre-merger HHI contribution bound.
|
|
672
|
-
bdry_dps
|
|
673
|
-
Number of decimal places for rounding reported shares.
|
|
310
|
+
coordinates: ArrayDouble = field(init=False, kw_only=True)
|
|
311
|
+
"""Market-share pairs as Cartesian coordinates of points on the diversion ratio boundary."""
|
|
674
312
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
Array of share-pairs, area under boundary.
|
|
313
|
+
area: float = field(init=False, kw_only=True)
|
|
314
|
+
"""Area under the diversion ratio boundary."""
|
|
678
315
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
weighting
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
)
|
|
729
|
-
|
|
730
|
-
_agg_method = (
|
|
731
|
-
"arithmetic"
|
|
732
|
-
if _bdry_spec.agg_method.value.endswith("average")
|
|
733
|
-
else "distance"
|
|
734
|
-
)
|
|
735
|
-
|
|
736
|
-
return shrratio_boundary_wtd_avg(
|
|
737
|
-
_bdry_spec.share_ratio,
|
|
738
|
-
_bdry_spec.rec,
|
|
739
|
-
agg_method=_agg_method, # type: ignore
|
|
740
|
-
weighting=_weighting, # type: ignore
|
|
741
|
-
recapture_form=_bdry_spec.recapture_form.value, # type: ignore
|
|
742
|
-
prec=_bdry_spec.precision,
|
|
743
|
-
)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
def shrratio_boundary_max(
|
|
747
|
-
_delta_star: float = 0.075, _r_val: float = 0.80, /, *, prec: int = 10
|
|
748
|
-
) -> GuidelinesBoundary:
|
|
749
|
-
"""
|
|
750
|
-
Share combinations on the minimum GUPPI boundary with symmetric
|
|
751
|
-
merging-firm margins.
|
|
752
|
-
|
|
753
|
-
Parameters
|
|
754
|
-
----------
|
|
755
|
-
_delta_star
|
|
756
|
-
Margin-adjusted benchmark share ratio.
|
|
757
|
-
_r_val
|
|
758
|
-
Recapture ratio.
|
|
759
|
-
prec
|
|
760
|
-
Number of decimal places for rounding returned shares.
|
|
761
|
-
|
|
762
|
-
Returns
|
|
763
|
-
-------
|
|
764
|
-
Array of share-pairs, area under boundary.
|
|
765
|
-
|
|
766
|
-
"""
|
|
767
|
-
|
|
768
|
-
# _r_val is not needed for max boundary, but is specified for consistency
|
|
769
|
-
# of function call with other shrratio_mgnsym_boundary functions
|
|
770
|
-
del _r_val
|
|
771
|
-
_delta_star = mpf(f"{_delta_star}")
|
|
772
|
-
_s_intcpt = _delta_star
|
|
773
|
-
_s_mid = _delta_star / (1 + _delta_star)
|
|
774
|
-
|
|
775
|
-
_s1_pts = (0, _s_mid, _s_intcpt)
|
|
776
|
-
|
|
777
|
-
return GuidelinesBoundary(
|
|
778
|
-
np.column_stack((
|
|
779
|
-
np.array(_s1_pts, np.float64),
|
|
780
|
-
np.array(_s1_pts[::-1], np.float64),
|
|
781
|
-
)),
|
|
782
|
-
round(float(_s_intcpt * _s_mid), prec), # simplified calculation
|
|
783
|
-
)
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
def shrratio_boundary_min(
|
|
787
|
-
_delta_star: float = 0.075,
|
|
788
|
-
_r_val: float = 0.80,
|
|
316
|
+
def __attrs_post_init__(self, /) -> None:
|
|
317
|
+
_share_ratio = critical_share_ratio(
|
|
318
|
+
self.diversion_ratio, r_bar=self.recapture_ratio
|
|
319
|
+
)
|
|
320
|
+
_upp_agg_kwargs: gbfn.ShareRatioBoundaryKeywords = {
|
|
321
|
+
"recapture_form": getattr(self.recapture_form, "value", "inside-out"),
|
|
322
|
+
"dps": self.precision,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
match self.agg_method:
|
|
326
|
+
case UPPAggrSelector.DIS:
|
|
327
|
+
_upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
|
|
328
|
+
_upp_agg_kwargs |= {"agg_method": "distance", "weighting": None}
|
|
329
|
+
case UPPAggrSelector.AVG:
|
|
330
|
+
_upp_agg_fn = gbfn.shrratio_boundary_xact_avg # type: ignore
|
|
331
|
+
case UPPAggrSelector.MAX:
|
|
332
|
+
_upp_agg_fn = gbfn.shrratio_boundary_max # type: ignore
|
|
333
|
+
_upp_agg_kwargs = {"dps": 10} # replace here
|
|
334
|
+
case UPPAggrSelector.MIN:
|
|
335
|
+
_upp_agg_fn = gbfn.shrratio_boundary_min # type: ignore
|
|
336
|
+
_upp_agg_kwargs |= {"dps": 10} # update here
|
|
337
|
+
case _:
|
|
338
|
+
_upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
|
|
339
|
+
|
|
340
|
+
_aggregator: Literal["arithmetic mean", "geometric mean", "distance"]
|
|
341
|
+
if self.agg_method.value.endswith("average"):
|
|
342
|
+
_aggregator = "arithmetic mean"
|
|
343
|
+
elif self.agg_method.value.endswith("geometric mean"):
|
|
344
|
+
_aggregator = "geometric mean"
|
|
345
|
+
else:
|
|
346
|
+
_aggregator = "distance"
|
|
347
|
+
|
|
348
|
+
_wgt_type: Literal["cross-product-share", "own-share", None]
|
|
349
|
+
if self.agg_method.value.startswith("cross-product-share"):
|
|
350
|
+
_wgt_type = "cross-product-share"
|
|
351
|
+
elif self.agg_method.value.startswith("own-share"):
|
|
352
|
+
_wgt_type = "own-share"
|
|
353
|
+
else:
|
|
354
|
+
_wgt_type = None
|
|
355
|
+
|
|
356
|
+
_upp_agg_kwargs |= {"agg_method": _aggregator, "weighting": _wgt_type}
|
|
357
|
+
|
|
358
|
+
_boundary = _upp_agg_fn(_share_ratio, self.recapture_ratio, **_upp_agg_kwargs)
|
|
359
|
+
object.__setattr__(self, "coordinates", _boundary.coordinates)
|
|
360
|
+
object.__setattr__(self, "area", _boundary.area)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def guppi_from_delta(
|
|
364
|
+
_delta_bound: float = 0.01,
|
|
789
365
|
/,
|
|
790
366
|
*,
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
) ->
|
|
367
|
+
m_star: float = 1.00,
|
|
368
|
+
r_bar: float = DEFAULT_REC_RATIO,
|
|
369
|
+
) -> decimal.Decimal:
|
|
794
370
|
"""
|
|
795
|
-
|
|
796
|
-
merging-firm margins.
|
|
797
|
-
|
|
798
|
-
Notes
|
|
799
|
-
-----
|
|
800
|
-
With symmetric merging-firm margins, the maximum GUPPI boundary is
|
|
801
|
-
defined by the diversion ratio from the smaller merging-firm to the
|
|
802
|
-
larger one, and is hence unaffected by the method of estimating the
|
|
803
|
-
diversion ratio for the larger firm.
|
|
371
|
+
Translate ∆HHI bound to GUPPI bound.
|
|
804
372
|
|
|
805
373
|
Parameters
|
|
806
374
|
----------
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
value for both merging firms ("proportional").
|
|
814
|
-
prec
|
|
815
|
-
Number of decimal places for rounding returned shares.
|
|
375
|
+
_delta_bound
|
|
376
|
+
Specified ∆HHI bound.
|
|
377
|
+
m_star
|
|
378
|
+
Parametric price-cost margin.
|
|
379
|
+
r_bar
|
|
380
|
+
Default recapture ratio.
|
|
816
381
|
|
|
817
382
|
Returns
|
|
818
383
|
-------
|
|
819
|
-
|
|
384
|
+
GUPPI bound corresponding to ∆HHI bound, at given margin and recapture ratio.
|
|
820
385
|
|
|
821
386
|
"""
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
if recapture_form == "inside-out":
|
|
828
|
-
# ## Plot envelope of GUPPI boundaries with r_k = r_bar if s_k = min(_s_1, _s_2)
|
|
829
|
-
# ## See (s_i, s_j) in equation~(44), or thereabouts, in paper
|
|
830
|
-
_smin_nr = _delta_star * (1 - _r_val)
|
|
831
|
-
_smax_nr = 1 - _delta_star * _r_val
|
|
832
|
-
_guppi_bdry_env_dr = _smin_nr + _smax_nr
|
|
833
|
-
_s1_pts = np.array(
|
|
834
|
-
(
|
|
835
|
-
0,
|
|
836
|
-
_smin_nr / _guppi_bdry_env_dr,
|
|
837
|
-
_s_mid,
|
|
838
|
-
_smax_nr / _guppi_bdry_env_dr,
|
|
839
|
-
_s_intcpt,
|
|
840
|
-
),
|
|
841
|
-
np.float64,
|
|
842
|
-
)
|
|
843
|
-
|
|
844
|
-
_gbd_area = _s_mid + _s1_pts[1] * (1 - 2 * _s_mid)
|
|
845
|
-
else:
|
|
846
|
-
_s1_pts, _gbd_area = np.array((0, _s_mid, _s_intcpt), np.float64), _s_mid
|
|
847
|
-
|
|
848
|
-
return GuidelinesBoundary(
|
|
849
|
-
np.column_stack((_s1_pts, _s1_pts[::-1])), round(float(_gbd_area), prec)
|
|
387
|
+
return gbfn.round_cust(
|
|
388
|
+
m_star * r_bar * (_s_m := np.sqrt(_delta_bound / 2)) / (1 - _s_m),
|
|
389
|
+
frac=0.005,
|
|
390
|
+
rounding_mode="ROUND_HALF_DOWN",
|
|
850
391
|
)
|
|
851
392
|
|
|
852
393
|
|
|
853
|
-
def
|
|
854
|
-
|
|
855
|
-
_r_val: float = 0.80,
|
|
394
|
+
def critical_share_ratio(
|
|
395
|
+
_guppi_bound: float | decimal.Decimal = 0.075,
|
|
856
396
|
/,
|
|
857
397
|
*,
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
) -> GuidelinesBoundary:
|
|
398
|
+
m_star: float = 1.00,
|
|
399
|
+
r_bar: float = 1.00,
|
|
400
|
+
frac: float = 1e-16,
|
|
401
|
+
) -> decimal.Decimal:
|
|
863
402
|
"""
|
|
864
|
-
|
|
865
|
-
merging-firm margins.
|
|
403
|
+
Corollary to GUPPI bound.
|
|
866
404
|
|
|
867
405
|
Parameters
|
|
868
406
|
----------
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
weighting
|
|
876
|
-
Whether "own-share" or "cross-product-share" (or None for simple, unweighted average).
|
|
877
|
-
recapture_form
|
|
878
|
-
Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
|
|
879
|
-
value for both merging firms ("proportional").
|
|
880
|
-
prec
|
|
881
|
-
Number of decimal places for rounding returned shares and area.
|
|
407
|
+
_guppi_bound
|
|
408
|
+
Specified GUPPI bound.
|
|
409
|
+
m_star
|
|
410
|
+
Parametric price-cost margin.
|
|
411
|
+
r_bar
|
|
412
|
+
Default recapture ratio.
|
|
882
413
|
|
|
883
414
|
Returns
|
|
884
415
|
-------
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
Notes
|
|
888
|
-
-----
|
|
889
|
-
An analytical expression for the share-weighted arithmetic mean boundary
|
|
890
|
-
is derived and plotted from y-intercept to the ray of symmetry as follows::
|
|
891
|
-
|
|
892
|
-
from sympy import plot as symplot, solve, symbols
|
|
893
|
-
s_1, s_2 = symbols("s_1 s_2", positive=True)
|
|
894
|
-
|
|
895
|
-
g_val, r_val, m_val = 0.06, 0.80, 0.30
|
|
896
|
-
delta_star = g_val / (r_val * m_val)
|
|
897
|
-
|
|
898
|
-
# recapture_form == "inside-out"
|
|
899
|
-
oswag = solve(
|
|
900
|
-
s_1 * s_2 / (1 - s_1)
|
|
901
|
-
+ s_2 * s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1))
|
|
902
|
-
- (s_1 + s_2) * delta_star,
|
|
903
|
-
s_2
|
|
904
|
-
)[0]
|
|
905
|
-
symplot(
|
|
906
|
-
oswag,
|
|
907
|
-
(s_1, 0., d_hat / (1 + d_hat)),
|
|
908
|
-
ylabel=s_2
|
|
909
|
-
)
|
|
910
|
-
|
|
911
|
-
cpswag = solve(
|
|
912
|
-
s_2 * s_2 / (1 - s_1)
|
|
913
|
-
+ s_1 * s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1))
|
|
914
|
-
- (s_1 + s_2) * delta_star,
|
|
915
|
-
s_2
|
|
916
|
-
)[1]
|
|
917
|
-
symplot(
|
|
918
|
-
cpwag,
|
|
919
|
-
(s_1, 0., d_hat / (1 + d_hat)),
|
|
920
|
-
ylabel=s_2
|
|
921
|
-
)
|
|
922
|
-
|
|
923
|
-
# recapture_form == "proportional"
|
|
924
|
-
oswag = solve(
|
|
925
|
-
s_1 * s_2 / (1 - s_1)
|
|
926
|
-
+ s_2 * s_1 / (1 - s_2)
|
|
927
|
-
- (s_1 + s_2) * delta_star,
|
|
928
|
-
s_2
|
|
929
|
-
)[0]
|
|
930
|
-
symplot(
|
|
931
|
-
oswag,
|
|
932
|
-
(s_1, 0., d_hat / (1 + d_hat)),
|
|
933
|
-
ylabel=s_2
|
|
934
|
-
)
|
|
935
|
-
|
|
936
|
-
cpswag = solve(
|
|
937
|
-
s_2 * s_2 / (1 - s_1)
|
|
938
|
-
+ s_1 * s_1 / (1 - s_2)
|
|
939
|
-
- (s_1 + s_2) * delta_star,
|
|
940
|
-
s_2
|
|
941
|
-
)[1]
|
|
942
|
-
symplot(
|
|
943
|
-
cpswag,
|
|
944
|
-
(s_1, 0.0, d_hat / (1 + d_hat)),
|
|
945
|
-
ylabel=s_2
|
|
946
|
-
)
|
|
947
|
-
|
|
416
|
+
Critical share ratio (share ratio bound) corresponding to the GUPPI bound
|
|
417
|
+
for given margin and recapture ratio.
|
|
948
418
|
|
|
949
419
|
"""
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
_s_mid = _delta_star / (1 + _delta_star)
|
|
953
|
-
|
|
954
|
-
# initial conditions
|
|
955
|
-
_gbdry_points = [(_s_mid, _s_mid)]
|
|
956
|
-
_s_1_pre, _s_2_pre = _s_mid, _s_mid
|
|
957
|
-
_s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0, 0
|
|
958
|
-
|
|
959
|
-
# parameters for iteration
|
|
960
|
-
_gbd_step_sz = mp.power(10, -prec)
|
|
961
|
-
_theta = _gbd_step_sz * (10 if weighting == "cross-product-share" else 1)
|
|
962
|
-
for _s_1 in mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz):
|
|
963
|
-
# The wtd. avg. GUPPI is not always convex to the origin, so we
|
|
964
|
-
# increment _s_2 after each iteration in which our algorithm
|
|
965
|
-
# finds (s1, s2) on the boundary
|
|
966
|
-
_s_2 = _s_2_pre * (1 + _theta)
|
|
967
|
-
|
|
968
|
-
if (_s_1 + _s_2) > mpf("0.99875"):
|
|
969
|
-
# Loss of accuracy at 3-9s and up
|
|
970
|
-
break
|
|
971
|
-
|
|
972
|
-
while True:
|
|
973
|
-
_de_1 = _s_2 / (1 - _s_1)
|
|
974
|
-
_de_2 = (
|
|
975
|
-
_s_1 / (1 - lerp(_s_1, _s_2, _r_val))
|
|
976
|
-
if recapture_form == "inside-out"
|
|
977
|
-
else _s_1 / (1 - _s_2)
|
|
978
|
-
)
|
|
979
|
-
|
|
980
|
-
_r = (
|
|
981
|
-
mp.fdiv(
|
|
982
|
-
_s_1 if weighting == "cross-product-share" else _s_2, _s_1 + _s_2
|
|
983
|
-
)
|
|
984
|
-
if weighting
|
|
985
|
-
else 0.5
|
|
986
|
-
)
|
|
987
|
-
|
|
988
|
-
match agg_method:
|
|
989
|
-
case "geometric":
|
|
990
|
-
_delta_test = mp.expm1(lerp(mp.log1p(_de_1), mp.log1p(_de_2), _r))
|
|
991
|
-
case "distance":
|
|
992
|
-
_delta_test = mp.sqrt(lerp(_de_1**2, _de_2**2, _r))
|
|
993
|
-
case _:
|
|
994
|
-
_delta_test = lerp(_de_1, _de_2, _r)
|
|
995
|
-
|
|
996
|
-
_test_flag, _incr_decr = (
|
|
997
|
-
(_delta_test > _delta_star, -1)
|
|
998
|
-
if weighting == "cross-product-share"
|
|
999
|
-
else (_delta_test < _delta_star, 1)
|
|
1000
|
-
)
|
|
1001
|
-
|
|
1002
|
-
if _test_flag:
|
|
1003
|
-
_s_2 += _incr_decr * _gbd_step_sz
|
|
1004
|
-
else:
|
|
1005
|
-
break
|
|
1006
|
-
|
|
1007
|
-
# Build-up boundary points
|
|
1008
|
-
_gbdry_points.append((_s_1, _s_2))
|
|
1009
|
-
|
|
1010
|
-
# Build up area terms
|
|
1011
|
-
_s_2_oddsum += _s_2 if _s_2_oddval else 0
|
|
1012
|
-
_s_2_evnsum += _s_2 if not _s_2_oddval else 0
|
|
1013
|
-
_s_2_oddval = not _s_2_oddval
|
|
1014
|
-
|
|
1015
|
-
# Hold share points
|
|
1016
|
-
_s_2_pre = _s_2
|
|
1017
|
-
_s_1_pre = _s_1
|
|
1018
|
-
|
|
1019
|
-
if _s_2_oddval:
|
|
1020
|
-
_s_2_evnsum -= _s_2_pre
|
|
1021
|
-
else:
|
|
1022
|
-
_s_2_oddsum -= _s_1_pre
|
|
1023
|
-
|
|
1024
|
-
_s_intcpt = _shrratio_boundary_intcpt(
|
|
1025
|
-
_s_1_pre,
|
|
1026
|
-
_delta_star,
|
|
1027
|
-
_r_val,
|
|
1028
|
-
recapture_form=recapture_form,
|
|
1029
|
-
agg_method=agg_method,
|
|
1030
|
-
weighting=weighting,
|
|
1031
|
-
)
|
|
1032
|
-
|
|
1033
|
-
if weighting == "own-share":
|
|
1034
|
-
_gbd_prtlarea = (
|
|
1035
|
-
_gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_2_pre) / 3
|
|
1036
|
-
)
|
|
1037
|
-
# Area under boundary
|
|
1038
|
-
_gbdry_area_total = float(
|
|
1039
|
-
2 * (_s_1_pre + _gbd_prtlarea)
|
|
1040
|
-
- (mp.power(_s_mid, "2") + mp.power(_s_1_pre, "2"))
|
|
1041
|
-
)
|
|
1042
|
-
|
|
1043
|
-
else:
|
|
1044
|
-
_gbd_prtlarea = (
|
|
1045
|
-
_gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_intcpt) / 3
|
|
1046
|
-
)
|
|
1047
|
-
# Area under boundary
|
|
1048
|
-
_gbdry_area_total = float(2 * _gbd_prtlarea - mp.power(_s_mid, "2"))
|
|
1049
|
-
|
|
1050
|
-
_gbdry_points = np.row_stack((_gbdry_points, (mpf("0.0"), _s_intcpt))).astype(
|
|
1051
|
-
np.float64
|
|
1052
|
-
)
|
|
1053
|
-
|
|
1054
|
-
# Points defining boundary to point-of-symmetry
|
|
1055
|
-
return GuidelinesBoundary(
|
|
1056
|
-
np.row_stack((np.flip(_gbdry_points, 0), np.flip(_gbdry_points[1:], 1))),
|
|
1057
|
-
round(float(_gbdry_area_total), prec),
|
|
420
|
+
return gbfn.round_cust(
|
|
421
|
+
mpf(f"{_guppi_bound}") / mp.fmul(f"{m_star}", f"{r_bar}"), frac=frac
|
|
1058
422
|
)
|
|
1059
423
|
|
|
1060
424
|
|
|
1061
|
-
def
|
|
1062
|
-
|
|
1063
|
-
_r_val: float = 0.80,
|
|
425
|
+
def share_from_guppi(
|
|
426
|
+
_guppi_bound: float | decimal.Decimal = 0.065,
|
|
1064
427
|
/,
|
|
1065
428
|
*,
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
) ->
|
|
429
|
+
m_star: float = 1.00,
|
|
430
|
+
r_bar: float = DEFAULT_REC_RATIO,
|
|
431
|
+
) -> decimal.Decimal:
|
|
1069
432
|
"""
|
|
1070
|
-
|
|
1071
|
-
merging-firm margins.
|
|
1072
|
-
|
|
1073
|
-
Notes
|
|
1074
|
-
-----
|
|
1075
|
-
An analytical expression for the exact average boundary is derived
|
|
1076
|
-
and plotted from the y-intercept to the ray of symmetry as follows::
|
|
1077
|
-
|
|
1078
|
-
from sympy import latex, plot as symplot, solve, symbols
|
|
1079
|
-
|
|
1080
|
-
s_1, s_2 = symbols("s_1 s_2")
|
|
1081
|
-
|
|
1082
|
-
g_val, r_val, m_val = 0.06, 0.80, 0.30
|
|
1083
|
-
d_hat = g_val / (r_val * m_val)
|
|
1084
|
-
|
|
1085
|
-
# recapture_form = "inside-out"
|
|
1086
|
-
sag = solve(
|
|
1087
|
-
(s_2 / (1 - s_1))
|
|
1088
|
-
+ (s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1)))
|
|
1089
|
-
- 2 * d_hat,
|
|
1090
|
-
s_2
|
|
1091
|
-
)[0]
|
|
1092
|
-
symplot(
|
|
1093
|
-
sag,
|
|
1094
|
-
(s_1, 0., d_hat / (1 + d_hat)),
|
|
1095
|
-
ylabel=s_2
|
|
1096
|
-
)
|
|
1097
|
-
|
|
1098
|
-
# recapture_form = "proportional"
|
|
1099
|
-
sag = solve((s_2/(1 - s_1)) + (s_1/(1 - s_2)) - 2 * d_hat, s_2)[0]
|
|
1100
|
-
symplot(
|
|
1101
|
-
sag,
|
|
1102
|
-
(s_1, 0., d_hat / (1 + d_hat)),
|
|
1103
|
-
ylabel=s_2
|
|
1104
|
-
)
|
|
433
|
+
Symmetric-firm share for given GUPPI, margin, and recapture ratio.
|
|
1105
434
|
|
|
1106
435
|
Parameters
|
|
1107
436
|
----------
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
value for both merging firms ("proportional").
|
|
1115
|
-
prec
|
|
1116
|
-
Number of decimal places for rounding returned shares.
|
|
437
|
+
_guppi_bound
|
|
438
|
+
GUPPI bound.
|
|
439
|
+
m_star
|
|
440
|
+
Parametric price-cost margin.
|
|
441
|
+
r_bar
|
|
442
|
+
Default recapture ratio.
|
|
1117
443
|
|
|
1118
444
|
Returns
|
|
1119
445
|
-------
|
|
1120
|
-
|
|
446
|
+
float
|
|
447
|
+
Symmetric firm market share on GUPPI boundary, for given margin and
|
|
448
|
+
recapture ratio.
|
|
1121
449
|
|
|
1122
450
|
"""
|
|
1123
451
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
_gbdry_points_start = np.array([(_s_mid, _s_mid)])
|
|
1129
|
-
_s_1 = np.array(mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz), np.float64)
|
|
1130
|
-
if recapture_form == "inside-out":
|
|
1131
|
-
_s_intcpt = mp.fdiv(
|
|
1132
|
-
mp.fsub(
|
|
1133
|
-
2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
|
|
1134
|
-
),
|
|
1135
|
-
2 * mpf(f"{_r_val}"),
|
|
1136
|
-
)
|
|
1137
|
-
_nr_t1 = 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val)
|
|
1138
|
-
|
|
1139
|
-
_nr_sqrt_mdr = 4 * _delta_star * _r_val
|
|
1140
|
-
_nr_sqrt_mdr2 = _nr_sqrt_mdr * _r_val
|
|
1141
|
-
_nr_sqrt_md2r2 = _nr_sqrt_mdr2 * _delta_star
|
|
1142
|
-
|
|
1143
|
-
_nr_sqrt_t1 = _nr_sqrt_md2r2 * (_s_1**2 - 2 * _s_1 + 1)
|
|
1144
|
-
_nr_sqrt_t2 = _nr_sqrt_mdr2 * _s_1 * (_s_1 - 1)
|
|
1145
|
-
_nr_sqrt_t3 = _nr_sqrt_mdr * (2 * _s_1 - _s_1**2 - 1)
|
|
1146
|
-
_nr_sqrt_t4 = (_s_1**2) * (_r_val**2 - 6 * _r_val + 1)
|
|
1147
|
-
_nr_sqrt_t5 = _s_1 * (6 * _r_val - 2) + 1
|
|
1148
|
-
|
|
1149
|
-
_nr_t2_mdr = _nr_sqrt_t1 + _nr_sqrt_t2 + _nr_sqrt_t3 + _nr_sqrt_t4 + _nr_sqrt_t5
|
|
1150
|
-
|
|
1151
|
-
# Alternative grouping of terms in np.sqrt
|
|
1152
|
-
_nr_sqrt_s1sq = (_s_1**2) * (
|
|
1153
|
-
_nr_sqrt_md2r2 + _nr_sqrt_mdr2 - _nr_sqrt_mdr + _r_val**2 - 6 * _r_val + 1
|
|
1154
|
-
)
|
|
1155
|
-
_nr_sqrt_s1 = _s_1 * (
|
|
1156
|
-
-2 * _nr_sqrt_md2r2 - _nr_sqrt_mdr2 + 2 * _nr_sqrt_mdr + 6 * _r_val - 2
|
|
1157
|
-
)
|
|
1158
|
-
_nr_sqrt_nos1 = _nr_sqrt_md2r2 - _nr_sqrt_mdr + 1
|
|
1159
|
-
|
|
1160
|
-
_nr_t2_s1 = _nr_sqrt_s1sq + _nr_sqrt_s1 + _nr_sqrt_nos1
|
|
1161
|
-
|
|
1162
|
-
if not np.isclose(
|
|
1163
|
-
np.einsum("i->", _nr_t2_mdr.astype(np.float64)),
|
|
1164
|
-
np.einsum("i->", _nr_t2_s1.astype(np.float64)),
|
|
1165
|
-
rtol=0,
|
|
1166
|
-
atol=0.5 * prec,
|
|
1167
|
-
):
|
|
1168
|
-
raise RuntimeError(
|
|
1169
|
-
"Calculation of sq. root term in exact average GUPPI"
|
|
1170
|
-
f"with recapture spec, {f'"{recapture_form}"'} is incorrect."
|
|
1171
|
-
)
|
|
1172
|
-
|
|
1173
|
-
_s_2 = (_nr_t1 - np.sqrt(_nr_t2_s1)) / (2 * _r_val)
|
|
1174
|
-
|
|
1175
|
-
else:
|
|
1176
|
-
_s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
|
|
1177
|
-
_s_2 = (
|
|
1178
|
-
(1 / 2)
|
|
1179
|
-
+ _delta_star
|
|
1180
|
-
- _delta_star * _s_1
|
|
1181
|
-
- np.sqrt(
|
|
1182
|
-
((_delta_star**2) - 1) * (_s_1**2)
|
|
1183
|
-
+ (-2 * (_delta_star**2) + _delta_star + 1) * _s_1
|
|
1184
|
-
+ (_delta_star**2)
|
|
1185
|
-
- _delta_star
|
|
1186
|
-
+ (1 / 4)
|
|
1187
|
-
)
|
|
1188
|
-
)
|
|
1189
|
-
|
|
1190
|
-
_gbdry_points_inner = np.column_stack((_s_1, _s_2))
|
|
1191
|
-
_gbdry_points_end = np.array([(mpf("0.0"), _s_intcpt)], np.float64)
|
|
1192
|
-
|
|
1193
|
-
_gbdry_points = np.row_stack((
|
|
1194
|
-
_gbdry_points_end,
|
|
1195
|
-
np.flip(_gbdry_points_inner, 0),
|
|
1196
|
-
_gbdry_points_start,
|
|
1197
|
-
np.flip(_gbdry_points_inner, 1),
|
|
1198
|
-
np.flip(_gbdry_points_end, 1),
|
|
1199
|
-
)).astype(np.float64)
|
|
1200
|
-
_s_2 = np.concatenate((np.array([_s_mid], np.float64), _s_2))
|
|
1201
|
-
|
|
1202
|
-
_gbdry_ends = [0, -1]
|
|
1203
|
-
_gbdry_odds = np.array(range(1, len(_s_2), 2), np.int64)
|
|
1204
|
-
_gbdry_evns = np.array(range(2, len(_s_2), 2), np.int64)
|
|
1205
|
-
|
|
1206
|
-
# Double the are under the curve, and subtract the double counted bit.
|
|
1207
|
-
_gbdry_area_simpson = 2 * _gbd_step_sz * (
|
|
1208
|
-
(4 / 3) * np.sum(_s_2.take(_gbdry_odds))
|
|
1209
|
-
+ (2 / 3) * np.sum(_s_2.take(_gbdry_evns))
|
|
1210
|
-
+ (1 / 3) * np.sum(_s_2.take(_gbdry_ends))
|
|
1211
|
-
) - np.power(_s_mid, 2)
|
|
1212
|
-
|
|
1213
|
-
_s_1_pts, _s_2_pts = np.split(_gbdry_points, 2, axis=1)
|
|
1214
|
-
return GuidelinesBoundary(
|
|
1215
|
-
np.column_stack((np.array(_s_1_pts), np.array(_s_2_pts))),
|
|
1216
|
-
round(float(_gbdry_area_simpson), prec),
|
|
452
|
+
return gbfn.round_cust(
|
|
453
|
+
(_d0 := critical_share_ratio(_guppi_bound, m_star=m_star, r_bar=r_bar))
|
|
454
|
+
/ (1 + _d0)
|
|
1217
455
|
)
|
|
1218
456
|
|
|
1219
457
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
/,
|
|
1225
|
-
*,
|
|
1226
|
-
recapture_form: Literal["inside-out", "proportional"],
|
|
1227
|
-
agg_method: Literal["arithmetic", "geometric", "distance"],
|
|
1228
|
-
weighting: Literal["cross-product-share", "own-share"] | None,
|
|
1229
|
-
) -> float:
|
|
1230
|
-
match weighting:
|
|
1231
|
-
case "cross-product-share":
|
|
1232
|
-
_s_intcpt: float = _delta_star
|
|
1233
|
-
case "own-share":
|
|
1234
|
-
_s_intcpt = mpf("1.0")
|
|
1235
|
-
case None if agg_method == "distance":
|
|
1236
|
-
_s_intcpt = _delta_star * mp.sqrt("2")
|
|
1237
|
-
case None if agg_method == "arithmetic" and recapture_form == "inside-out":
|
|
1238
|
-
_s_intcpt = mp.fdiv(
|
|
1239
|
-
mp.fsub(
|
|
1240
|
-
2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
|
|
1241
|
-
),
|
|
1242
|
-
2 * mpf(f"{_r_val}"),
|
|
1243
|
-
)
|
|
1244
|
-
case None if agg_method == "arithmetic":
|
|
1245
|
-
_s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
|
|
1246
|
-
case _:
|
|
1247
|
-
_s_intcpt = _s_2_pre
|
|
1248
|
-
|
|
1249
|
-
return _s_intcpt
|
|
458
|
+
if __name__ == "__main__":
|
|
459
|
+
print(
|
|
460
|
+
"This module defines classes with methods for generating boundaries for concentration and diversion-ratio screens."
|
|
461
|
+
)
|