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