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
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
import decimal
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Literal, TypedDict
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from mpmath import mp, mpf # type: ignore
|
|
7
|
+
|
|
8
|
+
from .. import DEFAULT_REC_RATIO, VERSION, ArrayBIGINT, ArrayDouble # noqa: TID252
|
|
9
|
+
from . import MPFloat
|
|
10
|
+
|
|
11
|
+
__version__ = VERSION
|
|
12
|
+
|
|
13
|
+
mp.dps = 32
|
|
14
|
+
mp.trap_complex = True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ShareRatioBoundaryKeywords(TypedDict, total=False):
|
|
18
|
+
"""Keyword arguments for functions generating share ratio boundaries."""
|
|
19
|
+
|
|
20
|
+
recapture_form: Literal["inside-out", "proportional"]
|
|
21
|
+
dps: int
|
|
22
|
+
agg_method: Literal["arithmetic mean", "geometric mean", "distance"]
|
|
23
|
+
weighting: Literal["own-share", "cross-product-share", None]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class GuidelinesBoundary:
|
|
28
|
+
"""Output of a Guidelines boundary function."""
|
|
29
|
+
|
|
30
|
+
coordinates: ArrayDouble
|
|
31
|
+
"""Market-share pairs as Cartesian coordinates of points on the boundary."""
|
|
32
|
+
|
|
33
|
+
area: float
|
|
34
|
+
"""Area under the boundary."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def dh_area(_delta_bound: float | MPFloat = 0.01, /, *, dps: int = 9) -> float:
|
|
38
|
+
R"""
|
|
39
|
+
Area under the ΔHHI boundary.
|
|
40
|
+
|
|
41
|
+
When the given ΔHHI bound matches a Guidelines standard,
|
|
42
|
+
the area under the boundary is half the intrinsic clearance rate
|
|
43
|
+
for the ΔHHI safeharbor.
|
|
44
|
+
|
|
45
|
+
Notes
|
|
46
|
+
-----
|
|
47
|
+
To derive the knots, :math:`(s^0_1, s^1_1), (s^1_1, s^0_1)`
|
|
48
|
+
of the ΔHHI boundary, i.e., the points where it intersects
|
|
49
|
+
the merger-to-monopoly boundary, solve
|
|
50
|
+
|
|
51
|
+
.. math::
|
|
52
|
+
|
|
53
|
+
2 s1 s_2 &= ΔHHI\\
|
|
54
|
+
s_1 + s_2 &= 1
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
_delta_bound
|
|
59
|
+
Change in concentration.
|
|
60
|
+
dps
|
|
61
|
+
Specified precision in decimal places.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
Area under ΔHHI boundary.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
_delta_bound = mpf(f"{_delta_bound}")
|
|
70
|
+
_s_naught = (1 - mp.sqrt(1 - 2 * _delta_bound)) / 2
|
|
71
|
+
|
|
72
|
+
return round(
|
|
73
|
+
float(
|
|
74
|
+
_s_naught + (_delta_bound / 2) * (mp.ln(1 - _s_naught) - mp.ln(_s_naught))
|
|
75
|
+
),
|
|
76
|
+
dps,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def hhi_delta_boundary(
|
|
81
|
+
_delta_bound: float | decimal.Decimal | MPFloat = 0.01, /, *, dps: int = 5
|
|
82
|
+
) -> GuidelinesBoundary:
|
|
83
|
+
"""
|
|
84
|
+
Generate the list of share combination on the ΔHHI boundary.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
_delta_bound:
|
|
89
|
+
Merging-firms' ΔHHI bound.
|
|
90
|
+
dps
|
|
91
|
+
Number of decimal places for rounding reported shares.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
Array of share-pairs, area under boundary.
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
_delta_bound = mpf(f"{_delta_bound}")
|
|
100
|
+
_s_naught = 1 / 2 * (1 - mp.sqrt(1 - 2 * _delta_bound))
|
|
101
|
+
_s_mid = mp.sqrt(_delta_bound / 2)
|
|
102
|
+
|
|
103
|
+
_dh_step_sz = mp.power(10, -6)
|
|
104
|
+
_s_1 = np.array(mp.arange(_s_mid, _s_naught - mp.eps, -_dh_step_sz))
|
|
105
|
+
|
|
106
|
+
# Boundary points
|
|
107
|
+
_dh_half = np.vstack((
|
|
108
|
+
np.column_stack((_s_1, _delta_bound / (2 * _s_1))).astype(np.float64),
|
|
109
|
+
np.array([(mpf("0.0"), mpf("1.0"))], np.float64),
|
|
110
|
+
))
|
|
111
|
+
_dh_bdry_pts = np.vstack((_dh_half[::-1], _dh_half[1:, ::-1]))
|
|
112
|
+
|
|
113
|
+
return GuidelinesBoundary(_dh_bdry_pts, dh_area(_delta_bound, dps=dps))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def hhi_pre_contrib_boundary(
|
|
117
|
+
_hhi_bound: float | decimal.Decimal | MPFloat = 0.03125, /, *, dps: int = 5
|
|
118
|
+
) -> GuidelinesBoundary:
|
|
119
|
+
"""
|
|
120
|
+
Share combinations on the premerger HHI contribution boundary.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
_hhi_bound:
|
|
125
|
+
Merging-firms' pre-merger HHI contribution bound.
|
|
126
|
+
dps
|
|
127
|
+
Number of decimal places for rounding reported shares.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
Array of share-pairs, area under boundary.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
_hhi_bound = mpf(f"{_hhi_bound}")
|
|
135
|
+
_s_mid = mp.sqrt(_hhi_bound / 2)
|
|
136
|
+
|
|
137
|
+
_bdry_step_sz = mp.power(10, -dps)
|
|
138
|
+
# Range-limit is 0 less a step, which is -1 * step-size
|
|
139
|
+
_s_1 = np.array(mp.arange(_s_mid, -_bdry_step_sz, -_bdry_step_sz))
|
|
140
|
+
_s_2 = np.sqrt(_hhi_bound - _s_1**2)
|
|
141
|
+
_bdry_pts_mid = np.column_stack((_s_1, _s_2)).astype(np.float64)
|
|
142
|
+
|
|
143
|
+
return GuidelinesBoundary(
|
|
144
|
+
np.vstack((_bdry_pts_mid[::-1], _bdry_pts_mid[1:, ::-1])),
|
|
145
|
+
round(float(mp.pi * _hhi_bound / 4), dps),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def combined_share_boundary(
|
|
150
|
+
_s_intcpt: float | decimal.Decimal | MPFloat = 0.0625, /, *, dps: int = 10
|
|
151
|
+
) -> GuidelinesBoundary:
|
|
152
|
+
"""
|
|
153
|
+
Share combinations on the merging-firms' combined share boundary.
|
|
154
|
+
|
|
155
|
+
Assumes symmetric merging-firm margins. The combined-share is
|
|
156
|
+
congruent to the post-merger HHI contribution boundary, as the
|
|
157
|
+
post-merger HHI bound is the square of the combined-share bound.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
_s_intcpt:
|
|
162
|
+
Merging-firms' combined share.
|
|
163
|
+
dps
|
|
164
|
+
Number of decimal places for rounding reported shares.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
Array of share-pairs, area under boundary.
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
_s_intcpt = mpf(f"{_s_intcpt}")
|
|
172
|
+
_s_mid = _s_intcpt / 2
|
|
173
|
+
|
|
174
|
+
_s1_pts = np.array([0, _s_mid, _s_intcpt], np.float64)
|
|
175
|
+
return GuidelinesBoundary(
|
|
176
|
+
np.column_stack((_s1_pts, _s1_pts[::-1])), round(float(_s_intcpt * _s_mid), dps)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def hhi_post_contrib_boundary(
|
|
181
|
+
_hhi_bound: float | decimal.Decimal | MPFloat = 0.800, /, *, dps: int = 10
|
|
182
|
+
) -> GuidelinesBoundary:
|
|
183
|
+
"""
|
|
184
|
+
Share combinations on the postmerger HHI contribution boundary.
|
|
185
|
+
|
|
186
|
+
The post-merger HHI contribution boundary is identical to the
|
|
187
|
+
combined-share boundary.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
_hhi_bound:
|
|
192
|
+
Merging-firms' pre-merger HHI contribution bound.
|
|
193
|
+
dps
|
|
194
|
+
Number of decimal places for rounding reported shares.
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
Array of share-pairs, area under boundary.
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
return combined_share_boundary(
|
|
202
|
+
_hhi_bound.sqrt()
|
|
203
|
+
if isinstance(_hhi_bound, decimal.Decimal | mpf)
|
|
204
|
+
else np.sqrt(_hhi_bound),
|
|
205
|
+
dps=dps,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def shrratio_boundary_wtd_avg(
|
|
210
|
+
_delta_star: float | decimal.Decimal | MPFloat = 0.075,
|
|
211
|
+
_r_val: float = DEFAULT_REC_RATIO,
|
|
212
|
+
/,
|
|
213
|
+
*,
|
|
214
|
+
agg_method: Literal[
|
|
215
|
+
"arithmetic mean", "geometric mean", "distance"
|
|
216
|
+
] = "arithmetic mean",
|
|
217
|
+
weighting: Literal["own-share", "cross-product-share", None] = "own-share",
|
|
218
|
+
recapture_form: Literal["inside-out", "proportional"] = "inside-out",
|
|
219
|
+
dps: int = 5,
|
|
220
|
+
) -> GuidelinesBoundary:
|
|
221
|
+
"""
|
|
222
|
+
Share combinations on the share-weighted average diversion ratio boundary.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
_delta_star
|
|
227
|
+
Share ratio (:math:`\\overline{d} / \\overline{r}`)
|
|
228
|
+
_r_val
|
|
229
|
+
recapture ratio
|
|
230
|
+
agg_method
|
|
231
|
+
Whether "arithmetic mean", "geometric mean", or "distance".
|
|
232
|
+
weighting
|
|
233
|
+
Whether "own-share" or "cross-product-share" (or None for simple, unweighted average).
|
|
234
|
+
recapture_form
|
|
235
|
+
Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
|
|
236
|
+
value for both merging firms ("proportional").
|
|
237
|
+
dps
|
|
238
|
+
Number of decimal places for rounding returned shares and area.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
Array of share-pairs, area under boundary.
|
|
243
|
+
|
|
244
|
+
Notes
|
|
245
|
+
-----
|
|
246
|
+
An analytical expression for the share-weighted arithmetic mean boundary
|
|
247
|
+
is derived and plotted from y-intercept to the ray of symmetry as follows::
|
|
248
|
+
|
|
249
|
+
from sympy import plot as symplot, solve, symbols
|
|
250
|
+
s_1, s_2 = symbols("s_1 s_2", positive=True)
|
|
251
|
+
|
|
252
|
+
g_val, r_val, m_val = 0.06, 0.80, 0.30
|
|
253
|
+
delta_star = g_val / (r_val * m_val)
|
|
254
|
+
|
|
255
|
+
# recapture_form == "inside-out"
|
|
256
|
+
oswag = solve(
|
|
257
|
+
s_1 * s_2 / (1 - s_1)
|
|
258
|
+
+ s_2 * s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1))
|
|
259
|
+
- (s_1 + s_2) * delta_star,
|
|
260
|
+
s_2
|
|
261
|
+
)[0]
|
|
262
|
+
symplot(
|
|
263
|
+
oswag,
|
|
264
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
265
|
+
ylabel=s_2
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
cpswag = solve(
|
|
269
|
+
s_2 * s_2 / (1 - s_1)
|
|
270
|
+
+ s_1 * s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1))
|
|
271
|
+
- (s_1 + s_2) * delta_star,
|
|
272
|
+
s_2
|
|
273
|
+
)[1]
|
|
274
|
+
symplot(
|
|
275
|
+
cpwag,
|
|
276
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
277
|
+
ylabel=s_2
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# recapture_form == "proportional"
|
|
281
|
+
oswag = solve(
|
|
282
|
+
s_1 * s_2 / (1 - s_1)
|
|
283
|
+
+ s_2 * s_1 / (1 - s_2)
|
|
284
|
+
- (s_1 + s_2) * delta_star,
|
|
285
|
+
s_2
|
|
286
|
+
)[0]
|
|
287
|
+
symplot(
|
|
288
|
+
oswag,
|
|
289
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
290
|
+
ylabel=s_2
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
cpswag = solve(
|
|
294
|
+
s_2 * s_2 / (1 - s_1)
|
|
295
|
+
+ s_1 * s_1 / (1 - s_2)
|
|
296
|
+
- (s_1 + s_2) * delta_star,
|
|
297
|
+
s_2
|
|
298
|
+
)[1]
|
|
299
|
+
symplot(
|
|
300
|
+
cpswag,
|
|
301
|
+
(s_1, 0.0, d_hat / (1 + d_hat)),
|
|
302
|
+
ylabel=s_2
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
_delta_star = mpf(f"{_delta_star}")
|
|
309
|
+
_s_mid = mp.fdiv(_delta_star, 1 + _delta_star)
|
|
310
|
+
|
|
311
|
+
# initial conditions
|
|
312
|
+
_gbdry_points = [(_s_mid, _s_mid)]
|
|
313
|
+
_s_1_pre, _s_2_pre = _s_mid, _s_mid
|
|
314
|
+
_s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0.0, 0.0
|
|
315
|
+
|
|
316
|
+
# parameters for iteration
|
|
317
|
+
_gbd_step_sz = mp.power(10, -dps)
|
|
318
|
+
_theta = _gbd_step_sz * (10 if weighting == "cross-product-share" else 1)
|
|
319
|
+
for _s_1 in mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz):
|
|
320
|
+
# The wtd. avg. GUPPI is not always convex to the origin, so we
|
|
321
|
+
# increment _s_2 after each iteration in which our algorithm
|
|
322
|
+
# finds (s1, s2) on the boundary
|
|
323
|
+
_s_2 = _s_2_pre * (1 + _theta)
|
|
324
|
+
|
|
325
|
+
if (_s_1 + _s_2) > mpf("0.99875"):
|
|
326
|
+
# Loss of accuracy at 3-9s and up
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
while True:
|
|
330
|
+
_de_1 = _s_2 / (1 - _s_1)
|
|
331
|
+
_de_2 = (
|
|
332
|
+
_s_1 / (1 - lerp(_s_1, _s_2, _r_val))
|
|
333
|
+
if recapture_form == "inside-out"
|
|
334
|
+
else _s_1 / (1 - _s_2)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
_r = (
|
|
338
|
+
mp.fdiv(
|
|
339
|
+
_s_1 if weighting == "cross-product-share" else _s_2, _s_1 + _s_2
|
|
340
|
+
)
|
|
341
|
+
if weighting
|
|
342
|
+
else 0.5
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
match agg_method:
|
|
346
|
+
case "geometric mean":
|
|
347
|
+
_delta_test = mp.expm1(lerp(mp.log1p(_de_1), mp.log1p(_de_2), _r))
|
|
348
|
+
case "distance":
|
|
349
|
+
_delta_test = mp.sqrt(lerp(_de_1**2, _de_2**2, _r))
|
|
350
|
+
case _:
|
|
351
|
+
_delta_test = lerp(_de_1, _de_2, _r)
|
|
352
|
+
|
|
353
|
+
_test_flag, _incr_decr = (
|
|
354
|
+
(_delta_test > _delta_star, -1)
|
|
355
|
+
if weighting == "cross-product-share"
|
|
356
|
+
else (_delta_test < _delta_star, 1)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if _test_flag:
|
|
360
|
+
_s_2 += _incr_decr * _gbd_step_sz
|
|
361
|
+
else:
|
|
362
|
+
break
|
|
363
|
+
|
|
364
|
+
# Build-up boundary points
|
|
365
|
+
_gbdry_points.append((_s_1, _s_2))
|
|
366
|
+
|
|
367
|
+
# Build up area terms
|
|
368
|
+
_s_2_oddsum += _s_2 if _s_2_oddval else 0
|
|
369
|
+
_s_2_evnsum += _s_2 if not _s_2_oddval else 0
|
|
370
|
+
_s_2_oddval = not _s_2_oddval
|
|
371
|
+
|
|
372
|
+
# Hold share points
|
|
373
|
+
_s_2_pre = _s_2
|
|
374
|
+
_s_1_pre = _s_1
|
|
375
|
+
|
|
376
|
+
if _s_2_oddval:
|
|
377
|
+
_s_2_evnsum -= _s_2_pre
|
|
378
|
+
else:
|
|
379
|
+
_s_2_oddsum -= _s_1_pre
|
|
380
|
+
|
|
381
|
+
_s_intcpt = _shrratio_boundary_intcpt(
|
|
382
|
+
_s_2_pre,
|
|
383
|
+
_delta_star,
|
|
384
|
+
_r_val,
|
|
385
|
+
recapture_form=recapture_form,
|
|
386
|
+
agg_method=agg_method,
|
|
387
|
+
weighting=weighting,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if weighting == "own-share":
|
|
391
|
+
_gbd_prtlarea = (
|
|
392
|
+
_gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_2_pre) / 3
|
|
393
|
+
)
|
|
394
|
+
# Area under boundary
|
|
395
|
+
_gbdry_area_total = float(
|
|
396
|
+
2 * (_s_1_pre + _gbd_prtlarea)
|
|
397
|
+
- (mp.power(_s_mid, "2") + mp.power(_s_1_pre, "2"))
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
else:
|
|
401
|
+
_gbd_prtlarea = (
|
|
402
|
+
_gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_intcpt) / 3
|
|
403
|
+
)
|
|
404
|
+
# Area under boundary
|
|
405
|
+
_gbdry_area_total = float(2 * _gbd_prtlarea - mp.power(_s_mid, "2"))
|
|
406
|
+
|
|
407
|
+
_gbdry_points.append((mpf("0.0"), _s_intcpt))
|
|
408
|
+
_gbdry_array = np.array(_gbdry_points).astype(np.float64)
|
|
409
|
+
|
|
410
|
+
# Points defining boundary to point-of-symmetry
|
|
411
|
+
return GuidelinesBoundary(
|
|
412
|
+
np.vstack((_gbdry_array[::-1], _gbdry_array[1:, ::-1])),
|
|
413
|
+
round(float(_gbdry_area_total), dps),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def shrratio_boundary_xact_avg(
|
|
418
|
+
_delta_star: float | decimal.Decimal | MPFloat = 0.075,
|
|
419
|
+
_r_val: float = DEFAULT_REC_RATIO,
|
|
420
|
+
/,
|
|
421
|
+
*,
|
|
422
|
+
recapture_form: Literal["inside-out", "proportional"] = "inside-out",
|
|
423
|
+
dps: int = 5,
|
|
424
|
+
) -> GuidelinesBoundary:
|
|
425
|
+
"""
|
|
426
|
+
Share combinations for the simple average GUPPI boundary with symmetric
|
|
427
|
+
merging-firm margins.
|
|
428
|
+
|
|
429
|
+
Notes
|
|
430
|
+
-----
|
|
431
|
+
An analytical expression for the exact average boundary is derived
|
|
432
|
+
and plotted from the y-intercept to the ray of symmetry as follows::
|
|
433
|
+
|
|
434
|
+
from sympy import latex, plot as symplot, solve, symbols
|
|
435
|
+
|
|
436
|
+
s_1, s_2 = symbols("s_1 s_2")
|
|
437
|
+
|
|
438
|
+
g_val, r_val, m_val = 0.06, 0.80, 0.30
|
|
439
|
+
d_hat = g_val / (r_val * m_val)
|
|
440
|
+
|
|
441
|
+
# recapture_form = "inside-out"
|
|
442
|
+
sag = solve(
|
|
443
|
+
(s_2 / (1 - s_1))
|
|
444
|
+
+ (s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1)))
|
|
445
|
+
- 2 * d_hat,
|
|
446
|
+
s_2
|
|
447
|
+
)[0]
|
|
448
|
+
symplot(
|
|
449
|
+
sag,
|
|
450
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
451
|
+
ylabel=s_2
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# recapture_form = "proportional"
|
|
455
|
+
sag = solve((s_2/(1 - s_1)) + (s_1/(1 - s_2)) - 2 * d_hat, s_2)[0]
|
|
456
|
+
symplot(
|
|
457
|
+
sag,
|
|
458
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
459
|
+
ylabel=s_2
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
Parameters
|
|
463
|
+
----------
|
|
464
|
+
_delta_star
|
|
465
|
+
Share ratio (:math:`\\overline{d} / \\overline{r}`).
|
|
466
|
+
_r_val
|
|
467
|
+
Recapture ratio
|
|
468
|
+
recapture_form
|
|
469
|
+
Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
|
|
470
|
+
value for both merging firms ("proportional").
|
|
471
|
+
dps
|
|
472
|
+
Number of decimal places for rounding returned shares.
|
|
473
|
+
|
|
474
|
+
Returns
|
|
475
|
+
-------
|
|
476
|
+
Array of share-pairs, area under boundary, area under boundary.
|
|
477
|
+
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
_delta_star = mpf(f"{_delta_star}")
|
|
481
|
+
_s_mid = _delta_star / (1 + _delta_star)
|
|
482
|
+
_gbd_step_sz = mp.power(10, -dps)
|
|
483
|
+
|
|
484
|
+
_gbdry_points_start = np.array([(_s_mid, _s_mid)])
|
|
485
|
+
_s_1 = np.array(mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz), np.float64)
|
|
486
|
+
if recapture_form == "inside-out":
|
|
487
|
+
_s_intcpt = mp.fdiv(
|
|
488
|
+
mp.fsub(
|
|
489
|
+
2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
|
|
490
|
+
),
|
|
491
|
+
2 * mpf(f"{_r_val}"),
|
|
492
|
+
)
|
|
493
|
+
_nr_t1 = 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val)
|
|
494
|
+
|
|
495
|
+
_nr_sqrt_mdr = 4 * _delta_star * _r_val
|
|
496
|
+
_nr_sqrt_mdr2 = _nr_sqrt_mdr * _r_val
|
|
497
|
+
_nr_sqrt_md2r2 = _nr_sqrt_mdr2 * _delta_star
|
|
498
|
+
|
|
499
|
+
_nr_sqrt_t1 = _nr_sqrt_md2r2 * (_s_1**2 - 2 * _s_1 + 1)
|
|
500
|
+
_nr_sqrt_t2 = _nr_sqrt_mdr2 * _s_1 * (_s_1 - 1)
|
|
501
|
+
_nr_sqrt_t3 = _nr_sqrt_mdr * (2 * _s_1 - _s_1**2 - 1)
|
|
502
|
+
_nr_sqrt_t4 = (_s_1**2) * (_r_val**2 - 6 * _r_val + 1)
|
|
503
|
+
_nr_sqrt_t5 = _s_1 * (6 * _r_val - 2) + 1
|
|
504
|
+
|
|
505
|
+
_nr_t2_mdr = _nr_sqrt_t1 + _nr_sqrt_t2 + _nr_sqrt_t3 + _nr_sqrt_t4 + _nr_sqrt_t5
|
|
506
|
+
|
|
507
|
+
# Alternative grouping of terms in np.sqrt
|
|
508
|
+
_nr_sqrt_s1sq = (_s_1**2) * (
|
|
509
|
+
_nr_sqrt_md2r2 + _nr_sqrt_mdr2 - _nr_sqrt_mdr + _r_val**2 - 6 * _r_val + 1
|
|
510
|
+
)
|
|
511
|
+
_nr_sqrt_s1 = _s_1 * (
|
|
512
|
+
-2 * _nr_sqrt_md2r2 - _nr_sqrt_mdr2 + 2 * _nr_sqrt_mdr + 6 * _r_val - 2
|
|
513
|
+
)
|
|
514
|
+
_nr_sqrt_nos1 = _nr_sqrt_md2r2 - _nr_sqrt_mdr + 1
|
|
515
|
+
|
|
516
|
+
_nr_t2_s1 = _nr_sqrt_s1sq + _nr_sqrt_s1 + _nr_sqrt_nos1
|
|
517
|
+
|
|
518
|
+
if not np.isclose(
|
|
519
|
+
np.einsum("i->", _nr_t2_mdr.astype(np.float64)),
|
|
520
|
+
np.einsum("i->", _nr_t2_s1.astype(np.float64)),
|
|
521
|
+
rtol=0,
|
|
522
|
+
atol=0.5 * dps,
|
|
523
|
+
):
|
|
524
|
+
raise RuntimeError(
|
|
525
|
+
"Calculation of sq. root term in exact average GUPPI"
|
|
526
|
+
f"with recapture spec, {f'"{recapture_form}"'} is incorrect."
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
_s_2 = (_nr_t1 - np.sqrt(_nr_t2_s1)) / (2 * _r_val)
|
|
530
|
+
|
|
531
|
+
else:
|
|
532
|
+
_s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
|
|
533
|
+
_s_2 = (
|
|
534
|
+
(1 / 2)
|
|
535
|
+
+ _delta_star
|
|
536
|
+
- _delta_star * _s_1
|
|
537
|
+
- np.sqrt(
|
|
538
|
+
((_delta_star**2) - 1) * (_s_1**2)
|
|
539
|
+
+ (-2 * (_delta_star**2) + _delta_star + 1) * _s_1
|
|
540
|
+
+ (_delta_star**2)
|
|
541
|
+
- _delta_star
|
|
542
|
+
+ (1 / 4)
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
_gbdry_points_inner = np.column_stack((_s_1, _s_2))
|
|
547
|
+
_gbdry_points_end = np.array([(mpf("0.0"), _s_intcpt)], np.float64)
|
|
548
|
+
|
|
549
|
+
_gbdry_points = np.vstack((
|
|
550
|
+
_gbdry_points_end,
|
|
551
|
+
_gbdry_points_inner[::-1],
|
|
552
|
+
_gbdry_points_start,
|
|
553
|
+
_gbdry_points_inner[:, ::-1],
|
|
554
|
+
_gbdry_points_end[:, ::-1],
|
|
555
|
+
)).astype(np.float64)
|
|
556
|
+
_s_2 = np.concatenate((np.array([_s_mid], np.float64), _s_2))
|
|
557
|
+
|
|
558
|
+
_gbdry_ends = [0, -1]
|
|
559
|
+
_gbdry_odds = np.array(range(1, len(_s_2), 2), np.int64)
|
|
560
|
+
_gbdry_evns = np.array(range(2, len(_s_2), 2), np.int64)
|
|
561
|
+
|
|
562
|
+
# Double the are under the curve, and subtract the double counted bit.
|
|
563
|
+
_gbdry_area_simpson = 2 * _gbd_step_sz * (
|
|
564
|
+
(4 / 3) * np.sum(_s_2.take(_gbdry_odds))
|
|
565
|
+
+ (2 / 3) * np.sum(_s_2.take(_gbdry_evns))
|
|
566
|
+
+ (1 / 3) * np.sum(_s_2.take(_gbdry_ends))
|
|
567
|
+
) - np.power(_s_mid, 2)
|
|
568
|
+
|
|
569
|
+
return GuidelinesBoundary(_gbdry_points, round(float(_gbdry_area_simpson), dps))
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def shrratio_boundary_xact_avg_mp(
|
|
573
|
+
_delta_star: float | decimal.Decimal | MPFloat = 0.075,
|
|
574
|
+
_r_val: float = DEFAULT_REC_RATIO,
|
|
575
|
+
/,
|
|
576
|
+
*,
|
|
577
|
+
recapture_form: Literal["inside-out", "proportional"] = "inside-out",
|
|
578
|
+
dps: int = 5,
|
|
579
|
+
) -> GuidelinesBoundary:
|
|
580
|
+
"""
|
|
581
|
+
Share combinations for the simple average GUPPI boundary with symmetric
|
|
582
|
+
merging-firm margins.
|
|
583
|
+
|
|
584
|
+
Notes
|
|
585
|
+
-----
|
|
586
|
+
An analytical expression for the exact average boundary is derived
|
|
587
|
+
and plotted from the y-intercept to the ray of symmetry as follows::
|
|
588
|
+
|
|
589
|
+
from sympy import latex, plot as symplot, solve, symbols
|
|
590
|
+
|
|
591
|
+
s_1, s_2 = symbols("s_1 s_2")
|
|
592
|
+
|
|
593
|
+
g_val, r_val, m_val = 0.06, 0.80, 0.30
|
|
594
|
+
d_hat = g_val / (r_val * m_val)
|
|
595
|
+
|
|
596
|
+
# recapture_form = "inside-out"
|
|
597
|
+
sag = solve(
|
|
598
|
+
(s_2 / (1 - s_1))
|
|
599
|
+
+ (s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1)))
|
|
600
|
+
- 2 * d_hat,
|
|
601
|
+
s_2
|
|
602
|
+
)[0]
|
|
603
|
+
symplot(
|
|
604
|
+
sag,
|
|
605
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
606
|
+
ylabel=s_2
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# recapture_form = "proportional"
|
|
610
|
+
sag = solve((s_2/(1 - s_1)) + (s_1/(1 - s_2)) - 2 * d_hat, s_2)[0]
|
|
611
|
+
symplot(
|
|
612
|
+
sag,
|
|
613
|
+
(s_1, 0., d_hat / (1 + d_hat)),
|
|
614
|
+
ylabel=s_2
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
Parameters
|
|
618
|
+
----------
|
|
619
|
+
_delta_star
|
|
620
|
+
Share ratio (:math:`\\overline{d} / \\overline{r}`).
|
|
621
|
+
_r_val
|
|
622
|
+
Recapture ratio
|
|
623
|
+
recapture_form
|
|
624
|
+
Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
|
|
625
|
+
value for both merging firms ("proportional").
|
|
626
|
+
dps
|
|
627
|
+
Number of decimal places for rounding returned shares.
|
|
628
|
+
|
|
629
|
+
Returns
|
|
630
|
+
-------
|
|
631
|
+
Array of share-pairs, area under boundary, area under boundary.
|
|
632
|
+
|
|
633
|
+
"""
|
|
634
|
+
|
|
635
|
+
_delta_star = mpf(f"{_delta_star}")
|
|
636
|
+
_s_mid = _delta_star / (1 + _delta_star)
|
|
637
|
+
_gbd_step_sz = mp.power(10, -dps)
|
|
638
|
+
|
|
639
|
+
_gbdry_points_start = np.array([(_s_mid, _s_mid)])
|
|
640
|
+
_s_1 = np.array(mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz), np.float64)
|
|
641
|
+
if recapture_form == "inside-out":
|
|
642
|
+
_s_intcpt = mp.fdiv(
|
|
643
|
+
mp.fsub(
|
|
644
|
+
2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
|
|
645
|
+
),
|
|
646
|
+
2 * mpf(f"{_r_val}"),
|
|
647
|
+
)
|
|
648
|
+
_nr_t1 = 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val)
|
|
649
|
+
|
|
650
|
+
_nr_sqrt_mdr = 4 * _delta_star * _r_val
|
|
651
|
+
_nr_sqrt_mdr2 = _nr_sqrt_mdr * _r_val
|
|
652
|
+
_nr_sqrt_md2r2 = _nr_sqrt_mdr2 * _delta_star
|
|
653
|
+
|
|
654
|
+
_nr_sqrt_t1 = _nr_sqrt_md2r2 * (_s_1**2 - 2 * _s_1 + 1)
|
|
655
|
+
_nr_sqrt_t2 = _nr_sqrt_mdr2 * _s_1 * (_s_1 - 1)
|
|
656
|
+
_nr_sqrt_t3 = _nr_sqrt_mdr * (2 * _s_1 - _s_1**2 - 1)
|
|
657
|
+
_nr_sqrt_t4 = (_s_1**2) * (_r_val**2 - 6 * _r_val + 1)
|
|
658
|
+
_nr_sqrt_t5 = _s_1 * (6 * _r_val - 2) + 1
|
|
659
|
+
|
|
660
|
+
_nr_t2_mdr = _nr_sqrt_t1 + _nr_sqrt_t2 + _nr_sqrt_t3 + _nr_sqrt_t4 + _nr_sqrt_t5
|
|
661
|
+
|
|
662
|
+
# Alternative grouping of terms in np.sqrt
|
|
663
|
+
_nr_sqrt_s1sq = (_s_1**2) * (
|
|
664
|
+
_nr_sqrt_md2r2 + _nr_sqrt_mdr2 - _nr_sqrt_mdr + _r_val**2 - 6 * _r_val + 1
|
|
665
|
+
)
|
|
666
|
+
_nr_sqrt_s1 = _s_1 * (
|
|
667
|
+
-2 * _nr_sqrt_md2r2 - _nr_sqrt_mdr2 + 2 * _nr_sqrt_mdr + 6 * _r_val - 2
|
|
668
|
+
)
|
|
669
|
+
_nr_sqrt_nos1 = _nr_sqrt_md2r2 - _nr_sqrt_mdr + 1
|
|
670
|
+
|
|
671
|
+
_nr_t2_s1 = _nr_sqrt_s1sq + _nr_sqrt_s1 + _nr_sqrt_nos1
|
|
672
|
+
|
|
673
|
+
if not np.isclose(
|
|
674
|
+
np.einsum("i->", _nr_t2_mdr.astype(np.float64)),
|
|
675
|
+
np.einsum("i->", _nr_t2_s1.astype(np.float64)),
|
|
676
|
+
rtol=0,
|
|
677
|
+
atol=0.5 * dps,
|
|
678
|
+
):
|
|
679
|
+
raise RuntimeError(
|
|
680
|
+
"Calculation of sq. root term in exact average GUPPI"
|
|
681
|
+
f"with recapture spec, {f'"{recapture_form}"'} is incorrect."
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
_s_2 = (_nr_t1 - np.sqrt(_nr_t2_s1)) / (2 * _r_val)
|
|
685
|
+
|
|
686
|
+
else:
|
|
687
|
+
_s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
|
|
688
|
+
_s_2 = (
|
|
689
|
+
(1 / 2)
|
|
690
|
+
+ _delta_star
|
|
691
|
+
- _delta_star * _s_1
|
|
692
|
+
- np.sqrt(
|
|
693
|
+
((_delta_star**2) - 1) * (_s_1**2)
|
|
694
|
+
+ (-2 * (_delta_star**2) + _delta_star + 1) * _s_1
|
|
695
|
+
+ (_delta_star**2)
|
|
696
|
+
- _delta_star
|
|
697
|
+
+ (1 / 4)
|
|
698
|
+
)
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
_gbdry_points_inner = np.column_stack((_s_1, _s_2))
|
|
702
|
+
_gbdry_points_end = np.array([(mpf("0.0"), _s_intcpt)], np.float64)
|
|
703
|
+
|
|
704
|
+
_gbdry_points = np.vstack((
|
|
705
|
+
_gbdry_points_end,
|
|
706
|
+
_gbdry_points_inner[::-1],
|
|
707
|
+
_gbdry_points_start,
|
|
708
|
+
np.flip(_gbdry_points_inner, 1),
|
|
709
|
+
np.flip(_gbdry_points_end, 1),
|
|
710
|
+
)).astype(np.float64)
|
|
711
|
+
_s_2 = np.concatenate((np.array([_s_mid], np.float64), _s_2))
|
|
712
|
+
|
|
713
|
+
_gbdry_ends = [0, -1]
|
|
714
|
+
_gbdry_odds = np.array(range(1, len(_s_2), 2), np.int64)
|
|
715
|
+
_gbdry_evns = np.array(range(2, len(_s_2), 2), np.int64)
|
|
716
|
+
|
|
717
|
+
# Double the are under the curve, and subtract the double counted bit.
|
|
718
|
+
_gbdry_area_simpson = 2 * _gbd_step_sz * (
|
|
719
|
+
(4 / 3) * np.sum(_s_2.take(_gbdry_odds))
|
|
720
|
+
+ (2 / 3) * np.sum(_s_2.take(_gbdry_evns))
|
|
721
|
+
+ (1 / 3) * np.sum(_s_2.take(_gbdry_ends))
|
|
722
|
+
) - np.power(_s_mid, 2)
|
|
723
|
+
|
|
724
|
+
return GuidelinesBoundary(_gbdry_points, round(float(_gbdry_area_simpson), dps))
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def shrratio_boundary_min(
|
|
728
|
+
_delta_star: float | decimal.Decimal | MPFloat = 0.075,
|
|
729
|
+
_r_val: float = DEFAULT_REC_RATIO,
|
|
730
|
+
/,
|
|
731
|
+
*,
|
|
732
|
+
recapture_form: str = "inside-out",
|
|
733
|
+
dps: int = 10,
|
|
734
|
+
) -> GuidelinesBoundary:
|
|
735
|
+
"""
|
|
736
|
+
Share combinations on the minimum GUPPI boundary, with symmetric
|
|
737
|
+
merging-firm margins.
|
|
738
|
+
|
|
739
|
+
Notes
|
|
740
|
+
-----
|
|
741
|
+
With symmetric merging-firm margins, the maximum GUPPI boundary is
|
|
742
|
+
defined by the diversion ratio from the smaller merging-firm to the
|
|
743
|
+
larger one, and is hence unaffected by the method of estimating the
|
|
744
|
+
diversion ratio for the larger firm.
|
|
745
|
+
|
|
746
|
+
Parameters
|
|
747
|
+
----------
|
|
748
|
+
_delta_star
|
|
749
|
+
Share ratio (:math:`\\overline{d} / \\overline{r}`).
|
|
750
|
+
_r_val
|
|
751
|
+
Recapture ratio.
|
|
752
|
+
recapture_form
|
|
753
|
+
Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
|
|
754
|
+
value for both merging firms ("proportional").
|
|
755
|
+
dps
|
|
756
|
+
Number of decimal places for rounding returned shares.
|
|
757
|
+
|
|
758
|
+
Returns
|
|
759
|
+
-------
|
|
760
|
+
Array of share-pairs, area under boundary.
|
|
761
|
+
|
|
762
|
+
"""
|
|
763
|
+
|
|
764
|
+
_delta_star = mpf(f"{_delta_star}")
|
|
765
|
+
_s_intcpt = mpf("1.00")
|
|
766
|
+
_s_mid = _delta_star / (1 + _delta_star)
|
|
767
|
+
|
|
768
|
+
if recapture_form == "inside-out":
|
|
769
|
+
# ## Plot envelope of GUPPI boundaries with r_k = r_bar if s_k = min(_s_1, _s_2)
|
|
770
|
+
# ## See (s_i, s_j) in equation~(44), or thereabouts, in paper
|
|
771
|
+
_smin_nr = _delta_star * (1 - _r_val)
|
|
772
|
+
_smax_nr = 1 - _delta_star * _r_val
|
|
773
|
+
_guppi_bdry_env_dr = _smin_nr + _smax_nr
|
|
774
|
+
_s1_pts = np.array(
|
|
775
|
+
(
|
|
776
|
+
0,
|
|
777
|
+
_smin_nr / _guppi_bdry_env_dr,
|
|
778
|
+
_s_mid,
|
|
779
|
+
_smax_nr / _guppi_bdry_env_dr,
|
|
780
|
+
_s_intcpt,
|
|
781
|
+
),
|
|
782
|
+
np.float64,
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
_gbd_area = _s_mid + _s1_pts[1] * (1 - 2 * _s_mid)
|
|
786
|
+
else:
|
|
787
|
+
_s1_pts, _gbd_area = np.array((0, _s_mid, _s_intcpt), np.float64), _s_mid
|
|
788
|
+
|
|
789
|
+
return GuidelinesBoundary(
|
|
790
|
+
np.column_stack((_s1_pts, _s1_pts[::-1])), round(float(_gbd_area), dps)
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def shrratio_boundary_max(
|
|
795
|
+
_delta_star: float | decimal.Decimal | MPFloat = 0.075,
|
|
796
|
+
_r_val: float = DEFAULT_REC_RATIO,
|
|
797
|
+
/,
|
|
798
|
+
*,
|
|
799
|
+
dps: int = 10,
|
|
800
|
+
) -> GuidelinesBoundary:
|
|
801
|
+
"""
|
|
802
|
+
Share combinations on the minimum GUPPI boundary with symmetric
|
|
803
|
+
merging-firm margins.
|
|
804
|
+
|
|
805
|
+
Parameters
|
|
806
|
+
----------
|
|
807
|
+
_delta_star
|
|
808
|
+
Share ratio (:math:`\\overline{d} / \\overline{r}`).
|
|
809
|
+
_r_val
|
|
810
|
+
Recapture ratio.
|
|
811
|
+
dps
|
|
812
|
+
Number of decimal places for rounding returned shares.
|
|
813
|
+
|
|
814
|
+
Returns
|
|
815
|
+
-------
|
|
816
|
+
Array of share-pairs, area under boundary.
|
|
817
|
+
|
|
818
|
+
"""
|
|
819
|
+
|
|
820
|
+
# _r_val is not needed for max boundary, but is specified for consistency
|
|
821
|
+
# of function call with other share-ratio boundary functions
|
|
822
|
+
del _r_val
|
|
823
|
+
_delta_star = mpf(f"{_delta_star}")
|
|
824
|
+
_s_intcpt = _delta_star
|
|
825
|
+
_s_mid = _delta_star / (1 + _delta_star)
|
|
826
|
+
|
|
827
|
+
_s1_pts = (0, _s_mid, _s_intcpt)
|
|
828
|
+
|
|
829
|
+
return GuidelinesBoundary(
|
|
830
|
+
np.column_stack((
|
|
831
|
+
np.array(_s1_pts, np.float64),
|
|
832
|
+
np.array(_s1_pts[::-1], np.float64),
|
|
833
|
+
)),
|
|
834
|
+
round(float(_s_intcpt * _s_mid), dps), # simplified calculation
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _shrratio_boundary_intcpt(
|
|
839
|
+
_s_2_pre: float,
|
|
840
|
+
_delta_star: MPFloat,
|
|
841
|
+
_r_val: MPFloat,
|
|
842
|
+
/,
|
|
843
|
+
*,
|
|
844
|
+
recapture_form: Literal["inside-out", "proportional"],
|
|
845
|
+
agg_method: Literal["arithmetic mean", "geometric mean", "distance"],
|
|
846
|
+
weighting: Literal["cross-product-share", "own-share", None],
|
|
847
|
+
) -> float:
|
|
848
|
+
match weighting:
|
|
849
|
+
case "cross-product-share":
|
|
850
|
+
_s_intcpt: float = _delta_star
|
|
851
|
+
case "own-share":
|
|
852
|
+
_s_intcpt = mpf("1.0")
|
|
853
|
+
case None if agg_method == "distance":
|
|
854
|
+
_s_intcpt = _delta_star * mp.sqrt("2")
|
|
855
|
+
case None if agg_method == "arithmetic mean" and recapture_form == "inside-out":
|
|
856
|
+
_s_intcpt = mp.fdiv(
|
|
857
|
+
mp.fsub(
|
|
858
|
+
2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
|
|
859
|
+
),
|
|
860
|
+
2 * mpf(f"{_r_val}"),
|
|
861
|
+
)
|
|
862
|
+
case None if (
|
|
863
|
+
agg_method == "arithmetic mean" and recapture_form == "proportional"
|
|
864
|
+
):
|
|
865
|
+
_s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
|
|
866
|
+
case _:
|
|
867
|
+
_s_intcpt = _s_2_pre
|
|
868
|
+
|
|
869
|
+
return _s_intcpt
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def lerp[LerpT: (float, MPFloat, ArrayDouble, ArrayBIGINT)](
|
|
873
|
+
_x1: LerpT, _x2: LerpT, _r: float = 0.25, /
|
|
874
|
+
) -> LerpT:
|
|
875
|
+
"""
|
|
876
|
+
From the function of the same name in the C++ standard [2]_
|
|
877
|
+
|
|
878
|
+
Constructs the weighted average, :math:`w_1 x_1 + w_2 x_2`, where
|
|
879
|
+
:math:`w_1 = 1 - r` and :math:`w_2 = r`.
|
|
880
|
+
|
|
881
|
+
Parameters
|
|
882
|
+
----------
|
|
883
|
+
_x1, _x2
|
|
884
|
+
bounds :math:`x_1, x_2` to interpolate between.
|
|
885
|
+
_r
|
|
886
|
+
interpolation weight :math:`r` assigned to :math:`x_2`
|
|
887
|
+
|
|
888
|
+
Returns
|
|
889
|
+
-------
|
|
890
|
+
The linear interpolation, or weighted average,
|
|
891
|
+
:math:`x_1 + r \\cdot (x_1 - x_2) \\equiv (1 - r) \\cdot x_1 + r \\cdot x_2`.
|
|
892
|
+
|
|
893
|
+
Raises
|
|
894
|
+
------
|
|
895
|
+
ValueError
|
|
896
|
+
If the interpolation weight is not in the interval, :math:`[0, 1]`.
|
|
897
|
+
|
|
898
|
+
References
|
|
899
|
+
----------
|
|
900
|
+
|
|
901
|
+
.. [2] C++ Reference, https://en.cppreference.com/w/cpp/numeric/lerp
|
|
902
|
+
|
|
903
|
+
"""
|
|
904
|
+
|
|
905
|
+
if not 0 <= _r <= 1:
|
|
906
|
+
raise ValueError("Specified interpolation weight must lie in [0, 1].")
|
|
907
|
+
elif _r == 0:
|
|
908
|
+
return _x1
|
|
909
|
+
elif _r == 1:
|
|
910
|
+
return _x2
|
|
911
|
+
else:
|
|
912
|
+
return _r * _x2 + (1 - _r) * _x1 # pyright: ignore
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def round_cust(
|
|
916
|
+
_num: float | decimal.Decimal | MPFloat = 0.060215,
|
|
917
|
+
/,
|
|
918
|
+
*,
|
|
919
|
+
frac: float = 0.005,
|
|
920
|
+
rounding_mode: str = "ROUND_HALF_UP",
|
|
921
|
+
) -> decimal.Decimal:
|
|
922
|
+
"""
|
|
923
|
+
Custom rounding, to the nearest 0.5% by default.
|
|
924
|
+
|
|
925
|
+
Parameters
|
|
926
|
+
----------
|
|
927
|
+
_num
|
|
928
|
+
Number to be rounded.
|
|
929
|
+
frac
|
|
930
|
+
Fraction to be rounded to.
|
|
931
|
+
rounding_mode
|
|
932
|
+
Rounding mode, as defined in the :code:`decimal` package.
|
|
933
|
+
|
|
934
|
+
Returns
|
|
935
|
+
-------
|
|
936
|
+
The given number, rounded as specified.
|
|
937
|
+
|
|
938
|
+
Raises
|
|
939
|
+
------
|
|
940
|
+
ValueError
|
|
941
|
+
If rounding mode is not defined in the :code:`decimal` package.
|
|
942
|
+
|
|
943
|
+
Notes
|
|
944
|
+
-----
|
|
945
|
+
Integer-round the quotient, :code:`(_num / frac)` using the specified
|
|
946
|
+
rounding mode. Return the product of the rounded quotient times
|
|
947
|
+
the specified precision, :code:`frac`.
|
|
948
|
+
|
|
949
|
+
"""
|
|
950
|
+
|
|
951
|
+
if rounding_mode not in (
|
|
952
|
+
decimal.ROUND_05UP,
|
|
953
|
+
decimal.ROUND_CEILING,
|
|
954
|
+
decimal.ROUND_DOWN,
|
|
955
|
+
decimal.ROUND_FLOOR,
|
|
956
|
+
decimal.ROUND_HALF_DOWN,
|
|
957
|
+
decimal.ROUND_HALF_EVEN,
|
|
958
|
+
decimal.ROUND_HALF_UP,
|
|
959
|
+
decimal.ROUND_UP,
|
|
960
|
+
):
|
|
961
|
+
raise ValueError(
|
|
962
|
+
f"Value, {f'"{rounding_mode}"'} is invalid for rounding_mode."
|
|
963
|
+
'Documentation for the, "decimal" built-in lists valid rounding modes.'
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
_n, _f, _e = (decimal.Decimal(f"{_g}") for _g in [_num, frac, 1])
|
|
967
|
+
|
|
968
|
+
return _f * (_n / _f).quantize(_e, rounding=rounding_mode)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def boundary_plot(*, mktshares_plot_flag: bool = True) -> tuple[Any, ...]:
|
|
972
|
+
"""Setup basic figure and axes for plots of safe harbor boundaries.
|
|
973
|
+
|
|
974
|
+
See, https://matplotlib.org/stable/tutorials/text/pgf.html
|
|
975
|
+
"""
|
|
976
|
+
|
|
977
|
+
import matplotlib as mpl
|
|
978
|
+
import matplotlib.axes as mpa
|
|
979
|
+
import matplotlib.patches as mpp
|
|
980
|
+
import matplotlib.ticker as mpt
|
|
981
|
+
|
|
982
|
+
mpl.use("pgf")
|
|
983
|
+
import matplotlib.pyplot as _plt # noqa: ICN001
|
|
984
|
+
|
|
985
|
+
_plt.rcParams.update({
|
|
986
|
+
"pgf.rcfonts": False,
|
|
987
|
+
"pgf.texsystem": "lualatex",
|
|
988
|
+
"pgf.preamble": "\n".join([
|
|
989
|
+
R"\pdfvariable minorversion=7",
|
|
990
|
+
R"\usepackage{fontspec}",
|
|
991
|
+
R"\usepackage{luacode}",
|
|
992
|
+
R"\begin{luacode}",
|
|
993
|
+
R"local function embedfull(tfmdata)",
|
|
994
|
+
R' tfmdata.embedding = "full"',
|
|
995
|
+
R"end",
|
|
996
|
+
R"",
|
|
997
|
+
R"luatexbase.add_to_callback("
|
|
998
|
+
R' "luaotfload.patch_font", embedfull, "embedfull"'
|
|
999
|
+
R")",
|
|
1000
|
+
R"\end{luacode}",
|
|
1001
|
+
R"\usepackage{mathtools}",
|
|
1002
|
+
R"\usepackage{unicode-math}",
|
|
1003
|
+
R"\setmathfont[math-style=ISO]{STIX Two Math}",
|
|
1004
|
+
R"\setmainfont{STIX Two Text}",
|
|
1005
|
+
r"\setsansfont{Fira Sans Light}",
|
|
1006
|
+
R"\setmonofont[Scale=MatchLowercase,]{Fira Mono}",
|
|
1007
|
+
R"\defaultfontfeatures[\rmfamily]{",
|
|
1008
|
+
R" Ligatures={TeX, Common},",
|
|
1009
|
+
R" Numbers={Proportional, Lining},",
|
|
1010
|
+
R" }",
|
|
1011
|
+
R"\defaultfontfeatures[\sffamily]{",
|
|
1012
|
+
R" Ligatures={TeX, Common},",
|
|
1013
|
+
R" Numbers={Monospaced, Lining},",
|
|
1014
|
+
R" LetterSpace=0.50,",
|
|
1015
|
+
R" }",
|
|
1016
|
+
R"\usepackage[",
|
|
1017
|
+
R" activate={true, nocompatibility},",
|
|
1018
|
+
R" tracking=true,",
|
|
1019
|
+
R" ]{microtype}",
|
|
1020
|
+
]),
|
|
1021
|
+
})
|
|
1022
|
+
|
|
1023
|
+
# Initialize a canvas with a single figure (set of axes)
|
|
1024
|
+
_fig = _plt.figure(figsize=(5, 5), dpi=600)
|
|
1025
|
+
_ax_out = _fig.add_subplot()
|
|
1026
|
+
|
|
1027
|
+
def _set_axis_def(
|
|
1028
|
+
_ax1: mpa.Axes,
|
|
1029
|
+
/,
|
|
1030
|
+
*,
|
|
1031
|
+
mktshares_plot_flag: bool = False,
|
|
1032
|
+
mktshares_axlbls_flag: bool = False,
|
|
1033
|
+
) -> mpa.Axes:
|
|
1034
|
+
# Set the width of axis grid lines, and tick marks:
|
|
1035
|
+
# both axes, both major and minor ticks
|
|
1036
|
+
# Frame, grid, and face color
|
|
1037
|
+
for _spos0 in "left", "bottom":
|
|
1038
|
+
_ax1.spines[_spos0].set_linewidth(0.5)
|
|
1039
|
+
_ax1.spines[_spos0].set_zorder(5)
|
|
1040
|
+
for _spos1 in "top", "right":
|
|
1041
|
+
_ax1.spines[_spos1].set_linewidth(0.0)
|
|
1042
|
+
_ax1.spines[_spos1].set_zorder(0)
|
|
1043
|
+
_ax1.spines[_spos1].set_visible(False)
|
|
1044
|
+
_ax1.set_facecolor("#E6E6E6")
|
|
1045
|
+
|
|
1046
|
+
_ax1.grid(linewidth=0.5, linestyle=":", color="grey", zorder=1)
|
|
1047
|
+
_ax1.tick_params(axis="x", which="both", width=0.5)
|
|
1048
|
+
_ax1.tick_params(axis="y", which="both", width=0.5)
|
|
1049
|
+
|
|
1050
|
+
# Tick marks skip, size, and rotation
|
|
1051
|
+
# x-axis
|
|
1052
|
+
_plt.setp(
|
|
1053
|
+
_ax1.xaxis.get_majorticklabels(),
|
|
1054
|
+
horizontalalignment="right",
|
|
1055
|
+
fontsize=6,
|
|
1056
|
+
rotation=45,
|
|
1057
|
+
)
|
|
1058
|
+
# y-axis
|
|
1059
|
+
_plt.setp(
|
|
1060
|
+
_ax1.yaxis.get_majorticklabels(), horizontalalignment="right", fontsize=6
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
if mktshares_plot_flag:
|
|
1064
|
+
# Axis labels
|
|
1065
|
+
if mktshares_axlbls_flag:
|
|
1066
|
+
# x-axis
|
|
1067
|
+
_ax1.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
|
|
1068
|
+
_ax1.xaxis.set_label_coords(0.75, -0.1)
|
|
1069
|
+
# y-axis
|
|
1070
|
+
_ax1.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
|
|
1071
|
+
_ax1.yaxis.set_label_coords(-0.1, 0.75)
|
|
1072
|
+
|
|
1073
|
+
# Plot the ray of symmetry
|
|
1074
|
+
_ax1.plot(
|
|
1075
|
+
[0, 1], [0, 1], linewidth=0.5, linestyle=":", color="grey", zorder=1
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
# Axis scale
|
|
1079
|
+
_ax1.set_xlim(0, 1)
|
|
1080
|
+
_ax1.set_ylim(0, 1)
|
|
1081
|
+
_ax1.set_aspect(1.0)
|
|
1082
|
+
|
|
1083
|
+
# Truncate the axis frame to a triangle:
|
|
1084
|
+
_ax1.add_patch(
|
|
1085
|
+
mpp.Rectangle(
|
|
1086
|
+
xy=(1.0025, 0.00),
|
|
1087
|
+
width=1.1 * mp.sqrt(2),
|
|
1088
|
+
height=1.1 * mp.sqrt(2),
|
|
1089
|
+
angle=45,
|
|
1090
|
+
color="white",
|
|
1091
|
+
edgecolor=None,
|
|
1092
|
+
fill=True,
|
|
1093
|
+
clip_on=True,
|
|
1094
|
+
zorder=5,
|
|
1095
|
+
)
|
|
1096
|
+
)
|
|
1097
|
+
# Feasible space is bounded by the other diagonal:
|
|
1098
|
+
_ax1.plot(
|
|
1099
|
+
[0, 1], [1, 0], linestyle="-", linewidth=0.5, color="black", zorder=1
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
# Axis Tick-mark locations
|
|
1103
|
+
# One can supply an argument to mpt.AutoMinorLocator to
|
|
1104
|
+
# specify a fixed number of minor intervals per major interval, e.g.:
|
|
1105
|
+
# minorLocator = mpt.AutoMinorLocator(2)
|
|
1106
|
+
# would lead to a single minor tick between major ticks.
|
|
1107
|
+
_minorLocator = mpt.AutoMinorLocator(5)
|
|
1108
|
+
_majorLocator = mpt.MultipleLocator(0.05)
|
|
1109
|
+
for _axs in _ax1.xaxis, _ax1.yaxis:
|
|
1110
|
+
if _axs == _ax1.xaxis:
|
|
1111
|
+
_majorticklabels_rot = 45
|
|
1112
|
+
elif _axs == _ax1.yaxis:
|
|
1113
|
+
_majorticklabels_rot = 0
|
|
1114
|
+
# x-axis
|
|
1115
|
+
_axs.set_major_locator(_majorLocator)
|
|
1116
|
+
_axs.set_minor_locator(_minorLocator)
|
|
1117
|
+
# It"s always x when specifying the format
|
|
1118
|
+
_axs.set_major_formatter(mpt.StrMethodFormatter("{x:>3.0%}"))
|
|
1119
|
+
|
|
1120
|
+
# Hide every other tick-label
|
|
1121
|
+
for _axl in _ax1.get_xticklabels(), _ax1.get_yticklabels():
|
|
1122
|
+
_plt.setp(_axl[::2], visible=False)
|
|
1123
|
+
|
|
1124
|
+
return _ax1
|
|
1125
|
+
|
|
1126
|
+
_ax_out = _set_axis_def(_ax_out, mktshares_plot_flag=mktshares_plot_flag)
|
|
1127
|
+
|
|
1128
|
+
return _plt, _fig, _ax_out, _set_axis_def
|