mergeron 2025.739341.9__py3-none-any.whl → 2025.739355.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mergeron might be problematic. Click here for more details.
- mergeron/__init__.py +1 -1
- mergeron/core/__init__.py +1 -1
- mergeron/core/empirical_margin_distribution.py +24 -21
- mergeron/core/guidelines_boundaries.py +5 -5
- mergeron/core/guidelines_boundary_functions.py +59 -75
- mergeron/core/guidelines_boundary_functions_extra.py +13 -15
- mergeron/gen/__init__.py +10 -2
- mergeron/gen/data_generation.py +23 -20
- {mergeron-2025.739341.9.dist-info → mergeron-2025.739355.0.dist-info}/METADATA +18 -19
- mergeron-2025.739355.0.dist-info/RECORD +20 -0
- mergeron-2025.739341.9.dist-info/RECORD +0 -20
- {mergeron-2025.739341.9.dist-info → mergeron-2025.739355.0.dist-info}/WHEEL +0 -0
mergeron/__init__.py
CHANGED
mergeron/core/__init__.py
CHANGED
|
@@ -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[
|
|
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)
|
|
174
|
-
for
|
|
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
|
-
|
|
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,45 +233,46 @@ 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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
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
|
|
253
|
+
dict(zip(row_keys, xl_row[1:], strict=True)) # type: ignore
|
|
253
254
|
)
|
|
254
255
|
})
|
|
255
256
|
|
|
256
|
-
|
|
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(
|
|
262
|
+
this_yaml.dump(margin_data_map, _yfh)
|
|
262
263
|
|
|
263
|
-
return
|
|
264
|
+
return margin_data_map
|
|
264
265
|
|
|
265
266
|
|
|
266
|
-
def margin_data_downloader() ->
|
|
267
|
+
def margin_data_downloader() -> None:
|
|
267
268
|
"""Download Prof.Damodaran's margin data."""
|
|
268
269
|
_u3pm = urllib3.PoolManager(ca_certs=certifi.where())
|
|
269
270
|
_data_source_url = "https://pages.stern.nyu.edu/~adamodar/pc/datasets/"
|
|
270
271
|
_archive_source_url = "https://pages.stern.nyu.edu/~adamodar/pc/archives/"
|
|
271
272
|
|
|
272
273
|
dest_dir = WORK_DIR / "damodaran_margin_data_archive"
|
|
274
|
+
if not dest_dir.is_dir():
|
|
275
|
+
dest_dir.mkdir()
|
|
273
276
|
|
|
274
277
|
# Get current-year margin data
|
|
275
278
|
workbook_name = "margin.xls"
|
|
@@ -334,18 +334,18 @@ class DiversionRatioBoundary:
|
|
|
334
334
|
|
|
335
335
|
match self.agg_method:
|
|
336
336
|
case UPPAggrSelector.DIS:
|
|
337
|
-
upp_agg_fn = gbfn.
|
|
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.
|
|
340
|
+
upp_agg_fn = gbfn.diversion_share_boundary_xact_avg # type: ignore
|
|
341
341
|
case UPPAggrSelector.MAX:
|
|
342
|
-
upp_agg_fn = gbfn.
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
409
|
+
def diversion_share_boundary_xact_avg(
|
|
410
410
|
_delta_star: float = 0.075,
|
|
411
411
|
_r_val: float = DEFAULT_REC_RATIO,
|
|
412
412
|
/,
|
|
@@ -559,7 +559,7 @@ def shrratio_boundary_xact_avg(
|
|
|
559
559
|
return GuidelinesBoundary(bdry, round(float(bdry_area_simpson), dps))
|
|
560
560
|
|
|
561
561
|
|
|
562
|
-
def
|
|
562
|
+
def diversion_share_boundary_min(
|
|
563
563
|
_delta_star: float = 0.075,
|
|
564
564
|
_r_val: float = DEFAULT_REC_RATIO,
|
|
565
565
|
/,
|
|
@@ -615,7 +615,7 @@ def shrratio_boundary_min(
|
|
|
615
615
|
)
|
|
616
616
|
|
|
617
617
|
|
|
618
|
-
def
|
|
618
|
+
def diversion_share_boundary_max(
|
|
619
619
|
_delta_star: float = 0.075, _: float = DEFAULT_REC_RATIO, /, *, dps: int = 10
|
|
620
620
|
) -> GuidelinesBoundary:
|
|
621
621
|
R"""
|
|
@@ -647,7 +647,7 @@ def shrratio_boundary_max(
|
|
|
647
647
|
)
|
|
648
648
|
|
|
649
649
|
|
|
650
|
-
def
|
|
650
|
+
def _diversion_share_boundary_intcpt(
|
|
651
651
|
s_2_pre: float,
|
|
652
652
|
_delta_star: MPFloat,
|
|
653
653
|
_r_val: MPFloat,
|
|
@@ -783,7 +783,7 @@ def boundary_plot(
|
|
|
783
783
|
mktshare_plot_flag: bool = True,
|
|
784
784
|
mktshare_axes_flag: bool = True,
|
|
785
785
|
backend: Literal["pgf"] | str | None = "pgf",
|
|
786
|
-
) -> tuple[mpl.
|
|
786
|
+
) -> tuple[mpl.figure.Figure, Callable[..., None]]:
|
|
787
787
|
"""Set up basic figure and axes for plots of safe harbor boundaries.
|
|
788
788
|
|
|
789
789
|
See, https://matplotlib.org/stable/tutorials/text/pgf.html
|
|
@@ -807,9 +807,6 @@ def boundary_plot(
|
|
|
807
807
|
R' "luaotfload.patch_font", embedfull, "embedfull"'
|
|
808
808
|
R")",
|
|
809
809
|
R"\end{luacode}",
|
|
810
|
-
R"\usepackage{mathtools}",
|
|
811
|
-
R"\usepackage{unicode-math}",
|
|
812
|
-
R"\setmathfont[math-style=ISO]{STIX Two Math}",
|
|
813
810
|
R"\setmainfont{STIX Two Text}",
|
|
814
811
|
r"\setsansfont{Fira Sans Light}",
|
|
815
812
|
R"\setmonofont[Scale=MatchLowercase,]{Fira Mono}",
|
|
@@ -822,6 +819,9 @@ def boundary_plot(
|
|
|
822
819
|
R" Numbers={Monospaced, Lining},",
|
|
823
820
|
R" LetterSpace=0.50,",
|
|
824
821
|
R" }",
|
|
822
|
+
R"\usepackage{mathtools}",
|
|
823
|
+
R"\usepackage{unicode-math}",
|
|
824
|
+
R"\setmathfont[math-style=ISO]{STIX Two Math}",
|
|
825
825
|
R"\usepackage[",
|
|
826
826
|
R" activate={true, nocompatibility},",
|
|
827
827
|
R" tracking=true,",
|
|
@@ -831,57 +831,53 @@ def boundary_plot(
|
|
|
831
831
|
|
|
832
832
|
# Initialize a canvas with a single figure (set of axes)
|
|
833
833
|
fig_ = plt.figure(figsize=(5, 5), dpi=600)
|
|
834
|
-
|
|
834
|
+
ax_ = fig_.add_subplot()
|
|
835
|
+
# Set the width of axis grid lines, and tick marks:
|
|
836
|
+
# both axes, both major and minor ticks
|
|
837
|
+
# Frame, grid, and face color
|
|
838
|
+
for _spos0 in "left", "bottom":
|
|
839
|
+
ax_.spines[_spos0].set_linewidth(0.5)
|
|
840
|
+
ax_.spines[_spos0].set_zorder(5)
|
|
841
|
+
for _spos1 in "top", "right":
|
|
842
|
+
ax_.spines[_spos1].set_linewidth(0.0)
|
|
843
|
+
ax_.spines[_spos1].set_zorder(0)
|
|
844
|
+
ax_.spines[_spos1].set_visible(False)
|
|
845
|
+
ax_.set_facecolor("#E6E6E6")
|
|
846
|
+
|
|
847
|
+
ax_.grid(linewidth=0.5, linestyle=":", color="grey", zorder=1)
|
|
848
|
+
ax_.tick_params(axis="both", which="both", width=0.5)
|
|
849
|
+
|
|
850
|
+
# Tick marks skip, size, and rotation
|
|
851
|
+
# x-axis
|
|
852
|
+
for _t in ax_.get_xticklabels():
|
|
853
|
+
_t.update({"fontsize": 6, "rotation": 45, "ha": "right"})
|
|
854
|
+
# y-axis
|
|
855
|
+
for _t in ax_.get_yticklabels():
|
|
856
|
+
_t.update({"fontsize": 6, "rotation": 0, "ha": "right"})
|
|
835
857
|
|
|
836
858
|
def _set_axis_def(
|
|
837
|
-
|
|
859
|
+
ax0_: mpa.Axes,
|
|
838
860
|
/,
|
|
839
861
|
*,
|
|
840
862
|
mktshare_plot_flag: bool = False,
|
|
841
863
|
mktshare_axes_flag: bool = False,
|
|
842
|
-
) ->
|
|
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
|
-
|
|
864
|
+
) -> None:
|
|
872
865
|
if mktshare_plot_flag:
|
|
866
|
+
# Axis scale
|
|
867
|
+
ax0_.set_xlim(0, 1)
|
|
868
|
+
ax0_.set_ylim(0, 1)
|
|
869
|
+
ax0_.set_aspect(1.0)
|
|
870
|
+
|
|
873
871
|
# Plot the ray of symmetry
|
|
874
|
-
|
|
872
|
+
ax0_.plot(
|
|
875
873
|
[0, 1], [0, 1], linewidth=0.5, linestyle=":", color="grey", zorder=1
|
|
876
874
|
)
|
|
877
875
|
|
|
878
|
-
#
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
# Truncate the axis frame to a triangle:
|
|
884
|
-
ax1_.add_patch(
|
|
876
|
+
# Truncate the axis frame to a triangle bounded by the other diagonal:
|
|
877
|
+
ax0_.plot(
|
|
878
|
+
[0, 1], [1, 0], linestyle="-", linewidth=0.5, color="black", zorder=1
|
|
879
|
+
)
|
|
880
|
+
ax0_.add_patch(
|
|
885
881
|
mpp.Rectangle(
|
|
886
882
|
xy=(1.0025, 0.00),
|
|
887
883
|
width=1.1 * mp.sqrt(2),
|
|
@@ -894,48 +890,36 @@ def boundary_plot(
|
|
|
894
890
|
zorder=5,
|
|
895
891
|
)
|
|
896
892
|
)
|
|
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
893
|
|
|
902
894
|
# Axis Tick-mark locations
|
|
903
895
|
# One can supply an argument to mpt.AutoMinorLocator to
|
|
904
896
|
# specify a fixed number of minor intervals per major interval, e.g.:
|
|
905
897
|
# minorLocator = mpt.AutoMinorLocator(2)
|
|
906
898
|
# would lead to a single minor tick between major ticks.
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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)
|
|
899
|
+
for axs_ in ax0_.xaxis, ax0_.yaxis:
|
|
900
|
+
axs_.set_major_locator(mpt.MultipleLocator(0.05))
|
|
901
|
+
axs_.set_minor_locator(mpt.AutoMinorLocator(5))
|
|
917
902
|
# It"s always x when specifying the format
|
|
918
903
|
axs_.set_major_formatter(mpt.StrMethodFormatter("{x:>3.0%}"))
|
|
919
904
|
|
|
920
905
|
# Hide every other tick-label
|
|
921
|
-
for axl_ in
|
|
922
|
-
|
|
906
|
+
for axl_ in ax0_.get_xticklabels(), ax0_.get_yticklabels():
|
|
907
|
+
for _t in axl_[::2]:
|
|
908
|
+
_t.set_visible(False)
|
|
923
909
|
|
|
924
910
|
# Axis labels
|
|
925
911
|
if mktshare_axes_flag:
|
|
926
912
|
# x-axis
|
|
927
|
-
|
|
928
|
-
|
|
913
|
+
ax0_.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
|
|
914
|
+
ax0_.xaxis.set_label_coords(0.75, -0.1)
|
|
929
915
|
# y-axis
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
return ax1_
|
|
916
|
+
ax0_.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
|
|
917
|
+
ax0_.yaxis.set_label_coords(-0.1, 0.75)
|
|
934
918
|
|
|
935
|
-
|
|
936
|
-
|
|
919
|
+
_set_axis_def(
|
|
920
|
+
ax_,
|
|
937
921
|
mktshare_plot_flag=mktshare_plot_flag,
|
|
938
922
|
mktshare_axes_flag=mktshare_axes_flag,
|
|
939
923
|
)
|
|
940
924
|
|
|
941
|
-
return
|
|
925
|
+
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
|
|
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
|
|
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
|
-
) ->
|
|
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
|
-
`
|
|
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 `
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
) ->
|
|
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
|
|
516
|
+
return GuidelinesBoundary(bdry, float(mp.nstr(bdry_area_simpson, dps)))
|
|
519
517
|
|
|
520
518
|
|
|
521
|
-
#
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
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})
|
mergeron/gen/data_generation.py
CHANGED
|
@@ -445,20 +445,22 @@ class MarketSample:
|
|
|
445
445
|
this_yaml.dump(self, _yfh)
|
|
446
446
|
|
|
447
447
|
if save_dataset:
|
|
448
|
-
if
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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(
|
|
471
|
+
market_sample_: MarketSample = this_yaml.load(
|
|
472
|
+
(zpath / f"{name_root}.yaml").read_text()
|
|
473
|
+
)
|
|
470
474
|
|
|
471
475
|
if restore_dataset:
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
482
|
-
|
|
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_, "
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mergeron
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.739355.0
|
|
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
|
|
22
|
-
Requires-Dist: attrs (>=
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist: certifi (>=
|
|
25
|
-
Requires-Dist: h5py (>=3.13.0
|
|
26
|
-
Requires-Dist: jinja2 (>=3.1)
|
|
27
|
-
Requires-Dist: joblib (>=1.
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist:
|
|
32
|
-
Requires-Dist:
|
|
33
|
-
Requires-Dist:
|
|
34
|
-
Requires-Dist:
|
|
35
|
-
Requires-Dist:
|
|
36
|
-
Requires-Dist:
|
|
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,7 +86,7 @@ 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
92
|
Usage guide and API reference available `here <https://capeconomics.github.io/mergeron/>`_.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
mergeron/__init__.py,sha256=qjmFjh6yCKnTzrgdmI7KamtTpqwtjZrAtJmc9piXGHo,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=ZTx4uaaR-3Qcc3Uh27S_shIu-4eyW8rLR2Lj13WvDX0,28513
|
|
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=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.739355.0.dist-info/METADATA,sha256=DwSbyhgSZPnJ6V89zcswJC98ojEDui7Y3zJkrrZc0v4,3865
|
|
19
|
+
mergeron-2025.739355.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
20
|
+
mergeron-2025.739355.0.dist-info/RECORD,,
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
mergeron/__init__.py,sha256=1fy_MtURkE0G64B516588FRg_-zlk10Z_SwEWNFEE3c,5773
|
|
2
|
-
mergeron/core/__init__.py,sha256=NChJfOkahbGPa1Ll1fO3agY5-IoDlEra3xNkXPnKIVM,3259
|
|
3
|
-
mergeron/core/empirical_margin_distribution.py,sha256=Gm3nLbovlqD7gk2lrBx_RWufj4MK5SkUQ_BUj-d9bhg,11484
|
|
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.9.dist-info/METADATA,sha256=mj88h8xd5Ni04z7kTP-rxpC0jCFNyK3Nqt_XzAM1Agw,3930
|
|
19
|
-
mergeron-2025.739341.9.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
20
|
-
mergeron-2025.739341.9.dist-info/RECORD,,
|
|
File without changes
|