mergeron 2024.738949.1__py3-none-any.whl → 2024.738949.6__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.

@@ -5,6 +5,7 @@ with a canvas on which to draw boundaries for Guidelines standards.
5
5
  """
6
6
 
7
7
  import decimal
8
+ from collections.abc import Callable
8
9
  from dataclasses import dataclass
9
10
  from importlib.metadata import version
10
11
  from typing import Any, Literal, TypeAlias
@@ -13,9 +14,9 @@ import numpy as np
13
14
  from attrs import define, field
14
15
  from mpmath import mp, mpf # type: ignore
15
16
  from numpy.typing import NDArray
16
- from scipy.spatial.distance import minkowski as distance_function
17
17
 
18
- from .. import _PKG_NAME # noqa: TID252
18
+ from .. import _PKG_NAME, UPPAggrSelector # noqa: TID252
19
+ from . import UPPBoundarySpec
19
20
 
20
21
  __version__ = version(_PKG_NAME)
21
22
 
@@ -27,12 +28,6 @@ HMGPubYear: TypeAlias = Literal[1992, 2010, 2023]
27
28
 
28
29
 
29
30
  @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
31
  class HMGThresholds:
37
32
  delta: float
38
33
  rec: float
@@ -42,6 +37,19 @@ class HMGThresholds:
42
37
  ipr: float
43
38
 
44
39
 
40
+ @dataclass(slots=True, frozen=True)
41
+ class GuidelinesBoundary:
42
+ coordinates: NDArray[np.float64]
43
+ area: float
44
+
45
+
46
+ @dataclass(slots=True, frozen=True)
47
+ class GuidelinesBoundaryCallable:
48
+ boundary_function: Callable[[NDArray[np.float64]], NDArray[np.float64]]
49
+ area: float
50
+ s_naught: float = 0
51
+
52
+
45
53
  @define(slots=True, frozen=True)
46
54
  class GuidelinesThresholds:
47
55
  """
@@ -81,7 +89,7 @@ class GuidelinesThresholds:
81
89
  diversion ratio limit, CMCR, and IPR
82
90
  """
83
91
 
84
- def __attrs_post_init__(self, /):
92
+ def __attrs_post_init__(self, /) -> None:
85
93
  # In the 2023 Guidlines, the agencies do not define a
86
94
  # negative presumption, or safeharbor. Practically speaking,
87
95
  # given resource constraints and loss aversion, it is likely
@@ -278,7 +286,12 @@ def gbd_from_dsf(
278
286
 
279
287
 
280
288
  def critical_shrratio(
281
- _gbd: float = 0.06, /, *, m_star: float = 1.00, r_bar: float = 0.80
289
+ _gbd: float = 0.06,
290
+ /,
291
+ *,
292
+ m_star: float = 1.00,
293
+ r_bar: float = 0.80,
294
+ frac: float = 1e-16,
282
295
  ) -> mpf:
283
296
  """
284
297
  Corollary to GUPPI bound.
@@ -298,7 +311,7 @@ def critical_shrratio(
298
311
  for given margin and recapture rate.
299
312
 
300
313
  """
301
- return mpf(f"{_gbd}") / mp.fmul(f"{m_star}", f"{r_bar}")
314
+ return round_cust(mpf(f"{_gbd}") / mp.fmul(f"{m_star}", f"{r_bar}"), frac=frac)
302
315
 
303
316
 
304
317
  def shr_from_gbd(
@@ -371,18 +384,18 @@ def boundary_plot(*, mktshares_plot_flag: bool = True) -> tuple[Any, ...]:
371
384
  r"\setsansfont{Fira Sans Light}",
372
385
  R"\setmonofont[Scale=MatchLowercase,]{Fira Mono}",
373
386
  R"\defaultfontfeatures[\rmfamily]{",
374
- R" Ligatures={TeX, Common},",
375
- R" Numbers={Proportional, Lining},",
376
- R" }",
387
+ R" Ligatures={TeX, Common},",
388
+ R" Numbers={Proportional, Lining},",
389
+ R" }",
377
390
  R"\defaultfontfeatures[\sffamily]{",
378
- R" Ligatures={TeX, Common},",
379
- R" Numbers={Monospaced, Lining},",
380
- R" LetterSpace=0.50,",
381
- R" }",
391
+ R" Ligatures={TeX, Common},",
392
+ R" Numbers={Monospaced, Lining},",
393
+ R" LetterSpace=0.50,",
394
+ R" }",
382
395
  R"\usepackage[",
383
- R" activate={true, nocompatibility},",
384
- R" tracking=true,",
385
- R" ]{microtype}",
396
+ R" activate={true, nocompatibility},",
397
+ R" tracking=true,",
398
+ R" ]{microtype}",
386
399
  ]),
387
400
  })
388
401
 
@@ -527,11 +540,11 @@ def dh_area(_dh_val: float = 0.01, /, *, dh_dps: int = 9) -> float:
527
540
  """
528
541
 
529
542
  _dh_val = mpf(f"{_dh_val}")
530
- _s1_zero = (1 - mp.sqrt(1 - 2 * _dh_val)) / 2
531
- _s1_one = 1 - _s1_zero
543
+ _s_naught = (1 - mp.sqrt(1 - 2 * _dh_val)) / 2
532
544
 
533
545
  return round(
534
- float(_s1_zero + (_dh_val / 2) * (mp.ln(_s1_one) - mp.ln(_s1_zero))), dh_dps
546
+ float(_s_naught + (_dh_val / 2) * (mp.ln(1 - _s_naught) - mp.ln(_s_naught))),
547
+ dh_dps,
535
548
  )
536
549
 
537
550
 
@@ -557,17 +570,18 @@ def dh_area_quad(_dh_val: float = 0.01, /, *, dh_dps: int = 9) -> float:
557
570
  """
558
571
 
559
572
  _dh_val = mpf(f"{_dh_val}")
560
- _s1_zero = (1 - mp.sqrt(1 - 2 * _dh_val)) / 2
561
- _s1_one = 1 - _s1_zero
573
+ _s_naught = (1 - mp.sqrt(1 - 2 * _dh_val)) / 2
562
574
 
563
575
  return round(
564
- float(_s1_zero + mp.quad(lambda x: _dh_val / (2 * x), [_s1_zero, _s1_one])),
576
+ float(
577
+ _s_naught + mp.quad(lambda x: _dh_val / (2 * x), [_s_naught, 1 - _s_naught])
578
+ ),
565
579
  dh_dps,
566
580
  )
567
581
 
568
582
 
569
583
  def delta_hhi_boundary(
570
- _dh_val: float = 0.01, /, *, dh_dps: int = 5
584
+ _dh_val: float = 0.01, /, *, prec: int = 5
571
585
  ) -> GuidelinesBoundary:
572
586
  """
573
587
  Generate the list of share combination on the ΔHHI boundary.
@@ -586,19 +600,16 @@ def delta_hhi_boundary(
586
600
  """
587
601
 
588
602
  _dh_val = mpf(f"{_dh_val}")
589
- _s1_zero = 1 / 2 * (1 - mp.sqrt(1 - 2 * _dh_val))
590
- _s1_one = 1 - _s1_zero
591
-
603
+ _s_naught = 1 / 2 * (1 - mp.sqrt(1 - 2 * _dh_val))
592
604
  _s_mid = mp.sqrt(_dh_val / 2)
593
605
 
594
606
  _dh_step_sz = mp.power(10, -6)
595
- _s_1 = np.array(mp.arange(_s_mid, _s1_zero, -_dh_step_sz))
607
+ _s_1 = np.array(mp.arange(_s_mid, _s_naught - mp.eps, -_dh_step_sz))
596
608
  _s_2 = _dh_val / (2 * _s_1)
597
609
 
598
610
  # Boundary points
599
611
  _dh_half = np.row_stack((
600
612
  np.column_stack((_s_1, _s_2)),
601
- np.array([(_s1_zero, _s1_one)]),
602
613
  np.array([(mpf("0.0"), mpf("1.0"))]),
603
614
  ))
604
615
  _dh_bdry_pts = np.row_stack((np.flip(_dh_half, 0), np.flip(_dh_half[1:], 1)))
@@ -609,7 +620,7 @@ def delta_hhi_boundary(
609
620
  np.array(_s_1_pts, np.float64),
610
621
  np.array(_s_2_pts, np.float64),
611
622
  )),
612
- dh_area(_dh_val, dh_dps=dh_dps),
623
+ dh_area(_dh_val, dh_dps=prec),
613
624
  )
614
625
 
615
626
 
@@ -680,8 +691,60 @@ def hhi_pre_contrib_boundary(
680
691
  )
681
692
 
682
693
 
694
+ def shrratio_boundary(_bdry_spec: UPPBoundarySpec) -> GuidelinesBoundary:
695
+ match _bdry_spec.agg_method:
696
+ case UPPAggrSelector.AVG:
697
+ return shrratio_boundary_xact_avg(
698
+ _bdry_spec.share_ratio,
699
+ _bdry_spec.rec,
700
+ recapture_spec=_bdry_spec.recapture_spec.value, # type: ignore
701
+ prec=_bdry_spec.precision,
702
+ )
703
+ case UPPAggrSelector.MAX:
704
+ return shrratio_boundary_max(
705
+ _bdry_spec.share_ratio, _bdry_spec.rec, prec=_bdry_spec.precision
706
+ )
707
+ case UPPAggrSelector.MIN:
708
+ return shrratio_boundary_min(
709
+ _bdry_spec.share_ratio,
710
+ _bdry_spec.rec,
711
+ recapture_spec=_bdry_spec.recapture_spec.value, # type: ignore
712
+ prec=_bdry_spec.precision,
713
+ )
714
+ case UPPAggrSelector.DIS:
715
+ return shrratio_boundary_wtd_avg(
716
+ _bdry_spec.share_ratio,
717
+ _bdry_spec.rec,
718
+ agg_method="distance",
719
+ weighting=None,
720
+ recapture_spec=_bdry_spec.recapture_spec.value, # type: ignore
721
+ prec=_bdry_spec.precision,
722
+ )
723
+ case _:
724
+ _weighting = (
725
+ "cross-product-share"
726
+ if _bdry_spec.agg_method.value.startswith("cross-product-share")
727
+ else "own-share"
728
+ )
729
+
730
+ _agg_method = (
731
+ "arithmetic"
732
+ if _bdry_spec.agg_method.value.endswith("average")
733
+ else "distance"
734
+ )
735
+
736
+ return shrratio_boundary_wtd_avg(
737
+ _bdry_spec.share_ratio,
738
+ _bdry_spec.rec,
739
+ agg_method=_agg_method, # type: ignore
740
+ weighting=_weighting, # type: ignore
741
+ recapture_spec=_bdry_spec.recapture_spec.value, # type: ignore
742
+ prec=_bdry_spec.precision,
743
+ )
744
+
745
+
683
746
  def shrratio_boundary_max(
684
- _delta_star: float = 0.075, _r_val: float = 0.80, /, *, gbd_dps: int = 10
747
+ _delta_star: float = 0.075, _r_val: float = 0.80, /, *, prec: int = 10
685
748
  ) -> GuidelinesBoundary:
686
749
  """
687
750
  Share combinations on the minimum GUPPI boundary with symmetric
@@ -693,7 +756,7 @@ def shrratio_boundary_max(
693
756
  Margin-adjusted benchmark share ratio.
694
757
  _r_val
695
758
  Recapture ratio.
696
- gbd_dps
759
+ prec
697
760
  Number of decimal places for rounding returned shares.
698
761
 
699
762
  Returns
@@ -702,12 +765,6 @@ def shrratio_boundary_max(
702
765
 
703
766
  """
704
767
 
705
- if _delta_star > 1:
706
- raise ValueError(
707
- "Invalid combination specified; "
708
- "Margin-adjusted benchmark share ratio cannot exceed 1."
709
- )
710
-
711
768
  # _r_val is not needed for max boundary, but is specified for consistency
712
769
  # of function call with other shrratio_mgnsym_boundary functions
713
770
  del _r_val
@@ -722,7 +779,7 @@ def shrratio_boundary_max(
722
779
  np.array(_s1_pts, np.float64),
723
780
  np.array(_s1_pts[::-1], np.float64),
724
781
  )),
725
- round(float(_s_intcpt * _s_mid), gbd_dps), # simplified calculation
782
+ round(float(_s_intcpt * _s_mid), prec), # simplified calculation
726
783
  )
727
784
 
728
785
 
@@ -732,7 +789,7 @@ def shrratio_boundary_min(
732
789
  /,
733
790
  *,
734
791
  recapture_spec: str = "inside-out",
735
- gbd_dps: int = 10,
792
+ prec: int = 10,
736
793
  ) -> GuidelinesBoundary:
737
794
  """
738
795
  Share combinations on the minimum GUPPI boundary, with symmetric
@@ -754,7 +811,7 @@ def shrratio_boundary_min(
754
811
  recapture_spec
755
812
  Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
756
813
  value for both merging firms ("proportional").
757
- gbd_dps
814
+ prec
758
815
  Number of decimal places for rounding returned shares.
759
816
 
760
817
  Returns
@@ -763,14 +820,6 @@ def shrratio_boundary_min(
763
820
 
764
821
  """
765
822
 
766
- if _delta_star > 1:
767
- raise ValueError("Margin-adjusted benchmark share ratio cannot exceed 1.")
768
-
769
- if recapture_spec not in (_recspecs := ("inside-out", "proportional")):
770
- raise ValueError(
771
- f"Recapture_spec value, {f'"{recapture_spec}"'} not in {_recspecs!r}"
772
- )
773
-
774
823
  _delta_star = mpf(f"{_delta_star}")
775
824
  _s_intcpt = mpf("1.00")
776
825
  _s_mid = _delta_star / (1 + _delta_star)
@@ -797,7 +846,7 @@ def shrratio_boundary_min(
797
846
  _s1_pts, _gbd_area = np.array((0, _s_mid, _s_intcpt), np.float64), _s_mid
798
847
 
799
848
  return GuidelinesBoundary(
800
- np.column_stack((_s1_pts, _s1_pts[::-1])), round(float(_gbd_area), gbd_dps)
849
+ np.column_stack((_s1_pts, _s1_pts[::-1])), round(float(_gbd_area), prec)
801
850
  )
802
851
 
803
852
 
@@ -806,25 +855,45 @@ def shrratio_boundary_wtd_avg(
806
855
  _r_val: float = 0.80,
807
856
  /,
808
857
  *,
809
- avg_method: Literal["arithmetic", "geometric", "distance"] = "arithmetic",
810
- wgtng_policy: Literal["own-share", "cross-product-share"] | None = "own-share",
858
+ agg_method: Literal["arithmetic", "geometric", "distance"] = "arithmetic",
859
+ weighting: Literal["own-share", "cross-product-share"] | None = "own-share",
811
860
  recapture_spec: Literal["inside-out", "proportional"] = "inside-out",
812
- gbd_dps: int = 5,
861
+ prec: int = 5,
813
862
  ) -> GuidelinesBoundary:
814
863
  """
815
864
  Share combinations for the share-weighted average GUPPI boundary with symmetric
816
865
  merging-firm margins.
817
866
 
867
+ Parameters
868
+ ----------
869
+ _delta_star
870
+ corollary to GUPPI bound (:math:`\\overline{g} / (m^* \\cdot \\overline{r})`)
871
+ _r_val
872
+ recapture ratio
873
+ agg_method
874
+ Whether "arithmetic", "geometric", or "distance".
875
+ weighting
876
+ Whether "own-share" or "cross-product-share" (or None for simple, unweighted average).
877
+ recapture_spec
878
+ Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
879
+ value for both merging firms ("proportional").
880
+ prec
881
+ Number of decimal places for rounding returned shares and area.
882
+
883
+ Returns
884
+ -------
885
+ Array of share-pairs, area under boundary.
886
+
818
887
  Notes
819
888
  -----
820
889
  An analytical expression for the share-weighted arithmetic mean boundary
821
890
  is derived and plotted from y-intercept to the ray of symmetry as follows::
822
891
 
823
892
  from sympy import plot as symplot, solve, symbols
824
- s_1, s_2, delta_star = symbols("s_1 s_2 \\delta^*")
893
+ s_1, s_2 = symbols("s_1 s_2", positive=True)
825
894
 
826
895
  g_val, r_val, m_val = 0.06, 0.80, 0.30
827
- d_hat = g_val / (r_val * m_val)
896
+ delta_star = g_val / (r_val * m_val)
828
897
 
829
898
  # recapture_spec == "inside-out"
830
899
  oswag = solve(
@@ -834,7 +903,7 @@ def shrratio_boundary_wtd_avg(
834
903
  s_2
835
904
  )[0]
836
905
  symplot(
837
- oswag.subs({delta_star: d_hat}),
906
+ oswag,
838
907
  (s_1, 0., d_hat / (1 + d_hat)),
839
908
  ylabel=s_2
840
909
  )
@@ -846,7 +915,7 @@ def shrratio_boundary_wtd_avg(
846
915
  s_2
847
916
  )[1]
848
917
  symplot(
849
- cpwag.subs({delta_star: d_hat}),
918
+ cpwag,
850
919
  (s_1, 0., d_hat / (1 + d_hat)),
851
920
  ylabel=s_2
852
921
  )
@@ -859,7 +928,7 @@ def shrratio_boundary_wtd_avg(
859
928
  s_2
860
929
  )[0]
861
930
  symplot(
862
- oswag.subs({delta_star: d_hat}),
931
+ oswag,
863
932
  (s_1, 0., d_hat / (1 + d_hat)),
864
933
  ylabel=s_2
865
934
  )
@@ -871,38 +940,14 @@ def shrratio_boundary_wtd_avg(
871
940
  s_2
872
941
  )[1]
873
942
  symplot(
874
- cpswag.subs({delta_star: d_hat}),
943
+ cpswag,
875
944
  (s_1, 0.0, d_hat / (1 + d_hat)),
876
945
  ylabel=s_2
877
946
  )
878
947
 
879
- Parameters
880
- ----------
881
- _delta_star
882
- corollary to GUPPI bound (:math:`\\overline{g} / (m^* \\cdot \\overline{r})`)
883
- _r_val
884
- recapture ratio
885
- avg_method
886
- Whether "arithmetic", "geometric", or "distance".
887
- wgtng_policy
888
- Whether "own-share" or "cross-product-share".
889
- recapture_spec
890
- Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
891
- value for both merging firms ("proportional").
892
- gbd_dps
893
- Number of decimal places for rounding returned shares and area.
894
-
895
- Returns
896
- -------
897
- Array of share-pairs, area under boundary.
898
948
 
899
949
  """
900
950
 
901
- if _delta_star > 1:
902
- raise ValueError(
903
- "Margin-adjusted benchmark share ratio, `_delta_star` cannot exceed 1."
904
- )
905
-
906
951
  _delta_star = mpf(f"{_delta_star}")
907
952
  _s_mid = _delta_star / (1 + _delta_star)
908
953
 
@@ -912,8 +957,8 @@ def shrratio_boundary_wtd_avg(
912
957
  _s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0, 0
913
958
 
914
959
  # parameters for iteration
915
- _gbd_step_sz = mp.power(10, -gbd_dps)
916
- _theta = _gbd_step_sz * (10 if wgtng_policy == "cross-product-share" else 1)
960
+ _gbd_step_sz = mp.power(10, -prec)
961
+ _theta = _gbd_step_sz * (10 if weighting == "cross-product-share" else 1)
917
962
  for _s_1 in mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz):
918
963
  # The wtd. avg. GUPPI is not always convex to the origin, so we
919
964
  # increment _s_2 after each iteration in which our algorithm
@@ -921,7 +966,7 @@ def shrratio_boundary_wtd_avg(
921
966
  _s_2 = _s_2_pre * (1 + _theta)
922
967
 
923
968
  if (_s_1 + _s_2) > mpf("0.99875"):
924
- # 1: # We lose accuracy at 3-9s and up
969
+ # Loss of accuracy at 3-9s and up
925
970
  break
926
971
 
927
972
  while True:
@@ -934,13 +979,13 @@ def shrratio_boundary_wtd_avg(
934
979
 
935
980
  _r = (
936
981
  mp.fdiv(
937
- _s_1 if wgtng_policy == "cross-product-share" else _s_2, _s_1 + _s_2
982
+ _s_1 if weighting == "cross-product-share" else _s_2, _s_1 + _s_2
938
983
  )
939
- if wgtng_policy
984
+ if weighting
940
985
  else 0.5
941
986
  )
942
987
 
943
- match avg_method:
988
+ match agg_method:
944
989
  case "geometric":
945
990
  _delta_test = mp.expm1(lerp(mp.log1p(_de_1), mp.log1p(_de_2), _r))
946
991
  case "distance":
@@ -948,10 +993,11 @@ def shrratio_boundary_wtd_avg(
948
993
  case _:
949
994
  _delta_test = lerp(_de_1, _de_2, _r)
950
995
 
951
- if wgtng_policy == "cross-product-share":
952
- _test_flag, _incr_decr = (_delta_test > _delta_star, -1)
953
- else:
954
- _test_flag, _incr_decr = (_delta_test < _delta_star, 1)
996
+ _test_flag, _incr_decr = (
997
+ (_delta_test > _delta_star, -1)
998
+ if weighting == "cross-product-share"
999
+ else (_delta_test < _delta_star, 1)
1000
+ )
955
1001
 
956
1002
  if _test_flag:
957
1003
  _s_2 += _incr_decr * _gbd_step_sz
@@ -970,35 +1016,45 @@ def shrratio_boundary_wtd_avg(
970
1016
  _s_2_pre = _s_2
971
1017
  _s_1_pre = _s_1
972
1018
 
973
- _gbd_prtlarea = _gbd_step_sz * (
974
- (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _delta_star) / 3
975
- if wgtng_policy == "cross-product-share"
976
- else (
977
- (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_2_pre) / 3
978
- + _s_1_pre * (1 + _s_2_pre) / 2
979
- )
1019
+ if _s_2_oddval:
1020
+ _s_2_evnsum -= _s_2_pre
1021
+ else:
1022
+ _s_2_oddsum -= _s_1_pre
1023
+
1024
+ _s_intcpt = _shrratio_boundary_intcpt(
1025
+ _s_1_pre,
1026
+ _delta_star,
1027
+ _r_val,
1028
+ recapture_spec=recapture_spec,
1029
+ agg_method=agg_method,
1030
+ weighting=weighting,
980
1031
  )
981
1032
 
982
- # Area under boundary
983
- _gbdry_area_total = 2 * _gbd_prtlarea - mp.power(_s_mid, 2)
1033
+ if weighting == "own-share":
1034
+ _gbd_prtlarea = (
1035
+ _gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_2_pre) / 3
1036
+ )
1037
+ # Area under boundary
1038
+ _gbdry_area_total = float(
1039
+ 2 * (_s_1_pre + _gbd_prtlarea)
1040
+ - (mp.power(_s_mid, "2") + mp.power(_s_1_pre, "2"))
1041
+ )
984
1042
 
985
- match wgtng_policy:
986
- case "cross-product-share":
987
- _s_intcpt = _delta_star
988
- case "own-product-share":
989
- _s_intcpt = mpf("1.0")
990
- case None if avg_method == "distance":
991
- _s_intcpt = _delta_star * mp.sqrt("2")
992
- case _:
993
- _s_intcpt = _s_2_pre
1043
+ else:
1044
+ _gbd_prtlarea = (
1045
+ _gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_intcpt) / 3
1046
+ )
1047
+ # Area under boundary
1048
+ _gbdry_area_total = float(2 * _gbd_prtlarea - mp.power(_s_mid, "2"))
994
1049
 
995
1050
  _gbdry_points = np.row_stack((_gbdry_points, (mpf("0.0"), _s_intcpt))).astype(
996
1051
  np.float64
997
1052
  )
1053
+
998
1054
  # Points defining boundary to point-of-symmetry
999
1055
  return GuidelinesBoundary(
1000
1056
  np.row_stack((np.flip(_gbdry_points, 0), np.flip(_gbdry_points[1:], 1))),
1001
- round(float(_gbdry_area_total), gbd_dps),
1057
+ round(float(_gbdry_area_total), prec),
1002
1058
  )
1003
1059
 
1004
1060
 
@@ -1008,7 +1064,7 @@ def shrratio_boundary_xact_avg(
1008
1064
  /,
1009
1065
  *,
1010
1066
  recapture_spec: Literal["inside-out", "proportional"] = "inside-out",
1011
- gbd_dps: int = 5,
1067
+ prec: int = 5,
1012
1068
  ) -> GuidelinesBoundary:
1013
1069
  """
1014
1070
  Share combinations for the simple average GUPPI boundary with symmetric
@@ -1056,7 +1112,7 @@ def shrratio_boundary_xact_avg(
1056
1112
  recapture_spec
1057
1113
  Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
1058
1114
  value for both merging firms ("proportional").
1059
- gbd_dps
1115
+ prec
1060
1116
  Number of decimal places for rounding returned shares.
1061
1117
 
1062
1118
  Returns
@@ -1065,18 +1121,9 @@ def shrratio_boundary_xact_avg(
1065
1121
 
1066
1122
  """
1067
1123
 
1068
- if _delta_star > 1:
1069
- raise ValueError(
1070
- "Invalid combination specified; "
1071
- "Margin-adjusted benchmark share ratio cannot exceed 1."
1072
- )
1073
-
1074
- if recapture_spec not in (_recspecs := ("inside-out", "proportional")):
1075
- raise ValueError(f"Recapture spec must be one of {_recspecs:!}")
1076
-
1077
1124
  _delta_star = mpf(f"{_delta_star}")
1078
1125
  _s_mid = _delta_star / (1 + _delta_star)
1079
- _gbd_step_sz = mp.power(10, -gbd_dps)
1126
+ _gbd_step_sz = mp.power(10, -prec)
1080
1127
 
1081
1128
  _gbdry_points_start = np.array([(_s_mid, _s_mid)])
1082
1129
  _s_1 = np.array(mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz), np.float64)
@@ -1113,10 +1160,10 @@ def shrratio_boundary_xact_avg(
1113
1160
  _nr_t2_s1 = _nr_sqrt_s1sq + _nr_sqrt_s1 + _nr_sqrt_nos1
1114
1161
 
1115
1162
  if not np.isclose( # type: ignore
1116
- np.einsum("i->", _nr_t2_mdr.astype(np.float64)), # type: ignore from mpf to float64
1117
- np.einsum("i->", _nr_t2_s1.astype(np.float64)), # type: ignore from mpf to float64
1163
+ np.einsum("i->", _nr_t2_mdr.astype(np.float64)),
1164
+ np.einsum("i->", _nr_t2_s1.astype(np.float64)),
1118
1165
  rtol=0,
1119
- atol=0.5 * gbd_dps,
1166
+ atol=0.5 * prec,
1120
1167
  ):
1121
1168
  raise RuntimeError(
1122
1169
  "Calculation of sq. root term in exact average GUPPI"
@@ -1166,290 +1213,37 @@ def shrratio_boundary_xact_avg(
1166
1213
  _s_1_pts, _s_2_pts = np.split(_gbdry_points, 2, axis=1)
1167
1214
  return GuidelinesBoundary(
1168
1215
  np.column_stack((np.array(_s_1_pts), np.array(_s_2_pts))),
1169
- round(float(_gbdry_area_simpson), gbd_dps),
1170
- )
1171
-
1172
-
1173
- def shrratio_boundary_avg(
1174
- _delta_star: float = 0.075,
1175
- _r_val: float = 0.80,
1176
- /,
1177
- *,
1178
- avg_method: Literal["arithmetic", "geometric", "distance"] = "arithmetic",
1179
- recapture_spec: Literal["inside-out", "proportional"] = "inside-out",
1180
- gbd_dps: int = 5,
1181
- ) -> GuidelinesBoundary:
1182
- """
1183
- Share combinations along the average GUPPI boundary, with
1184
- symmetric merging-firm margins.
1185
-
1186
- Reimplements the unweighted average and distance estimations from function,
1187
- `shrratio_boundary_wtd_avg`. This reimplementation
1188
- is primarifly useful for testing the output of `shrratio_boundary_wtd_avg`
1189
- as it tests considerably slower.
1190
-
1191
-
1192
- Parameters
1193
- ----------
1194
- _delta_star
1195
- Margin-adjusted benchmark share ratio.
1196
- _r_val
1197
- Recapture ratio.
1198
- avg_method
1199
- Whether "arithmetic", "geometric", or "distance".
1200
- recapture_spec
1201
- Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
1202
- value for both merging firms ("proportional").
1203
- gbd_dps
1204
- Number of decimal places for rounding returned shares.
1205
-
1206
- Returns
1207
- -------
1208
- Array of share-pairs, area under boundary.
1209
-
1210
- """
1211
-
1212
- if _delta_star > 1:
1213
- raise ValueError(
1214
- "Invalid combination specified; "
1215
- "Margin-adjusted benchmark share ratio cannot exceed 1."
1216
- )
1217
-
1218
- if avg_method not in (_avgmthds := ("arithmetic", "geometric", "distance")):
1219
- raise ValueError(
1220
- f"Averarging method, {f'"{avg_method}"'} is invalid. "
1221
- f"Must be one of, {_avgmthds!r}."
1222
- )
1223
-
1224
- if recapture_spec not in (_recspecs := ("inside-out", "proportional")):
1225
- raise ValueError(
1226
- f"Recapture spec, {f'"{recapture_spec}"'} is invalid. "
1227
- f"Must be one of {_recspecs!r}."
1228
- )
1229
-
1230
- _delta_star = mpf(f"{_delta_star}")
1231
- _s_mid = _delta_star / (1 + _delta_star)
1232
-
1233
- # initial conditions
1234
- _s_2 = _s_mid
1235
- _s_2_oddval = True
1236
- _s_2_oddsum = 0
1237
- _s_2_evnsum = 0
1238
- _gbdry_points = [(_s_mid, _s_mid)]
1239
-
1240
- # parameters for iteration
1241
- _gbd_step_sz = mp.power(10, -gbd_dps)
1242
- for _s_1 in mp.arange(_s_mid, 0, -_gbd_step_sz):
1243
- _s_1 -= _gbd_step_sz
1244
- while True:
1245
- _delta_12 = _s_2 / (1 - _s_1)
1246
- _delta_21 = (
1247
- _s_1 / (1 - _s_2)
1248
- if recapture_spec == "proportional"
1249
- else _s_1 / (1 - lerp(_s_1, _s_2, _r_val))
1250
- )
1251
-
1252
- match avg_method:
1253
- case "geometric":
1254
- _delta_test = mp.sqrt(_delta_12 * _delta_21)
1255
- case "distance":
1256
- # _delta_test = mp.sqrt(mp.fdiv((_delta_12**2 + _delta_21**2), "2"))
1257
- _delta_test = mp.sqrt(
1258
- mp.fdiv(
1259
- mp.fsum(
1260
- mp.power(f"{_g}", "2") for _g in (_delta_12, _delta_21)
1261
- ),
1262
- "2",
1263
- )
1264
- )
1265
- case _:
1266
- _delta_test = mp.fdiv(_delta_12 + _delta_21, "2")
1267
-
1268
- if _delta_test < _delta_star:
1269
- _s_2 += _gbd_step_sz
1270
- else:
1271
- break
1272
-
1273
- _gbdry_points.append((_s_1, _s_2))
1274
-
1275
- _s_2_oddsum += _s_2 if _s_2_oddval else 0
1276
- _s_2_evnsum += _s_2 if not _s_2_oddval else 0
1277
- _s_2_oddval = not _s_2_oddval
1278
-
1279
- # Starting at _s_id - _gbd_step_sz means _s_1 is not always
1280
- # an even multiple of _gbd_step_sz
1281
- _s_intcpt = _s_2
1282
-
1283
- _gbd_prtlarea = 2 * _gbd_step_sz * (
1284
- mp.fmul(4 / 3, _s_2_oddsum)
1285
- + mp.fmul(2 / 3, _s_2_evnsum)
1286
- + mp.fmul(1 / 3, _s_mid + _s_intcpt)
1287
- ) - mp.power(_s_mid, 2)
1288
-
1289
- _gbdry_points = np.array(_gbdry_points, np.float64)
1290
- return GuidelinesBoundary(
1291
- np.row_stack((np.flip(_gbdry_points, 0), np.flip(_gbdry_points[1:], 1))),
1292
- round(float(_gbd_prtlarea), gbd_dps),
1216
+ round(float(_gbdry_area_simpson), prec),
1293
1217
  )
1294
1218
 
1295
1219
 
1296
- def shrratio_boundary_distance(
1297
- _delta_star: float = 0.075,
1298
- _r_val: float = 0.80,
1220
+ def _shrratio_boundary_intcpt(
1221
+ _s_2_pre: float,
1222
+ _delta_star: mpf,
1223
+ _r_val: mpf,
1299
1224
  /,
1300
1225
  *,
1301
- avg_method: Literal["arithmetic", "distance"] = "arithmetic",
1302
- wgtng_policy: Literal["own-share", "cross-product-share"] | None = "own-share",
1303
- recapture_spec: Literal["inside-out", "proportional"] = "inside-out",
1304
- gbd_dps: int = 5,
1305
- ) -> GuidelinesBoundary:
1306
- """
1307
- Share combinations for the GUPPI boundaries using various aggregators with
1308
- symmetric merging-firm margins.
1309
-
1310
- Reimplements the arithmetic-averages and distance estimations from function,
1311
- `shrratio_boundary_wtd_avg`but uses the Minkowski-distance function,
1312
- `scipy.spatial.distance.minkowski` for all aggregators. This reimplementation
1313
- is primarifly useful for testing the output of `shrratio_boundary_wtd_avg`
1314
- as it tests considerably slower.
1315
-
1316
- Parameters
1317
- ----------
1318
- _delta_star
1319
- corollary to GUPPI bound (:math:`\\overline{g} / (m^* \\cdot \\overline{r})`)
1320
- _r_val
1321
- recapture ratio
1322
- avg_method
1323
- Whether "arithmetic", "geometric", or "distance".
1324
- wgtng_policy
1325
- Whether "own-share" or "cross-product-share".
1326
- recapture_spec
1327
- Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
1328
- value for both merging firms ("proportional").
1329
- gbd_dps
1330
- Number of decimal places for rounding returned shares and area.
1331
-
1332
- Returns
1333
- -------
1334
- Array of share-pairs, area under boundary.
1335
-
1336
- """
1337
-
1338
- if _delta_star > 1:
1339
- raise ValueError(
1340
- "Margin-adjusted benchmark share ratio, `_delta_star` cannot exceed 1."
1341
- )
1342
-
1343
- _delta_star = mpf(f"{_delta_star}")
1344
- _s_mid = _delta_star / (1 + _delta_star)
1345
-
1346
- # initial conditions
1347
- _gbdry_points = [(_s_mid, _s_mid)]
1348
- _s_1_pre, _s_2_pre = _s_mid, _s_mid
1349
- _s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0, 0
1350
-
1351
- # parameters for iteration
1352
- _weights_base = (mpf("0.5"),) * 2
1353
- _gbd_step_sz = mp.power(10, -gbd_dps)
1354
- _theta = _gbd_step_sz * (10 if wgtng_policy == "cross-product-share" else 1)
1355
- for _s_1 in mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz):
1356
- # The wtd. avg. GUPPI is not always convex to the origin, so we
1357
- # increment _s_2 after each iteration in which our algorithm
1358
- # finds (s1, s2) on the boundary
1359
- _s_2 = _s_2_pre * (1 + _theta)
1360
-
1361
- if (_s_1 + _s_2) > mpf("0.99875"):
1362
- # 1: # We lose accuracy at 3-9s and up
1363
- break
1364
-
1365
- while True:
1366
- _de_1 = _s_2 / (1 - _s_1)
1367
- _de_2 = (
1368
- _s_1 / (1 - lerp(_s_1, _s_2, _r_val))
1369
- if recapture_spec == "inside-out"
1370
- else _s_1 / (1 - _s_2)
1371
- )
1372
-
1373
- _weights_i = (
1374
- (
1375
- _w1 := mp.fdiv(
1376
- _s_2 if wgtng_policy == "cross-product-share" else _s_1,
1377
- _s_1 + _s_2,
1378
- ),
1379
- 1 - _w1,
1380
- )
1381
- if wgtng_policy
1382
- else _weights_base
1383
- )
1384
-
1385
- match avg_method:
1386
- case "arithmetic":
1387
- _delta_test = distance_function(
1388
- (_de_1, _de_2), (0.0, 0.0), p=1, w=_weights_i
1389
- )
1390
- case "distance":
1391
- _delta_test = distance_function(
1392
- (_de_1, _de_2), (0.0, 0.0), p=2, w=_weights_i
1393
- )
1394
-
1395
- if wgtng_policy == "cross-product-share":
1396
- _test_flag, _incr_decr = (_delta_test > _delta_star, -1)
1397
- else:
1398
- _test_flag, _incr_decr = (_delta_test < _delta_star, 1)
1399
-
1400
- if _test_flag:
1401
- _s_2 += _incr_decr * _gbd_step_sz
1402
- else:
1403
- break
1404
-
1405
- # Build-up boundary points
1406
- _gbdry_points.append((_s_1, _s_2))
1407
-
1408
- # Build up area terms
1409
- _s_2_oddsum += _s_2 if _s_2_oddval else 0
1410
- _s_2_evnsum += _s_2 if not _s_2_oddval else 0
1411
- _s_2_oddval = not _s_2_oddval
1412
-
1413
- # Hold share points
1414
- _s_2_pre = _s_2
1415
- _s_1_pre = _s_1
1416
-
1417
- _gbd_prtlarea = _gbd_step_sz * (
1418
- (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _delta_star) / 3
1419
- if wgtng_policy == "cross-product-share"
1420
- else (
1421
- (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_2_pre) / 3
1422
- + _s_1_pre * (1 + _s_2_pre) / 2
1423
- )
1424
- )
1425
-
1426
- # Area under boundary
1427
- _gbdry_area_total = 2 * _gbd_prtlarea - mp.power(_s_mid, 2)
1428
-
1429
- match wgtng_policy:
1226
+ recapture_spec: Literal["inside-out", "proportional"],
1227
+ agg_method: Literal["arithmetic", "geometric", "distance"],
1228
+ weighting: Literal["cross-product-share", "own-share"] | None,
1229
+ ) -> float:
1230
+ match weighting:
1430
1231
  case "cross-product-share":
1431
- _s_intcpt = _delta_star
1432
- case "own-product-share":
1232
+ _s_intcpt: float = _delta_star
1233
+ case "own-share":
1433
1234
  _s_intcpt = mpf("1.0")
1434
- case None if avg_method == "distance":
1235
+ case None if agg_method == "distance":
1435
1236
  _s_intcpt = _delta_star * mp.sqrt("2")
1436
- case None if avg_method == "arithmetic" and recapture_spec == "inside-out":
1237
+ case None if agg_method == "arithmetic" and recapture_spec == "inside-out":
1437
1238
  _s_intcpt = mp.fdiv(
1438
1239
  mp.fsub(
1439
1240
  2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
1440
1241
  ),
1441
1242
  2 * mpf(f"{_r_val}"),
1442
1243
  )
1443
- case None if avg_method == "arithmetic":
1244
+ case None if agg_method == "arithmetic":
1444
1245
  _s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
1445
1246
  case _:
1446
1247
  _s_intcpt = _s_2_pre
1447
1248
 
1448
- _gbdry_points = np.row_stack((_gbdry_points, (mpf("0.0"), _s_intcpt))).astype(
1449
- np.float64
1450
- )
1451
- # Points defining boundary to point-of-symmetry
1452
- return GuidelinesBoundary(
1453
- np.row_stack((np.flip(_gbdry_points, 0), np.flip(_gbdry_points[1:], 1))),
1454
- round(float(_gbdry_area_total), gbd_dps),
1455
- )
1249
+ return _s_intcpt