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.

@@ -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 Any, Literal, TypeAlias
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(slots=True, frozen=True)
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
- _r := round_cust((_fc := int(np.ceil(1 / _hhi_p))) / (_fc + 1)),
114
- _g_s := gbd_from_dsf(_dh_s, m_star=1.0, r_bar=_r),
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 := gbd_from_dsf(_dh_i, m_star=1.0, r_bar=_r_i),
130
- round_cust((1 / 2) / (_fc - 1 / 2)),
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 := gbd_from_dsf(_dh_i, m_star=1.0, r_bar=_r),
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 := gbd_from_dsf(_dh_p, m_star=1.0, r_bar=_r),
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 round_cust(
161
- _num: float = 0.060215,
162
- /,
163
- *,
164
- frac: float = 0.005,
165
- rounding_mode: str = "ROUND_HALF_UP",
166
- ) -> float:
167
- """
168
- Custom rounding, to the nearest 0.5% by default.
169
-
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(_deltasf / 2)) / (1 - _s_m),
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 critical_shrratio(
292
- _gbd: float = 0.06,
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 = 0.80,
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
- _gbd
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(mpf(f"{_gbd}") / mp.fmul(f"{m_star}", f"{r_bar}"), frac=frac)
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 shr_from_gbd(
321
- _gbd: float = 0.06, /, *, m_star: float = 1.00, r_bar: float = 0.80
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
- _gbd
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 := critical_shrratio(_gbd, m_star=m_star, r_bar=r_bar)) / (1 + _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 dh_area_quad(_dh_val: float = 0.01, /, *, prec: int = 9) -> float:
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 shrratio_boundary(_bdry_spec: UPPBoundarySpec) -> GuidelinesBoundary:
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
- _bdry_spec.share_ratio,
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
- _bdry_spec.share_ratio, _bdry_spec.rec, prec=_bdry_spec.precision
401
+ _share_ratio, _bdry_spec.rec, prec=_bdry_spec.precision
709
402
  )
710
403
  case UPPAggrSelector.MIN:
711
404
  return shrratio_boundary_min(
712
- _bdry_spec.share_ratio,
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
- _bdry_spec.share_ratio,
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
- _bdry_spec.share_ratio,
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