mergeron 2024.738936.0__py3-none-any.whl → 2024.738940.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,7 +4,6 @@ with a canvas on which to draw boundaries for Guidelines standards.
4
4
 
5
5
  """
6
6
 
7
- from collections import namedtuple
8
7
  from importlib.metadata import version
9
8
 
10
9
  from .. import _PKG_NAME # noqa: TID252
@@ -12,19 +11,38 @@ from .. import _PKG_NAME # noqa: TID252
12
11
  __version__ = version(_PKG_NAME)
13
12
 
14
13
  import decimal
14
+ from dataclasses import dataclass
15
15
  from typing import Any, Literal, TypeAlias
16
16
 
17
17
  import numpy as np
18
+ from attr import define, field
18
19
  from mpmath import mp, mpf # type: ignore
19
20
  from numpy.typing import NDArray
21
+ from scipy.spatial.distance import minkowski as distance_function
20
22
 
21
23
  mp.prec = 80
22
24
  mp.trap_complex = True
23
25
 
24
26
  HMGPubYear: TypeAlias = Literal[1992, 2010, 2023]
25
- GuidelinesSTD = namedtuple("GuidelinesSTD", "delta rec guppi divr cmcr ipr")
26
27
 
27
28
 
29
+ @dataclass(slots=True, frozen=True)
30
+ class GuidelinesBoundary:
31
+ coordinates: NDArray[np.float64]
32
+ area: float
33
+
34
+
35
+ @define(slots=True, frozen=True)
36
+ class GuidelinesSTD:
37
+ delta: float
38
+ rec: float
39
+ guppi: float
40
+ divr: float
41
+ cmcr: float
42
+ ipr: float
43
+
44
+
45
+ @define(slots=True, frozen=True)
28
46
  class GuidelinesStandards:
29
47
  """
30
48
  Guidelines standards by Guidelines publication year
@@ -32,76 +50,100 @@ class GuidelinesStandards:
32
50
  Diversion ratio, GUPPI, CMCR, and IPR standards are constructed from
33
51
  concentration standards.
34
52
 
35
- Attributes
36
- ----------
53
+ """
54
+
37
55
  pub_year: HMGPubYear
38
- Year of publication of the U.S. Horizontal Merger Guidelines (HMG);
39
- 1992, 2010, or 2023
40
- safeharbor: GuidelinesSTD
41
- ΔHHI safeharbor bound, default recapture rate, GUPPI bound and
42
- diversion ratio limit at ΔHHI safeharbor
43
- inferred_presumption: GuidelinesSTD
44
- ΔHHI safeharbor bound, default recapture rate, GUPPI bound and
45
- diversion ratio limit at enforcement margin for Guidelines
46
- presumption of harm, interpreted strictly
47
- presumption: GuidelinesSTD
48
- ΔHHI safeharbor bound, default recapture rate, GUPPI bound and
49
- diversion ratio limit at enforcement margin for Guidelines
50
- presumption of harm, as typically interpreted in the literature
51
- on merger enforcement
52
56
  """
57
+ Year of publication of the U.S. Horizontal Merger Guidelines (HMG)
58
+ """
59
+
60
+ safeharbor: GuidelinesSTD = field(kw_only=True, default=None)
61
+ """
62
+ Negative presumption defined on various measures
53
63
 
54
- def __init__(self, _pub_year: HMGPubYear = 1992, /):
55
- self.pub_year: HMGPubYear = _pub_year
64
+ ΔHHI safeharbor bound, default recapture rate, GUPPI bound,
65
+ diversion ratio limit, CMCR, and IPR
66
+ """
67
+
68
+ inferred_presumption: GuidelinesSTD = field(kw_only=True, default=None)
69
+ """
70
+ Inferred ΔHHI safeharbor presumption and related measures
71
+
72
+ ΔHHI bound inferred from strict numbers-equivalent
73
+ of (post-merger) HHI presumption, and corresponding default recapture rate,
74
+ GUPPI bound, diversion ratio limit, CMCR, and IPR
75
+ """
56
76
 
57
- # In the 2023 Guidlines, the agencies do not specify a safeharbor
58
- # Practically speaking, given resource constraints and loss aversion,
59
- # it is likely that staff only investigates mergers that
60
- # meet the presumption; nonetheless, the "safeharbor" is set here at
61
- # the 1992 level
77
+ presumption: GuidelinesSTD = field(kw_only=True, default=None)
78
+ """
79
+ Guidelines ΔHHI safeharbor presumption and related measures
80
+
81
+ ΔHHI bound and corresponding default recapture rate, GUPPI bound,
82
+ diversion ratio limit, CMCR, and IPR
83
+ """
84
+
85
+ def __attrs_post_init__(self, /):
86
+ # In the 2023 Guidlines, the agencies do not define a
87
+ # negative presumption, or safeharbor. Practically speaking,
88
+ # given resource constraints and loss aversion, it is likely
89
+ # that staff only investigates mergers that meet the presumption;
90
+ # thus, here, the tentative delta safeharbor under
91
+ # the 2023 Guidelines is 100 points
62
92
  _hhi_p, _dh_s, _dh_p = {
63
93
  1992: (0.18, 0.005, 0.01),
64
94
  2010: (0.25, 0.01, 0.02),
65
95
  2023: (0.18, 0.01, 0.01),
66
- }[_pub_year]
67
-
68
- self.safeharbor = GuidelinesSTD(
69
- _dh_s,
70
- _r := round_cust((_fc := int(np.ceil(1 / _hhi_p))) / (_fc + 1)),
71
- _g_s := gbd_from_dsf(_dh_s, m_star=1.0, r_bar=_r),
72
- _dr := round_cust(1 / (_fc + 1)),
73
- _cmcr := 0.03, # Not strictly a Guidelines standard
74
- _ipr := _g_s, # Not strictly a Guidelines standard
96
+ }[self.pub_year]
97
+
98
+ object.__setattr__(
99
+ self,
100
+ "safeharbor",
101
+ GuidelinesSTD(
102
+ _dh_s,
103
+ _r := round_cust((_fc := int(np.ceil(1 / _hhi_p))) / (_fc + 1)),
104
+ _g_s := gbd_from_dsf(_dh_s, m_star=1.0, r_bar=_r),
105
+ _dr := round_cust(1 / (_fc + 1)),
106
+ _cmcr := 0.03, # Not strictly a Guidelines standard
107
+ _ipr := _g_s, # Not strictly a Guidelines standard
108
+ ),
75
109
  )
76
110
 
77
111
  # inferred_presumption is relevant for 2010 Guidelines
78
- self.inferred_presumption = (
112
+ object.__setattr__(
113
+ self,
114
+ "inferred_presumption",
115
+ (
116
+ GuidelinesSTD(
117
+ _dh_i := 2 * (0.5 / _fc) ** 2,
118
+ _r_i := round_cust((_fc - 1 / 2) / (_fc + 1 / 2)),
119
+ _g_i := gbd_from_dsf(_dh_i, m_star=1.0, r_bar=_r_i),
120
+ round_cust((1 / 2) / (_fc - 1 / 2)),
121
+ _cmcr,
122
+ _g_i,
123
+ )
124
+ if self.pub_year == 2010
125
+ else GuidelinesSTD(
126
+ _dh_i := 2 * (1 / (_fc + 1)) ** 2,
127
+ _r,
128
+ _g_i := gbd_from_dsf(_dh_i, m_star=1.0, r_bar=_r),
129
+ _dr,
130
+ _cmcr,
131
+ _g_i,
132
+ )
133
+ ),
134
+ )
135
+
136
+ object.__setattr__(
137
+ self,
138
+ "presumption",
79
139
  GuidelinesSTD(
80
- _dh_i := 2 * (_s := 1 / (_fc + 1)) * _s,
140
+ _dh_p,
81
141
  _r,
82
- _g_i := gbd_from_dsf(_dh_i, m_star=1.0, r_bar=_r),
142
+ _g_p := gbd_from_dsf(_dh_p, m_star=1.0, r_bar=_r),
83
143
  _dr,
84
144
  _cmcr,
85
- _g_i,
86
- )
87
- if _pub_year in (1992, 2023)
88
- else GuidelinesSTD(
89
- _dh_i := 2 * (_s := 0.5 / _fc) * _s,
90
- _r_i := round_cust((_fc - 1 / 2) / (_fc + 1 / 2)),
91
- _g_i := gbd_from_dsf(_dh_i, m_star=1.0, r_bar=_r_i),
92
- round_cust((1 / 2) / (_fc - 1 / 2)),
93
- _cmcr,
94
- _g_i,
95
- )
96
- )
97
-
98
- self.presumption = GuidelinesSTD(
99
- _dh_p,
100
- _r,
101
- _g_p := gbd_from_dsf(_dh_p, m_star=1.0, r_bar=_r),
102
- _dr,
103
- _cmcr,
104
- _ipr := _g_p,
145
+ _ipr := _g_p,
146
+ ),
105
147
  )
106
148
 
107
149
 
@@ -168,7 +210,7 @@ def lerp(
168
210
  /,
169
211
  ) -> float | mpf | NDArray[np.float64]:
170
212
  """
171
- From the C++ standard function of the same name.
213
+ From the function of the same name in the C++ standard [2]_
172
214
 
173
215
  Constructs the weighted average, :math:`w_1 x_1 + w_2 x_2`, where
174
216
  :math:`w_1 = 1 - r` and :math:`w_2 = r`.
@@ -178,21 +220,35 @@ def lerp(
178
220
  _x1, _x2
179
221
  bounds :math:`x_1, x_2` to interpolate between.
180
222
  _r
181
- interpolation weight :math:`r` assigned to :math:`x_2`.
223
+ interpolation weight :math:`r` assigned to :math:`x_2`
182
224
 
183
225
  Returns
184
226
  -------
185
- The linear interpolation, or weighted average, :math:`x_1 (1 - r) + x_2 r`.
227
+ The linear interpolation, or weighted average,
228
+ :math:`x_1 + r \\cdot (x_1 - x_2) \\equiv (1 - r) \\cdot x_1 + r \\cdot x_2`.
186
229
 
187
230
  Raises
188
231
  ------
189
232
  ValueError
190
233
  If the interpolation weight is not in the interval, :math:`[0, 1]`.
191
234
 
235
+ References
236
+ ----------
237
+
238
+ .. [2] C++ Reference, https://en.cppreference.com/w/cpp/numeric/lerp
239
+
192
240
  """
241
+
193
242
  if not 0 <= _r <= 1:
194
243
  raise ValueError("Specified interpolation weight must lie in [0, 1].")
195
- return _x2 * _r + _x1 * (1 - _r)
244
+ elif _r == 0:
245
+ return _x1
246
+ elif _r == 1:
247
+ return _x2
248
+ elif _r == 0.5:
249
+ return 1 / 2 * (_x1 + _x2)
250
+ else:
251
+ return _r * _x2 + (1 - _r) * _x1
196
252
 
197
253
 
198
254
  def gbd_from_dsf(
@@ -506,7 +562,7 @@ def dh_area_quad(_dh_val: float = 0.01, /, *, dh_dps: int = 9) -> float:
506
562
 
507
563
  def delta_hhi_boundary(
508
564
  _dh_val: float = 0.01, /, *, dh_dps: int = 5
509
- ) -> tuple[NDArray[np.float64], float]:
565
+ ) -> GuidelinesBoundary:
510
566
  """
511
567
  Generate the list of share combination on the ΔHHI boundary.
512
568
 
@@ -542,7 +598,7 @@ def delta_hhi_boundary(
542
598
  _dh_bdry_pts = np.row_stack((np.flip(_dh_half, 0), np.flip(_dh_half[1:], 1)))
543
599
 
544
600
  _s_1_pts, _s_2_pts = np.split(_dh_bdry_pts, 2, axis=1)
545
- return (
601
+ return GuidelinesBoundary(
546
602
  np.column_stack((
547
603
  np.array(_s_1_pts, np.float64),
548
604
  np.array(_s_2_pts, np.float64),
@@ -552,8 +608,8 @@ def delta_hhi_boundary(
552
608
 
553
609
 
554
610
  def combined_share_boundary(
555
- _s_incpt: float = 0.0625, /, *, bdry_dps: int = 10
556
- ) -> tuple[NDArray[np.float64], float]:
611
+ _s_intcpt: float = 0.0625, /, *, bdry_dps: int = 10
612
+ ) -> GuidelinesBoundary:
557
613
  """
558
614
  Share combinations on the merging-firms' combined share boundary.
559
615
 
@@ -563,7 +619,7 @@ def combined_share_boundary(
563
619
 
564
620
  Parameters
565
621
  ----------
566
- _s_incpt:
622
+ _s_intcpt:
567
623
  Merging-firms' combined share.
568
624
  bdry_dps
569
625
  Number of decimal places for rounding reported shares.
@@ -573,22 +629,22 @@ def combined_share_boundary(
573
629
  Array of share-pairs, area under boundary.
574
630
 
575
631
  """
576
- _s_incpt = mpf(f"{_s_incpt}")
577
- _s_mid = _s_incpt / 2
632
+ _s_intcpt = mpf(f"{_s_intcpt}")
633
+ _s_mid = _s_intcpt / 2
578
634
 
579
- _s1_pts = (0, _s_mid, _s_incpt)
580
- return (
635
+ _s1_pts = (0, _s_mid, _s_intcpt)
636
+ return GuidelinesBoundary(
581
637
  np.column_stack((
582
638
  np.array(_s1_pts, np.float64),
583
639
  np.array(_s1_pts[::-1], np.float64),
584
640
  )),
585
- round(float(_s_incpt * _s_mid), bdry_dps),
641
+ round(float(_s_intcpt * _s_mid), bdry_dps),
586
642
  )
587
643
 
588
644
 
589
645
  def hhi_pre_contrib_boundary(
590
646
  _hhi_contrib: float = 0.03125, /, *, bdry_dps: int = 5
591
- ) -> tuple[NDArray[np.float64], float]:
647
+ ) -> GuidelinesBoundary:
592
648
  """
593
649
  Share combinations on the premerger HHI contribution boundary.
594
650
 
@@ -612,15 +668,15 @@ def hhi_pre_contrib_boundary(
612
668
  _s_1 = np.array(mp.arange(_s_mid, -_bdry_step_sz, -_bdry_step_sz), np.float64)
613
669
  _s_2 = np.sqrt(_hhi_contrib - _s_1**2).astype(np.float64)
614
670
  _bdry_pts_mid = np.column_stack((_s_1, _s_2))
615
- return (
671
+ return GuidelinesBoundary(
616
672
  np.row_stack((np.flip(_bdry_pts_mid, 0), np.flip(_bdry_pts_mid[1:], 1))),
617
673
  round(float(mp.pi * _hhi_contrib / 4), bdry_dps),
618
674
  )
619
675
 
620
676
 
621
- def shrratio_mgnsym_boundary_max(
677
+ def shrratio_boundary_max(
622
678
  _delta_star: float = 0.075, _r_val: float = 0.80, /, *, gbd_dps: int = 10
623
- ) -> tuple[NDArray[np.float64], float]:
679
+ ) -> GuidelinesBoundary:
624
680
  """
625
681
  Share combinations on the minimum GUPPI boundary with symmetric
626
682
  merging-firm margins.
@@ -650,28 +706,28 @@ def shrratio_mgnsym_boundary_max(
650
706
  # of function call with other shrratio_mgnsym_boundary functions
651
707
  del _r_val
652
708
  _delta_star = mpf(f"{_delta_star}")
653
- _s_incpt = _delta_star
709
+ _s_intcpt = _delta_star
654
710
  _s_mid = _delta_star / (1 + _delta_star)
655
711
 
656
- _s1_pts = (0, _s_mid, _s_incpt)
712
+ _s1_pts = (0, _s_mid, _s_intcpt)
657
713
 
658
- return (
714
+ return GuidelinesBoundary(
659
715
  np.column_stack((
660
716
  np.array(_s1_pts, np.float64),
661
717
  np.array(_s1_pts[::-1], np.float64),
662
718
  )),
663
- round(float(_s_incpt * _s_mid), gbd_dps), # simplified calculation
719
+ round(float(_s_intcpt * _s_mid), gbd_dps), # simplified calculation
664
720
  )
665
721
 
666
722
 
667
- def shrratio_mgnsym_boundary_min(
723
+ def shrratio_boundary_min(
668
724
  _delta_star: float = 0.075,
669
725
  _r_val: float = 0.80,
670
726
  /,
671
727
  *,
672
728
  recapture_spec: str = "inside-out",
673
729
  gbd_dps: int = 10,
674
- ) -> tuple[NDArray[np.float64], float]:
730
+ ) -> GuidelinesBoundary:
675
731
  """
676
732
  Share combinations on the minimum GUPPI boundary, with symmetric
677
733
  merging-firm margins.
@@ -710,7 +766,7 @@ def shrratio_mgnsym_boundary_min(
710
766
  )
711
767
 
712
768
  _delta_star = mpf(f"{_delta_star}")
713
- _s_incpt = mpf("1.00")
769
+ _s_intcpt = mpf("1.00")
714
770
  _s_mid = _delta_star / (1 + _delta_star)
715
771
 
716
772
  if recapture_spec == "inside-out":
@@ -725,19 +781,21 @@ def shrratio_mgnsym_boundary_min(
725
781
  _smin_nr / _guppi_bdry_env_dr,
726
782
  _s_mid,
727
783
  _smax_nr / _guppi_bdry_env_dr,
728
- _s_incpt,
784
+ _s_intcpt,
729
785
  ),
730
786
  np.float64,
731
787
  )
732
788
 
733
789
  _gbd_area = (_smin_nr + (_smax_nr - _smin_nr) * _s_mid) / _guppi_bdry_env_dr
734
790
  else:
735
- _s1_pts, _gbd_area = np.array((0, _s_mid, _s_incpt), np.float64), _s_mid
791
+ _s1_pts, _gbd_area = np.array((0, _s_mid, _s_intcpt), np.float64), _s_mid
736
792
 
737
- return np.column_stack((_s1_pts, _s1_pts[::-1])), round(float(_gbd_area), gbd_dps)
793
+ return GuidelinesBoundary(
794
+ np.column_stack((_s1_pts, _s1_pts[::-1])), round(float(_gbd_area), gbd_dps)
795
+ )
738
796
 
739
797
 
740
- def shrratio_mgnsym_boundary_wtd_avg(
798
+ def shrratio_boundary_wtd_avg(
741
799
  _delta_star: float = 0.075,
742
800
  _r_val: float = 0.80,
743
801
  /,
@@ -746,7 +804,7 @@ def shrratio_mgnsym_boundary_wtd_avg(
746
804
  wgtng_policy: Literal["own-share", "cross-product-share"] | None = "own-share",
747
805
  recapture_spec: Literal["inside-out", "proportional"] = "inside-out",
748
806
  gbd_dps: int = 5,
749
- ) -> tuple[NDArray[np.float64], float]:
807
+ ) -> GuidelinesBoundary:
750
808
  """
751
809
  Share combinations for the share-weighted average GUPPI boundary with symmetric
752
810
  merging-firm margins.
@@ -842,10 +900,12 @@ def shrratio_mgnsym_boundary_wtd_avg(
842
900
  _delta_star = mpf(f"{_delta_star}")
843
901
  _s_mid = _delta_star / (1 + _delta_star)
844
902
 
903
+ # initial conditions
845
904
  _gbdry_points = [(_s_mid, _s_mid)]
846
905
  _s_1_pre, _s_2_pre = _s_mid, _s_mid
847
906
  _s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0, 0
848
907
 
908
+ # parameters for iteration
849
909
  _gbd_step_sz = mp.power(10, -gbd_dps)
850
910
  _theta = _gbd_step_sz * (10 if wgtng_policy == "cross-product-share" else 1)
851
911
  for _s_1 in mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz):
@@ -883,12 +943,12 @@ def shrratio_mgnsym_boundary_wtd_avg(
883
943
  _delta_test = lerp(_de_1, _de_2, _r)
884
944
 
885
945
  if wgtng_policy == "cross-product-share":
886
- test_flag, incr_decr = (_delta_test > _delta_star, -1)
946
+ _test_flag, _incr_decr = (_delta_test > _delta_star, -1)
887
947
  else:
888
- test_flag, incr_decr = (_delta_test < _delta_star, 1)
948
+ _test_flag, _incr_decr = (_delta_test < _delta_star, 1)
889
949
 
890
- if test_flag:
891
- _s_2 += incr_decr * _gbd_step_sz
950
+ if _test_flag:
951
+ _s_2 += _incr_decr * _gbd_step_sz
892
952
  else:
893
953
  break
894
954
 
@@ -916,28 +976,34 @@ def shrratio_mgnsym_boundary_wtd_avg(
916
976
  # Area under boundary
917
977
  _gbdry_area_total = 2 * _gbd_prtlarea - mp.power(_s_mid, 2)
918
978
 
919
- _gbdry_points = np.row_stack((
920
- _gbdry_points,
921
- (
922
- mpf("0.0"),
923
- _delta_star if wgtng_policy == "cross-product-share" else mpf("1.0"),
924
- ),
925
- )).astype(np.float64)
979
+ match wgtng_policy:
980
+ case "cross-product-share":
981
+ _s_intcpt = _delta_star
982
+ case "own-product-share":
983
+ _s_intcpt = mpf("1.0")
984
+ case None if avg_method == "distance":
985
+ _s_intcpt = _delta_star * mp.sqrt("2")
986
+ case _:
987
+ _s_intcpt = _s_2_pre
988
+
989
+ _gbdry_points = np.row_stack((_gbdry_points, (mpf("0.0"), _s_intcpt))).astype(
990
+ np.float64
991
+ )
926
992
  # Points defining boundary to point-of-symmetry
927
- return (
993
+ return GuidelinesBoundary(
928
994
  np.row_stack((np.flip(_gbdry_points, 0), np.flip(_gbdry_points[1:], 1))),
929
995
  round(float(_gbdry_area_total), gbd_dps),
930
996
  )
931
997
 
932
998
 
933
- def shrratio_mgnsym_boundary_xact_avg(
999
+ def shrratio_boundary_xact_avg(
934
1000
  _delta_star: float = 0.075,
935
1001
  _r_val: float = 0.80,
936
1002
  /,
937
1003
  *,
938
1004
  recapture_spec: Literal["inside-out", "proportional"] = "inside-out",
939
1005
  gbd_dps: int = 5,
940
- ) -> tuple[NDArray[np.float64], float]:
1006
+ ) -> GuidelinesBoundary:
941
1007
  """
942
1008
  Share combinations for the simple average GUPPI boundary with symmetric
943
1009
  merging-firm margins.
@@ -1009,13 +1075,13 @@ def shrratio_mgnsym_boundary_xact_avg(
1009
1075
  _gbdry_points_start = np.array([(_s_mid, _s_mid)])
1010
1076
  _s_1 = np.array(mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz), np.float64)
1011
1077
  if recapture_spec == "inside-out":
1012
- _s_incpt = mp.fdiv(
1078
+ _s_intcpt = mp.fdiv(
1013
1079
  mp.fsub(
1014
1080
  2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
1015
1081
  ),
1016
1082
  2 * mpf(f"{_r_val}"),
1017
1083
  )
1018
- _nr_t1 = 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val)
1084
+ _nr_t1 = 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val) # type: ignore
1019
1085
 
1020
1086
  _nr_sqrt_mdr = 4 * _delta_star * _r_val
1021
1087
  _nr_sqrt_mdr2 = _nr_sqrt_mdr * _r_val
@@ -1040,9 +1106,9 @@ def shrratio_mgnsym_boundary_xact_avg(
1040
1106
 
1041
1107
  _nr_t2_s1 = _nr_sqrt_s1sq + _nr_sqrt_s1 + _nr_sqrt_nos1
1042
1108
 
1043
- if not np.isclose(
1044
- np.einsum("i->", _nr_t2_mdr.astype(np.float64)), # from mpf to float64
1045
- np.einsum("i->", _nr_t2_s1.astype(np.float64)),
1109
+ if not np.isclose( # type: ignore
1110
+ np.einsum("i->", _nr_t2_mdr.astype(np.float64)), # type: ignore from mpf to float64
1111
+ np.einsum("i->", _nr_t2_s1.astype(np.float64)), # type: ignore from mpf to float64
1046
1112
  rtol=0,
1047
1113
  atol=0.5 * gbd_dps,
1048
1114
  ):
@@ -1054,7 +1120,7 @@ def shrratio_mgnsym_boundary_xact_avg(
1054
1120
  _s_2 = (_nr_t1 - np.sqrt(_nr_t2_s1)) / (2 * _r_val)
1055
1121
 
1056
1122
  else:
1057
- _s_incpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
1123
+ _s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
1058
1124
  _s_2 = (
1059
1125
  (1 / 2)
1060
1126
  + _delta_star
@@ -1069,7 +1135,7 @@ def shrratio_mgnsym_boundary_xact_avg(
1069
1135
  )
1070
1136
 
1071
1137
  _gbdry_points_inner = np.column_stack((_s_1, _s_2))
1072
- _gbdry_points_end = np.array([(mpf("0.0"), _s_incpt)], np.float64)
1138
+ _gbdry_points_end = np.array([(mpf("0.0"), _s_intcpt)], np.float64)
1073
1139
 
1074
1140
  _gbdry_points = np.row_stack((
1075
1141
  _gbdry_points_end,
@@ -1092,13 +1158,13 @@ def shrratio_mgnsym_boundary_xact_avg(
1092
1158
  ) - np.power(_s_mid, 2)
1093
1159
 
1094
1160
  _s_1_pts, _s_2_pts = np.split(_gbdry_points, 2, axis=1)
1095
- return (
1161
+ return GuidelinesBoundary(
1096
1162
  np.column_stack((np.array(_s_1_pts), np.array(_s_2_pts))),
1097
1163
  round(float(_gbdry_area_simpson), gbd_dps),
1098
1164
  )
1099
1165
 
1100
1166
 
1101
- def shrratio_mgnsym_boundary_avg(
1167
+ def shrratio_boundary_avg(
1102
1168
  _delta_star: float = 0.075,
1103
1169
  _r_val: float = 0.80,
1104
1170
  /,
@@ -1106,11 +1172,17 @@ def shrratio_mgnsym_boundary_avg(
1106
1172
  avg_method: Literal["arithmetic", "geometric", "distance"] = "arithmetic",
1107
1173
  recapture_spec: Literal["inside-out", "proportional"] = "inside-out",
1108
1174
  gbd_dps: int = 5,
1109
- ) -> tuple[NDArray[np.float64], float]:
1175
+ ) -> GuidelinesBoundary:
1110
1176
  """
1111
1177
  Share combinations along the average GUPPI boundary, with
1112
1178
  symmetric merging-firm margins.
1113
1179
 
1180
+ Reimplements the unweighted average and distance estimations from function,
1181
+ `shrratio_boundary_wtd_avg`. This reimplementation
1182
+ is primarifly useful for testing the output of `shrratio_boundary_wtd_avg`
1183
+ as it tests considerably slower.
1184
+
1185
+
1114
1186
  Parameters
1115
1187
  ----------
1116
1188
  _delta_star
@@ -1118,7 +1190,7 @@ def shrratio_mgnsym_boundary_avg(
1118
1190
  _r_val
1119
1191
  Recapture ratio.
1120
1192
  avg_method
1121
- Whether "arithmetic", "geometric", or "root-mean-square".
1193
+ Whether "arithmetic", "geometric", or "distance".
1122
1194
  recapture_spec
1123
1195
  Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
1124
1196
  value for both merging firms ("proportional").
@@ -1137,7 +1209,7 @@ def shrratio_mgnsym_boundary_avg(
1137
1209
  "Margin-adjusted benchmark share ratio cannot exceed 1."
1138
1210
  )
1139
1211
 
1140
- if avg_method not in (_avgmthds := ("arithmetic", "geometric", "root-mean-square")):
1212
+ if avg_method not in (_avgmthds := ("arithmetic", "geometric", "distance")):
1141
1213
  raise ValueError(
1142
1214
  f"Averarging method, {f'"{avg_method}"'} is invalid. "
1143
1215
  f"Must be one of, {_avgmthds!r}."
@@ -1151,18 +1223,21 @@ def shrratio_mgnsym_boundary_avg(
1151
1223
 
1152
1224
  _delta_star = mpf(f"{_delta_star}")
1153
1225
  _s_mid = _delta_star / (1 + _delta_star)
1154
- _gbd_step_sz = mp.power(10, -gbd_dps)
1155
1226
 
1227
+ # initial conditions
1156
1228
  _s_2 = _s_mid
1157
1229
  _s_2_oddval = True
1158
1230
  _s_2_oddsum = 0
1159
1231
  _s_2_evnsum = 0
1160
1232
  _gbdry_points = [(_s_mid, _s_mid)]
1233
+
1234
+ # parameters for iteration
1235
+ _gbd_step_sz = mp.power(10, -gbd_dps)
1161
1236
  for _s_1 in mp.arange(_s_mid, 0, -_gbd_step_sz):
1162
1237
  _s_1 -= _gbd_step_sz
1163
1238
  while True:
1164
- _shratio_12 = _s_2 / (1 - _s_1)
1165
- _shratio_21 = (
1239
+ _delta_12 = _s_2 / (1 - _s_1)
1240
+ _delta_21 = (
1166
1241
  _s_1 / (1 - _s_2)
1167
1242
  if recapture_spec == "proportional"
1168
1243
  else _s_1 / (1 - lerp(_s_1, _s_2, _r_val))
@@ -1170,13 +1245,21 @@ def shrratio_mgnsym_boundary_avg(
1170
1245
 
1171
1246
  match avg_method:
1172
1247
  case "geometric":
1173
- _small_d = mp.sqrt(_shratio_12 * _shratio_21)
1248
+ _delta_test = mp.sqrt(_delta_12 * _delta_21)
1174
1249
  case "distance":
1175
- _small_d = mp.sqrt(mp.fdiv((_shratio_12**2 + _shratio_21**2), "2"))
1250
+ # _delta_test = mp.sqrt(mp.fdiv((_delta_12**2 + _delta_21**2), "2"))
1251
+ _delta_test = mp.sqrt(
1252
+ mp.fdiv(
1253
+ mp.fsum(
1254
+ mp.power(f"{_g}", "2") for _g in (_delta_12, _delta_21)
1255
+ ),
1256
+ "2",
1257
+ )
1258
+ )
1176
1259
  case _:
1177
- _small_d = mp.fdiv(_shratio_12 + _shratio_21, "2")
1260
+ _delta_test = mp.fdiv(_delta_12 + _delta_21, "2")
1178
1261
 
1179
- if _small_d < _delta_star:
1262
+ if _delta_test < _delta_star:
1180
1263
  _s_2 += _gbd_step_sz
1181
1264
  else:
1182
1265
  break
@@ -1189,16 +1272,178 @@ def shrratio_mgnsym_boundary_avg(
1189
1272
 
1190
1273
  # Starting at _s_id - _gbd_step_sz means _s_1 is not always
1191
1274
  # an even multiple of _gbd_step_sz
1192
- _s_incpt = _s_2
1275
+ _s_intcpt = _s_2
1193
1276
 
1194
1277
  _gbd_prtlarea = 2 * _gbd_step_sz * (
1195
1278
  mp.fmul(4 / 3, _s_2_oddsum)
1196
1279
  + mp.fmul(2 / 3, _s_2_evnsum)
1197
- + mp.fmul(1 / 3, _s_mid + _s_incpt)
1280
+ + mp.fmul(1 / 3, _s_mid + _s_intcpt)
1198
1281
  ) - mp.power(_s_mid, 2)
1199
1282
 
1200
1283
  _gbdry_points = np.array(_gbdry_points, np.float64)
1201
- return (
1284
+ return GuidelinesBoundary(
1202
1285
  np.row_stack((np.flip(_gbdry_points, 0), np.flip(_gbdry_points[1:], 1))),
1203
1286
  round(float(_gbd_prtlarea), gbd_dps),
1204
1287
  )
1288
+
1289
+
1290
+ def shrratio_boundary_distance(
1291
+ _delta_star: float = 0.075,
1292
+ _r_val: float = 0.80,
1293
+ /,
1294
+ *,
1295
+ avg_method: Literal["arithmetic", "distance"] = "arithmetic",
1296
+ wgtng_policy: Literal["own-share", "cross-product-share"] | None = "own-share",
1297
+ recapture_spec: Literal["inside-out", "proportional"] = "inside-out",
1298
+ gbd_dps: int = 5,
1299
+ ) -> GuidelinesBoundary:
1300
+ """
1301
+ Share combinations for the GUPPI boundaries using various aggregators with
1302
+ symmetric merging-firm margins.
1303
+
1304
+ Reimplements the arithmetic-averages and distance estimations from function,
1305
+ `shrratio_boundary_wtd_avg`but uses the Minkowski-distance function,
1306
+ `scipy.spatial.distance.minkowski` for all aggregators. This reimplementation
1307
+ is primarifly useful for testing the output of `shrratio_boundary_wtd_avg`
1308
+ as it tests considerably slower.
1309
+
1310
+ Parameters
1311
+ ----------
1312
+ _delta_star
1313
+ corollary to GUPPI bound (:math:`\\overline{g} / (m^* \\cdot \\overline{r})`)
1314
+ _r_val
1315
+ recapture ratio
1316
+ avg_method
1317
+ Whether "arithmetic", "geometric", or "distance".
1318
+ wgtng_policy
1319
+ Whether "own-share" or "cross-product-share".
1320
+ recapture_spec
1321
+ Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
1322
+ value for both merging firms ("proportional").
1323
+ gbd_dps
1324
+ Number of decimal places for rounding returned shares and area.
1325
+
1326
+ Returns
1327
+ -------
1328
+ Array of share-pairs, area under boundary.
1329
+
1330
+ """
1331
+
1332
+ if _delta_star > 1:
1333
+ raise ValueError(
1334
+ "Margin-adjusted benchmark share ratio, `_delta_star` cannot exceed 1."
1335
+ )
1336
+
1337
+ _delta_star = mpf(f"{_delta_star}")
1338
+ _s_mid = _delta_star / (1 + _delta_star)
1339
+
1340
+ # initial conditions
1341
+ _gbdry_points = [(_s_mid, _s_mid)]
1342
+ _s_1_pre, _s_2_pre = _s_mid, _s_mid
1343
+ _s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0, 0
1344
+
1345
+ # parameters for iteration
1346
+ _weights_base = (mpf("0.5"),) * 2
1347
+ _gbd_step_sz = mp.power(10, -gbd_dps)
1348
+ _theta = _gbd_step_sz * (10 if wgtng_policy == "cross-product-share" else 1)
1349
+ for _s_1 in mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz):
1350
+ # The wtd. avg. GUPPI is not always convex to the origin, so we
1351
+ # increment _s_2 after each iteration in which our algorithm
1352
+ # finds (s1, s2) on the boundary
1353
+ _s_2 = _s_2_pre * (1 + _theta)
1354
+
1355
+ if (_s_1 + _s_2) > mpf("0.99875"):
1356
+ # 1: # We lose accuracy at 3-9s and up
1357
+ break
1358
+
1359
+ while True:
1360
+ _de_1 = _s_2 / (1 - _s_1)
1361
+ _de_2 = (
1362
+ _s_1 / (1 - lerp(_s_1, _s_2, _r_val))
1363
+ if recapture_spec == "inside-out"
1364
+ else _s_1 / (1 - _s_2)
1365
+ )
1366
+
1367
+ _weights_i = (
1368
+ (
1369
+ _w1 := mp.fdiv(
1370
+ _s_2 if wgtng_policy == "cross-product-share" else _s_1,
1371
+ _s_1 + _s_2,
1372
+ ),
1373
+ 1 - _w1,
1374
+ )
1375
+ if wgtng_policy
1376
+ else _weights_base
1377
+ )
1378
+
1379
+ match avg_method:
1380
+ case "arithmetic":
1381
+ _delta_test = distance_function(
1382
+ (_de_1, _de_2), (0.0, 0.0), p=1, w=_weights_i
1383
+ )
1384
+ case "distance":
1385
+ _delta_test = distance_function(
1386
+ (_de_1, _de_2), (0.0, 0.0), p=2, w=_weights_i
1387
+ )
1388
+
1389
+ if wgtng_policy == "cross-product-share":
1390
+ _test_flag, _incr_decr = (_delta_test > _delta_star, -1)
1391
+ else:
1392
+ _test_flag, _incr_decr = (_delta_test < _delta_star, 1)
1393
+
1394
+ if _test_flag:
1395
+ _s_2 += _incr_decr * _gbd_step_sz
1396
+ else:
1397
+ break
1398
+
1399
+ # Build-up boundary points
1400
+ _gbdry_points.append((_s_1, _s_2))
1401
+
1402
+ # Build up area terms
1403
+ _s_2_oddsum += _s_2 if _s_2_oddval else 0
1404
+ _s_2_evnsum += _s_2 if not _s_2_oddval else 0
1405
+ _s_2_oddval = not _s_2_oddval
1406
+
1407
+ # Hold share points
1408
+ _s_2_pre = _s_2
1409
+ _s_1_pre = _s_1
1410
+
1411
+ _gbd_prtlarea = _gbd_step_sz * (
1412
+ (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _delta_star) / 3
1413
+ if wgtng_policy == "cross-product-share"
1414
+ else (
1415
+ (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_2_pre) / 3
1416
+ + _s_1_pre * (1 + _s_2_pre) / 2
1417
+ )
1418
+ )
1419
+
1420
+ # Area under boundary
1421
+ _gbdry_area_total = 2 * _gbd_prtlarea - mp.power(_s_mid, 2)
1422
+
1423
+ match wgtng_policy:
1424
+ case "cross-product-share":
1425
+ _s_intcpt = _delta_star
1426
+ case "own-product-share":
1427
+ _s_intcpt = mpf("1.0")
1428
+ case None if avg_method == "distance":
1429
+ _s_intcpt = _delta_star * mp.sqrt("2")
1430
+ case None if avg_method == "arithmetic" and recapture_spec == "inside-out":
1431
+ _s_intcpt = mp.fdiv(
1432
+ mp.fsub(
1433
+ 2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
1434
+ ),
1435
+ 2 * mpf(f"{_r_val}"),
1436
+ )
1437
+ case None if avg_method == "arithmetic":
1438
+ _s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
1439
+ case _:
1440
+ _s_intcpt = _s_2_pre
1441
+
1442
+ _gbdry_points = np.row_stack((_gbdry_points, (mpf("0.0"), _s_intcpt))).astype(
1443
+ np.float64
1444
+ )
1445
+ # Points defining boundary to point-of-symmetry
1446
+ return GuidelinesBoundary(
1447
+ np.row_stack((np.flip(_gbdry_points, 0), np.flip(_gbdry_points[1:], 1))),
1448
+ round(float(_gbdry_area_total), gbd_dps),
1449
+ )