mergeron 2025.739290.5__tar.gz → 2025.739290.6__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.
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/PKG-INFO +1 -1
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/pyproject.toml +4 -4
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/__init__.py +35 -21
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/core/__init__.py +2 -2
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/core/empirical_margin_distribution.py +16 -16
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/core/ftc_merger_investigations_data.py +15 -26
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/core/guidelines_boundaries.py +14 -13
- mergeron-2025.739290.6/src/mergeron/data/__init__.py +57 -0
- mergeron-2025.739290.6/src/mergeron/data/ftc_merger_investigations_data.zip +0 -0
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/demo/visualize_empirical_margin_distribution.py +5 -2
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/gen/__init__.py +11 -5
- mergeron-2025.739290.5/src/mergeron/data/damodaran_margin_data_serialized.zip +0 -0
- mergeron-2025.739290.5/src/mergeron/data/ftc_invdata.msgpack +0 -0
- mergeron-2025.739290.5/src/mergeron/data/ftc_invdata.zip +0 -0
- mergeron-2025.739290.5/src/mergeron/demo/__init__.py +0 -3
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/README.rst +0 -0
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/core/guidelines_boundary_functions.py +0 -0
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/core/guidelines_boundary_functions_extra.py +0 -0
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/core/pseudorandom_numbers.py +0 -0
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/data/damodaran_margin_data.xls +0 -0
- {mergeron-2025.739290.5/src/mergeron/data → mergeron-2025.739290.6/src/mergeron/demo}/__init__.py +0 -0
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/gen/data_generation.py +0 -0
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/gen/data_generation_functions.py +0 -0
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/gen/enforcement_stats.py +0 -0
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/gen/upp_tests.py +0 -0
- {mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mergeron
|
|
3
|
-
Version: 2025.739290.
|
|
3
|
+
Version: 2025.739290.6
|
|
4
4
|
Summary: Analyze merger enforcement policy using Python
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: merger policy analysis,merger guidelines,merger screening,policy presumptions,concentration standards,upward pricing pressure,GUPPI
|
|
@@ -13,7 +13,7 @@ keywords = [
|
|
|
13
13
|
"upward pricing pressure",
|
|
14
14
|
"GUPPI",
|
|
15
15
|
]
|
|
16
|
-
version = "2025.739290.
|
|
16
|
+
version = "2025.739290.6"
|
|
17
17
|
|
|
18
18
|
# Classifiers list: https://pypi.org/classifiers/
|
|
19
19
|
classifiers = [
|
|
@@ -68,11 +68,11 @@ pendulum = ">=3.0.0"
|
|
|
68
68
|
ruff = ">=0.5"
|
|
69
69
|
poetry-plugin-export = "^1.8.0"
|
|
70
70
|
pytest = ">=8.0"
|
|
71
|
-
|
|
71
|
+
sphinx = ">8.2"
|
|
72
72
|
semver = ">=3.0"
|
|
73
73
|
sphinx-autodoc-typehints = ">=2.0.0"
|
|
74
|
-
sphinx-autoapi = ">=3.0"
|
|
75
|
-
sphinx-immaterial = "
|
|
74
|
+
sphinx-autoapi = ">=3.6.0"
|
|
75
|
+
sphinx-immaterial = ">0.11"
|
|
76
76
|
pipdeptree = ">=2.15.1"
|
|
77
77
|
types-openpyxl = ">=3.0.0"
|
|
78
78
|
virtualenv = ">=20.28.0"
|
|
@@ -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.
|
|
15
|
+
VERSION = "2025.739290.6"
|
|
16
16
|
|
|
17
17
|
__version__ = VERSION
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
WORK_DIR = globals().get("WORK_DIR", Path.home() / _PKG_NAME)
|
|
20
20
|
"""
|
|
21
|
-
|
|
21
|
+
If defined, the global variable WORK_DIR is used as a data store.
|
|
22
22
|
|
|
23
|
-
If the
|
|
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
|
|
26
|
-
|
|
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[
|
|
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.
|
|
45
|
+
type ArrayINT = NDArray[np.integer]
|
|
44
46
|
|
|
45
47
|
type ArrayDouble = NDArray[np.float64]
|
|
46
|
-
type ArrayBIGINT = NDArray[np.
|
|
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
|
-
|
|
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 = \
|
|
153
|
+
REC_i = \overline{r} {\ } \forall {\ } i \in \mathscr{M}
|
|
144
154
|
|
|
145
155
|
"""
|
|
146
156
|
|
|
147
|
-
|
|
157
|
+
INOUT = "inside-out"
|
|
148
158
|
R"""
|
|
149
|
-
Given, :math:`\
|
|
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{\
|
|
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
|
-
|
|
158
|
-
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 = \
|
|
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
|
|
|
@@ -61,14 +61,14 @@ type INVData_in = Mapping[str, Mapping[str, Mapping[str, INVTableData]]]
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def _dict_from_mapping(_p: Mapping[Any, Any], /) -> dict[Any, Any]:
|
|
64
|
-
retval = {}
|
|
64
|
+
retval: dict[Any, Any] = {}
|
|
65
65
|
for _k, _v in _p.items(): # for subit in it:
|
|
66
66
|
retval |= {_k: _dict_from_mapping(_v)} if isinstance(_v, Mapping) else {_k: _v}
|
|
67
67
|
return retval
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def _mappingproxy_from_mapping(_p: Mapping[Any, Any], /) -> MappingProxyType[Any, Any]:
|
|
71
|
-
retval = {}
|
|
71
|
+
retval: dict[Any, Any] = {}
|
|
72
72
|
for _k, _v in _p.items(): # for subit in it:
|
|
73
73
|
retval |= (
|
|
74
74
|
{_k: _mappingproxy_from_mapping(_v)}
|
{mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/core/empirical_margin_distribution.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
153
|
+
return margin_dict
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
def margin_data_builder(
|
|
@@ -13,7 +13,6 @@ from __future__ import annotations
|
|
|
13
13
|
import re
|
|
14
14
|
import shutil
|
|
15
15
|
from collections.abc import Sequence
|
|
16
|
-
from importlib import resources
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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")
|
|
@@ -442,12 +431,12 @@ def _parse_invdata() -> INVData:
|
|
|
442
431
|
# )
|
|
443
432
|
import pymupdf # type: ignore # noqa: PLC0415
|
|
444
433
|
|
|
445
|
-
invdata_docnames = _download_invdata(
|
|
434
|
+
invdata_docnames = _download_invdata(FID_WORK_DIR)
|
|
446
435
|
|
|
447
436
|
invdata: INVData_in = {}
|
|
448
437
|
|
|
449
438
|
for invdata_docname in invdata_docnames:
|
|
450
|
-
invdata_pdf_path =
|
|
439
|
+
invdata_pdf_path = FID_WORK_DIR.joinpath(invdata_docname)
|
|
451
440
|
|
|
452
441
|
invdata_doc = pymupdf.open(invdata_pdf_path)
|
|
453
442
|
invdata_meta = invdata_doc.metadata
|
|
@@ -709,7 +698,7 @@ def _process_table_blks_cnt_type(
|
|
|
709
698
|
return invdata_array[np.argsort(invdata_array[:, 0])]
|
|
710
699
|
|
|
711
700
|
|
|
712
|
-
def _download_invdata(_dl_path: Path =
|
|
701
|
+
def _download_invdata(_dl_path: Path = FID_WORK_DIR) -> tuple[str, ...]:
|
|
713
702
|
if not _dl_path.is_dir():
|
|
714
703
|
_dl_path.mkdir(parents=True)
|
|
715
704
|
|
{mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/core/guidelines_boundaries.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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]
|
|
@@ -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
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data useful for empirical analysis of merger enforcement policy
|
|
3
|
+
|
|
4
|
+
These data are processed for further analysis within relevant
|
|
5
|
+
submodules of the parent package. Thus, direct access is
|
|
6
|
+
unnecessary in routine use of this package.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from importlib import resources
|
|
10
|
+
|
|
11
|
+
from .. import _PKG_NAME, VERSION # noqa: TID252
|
|
12
|
+
|
|
13
|
+
__version__ = VERSION
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DAMODARAN_MARGIN_WORKBOOK = resources.files(f"{_PKG_NAME}.data").joinpath(
|
|
17
|
+
"damodaran_margin_data.xls"
|
|
18
|
+
)
|
|
19
|
+
"""
|
|
20
|
+
Python object pointing to included copy of Prof. Damodaran's margin data
|
|
21
|
+
|
|
22
|
+
Only used as a fallback, in case direct download from source fails.
|
|
23
|
+
|
|
24
|
+
NOTES
|
|
25
|
+
-----
|
|
26
|
+
Source data are from Prof. Aswath Damodaran, Stern School of Business, NYU; available online
|
|
27
|
+
at https://pages.stern.nyu.edu/~adamodar/pc/datasets/margin.xls
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Use as, for example:
|
|
31
|
+
|
|
32
|
+
.. code-block:: python
|
|
33
|
+
|
|
34
|
+
from mergeron.data import DAMODARAN_MARGIN_WORKBOOK
|
|
35
|
+
|
|
36
|
+
shutil.copy2(DAMODARAN_MARGIN_WORKBOOK, Path.home() / f"{DAMODARAN_MARGIN_WORKBOOK.name}")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
FTC_MERGER_INVESTIGATIONS_DATA = resources.files(f"{_PKG_NAME}.data").joinpath(
|
|
40
|
+
"ftc_merger_investigations_data.zip"
|
|
41
|
+
)
|
|
42
|
+
"""
|
|
43
|
+
FTC merger investigtions data published in 2004, 2007, 2008, and 2013
|
|
44
|
+
|
|
45
|
+
NOTES
|
|
46
|
+
-----
|
|
47
|
+
Raw data tables published by the FTC are loaded into a nested distionary, organized by
|
|
48
|
+
data period, table type, and table number. Each table is stored as a numerical array
|
|
49
|
+
(:module:`numpy` arrray), with additonal attrubutes for the industry group and additonal
|
|
50
|
+
evidence noted in the source data.
|
|
51
|
+
|
|
52
|
+
Data for additonal data periods (time spans) not reported in the source data,
|
|
53
|
+
e.g., 2004-2011, are constructed by subtracting counts in the base data from counts
|
|
54
|
+
in the cumulative data, by table, for "enforced" mergers and "closed" mergers, when
|
|
55
|
+
the cumulative data for the longer period are consistent with the base data for
|
|
56
|
+
a sub-period.
|
|
57
|
+
"""
|
|
@@ -13,9 +13,12 @@ from numpy.random import PCG64DXSM, Generator, SeedSequence
|
|
|
13
13
|
from scipy import stats # type: ignore
|
|
14
14
|
|
|
15
15
|
import mergeron.core.empirical_margin_distribution as emd
|
|
16
|
-
from mergeron import
|
|
16
|
+
from mergeron import WORK_DIR as PKG_WORK_DIR
|
|
17
17
|
from mergeron.core.guidelines_boundary_functions import boundary_plot
|
|
18
18
|
|
|
19
|
+
WORK_DIR = globals().get("WORK_DIR", PKG_WORK_DIR)
|
|
20
|
+
"""Redefined, in case the user defines WORK_DIR betweeen module imports."""
|
|
21
|
+
|
|
19
22
|
SAMPLE_SIZE = 10**6
|
|
20
23
|
BIN_COUNT = 25
|
|
21
24
|
margin_data_obs, margin_data_wts, margin_data_stats = emd.margin_data_builder()
|
|
@@ -85,4 +88,4 @@ mgn_ax.set_xlabel("Price Cost Margin", fontsize=10)
|
|
|
85
88
|
mgn_ax.set_ylabel("Relative Frequency", fontsize=10)
|
|
86
89
|
|
|
87
90
|
mgn_fig.tight_layout()
|
|
88
|
-
plt.savefig(
|
|
91
|
+
plt.savefig(WORK_DIR / f"{Path(__file__).stem}.pdf")
|
|
@@ -11,7 +11,7 @@ import io
|
|
|
11
11
|
from collections.abc import Sequence
|
|
12
12
|
from operator import attrgetter
|
|
13
13
|
|
|
14
|
-
import h5py
|
|
14
|
+
import h5py # type: ignore
|
|
15
15
|
import numpy as np
|
|
16
16
|
from attrs import Attribute, Converter, cmp_using, field, frozen, validators
|
|
17
17
|
from numpy.random import SeedSequence
|
|
@@ -255,7 +255,7 @@ class ShareSpec:
|
|
|
255
255
|
in published merger guidelines. Accordingly, the recapture ratio rounded to
|
|
256
256
|
the nearest 5% is:
|
|
257
257
|
|
|
258
|
-
* 0.85, **7-to-6 merger from symmetry**; US Guidelines,
|
|
258
|
+
* 0.85, **7-to-6 merger from symmetry**; US Guidelines, 1992, 2023
|
|
259
259
|
* 0.80, 5-to-4 merger from symmetry
|
|
260
260
|
* 0.80, **5-to-4 merger to symmetry**; US Guidelines, 2010
|
|
261
261
|
|
|
@@ -470,7 +470,9 @@ class MarketSampleData:
|
|
|
470
470
|
|
|
471
471
|
@aggregate_purchase_prob.default
|
|
472
472
|
def __appd(_i: MarketSampleData) -> ArrayDouble:
|
|
473
|
-
|
|
473
|
+
retval: ArrayDouble = np.empty_like(_i.frmshr_array[:, :1], float)
|
|
474
|
+
retval.fill(np.nan)
|
|
475
|
+
return retval
|
|
474
476
|
|
|
475
477
|
fcounts: ArrayINT = field(eq=cmp_using(np.array_equal))
|
|
476
478
|
"""Number of firms in market"""
|
|
@@ -488,14 +490,18 @@ class MarketSampleData:
|
|
|
488
490
|
|
|
489
491
|
@nth_firm_share.default
|
|
490
492
|
def __nfsd(_i: MarketSampleData) -> ArrayDouble:
|
|
491
|
-
|
|
493
|
+
retval: ArrayDouble = np.empty_like(_i.frmshr_array[:, :1], float)
|
|
494
|
+
retval.fill(np.nan)
|
|
495
|
+
return retval
|
|
492
496
|
|
|
493
497
|
hhi_post: ArrayDouble = field(eq=cmp_using(np.array_equal))
|
|
494
498
|
"""Post-merger change in Herfindahl-Hirschmann Index (HHI)"""
|
|
495
499
|
|
|
496
500
|
@hhi_post.default
|
|
497
501
|
def __hpd(_i: MarketSampleData) -> ArrayDouble:
|
|
498
|
-
|
|
502
|
+
retval: ArrayDouble = np.empty_like(_i.frmshr_array[:, :1], float)
|
|
503
|
+
retval.fill(np.nan)
|
|
504
|
+
return retval
|
|
499
505
|
|
|
500
506
|
def to_h5bin(self) -> bytes:
|
|
501
507
|
"""Save market sample data to HDF5 file."""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
{mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/core/guidelines_boundary_functions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/data/damodaran_margin_data.xls
RENAMED
|
File without changes
|
{mergeron-2025.739290.5/src/mergeron/data → mergeron-2025.739290.6/src/mergeron/demo}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mergeron-2025.739290.5 → mergeron-2025.739290.6}/src/mergeron/gen/data_generation_functions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|