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
|
@@ -4,19 +4,24 @@ with a canvas on which to draw boundaries for Guidelines standards.
|
|
|
4
4
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import decimal
|
|
8
|
-
from collections.abc import Callable
|
|
9
7
|
from dataclasses import dataclass
|
|
10
8
|
from importlib.metadata import version
|
|
11
|
-
from typing import
|
|
9
|
+
from typing import Literal, TypeAlias
|
|
12
10
|
|
|
13
11
|
import numpy as np
|
|
14
|
-
from attrs import
|
|
12
|
+
from attrs import field, frozen
|
|
15
13
|
from mpmath import mp, mpf # type: ignore
|
|
16
|
-
from numpy.typing import NDArray
|
|
17
14
|
|
|
18
15
|
from .. import _PKG_NAME, UPPAggrSelector # noqa: TID252
|
|
19
|
-
from . import UPPBoundarySpec
|
|
16
|
+
from . import GuidelinesBoundary, UPPBoundarySpec
|
|
17
|
+
from .guidelines_boundary_functions import (
|
|
18
|
+
dh_area,
|
|
19
|
+
round_cust,
|
|
20
|
+
shrratio_boundary_max,
|
|
21
|
+
shrratio_boundary_min,
|
|
22
|
+
shrratio_boundary_wtd_avg,
|
|
23
|
+
shrratio_boundary_xact_avg,
|
|
24
|
+
)
|
|
20
25
|
|
|
21
26
|
__version__ = version(_PKG_NAME)
|
|
22
27
|
|
|
@@ -24,12 +29,13 @@ __version__ = version(_PKG_NAME)
|
|
|
24
29
|
mp.prec = 80
|
|
25
30
|
mp.trap_complex = True
|
|
26
31
|
|
|
27
|
-
HMGPubYear: TypeAlias = Literal[1992, 2010, 2023]
|
|
32
|
+
HMGPubYear: TypeAlias = Literal[1992, 2004, 2010, 2023]
|
|
28
33
|
|
|
29
34
|
|
|
30
|
-
@dataclass(
|
|
35
|
+
@dataclass(frozen=True)
|
|
31
36
|
class HMGThresholds:
|
|
32
37
|
delta: float
|
|
38
|
+
fc: float
|
|
33
39
|
rec: float
|
|
34
40
|
guppi: float
|
|
35
41
|
divr: float
|
|
@@ -37,31 +43,28 @@ class HMGThresholds:
|
|
|
37
43
|
ipr: float
|
|
38
44
|
|
|
39
45
|
|
|
40
|
-
@
|
|
41
|
-
class GuidelinesBoundary:
|
|
42
|
-
coordinates: NDArray[np.float64]
|
|
43
|
-
area: float
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@dataclass(slots=True, frozen=True)
|
|
47
|
-
class GuidelinesBoundaryCallable:
|
|
48
|
-
boundary_function: Callable[[NDArray[np.float64]], NDArray[np.float64]]
|
|
49
|
-
area: float
|
|
50
|
-
s_naught: float = 0
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@define(slots=True, frozen=True)
|
|
46
|
+
@frozen
|
|
54
47
|
class GuidelinesThresholds:
|
|
55
48
|
"""
|
|
56
49
|
Guidelines threholds by Guidelines publication year
|
|
57
50
|
|
|
58
51
|
ΔHHI, Recapture Rate, GUPPI, Diversion ratio, CMCR, and IPR thresholds
|
|
59
|
-
constructed from concentration standards
|
|
52
|
+
constructed from concentration standards in Guidelines published in
|
|
53
|
+
1992, 2004, 2010, and 2023.
|
|
54
|
+
|
|
55
|
+
The 2004 Guidelines refernced here are the EU Commission
|
|
56
|
+
guidelines on assessment of horizontal mergers. These
|
|
57
|
+
guidelines also define a presumption for mergers with
|
|
58
|
+
post-merger HHI in [1000, 2000) and ΔHHI >= 250 points,
|
|
59
|
+
whi is not modeled here.
|
|
60
|
+
|
|
61
|
+
All other Guidelines modeled here are U.S. merger guidelines.
|
|
62
|
+
|
|
60
63
|
"""
|
|
61
64
|
|
|
62
65
|
pub_year: HMGPubYear
|
|
63
66
|
"""
|
|
64
|
-
Year of publication of the
|
|
67
|
+
Year of publication of the Guidelines
|
|
65
68
|
"""
|
|
66
69
|
|
|
67
70
|
safeharbor: HMGThresholds = field(kw_only=True, default=None)
|
|
@@ -99,6 +102,7 @@ class GuidelinesThresholds:
|
|
|
99
102
|
_hhi_p, _dh_s, _dh_p = {
|
|
100
103
|
1992: (0.18, 0.005, 0.01),
|
|
101
104
|
2010: (0.25, 0.01, 0.02),
|
|
105
|
+
2004: (0.20, 0.015, 0.015),
|
|
102
106
|
2023: (0.18, 0.01, 0.01),
|
|
103
107
|
}[self.pub_year]
|
|
104
108
|
|
|
@@ -107,8 +111,9 @@ class GuidelinesThresholds:
|
|
|
107
111
|
"safeharbor",
|
|
108
112
|
HMGThresholds(
|
|
109
113
|
_dh_s,
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
_fc := int(np.ceil(1 / _hhi_p)),
|
|
115
|
+
_r := round_cust(_fc / (_fc + 1)),
|
|
116
|
+
_g_s := guppi_from_delta(_dh_s, m_star=1.0, r_bar=_r),
|
|
112
117
|
_dr := round_cust(1 / (_fc + 1)),
|
|
113
118
|
_cmcr := 0.03, # Not strictly a Guidelines standard
|
|
114
119
|
_ipr := _g_s, # Not strictly a Guidelines standard
|
|
@@ -122,17 +127,19 @@ class GuidelinesThresholds:
|
|
|
122
127
|
(
|
|
123
128
|
HMGThresholds(
|
|
124
129
|
_dh_i := 2 * (0.5 / _fc) ** 2,
|
|
130
|
+
_fc,
|
|
125
131
|
_r_i := round_cust((_fc - 1 / 2) / (_fc + 1 / 2)),
|
|
126
|
-
_g_i :=
|
|
127
|
-
round_cust((1 / 2) / (_fc
|
|
132
|
+
_g_i := guppi_from_delta(_dh_i, m_star=1.0, r_bar=_r_i),
|
|
133
|
+
round_cust((1 / 2) / (_fc + 1 / 2)),
|
|
128
134
|
_cmcr,
|
|
129
135
|
_g_i,
|
|
130
136
|
)
|
|
131
137
|
if self.pub_year == 2010
|
|
132
138
|
else HMGThresholds(
|
|
133
139
|
_dh_i := 2 * (1 / (_fc + 1)) ** 2,
|
|
140
|
+
_fc,
|
|
134
141
|
_r,
|
|
135
|
-
_g_i :=
|
|
142
|
+
_g_i := guppi_from_delta(_dh_i, m_star=1.0, r_bar=_r),
|
|
136
143
|
_dr,
|
|
137
144
|
_cmcr,
|
|
138
145
|
_g_i,
|
|
@@ -145,8 +152,9 @@ class GuidelinesThresholds:
|
|
|
145
152
|
"presumption",
|
|
146
153
|
HMGThresholds(
|
|
147
154
|
_dh_p,
|
|
155
|
+
_fc,
|
|
148
156
|
_r,
|
|
149
|
-
_g_p :=
|
|
157
|
+
_g_p := guppi_from_delta(_dh_p, m_star=1.0, r_bar=_r),
|
|
150
158
|
_dr,
|
|
151
159
|
_cmcr,
|
|
152
160
|
_ipr := _g_p,
|
|
@@ -154,112 +162,8 @@ class GuidelinesThresholds:
|
|
|
154
162
|
)
|
|
155
163
|
|
|
156
164
|
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
/,
|
|
160
|
-
*,
|
|
161
|
-
frac: float = 0.005,
|
|
162
|
-
rounding_mode: str = "ROUND_HALF_UP",
|
|
163
|
-
) -> float:
|
|
164
|
-
"""
|
|
165
|
-
Custom rounding, to the nearest 0.5% by default.
|
|
166
|
-
|
|
167
|
-
Parameters
|
|
168
|
-
----------
|
|
169
|
-
_num
|
|
170
|
-
Number to be rounded.
|
|
171
|
-
frac
|
|
172
|
-
Fraction to be rounded to.
|
|
173
|
-
rounding_mode
|
|
174
|
-
Rounding mode, as defined in the :code:`decimal` package.
|
|
175
|
-
|
|
176
|
-
Returns
|
|
177
|
-
-------
|
|
178
|
-
The given number, rounded as specified.
|
|
179
|
-
|
|
180
|
-
Raises
|
|
181
|
-
------
|
|
182
|
-
ValueError
|
|
183
|
-
If rounding mode is not defined in the :code:`decimal` package.
|
|
184
|
-
|
|
185
|
-
Notes
|
|
186
|
-
-----
|
|
187
|
-
Integer-round the quotient, :code:`(_num / frac)` using the specified
|
|
188
|
-
rounding mode. Return the product of the rounded quotient times
|
|
189
|
-
the specified precision, :code:`frac`.
|
|
190
|
-
|
|
191
|
-
"""
|
|
192
|
-
|
|
193
|
-
if rounding_mode not in (
|
|
194
|
-
decimal.ROUND_05UP,
|
|
195
|
-
decimal.ROUND_CEILING,
|
|
196
|
-
decimal.ROUND_DOWN,
|
|
197
|
-
decimal.ROUND_FLOOR,
|
|
198
|
-
decimal.ROUND_HALF_DOWN,
|
|
199
|
-
decimal.ROUND_HALF_EVEN,
|
|
200
|
-
decimal.ROUND_HALF_UP,
|
|
201
|
-
decimal.ROUND_UP,
|
|
202
|
-
):
|
|
203
|
-
raise ValueError(
|
|
204
|
-
f"Value, {f'"{rounding_mode}"'} is invalid for rounding_mode."
|
|
205
|
-
"Documentation for the, \"decimal\" built-in lists valid rounding modes."
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
_n, _f, _e = (decimal.Decimal(f"{_g}") for _g in [_num, frac, 1])
|
|
209
|
-
|
|
210
|
-
return float(_f * (_n / _f).quantize(_e, rounding=rounding_mode))
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def lerp(
|
|
214
|
-
_x1: int | float | mpf | NDArray[np.float64 | np.int64] = 3,
|
|
215
|
-
_x2: int | float | mpf | NDArray[np.float64 | np.int64] = 1,
|
|
216
|
-
_r: float | mpf = 0.25,
|
|
217
|
-
/,
|
|
218
|
-
) -> float | mpf | NDArray[np.float64]:
|
|
219
|
-
"""
|
|
220
|
-
From the function of the same name in the C++ standard [2]_
|
|
221
|
-
|
|
222
|
-
Constructs the weighted average, :math:`w_1 x_1 + w_2 x_2`, where
|
|
223
|
-
:math:`w_1 = 1 - r` and :math:`w_2 = r`.
|
|
224
|
-
|
|
225
|
-
Parameters
|
|
226
|
-
----------
|
|
227
|
-
_x1, _x2
|
|
228
|
-
bounds :math:`x_1, x_2` to interpolate between.
|
|
229
|
-
_r
|
|
230
|
-
interpolation weight :math:`r` assigned to :math:`x_2`
|
|
231
|
-
|
|
232
|
-
Returns
|
|
233
|
-
-------
|
|
234
|
-
The linear interpolation, or weighted average,
|
|
235
|
-
:math:`x_1 + r \\cdot (x_1 - x_2) \\equiv (1 - r) \\cdot x_1 + r \\cdot x_2`.
|
|
236
|
-
|
|
237
|
-
Raises
|
|
238
|
-
------
|
|
239
|
-
ValueError
|
|
240
|
-
If the interpolation weight is not in the interval, :math:`[0, 1]`.
|
|
241
|
-
|
|
242
|
-
References
|
|
243
|
-
----------
|
|
244
|
-
|
|
245
|
-
.. [2] C++ Reference, https://en.cppreference.com/w/cpp/numeric/lerp
|
|
246
|
-
|
|
247
|
-
"""
|
|
248
|
-
|
|
249
|
-
if not 0 <= _r <= 1:
|
|
250
|
-
raise ValueError("Specified interpolation weight must lie in [0, 1].")
|
|
251
|
-
elif _r == 0:
|
|
252
|
-
return _x1
|
|
253
|
-
elif _r == 1:
|
|
254
|
-
return _x2
|
|
255
|
-
elif _r == 0.5:
|
|
256
|
-
return 1 / 2 * (_x1 + _x2)
|
|
257
|
-
else:
|
|
258
|
-
return _r * _x2 + (1 - _r) * _x1
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def gbd_from_dsf(
|
|
262
|
-
_deltasf: float = 0.01, /, *, m_star: float = 1.00, r_bar: float = 0.80
|
|
165
|
+
def guppi_from_delta(
|
|
166
|
+
_delta_bound: float = 0.01, /, *, m_star: float = 1.00, r_bar: float = 0.855
|
|
263
167
|
) -> float:
|
|
264
168
|
"""
|
|
265
169
|
Translate ∆HHI bound to GUPPI bound.
|
|
@@ -279,18 +183,18 @@ def gbd_from_dsf(
|
|
|
279
183
|
|
|
280
184
|
"""
|
|
281
185
|
return round_cust(
|
|
282
|
-
m_star * r_bar * (_s_m := np.sqrt(
|
|
186
|
+
m_star * r_bar * (_s_m := np.sqrt(_delta_bound / 2)) / (1 - _s_m),
|
|
283
187
|
frac=0.005,
|
|
284
188
|
rounding_mode="ROUND_HALF_DOWN",
|
|
285
189
|
)
|
|
286
190
|
|
|
287
191
|
|
|
288
|
-
def
|
|
289
|
-
|
|
192
|
+
def critical_share_ratio(
|
|
193
|
+
_guppi_bound: float = 0.075,
|
|
290
194
|
/,
|
|
291
195
|
*,
|
|
292
196
|
m_star: float = 1.00,
|
|
293
|
-
r_bar: float =
|
|
197
|
+
r_bar: float = 1.00,
|
|
294
198
|
frac: float = 1e-16,
|
|
295
199
|
) -> mpf:
|
|
296
200
|
"""
|
|
@@ -298,7 +202,7 @@ def critical_shrratio(
|
|
|
298
202
|
|
|
299
203
|
Parameters
|
|
300
204
|
----------
|
|
301
|
-
|
|
205
|
+
_guppi_bound
|
|
302
206
|
Specified GUPPI bound.
|
|
303
207
|
m_star
|
|
304
208
|
Parametric price-cost margin.
|
|
@@ -311,18 +215,20 @@ def critical_shrratio(
|
|
|
311
215
|
for given margin and recapture rate.
|
|
312
216
|
|
|
313
217
|
"""
|
|
314
|
-
return round_cust(
|
|
218
|
+
return round_cust(
|
|
219
|
+
mpf(f"{_guppi_bound}") / mp.fmul(f"{m_star}", f"{r_bar}"), frac=frac
|
|
220
|
+
)
|
|
315
221
|
|
|
316
222
|
|
|
317
|
-
def
|
|
318
|
-
|
|
223
|
+
def share_from_guppi(
|
|
224
|
+
_guppi_bound: float = 0.065, /, *, m_star: float = 1.00, r_bar: float = 0.855
|
|
319
225
|
) -> float:
|
|
320
226
|
"""
|
|
321
227
|
Symmetric-firm share for given GUPPI, margin, and recapture rate.
|
|
322
228
|
|
|
323
229
|
Parameters
|
|
324
230
|
----------
|
|
325
|
-
|
|
231
|
+
_guppi_bound
|
|
326
232
|
GUPPI bound.
|
|
327
233
|
m_star
|
|
328
234
|
Parametric price-cost margin.
|
|
@@ -338,249 +244,12 @@ def shr_from_gbd(
|
|
|
338
244
|
"""
|
|
339
245
|
|
|
340
246
|
return round_cust(
|
|
341
|
-
(_d0 :=
|
|
247
|
+
(_d0 := critical_share_ratio(_guppi_bound, m_star=m_star, r_bar=r_bar))
|
|
248
|
+
/ (1 + _d0)
|
|
342
249
|
)
|
|
343
250
|
|
|
344
251
|
|
|
345
|
-
def
|
|
346
|
-
"""Setup basic figure and axes for plots of safe harbor boundaries.
|
|
347
|
-
|
|
348
|
-
See, https://matplotlib.org/stable/tutorials/text/pgf.html
|
|
349
|
-
"""
|
|
350
|
-
|
|
351
|
-
import matplotlib as mpl
|
|
352
|
-
import matplotlib.axes as mpa
|
|
353
|
-
import matplotlib.patches as mpp
|
|
354
|
-
import matplotlib.ticker as mpt
|
|
355
|
-
|
|
356
|
-
mpl.use("pgf")
|
|
357
|
-
import matplotlib.pyplot as plt
|
|
358
|
-
# from matplotlib.backends.backend_pgf import FigureCanvasPgf
|
|
359
|
-
|
|
360
|
-
# from matplotlib.backends.backend_pgf import FigureCanvasPgf
|
|
361
|
-
# mpl.backend_bases.register_backend("pdf", FigureCanvasPgf)
|
|
362
|
-
# import matplotlib.pyplot as plt
|
|
363
|
-
|
|
364
|
-
plt.rcParams.update({
|
|
365
|
-
"pgf.rcfonts": False,
|
|
366
|
-
"pgf.texsystem": "lualatex",
|
|
367
|
-
"pgf.preamble": "\n".join([
|
|
368
|
-
R"\pdfvariable minorversion=7",
|
|
369
|
-
R"\usepackage{fontspec}",
|
|
370
|
-
R"\usepackage{luacode}",
|
|
371
|
-
R"\begin{luacode}",
|
|
372
|
-
R"local function embedfull(tfmdata)",
|
|
373
|
-
R' tfmdata.embedding = "full"',
|
|
374
|
-
R"end",
|
|
375
|
-
R"",
|
|
376
|
-
R"luatexbase.add_to_callback("
|
|
377
|
-
R' "luaotfload.patch_font", embedfull, "embedfull"'
|
|
378
|
-
R")",
|
|
379
|
-
R"\end{luacode}",
|
|
380
|
-
R"\usepackage{mathtools}",
|
|
381
|
-
R"\usepackage{unicode-math}",
|
|
382
|
-
R"\setmathfont[math-style=ISO]{STIX Two Math}",
|
|
383
|
-
R"\setmainfont{STIX Two Text}",
|
|
384
|
-
r"\setsansfont{Fira Sans Light}",
|
|
385
|
-
R"\setmonofont[Scale=MatchLowercase,]{Fira Mono}",
|
|
386
|
-
R"\defaultfontfeatures[\rmfamily]{",
|
|
387
|
-
R" Ligatures={TeX, Common},",
|
|
388
|
-
R" Numbers={Proportional, Lining},",
|
|
389
|
-
R" }",
|
|
390
|
-
R"\defaultfontfeatures[\sffamily]{",
|
|
391
|
-
R" Ligatures={TeX, Common},",
|
|
392
|
-
R" Numbers={Monospaced, Lining},",
|
|
393
|
-
R" LetterSpace=0.50,",
|
|
394
|
-
R" }",
|
|
395
|
-
R"\usepackage[",
|
|
396
|
-
R" activate={true, nocompatibility},",
|
|
397
|
-
R" tracking=true,",
|
|
398
|
-
R" ]{microtype}",
|
|
399
|
-
]),
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
# Initialize a canvas with a single figure (set of axes)
|
|
403
|
-
_fig = plt.figure(figsize=(5, 5), dpi=600)
|
|
404
|
-
_ax_out = _fig.add_subplot()
|
|
405
|
-
|
|
406
|
-
def _set_axis_def(
|
|
407
|
-
_ax1: mpa.Axes,
|
|
408
|
-
/,
|
|
409
|
-
*,
|
|
410
|
-
mktshares_plot_flag: bool = False,
|
|
411
|
-
mktshares_axlbls_flag: bool = False,
|
|
412
|
-
) -> mpa.Axes:
|
|
413
|
-
# Set the width of axis gridlines, and tick marks:
|
|
414
|
-
# both axes, both major and minor ticks
|
|
415
|
-
# Frame, grid, and facecolor
|
|
416
|
-
for _spos0 in "left", "bottom":
|
|
417
|
-
_ax1.spines[_spos0].set_linewidth(0.5)
|
|
418
|
-
_ax1.spines[_spos0].set_zorder(5)
|
|
419
|
-
for _spos1 in "top", "right":
|
|
420
|
-
_ax1.spines[_spos1].set_linewidth(0.0)
|
|
421
|
-
_ax1.spines[_spos1].set_zorder(0)
|
|
422
|
-
_ax1.spines[_spos1].set_visible(False)
|
|
423
|
-
_ax1.set_facecolor("#E6E6E6")
|
|
424
|
-
|
|
425
|
-
_ax1.grid(linewidth=0.5, linestyle=":", color="grey", zorder=1)
|
|
426
|
-
_ax1.tick_params(axis="x", which="both", width=0.5)
|
|
427
|
-
_ax1.tick_params(axis="y", which="both", width=0.5)
|
|
428
|
-
|
|
429
|
-
# Tick marks skip, size, and rotation
|
|
430
|
-
# x-axis
|
|
431
|
-
plt.setp(
|
|
432
|
-
_ax1.xaxis.get_majorticklabels(),
|
|
433
|
-
horizontalalignment="right",
|
|
434
|
-
fontsize=6,
|
|
435
|
-
rotation=45,
|
|
436
|
-
)
|
|
437
|
-
# y-axis
|
|
438
|
-
plt.setp(
|
|
439
|
-
_ax1.yaxis.get_majorticklabels(), horizontalalignment="right", fontsize=6
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
if mktshares_plot_flag:
|
|
443
|
-
# Axis labels
|
|
444
|
-
if mktshares_axlbls_flag:
|
|
445
|
-
# x-axis
|
|
446
|
-
_ax1.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
|
|
447
|
-
_ax1.xaxis.set_label_coords(0.75, -0.1)
|
|
448
|
-
# y-axis
|
|
449
|
-
_ax1.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
|
|
450
|
-
_ax1.yaxis.set_label_coords(-0.1, 0.75)
|
|
451
|
-
|
|
452
|
-
# Plot the ray of symmetry
|
|
453
|
-
_ax1.plot(
|
|
454
|
-
[0, 1], [0, 1], linewidth=0.5, linestyle=":", color="grey", zorder=1
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
# Axis scale
|
|
458
|
-
_ax1.set_xlim(0, 1)
|
|
459
|
-
_ax1.set_ylim(0, 1)
|
|
460
|
-
_ax1.set_aspect(1.0)
|
|
461
|
-
|
|
462
|
-
# Truncate the axis frame to a triangle:
|
|
463
|
-
_ax1.add_patch(
|
|
464
|
-
mpp.Rectangle(
|
|
465
|
-
xy=(1.0025, 0.00),
|
|
466
|
-
width=1.1 * mp.sqrt(2),
|
|
467
|
-
height=1.1 * mp.sqrt(2),
|
|
468
|
-
angle=45,
|
|
469
|
-
color="white",
|
|
470
|
-
edgecolor=None,
|
|
471
|
-
fill=True,
|
|
472
|
-
clip_on=True,
|
|
473
|
-
zorder=5,
|
|
474
|
-
)
|
|
475
|
-
)
|
|
476
|
-
# Feasible space is bounded by the other diagonal:
|
|
477
|
-
_ax1.plot(
|
|
478
|
-
[0, 1], [1, 0], linestyle="-", linewidth=0.5, color="black", zorder=1
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
# Axis Tick-mark locations
|
|
482
|
-
# One can supply an argument to mpt.AutoMinorLocator to
|
|
483
|
-
# specify a fixed number of minor intervals per major interval, e.g.:
|
|
484
|
-
# minorLocator = mpt.AutoMinorLocator(2)
|
|
485
|
-
# would lead to a single minor tick between major ticks.
|
|
486
|
-
_minorLocator = mpt.AutoMinorLocator(5)
|
|
487
|
-
_majorLocator = mpt.MultipleLocator(0.05)
|
|
488
|
-
for _axs in _ax1.xaxis, _ax1.yaxis:
|
|
489
|
-
if _axs == _ax1.xaxis:
|
|
490
|
-
_majorticklabels_rot = 45
|
|
491
|
-
elif _axs == _ax1.yaxis:
|
|
492
|
-
_majorticklabels_rot = 0
|
|
493
|
-
# x-axis
|
|
494
|
-
_axs.set_major_locator(_majorLocator)
|
|
495
|
-
_axs.set_minor_locator(_minorLocator)
|
|
496
|
-
# It"s always x when specifying the format
|
|
497
|
-
_axs.set_major_formatter(mpt.StrMethodFormatter("{x:>3.0%}"))
|
|
498
|
-
|
|
499
|
-
# Hide every other tick-label
|
|
500
|
-
for _axl in _ax1.get_xticklabels(), _ax1.get_yticklabels():
|
|
501
|
-
plt.setp(_axl[::2], visible=False)
|
|
502
|
-
|
|
503
|
-
return _ax1
|
|
504
|
-
|
|
505
|
-
_ax_out = _set_axis_def(_ax_out, mktshares_plot_flag=mktshares_plot_flag)
|
|
506
|
-
|
|
507
|
-
return plt, _fig, _ax_out, _set_axis_def
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
def dh_area(_dh_val: float = 0.01, /, *, prec: int = 9) -> float:
|
|
511
|
-
R"""
|
|
512
|
-
Area under the ΔHHI boundary.
|
|
513
|
-
|
|
514
|
-
When the given ΔHHI bound matches a Guidelines standard,
|
|
515
|
-
the area under the boundary is half the intrinsic clearance rate
|
|
516
|
-
for the ΔHHI safeharbor.
|
|
517
|
-
|
|
518
|
-
Notes
|
|
519
|
-
-----
|
|
520
|
-
To derive the knots, :math:`(s^0_1, s^1_1), (s^1_1, s^0_1)`
|
|
521
|
-
of the ΔHHI boundary, i.e., the points where it intersects
|
|
522
|
-
the merger-to-monopoly boundary, solve
|
|
523
|
-
|
|
524
|
-
.. math::
|
|
525
|
-
|
|
526
|
-
2 s1 s_2 &= ΔHHI\\
|
|
527
|
-
s_1 + s_2 &= 1
|
|
528
|
-
|
|
529
|
-
Parameters
|
|
530
|
-
----------
|
|
531
|
-
_dh_val
|
|
532
|
-
Change in concentration.
|
|
533
|
-
prec
|
|
534
|
-
Specified precision in decimal places.
|
|
535
|
-
|
|
536
|
-
Returns
|
|
537
|
-
-------
|
|
538
|
-
Area under ΔHHI boundary.
|
|
539
|
-
|
|
540
|
-
"""
|
|
541
|
-
|
|
542
|
-
_dh_val = mpf(f"{_dh_val}")
|
|
543
|
-
_s_naught = (1 - mp.sqrt(1 - 2 * _dh_val)) / 2
|
|
544
|
-
|
|
545
|
-
return round(
|
|
546
|
-
float(_s_naught + (_dh_val / 2) * (mp.ln(1 - _s_naught) - mp.ln(_s_naught))),
|
|
547
|
-
prec,
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
def dh_area_quad(_dh_val: float = 0.01, /, *, prec: int = 9) -> float:
|
|
552
|
-
"""
|
|
553
|
-
Area under the ΔHHI boundary.
|
|
554
|
-
|
|
555
|
-
When the given ΔHHI bound matches a Guidelines safeharbor,
|
|
556
|
-
the area under the boundary is half the intrinsic clearance rate
|
|
557
|
-
for the ΔHHI safeharbor.
|
|
558
|
-
|
|
559
|
-
Parameters
|
|
560
|
-
----------
|
|
561
|
-
_dh_val
|
|
562
|
-
Merging-firms' ΔHHI bound.
|
|
563
|
-
prec
|
|
564
|
-
Specified precision in decimal places.
|
|
565
|
-
|
|
566
|
-
Returns
|
|
567
|
-
-------
|
|
568
|
-
Area under ΔHHI boundary.
|
|
569
|
-
|
|
570
|
-
"""
|
|
571
|
-
|
|
572
|
-
_dh_val = mpf(f"{_dh_val}")
|
|
573
|
-
_s_naught = (1 - mp.sqrt(1 - 2 * _dh_val)) / 2
|
|
574
|
-
|
|
575
|
-
return round(
|
|
576
|
-
float(
|
|
577
|
-
_s_naught + mp.quad(lambda x: _dh_val / (2 * x), [_s_naught, 1 - _s_naught])
|
|
578
|
-
),
|
|
579
|
-
prec,
|
|
580
|
-
)
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
def delta_hhi_boundary(
|
|
252
|
+
def hhi_delta_boundary(
|
|
584
253
|
_dh_val: float = 0.01, /, *, prec: int = 5
|
|
585
254
|
) -> GuidelinesBoundary:
|
|
586
255
|
"""
|
|
@@ -691,29 +360,56 @@ def hhi_pre_contrib_boundary(
|
|
|
691
360
|
)
|
|
692
361
|
|
|
693
362
|
|
|
694
|
-
def
|
|
363
|
+
def hhi_post_contrib_boundary(
|
|
364
|
+
_hhi_contrib: float = 0.800, /, *, bdry_dps: int = 10
|
|
365
|
+
) -> GuidelinesBoundary:
|
|
366
|
+
"""
|
|
367
|
+
Share combinations on the postmerger HHI contribution boundary.
|
|
368
|
+
|
|
369
|
+
The post-merger HHI contribution boundary is identical to the
|
|
370
|
+
combined-share boundary.
|
|
371
|
+
|
|
372
|
+
Parameters
|
|
373
|
+
----------
|
|
374
|
+
_hhi_contrib:
|
|
375
|
+
Merging-firms' pre-merger HHI contribution bound.
|
|
376
|
+
bdry_dps
|
|
377
|
+
Number of decimal places for rounding reported shares.
|
|
378
|
+
|
|
379
|
+
Returns
|
|
380
|
+
-------
|
|
381
|
+
Array of share-pairs, area under boundary.
|
|
382
|
+
|
|
383
|
+
"""
|
|
384
|
+
return combined_share_boundary(np.sqrt(_hhi_contrib), bdry_dps=bdry_dps)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def diversion_ratio_boundary(_bdry_spec: UPPBoundarySpec) -> GuidelinesBoundary:
|
|
388
|
+
_share_ratio = critical_share_ratio(
|
|
389
|
+
_bdry_spec.diversion_ratio, r_bar=_bdry_spec.rec
|
|
390
|
+
)
|
|
695
391
|
match _bdry_spec.agg_method:
|
|
696
392
|
case UPPAggrSelector.AVG:
|
|
697
393
|
return shrratio_boundary_xact_avg(
|
|
698
|
-
|
|
394
|
+
_share_ratio,
|
|
699
395
|
_bdry_spec.rec,
|
|
700
396
|
recapture_form=_bdry_spec.recapture_form.value, # type: ignore
|
|
701
397
|
prec=_bdry_spec.precision,
|
|
702
398
|
)
|
|
703
399
|
case UPPAggrSelector.MAX:
|
|
704
400
|
return shrratio_boundary_max(
|
|
705
|
-
|
|
401
|
+
_share_ratio, _bdry_spec.rec, prec=_bdry_spec.precision
|
|
706
402
|
)
|
|
707
403
|
case UPPAggrSelector.MIN:
|
|
708
404
|
return shrratio_boundary_min(
|
|
709
|
-
|
|
405
|
+
_share_ratio,
|
|
710
406
|
_bdry_spec.rec,
|
|
711
407
|
recapture_form=_bdry_spec.recapture_form.value, # type: ignore
|
|
712
408
|
prec=_bdry_spec.precision,
|
|
713
409
|
)
|
|
714
410
|
case UPPAggrSelector.DIS:
|
|
715
411
|
return shrratio_boundary_wtd_avg(
|
|
716
|
-
|
|
412
|
+
_share_ratio,
|
|
717
413
|
_bdry_spec.rec,
|
|
718
414
|
agg_method="distance",
|
|
719
415
|
weighting=None,
|
|
@@ -734,516 +430,10 @@ def shrratio_boundary(_bdry_spec: UPPBoundarySpec) -> GuidelinesBoundary:
|
|
|
734
430
|
)
|
|
735
431
|
|
|
736
432
|
return shrratio_boundary_wtd_avg(
|
|
737
|
-
|
|
433
|
+
_share_ratio,
|
|
738
434
|
_bdry_spec.rec,
|
|
739
435
|
agg_method=_agg_method, # type: ignore
|
|
740
436
|
weighting=_weighting, # type: ignore
|
|
741
437
|
recapture_form=_bdry_spec.recapture_form.value, # type: ignore
|
|
742
438
|
prec=_bdry_spec.precision,
|
|
743
439
|
)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
def shrratio_boundary_max(
|
|
747
|
-
_delta_star: float = 0.075, _r_val: float = 0.80, /, *, prec: int = 10
|
|
748
|
-
) -> GuidelinesBoundary:
|
|
749
|
-
"""
|
|
750
|
-
Share combinations on the minimum GUPPI boundary with symmetric
|
|
751
|
-
merging-firm margins.
|
|
752
|
-
|
|
753
|
-
Parameters
|
|
754
|
-
----------
|
|
755
|
-
_delta_star
|
|
756
|
-
Margin-adjusted benchmark share ratio.
|
|
757
|
-
_r_val
|
|
758
|
-
Recapture ratio.
|
|
759
|
-
prec
|
|
760
|
-
Number of decimal places for rounding returned shares.
|
|
761
|
-
|
|
762
|
-
Returns
|
|
763
|
-
-------
|
|
764
|
-
Array of share-pairs, area under boundary.
|
|
765
|
-
|
|
766
|
-
"""
|
|
767
|
-
|
|
768
|
-
# _r_val is not needed for max boundary, but is specified for consistency
|
|
769
|
-
# of function call with other shrratio_mgnsym_boundary functions
|
|
770
|
-
del _r_val
|
|
771
|
-
_delta_star = mpf(f"{_delta_star}")
|
|
772
|
-
_s_intcpt = _delta_star
|
|
773
|
-
_s_mid = _delta_star / (1 + _delta_star)
|
|
774
|
-
|
|
775
|
-
_s1_pts = (0, _s_mid, _s_intcpt)
|
|
776
|
-
|
|
777
|
-
return GuidelinesBoundary(
|
|
778
|
-
np.column_stack((
|
|
779
|
-
np.array(_s1_pts, np.float64),
|
|
780
|
-
np.array(_s1_pts[::-1], np.float64),
|
|
781
|
-
)),
|
|
782
|
-
round(float(_s_intcpt * _s_mid), prec), # simplified calculation
|
|
783
|
-
)
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
def shrratio_boundary_min(
|
|
787
|
-
_delta_star: float = 0.075,
|
|
788
|
-
_r_val: float = 0.80,
|
|
789
|
-
/,
|
|
790
|
-
*,
|
|
791
|
-
recapture_form: str = "inside-out",
|
|
792
|
-
prec: int = 10,
|
|
793
|
-
) -> GuidelinesBoundary:
|
|
794
|
-
"""
|
|
795
|
-
Share combinations on the minimum GUPPI boundary, with symmetric
|
|
796
|
-
merging-firm margins.
|
|
797
|
-
|
|
798
|
-
Notes
|
|
799
|
-
-----
|
|
800
|
-
With symmetric merging-firm margins, the maximum GUPPI boundary is
|
|
801
|
-
defined by the diversion ratio from the smaller merging-firm to the
|
|
802
|
-
larger one, and is hence unaffected by the method of estimating the
|
|
803
|
-
diversion ratio for the larger firm.
|
|
804
|
-
|
|
805
|
-
Parameters
|
|
806
|
-
----------
|
|
807
|
-
_delta_star
|
|
808
|
-
Margin-adjusted benchmark share ratio.
|
|
809
|
-
_r_val
|
|
810
|
-
Recapture ratio.
|
|
811
|
-
recapture_form
|
|
812
|
-
Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
|
|
813
|
-
value for both merging firms ("proportional").
|
|
814
|
-
prec
|
|
815
|
-
Number of decimal places for rounding returned shares.
|
|
816
|
-
|
|
817
|
-
Returns
|
|
818
|
-
-------
|
|
819
|
-
Array of share-pairs, area under boundary.
|
|
820
|
-
|
|
821
|
-
"""
|
|
822
|
-
|
|
823
|
-
_delta_star = mpf(f"{_delta_star}")
|
|
824
|
-
_s_intcpt = mpf("1.00")
|
|
825
|
-
_s_mid = _delta_star / (1 + _delta_star)
|
|
826
|
-
|
|
827
|
-
if recapture_form == "inside-out":
|
|
828
|
-
# ## Plot envelope of GUPPI boundaries with r_k = r_bar if s_k = min(_s_1, _s_2)
|
|
829
|
-
# ## See (s_i, s_j) in equation~(44), or thereabouts, in paper
|
|
830
|
-
_smin_nr = _delta_star * (1 - _r_val)
|
|
831
|
-
_smax_nr = 1 - _delta_star * _r_val
|
|
832
|
-
_guppi_bdry_env_dr = _smin_nr + _smax_nr
|
|
833
|
-
_s1_pts = np.array(
|
|
834
|
-
(
|
|
835
|
-
0,
|
|
836
|
-
_smin_nr / _guppi_bdry_env_dr,
|
|
837
|
-
_s_mid,
|
|
838
|
-
_smax_nr / _guppi_bdry_env_dr,
|
|
839
|
-
_s_intcpt,
|
|
840
|
-
),
|
|
841
|
-
np.float64,
|
|
842
|
-
)
|
|
843
|
-
|
|
844
|
-
_gbd_area = _s_mid + _s1_pts[1] * (1 - 2 * _s_mid)
|
|
845
|
-
else:
|
|
846
|
-
_s1_pts, _gbd_area = np.array((0, _s_mid, _s_intcpt), np.float64), _s_mid
|
|
847
|
-
|
|
848
|
-
return GuidelinesBoundary(
|
|
849
|
-
np.column_stack((_s1_pts, _s1_pts[::-1])), round(float(_gbd_area), prec)
|
|
850
|
-
)
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
def shrratio_boundary_wtd_avg(
|
|
854
|
-
_delta_star: float = 0.075,
|
|
855
|
-
_r_val: float = 0.80,
|
|
856
|
-
/,
|
|
857
|
-
*,
|
|
858
|
-
agg_method: Literal["arithmetic", "geometric", "distance"] = "arithmetic",
|
|
859
|
-
weighting: Literal["own-share", "cross-product-share"] | None = "own-share",
|
|
860
|
-
recapture_form: Literal["inside-out", "proportional"] = "inside-out",
|
|
861
|
-
prec: int = 5,
|
|
862
|
-
) -> GuidelinesBoundary:
|
|
863
|
-
"""
|
|
864
|
-
Share combinations for the share-weighted average GUPPI boundary with symmetric
|
|
865
|
-
merging-firm margins.
|
|
866
|
-
|
|
867
|
-
Parameters
|
|
868
|
-
----------
|
|
869
|
-
_delta_star
|
|
870
|
-
corollary to GUPPI bound (:math:`\\overline{g} / (m^* \\cdot \\overline{r})`)
|
|
871
|
-
_r_val
|
|
872
|
-
recapture ratio
|
|
873
|
-
agg_method
|
|
874
|
-
Whether "arithmetic", "geometric", or "distance".
|
|
875
|
-
weighting
|
|
876
|
-
Whether "own-share" or "cross-product-share" (or None for simple, unweighted average).
|
|
877
|
-
recapture_form
|
|
878
|
-
Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
|
|
879
|
-
value for both merging firms ("proportional").
|
|
880
|
-
prec
|
|
881
|
-
Number of decimal places for rounding returned shares and area.
|
|
882
|
-
|
|
883
|
-
Returns
|
|
884
|
-
-------
|
|
885
|
-
Array of share-pairs, area under boundary.
|
|
886
|
-
|
|
887
|
-
Notes
|
|
888
|
-
-----
|
|
889
|
-
An analytical expression for the share-weighted arithmetic mean boundary
|
|
890
|
-
is derived and plotted from y-intercept to the ray of symmetry as follows::
|
|
891
|
-
|
|
892
|
-
from sympy import plot as symplot, solve, symbols
|
|
893
|
-
s_1, s_2 = symbols("s_1 s_2", positive=True)
|
|
894
|
-
|
|
895
|
-
g_val, r_val, m_val = 0.06, 0.80, 0.30
|
|
896
|
-
delta_star = g_val / (r_val * m_val)
|
|
897
|
-
|
|
898
|
-
# recapture_form == "inside-out"
|
|
899
|
-
oswag = solve(
|
|
900
|
-
s_1 * s_2 / (1 - s_1)
|
|
901
|
-
+ s_2 * s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1))
|
|
902
|
-
- (s_1 + s_2) * delta_star,
|
|
903
|
-
s_2
|
|
904
|
-
)[0]
|
|
905
|
-
symplot(
|
|
906
|
-
oswag,
|
|
907
|
-
(s_1, 0., d_hat / (1 + d_hat)),
|
|
908
|
-
ylabel=s_2
|
|
909
|
-
)
|
|
910
|
-
|
|
911
|
-
cpswag = solve(
|
|
912
|
-
s_2 * s_2 / (1 - s_1)
|
|
913
|
-
+ s_1 * s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1))
|
|
914
|
-
- (s_1 + s_2) * delta_star,
|
|
915
|
-
s_2
|
|
916
|
-
)[1]
|
|
917
|
-
symplot(
|
|
918
|
-
cpwag,
|
|
919
|
-
(s_1, 0., d_hat / (1 + d_hat)),
|
|
920
|
-
ylabel=s_2
|
|
921
|
-
)
|
|
922
|
-
|
|
923
|
-
# recapture_form == "proportional"
|
|
924
|
-
oswag = solve(
|
|
925
|
-
s_1 * s_2 / (1 - s_1)
|
|
926
|
-
+ s_2 * s_1 / (1 - s_2)
|
|
927
|
-
- (s_1 + s_2) * delta_star,
|
|
928
|
-
s_2
|
|
929
|
-
)[0]
|
|
930
|
-
symplot(
|
|
931
|
-
oswag,
|
|
932
|
-
(s_1, 0., d_hat / (1 + d_hat)),
|
|
933
|
-
ylabel=s_2
|
|
934
|
-
)
|
|
935
|
-
|
|
936
|
-
cpswag = solve(
|
|
937
|
-
s_2 * s_2 / (1 - s_1)
|
|
938
|
-
+ s_1 * s_1 / (1 - s_2)
|
|
939
|
-
- (s_1 + s_2) * delta_star,
|
|
940
|
-
s_2
|
|
941
|
-
)[1]
|
|
942
|
-
symplot(
|
|
943
|
-
cpswag,
|
|
944
|
-
(s_1, 0.0, d_hat / (1 + d_hat)),
|
|
945
|
-
ylabel=s_2
|
|
946
|
-
)
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
"""
|
|
950
|
-
|
|
951
|
-
_delta_star = mpf(f"{_delta_star}")
|
|
952
|
-
_s_mid = _delta_star / (1 + _delta_star)
|
|
953
|
-
|
|
954
|
-
# initial conditions
|
|
955
|
-
_gbdry_points = [(_s_mid, _s_mid)]
|
|
956
|
-
_s_1_pre, _s_2_pre = _s_mid, _s_mid
|
|
957
|
-
_s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0, 0
|
|
958
|
-
|
|
959
|
-
# parameters for iteration
|
|
960
|
-
_gbd_step_sz = mp.power(10, -prec)
|
|
961
|
-
_theta = _gbd_step_sz * (10 if weighting == "cross-product-share" else 1)
|
|
962
|
-
for _s_1 in mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz):
|
|
963
|
-
# The wtd. avg. GUPPI is not always convex to the origin, so we
|
|
964
|
-
# increment _s_2 after each iteration in which our algorithm
|
|
965
|
-
# finds (s1, s2) on the boundary
|
|
966
|
-
_s_2 = _s_2_pre * (1 + _theta)
|
|
967
|
-
|
|
968
|
-
if (_s_1 + _s_2) > mpf("0.99875"):
|
|
969
|
-
# Loss of accuracy at 3-9s and up
|
|
970
|
-
break
|
|
971
|
-
|
|
972
|
-
while True:
|
|
973
|
-
_de_1 = _s_2 / (1 - _s_1)
|
|
974
|
-
_de_2 = (
|
|
975
|
-
_s_1 / (1 - lerp(_s_1, _s_2, _r_val))
|
|
976
|
-
if recapture_form == "inside-out"
|
|
977
|
-
else _s_1 / (1 - _s_2)
|
|
978
|
-
)
|
|
979
|
-
|
|
980
|
-
_r = (
|
|
981
|
-
mp.fdiv(
|
|
982
|
-
_s_1 if weighting == "cross-product-share" else _s_2, _s_1 + _s_2
|
|
983
|
-
)
|
|
984
|
-
if weighting
|
|
985
|
-
else 0.5
|
|
986
|
-
)
|
|
987
|
-
|
|
988
|
-
match agg_method:
|
|
989
|
-
case "geometric":
|
|
990
|
-
_delta_test = mp.expm1(lerp(mp.log1p(_de_1), mp.log1p(_de_2), _r))
|
|
991
|
-
case "distance":
|
|
992
|
-
_delta_test = mp.sqrt(lerp(_de_1**2, _de_2**2, _r))
|
|
993
|
-
case _:
|
|
994
|
-
_delta_test = lerp(_de_1, _de_2, _r)
|
|
995
|
-
|
|
996
|
-
_test_flag, _incr_decr = (
|
|
997
|
-
(_delta_test > _delta_star, -1)
|
|
998
|
-
if weighting == "cross-product-share"
|
|
999
|
-
else (_delta_test < _delta_star, 1)
|
|
1000
|
-
)
|
|
1001
|
-
|
|
1002
|
-
if _test_flag:
|
|
1003
|
-
_s_2 += _incr_decr * _gbd_step_sz
|
|
1004
|
-
else:
|
|
1005
|
-
break
|
|
1006
|
-
|
|
1007
|
-
# Build-up boundary points
|
|
1008
|
-
_gbdry_points.append((_s_1, _s_2))
|
|
1009
|
-
|
|
1010
|
-
# Build up area terms
|
|
1011
|
-
_s_2_oddsum += _s_2 if _s_2_oddval else 0
|
|
1012
|
-
_s_2_evnsum += _s_2 if not _s_2_oddval else 0
|
|
1013
|
-
_s_2_oddval = not _s_2_oddval
|
|
1014
|
-
|
|
1015
|
-
# Hold share points
|
|
1016
|
-
_s_2_pre = _s_2
|
|
1017
|
-
_s_1_pre = _s_1
|
|
1018
|
-
|
|
1019
|
-
if _s_2_oddval:
|
|
1020
|
-
_s_2_evnsum -= _s_2_pre
|
|
1021
|
-
else:
|
|
1022
|
-
_s_2_oddsum -= _s_1_pre
|
|
1023
|
-
|
|
1024
|
-
_s_intcpt = _shrratio_boundary_intcpt(
|
|
1025
|
-
_s_1_pre,
|
|
1026
|
-
_delta_star,
|
|
1027
|
-
_r_val,
|
|
1028
|
-
recapture_form=recapture_form,
|
|
1029
|
-
agg_method=agg_method,
|
|
1030
|
-
weighting=weighting,
|
|
1031
|
-
)
|
|
1032
|
-
|
|
1033
|
-
if weighting == "own-share":
|
|
1034
|
-
_gbd_prtlarea = (
|
|
1035
|
-
_gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_2_pre) / 3
|
|
1036
|
-
)
|
|
1037
|
-
# Area under boundary
|
|
1038
|
-
_gbdry_area_total = float(
|
|
1039
|
-
2 * (_s_1_pre + _gbd_prtlarea)
|
|
1040
|
-
- (mp.power(_s_mid, "2") + mp.power(_s_1_pre, "2"))
|
|
1041
|
-
)
|
|
1042
|
-
|
|
1043
|
-
else:
|
|
1044
|
-
_gbd_prtlarea = (
|
|
1045
|
-
_gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_intcpt) / 3
|
|
1046
|
-
)
|
|
1047
|
-
# Area under boundary
|
|
1048
|
-
_gbdry_area_total = float(2 * _gbd_prtlarea - mp.power(_s_mid, "2"))
|
|
1049
|
-
|
|
1050
|
-
_gbdry_points = np.row_stack((_gbdry_points, (mpf("0.0"), _s_intcpt))).astype(
|
|
1051
|
-
np.float64
|
|
1052
|
-
)
|
|
1053
|
-
|
|
1054
|
-
# Points defining boundary to point-of-symmetry
|
|
1055
|
-
return GuidelinesBoundary(
|
|
1056
|
-
np.row_stack((np.flip(_gbdry_points, 0), np.flip(_gbdry_points[1:], 1))),
|
|
1057
|
-
round(float(_gbdry_area_total), prec),
|
|
1058
|
-
)
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
def shrratio_boundary_xact_avg(
|
|
1062
|
-
_delta_star: float = 0.075,
|
|
1063
|
-
_r_val: float = 0.80,
|
|
1064
|
-
/,
|
|
1065
|
-
*,
|
|
1066
|
-
recapture_form: Literal["inside-out", "proportional"] = "inside-out",
|
|
1067
|
-
prec: int = 5,
|
|
1068
|
-
) -> GuidelinesBoundary:
|
|
1069
|
-
"""
|
|
1070
|
-
Share combinations for the simple average GUPPI boundary with symmetric
|
|
1071
|
-
merging-firm margins.
|
|
1072
|
-
|
|
1073
|
-
Notes
|
|
1074
|
-
-----
|
|
1075
|
-
An analytical expression for the exact average boundary is derived
|
|
1076
|
-
and plotted from the y-intercept to the ray of symmetry as follows::
|
|
1077
|
-
|
|
1078
|
-
from sympy import latex, plot as symplot, solve, symbols
|
|
1079
|
-
|
|
1080
|
-
s_1, s_2 = symbols("s_1 s_2")
|
|
1081
|
-
|
|
1082
|
-
g_val, r_val, m_val = 0.06, 0.80, 0.30
|
|
1083
|
-
d_hat = g_val / (r_val * m_val)
|
|
1084
|
-
|
|
1085
|
-
# recapture_form = "inside-out"
|
|
1086
|
-
sag = solve(
|
|
1087
|
-
(s_2 / (1 - s_1))
|
|
1088
|
-
+ (s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1)))
|
|
1089
|
-
- 2 * d_hat,
|
|
1090
|
-
s_2
|
|
1091
|
-
)[0]
|
|
1092
|
-
symplot(
|
|
1093
|
-
sag,
|
|
1094
|
-
(s_1, 0., d_hat / (1 + d_hat)),
|
|
1095
|
-
ylabel=s_2
|
|
1096
|
-
)
|
|
1097
|
-
|
|
1098
|
-
# recapture_form = "proportional"
|
|
1099
|
-
sag = solve((s_2/(1 - s_1)) + (s_1/(1 - s_2)) - 2 * d_hat, s_2)[0]
|
|
1100
|
-
symplot(
|
|
1101
|
-
sag,
|
|
1102
|
-
(s_1, 0., d_hat / (1 + d_hat)),
|
|
1103
|
-
ylabel=s_2
|
|
1104
|
-
)
|
|
1105
|
-
|
|
1106
|
-
Parameters
|
|
1107
|
-
----------
|
|
1108
|
-
_delta_star
|
|
1109
|
-
Margin-adjusted benchmark share ratio.
|
|
1110
|
-
_r_val
|
|
1111
|
-
Recapture ratio
|
|
1112
|
-
recapture_form
|
|
1113
|
-
Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
|
|
1114
|
-
value for both merging firms ("proportional").
|
|
1115
|
-
prec
|
|
1116
|
-
Number of decimal places for rounding returned shares.
|
|
1117
|
-
|
|
1118
|
-
Returns
|
|
1119
|
-
-------
|
|
1120
|
-
Array of share-pairs, area under boundary, area under boundary.
|
|
1121
|
-
|
|
1122
|
-
"""
|
|
1123
|
-
|
|
1124
|
-
_delta_star = mpf(f"{_delta_star}")
|
|
1125
|
-
_s_mid = _delta_star / (1 + _delta_star)
|
|
1126
|
-
_gbd_step_sz = mp.power(10, -prec)
|
|
1127
|
-
|
|
1128
|
-
_gbdry_points_start = np.array([(_s_mid, _s_mid)])
|
|
1129
|
-
_s_1 = np.array(mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz), np.float64)
|
|
1130
|
-
if recapture_form == "inside-out":
|
|
1131
|
-
_s_intcpt = mp.fdiv(
|
|
1132
|
-
mp.fsub(
|
|
1133
|
-
2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
|
|
1134
|
-
),
|
|
1135
|
-
2 * mpf(f"{_r_val}"),
|
|
1136
|
-
)
|
|
1137
|
-
_nr_t1 = 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val)
|
|
1138
|
-
|
|
1139
|
-
_nr_sqrt_mdr = 4 * _delta_star * _r_val
|
|
1140
|
-
_nr_sqrt_mdr2 = _nr_sqrt_mdr * _r_val
|
|
1141
|
-
_nr_sqrt_md2r2 = _nr_sqrt_mdr2 * _delta_star
|
|
1142
|
-
|
|
1143
|
-
_nr_sqrt_t1 = _nr_sqrt_md2r2 * (_s_1**2 - 2 * _s_1 + 1)
|
|
1144
|
-
_nr_sqrt_t2 = _nr_sqrt_mdr2 * _s_1 * (_s_1 - 1)
|
|
1145
|
-
_nr_sqrt_t3 = _nr_sqrt_mdr * (2 * _s_1 - _s_1**2 - 1)
|
|
1146
|
-
_nr_sqrt_t4 = (_s_1**2) * (_r_val**2 - 6 * _r_val + 1)
|
|
1147
|
-
_nr_sqrt_t5 = _s_1 * (6 * _r_val - 2) + 1
|
|
1148
|
-
|
|
1149
|
-
_nr_t2_mdr = _nr_sqrt_t1 + _nr_sqrt_t2 + _nr_sqrt_t3 + _nr_sqrt_t4 + _nr_sqrt_t5
|
|
1150
|
-
|
|
1151
|
-
# Alternative grouping of terms in np.sqrt
|
|
1152
|
-
_nr_sqrt_s1sq = (_s_1**2) * (
|
|
1153
|
-
_nr_sqrt_md2r2 + _nr_sqrt_mdr2 - _nr_sqrt_mdr + _r_val**2 - 6 * _r_val + 1
|
|
1154
|
-
)
|
|
1155
|
-
_nr_sqrt_s1 = _s_1 * (
|
|
1156
|
-
-2 * _nr_sqrt_md2r2 - _nr_sqrt_mdr2 + 2 * _nr_sqrt_mdr + 6 * _r_val - 2
|
|
1157
|
-
)
|
|
1158
|
-
_nr_sqrt_nos1 = _nr_sqrt_md2r2 - _nr_sqrt_mdr + 1
|
|
1159
|
-
|
|
1160
|
-
_nr_t2_s1 = _nr_sqrt_s1sq + _nr_sqrt_s1 + _nr_sqrt_nos1
|
|
1161
|
-
|
|
1162
|
-
if not np.isclose(
|
|
1163
|
-
np.einsum("i->", _nr_t2_mdr.astype(np.float64)),
|
|
1164
|
-
np.einsum("i->", _nr_t2_s1.astype(np.float64)),
|
|
1165
|
-
rtol=0,
|
|
1166
|
-
atol=0.5 * prec,
|
|
1167
|
-
):
|
|
1168
|
-
raise RuntimeError(
|
|
1169
|
-
"Calculation of sq. root term in exact average GUPPI"
|
|
1170
|
-
f"with recapture spec, {f'"{recapture_form}"'} is incorrect."
|
|
1171
|
-
)
|
|
1172
|
-
|
|
1173
|
-
_s_2 = (_nr_t1 - np.sqrt(_nr_t2_s1)) / (2 * _r_val)
|
|
1174
|
-
|
|
1175
|
-
else:
|
|
1176
|
-
_s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
|
|
1177
|
-
_s_2 = (
|
|
1178
|
-
(1 / 2)
|
|
1179
|
-
+ _delta_star
|
|
1180
|
-
- _delta_star * _s_1
|
|
1181
|
-
- np.sqrt(
|
|
1182
|
-
((_delta_star**2) - 1) * (_s_1**2)
|
|
1183
|
-
+ (-2 * (_delta_star**2) + _delta_star + 1) * _s_1
|
|
1184
|
-
+ (_delta_star**2)
|
|
1185
|
-
- _delta_star
|
|
1186
|
-
+ (1 / 4)
|
|
1187
|
-
)
|
|
1188
|
-
)
|
|
1189
|
-
|
|
1190
|
-
_gbdry_points_inner = np.column_stack((_s_1, _s_2))
|
|
1191
|
-
_gbdry_points_end = np.array([(mpf("0.0"), _s_intcpt)], np.float64)
|
|
1192
|
-
|
|
1193
|
-
_gbdry_points = np.row_stack((
|
|
1194
|
-
_gbdry_points_end,
|
|
1195
|
-
np.flip(_gbdry_points_inner, 0),
|
|
1196
|
-
_gbdry_points_start,
|
|
1197
|
-
np.flip(_gbdry_points_inner, 1),
|
|
1198
|
-
np.flip(_gbdry_points_end, 1),
|
|
1199
|
-
)).astype(np.float64)
|
|
1200
|
-
_s_2 = np.concatenate((np.array([_s_mid], np.float64), _s_2))
|
|
1201
|
-
|
|
1202
|
-
_gbdry_ends = [0, -1]
|
|
1203
|
-
_gbdry_odds = np.array(range(1, len(_s_2), 2), np.int64)
|
|
1204
|
-
_gbdry_evns = np.array(range(2, len(_s_2), 2), np.int64)
|
|
1205
|
-
|
|
1206
|
-
# Double the are under the curve, and subtract the double counted bit.
|
|
1207
|
-
_gbdry_area_simpson = 2 * _gbd_step_sz * (
|
|
1208
|
-
(4 / 3) * np.sum(_s_2.take(_gbdry_odds))
|
|
1209
|
-
+ (2 / 3) * np.sum(_s_2.take(_gbdry_evns))
|
|
1210
|
-
+ (1 / 3) * np.sum(_s_2.take(_gbdry_ends))
|
|
1211
|
-
) - np.power(_s_mid, 2)
|
|
1212
|
-
|
|
1213
|
-
_s_1_pts, _s_2_pts = np.split(_gbdry_points, 2, axis=1)
|
|
1214
|
-
return GuidelinesBoundary(
|
|
1215
|
-
np.column_stack((np.array(_s_1_pts), np.array(_s_2_pts))),
|
|
1216
|
-
round(float(_gbdry_area_simpson), prec),
|
|
1217
|
-
)
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
def _shrratio_boundary_intcpt(
|
|
1221
|
-
_s_2_pre: float,
|
|
1222
|
-
_delta_star: mpf,
|
|
1223
|
-
_r_val: mpf,
|
|
1224
|
-
/,
|
|
1225
|
-
*,
|
|
1226
|
-
recapture_form: Literal["inside-out", "proportional"],
|
|
1227
|
-
agg_method: Literal["arithmetic", "geometric", "distance"],
|
|
1228
|
-
weighting: Literal["cross-product-share", "own-share"] | None,
|
|
1229
|
-
) -> float:
|
|
1230
|
-
match weighting:
|
|
1231
|
-
case "cross-product-share":
|
|
1232
|
-
_s_intcpt: float = _delta_star
|
|
1233
|
-
case "own-share":
|
|
1234
|
-
_s_intcpt = mpf("1.0")
|
|
1235
|
-
case None if agg_method == "distance":
|
|
1236
|
-
_s_intcpt = _delta_star * mp.sqrt("2")
|
|
1237
|
-
case None if agg_method == "arithmetic" and recapture_form == "inside-out":
|
|
1238
|
-
_s_intcpt = mp.fdiv(
|
|
1239
|
-
mp.fsub(
|
|
1240
|
-
2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
|
|
1241
|
-
),
|
|
1242
|
-
2 * mpf(f"{_r_val}"),
|
|
1243
|
-
)
|
|
1244
|
-
case None if agg_method == "arithmetic":
|
|
1245
|
-
_s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
|
|
1246
|
-
case _:
|
|
1247
|
-
_s_intcpt = _s_2_pre
|
|
1248
|
-
|
|
1249
|
-
return _s_intcpt
|