mergeron 2025.739341.9__tar.gz → 2025.739355.0__tar.gz

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.

Files changed (20) hide show
  1. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/PKG-INFO +18 -19
  2. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/README.rst +1 -1
  3. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/pyproject.toml +42 -43
  4. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/__init__.py +1 -1
  5. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/core/__init__.py +1 -1
  6. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/core/empirical_margin_distribution.py +24 -21
  7. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/core/guidelines_boundaries.py +5 -5
  8. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/core/guidelines_boundary_functions.py +59 -75
  9. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/core/guidelines_boundary_functions_extra.py +13 -15
  10. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/gen/__init__.py +10 -2
  11. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/gen/data_generation.py +23 -20
  12. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/core/ftc_merger_investigations_data.py +0 -0
  13. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/core/pseudorandom_numbers.py +0 -0
  14. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/data/__init__.py +0 -0
  15. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/data/damodaran_margin_data_serialized.zip +0 -0
  16. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/data/ftc_merger_investigations_data.zip +0 -0
  17. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/gen/data_generation_functions.py +0 -0
  18. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/gen/enforcement_stats.py +0 -0
  19. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/gen/upp_tests.py +0 -0
  20. {mergeron-2025.739341.9 → mergeron-2025.739355.0}/src/mergeron/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mergeron
3
- Version: 2025.739341.9
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,<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,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/>`_.
@@ -46,7 +46,7 @@ To install the package, use the following shell command:
46
46
  pip install mergeron
47
47
 
48
48
 
49
- Documentation
49
+ Documentation
50
50
  -------------
51
51
 
52
52
  Usage guide and API reference available `here <https://capeconomics.github.io/mergeron/>`_.
@@ -4,6 +4,7 @@ authors = [{ name = "Murthy Kambhampaty", email = "smk@capeconomics.com" }]
4
4
  description = "Python for analyzing merger enforcement policy"
5
5
  readme = "README.rst"
6
6
  license = "MIT"
7
+ license-files = ["./docs/source/license.rst"]
7
8
  keywords = [
8
9
  "merger enforcement policy",
9
10
  "merger guidelines",
@@ -14,7 +15,7 @@ keywords = [
14
15
  "upward pricing pressure",
15
16
  "GUPPI",
16
17
  ]
17
- version = "2025.739341.9"
18
+ version = "2025.739355.0"
18
19
 
19
20
  # Classifiers list: https://pypi.org/classifiers/
20
21
  classifiers = [
@@ -33,57 +34,54 @@ classifiers = [
33
34
 
34
35
  requires-python = ">=3.12"
35
36
 
36
- dynamic = ["dependencies"]
37
-
38
- [project.urls]
39
- Documentation = "https://capeconomics.github.io/mergeron/"
40
- Repository = "https://github.com/capeconomics/mergeron.git"
37
+ dependencies = [
38
+ "aenum>=3.1.15",
39
+ "attrs>=25.3.0",
40
+ "beautifulsoup4>=4.13.3",
41
+ "certifi>=2025.1.31",
42
+ "h5py>=3.13.0",
43
+ "jinja2>=3.1.6",
44
+ "joblib>=1.4.2",
45
+ "lxml>=5.3.2",
46
+ "matplotlib>=3.10.1",
47
+ "mpmath>=1.3.0",
48
+ "python-calamine>=0.3.2",
49
+ "ruamel-yaml>=0.18.10",
50
+ "scipy>=1.15.2",
51
+ "sympy>=1.13.3",
52
+ "types-beautifulsoup4>=4.12.0",
53
+ "urllib3>=2.3.0",
54
+ ]
41
55
 
42
56
  [build-system]
43
57
  requires = ["poetry-core"]
44
58
  build-backend = "poetry.core.masonry.api"
45
59
 
46
60
 
47
- [tool.poetry.dependencies]
48
- # You may need to apply the fixes in, https://github.com/python-poetry/poetry/issues/3365
49
- # if poetry dependency resolution appears to hang (read the page at link to the end)
50
- aenum = "^3.1.15"
51
- attrs = ">=23.2"
52
- bs4 = ">=0.0.1"
53
- jinja2 = ">=3.1"
54
- joblib = ">=1.3"
55
- matplotlib = ">=3.8"
56
- mpmath = ">=1.3"
57
- python = "^3.12"
58
- scipy = ">=1.12"
59
- sympy = ">=1.12"
60
- certifi = ">=2023.11.17"
61
- types-beautifulsoup4 = ">=4.11.2"
62
- urllib3 = "^2.2.2"
63
- ruamel-yaml = "^0.18.10"
64
- h5py = "^3.13.0"
65
- linuxdoc = "^20240924"
66
- lxml = "^5.3.1"
67
- python-calamine = "^0.3.1"
68
-
61
+ [project.urls]
62
+ Documentation = "https://capeconomics.github.io/mergeron/"
63
+ Repository = "https://github.com/capeconomics/mergeron.git"
69
64
 
70
65
  [tool.poetry.group.dev.dependencies]
71
- jinja2 = ">=3.1.5"
72
- mypy = ">=1.8"
73
- ruff = ">=0.5"
74
- poetry-plugin-export = "^1.8.0"
75
- pytest = ">=8.0"
76
- sphinx = ">8.1"
77
- semver = ">=3.0"
78
- sphinx-autodoc-typehints = ">=2.0.0"
66
+ ipython = ">=9.1.0"
67
+ ipython-pygments-lexers = ">=1.1.1"
68
+ linuxdoc = ">=20240924"
69
+ mypy = ">=1.15.0"
70
+ pendulum = ">=3.0.0"
71
+ pipdeptree = ">=2.26.0"
72
+ coverage = ">=7.8.0"
73
+ pytest = ">=8.3.5"
74
+ pytest-cov = ">=6.1.1"
75
+ pytest-xdist = ">=3.6.1"
76
+ ruff = ">=0.11.4"
77
+ semver = ">=3.0.4"
78
+ twine = ">=6.1.0"
79
+ virtualenv = ">=20.30.0"
80
+ sphinx = ">=8.2.3"
79
81
  sphinx-autoapi = ">=3.6.0"
80
- sphinx-immaterial = ">0.11"
81
- pipdeptree = ">=2.15.1"
82
- virtualenv = ">=20.28.0"
83
- pytest-cov = "^6.0.0"
84
- pendulum = "^3.0.0"
85
- rstcheck = "^6.2.4"
86
- ipython-pygments-lexers = "^1.1.1"
82
+ sphinx-autodoc-typehints = ">=3.1.0"
83
+ sphinx-immaterial = ">=0.13.5"
84
+ pkginfo = ">=1.12.1.2"
87
85
 
88
86
 
89
87
  [tool.ruff]
@@ -186,6 +184,7 @@ preview = true
186
184
 
187
185
  [tool.mypy]
188
186
  python_version = "3.12"
187
+ cache_fine_grained = true
189
188
  ignore_missing_imports = false
190
189
  strict = true
191
190
  enable_incomplete_feature = ["PreciseTupleTypes"]
@@ -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.9"
18
+ VERSION = "2025.739355.0"
19
19
 
20
20
  __version__ = VERSION
21
21
 
@@ -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,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
- 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
- def margin_data_downloader() -> DamodaranMarginData:
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.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
  /,
@@ -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 shrratio_boundary_min(
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 shrratio_boundary_max(
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 _shrratio_boundary_intcpt(
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.pyplot, mpl.pyplot.Figure, mpl.axes.Axes, Callable[..., mpl.axes.Axes]]:
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
- ax_out = fig_.add_subplot()
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
- ax1_: mpa.Axes,
859
+ ax0_: mpa.Axes,
838
860
  /,
839
861
  *,
840
862
  mktshare_plot_flag: bool = False,
841
863
  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
-
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
- ax1_.plot(
872
+ ax0_.plot(
875
873
  [0, 1], [0, 1], linewidth=0.5, linestyle=":", color="grey", zorder=1
876
874
  )
877
875
 
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(
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
- 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)
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 ax1_.get_xticklabels(), ax1_.get_yticklabels():
922
- plt.setp(axl_[::2], visible=False)
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
- ax1_.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
928
- ax1_.xaxis.set_label_coords(0.75, -0.1)
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
- 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_
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
- ax_out = _set_axis_def(
936
- ax_out,
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 plt, fig_, ax_out, _set_axis_def
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 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,
@@ -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