mergeron 2024.738963.0__py3-none-any.whl → 2024.738973.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/core/__init__.py +18 -8
- mergeron/core/guidelines_boundaries.py +87 -897
- mergeron/core/guidelines_boundary_functions.py +826 -0
- mergeron/core/{guidelines_boundaries_specialized_functions.py → guidelines_boundary_functions_extra.py} +48 -9
- mergeron/gen/__init__.py +20 -37
- mergeron/gen/{_data_generation_functions_nonpublic.py → _data_generation_functions.py} +77 -19
- mergeron/gen/data_generation.py +17 -14
- mergeron/gen/market_sample.py +79 -0
- mergeron/gen/upp_tests.py +99 -66
- {mergeron-2024.738963.0.dist-info → mergeron-2024.738973.0.dist-info}/METADATA +1 -1
- {mergeron-2024.738963.0.dist-info → mergeron-2024.738973.0.dist-info}/RECORD +12 -10
- {mergeron-2024.738963.0.dist-info → mergeron-2024.738973.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
import decimal
|
|
2
|
+
from importlib.metadata import version
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from mpmath import mp, mpf # type: ignore
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
from .. import _PKG_NAME # noqa: TID252
|
|
10
|
+
from . import GuidelinesBoundary
|
|
11
|
+
|
|
12
|
+
__version__ = version(_PKG_NAME)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
mp.prec = 80
|
|
16
|
+
mp.trap_complex = True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def dh_area(_dh_val: float = 0.01, /, *, prec: int = 9) -> float:
|
|
20
|
+
R"""
|
|
21
|
+
Area under the ΔHHI boundary.
|
|
22
|
+
|
|
23
|
+
When the given ΔHHI bound matches a Guidelines standard,
|
|
24
|
+
the area under the boundary is half the intrinsic clearance rate
|
|
25
|
+
for the ΔHHI safeharbor.
|
|
26
|
+
|
|
27
|
+
Notes
|
|
28
|
+
-----
|
|
29
|
+
To derive the knots, :math:`(s^0_1, s^1_1), (s^1_1, s^0_1)`
|
|
30
|
+
of the ΔHHI boundary, i.e., the points where it intersects
|
|
31
|
+
the merger-to-monopoly boundary, solve
|
|
32
|
+
|
|
33
|
+
.. math::
|
|
34
|
+
|
|
35
|
+
2 s1 s_2 &= ΔHHI\\
|
|
36
|
+
s_1 + s_2 &= 1
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
_dh_val
|
|
41
|
+
Change in concentration.
|
|
42
|
+
prec
|
|
43
|
+
Specified precision in decimal places.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
Area under ΔHHI boundary.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
_dh_val = mpf(f"{_dh_val}")
|
|
52
|
+
_s_naught = (1 - mp.sqrt(1 - 2 * _dh_val)) / 2
|
|
53
|
+
|
|
54
|
+
return round(
|
|
55
|
+
float(_s_naught + (_dh_val / 2) * (mp.ln(1 - _s_naught) - mp.ln(_s_naught))),
|
|
56
|
+
prec,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def shrratio_boundary_wtd_avg(
|
|
61
|
+
_delta_star: float = 0.075,
|
|
62
|
+
_r_val: float = 0.855,
|
|
63
|
+
/,
|
|
64
|
+
*,
|
|
65
|
+
agg_method: Literal["arithmetic", "geometric", "distance"] = "arithmetic",
|
|
66
|
+
weighting: Literal["own-share", "cross-product-share"] | None = "own-share",
|
|
67
|
+
recapture_form: Literal["inside-out", "proportional"] = "inside-out",
|
|
68
|
+
prec: int = 5,
|
|
69
|
+
) -> GuidelinesBoundary:
|
|
70
|
+
"""
|
|
71
|
+
Share combinations on the share-weighted average diversion ratio boundary.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
_delta_star
|
|
76
|
+
Share ratio (:math:`\\overline{d} / \\overline{r}`)
|
|
77
|
+
_r_val
|
|
78
|
+
recapture ratio
|
|
79
|
+
agg_method
|
|
80
|
+
Whether "arithmetic", "geometric", or "distance".
|
|
81
|
+
weighting
|
|
82
|
+
Whether "own-share" or "cross-product-share" (or None for simple, unweighted average).
|
|
83
|
+
recapture_form
|
|
84
|
+
Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
|
|
85
|
+
value for both merging firms ("proportional").
|
|
86
|
+
prec
|
|
87
|
+
Number of decimal places for rounding returned shares and area.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
Array of share-pairs, area under boundary.
|
|
92
|
+
|
|
93
|
+
Notes
|
|
94
|
+
-----
|
|
95
|
+
An analytical expression for the share-weighted arithmetic mean boundary
|
|
96
|
+
is derived and plotted from y-intercept to the ray of symmetry as follows::
|
|
97
|
+
|
|
98
|
+
from sympy import plot as symplot, solve, symbols
|
|
99
|
+
s_1, s_2 = symbols("s_1 s_2", positive=True)
|
|
100
|
+
|
|
101
|
+
g_val, r_val, m_val = 0.06, 0.80, 0.30
|
|
102
|
+
delta_star = g_val / (r_val * m_val)
|
|
103
|
+
|
|
104
|
+
# recapture_form == "inside-out"
|
|
105
|
+
oswag = solve(
|
|
106
|
+
s_1 * s_2 / (1 - s_1)
|
|
107
|
+
+ s_2 * s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1))
|
|
108
|
+
- (s_1 + s_2) * delta_star,
|
|
109
|
+
s_2
|
|
110
|
+
)[0]
|
|
111
|
+
symplot(
|
|
112
|
+
oswag,
|
|
113
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
114
|
+
ylabel=s_2
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
cpswag = solve(
|
|
118
|
+
s_2 * s_2 / (1 - s_1)
|
|
119
|
+
+ s_1 * s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1))
|
|
120
|
+
- (s_1 + s_2) * delta_star,
|
|
121
|
+
s_2
|
|
122
|
+
)[1]
|
|
123
|
+
symplot(
|
|
124
|
+
cpwag,
|
|
125
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
126
|
+
ylabel=s_2
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# recapture_form == "proportional"
|
|
130
|
+
oswag = solve(
|
|
131
|
+
s_1 * s_2 / (1 - s_1)
|
|
132
|
+
+ s_2 * s_1 / (1 - s_2)
|
|
133
|
+
- (s_1 + s_2) * delta_star,
|
|
134
|
+
s_2
|
|
135
|
+
)[0]
|
|
136
|
+
symplot(
|
|
137
|
+
oswag,
|
|
138
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
139
|
+
ylabel=s_2
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
cpswag = solve(
|
|
143
|
+
s_2 * s_2 / (1 - s_1)
|
|
144
|
+
+ s_1 * s_1 / (1 - s_2)
|
|
145
|
+
- (s_1 + s_2) * delta_star,
|
|
146
|
+
s_2
|
|
147
|
+
)[1]
|
|
148
|
+
symplot(
|
|
149
|
+
cpswag,
|
|
150
|
+
(s_1, 0.0, d_hat / (1 + d_hat)),
|
|
151
|
+
ylabel=s_2
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
_delta_star = mpf(f"{_delta_star}")
|
|
158
|
+
_s_mid = _delta_star / (1 + _delta_star)
|
|
159
|
+
|
|
160
|
+
# initial conditions
|
|
161
|
+
_gbdry_points = [(_s_mid, _s_mid)]
|
|
162
|
+
_s_1_pre, _s_2_pre = _s_mid, _s_mid
|
|
163
|
+
_s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0, 0
|
|
164
|
+
|
|
165
|
+
# parameters for iteration
|
|
166
|
+
_gbd_step_sz = mp.power(10, -prec)
|
|
167
|
+
_theta = _gbd_step_sz * (10 if weighting == "cross-product-share" else 1)
|
|
168
|
+
for _s_1 in mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz):
|
|
169
|
+
# The wtd. avg. GUPPI is not always convex to the origin, so we
|
|
170
|
+
# increment _s_2 after each iteration in which our algorithm
|
|
171
|
+
# finds (s1, s2) on the boundary
|
|
172
|
+
_s_2 = _s_2_pre * (1 + _theta)
|
|
173
|
+
|
|
174
|
+
if (_s_1 + _s_2) > mpf("0.99875"):
|
|
175
|
+
# Loss of accuracy at 3-9s and up
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
while True:
|
|
179
|
+
_de_1 = _s_2 / (1 - _s_1)
|
|
180
|
+
_de_2 = (
|
|
181
|
+
_s_1 / (1 - lerp(_s_1, _s_2, _r_val))
|
|
182
|
+
if recapture_form == "inside-out"
|
|
183
|
+
else _s_1 / (1 - _s_2)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
_r = (
|
|
187
|
+
mp.fdiv(
|
|
188
|
+
_s_1 if weighting == "cross-product-share" else _s_2, _s_1 + _s_2
|
|
189
|
+
)
|
|
190
|
+
if weighting
|
|
191
|
+
else 0.5
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
match agg_method:
|
|
195
|
+
case "geometric":
|
|
196
|
+
_delta_test = mp.expm1(lerp(mp.log1p(_de_1), mp.log1p(_de_2), _r))
|
|
197
|
+
case "distance":
|
|
198
|
+
_delta_test = mp.sqrt(lerp(_de_1**2, _de_2**2, _r))
|
|
199
|
+
case _:
|
|
200
|
+
_delta_test = lerp(_de_1, _de_2, _r)
|
|
201
|
+
|
|
202
|
+
_test_flag, _incr_decr = (
|
|
203
|
+
(_delta_test > _delta_star, -1)
|
|
204
|
+
if weighting == "cross-product-share"
|
|
205
|
+
else (_delta_test < _delta_star, 1)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if _test_flag:
|
|
209
|
+
_s_2 += _incr_decr * _gbd_step_sz
|
|
210
|
+
else:
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
# Build-up boundary points
|
|
214
|
+
_gbdry_points.append((_s_1, _s_2))
|
|
215
|
+
|
|
216
|
+
# Build up area terms
|
|
217
|
+
_s_2_oddsum += _s_2 if _s_2_oddval else 0
|
|
218
|
+
_s_2_evnsum += _s_2 if not _s_2_oddval else 0
|
|
219
|
+
_s_2_oddval = not _s_2_oddval
|
|
220
|
+
|
|
221
|
+
# Hold share points
|
|
222
|
+
_s_2_pre = _s_2
|
|
223
|
+
_s_1_pre = _s_1
|
|
224
|
+
|
|
225
|
+
if _s_2_oddval:
|
|
226
|
+
_s_2_evnsum -= _s_2_pre
|
|
227
|
+
else:
|
|
228
|
+
_s_2_oddsum -= _s_1_pre
|
|
229
|
+
|
|
230
|
+
_s_intcpt = _shrratio_boundary_intcpt(
|
|
231
|
+
_s_1_pre,
|
|
232
|
+
_delta_star,
|
|
233
|
+
_r_val,
|
|
234
|
+
recapture_form=recapture_form,
|
|
235
|
+
agg_method=agg_method,
|
|
236
|
+
weighting=weighting,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if weighting == "own-share":
|
|
240
|
+
_gbd_prtlarea = (
|
|
241
|
+
_gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_2_pre) / 3
|
|
242
|
+
)
|
|
243
|
+
# Area under boundary
|
|
244
|
+
_gbdry_area_total = float(
|
|
245
|
+
2 * (_s_1_pre + _gbd_prtlarea)
|
|
246
|
+
- (mp.power(_s_mid, "2") + mp.power(_s_1_pre, "2"))
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
else:
|
|
250
|
+
_gbd_prtlarea = (
|
|
251
|
+
_gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_intcpt) / 3
|
|
252
|
+
)
|
|
253
|
+
# Area under boundary
|
|
254
|
+
_gbdry_area_total = float(2 * _gbd_prtlarea - mp.power(_s_mid, "2"))
|
|
255
|
+
|
|
256
|
+
_gbdry_points = np.row_stack((_gbdry_points, (mpf("0.0"), _s_intcpt))).astype(
|
|
257
|
+
np.float64
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Points defining boundary to point-of-symmetry
|
|
261
|
+
return GuidelinesBoundary(
|
|
262
|
+
np.row_stack((np.flip(_gbdry_points, 0), np.flip(_gbdry_points[1:], 1))),
|
|
263
|
+
round(float(_gbdry_area_total), prec),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def shrratio_boundary_xact_avg(
|
|
268
|
+
_delta_star: float = 0.075,
|
|
269
|
+
_r_val: float = 0.855,
|
|
270
|
+
/,
|
|
271
|
+
*,
|
|
272
|
+
recapture_form: Literal["inside-out", "proportional"] = "inside-out",
|
|
273
|
+
prec: int = 5,
|
|
274
|
+
) -> GuidelinesBoundary:
|
|
275
|
+
"""
|
|
276
|
+
Share combinations for the simple average GUPPI boundary with symmetric
|
|
277
|
+
merging-firm margins.
|
|
278
|
+
|
|
279
|
+
Notes
|
|
280
|
+
-----
|
|
281
|
+
An analytical expression for the exact average boundary is derived
|
|
282
|
+
and plotted from the y-intercept to the ray of symmetry as follows::
|
|
283
|
+
|
|
284
|
+
from sympy import latex, plot as symplot, solve, symbols
|
|
285
|
+
|
|
286
|
+
s_1, s_2 = symbols("s_1 s_2")
|
|
287
|
+
|
|
288
|
+
g_val, r_val, m_val = 0.06, 0.80, 0.30
|
|
289
|
+
d_hat = g_val / (r_val * m_val)
|
|
290
|
+
|
|
291
|
+
# recapture_form = "inside-out"
|
|
292
|
+
sag = solve(
|
|
293
|
+
(s_2 / (1 - s_1))
|
|
294
|
+
+ (s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1)))
|
|
295
|
+
- 2 * d_hat,
|
|
296
|
+
s_2
|
|
297
|
+
)[0]
|
|
298
|
+
symplot(
|
|
299
|
+
sag,
|
|
300
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
301
|
+
ylabel=s_2
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# recapture_form = "proportional"
|
|
305
|
+
sag = solve((s_2/(1 - s_1)) + (s_1/(1 - s_2)) - 2 * d_hat, s_2)[0]
|
|
306
|
+
symplot(
|
|
307
|
+
sag,
|
|
308
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
309
|
+
ylabel=s_2
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
_delta_star
|
|
315
|
+
Share ratio (:math:`\\overline{d} / \\overline{r}`).
|
|
316
|
+
_r_val
|
|
317
|
+
Recapture ratio
|
|
318
|
+
recapture_form
|
|
319
|
+
Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
|
|
320
|
+
value for both merging firms ("proportional").
|
|
321
|
+
prec
|
|
322
|
+
Number of decimal places for rounding returned shares.
|
|
323
|
+
|
|
324
|
+
Returns
|
|
325
|
+
-------
|
|
326
|
+
Array of share-pairs, area under boundary, area under boundary.
|
|
327
|
+
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
_delta_star = mpf(f"{_delta_star}")
|
|
331
|
+
_s_mid = _delta_star / (1 + _delta_star)
|
|
332
|
+
_gbd_step_sz = mp.power(10, -prec)
|
|
333
|
+
|
|
334
|
+
_gbdry_points_start = np.array([(_s_mid, _s_mid)])
|
|
335
|
+
_s_1 = np.array(mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz), np.float64)
|
|
336
|
+
if recapture_form == "inside-out":
|
|
337
|
+
_s_intcpt = mp.fdiv(
|
|
338
|
+
mp.fsub(
|
|
339
|
+
2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
|
|
340
|
+
),
|
|
341
|
+
2 * mpf(f"{_r_val}"),
|
|
342
|
+
)
|
|
343
|
+
_nr_t1 = 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val)
|
|
344
|
+
|
|
345
|
+
_nr_sqrt_mdr = 4 * _delta_star * _r_val
|
|
346
|
+
_nr_sqrt_mdr2 = _nr_sqrt_mdr * _r_val
|
|
347
|
+
_nr_sqrt_md2r2 = _nr_sqrt_mdr2 * _delta_star
|
|
348
|
+
|
|
349
|
+
_nr_sqrt_t1 = _nr_sqrt_md2r2 * (_s_1**2 - 2 * _s_1 + 1)
|
|
350
|
+
_nr_sqrt_t2 = _nr_sqrt_mdr2 * _s_1 * (_s_1 - 1)
|
|
351
|
+
_nr_sqrt_t3 = _nr_sqrt_mdr * (2 * _s_1 - _s_1**2 - 1)
|
|
352
|
+
_nr_sqrt_t4 = (_s_1**2) * (_r_val**2 - 6 * _r_val + 1)
|
|
353
|
+
_nr_sqrt_t5 = _s_1 * (6 * _r_val - 2) + 1
|
|
354
|
+
|
|
355
|
+
_nr_t2_mdr = _nr_sqrt_t1 + _nr_sqrt_t2 + _nr_sqrt_t3 + _nr_sqrt_t4 + _nr_sqrt_t5
|
|
356
|
+
|
|
357
|
+
# Alternative grouping of terms in np.sqrt
|
|
358
|
+
_nr_sqrt_s1sq = (_s_1**2) * (
|
|
359
|
+
_nr_sqrt_md2r2 + _nr_sqrt_mdr2 - _nr_sqrt_mdr + _r_val**2 - 6 * _r_val + 1
|
|
360
|
+
)
|
|
361
|
+
_nr_sqrt_s1 = _s_1 * (
|
|
362
|
+
-2 * _nr_sqrt_md2r2 - _nr_sqrt_mdr2 + 2 * _nr_sqrt_mdr + 6 * _r_val - 2
|
|
363
|
+
)
|
|
364
|
+
_nr_sqrt_nos1 = _nr_sqrt_md2r2 - _nr_sqrt_mdr + 1
|
|
365
|
+
|
|
366
|
+
_nr_t2_s1 = _nr_sqrt_s1sq + _nr_sqrt_s1 + _nr_sqrt_nos1
|
|
367
|
+
|
|
368
|
+
if not np.isclose(
|
|
369
|
+
np.einsum("i->", _nr_t2_mdr.astype(np.float64)),
|
|
370
|
+
np.einsum("i->", _nr_t2_s1.astype(np.float64)),
|
|
371
|
+
rtol=0,
|
|
372
|
+
atol=0.5 * prec,
|
|
373
|
+
):
|
|
374
|
+
raise RuntimeError(
|
|
375
|
+
"Calculation of sq. root term in exact average GUPPI"
|
|
376
|
+
f"with recapture spec, {f'"{recapture_form}"'} is incorrect."
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
_s_2 = (_nr_t1 - np.sqrt(_nr_t2_s1)) / (2 * _r_val)
|
|
380
|
+
|
|
381
|
+
else:
|
|
382
|
+
_s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
|
|
383
|
+
_s_2 = (
|
|
384
|
+
(1 / 2)
|
|
385
|
+
+ _delta_star
|
|
386
|
+
- _delta_star * _s_1
|
|
387
|
+
- np.sqrt(
|
|
388
|
+
((_delta_star**2) - 1) * (_s_1**2)
|
|
389
|
+
+ (-2 * (_delta_star**2) + _delta_star + 1) * _s_1
|
|
390
|
+
+ (_delta_star**2)
|
|
391
|
+
- _delta_star
|
|
392
|
+
+ (1 / 4)
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
_gbdry_points_inner = np.column_stack((_s_1, _s_2))
|
|
397
|
+
_gbdry_points_end = np.array([(mpf("0.0"), _s_intcpt)], np.float64)
|
|
398
|
+
|
|
399
|
+
_gbdry_points = np.row_stack((
|
|
400
|
+
_gbdry_points_end,
|
|
401
|
+
np.flip(_gbdry_points_inner, 0),
|
|
402
|
+
_gbdry_points_start,
|
|
403
|
+
np.flip(_gbdry_points_inner, 1),
|
|
404
|
+
np.flip(_gbdry_points_end, 1),
|
|
405
|
+
)).astype(np.float64)
|
|
406
|
+
_s_2 = np.concatenate((np.array([_s_mid], np.float64), _s_2))
|
|
407
|
+
|
|
408
|
+
_gbdry_ends = [0, -1]
|
|
409
|
+
_gbdry_odds = np.array(range(1, len(_s_2), 2), np.int64)
|
|
410
|
+
_gbdry_evns = np.array(range(2, len(_s_2), 2), np.int64)
|
|
411
|
+
|
|
412
|
+
# Double the are under the curve, and subtract the double counted bit.
|
|
413
|
+
_gbdry_area_simpson = 2 * _gbd_step_sz * (
|
|
414
|
+
(4 / 3) * np.sum(_s_2.take(_gbdry_odds))
|
|
415
|
+
+ (2 / 3) * np.sum(_s_2.take(_gbdry_evns))
|
|
416
|
+
+ (1 / 3) * np.sum(_s_2.take(_gbdry_ends))
|
|
417
|
+
) - np.power(_s_mid, 2)
|
|
418
|
+
|
|
419
|
+
_s_1_pts, _s_2_pts = np.split(_gbdry_points, 2, axis=1)
|
|
420
|
+
return GuidelinesBoundary(
|
|
421
|
+
np.column_stack((np.array(_s_1_pts), np.array(_s_2_pts))),
|
|
422
|
+
round(float(_gbdry_area_simpson), prec),
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def shrratio_boundary_min(
|
|
427
|
+
_delta_star: float = 0.075,
|
|
428
|
+
_r_val: float = 0.855,
|
|
429
|
+
/,
|
|
430
|
+
*,
|
|
431
|
+
recapture_form: str = "inside-out",
|
|
432
|
+
prec: int = 10,
|
|
433
|
+
) -> GuidelinesBoundary:
|
|
434
|
+
"""
|
|
435
|
+
Share combinations on the minimum GUPPI boundary, with symmetric
|
|
436
|
+
merging-firm margins.
|
|
437
|
+
|
|
438
|
+
Notes
|
|
439
|
+
-----
|
|
440
|
+
With symmetric merging-firm margins, the maximum GUPPI boundary is
|
|
441
|
+
defined by the diversion ratio from the smaller merging-firm to the
|
|
442
|
+
larger one, and is hence unaffected by the method of estimating the
|
|
443
|
+
diversion ratio for the larger firm.
|
|
444
|
+
|
|
445
|
+
Parameters
|
|
446
|
+
----------
|
|
447
|
+
_delta_star
|
|
448
|
+
Share ratio (:math:`\\overline{d} / \\overline{r}`).
|
|
449
|
+
_r_val
|
|
450
|
+
Recapture ratio.
|
|
451
|
+
recapture_form
|
|
452
|
+
Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
|
|
453
|
+
value for both merging firms ("proportional").
|
|
454
|
+
prec
|
|
455
|
+
Number of decimal places for rounding returned shares.
|
|
456
|
+
|
|
457
|
+
Returns
|
|
458
|
+
-------
|
|
459
|
+
Array of share-pairs, area under boundary.
|
|
460
|
+
|
|
461
|
+
"""
|
|
462
|
+
|
|
463
|
+
_delta_star = mpf(f"{_delta_star}")
|
|
464
|
+
_s_intcpt = mpf("1.00")
|
|
465
|
+
_s_mid = _delta_star / (1 + _delta_star)
|
|
466
|
+
|
|
467
|
+
if recapture_form == "inside-out":
|
|
468
|
+
# ## Plot envelope of GUPPI boundaries with r_k = r_bar if s_k = min(_s_1, _s_2)
|
|
469
|
+
# ## See (s_i, s_j) in equation~(44), or thereabouts, in paper
|
|
470
|
+
_smin_nr = _delta_star * (1 - _r_val)
|
|
471
|
+
_smax_nr = 1 - _delta_star * _r_val
|
|
472
|
+
_guppi_bdry_env_dr = _smin_nr + _smax_nr
|
|
473
|
+
_s1_pts = np.array(
|
|
474
|
+
(
|
|
475
|
+
0,
|
|
476
|
+
_smin_nr / _guppi_bdry_env_dr,
|
|
477
|
+
_s_mid,
|
|
478
|
+
_smax_nr / _guppi_bdry_env_dr,
|
|
479
|
+
_s_intcpt,
|
|
480
|
+
),
|
|
481
|
+
np.float64,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
_gbd_area = _s_mid + _s1_pts[1] * (1 - 2 * _s_mid)
|
|
485
|
+
else:
|
|
486
|
+
_s1_pts, _gbd_area = np.array((0, _s_mid, _s_intcpt), np.float64), _s_mid
|
|
487
|
+
|
|
488
|
+
return GuidelinesBoundary(
|
|
489
|
+
np.column_stack((_s1_pts, _s1_pts[::-1])), round(float(_gbd_area), prec)
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def shrratio_boundary_max(
|
|
494
|
+
_delta_star: float = 0.075, _r_val: float = 0.855, /, *, prec: int = 10
|
|
495
|
+
) -> GuidelinesBoundary:
|
|
496
|
+
"""
|
|
497
|
+
Share combinations on the minimum GUPPI boundary with symmetric
|
|
498
|
+
merging-firm margins.
|
|
499
|
+
|
|
500
|
+
Parameters
|
|
501
|
+
----------
|
|
502
|
+
_delta_star
|
|
503
|
+
Share ratio (:math:`\\overline{d} / \\overline{r}`).
|
|
504
|
+
_r_val
|
|
505
|
+
Recapture ratio.
|
|
506
|
+
prec
|
|
507
|
+
Number of decimal places for rounding returned shares.
|
|
508
|
+
|
|
509
|
+
Returns
|
|
510
|
+
-------
|
|
511
|
+
Array of share-pairs, area under boundary.
|
|
512
|
+
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
# _r_val is not needed for max boundary, but is specified for consistency
|
|
516
|
+
# of function call with other shrratio_mgnsym_boundary functions
|
|
517
|
+
del _r_val
|
|
518
|
+
_delta_star = mpf(f"{_delta_star}")
|
|
519
|
+
_s_intcpt = _delta_star
|
|
520
|
+
_s_mid = _delta_star / (1 + _delta_star)
|
|
521
|
+
|
|
522
|
+
_s1_pts = (0, _s_mid, _s_intcpt)
|
|
523
|
+
|
|
524
|
+
return GuidelinesBoundary(
|
|
525
|
+
np.column_stack((
|
|
526
|
+
np.array(_s1_pts, np.float64),
|
|
527
|
+
np.array(_s1_pts[::-1], np.float64),
|
|
528
|
+
)),
|
|
529
|
+
round(float(_s_intcpt * _s_mid), prec), # simplified calculation
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _shrratio_boundary_intcpt(
|
|
534
|
+
_s_2_pre: float,
|
|
535
|
+
_delta_star: mpf,
|
|
536
|
+
_r_val: mpf,
|
|
537
|
+
/,
|
|
538
|
+
*,
|
|
539
|
+
recapture_form: Literal["inside-out", "proportional"],
|
|
540
|
+
agg_method: Literal["arithmetic", "geometric", "distance"],
|
|
541
|
+
weighting: Literal["cross-product-share", "own-share"] | None,
|
|
542
|
+
) -> float:
|
|
543
|
+
match weighting:
|
|
544
|
+
case "cross-product-share":
|
|
545
|
+
_s_intcpt: float = _delta_star
|
|
546
|
+
case "own-share":
|
|
547
|
+
_s_intcpt = mpf("1.0")
|
|
548
|
+
case None if agg_method == "distance":
|
|
549
|
+
_s_intcpt = _delta_star * mp.sqrt("2")
|
|
550
|
+
case None if agg_method == "arithmetic" and recapture_form == "inside-out":
|
|
551
|
+
_s_intcpt = mp.fdiv(
|
|
552
|
+
mp.fsub(
|
|
553
|
+
2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
|
|
554
|
+
),
|
|
555
|
+
2 * mpf(f"{_r_val}"),
|
|
556
|
+
)
|
|
557
|
+
case None if agg_method == "arithmetic":
|
|
558
|
+
_s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
|
|
559
|
+
case _:
|
|
560
|
+
_s_intcpt = _s_2_pre
|
|
561
|
+
|
|
562
|
+
return _s_intcpt
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def lerp(
|
|
566
|
+
_x1: int | float | mpf | NDArray[np.float64 | np.int64] = 3,
|
|
567
|
+
_x2: int | float | mpf | NDArray[np.float64 | np.int64] = 1,
|
|
568
|
+
_r: float | mpf = 0.25,
|
|
569
|
+
/,
|
|
570
|
+
) -> float | mpf | NDArray[np.float64]:
|
|
571
|
+
"""
|
|
572
|
+
From the function of the same name in the C++ standard [2]_
|
|
573
|
+
|
|
574
|
+
Constructs the weighted average, :math:`w_1 x_1 + w_2 x_2`, where
|
|
575
|
+
:math:`w_1 = 1 - r` and :math:`w_2 = r`.
|
|
576
|
+
|
|
577
|
+
Parameters
|
|
578
|
+
----------
|
|
579
|
+
_x1, _x2
|
|
580
|
+
bounds :math:`x_1, x_2` to interpolate between.
|
|
581
|
+
_r
|
|
582
|
+
interpolation weight :math:`r` assigned to :math:`x_2`
|
|
583
|
+
|
|
584
|
+
Returns
|
|
585
|
+
-------
|
|
586
|
+
The linear interpolation, or weighted average,
|
|
587
|
+
:math:`x_1 + r \\cdot (x_1 - x_2) \\equiv (1 - r) \\cdot x_1 + r \\cdot x_2`.
|
|
588
|
+
|
|
589
|
+
Raises
|
|
590
|
+
------
|
|
591
|
+
ValueError
|
|
592
|
+
If the interpolation weight is not in the interval, :math:`[0, 1]`.
|
|
593
|
+
|
|
594
|
+
References
|
|
595
|
+
----------
|
|
596
|
+
|
|
597
|
+
.. [2] C++ Reference, https://en.cppreference.com/w/cpp/numeric/lerp
|
|
598
|
+
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
if not 0 <= _r <= 1:
|
|
602
|
+
raise ValueError("Specified interpolation weight must lie in [0, 1].")
|
|
603
|
+
elif _r == 0:
|
|
604
|
+
return _x1
|
|
605
|
+
elif _r == 1:
|
|
606
|
+
return _x2
|
|
607
|
+
elif _r == 0.5:
|
|
608
|
+
return 1 / 2 * (_x1 + _x2)
|
|
609
|
+
else:
|
|
610
|
+
return _r * _x2 + (1 - _r) * _x1
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def round_cust(
|
|
614
|
+
_num: float = 0.060215,
|
|
615
|
+
/,
|
|
616
|
+
*,
|
|
617
|
+
frac: float = 0.005,
|
|
618
|
+
rounding_mode: str = "ROUND_HALF_UP",
|
|
619
|
+
) -> float:
|
|
620
|
+
"""
|
|
621
|
+
Custom rounding, to the nearest 0.5% by default.
|
|
622
|
+
|
|
623
|
+
Parameters
|
|
624
|
+
----------
|
|
625
|
+
_num
|
|
626
|
+
Number to be rounded.
|
|
627
|
+
frac
|
|
628
|
+
Fraction to be rounded to.
|
|
629
|
+
rounding_mode
|
|
630
|
+
Rounding mode, as defined in the :code:`decimal` package.
|
|
631
|
+
|
|
632
|
+
Returns
|
|
633
|
+
-------
|
|
634
|
+
The given number, rounded as specified.
|
|
635
|
+
|
|
636
|
+
Raises
|
|
637
|
+
------
|
|
638
|
+
ValueError
|
|
639
|
+
If rounding mode is not defined in the :code:`decimal` package.
|
|
640
|
+
|
|
641
|
+
Notes
|
|
642
|
+
-----
|
|
643
|
+
Integer-round the quotient, :code:`(_num / frac)` using the specified
|
|
644
|
+
rounding mode. Return the product of the rounded quotient times
|
|
645
|
+
the specified precision, :code:`frac`.
|
|
646
|
+
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
if rounding_mode not in (
|
|
650
|
+
decimal.ROUND_05UP,
|
|
651
|
+
decimal.ROUND_CEILING,
|
|
652
|
+
decimal.ROUND_DOWN,
|
|
653
|
+
decimal.ROUND_FLOOR,
|
|
654
|
+
decimal.ROUND_HALF_DOWN,
|
|
655
|
+
decimal.ROUND_HALF_EVEN,
|
|
656
|
+
decimal.ROUND_HALF_UP,
|
|
657
|
+
decimal.ROUND_UP,
|
|
658
|
+
):
|
|
659
|
+
raise ValueError(
|
|
660
|
+
f"Value, {f'"{rounding_mode}"'} is invalid for rounding_mode."
|
|
661
|
+
"Documentation for the, \"decimal\" built-in lists valid rounding modes."
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
_n, _f, _e = (decimal.Decimal(f"{_g}") for _g in [_num, frac, 1])
|
|
665
|
+
|
|
666
|
+
return float(_f * (_n / _f).quantize(_e, rounding=rounding_mode))
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def boundary_plot(*, mktshares_plot_flag: bool = True) -> tuple[Any, ...]:
|
|
670
|
+
"""Setup basic figure and axes for plots of safe harbor boundaries.
|
|
671
|
+
|
|
672
|
+
See, https://matplotlib.org/stable/tutorials/text/pgf.html
|
|
673
|
+
"""
|
|
674
|
+
|
|
675
|
+
import matplotlib as mpl
|
|
676
|
+
import matplotlib.axes as mpa
|
|
677
|
+
import matplotlib.patches as mpp
|
|
678
|
+
import matplotlib.ticker as mpt
|
|
679
|
+
|
|
680
|
+
mpl.use("pgf")
|
|
681
|
+
import matplotlib.pyplot as plt
|
|
682
|
+
|
|
683
|
+
plt.rcParams.update({
|
|
684
|
+
"pgf.rcfonts": False,
|
|
685
|
+
"pgf.texsystem": "lualatex",
|
|
686
|
+
"pgf.preamble": "\n".join([
|
|
687
|
+
R"\pdfvariable minorversion=7",
|
|
688
|
+
R"\usepackage{fontspec}",
|
|
689
|
+
R"\usepackage{luacode}",
|
|
690
|
+
R"\begin{luacode}",
|
|
691
|
+
R"local function embedfull(tfmdata)",
|
|
692
|
+
R' tfmdata.embedding = "full"',
|
|
693
|
+
R"end",
|
|
694
|
+
R"",
|
|
695
|
+
R"luatexbase.add_to_callback("
|
|
696
|
+
R' "luaotfload.patch_font", embedfull, "embedfull"'
|
|
697
|
+
R")",
|
|
698
|
+
R"\end{luacode}",
|
|
699
|
+
R"\usepackage{mathtools}",
|
|
700
|
+
R"\usepackage{unicode-math}",
|
|
701
|
+
R"\setmathfont[math-style=ISO]{STIX Two Math}",
|
|
702
|
+
R"\setmainfont{STIX Two Text}",
|
|
703
|
+
r"\setsansfont{Fira Sans Light}",
|
|
704
|
+
R"\setmonofont[Scale=MatchLowercase,]{Fira Mono}",
|
|
705
|
+
R"\defaultfontfeatures[\rmfamily]{",
|
|
706
|
+
R" Ligatures={TeX, Common},",
|
|
707
|
+
R" Numbers={Proportional, Lining},",
|
|
708
|
+
R" }",
|
|
709
|
+
R"\defaultfontfeatures[\sffamily]{",
|
|
710
|
+
R" Ligatures={TeX, Common},",
|
|
711
|
+
R" Numbers={Monospaced, Lining},",
|
|
712
|
+
R" LetterSpace=0.50,",
|
|
713
|
+
R" }",
|
|
714
|
+
R"\usepackage[",
|
|
715
|
+
R" activate={true, nocompatibility},",
|
|
716
|
+
R" tracking=true,",
|
|
717
|
+
R" ]{microtype}",
|
|
718
|
+
]),
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
# Initialize a canvas with a single figure (set of axes)
|
|
722
|
+
_fig = plt.figure(figsize=(5, 5), dpi=600)
|
|
723
|
+
_ax_out = _fig.add_subplot()
|
|
724
|
+
|
|
725
|
+
def _set_axis_def(
|
|
726
|
+
_ax1: mpa.Axes,
|
|
727
|
+
/,
|
|
728
|
+
*,
|
|
729
|
+
mktshares_plot_flag: bool = False,
|
|
730
|
+
mktshares_axlbls_flag: bool = False,
|
|
731
|
+
) -> mpa.Axes:
|
|
732
|
+
# Set the width of axis gridlines, and tick marks:
|
|
733
|
+
# both axes, both major and minor ticks
|
|
734
|
+
# Frame, grid, and facecolor
|
|
735
|
+
for _spos0 in "left", "bottom":
|
|
736
|
+
_ax1.spines[_spos0].set_linewidth(0.5)
|
|
737
|
+
_ax1.spines[_spos0].set_zorder(5)
|
|
738
|
+
for _spos1 in "top", "right":
|
|
739
|
+
_ax1.spines[_spos1].set_linewidth(0.0)
|
|
740
|
+
_ax1.spines[_spos1].set_zorder(0)
|
|
741
|
+
_ax1.spines[_spos1].set_visible(False)
|
|
742
|
+
_ax1.set_facecolor("#E6E6E6")
|
|
743
|
+
|
|
744
|
+
_ax1.grid(linewidth=0.5, linestyle=":", color="grey", zorder=1)
|
|
745
|
+
_ax1.tick_params(axis="x", which="both", width=0.5)
|
|
746
|
+
_ax1.tick_params(axis="y", which="both", width=0.5)
|
|
747
|
+
|
|
748
|
+
# Tick marks skip, size, and rotation
|
|
749
|
+
# x-axis
|
|
750
|
+
plt.setp(
|
|
751
|
+
_ax1.xaxis.get_majorticklabels(),
|
|
752
|
+
horizontalalignment="right",
|
|
753
|
+
fontsize=6,
|
|
754
|
+
rotation=45,
|
|
755
|
+
)
|
|
756
|
+
# y-axis
|
|
757
|
+
plt.setp(
|
|
758
|
+
_ax1.yaxis.get_majorticklabels(), horizontalalignment="right", fontsize=6
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
if mktshares_plot_flag:
|
|
762
|
+
# Axis labels
|
|
763
|
+
if mktshares_axlbls_flag:
|
|
764
|
+
# x-axis
|
|
765
|
+
_ax1.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
|
|
766
|
+
_ax1.xaxis.set_label_coords(0.75, -0.1)
|
|
767
|
+
# y-axis
|
|
768
|
+
_ax1.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
|
|
769
|
+
_ax1.yaxis.set_label_coords(-0.1, 0.75)
|
|
770
|
+
|
|
771
|
+
# Plot the ray of symmetry
|
|
772
|
+
_ax1.plot(
|
|
773
|
+
[0, 1], [0, 1], linewidth=0.5, linestyle=":", color="grey", zorder=1
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
# Axis scale
|
|
777
|
+
_ax1.set_xlim(0, 1)
|
|
778
|
+
_ax1.set_ylim(0, 1)
|
|
779
|
+
_ax1.set_aspect(1.0)
|
|
780
|
+
|
|
781
|
+
# Truncate the axis frame to a triangle:
|
|
782
|
+
_ax1.add_patch(
|
|
783
|
+
mpp.Rectangle(
|
|
784
|
+
xy=(1.0025, 0.00),
|
|
785
|
+
width=1.1 * mp.sqrt(2),
|
|
786
|
+
height=1.1 * mp.sqrt(2),
|
|
787
|
+
angle=45,
|
|
788
|
+
color="white",
|
|
789
|
+
edgecolor=None,
|
|
790
|
+
fill=True,
|
|
791
|
+
clip_on=True,
|
|
792
|
+
zorder=5,
|
|
793
|
+
)
|
|
794
|
+
)
|
|
795
|
+
# Feasible space is bounded by the other diagonal:
|
|
796
|
+
_ax1.plot(
|
|
797
|
+
[0, 1], [1, 0], linestyle="-", linewidth=0.5, color="black", zorder=1
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
# Axis Tick-mark locations
|
|
801
|
+
# One can supply an argument to mpt.AutoMinorLocator to
|
|
802
|
+
# specify a fixed number of minor intervals per major interval, e.g.:
|
|
803
|
+
# minorLocator = mpt.AutoMinorLocator(2)
|
|
804
|
+
# would lead to a single minor tick between major ticks.
|
|
805
|
+
_minorLocator = mpt.AutoMinorLocator(5)
|
|
806
|
+
_majorLocator = mpt.MultipleLocator(0.05)
|
|
807
|
+
for _axs in _ax1.xaxis, _ax1.yaxis:
|
|
808
|
+
if _axs == _ax1.xaxis:
|
|
809
|
+
_majorticklabels_rot = 45
|
|
810
|
+
elif _axs == _ax1.yaxis:
|
|
811
|
+
_majorticklabels_rot = 0
|
|
812
|
+
# x-axis
|
|
813
|
+
_axs.set_major_locator(_majorLocator)
|
|
814
|
+
_axs.set_minor_locator(_minorLocator)
|
|
815
|
+
# It"s always x when specifying the format
|
|
816
|
+
_axs.set_major_formatter(mpt.StrMethodFormatter("{x:>3.0%}"))
|
|
817
|
+
|
|
818
|
+
# Hide every other tick-label
|
|
819
|
+
for _axl in _ax1.get_xticklabels(), _ax1.get_yticklabels():
|
|
820
|
+
plt.setp(_axl[::2], visible=False)
|
|
821
|
+
|
|
822
|
+
return _ax1
|
|
823
|
+
|
|
824
|
+
_ax_out = _set_axis_def(_ax_out, mktshares_plot_flag=mktshares_plot_flag)
|
|
825
|
+
|
|
826
|
+
return plt, _fig, _ax_out, _set_axis_def
|