mergeron 2025.739341.10__py3-none-any.whl → 2025.739355.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mergeron might be problematic. Click here for more details.

mergeron/__init__.py CHANGED
@@ -15,7 +15,7 @@ from ruamel import yaml
15
15
 
16
16
  _PKG_NAME: str = Path(__file__).parent.name
17
17
 
18
- VERSION = "2025.739341.10"
18
+ VERSION = "2025.739355.1"
19
19
 
20
20
  __version__ = VERSION
21
21
 
mergeron/core/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Mapping
5
+ from collections.abc import Callable, Mapping
6
6
  from decimal import Decimal
7
7
  from types import MappingProxyType
8
8
  from typing import Any
@@ -51,7 +51,7 @@ from scipy import stats # type: ignore
51
51
 
52
52
  from .. import NTHREADS, VERSION, ArrayDouble, this_yaml # noqa: TID252
53
53
  from .. import WORK_DIR as PKG_WORK_DIR # noqa: TID252
54
- from . import DEFAULT_BITGENERATOR
54
+ from . import DEFAULT_BITGENERATOR, _mappingproxy_from_mapping
55
55
 
56
56
  __version__ = VERSION
57
57
 
@@ -60,7 +60,9 @@ WORK_DIR = globals().get("WORK_DIR", PKG_WORK_DIR)
60
60
 
61
61
  MGNDATA_ARCHIVE_PATH = WORK_DIR / "damodaran_margin_data_serialized.zip"
62
62
 
63
- type DamodaranMarginData = MappingProxyType[str, MappingProxyType[str, float | int]]
63
+ type DamodaranMarginData = MappingProxyType[
64
+ str, MappingProxyType[str, MappingProxyType[str, float | int]]
65
+ ]
64
66
 
65
67
  FINANCIAL_INDUSTRIES = {
66
68
  _i.upper()
@@ -170,8 +172,8 @@ def margin_data_builder(
170
172
 
171
173
  _missing = {"GROSS MARGIN": 0.0, "NUMBER OF FIRMS": 0.0}
172
174
  gm, fc = zip(*[
173
- [_v.get(_sk, _missing).get(_f) for _f in _missing]
174
- for _k, _v in _margin_data_dict.items()
175
+ [_v.get(_sk, _missing)[_f] for _f in _missing]
176
+ for _v in _margin_data_dict.values()
175
177
  ])
176
178
 
177
179
  average_margin, firm_count = np.array(gm, float), np.array(fc, int)
@@ -223,7 +225,7 @@ def margin_data_getter(
223
225
  ws_pat = re.compile(r"\s+")
224
226
 
225
227
  # Parse workbooks and save margin data dictionary
226
- margin_data_dict = {}
228
+ margin_data_: dict[str, dict[str, MappingProxyType[str, float]]] = {}
227
229
  for _p in (WORK_DIR / "damodaran_margin_data_archive").iterdir():
228
230
  xl_wbk = CalamineWorkbook.from_path(_p)
229
231
  xl_wks = xl_wbk.get_sheet_by_index(
@@ -231,36 +233,35 @@ def margin_data_getter(
231
233
  ).to_python()
232
234
  if xl_wks[8][2] != "Gross Margin":
233
235
  raise ValueError("Worksheet does not match expected layout.")
236
+ row_keys: list[str] = [_c.upper() for _c in xl_wks[8][1:]] # type: ignore
234
237
 
235
- update = xl_wks[0][1].isoformat()[:10]
236
- margin_data_annual = margin_data_dict.setdefault(update, {})
237
- row_keys: list[str] = []
238
- read_row_flag = False
239
- for xl_row in xl_wks:
238
+ _u = xl_wks[0][1]
239
+ if not isinstance(_u, datetime.datetime):
240
+ raise ValueError("Worksheet does not match expected layout.")
241
+ update: str = _u.isoformat()[:10]
242
+
243
+ margin_data_annual = margin_data_.setdefault(update, {})
244
+ for xl_row in xl_wks[9:]:
240
245
  row_key = _s.upper() if isinstance((_s := xl_row[0]), str) else ""
241
246
 
242
- if ws_pat.sub(" ", row_key) == "INDUSTRY NAME":
243
- read_row_flag = True
244
- row_keys = [_c.upper() for _c in xl_row]
245
- continue
246
- elif not read_row_flag or not row_key or row_key.startswith("TOTAL"):
247
+ if not row_key or row_key.startswith("TOTAL"):
247
248
  continue
248
249
  else:
249
- xl_row[1] = int(xl_row[1])
250
+ xl_row[1] = int(xl_row[1]) # type: ignore
250
251
  margin_data_annual |= MappingProxyType({
251
252
  row_key: MappingProxyType(
252
- dict(zip(row_keys[1:], xl_row[1:], strict=True))
253
+ dict(zip(row_keys, xl_row[1:], strict=True)) # type: ignore
253
254
  )
254
255
  })
255
256
 
256
- damodaran_margin_data = MappingProxyType(margin_data_dict)
257
+ margin_data_map: DamodaranMarginData = _mappingproxy_from_mapping(margin_data_)
257
258
  with (
258
259
  zipfile.ZipFile(data_archive_path, "w") as _yzp,
259
260
  _yzp.open(f"{data_archive_path.stem}.yaml", "w") as _yfh,
260
261
  ):
261
- this_yaml.dump(damodaran_margin_data, _yfh)
262
+ this_yaml.dump(margin_data_map, _yfh)
262
263
 
263
- return damodaran_margin_data
264
+ return margin_data_map
264
265
 
265
266
 
266
267
  def margin_data_downloader() -> None:
@@ -334,18 +334,18 @@ class DiversionRatioBoundary:
334
334
 
335
335
  match self.agg_method:
336
336
  case UPPAggrSelector.DIS:
337
- upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
337
+ upp_agg_fn = gbfn.diversion_share_boundary_wtd_avg
338
338
  upp_agg_kwargs |= {"agg_method": "distance", "weighting": None}
339
339
  case UPPAggrSelector.AVG:
340
- upp_agg_fn = gbfn.shrratio_boundary_xact_avg # type: ignore
340
+ upp_agg_fn = gbfn.diversion_share_boundary_xact_avg # type: ignore
341
341
  case UPPAggrSelector.MAX:
342
- upp_agg_fn = gbfn.shrratio_boundary_max # type: ignore
342
+ upp_agg_fn = gbfn.diversion_share_boundary_max # type: ignore
343
343
  upp_agg_kwargs = {"dps": 10} # replace here
344
344
  case UPPAggrSelector.MIN:
345
- upp_agg_fn = gbfn.shrratio_boundary_min # type: ignore
345
+ upp_agg_fn = gbfn.diversion_share_boundary_min # type: ignore
346
346
  upp_agg_kwargs |= {"dps": 10} # update here
347
347
  case _:
348
- upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
348
+ upp_agg_fn = gbfn.diversion_share_boundary_wtd_avg
349
349
 
350
350
  aggregator_: Literal["arithmetic mean", "geometric mean", "distance"]
351
351
  if self.agg_method.value.endswith("geometric mean"):
@@ -202,7 +202,7 @@ def hhi_post_contrib_boundary(
202
202
 
203
203
 
204
204
  # hand-rolled root finding
205
- def shrratio_boundary_wtd_avg(
205
+ def diversion_share_boundary_wtd_avg(
206
206
  _delta_star: float = 0.075,
207
207
  _r_val: float = DEFAULT_REC_RATIO,
208
208
  /,
@@ -370,7 +370,7 @@ def shrratio_boundary_wtd_avg(
370
370
  else:
371
371
  s_2_oddsum -= s_1_pre
372
372
 
373
- _s_intcpt = _shrratio_boundary_intcpt(
373
+ _s_intcpt = _diversion_share_boundary_intcpt(
374
374
  s_2_pre,
375
375
  _delta_star,
376
376
  _r_val,
@@ -406,7 +406,7 @@ def shrratio_boundary_wtd_avg(
406
406
  )
407
407
 
408
408
 
409
- def shrratio_boundary_xact_avg(
409
+ def diversion_share_boundary_xact_avg(
410
410
  _delta_star: float = 0.075,
411
411
  _r_val: float = DEFAULT_REC_RATIO,
412
412
  /,
@@ -467,47 +467,46 @@ def shrratio_boundary_xact_avg(
467
467
  Array of share-pairs, area under boundary, area under boundary.
468
468
 
469
469
  """
470
- _delta_star, _r_val = (mpf(f"{_v}") for _v in (_delta_star, _r_val))
471
470
  _s_mid = _delta_star / (1 + _delta_star)
472
- _step_size = mp.power(10, -dps)
471
+ _step_size = 10**-dps
473
472
 
474
473
  _bdry_start = np.array([(_s_mid, _s_mid)])
475
- _s_1 = np.array(mp.arange(_s_mid - _step_size, 0, -_step_size))
474
+ _s_1 = np.arange(_s_mid - _step_size, 0, -_step_size)
476
475
  if recapture_form == "inside-out":
477
- _s_intcpt = mp.fdiv(
478
- mp.fsub(
479
- 2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
480
- ),
481
- 2 * mpf(f"{_r_val}"),
476
+ _s_intcpt: float = (
477
+ 2 * _delta_star * _r_val + 1 - np.abs(2 * _delta_star * _r_val - 1)
478
+ ) / (2 * _r_val)
479
+ nr_t1: ArrayDouble = (
480
+ 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val)
482
481
  )
483
- nr_t1 = 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val)
484
482
 
485
- nr_sqrt_mdr = 4 * _delta_star * _r_val
486
- nr_sqrt_mdr2 = nr_sqrt_mdr * _r_val
487
- nr_sqrt_md2r2 = nr_sqrt_mdr2 * _delta_star
483
+ nr_sqrt_mdr: float = 4 * _delta_star * _r_val
484
+ nr_sqrt_mdr2: float = nr_sqrt_mdr * _r_val
485
+ nr_sqrt_md2r2: float = nr_sqrt_mdr2 * _delta_star
488
486
 
489
- nr_sqrt_t1 = nr_sqrt_md2r2 * (_s_1**2 - 2 * _s_1 + 1)
490
- nr_sqrt_t2 = nr_sqrt_mdr2 * _s_1 * (_s_1 - 1)
491
- nr_sqrt_t3 = nr_sqrt_mdr * (2 * _s_1 - _s_1**2 - 1)
492
- nr_sqrt_t4 = (_s_1**2) * (_r_val**2 - 6 * _r_val + 1)
493
- nr_sqrt_t5 = _s_1 * (6 * _r_val - 2) + 1
487
+ nr_sqrt_t1: ArrayDouble = nr_sqrt_md2r2 * (_s_1**2 - 2 * _s_1 + 1)
488
+ nr_sqrt_t2: ArrayDouble = nr_sqrt_mdr2 * _s_1 * (_s_1 - 1)
489
+ nr_sqrt_t3: ArrayDouble = nr_sqrt_mdr * (2 * _s_1 - _s_1**2 - 1)
490
+ nr_sqrt_t4: ArrayDouble = (_s_1**2) * (_r_val**2 - 6 * _r_val + 1)
491
+ nr_sqrt_t5: ArrayDouble = _s_1 * (6 * _r_val - 2) + 1
494
492
 
495
- nr_t2_mdr = nr_sqrt_t1 + nr_sqrt_t2 + nr_sqrt_t3 + nr_sqrt_t4 + nr_sqrt_t5
493
+ nr_t2_mdr: ArrayDouble = (
494
+ nr_sqrt_t1 + nr_sqrt_t2 + nr_sqrt_t3 + nr_sqrt_t4 + nr_sqrt_t5
495
+ )
496
496
 
497
497
  # Alternative grouping of terms in np.sqrt
498
- nr_sqrt_s1sq = (_s_1**2) * (
499
- nr_sqrt_md2r2 + nr_sqrt_mdr2 - nr_sqrt_mdr + _r_val**2 - 6 * _r_val + 1
500
- )
501
- nr_sqrt_s1 = _s_1 * (
498
+ nr_sqrt_nos1: float = nr_sqrt_md2r2 - nr_sqrt_mdr + 1
499
+ nr_sqrt_s1: ArrayDouble = _s_1 * (
502
500
  -2 * nr_sqrt_md2r2 - nr_sqrt_mdr2 + 2 * nr_sqrt_mdr + 6 * _r_val - 2
503
501
  )
504
- nr_sqrt_nos1 = nr_sqrt_md2r2 - nr_sqrt_mdr + 1
505
-
506
- nr_t2_s1 = nr_sqrt_s1sq + nr_sqrt_s1 + nr_sqrt_nos1
502
+ nr_sqrt_s1sq: ArrayDouble = (_s_1**2) * (
503
+ nr_sqrt_md2r2 + nr_sqrt_mdr2 - nr_sqrt_mdr + _r_val**2 - 6 * _r_val + 1
504
+ )
505
+ nr_t2_s1: ArrayDouble = nr_sqrt_s1sq + nr_sqrt_s1 + nr_sqrt_nos1
507
506
 
508
507
  if not np.isclose(
509
- np.einsum("i->", nr_t2_mdr.astype(float)),
510
- np.einsum("i->", nr_t2_s1.astype(float)),
508
+ np.einsum("i->", nr_t2_mdr),
509
+ np.einsum("i->", nr_t2_s1),
511
510
  rtol=0,
512
511
  atol=0.5 * dps,
513
512
  ):
@@ -516,25 +515,28 @@ def shrratio_boundary_xact_avg(
516
515
  f"with recapture spec, {f'"{recapture_form}"'} is incorrect."
517
516
  )
518
517
 
519
- s_2 = (nr_t1 - np.sqrt(nr_t2_s1)) / (2 * _r_val)
518
+ s_2: ArrayDouble = (nr_t1 - nr_t2_s1**0.5) / (2 * _r_val)
520
519
 
521
520
  else:
522
- _s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
523
- s_2 = (
524
- (1 / 2)
521
+ _s_intcpt: float = _delta_star + 1 / 2 - np.abs(_delta_star - 1 / 2)
522
+ s_2: ArrayDouble = (
523
+ 0.5
525
524
  + _delta_star
526
525
  - _delta_star * _s_1
527
- - np.sqrt(
528
- ((_delta_star**2) - 1) * (_s_1**2)
529
- + (-2 * (_delta_star**2) + _delta_star + 1) * _s_1
530
- + (_delta_star**2)
531
- - _delta_star
532
- + (1 / 4)
526
+ - (
527
+ (
528
+ ((_delta_star**2) - 1) * (_s_1**2)
529
+ + (-2 * (_delta_star**2) + _delta_star + 1) * _s_1
530
+ + (_delta_star**2)
531
+ - _delta_star
532
+ + (1 / 4)
533
+ )
534
+ ** 0.5
533
535
  )
534
536
  )
535
537
 
536
538
  bdry_inner = np.stack((_s_1, s_2), axis=1)
537
- bdry_end = np.array([(mpf("0.0"), _s_intcpt)], float)
539
+ bdry_end = np.array([(0.0, _s_intcpt)], float)
538
540
 
539
541
  bdry = np.vstack((
540
542
  bdry_end,
@@ -542,14 +544,14 @@ def shrratio_boundary_xact_avg(
542
544
  _bdry_start,
543
545
  bdry_inner[:, ::-1],
544
546
  bdry_end[:, ::-1],
545
- )).astype(float)
547
+ ))
546
548
  s_2 = np.concatenate((np.array([_s_mid], float), s_2))
547
549
 
548
550
  bdry_ends = [0, -1]
549
551
  bdry_odds = np.array(range(1, len(s_2), 2), int)
550
552
  bdry_evns = np.array(range(2, len(s_2), 2), int)
551
553
 
552
- # Double the are under the curve, and subtract the double counted bit.
554
+ # Double the area under the curve, and subtract the double counted bit.
553
555
  bdry_area_simpson = 2 * _step_size * (
554
556
  (4 / 3) * np.sum(s_2.take(bdry_odds))
555
557
  + (2 / 3) * np.sum(s_2.take(bdry_evns))
@@ -559,7 +561,7 @@ def shrratio_boundary_xact_avg(
559
561
  return GuidelinesBoundary(bdry, round(float(bdry_area_simpson), dps))
560
562
 
561
563
 
562
- def shrratio_boundary_min(
564
+ def diversion_share_boundary_min(
563
565
  _delta_star: float = 0.075,
564
566
  _r_val: float = DEFAULT_REC_RATIO,
565
567
  /,
@@ -615,7 +617,7 @@ def shrratio_boundary_min(
615
617
  )
616
618
 
617
619
 
618
- def shrratio_boundary_max(
620
+ def diversion_share_boundary_max(
619
621
  _delta_star: float = 0.075, _: float = DEFAULT_REC_RATIO, /, *, dps: int = 10
620
622
  ) -> GuidelinesBoundary:
621
623
  R"""
@@ -647,7 +649,7 @@ def shrratio_boundary_max(
647
649
  )
648
650
 
649
651
 
650
- def _shrratio_boundary_intcpt(
652
+ def _diversion_share_boundary_intcpt(
651
653
  s_2_pre: float,
652
654
  _delta_star: MPFloat,
653
655
  _r_val: MPFloat,
@@ -783,7 +785,7 @@ def boundary_plot(
783
785
  mktshare_plot_flag: bool = True,
784
786
  mktshare_axes_flag: bool = True,
785
787
  backend: Literal["pgf"] | str | None = "pgf",
786
- ) -> tuple[mpl.pyplot, mpl.pyplot.Figure, mpl.axes.Axes, Callable[..., mpl.axes.Axes]]:
788
+ ) -> tuple[mpl.figure.Figure, Callable[..., None]]:
787
789
  """Set up basic figure and axes for plots of safe harbor boundaries.
788
790
 
789
791
  See, https://matplotlib.org/stable/tutorials/text/pgf.html
@@ -807,9 +809,6 @@ def boundary_plot(
807
809
  R' "luaotfload.patch_font", embedfull, "embedfull"'
808
810
  R")",
809
811
  R"\end{luacode}",
810
- R"\usepackage{mathtools}",
811
- R"\usepackage{unicode-math}",
812
- R"\setmathfont[math-style=ISO]{STIX Two Math}",
813
812
  R"\setmainfont{STIX Two Text}",
814
813
  r"\setsansfont{Fira Sans Light}",
815
814
  R"\setmonofont[Scale=MatchLowercase,]{Fira Mono}",
@@ -822,6 +821,9 @@ def boundary_plot(
822
821
  R" Numbers={Monospaced, Lining},",
823
822
  R" LetterSpace=0.50,",
824
823
  R" }",
824
+ R"\usepackage{mathtools}",
825
+ R"\usepackage{unicode-math}",
826
+ R"\setmathfont[math-style=ISO]{STIX Two Math}",
825
827
  R"\usepackage[",
826
828
  R" activate={true, nocompatibility},",
827
829
  R" tracking=true,",
@@ -831,57 +833,53 @@ def boundary_plot(
831
833
 
832
834
  # Initialize a canvas with a single figure (set of axes)
833
835
  fig_ = plt.figure(figsize=(5, 5), dpi=600)
834
- ax_out = fig_.add_subplot()
836
+ ax_ = fig_.add_subplot()
837
+ # Set the width of axis grid lines, and tick marks:
838
+ # both axes, both major and minor ticks
839
+ # Frame, grid, and face color
840
+ for _spos0 in "left", "bottom":
841
+ ax_.spines[_spos0].set_linewidth(0.5)
842
+ ax_.spines[_spos0].set_zorder(5)
843
+ for _spos1 in "top", "right":
844
+ ax_.spines[_spos1].set_linewidth(0.0)
845
+ ax_.spines[_spos1].set_zorder(0)
846
+ ax_.spines[_spos1].set_visible(False)
847
+ ax_.set_facecolor("#E6E6E6")
848
+
849
+ ax_.grid(linewidth=0.5, linestyle=":", color="grey", zorder=1)
850
+ ax_.tick_params(axis="both", which="both", width=0.5)
851
+
852
+ # Tick marks skip, size, and rotation
853
+ # x-axis
854
+ for _t in ax_.get_xticklabels():
855
+ _t.update({"fontsize": 6, "rotation": 45, "ha": "right"})
856
+ # y-axis
857
+ for _t in ax_.get_yticklabels():
858
+ _t.update({"fontsize": 6, "rotation": 0, "ha": "right"})
835
859
 
836
860
  def _set_axis_def(
837
- ax1_: mpa.Axes,
861
+ ax0_: mpa.Axes,
838
862
  /,
839
863
  *,
840
864
  mktshare_plot_flag: bool = False,
841
865
  mktshare_axes_flag: bool = False,
842
- ) -> mpa.Axes:
843
- # Set the width of axis grid lines, and tick marks:
844
- # both axes, both major and minor ticks
845
- # Frame, grid, and face color
846
- for _spos0 in "left", "bottom":
847
- ax1_.spines[_spos0].set_linewidth(0.5)
848
- ax1_.spines[_spos0].set_zorder(5)
849
- for _spos1 in "top", "right":
850
- ax1_.spines[_spos1].set_linewidth(0.0)
851
- ax1_.spines[_spos1].set_zorder(0)
852
- ax1_.spines[_spos1].set_visible(False)
853
- ax1_.set_facecolor("#E6E6E6")
854
-
855
- ax1_.grid(linewidth=0.5, linestyle=":", color="grey", zorder=1)
856
- ax1_.tick_params(axis="x", which="both", width=0.5)
857
- ax1_.tick_params(axis="y", which="both", width=0.5)
858
-
859
- # Tick marks skip, size, and rotation
860
- # x-axis
861
- plt.setp(
862
- ax1_.xaxis.get_majorticklabels(),
863
- horizontalalignment="right",
864
- fontsize=6,
865
- rotation=45,
866
- )
867
- # y-axis
868
- plt.setp(
869
- ax1_.yaxis.get_majorticklabels(), horizontalalignment="right", fontsize=6
870
- )
871
-
866
+ ) -> None:
872
867
  if mktshare_plot_flag:
868
+ # Axis scale
869
+ ax0_.set_xlim(0, 1)
870
+ ax0_.set_ylim(0, 1)
871
+ ax0_.set_aspect(1.0)
872
+
873
873
  # Plot the ray of symmetry
874
- ax1_.plot(
874
+ ax0_.plot(
875
875
  [0, 1], [0, 1], linewidth=0.5, linestyle=":", color="grey", zorder=1
876
876
  )
877
877
 
878
- # Axis scale
879
- ax1_.set_xlim(0, 1)
880
- ax1_.set_ylim(0, 1)
881
- ax1_.set_aspect(1.0)
882
-
883
- # Truncate the axis frame to a triangle:
884
- ax1_.add_patch(
878
+ # Truncate the axis frame to a triangle bounded by the other diagonal:
879
+ ax0_.plot(
880
+ [0, 1], [1, 0], linestyle="-", linewidth=0.5, color="black", zorder=1
881
+ )
882
+ ax0_.add_patch(
885
883
  mpp.Rectangle(
886
884
  xy=(1.0025, 0.00),
887
885
  width=1.1 * mp.sqrt(2),
@@ -894,48 +892,36 @@ def boundary_plot(
894
892
  zorder=5,
895
893
  )
896
894
  )
897
- # Feasible space is bounded by the other diagonal:
898
- ax1_.plot(
899
- [0, 1], [1, 0], linestyle="-", linewidth=0.5, color="black", zorder=1
900
- )
901
895
 
902
896
  # Axis Tick-mark locations
903
897
  # One can supply an argument to mpt.AutoMinorLocator to
904
898
  # specify a fixed number of minor intervals per major interval, e.g.:
905
899
  # minorLocator = mpt.AutoMinorLocator(2)
906
900
  # would lead to a single minor tick between major ticks.
907
- minor_locator = mpt.AutoMinorLocator(5)
908
- major_locator = mpt.MultipleLocator(0.05)
909
- for axs_ in ax1_.xaxis, ax1_.yaxis:
910
- if axs_ == ax1_.xaxis:
911
- _majorticklabels_rot = 45
912
- elif axs_ == ax1_.yaxis:
913
- _majorticklabels_rot = 0
914
- # x-axis
915
- axs_.set_major_locator(major_locator)
916
- axs_.set_minor_locator(minor_locator)
901
+ for axs_ in ax0_.xaxis, ax0_.yaxis:
902
+ axs_.set_major_locator(mpt.MultipleLocator(0.05))
903
+ axs_.set_minor_locator(mpt.AutoMinorLocator(5))
917
904
  # It"s always x when specifying the format
918
905
  axs_.set_major_formatter(mpt.StrMethodFormatter("{x:>3.0%}"))
919
906
 
920
907
  # Hide every other tick-label
921
- for axl_ in ax1_.get_xticklabels(), ax1_.get_yticklabels():
922
- plt.setp(axl_[::2], visible=False)
908
+ for axl_ in ax0_.get_xticklabels(), ax0_.get_yticklabels():
909
+ for _t in axl_[::2]:
910
+ _t.set_visible(False)
923
911
 
924
912
  # Axis labels
925
913
  if mktshare_axes_flag:
926
914
  # x-axis
927
- ax1_.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
928
- ax1_.xaxis.set_label_coords(0.75, -0.1)
915
+ ax0_.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
916
+ ax0_.xaxis.set_label_coords(0.75, -0.1)
929
917
  # y-axis
930
- ax1_.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
931
- ax1_.yaxis.set_label_coords(-0.1, 0.75)
932
-
933
- return ax1_
918
+ ax0_.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
919
+ ax0_.yaxis.set_label_coords(-0.1, 0.75)
934
920
 
935
- ax_out = _set_axis_def(
936
- ax_out,
921
+ _set_axis_def(
922
+ ax_,
937
923
  mktshare_plot_flag=mktshare_plot_flag,
938
924
  mktshare_axes_flag=mktshare_axes_flag,
939
925
  )
940
926
 
941
- return plt, fig_, ax_out, _set_axis_def
927
+ return fig_, _set_axis_def
@@ -7,11 +7,9 @@ poor performance
7
7
 
8
8
  """
9
9
 
10
- from collections.abc import Callable
11
10
  from typing import Literal
12
11
 
13
12
  import numpy as np
14
- from attrs import frozen
15
13
  from mpmath import mp, mpf # type: ignore
16
14
  from scipy.spatial.distance import minkowski as distance_function # type: ignore
17
15
  from sympy import lambdify, simplify, solve, symbols # type: ignore
@@ -96,7 +94,7 @@ def hhi_delta_boundary_qdtr(_dh_val: float = 0.01, /) -> GuidelinesBoundaryCalla
96
94
  )
97
95
 
98
96
 
99
- def shrratio_boundary_qdtr_wtd_avg(
97
+ def diversion_share_boundary_qdtr_wtd_avg(
100
98
  _delta_star: float = 0.075,
101
99
  _r_val: float = DEFAULT_REC_RATIO,
102
100
  /,
@@ -211,7 +209,7 @@ def shrratio_boundary_qdtr_wtd_avg(
211
209
  )
212
210
 
213
211
 
214
- def shrratio_boundary_distance(
212
+ def diversion_share_boundary_distance(
215
213
  _delta_star: float = 0.075,
216
214
  _r_val: float = DEFAULT_REC_RATIO,
217
215
  /,
@@ -220,14 +218,14 @@ def shrratio_boundary_distance(
220
218
  weighting: Literal["own-share", "cross-product-share"] | None = "own-share",
221
219
  recapture_form: Literal["inside-out", "proportional"] = "inside-out",
222
220
  dps: int = 5,
223
- ) -> gbf.GuidelinesBoundary:
221
+ ) -> GuidelinesBoundary:
224
222
  R"""
225
223
  Share combinations for the share-ratio boundaries using various aggregators.
226
224
 
227
225
  Reimplements the arithmetic-averages and distance estimations from function,
228
- `shrratio_boundary_wtd_avg` but uses the Minkowski-distance function,
226
+ `diversion_share_boundary_wtd_avg` but uses the Minkowski-distance function,
229
227
  `scipy.spatial.distance.minkowski` for all aggregators. This reimplementation
230
- is useful for testing the output of `shrratio_boundary_wtd_avg`
228
+ is useful for testing the output of `diversion_share_boundary_wtd_avg`
231
229
  but runs considerably slower.
232
230
 
233
231
  Parameters
@@ -330,7 +328,7 @@ def shrratio_boundary_distance(
330
328
  else:
331
329
  s_2_oddsum -= s_1_pre
332
330
 
333
- s_intcpt = gbf._shrratio_boundary_intcpt(
331
+ s_intcpt = gbf._diversion_share_boundary_intcpt(
334
332
  s_1_pre,
335
333
  _delta_star,
336
334
  _r_val,
@@ -360,20 +358,20 @@ def shrratio_boundary_distance(
360
358
 
361
359
  bdry_points.append((mpf("0.0"), s_intcpt))
362
360
  # Points defining boundary to point-of-symmetry
363
- return gbf.GuidelinesBoundary(
361
+ return GuidelinesBoundary(
364
362
  np.vstack((bdry_points[::-1], np.flip(bdry_points[1:], 1))),
365
363
  round(float(bdry_area_total), dps),
366
364
  )
367
365
 
368
366
 
369
- def shrratio_boundary_xact_avg_mp(
367
+ def diversion_share_boundary_xact_avg_mp(
370
368
  _delta_star: float = 0.075,
371
369
  _r_val: float = DEFAULT_REC_RATIO,
372
370
  /,
373
371
  *,
374
372
  recapture_form: Literal["inside-out", "proportional"] = "inside-out",
375
373
  dps: int = 5,
376
- ) -> gbf.GuidelinesBoundary:
374
+ ) -> GuidelinesBoundary:
377
375
  R"""
378
376
  Share combinations along the simple average diversion-ratio boundary.
379
377
 
@@ -515,12 +513,12 @@ def shrratio_boundary_xact_avg_mp(
515
513
  + (1 / 3) * np.sum(s_2.take(bdry_ends))
516
514
  ) - mp.power(_s_mid, 2)
517
515
 
518
- return gbf.GuidelinesBoundary(bdry, float(mp.nstr(bdry_area_simpson, dps)))
516
+ return GuidelinesBoundary(bdry, float(mp.nstr(bdry_area_simpson, dps)))
519
517
 
520
518
 
521
- # shrratio_boundary_wtd_avg_autoroot
519
+ # diversion_share_boundary_wtd_avg_autoroot
522
520
  # this function is about half as fast as the manual one! ... and a touch less precise
523
- def _shrratio_boundary_wtd_avg_autoroot(
521
+ def _diversion_share_boundary_wtd_avg_autoroot(
524
522
  _delta_star: float = 0.075,
525
523
  _r_val: float = DEFAULT_REC_RATIO,
526
524
  /,
@@ -687,7 +685,7 @@ def _shrratio_boundary_wtd_avg_autoroot(
687
685
  else:
688
686
  s_2_oddsum -= s_1_pre
689
687
 
690
- _s_intcpt = gbf._shrratio_boundary_intcpt(
688
+ _s_intcpt = gbf._diversion_share_boundary_intcpt(
691
689
  s_2_pre,
692
690
  _delta_star,
693
691
  _r_val,
mergeron/gen/__init__.py CHANGED
@@ -4,8 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import enum
6
6
  import io
7
+ import zipfile
7
8
  from collections.abc import Sequence
8
9
  from operator import attrgetter
10
+ from typing import IO
9
11
 
10
12
  import h5py # type: ignore
11
13
  import numpy as np
@@ -370,7 +372,11 @@ class PCMSpec:
370
372
  _v: ArrayFloat | Sequence[ArrayDouble] | None,
371
373
  ) -> None:
372
374
  if _i.dist_type.name.startswith("BETA"):
373
- if _v is None or not any(_v.shape):
375
+ if (
376
+ _v is None
377
+ or not hasattr(_v, "len")
378
+ or (isinstance(_v, np.ndarray) and not any(_v.shape))
379
+ ):
374
380
  pass
375
381
  elif np.array_equal(_v, DEFAULT_DIST_PARMS):
376
382
  raise ValueError(
@@ -521,7 +527,9 @@ class MarketSampleData:
521
527
  return byte_stream.getvalue()
522
528
 
523
529
  @classmethod
524
- def from_h5f(cls, _hfh: io.BufferedReader) -> MarketSampleData:
530
+ def from_h5f(
531
+ cls, _hfh: io.BufferedReader | zipfile.ZipExtFile | IO[bytes]
532
+ ) -> MarketSampleData:
525
533
  """Load market sample data from HDF5 file."""
526
534
  with h5py.File(_hfh, "r") as _h5f:
527
535
  _retval = cls(**{_a: _h5f[_a][:] for _a in _h5f})
@@ -445,20 +445,22 @@ class MarketSample:
445
445
  this_yaml.dump(self, _yfh)
446
446
 
447
447
  if save_dataset:
448
- if all((_ndt := self.dataset is None, _net := self.enf_counts is None)):
448
+ if self.dataset is None and self.enf_counts is None:
449
449
  raise ValueError(
450
450
  "No dataset and/or enforcement counts available for saving. "
451
451
  "Generate some data or set save_dataset to False to proceed."
452
452
  )
453
453
 
454
- if not _ndt:
455
- with (zpath / f"{name_root}_dataset.h5").open("wb") as _hfh:
456
- _hfh.write(self.dataset.to_h5bin())
454
+ else:
455
+ if self.dataset is not None:
456
+ with (zpath / f"{name_root}_dataset.h5").open("wb") as _hfh:
457
+ _hfh.write(self.dataset.to_h5bin())
457
458
 
458
- if not _net:
459
- with (zpath / f"{name_root}_enf_counts.yaml").open("w") as _yfh:
460
- this_yaml.dump(self.enf_counts, _yfh)
459
+ if self.enf_counts is not None:
460
+ with (zpath / f"{name_root}_enf_counts.yaml").open("w") as _yfh:
461
+ this_yaml.dump(self.enf_counts, _yfh)
461
462
 
463
+ @staticmethod
462
464
  def from_archive(
463
465
  zip_: zipfile.ZipFile, _subdir: str = "", /, *, restore_dataset: bool = False
464
466
  ) -> MarketSample:
@@ -466,27 +468,28 @@ class MarketSample:
466
468
  zpath = zipfile.Path(zip_, at=_subdir)
467
469
  name_root = f"{_PKG_NAME}_market_sample"
468
470
 
469
- market_sample_ = this_yaml.load((zpath / f"{name_root}.yaml").read_text())
471
+ market_sample_: MarketSample = this_yaml.load(
472
+ (zpath / f"{name_root}.yaml").read_text()
473
+ )
470
474
 
471
475
  if restore_dataset:
472
- if not any((
473
- (_dt := (_dp := zpath / f"{name_root}_dataset.h5").is_file()),
474
- (_et := (_ep := zpath / f"{name_root}_enf_counts.yaml").is_file()),
475
- )):
476
+ _dt = (_dp := zpath / f"{name_root}_dataset.h5").is_file()
477
+ _et = (_ep := zpath / f"{name_root}_enf_counts.yaml").is_file()
478
+ if not (_dt or _et):
476
479
  raise ValueError(
477
480
  "Archive has no sample data to restore. "
478
481
  "Delete second argument, or set it False, and rerun."
479
482
  )
480
-
481
- if _dt:
482
- with _dp.open("rb") as _hfh:
483
+ else:
484
+ if _dt:
485
+ with _dp.open("rb") as _hfh:
486
+ object.__setattr__(
487
+ market_sample_, "dataset", MarketSampleData.from_h5f(_hfh)
488
+ )
489
+ if _et:
483
490
  object.__setattr__(
484
- market_sample_, "dataset", MarketSampleData.from_h5f(_hfh)
491
+ market_sample_, "enf_counts", this_yaml.load(_ep.read_text())
485
492
  )
486
- if _et:
487
- object.__setattr__(
488
- market_sample_, "enf_counts", this_yaml.load(_ep.read_text())
489
- )
490
493
  return market_sample_
491
494
 
492
495
  @classmethod
@@ -438,11 +438,13 @@ def gen_divr_array(
438
438
  """
439
439
  divr_array: ArrayDouble
440
440
  if _recapture_form == RECForm.FIXED:
441
- divr_array = _recapture_ratio * _frmshr_array[:, ::-1] / (1 - _frmshr_array) # type: ignore
441
+ divr_array = np.divide(
442
+ _recapture_ratio * _frmshr_array[:, ::-1], 1 - _frmshr_array
443
+ )
442
444
 
443
445
  else:
444
446
  purchprob_array = _aggregate_purchase_prob * _frmshr_array
445
- divr_array = purchprob_array[:, ::-1] / (1 - purchprob_array)
447
+ divr_array = np.divide(purchprob_array[:, ::-1], 1 - purchprob_array)
446
448
 
447
449
  divr_assert_test = (
448
450
  (np.round(np.einsum("ij->i", _frmshr_array), 15) == 1)
@@ -616,7 +618,7 @@ def gen_margin_price_data(
616
618
  # Revenue ratio has been 10-to-1 since inception
617
619
  # Thus, a simple form of the HSR filing test would impose a 10-to-1
618
620
  # ratio restriction on the merging firms' revenues
619
- rev_ratio = (rev_array.min(axis=1) / rev_array.max(axis=1)).round(4)
621
+ rev_ratio = np.divide(rev_array.min(axis=1), rev_array.max(axis=1)).round(4)
620
622
  hsr_filing_test = rev_ratio >= test_rev_ratio_inv
621
623
  # del _rev_array, _rev_ratio
622
624
  case SSZConstant.HSR_NTH:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mergeron
3
- Version: 2025.739341.10
3
+ Version: 2025.739355.1
4
4
  Summary: Python for analyzing merger enforcement policy
5
5
  License: MIT
6
6
  Keywords: merger enforcement policy,merger guidelines,merger screening,enforcement presumptions,concentration standards,diversion ratio,upward pricing pressure,GUPPI
@@ -18,23 +18,22 @@ Classifier: Programming Language :: Python :: 3
18
18
  Classifier: Programming Language :: Python :: 3 :: Only
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: Implementation :: CPython
21
- Requires-Dist: aenum (>=3.1.15,<4.0.0)
22
- Requires-Dist: attrs (>=23.2)
23
- Requires-Dist: bs4 (>=0.0.1)
24
- Requires-Dist: certifi (>=2023.11.17)
25
- Requires-Dist: h5py (>=3.13.0,<4.0.0)
26
- Requires-Dist: jinja2 (>=3.1)
27
- Requires-Dist: joblib (>=1.3)
28
- Requires-Dist: linuxdoc (>=20240924,<20240925)
29
- Requires-Dist: lxml (>=5.3.1,<6.0.0)
30
- Requires-Dist: matplotlib (>=3.8)
31
- Requires-Dist: mpmath (>=1.3)
32
- Requires-Dist: python-calamine (>=0.3.1,<0.4.0)
33
- Requires-Dist: ruamel-yaml (>=0.18.10,<0.19.0)
34
- Requires-Dist: scipy (>=1.12)
35
- Requires-Dist: sympy (>=1.12)
36
- Requires-Dist: types-beautifulsoup4 (>=4.11.2)
37
- Requires-Dist: urllib3 (>=2.2.2,<3.0.0)
21
+ Requires-Dist: aenum (>=3.1.15)
22
+ Requires-Dist: attrs (>=25.3.0)
23
+ Requires-Dist: beautifulsoup4 (>=4.13.3)
24
+ Requires-Dist: certifi (>=2025.1.31)
25
+ Requires-Dist: h5py (>=3.13.0)
26
+ Requires-Dist: jinja2 (>=3.1.6)
27
+ Requires-Dist: joblib (>=1.4.2)
28
+ Requires-Dist: lxml (>=5.3.2)
29
+ Requires-Dist: matplotlib (>=3.10.1)
30
+ Requires-Dist: mpmath (>=1.3.0)
31
+ Requires-Dist: python-calamine (>=0.3.2)
32
+ Requires-Dist: ruamel-yaml (>=0.18.10)
33
+ Requires-Dist: scipy (>=1.15.2)
34
+ Requires-Dist: sympy (>=1.13.3)
35
+ Requires-Dist: types-beautifulsoup4 (>=4.12.0)
36
+ Requires-Dist: urllib3 (>=2.3.0)
38
37
  Project-URL: Documentation, https://capeconomics.github.io/mergeron/
39
38
  Project-URL: Repository, https://github.com/capeconomics/mergeron.git
40
39
  Description-Content-Type: text/x-rst
@@ -87,8 +86,8 @@ To install the package, use the following shell command:
87
86
  pip install mergeron
88
87
 
89
88
 
90
- Documentation
89
+ Documentation
91
90
  -------------
92
91
 
93
- Usage guide and API reference available `here <https://capeconomics.github.io/mergeron/>`_.
92
+ Usage guide and API reference are `available <https://capeconomics.github.io/mergeron/>`_.
94
93
 
@@ -0,0 +1,20 @@
1
+ mergeron/__init__.py,sha256=EuIMqkVajtF2FCtP2VIkeEc9Kw9k4ZR5_dRZjxHbXHs,5773
2
+ mergeron/core/__init__.py,sha256=4Y_q-Qu7gXENVKHS-lNebn5mPZDy9oPHFwUV7fAW9Nw,3269
3
+ mergeron/core/empirical_margin_distribution.py,sha256=aqQ7JYpliHSjHpzyPRkYW9LhJfp-aAlSifRxYx3Dmbo,11623
4
+ mergeron/core/ftc_merger_investigations_data.py,sha256=k4TDkP1rDBmN4uKOYF0SUvSRkYmyVhbsBvLUKDYJqOo,28537
5
+ mergeron/core/guidelines_boundaries.py,sha256=noacM3NmhzqPKLPGm7HEvLKX2UlRI1DCw1kxDa2cFXk,15586
6
+ mergeron/core/guidelines_boundary_functions.py,sha256=yyq2Fn41OwfeSSlKdnvkVwaPMvUalp6_MCOXt49DbKY,28606
7
+ mergeron/core/guidelines_boundary_functions_extra.py,sha256=JompgWmwnwcWsodXrZbvzY_OXw-7ppb_H_Gsz9-fpgI,22080
8
+ mergeron/core/pseudorandom_numbers.py,sha256=-mPveXjJJ446NrBMAmWIa2jI6j0Px0xcCJTGEEsn3bo,10149
9
+ mergeron/data/__init__.py,sha256=CbqheFSkXEe7NOfuAV-NLaaEiNzl9pVCndGjtUUOj9g,1846
10
+ mergeron/data/damodaran_margin_data_serialized.zip,sha256=Wc1v9buSrYTWWAravG8W9nPbgsU07zMtSAR2RvMQU5s,623482
11
+ mergeron/data/ftc_merger_investigations_data.zip,sha256=tiB2TLFyS9LMSFIv8DBA_oEEx12DU4MyjHni4NlsRMU,24002
12
+ mergeron/gen/__init__.py,sha256=bRGGIFBqIVw4-zZ4AKwcNbOTM9aHKegPj7w_7MJV9-I,23856
13
+ mergeron/gen/data_generation.py,sha256=L44YNtxso-Ya50YT71rnG-el4_PgGn4vtoA7rFDD194,17487
14
+ mergeron/gen/data_generation_functions.py,sha256=AaDG0XxpTYsx_2fuUkE20b7bdFq9kfQMWvGIhEFiERw,26106
15
+ mergeron/gen/enforcement_stats.py,sha256=etTax-sBSn8DveF-IxuBJDdX0XSBD6oFU9vaZe6cYks,14387
16
+ mergeron/gen/upp_tests.py,sha256=gRJISQ2jGmIDmFOvaTIkvYooI4mK-QbgkfgL46RrRio,7445
17
+ mergeron/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
18
+ mergeron-2025.739355.1.dist-info/METADATA,sha256=qmUfVT8DcCL6o3cgQz-wK_mWvmANZbE9wSg8FSaxGXs,3864
19
+ mergeron-2025.739355.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
20
+ mergeron-2025.739355.1.dist-info/RECORD,,
@@ -1,20 +0,0 @@
1
- mergeron/__init__.py,sha256=9ZcHQeCALXJpkK-1dqEmimUlO2oPqxDfqICuDaJDvig,5774
2
- mergeron/core/__init__.py,sha256=NChJfOkahbGPa1Ll1fO3agY5-IoDlEra3xNkXPnKIVM,3259
3
- mergeron/core/empirical_margin_distribution.py,sha256=ZidHSloXyGiloO4pgdiQpSaruxXPqVlqfwGgKfLVA2M,11524
4
- mergeron/core/ftc_merger_investigations_data.py,sha256=k4TDkP1rDBmN4uKOYF0SUvSRkYmyVhbsBvLUKDYJqOo,28537
5
- mergeron/core/guidelines_boundaries.py,sha256=Ct6JiuFjSVuCz-m-yeUc50xTC7F4tLgsyqXitb_nObI,15551
6
- mergeron/core/guidelines_boundary_functions.py,sha256=ycz0W5tV9oJ9vVH83IVaprOW4RmG68q8t1QROkQcwxU,29048
7
- mergeron/core/guidelines_boundary_functions_extra.py,sha256=bFRpPGKPHhuhXI67GlEOKtQrNo7WVTK7yMd9NlHCc-M,22095
8
- mergeron/core/pseudorandom_numbers.py,sha256=-mPveXjJJ446NrBMAmWIa2jI6j0Px0xcCJTGEEsn3bo,10149
9
- mergeron/data/__init__.py,sha256=CbqheFSkXEe7NOfuAV-NLaaEiNzl9pVCndGjtUUOj9g,1846
10
- mergeron/data/damodaran_margin_data_serialized.zip,sha256=Wc1v9buSrYTWWAravG8W9nPbgsU07zMtSAR2RvMQU5s,623482
11
- mergeron/data/ftc_merger_investigations_data.zip,sha256=tiB2TLFyS9LMSFIv8DBA_oEEx12DU4MyjHni4NlsRMU,24002
12
- mergeron/gen/__init__.py,sha256=vr4B3AynBeohLPYeb6kD_iPdEOtta-dUfA7_Bi-l-jI,23649
13
- mergeron/gen/data_generation.py,sha256=M7RBJO8distfdFPFQcQM4Mpfi2Etxy9HEw5EoyjWDow,17347
14
- mergeron/gen/data_generation_functions.py,sha256=f6aLBg6JNqOoBq57k3ovevgvtaLfIAfT-ATzZl11CEA,26076
15
- mergeron/gen/enforcement_stats.py,sha256=etTax-sBSn8DveF-IxuBJDdX0XSBD6oFU9vaZe6cYks,14387
16
- mergeron/gen/upp_tests.py,sha256=gRJISQ2jGmIDmFOvaTIkvYooI4mK-QbgkfgL46RrRio,7445
17
- mergeron/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
18
- mergeron-2025.739341.10.dist-info/METADATA,sha256=4Bxc0ThL53E7MhJtYfWnIj27yMN_YWOmhFo5Te5Z0Xs,3931
19
- mergeron-2025.739341.10.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
20
- mergeron-2025.739341.10.dist-info/RECORD,,