mergeron 2025.739290.5__py3-none-any.whl → 2025.739290.7__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
@@ -12,18 +12,20 @@ from ruamel import yaml
12
12
 
13
13
  _PKG_NAME: str = Path(__file__).parent.stem
14
14
 
15
- VERSION = "2025.739290.5"
15
+ VERSION = "2025.739290.7"
16
16
 
17
17
  __version__ = VERSION
18
18
 
19
- DATA_DIR: Path = Path.home() / _PKG_NAME
19
+ WORK_DIR = globals().get("WORK_DIR", Path.home() / _PKG_NAME)
20
20
  """
21
- Defines a subdirectory named for this package in the user's home path.
21
+ If defined, the global variable WORK_DIR is used as a data store.
22
22
 
23
- If the subdirectory doesn't exist, it is created on package invocation.
23
+ If the user does not define WORK_DIR, a subdirectory in
24
+ the user's home directory, named for this package, is
25
+ created/reused.
24
26
  """
25
- if not DATA_DIR.is_dir():
26
- DATA_DIR.mkdir(parents=False)
27
+ if not WORK_DIR.is_dir():
28
+ WORK_DIR.mkdir(parents=False)
27
29
 
28
30
  DEFAULT_REC_RATIO = 0.85
29
31
 
@@ -36,14 +38,14 @@ PKG_ATTRS_MAP: dict[str, object] = {}
36
38
 
37
39
  np.set_printoptions(precision=24, floatmode="fixed")
38
40
 
39
- type HMGPubYear = Literal[1982, 1984, 1992, 2010, 2023]
41
+ type HMGPubYear = Literal[1992, 2010, 2023]
40
42
 
41
43
  type ArrayBoolean = NDArray[np.bool_]
42
44
  type ArrayFloat = NDArray[np.floating]
43
- type ArrayINT = NDArray[np.unsignedinteger]
45
+ type ArrayINT = NDArray[np.integer]
44
46
 
45
47
  type ArrayDouble = NDArray[np.float64]
46
- type ArrayBIGINT = NDArray[np.uint64]
48
+ type ArrayBIGINT = NDArray[np.int64]
47
49
 
48
50
 
49
51
  this_yaml = yaml.YAML(typ="rt")
@@ -131,35 +133,47 @@ class Enameled(enum.Enum):
131
133
  @this_yaml.register_class
132
134
  @enum.unique
133
135
  class RECForm(str, Enameled):
134
- """For derivation of recapture ratio from market shares."""
136
+ R"""For derivation of recapture ratio from market shares.
137
+
138
+ With :math:`\mathscr{N}` a set of firms, each supplying a
139
+ single differentiated product, and :math:`\mathscr{M} \subset \mathscr{N}`
140
+ a putative relevant product market, with
141
+ :math:`d_{ij}` denoting diversion ratio from good :math:`i` to good :math:`j`,
142
+ :math:`s_i` denoting market shares, and
143
+ :math:`\overline{r}` the default market recapture ratio,
144
+ market recapture ratios for the respective products may be specified
145
+ as having one of the following forms:
146
+ """
135
147
 
136
- INOUT = "inside-out"
137
- R"""
138
- Given, :math:`\overline{r}, s_i {\ } \forall {\ } i \in \set{1, 2, \ldots, m}`, with
139
- :math:`s_{min} = \min(s_1, s_2)`,
148
+ FIXED = "proportional"
149
+ R"""Given, :math:`\overline{r}`,
140
150
 
141
151
  .. math::
142
152
 
143
- REC_i = \frac{(1 - s_i) \overline{r}}{(1 - s_{min}) - (s_i - s_{min}) \overline{r}}
153
+ REC_i = \overline{r} {\ } \forall {\ } i \in \mathscr{M}
144
154
 
145
155
  """
146
156
 
147
- OUTIN = "outside-in"
157
+ INOUT = "inside-out"
148
158
  R"""
149
- Given, :math:`\pi_i {\ } \forall {\ } i \in N`,
159
+ Given, :math:`\overline{r}, s_i {\ } \forall {\ } i \in \mathscr{M}`, with
160
+ :math:`s_{min} = \min(s_1, s_2)`,
150
161
 
151
162
  .. math::
152
163
 
153
- REC_i = \frac{\sum_{i \in M} \pi_i}{\sum_{j \in N} \pi_j}
164
+ REC_i = \frac{\overline{r} (1 - s_i)}{1 - (1 - \overline{r}) s_{min} - \overline{r} s_i}
165
+ {\ } \forall {\ } i \in \mathscr{M}
154
166
 
155
167
  """
156
168
 
157
- FIXED = "proportional"
158
- R"""Given, :math:`\overline{r}`,
169
+ OUTIN = "outside-in"
170
+ R"""
171
+ Given, :math:`d_{ij} {\ } \forall {\ } i, j \in \mathscr{M}, i \neq j`,
159
172
 
160
173
  .. math::
161
174
 
162
- REC_i = \overline{r} {\ } \forall {\ } i \in M
175
+ REC_i = {\sum_{j \in \mathscr{M}}^{j \neq i} d_{ij}}
176
+ {\ } \forall {\ } i \in \mathscr{M}
163
177
 
164
178
  """
165
179
 
mergeron/core/__init__.py CHANGED
@@ -9,7 +9,14 @@ import mpmath # type: ignore
9
9
  import numpy as np
10
10
  from attrs import cmp_using, field, frozen
11
11
 
12
- from .. import VERSION, ArrayBIGINT, this_yaml, yaml_rt_mapper # noqa: TID252
12
+ from .. import ( # noqa: TID252
13
+ VERSION,
14
+ ArrayBIGINT,
15
+ ArrayDouble,
16
+ this_yaml,
17
+ yamelize_attrs,
18
+ yaml_rt_mapper,
19
+ )
13
20
 
14
21
  __version__ = VERSION
15
22
 
@@ -17,6 +24,17 @@ type MPFloat = mpmath.ctx_mp_python.mpf
17
24
  type MPMatrix = mpmath.matrix # type: ignore
18
25
 
19
26
 
27
+ @frozen
28
+ class GuidelinesBoundary:
29
+ """Output of a Guidelines boundary function."""
30
+
31
+ coordinates: ArrayDouble
32
+ """Market-share pairs as Cartesian coordinates of points on the boundary."""
33
+
34
+ area: float
35
+ """Area under the boundary."""
36
+
37
+
20
38
  @frozen
21
39
  class INVTableData:
22
40
  industry_group: str
@@ -29,6 +47,7 @@ type INVData = MappingProxyType[
29
47
  ]
30
48
  type INVData_in = Mapping[str, Mapping[str, Mapping[str, INVTableData]]]
31
49
 
50
+ yamelize_attrs(INVTableData)
32
51
 
33
52
  (_, _) = (
34
53
  this_yaml.representer.add_representer(
@@ -59,16 +78,26 @@ type INVData_in = Mapping[str, Mapping[str, Mapping[str, INVTableData]]]
59
78
  ),
60
79
  )
61
80
 
81
+ _, _ = (
82
+ this_yaml.representer.add_representer(
83
+ MappingProxyType,
84
+ lambda _r, _d: _r.represent_mapping("!mappingproxy", dict(_d.items())),
85
+ ),
86
+ this_yaml.constructor.add_constructor(
87
+ "!mappingproxy", lambda _c, _n: MappingProxyType(dict(**yaml_rt_mapper(_c, _n)))
88
+ ),
89
+ )
90
+
62
91
 
63
92
  def _dict_from_mapping(_p: Mapping[Any, Any], /) -> dict[Any, Any]:
64
- retval = {}
93
+ retval: dict[Any, Any] = {}
65
94
  for _k, _v in _p.items(): # for subit in it:
66
95
  retval |= {_k: _dict_from_mapping(_v)} if isinstance(_v, Mapping) else {_k: _v}
67
96
  return retval
68
97
 
69
98
 
70
99
  def _mappingproxy_from_mapping(_p: Mapping[Any, Any], /) -> MappingProxyType[Any, Any]:
71
- retval = {}
100
+ retval: dict[Any, Any] = {}
72
101
  for _k, _v in _p.items(): # for subit in it:
73
102
  retval |= (
74
103
  {_k: _mappingproxy_from_mapping(_v)}
@@ -76,34 +105,3 @@ def _mappingproxy_from_mapping(_p: Mapping[Any, Any], /) -> MappingProxyType[Any
76
105
  else {_k: _v}
77
106
  )
78
107
  return MappingProxyType(retval)
79
-
80
-
81
- _, _ = (
82
- this_yaml.representer.add_representer(
83
- MappingProxyType,
84
- lambda _r, _d: _r.represent_mapping("!mappingproxy", dict(_d.items())),
85
- ),
86
- this_yaml.constructor.add_constructor(
87
- "!mappingproxy", lambda _c, _n: MappingProxyType(yaml_rt_mapper(_c, _n))
88
- ),
89
- )
90
-
91
-
92
- for _typ in (INVTableData,):
93
- _, _ = (
94
- this_yaml.representer.add_representer(
95
- _typ,
96
- lambda _r, _d: _r.represent_mapping(
97
- f"!{_d.__class__.__name__}",
98
- {
99
- _a.name: getattr(_d, _a.name)
100
- for _a in _d.__attrs_attrs__
101
- if _a.name not in {"coordinates", "area"}
102
- },
103
- ),
104
- ),
105
- this_yaml.constructor.add_constructor(
106
- f"!{_typ.__name__}",
107
- lambda _c, _n: globals()[_n.tag.lstrip("!")](**yaml_rt_mapper(_c, _n)),
108
- ),
109
- )
@@ -39,7 +39,6 @@ price-cost margins fall in the interval :math:`[0, 1]`.
39
39
  import shutil
40
40
  import zipfile
41
41
  from collections.abc import Mapping
42
- from importlib import resources
43
42
  from pathlib import Path
44
43
  from types import MappingProxyType
45
44
 
@@ -49,12 +48,17 @@ from numpy.random import PCG64DXSM, Generator, SeedSequence
49
48
  from scipy import stats # type: ignore
50
49
  from xlrd import open_workbook # type: ignore
51
50
 
52
- from .. import _PKG_NAME, DATA_DIR, VERSION, ArrayDouble, this_yaml # noqa: TID252
51
+ from .. import VERSION, ArrayDouble, this_yaml # noqa: TID252
52
+ from .. import WORK_DIR as PKG_WORK_DIR # noqa: TID252
53
+ from .. import data as mdat # noqa: TID252
53
54
  from . import _mappingproxy_from_mapping
54
55
 
55
56
  __version__ = VERSION
56
57
 
57
- MGNDATA_ARCHIVE_PATH = DATA_DIR / "damodaran_margin_data_serialized.zip"
58
+ WORK_DIR = globals().get("WORK_DIR", PKG_WORK_DIR)
59
+ """Redefined, in case the user defines WORK_DIR betweeen module imports."""
60
+
61
+ MGNDATA_ARCHIVE_PATH = WORK_DIR / "damodaran_margin_data_serialized.zip"
58
62
 
59
63
 
60
64
  u3pm = urllib3.PoolManager()
@@ -74,14 +78,14 @@ def margin_data_getter( # noqa: PLR0912
74
78
  data_archive_path = data_archive_path or MGNDATA_ARCHIVE_PATH
75
79
  workbook_path = data_archive_path.parent / f"damodaran_{_table_name}_data.xls"
76
80
  if data_archive_path.is_file() and not data_download_flag:
77
- # with data_archive_path_.open("r") as _yfh:
78
- # margin_data_dict: dict[str, dict[str, float | int]] = this_yaml.load(_yfh)
79
81
  with (
80
82
  zipfile.ZipFile(data_archive_path) as _yzip,
81
83
  _yzip.open(f"{data_archive_path.stem}.yaml") as _yfh,
82
84
  ):
83
- margin_data_dict: dict[str, dict[str, float | int]] = this_yaml.load(_yfh)
84
- return _mappingproxy_from_mapping(margin_data_dict)
85
+ margin_data_dict: MappingProxyType[
86
+ str, MappingProxyType[str, float | int]
87
+ ] = this_yaml.load(_yfh)
88
+ return margin_data_dict
85
89
  elif workbook_path.is_file():
86
90
  workbook_path.unlink()
87
91
  if data_archive_path.is_file():
@@ -116,19 +120,14 @@ def margin_data_getter( # noqa: PLR0912
116
120
  "Using bundled copy."
117
121
  )
118
122
  if not workbook_path.is_file():
119
- with resources.as_file(
120
- resources.files(f"{_PKG_NAME}.data").joinpath(
121
- "empirical_margin_distribution.xls"
122
- )
123
- ) as margin_data_archive_path:
124
- shutil.copy2(margin_data_archive_path, workbook_path)
123
+ shutil.copy2(mdat.DAMODARAN_MARGIN_WORKBOOK, workbook_path)
125
124
  else:
126
125
  raise error_
127
126
 
128
127
  xl_book = open_workbook(workbook_path, ragged_rows=True, on_demand=True)
129
128
  xl_sheet = xl_book.sheet_by_name("Industry Averages")
130
129
 
131
- margin_dict: dict[str, dict[str, float | int]] = {}
130
+ margin_dict_in: dict[str, dict[str, float | int]] = {}
132
131
  row_keys: list[str] = []
133
132
  read_row_flag = False
134
133
  for _ridx in range(xl_sheet.nrows):
@@ -142,15 +141,16 @@ def margin_data_getter( # noqa: PLR0912
142
141
  continue
143
142
 
144
143
  xl_row[1] = int(xl_row[1])
145
- margin_dict[xl_row[0]] = dict(zip(row_keys[1:], xl_row[1:], strict=True))
144
+ margin_dict_in[xl_row[0]] = dict(zip(row_keys[1:], xl_row[1:], strict=True))
146
145
 
146
+ margin_dict = _mappingproxy_from_mapping(margin_dict_in)
147
147
  with (
148
148
  zipfile.ZipFile(data_archive_path, "w") as _yzip,
149
149
  _yzip.open(f"{data_archive_path.stem}.yaml", "w") as _yfh,
150
150
  ):
151
151
  this_yaml.dump(margin_dict, _yfh)
152
152
 
153
- return _mappingproxy_from_mapping(margin_dict)
153
+ return margin_dict
154
154
 
155
155
 
156
156
  def margin_data_builder(
@@ -12,8 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  import re
14
14
  import shutil
15
- from collections.abc import Sequence
16
- from importlib import resources
15
+ from collections.abc import Mapping, Sequence
17
16
  from operator import itemgetter
18
17
  from pathlib import Path
19
18
  from types import MappingProxyType
@@ -26,14 +25,9 @@ import urllib3
26
25
  from bs4 import BeautifulSoup
27
26
  from numpy.testing import assert_array_equal
28
27
 
29
- from .. import ( # noqa: TID252
30
- _PKG_NAME,
31
- DATA_DIR,
32
- EMPTY_ARRAYINT,
33
- VERSION,
34
- ArrayBIGINT,
35
- this_yaml,
36
- )
28
+ from .. import EMPTY_ARRAYINT, VERSION, ArrayBIGINT, this_yaml # noqa: TID252
29
+ from .. import WORK_DIR as PKG_WORK_DIR # noqa: TID252
30
+ from .. import data as mdat # noqa: TID252
37
31
  from . import (
38
32
  INVData,
39
33
  INVData_in,
@@ -46,21 +40,16 @@ __version__ = VERSION
46
40
 
47
41
  m.patch()
48
42
 
49
- FTCDATA_DIR = DATA_DIR / "FTCData"
50
- if not FTCDATA_DIR.is_dir():
51
- FTCDATA_DIR.mkdir(parents=True)
43
+ WORK_DIR = globals().get("WORK_DIR", PKG_WORK_DIR)
44
+ """Redefined, in case the user defines WORK_DIR betweeen module imports."""
52
45
 
53
- INVDATA_ARCHIVE_PATH = DATA_DIR / "ftc_invdata.zip"
54
- if (
55
- not INVDATA_ARCHIVE_PATH.is_file()
56
- and (
57
- _bundled_copy := resources.files(f"{_PKG_NAME}.data").joinpath(
58
- INVDATA_ARCHIVE_PATH.name
59
- )
60
- ).is_file()
61
- ):
62
- with resources.as_file(_bundled_copy) as _bundled_copy_path:
63
- shutil.copy2(_bundled_copy_path, INVDATA_ARCHIVE_PATH)
46
+ FID_WORK_DIR = WORK_DIR / "FTCData"
47
+ if not FID_WORK_DIR.is_dir():
48
+ FID_WORK_DIR.mkdir(parents=True)
49
+
50
+ INVDATA_ARCHIVE_PATH = WORK_DIR / mdat.FTC_MERGER_INVESTIGATIONS_DATA.name
51
+ if not INVDATA_ARCHIVE_PATH.is_file():
52
+ shutil.copy2(mdat.FTC_MERGER_INVESTIGATIONS_DATA, INVDATA_ARCHIVE_PATH)
64
53
 
65
54
  TABLE_NO_RE = re.compile(r"Table \d+\.\d+")
66
55
  TABLE_TYPES = ("ByHHIandDelta", "ByFirmCount")
@@ -105,6 +94,10 @@ CNT_FCOUNT_DICT = {
105
94
  }
106
95
 
107
96
 
97
+ def reverse_map(_dict: Mapping[Any, Any]) -> Mapping[Any, Any]:
98
+ return {_v: _k for _k, _v in _dict.items()}
99
+
100
+
108
101
  def construct_data(
109
102
  _archive_path: Path = INVDATA_ARCHIVE_PATH,
110
103
  *,
@@ -442,12 +435,12 @@ def _parse_invdata() -> INVData:
442
435
  # )
443
436
  import pymupdf # type: ignore # noqa: PLC0415
444
437
 
445
- invdata_docnames = _download_invdata(FTCDATA_DIR)
438
+ invdata_docnames = _download_invdata(FID_WORK_DIR)
446
439
 
447
440
  invdata: INVData_in = {}
448
441
 
449
442
  for invdata_docname in invdata_docnames:
450
- invdata_pdf_path = FTCDATA_DIR.joinpath(invdata_docname)
443
+ invdata_pdf_path = FID_WORK_DIR.joinpath(invdata_docname)
451
444
 
452
445
  invdata_doc = pymupdf.open(invdata_pdf_path)
453
446
  invdata_meta = invdata_doc.metadata
@@ -709,7 +702,7 @@ def _process_table_blks_cnt_type(
709
702
  return invdata_array[np.argsort(invdata_array[:, 0])]
710
703
 
711
704
 
712
- def _download_invdata(_dl_path: Path = FTCDATA_DIR) -> tuple[str, ...]:
705
+ def _download_invdata(_dl_path: Path = FID_WORK_DIR) -> tuple[str, ...]:
713
706
  if not _dl_path.is_dir():
714
707
  _dl_path.mkdir(parents=True)
715
708
 
@@ -51,14 +51,12 @@ class GuidelinesThresholds:
51
51
 
52
52
  ΔHHI, Recapture Ratio, GUPPI, Diversion ratio, CMCR, and IPR thresholds
53
53
  constructed from concentration standards in Guidelines published in
54
- 1982, 1984, 1992, 2010, and 2023.
54
+ 1992, 2010, and 2023.
55
55
 
56
56
  """
57
57
 
58
58
  pub_year: HMGPubYear = field(
59
- kw_only=False,
60
- default=2023,
61
- validator=validators.in_([1982, 1984, 1992, 2010, 2023]),
59
+ kw_only=False, default=2023, validator=validators.in_([1992, 2010, 2023])
62
60
  )
63
61
  """
64
62
  Year of publication of the Guidelines
@@ -97,9 +95,7 @@ class GuidelinesThresholds:
97
95
  # thus, here, the tentative delta safeharbor under
98
96
  # the 2023 Guidelines is 100 points
99
97
  hhi_p, dh_s, dh_p = {
100
- 1982: (_s1982 := (0.18, 0.005, 0.01)),
101
- 1984: _s1982,
102
- 1992: _s1982,
98
+ 1992: (0.18, 0.005, 0.01),
103
99
  2010: (0.25, 0.01, 0.02),
104
100
  2023: (0.18, 0.01, 0.01),
105
101
  }[self.pub_year]
@@ -150,6 +146,15 @@ class GuidelinesThresholds:
150
146
  class ConcentrationBoundary:
151
147
  """Concentration parameters, boundary coordinates, and area under concentration boundary."""
152
148
 
149
+ threshold: float = field(kw_only=False, default=0.01)
150
+
151
+ @threshold.validator
152
+ def _tv(
153
+ _instance: ConcentrationBoundary, _attribute: Attribute[float], _value: float, /
154
+ ) -> None:
155
+ if not 0 <= _value <= 1:
156
+ raise ValueError("Concentration threshold must lie between 0 and 1.")
157
+
153
158
  measure_name: Literal[
154
159
  "ΔHHI",
155
160
  "Combined share",
@@ -169,17 +174,8 @@ class ConcentrationBoundary:
169
174
  }:
170
175
  raise ValueError(f"Invalid name for a concentration measure, {_value!r}.")
171
176
 
172
- threshold: float = field(kw_only=False, default=0.01)
173
-
174
- @threshold.validator
175
- def _tv(
176
- _instance: ConcentrationBoundary, _attribute: Attribute[float], _value: float, /
177
- ) -> None:
178
- if not 0 <= _value <= 1:
179
- raise ValueError("Concentration threshold must lie between 0 and 1.")
180
-
181
177
  precision: int = field(
182
- kw_only=False, default=5, validator=validators.instance_of(int)
178
+ kw_only=True, default=5, validator=validators.instance_of(int)
183
179
  )
184
180
 
185
181
  area: float = field(init=False, kw_only=True)
@@ -240,7 +236,7 @@ class DiversionRatioBoundary:
240
236
  )
241
237
 
242
238
  recapture_form: RECForm | None = field(kw_only=True, default=RECForm.INOUT)
243
- """
239
+ R"""
244
240
  The form of the recapture ratio.
245
241
 
246
242
  When :attr:`mergeron.RECForm.INOUT`, the recapture ratio for
@@ -251,12 +247,17 @@ class DiversionRatioBoundary:
251
247
  constructed from the generated purchase-probabilities for products in
252
248
  the market and for the outside good, specify :attr:`mergeron.RECForm.OUTIN`.)
253
249
 
254
- The GUPPI boundary is a continuum of diversion ratio boundaries conditional on
255
- price-cost margins, :math:`d_{ij} = g_i * p_i / (m_j * p_j)`,
256
- with :math:`d_{ij}` the diverion ratio from product :math:`i` to product :math:`j`;
250
+ The GUPPI boundary is a continuum of conditional diversion ratio boundaries,
251
+
252
+ .. math::
253
+
254
+ d_{ij} \vert_{p_i, p_j, m_j} \triangleq \frac{g_i p_i}{m_j p_j} = \overline{d}
255
+
256
+ with :math:`d_{ij}` the diversion ratio from product :math:`i` to product :math:`j`;
257
257
  :math:`g_i` the GUPPI for product :math:`i`;
258
- :math:`m_j` the margin for product :math:`j`; and
259
- :math:`p_i, p_j` the prices of goods :math:`i, j`, respectively.
258
+ :math:`m_j` the price-cost margin on product :math:`j`;
259
+ :math:`p_i, p_j` the prices of goods :math:`i, j`, respectively; and
260
+ :math:`\overline{d}` the diversion ratio threshold (i.e., bound).
260
261
 
261
262
  """
262
263
 
@@ -283,7 +284,7 @@ class DiversionRatioBoundary:
283
284
 
284
285
  agg_method: UPPAggrSelector = field(
285
286
  kw_only=True,
286
- default=UPPAggrSelector.MAX,
287
+ default=UPPAggrSelector.MIN,
287
288
  validator=validators.instance_of(UPPAggrSelector),
288
289
  )
289
290
  """