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