mergeron 2024.738940.0__tar.gz → 2024.738949.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.
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/PKG-INFO +2 -5
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/README.rst +1 -1
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/pyproject.toml +3 -4
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/core/excel_helper.py +38 -25
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/core/ftc_merger_investigations_data.py +33 -30
- mergeron-2024.738940.0/src/mergeron/core/guidelines_standards.py → mergeron-2024.738949.0/src/mergeron/core/guidelines_boundaries.py +35 -29
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/core/proportions_tests.py +12 -10
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/concentration_as_diversion.py +30 -26
- mergeron-2024.738940.0/src/mergeron/examples/safeharbor_boundaries_for_mergers_with_asymmetric_shares.py → mergeron-2024.738949.0/src/mergeron/examples/enforcement_boundaries_for_mergers_with_asymmetric_shares.py +84 -90
- mergeron-2024.738940.0/src/mergeron/examples/safeharbor_boundaries_for_symmetric_firm_mergers.py → mergeron-2024.738949.0/src/mergeron/examples/enforcement_boundaries_for_symmetric_firm_mergers.py +3 -3
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/guidelines_enforcement_patterns.py +18 -16
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/investigations_stats_obs_tables.py +15 -14
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/investigations_stats_sim_tables.py +49 -54
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/plotSafeHarbs_symbolically.py +1 -1
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/sound_guppi_safeharbor.py +59 -54
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/summarize_ftc_investigations_data.py +4 -4
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/visualize_empirical_margin_distribution.py +2 -2
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/visualize_guidelines_tests.py +67 -65
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/gen/__init__.py +104 -42
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/gen/_data_generation_functions_nonpublic.py +6 -6
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/gen/data_generation.py +1 -4
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/gen/investigations_stats.py +21 -27
- mergeron-2024.738940.0/src/mergeron/gen/guidelines_tests.py → mergeron-2024.738949.0/src/mergeron/gen/upp_tests.py +98 -102
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/License.txt +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/__init__.py +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/core/InCommon RSA Server CA cert chain.pem +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/core/__init__.py +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/core/damodaran_margin_data.py +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/core/pseudorandom_numbers.py +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/__init__.py +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/example_parameterizations.py +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/examples/testIntrinsicClearanceRates.py +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/ext/__init__.py +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/ext/tol_colors.py +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/jinja_LaTex_templates/clrrate_cis_summary_table_template.tex.jinja2 +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/jinja_LaTex_templates/ftcinvdata_byhhianddelta_table_template.tex.jinja2 +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/jinja_LaTex_templates/ftcinvdata_summary_table_template.tex.jinja2 +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/jinja_LaTex_templates/ftcinvdata_summarypaired_table_template.tex.jinja2 +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/jinja_LaTex_templates/mergeron.cls +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/jinja_LaTex_templates/mergeron_table_collection_template.tex.jinja2 +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/jinja_LaTex_templates/setup_tikz_tables.tex.jinja2 +0 -0
- {mergeron-2024.738940.0 → mergeron-2024.738949.0}/src/mergeron/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mergeron
|
|
3
|
-
Version: 2024.
|
|
3
|
+
Version: 2024.738949.0
|
|
4
4
|
Summary: Analysis of standards defined in Horizontal Merger Guidelines
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: merger policy analysis,merger guidelines,merger screening,policy presumptions,concentration standards,upward pricing pressure,GUPPI
|
|
@@ -18,7 +18,6 @@ Classifier: Programming Language :: Python :: 3
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
20
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
|
-
Requires-Dist: aenum (>=3.1)
|
|
22
21
|
Requires-Dist: attrs (>=23.2)
|
|
23
22
|
Requires-Dist: bs4 (>=0.0.1)
|
|
24
23
|
Requires-Dist: certifi (>=2023.11.17)
|
|
@@ -28,13 +27,11 @@ Requires-Dist: jinja2 (>=3.1)
|
|
|
28
27
|
Requires-Dist: joblib (>=1.3)
|
|
29
28
|
Requires-Dist: lxml (>=5.0)
|
|
30
29
|
Requires-Dist: matplotlib (>=3.8)
|
|
31
|
-
Requires-Dist: mkl (==2023.2)
|
|
32
30
|
Requires-Dist: mpmath (>=1.3)
|
|
33
31
|
Requires-Dist: msgpack (>=1.0)
|
|
34
32
|
Requires-Dist: msgpack-numpy (>=0.4)
|
|
35
33
|
Requires-Dist: numpy (>=1.26)
|
|
36
34
|
Requires-Dist: openpyxl (>=3.1.2)
|
|
37
|
-
Requires-Dist: pyarrow (>=15.0)
|
|
38
35
|
Requires-Dist: pymupdf (>=1.23)
|
|
39
36
|
Requires-Dist: requests (>=2.31)
|
|
40
37
|
Requires-Dist: requests-toolbelt (>=1.0.0)
|
|
@@ -61,7 +58,7 @@ Routines for downloading and organizing FTC merger investigtions data published
|
|
|
61
58
|
|
|
62
59
|
Routines for plotting boundaries of (sets of mergers falling within) specified concentration and diversion ratio boundaries and for calibrating GUPPI thresholds to concentration (ΔHHI) thresholds, and vice-versa.
|
|
63
60
|
|
|
64
|
-
mergeron.core.
|
|
61
|
+
mergeron.core.guidelines_boundaries
|
|
65
62
|
|
|
66
63
|
Routines for generating industry data under various distributions of shares, prices, and margins. The user can specify whether rates are specified as, "proportional", "inside-out" (i.e., consistent with merging-firms' in-market shares and a default recapture rate), or "outside-in", (i.e., purchase probabilities are drawn at random for :math:`n+1` goods, from which are derived market shares and recapture rates for the :math:`n` goods in the putative market). Price-cost-margins may be specified as symmetric, i.i.d, or consistent with equilibrium conditions for (profit-mazimization in) Bertrand-Nash oligopoly with MNL demand. Prices may be specified as symmetric or asymmetric, and in the latter case, the direction of correlation between merging firm prices, if any, can also be specified. Two alternative approaches for modeling statutory filing requirements (HSR filing thresholds) are implemented.
|
|
67
64
|
|
|
@@ -14,7 +14,7 @@ Routines for downloading and organizing FTC merger investigtions data published
|
|
|
14
14
|
|
|
15
15
|
Routines for plotting boundaries of (sets of mergers falling within) specified concentration and diversion ratio boundaries and for calibrating GUPPI thresholds to concentration (ΔHHI) thresholds, and vice-versa.
|
|
16
16
|
|
|
17
|
-
mergeron.core.
|
|
17
|
+
mergeron.core.guidelines_boundaries
|
|
18
18
|
|
|
19
19
|
Routines for generating industry data under various distributions of shares, prices, and margins. The user can specify whether rates are specified as, "proportional", "inside-out" (i.e., consistent with merging-firms' in-market shares and a default recapture rate), or "outside-in", (i.e., purchase probabilities are drawn at random for :math:`n+1` goods, from which are derived market shares and recapture rates for the :math:`n` goods in the putative market). Price-cost-margins may be specified as symmetric, i.i.d, or consistent with equilibrium conditions for (profit-mazimization in) Bertrand-Nash oligopoly with MNL demand. Prices may be specified as symmetric or asymmetric, and in the latter case, the direction of correlation between merging firm prices, if any, can also be specified. Two alternative approaches for modeling statutory filing requirements (HSR filing thresholds) are implemented.
|
|
20
20
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "mergeron"
|
|
3
3
|
# See ./get_version_str.py
|
|
4
|
-
version = "2024.
|
|
4
|
+
version = "2024.738949.0"
|
|
5
5
|
description = "Analysis of standards defined in Horizontal Merger Guidelines"
|
|
6
6
|
keywords = [
|
|
7
7
|
"merger policy analysis",
|
|
@@ -34,7 +34,6 @@ classifiers = [
|
|
|
34
34
|
[tool.poetry.dependencies]
|
|
35
35
|
# You may need to apply the fixes in, https://github.com/python-poetry/poetry/issues/3365
|
|
36
36
|
# if poetry dependency resolution appears to hang (read the page at link to the end)
|
|
37
|
-
aenum = ">=3.1"
|
|
38
37
|
attrs = ">=23.2"
|
|
39
38
|
bs4 = ">=0.0.1"
|
|
40
39
|
google-re2 = ">=1.1"
|
|
@@ -42,13 +41,11 @@ jinja2 = ">=3.1"
|
|
|
42
41
|
joblib = ">=1.3"
|
|
43
42
|
lxml = ">=5.0"
|
|
44
43
|
matplotlib = ">=3.8"
|
|
45
|
-
mkl = "2023.2"
|
|
46
44
|
mpmath = ">=1.3"
|
|
47
45
|
msgpack = ">=1.0"
|
|
48
46
|
msgpack-numpy = ">=0.4"
|
|
49
47
|
numpy = ">=1.26"
|
|
50
48
|
openpyxl = ">=3.1.2"
|
|
51
|
-
pyarrow = ">=15.0"
|
|
52
49
|
pymupdf = ">=1.23"
|
|
53
50
|
python = "^3.12"
|
|
54
51
|
requests = ">=2.31"
|
|
@@ -71,6 +68,8 @@ sphinx = ">=7.2"
|
|
|
71
68
|
sphinx-autodoc-typehints = ">=2.0.0"
|
|
72
69
|
sphinx-autoapi = ">=3.0"
|
|
73
70
|
sphinx-immaterial = ">=0.11"
|
|
71
|
+
pipdeptree = ">=2.15.1"
|
|
72
|
+
uv = ">=0.1.11"
|
|
74
73
|
|
|
75
74
|
[build-system]
|
|
76
75
|
requires = ["poetry-core"]
|
|
@@ -14,16 +14,29 @@ from .. import _PKG_NAME # noqa: TID252
|
|
|
14
14
|
|
|
15
15
|
__version__ = version(_PKG_NAME)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
from
|
|
17
|
+
import enum
|
|
18
|
+
from collections.abc import Mapping, Sequence
|
|
19
|
+
from types import MappingProxyType
|
|
20
|
+
from typing import Any
|
|
19
21
|
|
|
20
|
-
import aenum # type: ignore
|
|
21
22
|
import numpy as np
|
|
22
23
|
import numpy.typing as npt
|
|
23
24
|
import xlsxwriter # type: ignore
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
@enum.unique
|
|
28
|
+
class CFmtParent(dict[str, Any], enum.ReprEnum):
|
|
29
|
+
def merge(self, _other) -> CFmtParent:
|
|
30
|
+
if isinstance(_other, CFmtParent):
|
|
31
|
+
return self.value | _other.value
|
|
32
|
+
else:
|
|
33
|
+
raise RuntimeWarning(
|
|
34
|
+
f"Object {_other!r} not valid for merge(), returned original."
|
|
35
|
+
)
|
|
36
|
+
return self.value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CFmt(CFmtParent): # type: ignore
|
|
27
40
|
"""
|
|
28
41
|
Initialize cell formats for xlsxwriter.
|
|
29
42
|
|
|
@@ -35,31 +48,31 @@ class CFmt(aenum.UniqueEnum): # type: ignore
|
|
|
35
48
|
See, https://xlsxwriter.readthedocs.io/format.html
|
|
36
49
|
"""
|
|
37
50
|
|
|
38
|
-
XL_DEFAULT
|
|
39
|
-
XL_DEFAULT_2003
|
|
51
|
+
XL_DEFAULT = MappingProxyType({"font_name": "Calibri", "font_size": 11})
|
|
52
|
+
XL_DEFAULT_2003 = MappingProxyType({"font_name": "Arial", "font_size": 10})
|
|
40
53
|
|
|
41
|
-
A_CTR
|
|
42
|
-
A_CTR_ACROSS
|
|
43
|
-
A_LEFT
|
|
44
|
-
A_RIGHT
|
|
54
|
+
A_CTR = MappingProxyType({"align": "center"})
|
|
55
|
+
A_CTR_ACROSS = MappingProxyType({"align": "center_across"})
|
|
56
|
+
A_LEFT = MappingProxyType({"align": "left"})
|
|
57
|
+
A_RIGHT = MappingProxyType({"align": "right"})
|
|
45
58
|
|
|
46
|
-
BOLD
|
|
47
|
-
ITALIC
|
|
48
|
-
ULINE
|
|
59
|
+
BOLD = MappingProxyType({"bold": True})
|
|
60
|
+
ITALIC = MappingProxyType({"italic": True})
|
|
61
|
+
ULINE = MappingProxyType({"underline": True})
|
|
49
62
|
|
|
50
|
-
TEXT_WRAP
|
|
51
|
-
IND_1
|
|
63
|
+
TEXT_WRAP = MappingProxyType({"text_wrap": True})
|
|
64
|
+
IND_1 = MappingProxyType({"indent": 1})
|
|
52
65
|
|
|
53
|
-
DOLLAR_NUM
|
|
54
|
-
DT_NUM
|
|
55
|
-
QTY_NUM
|
|
56
|
-
PCT_NUM
|
|
57
|
-
AREA_NUM
|
|
66
|
+
DOLLAR_NUM = MappingProxyType({"num_format": "[$$-409]#,##0.00"})
|
|
67
|
+
DT_NUM = MappingProxyType({"num_format": "mm/dd/yyyy"})
|
|
68
|
+
QTY_NUM = MappingProxyType({"num_format": "#,##0.0"})
|
|
69
|
+
PCT_NUM = MappingProxyType({"num_format": "##0.000000%"})
|
|
70
|
+
AREA_NUM = MappingProxyType({"num_format": "0.00000000"})
|
|
58
71
|
|
|
59
|
-
BAR_FILL
|
|
60
|
-
BOT_BORDER
|
|
61
|
-
TOP_BORDER
|
|
62
|
-
HDR_BORDER
|
|
72
|
+
BAR_FILL = MappingProxyType({"pattern": 1, "bg_color": "dfeadf"})
|
|
73
|
+
BOT_BORDER = MappingProxyType({"bottom": 1, "bottom_color": "000000"})
|
|
74
|
+
TOP_BORDER = MappingProxyType({"top": 1, "top_color": "000000"})
|
|
75
|
+
HDR_BORDER = TOP_BORDER | BOT_BORDER
|
|
63
76
|
|
|
64
77
|
|
|
65
78
|
def matrix_to_sheet(
|
|
@@ -216,7 +229,7 @@ def xl_fmt(
|
|
|
216
229
|
:code:`xlsxwriter` `Format` object
|
|
217
230
|
|
|
218
231
|
"""
|
|
219
|
-
_cell_fmt_dict:
|
|
232
|
+
_cell_fmt_dict: Mapping[str, Any] = MappingProxyType({})
|
|
220
233
|
if isinstance(_cell_fmt, tuple):
|
|
221
234
|
ensure_cell_format_spec_tuple(_cell_fmt)
|
|
222
235
|
for _cf in _cell_fmt:
|
|
@@ -81,13 +81,13 @@ CNT_FCOUNT_DICT = {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
class
|
|
84
|
+
class INVTableData(NamedTuple):
|
|
85
85
|
ind_grp: str
|
|
86
86
|
evid_cond: str
|
|
87
87
|
data_array: NDArray[np.int64]
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
INVData: TypeAlias = Mapping[str, dict[str, dict[str,
|
|
90
|
+
INVData: TypeAlias = Mapping[str, dict[str, dict[str, INVTableData]]]
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
def construct_data(
|
|
@@ -130,13 +130,13 @@ def construct_data(
|
|
|
130
130
|
if _archive_path.is_file() and not rebuild_data:
|
|
131
131
|
_archived_data = msgpack.unpackb(_archive_path.read_bytes(), use_list=False)
|
|
132
132
|
|
|
133
|
-
_invdata: dict[str, dict[str, dict[str,
|
|
133
|
+
_invdata: dict[str, dict[str, dict[str, INVTableData]]] = {}
|
|
134
134
|
for _period in _archived_data:
|
|
135
135
|
_invdata[_period] = {}
|
|
136
136
|
for _table_type in _archived_data[_period]:
|
|
137
137
|
_invdata[_period][_table_type] = {}
|
|
138
138
|
for _table_no in _archived_data[_period][_table_type]:
|
|
139
|
-
_invdata[_period][_table_type][_table_no] =
|
|
139
|
+
_invdata[_period][_table_type][_table_no] = INVTableData(
|
|
140
140
|
*_archived_data[_period][_table_type][_table_no]
|
|
141
141
|
)
|
|
142
142
|
return MappingProxyType(_invdata)
|
|
@@ -197,7 +197,7 @@ def _construct_no_entry_evidence_data(_invdata: INVData, _data_period: str, /) -
|
|
|
197
197
|
_invdata_evid_cond = "No Entry Evidence"
|
|
198
198
|
|
|
199
199
|
_invdata_sub_evid_cond_conc = _invdata[_data_period]["ByHHIandDelta"]
|
|
200
|
-
_invdata_sub_evid_cond_conc["Table 9.X"] =
|
|
200
|
+
_invdata_sub_evid_cond_conc["Table 9.X"] = INVTableData(
|
|
201
201
|
_invdata_ind_grp,
|
|
202
202
|
_invdata_evid_cond,
|
|
203
203
|
np.column_stack((
|
|
@@ -211,7 +211,7 @@ def _construct_no_entry_evidence_data(_invdata: INVData, _data_period: str, /) -
|
|
|
211
211
|
)
|
|
212
212
|
|
|
213
213
|
_invdata_sub_evid_cond_fcount = _invdata[_data_period]["ByFirmCount"]
|
|
214
|
-
_invdata_sub_evid_cond_fcount["Table 10.X"] =
|
|
214
|
+
_invdata_sub_evid_cond_fcount["Table 10.X"] = INVTableData(
|
|
215
215
|
_invdata_ind_grp,
|
|
216
216
|
_invdata_evid_cond,
|
|
217
217
|
np.column_stack((
|
|
@@ -231,7 +231,7 @@ def _construct_new_period_data(
|
|
|
231
231
|
/,
|
|
232
232
|
*,
|
|
233
233
|
flag_backward_compatibility: bool = False,
|
|
234
|
-
) -> dict[str, dict[str,
|
|
234
|
+
) -> dict[str, dict[str, INVTableData]]:
|
|
235
235
|
_cuml_period = "1996-{}".format(int(_data_period.split("-")[1]))
|
|
236
236
|
if _cuml_period != "1996-2011":
|
|
237
237
|
raise ValueError('Expected cumulative period, "1996-2011"')
|
|
@@ -249,7 +249,7 @@ def _construct_new_period_data(
|
|
|
249
249
|
_data_typesubdict = {}
|
|
250
250
|
for _table_no in _invdata_cuml[_table_type]:
|
|
251
251
|
_invdata_cuml_sub_table = _invdata_cuml[_table_type][_table_no]
|
|
252
|
-
|
|
252
|
+
_invdata_ind_group, _invdata_evid_cond, _invdata_cuml_array = (
|
|
253
253
|
_invdata_cuml_sub_table.ind_grp,
|
|
254
254
|
_invdata_cuml_sub_table.evid_cond,
|
|
255
255
|
_invdata_cuml_sub_table.data_array,
|
|
@@ -257,16 +257,16 @@ def _construct_new_period_data(
|
|
|
257
257
|
|
|
258
258
|
_invdata_base_sub_table = _invdata_base[_table_type].get(_table_no, None)
|
|
259
259
|
|
|
260
|
-
(
|
|
260
|
+
(_invdata_base_ind_group, _invdata_base_evid_cond, _invdata_base_array) = (
|
|
261
261
|
_invdata_base_sub_table or ("", "", None)
|
|
262
262
|
)
|
|
263
263
|
|
|
264
264
|
# Some tables can't be constructed due to inconsistencies in the data
|
|
265
265
|
# across time periods
|
|
266
266
|
if (
|
|
267
|
-
(_data_period != "2004-2011" and
|
|
268
|
-
or (
|
|
269
|
-
or (
|
|
267
|
+
(_data_period != "2004-2011" and _invdata_ind_group != "All Markets")
|
|
268
|
+
or (_invdata_ind_group in ('"Other" Markets', "Industries in Common"))
|
|
269
|
+
or (_invdata_base_ind_group in ('"Other" Markets', ""))
|
|
270
270
|
):
|
|
271
271
|
continue
|
|
272
272
|
|
|
@@ -334,7 +334,7 @@ def _construct_new_period_data(
|
|
|
334
334
|
# )
|
|
335
335
|
# if np.einsum('ij->', invdata_array_bld_tbc):
|
|
336
336
|
# print(
|
|
337
|
-
# f"{_data_period}, {_table_no}, {
|
|
337
|
+
# f"{_data_period}, {_table_no}, {_invdata_ind_group}:",
|
|
338
338
|
# abs(np.einsum('ij->', invdata_array_bld_tbc))
|
|
339
339
|
# )
|
|
340
340
|
|
|
@@ -350,22 +350,22 @@ def _construct_new_period_data(
|
|
|
350
350
|
np.einsum("ij->i", _invdata_array_bld_enfcls),
|
|
351
351
|
))
|
|
352
352
|
|
|
353
|
-
_data_typesubdict[_table_no] =
|
|
354
|
-
|
|
353
|
+
_data_typesubdict[_table_no] = INVTableData(
|
|
354
|
+
_invdata_ind_group, _invdata_evid_cond, _invdata_array_bld
|
|
355
355
|
)
|
|
356
|
-
del
|
|
357
|
-
del
|
|
356
|
+
del _invdata_ind_group, _invdata_evid_cond, _invdata_cuml_array
|
|
357
|
+
del _invdata_base_ind_group, _invdata_base_evid_cond, _invdata_base_array
|
|
358
358
|
del _invdata_array_bld
|
|
359
359
|
_invdata_bld[_table_type] = _data_typesubdict
|
|
360
360
|
return _invdata_bld
|
|
361
361
|
|
|
362
362
|
|
|
363
363
|
def _invdata_build_aggregate_table(
|
|
364
|
-
_data_typesub: dict[str,
|
|
365
|
-
) ->
|
|
364
|
+
_data_typesub: dict[str, INVTableData], _aggr_table_list: Sequence[str]
|
|
365
|
+
) -> INVTableData:
|
|
366
366
|
_hdr_table_no = _aggr_table_list[0]
|
|
367
367
|
|
|
368
|
-
return
|
|
368
|
+
return INVTableData(
|
|
369
369
|
"Industries in Common",
|
|
370
370
|
"Unrestricted on additional evidence",
|
|
371
371
|
np.column_stack((
|
|
@@ -403,7 +403,7 @@ def parse_invdata(
|
|
|
403
403
|
by range of HHI and ∆HHI.
|
|
404
404
|
|
|
405
405
|
"""
|
|
406
|
-
_invdata: dict[str, dict[str, dict[str,
|
|
406
|
+
_invdata: dict[str, dict[str, dict[str, INVTableData]]] = {}
|
|
407
407
|
|
|
408
408
|
for _invdata_docname in _invdata_docnames:
|
|
409
409
|
_invdata_pdf_path = FTCDATA_DIR.joinpath(_invdata_docname)
|
|
@@ -508,7 +508,7 @@ def _parse_table_blocks(
|
|
|
508
508
|
)
|
|
509
509
|
|
|
510
510
|
if _data_period == "1996-2011":
|
|
511
|
-
|
|
511
|
+
_invdata_ind_group = (
|
|
512
512
|
_table_blocks[1][-3].split("\n")[1]
|
|
513
513
|
if _table_num == "Table 4.8"
|
|
514
514
|
else _table_blocks[2][-3].split("\n")[0]
|
|
@@ -524,19 +524,19 @@ def _parse_table_blocks(
|
|
|
524
524
|
elif _data_period == "1996-2005":
|
|
525
525
|
_table_blocks = sorted(_table_blocks, key=itemgetter(6))
|
|
526
526
|
|
|
527
|
-
|
|
527
|
+
_invdata_ind_group = _table_blocks[3][-3].strip()
|
|
528
528
|
if _table_ser > 4:
|
|
529
529
|
_invdata_evid_cond = _table_blocks[5][-3].strip()
|
|
530
530
|
|
|
531
531
|
elif _table_ser % 2 == 0:
|
|
532
|
-
|
|
532
|
+
_invdata_ind_group = _table_blocks[1][-3].split("\n")[2]
|
|
533
533
|
if (_evid_cond_teststr := _table_blocks[2][-3].strip()) == "Outcome":
|
|
534
534
|
_invdata_evid_cond = "Unrestricted on additional evidence"
|
|
535
535
|
else:
|
|
536
536
|
_invdata_evid_cond = _evid_cond_teststr
|
|
537
537
|
|
|
538
538
|
elif _table_blocks[3][-3].startswith("FTC Horizontal Merger Investigations"):
|
|
539
|
-
|
|
539
|
+
_invdata_ind_group = _table_blocks[3][-3].split("\n")[2]
|
|
540
540
|
_invdata_evid_cond = "Unrestricted on additional evidence"
|
|
541
541
|
|
|
542
542
|
else:
|
|
@@ -546,10 +546,10 @@ def _parse_table_blocks(
|
|
|
546
546
|
if _table_ser == 9
|
|
547
547
|
else _table_blocks[3][-3].strip()
|
|
548
548
|
)
|
|
549
|
-
|
|
549
|
+
_invdata_ind_group = _table_blocks[4][-3].split("\n")[2]
|
|
550
550
|
|
|
551
|
-
if
|
|
552
|
-
|
|
551
|
+
if _invdata_ind_group == "Pharmaceutical Markets":
|
|
552
|
+
_invdata_ind_group = "Pharmaceuticals Markets"
|
|
553
553
|
|
|
554
554
|
process_table_func = (
|
|
555
555
|
_process_table_blks_conc_type
|
|
@@ -563,7 +563,7 @@ def _parse_table_blocks(
|
|
|
563
563
|
print(_table_blocks)
|
|
564
564
|
raise ValueError
|
|
565
565
|
|
|
566
|
-
_table_data =
|
|
566
|
+
_table_data = INVTableData(_invdata_ind_group, _invdata_evid_cond, _table_array)
|
|
567
567
|
_invdata[_data_period][_table_type] |= {_table_num: _table_data}
|
|
568
568
|
|
|
569
569
|
|
|
@@ -579,7 +579,7 @@ def _process_table_blks_conc_type(
|
|
|
579
579
|
_conc_row_pat = re.compile(r"((?:0|\d,\d{3}) (?:- \d+,\d{3}|\+)|TOTAL)")
|
|
580
580
|
|
|
581
581
|
_col_titles_array = tuple(CONC_DELTA_DICT.values())
|
|
582
|
-
|
|
582
|
+
_col_totals: NDArray[np.int64] = np.zeros(len(_col_titles_array), np.int64)
|
|
583
583
|
_invdata_array: NDArray[np.int64] = np.array(None)
|
|
584
584
|
|
|
585
585
|
for _tbl_blk in _table_blocks:
|
|
@@ -636,6 +636,9 @@ def _process_table_blks_cnt_type(
|
|
|
636
636
|
_cnt_row_pat = re.compile(r"(\d+ (?:to \d+|\+)|TOTAL)")
|
|
637
637
|
|
|
638
638
|
_invdata_array: NDArray[np.int64] = np.array(None)
|
|
639
|
+
_col_totals: NDArray[np.int64] = np.zeros(
|
|
640
|
+
3, np.int64
|
|
641
|
+
) # "enforced", "closed", "total"
|
|
639
642
|
|
|
640
643
|
for _tbl_blk in _table_blocks:
|
|
641
644
|
if _cnt_row_pat.match(_blk_str := _tbl_blk[-3]):
|
|
@@ -15,7 +15,7 @@ from dataclasses import dataclass
|
|
|
15
15
|
from typing import Any, Literal, TypeAlias
|
|
16
16
|
|
|
17
17
|
import numpy as np
|
|
18
|
-
from
|
|
18
|
+
from attrs import define, field
|
|
19
19
|
from mpmath import mp, mpf # type: ignore
|
|
20
20
|
from numpy.typing import NDArray
|
|
21
21
|
from scipy.spatial.distance import minkowski as distance_function
|
|
@@ -33,7 +33,7 @@ class GuidelinesBoundary:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
@define(slots=True, frozen=True)
|
|
36
|
-
class
|
|
36
|
+
class HMGThresholds:
|
|
37
37
|
delta: float
|
|
38
38
|
rec: float
|
|
39
39
|
guppi: float
|
|
@@ -43,13 +43,12 @@ class GuidelinesSTD:
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
@define(slots=True, frozen=True)
|
|
46
|
-
class
|
|
46
|
+
class GuidelinesThresholds:
|
|
47
47
|
"""
|
|
48
|
-
Guidelines
|
|
49
|
-
|
|
50
|
-
Diversion ratio, GUPPI, CMCR, and IPR standards are constructed from
|
|
51
|
-
concentration standards.
|
|
48
|
+
Guidelines threholds by Guidelines publication year
|
|
52
49
|
|
|
50
|
+
ΔHHI, Recapture Rate, GUPPI, Diversion ratio, CMCR, and IPR thresholds
|
|
51
|
+
constructed from concentration standards.
|
|
53
52
|
"""
|
|
54
53
|
|
|
55
54
|
pub_year: HMGPubYear
|
|
@@ -57,26 +56,26 @@ class GuidelinesStandards:
|
|
|
57
56
|
Year of publication of the U.S. Horizontal Merger Guidelines (HMG)
|
|
58
57
|
"""
|
|
59
58
|
|
|
60
|
-
safeharbor:
|
|
59
|
+
safeharbor: HMGThresholds = field(kw_only=True, default=None)
|
|
61
60
|
"""
|
|
62
|
-
Negative presumption
|
|
61
|
+
Negative presumption quantified on various measures
|
|
63
62
|
|
|
64
63
|
ΔHHI safeharbor bound, default recapture rate, GUPPI bound,
|
|
65
64
|
diversion ratio limit, CMCR, and IPR
|
|
66
65
|
"""
|
|
67
66
|
|
|
68
|
-
|
|
67
|
+
imputed_presumption: HMGThresholds = field(kw_only=True, default=None)
|
|
69
68
|
"""
|
|
70
|
-
|
|
69
|
+
Presumption of harm imputed from guidelines
|
|
71
70
|
|
|
72
71
|
ΔHHI bound inferred from strict numbers-equivalent
|
|
73
72
|
of (post-merger) HHI presumption, and corresponding default recapture rate,
|
|
74
73
|
GUPPI bound, diversion ratio limit, CMCR, and IPR
|
|
75
74
|
"""
|
|
76
75
|
|
|
77
|
-
presumption:
|
|
76
|
+
presumption: HMGThresholds = field(kw_only=True, default=None)
|
|
78
77
|
"""
|
|
79
|
-
|
|
78
|
+
Presumption of harm defined in HMG
|
|
80
79
|
|
|
81
80
|
ΔHHI bound and corresponding default recapture rate, GUPPI bound,
|
|
82
81
|
diversion ratio limit, CMCR, and IPR
|
|
@@ -98,7 +97,7 @@ class GuidelinesStandards:
|
|
|
98
97
|
object.__setattr__(
|
|
99
98
|
self,
|
|
100
99
|
"safeharbor",
|
|
101
|
-
|
|
100
|
+
HMGThresholds(
|
|
102
101
|
_dh_s,
|
|
103
102
|
_r := round_cust((_fc := int(np.ceil(1 / _hhi_p))) / (_fc + 1)),
|
|
104
103
|
_g_s := gbd_from_dsf(_dh_s, m_star=1.0, r_bar=_r),
|
|
@@ -108,12 +107,12 @@ class GuidelinesStandards:
|
|
|
108
107
|
),
|
|
109
108
|
)
|
|
110
109
|
|
|
111
|
-
#
|
|
110
|
+
# imputed_presumption is relevant for 2010 Guidelines
|
|
112
111
|
object.__setattr__(
|
|
113
112
|
self,
|
|
114
|
-
"
|
|
113
|
+
"imputed_presumption",
|
|
115
114
|
(
|
|
116
|
-
|
|
115
|
+
HMGThresholds(
|
|
117
116
|
_dh_i := 2 * (0.5 / _fc) ** 2,
|
|
118
117
|
_r_i := round_cust((_fc - 1 / 2) / (_fc + 1 / 2)),
|
|
119
118
|
_g_i := gbd_from_dsf(_dh_i, m_star=1.0, r_bar=_r_i),
|
|
@@ -122,7 +121,7 @@ class GuidelinesStandards:
|
|
|
122
121
|
_g_i,
|
|
123
122
|
)
|
|
124
123
|
if self.pub_year == 2010
|
|
125
|
-
else
|
|
124
|
+
else HMGThresholds(
|
|
126
125
|
_dh_i := 2 * (1 / (_fc + 1)) ** 2,
|
|
127
126
|
_r,
|
|
128
127
|
_g_i := gbd_from_dsf(_dh_i, m_star=1.0, r_bar=_r),
|
|
@@ -136,7 +135,7 @@ class GuidelinesStandards:
|
|
|
136
135
|
object.__setattr__(
|
|
137
136
|
self,
|
|
138
137
|
"presumption",
|
|
139
|
-
|
|
138
|
+
HMGThresholds(
|
|
140
139
|
_dh_p,
|
|
141
140
|
_r,
|
|
142
141
|
_g_p := gbd_from_dsf(_dh_p, m_star=1.0, r_bar=_r),
|
|
@@ -391,7 +390,13 @@ def boundary_plot(*, mktshares_plot_flag: bool = True) -> tuple[Any, ...]:
|
|
|
391
390
|
_fig = plt.figure(figsize=(5, 5), dpi=600)
|
|
392
391
|
_ax_out = _fig.add_subplot()
|
|
393
392
|
|
|
394
|
-
def _set_axis_def(
|
|
393
|
+
def _set_axis_def(
|
|
394
|
+
_ax1: mpa.Axes,
|
|
395
|
+
/,
|
|
396
|
+
*,
|
|
397
|
+
mktshares_plot_flag: bool = False,
|
|
398
|
+
mktshares_axlbls_flag: bool = False,
|
|
399
|
+
) -> mpa.Axes:
|
|
395
400
|
# Set the width of axis gridlines, and tick marks:
|
|
396
401
|
# both axes, both major and minor ticks
|
|
397
402
|
# Frame, grid, and facecolor
|
|
@@ -402,7 +407,7 @@ def boundary_plot(*, mktshares_plot_flag: bool = True) -> tuple[Any, ...]:
|
|
|
402
407
|
_ax1.spines[_spos1].set_linewidth(0.0)
|
|
403
408
|
_ax1.spines[_spos1].set_zorder(0)
|
|
404
409
|
_ax1.spines[_spos1].set_visible(False)
|
|
405
|
-
_ax1.set_facecolor("#
|
|
410
|
+
_ax1.set_facecolor("#E6E6E6")
|
|
406
411
|
|
|
407
412
|
_ax1.grid(linewidth=0.5, linestyle=":", color="grey", zorder=1)
|
|
408
413
|
_ax1.tick_params(axis="x", which="both", width=0.5)
|
|
@@ -421,15 +426,16 @@ def boundary_plot(*, mktshares_plot_flag: bool = True) -> tuple[Any, ...]:
|
|
|
421
426
|
_ax1.yaxis.get_majorticklabels(), horizontalalignment="right", fontsize=6
|
|
422
427
|
)
|
|
423
428
|
|
|
424
|
-
# Axis labels
|
|
425
|
-
# x-axis
|
|
426
|
-
_ax1.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
|
|
427
|
-
_ax1.xaxis.set_label_coords(0.75, -0.1)
|
|
428
|
-
# y-axis
|
|
429
|
-
_ax1.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
|
|
430
|
-
_ax1.yaxis.set_label_coords(-0.1, 0.75)
|
|
431
|
-
|
|
432
429
|
if mktshares_plot_flag:
|
|
430
|
+
# Axis labels
|
|
431
|
+
if mktshares_axlbls_flag:
|
|
432
|
+
# x-axis
|
|
433
|
+
_ax1.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
|
|
434
|
+
_ax1.xaxis.set_label_coords(0.75, -0.1)
|
|
435
|
+
# y-axis
|
|
436
|
+
_ax1.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
|
|
437
|
+
_ax1.yaxis.set_label_coords(-0.1, 0.75)
|
|
438
|
+
|
|
433
439
|
# Plot the ray of symmetry
|
|
434
440
|
_ax1.plot(
|
|
435
441
|
[0, 1], [0, 1], linewidth=0.5, linestyle=":", color="grey", zorder=1
|
|
@@ -7,6 +7,7 @@ Functions to estimate confidence intervals for
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from dataclasses import dataclass
|
|
10
11
|
from importlib.metadata import version
|
|
11
12
|
|
|
12
13
|
from .. import _PKG_NAME # noqa: TID252
|
|
@@ -14,7 +15,7 @@ from .. import _PKG_NAME # noqa: TID252
|
|
|
14
15
|
__version__ = version(_PKG_NAME)
|
|
15
16
|
|
|
16
17
|
from collections.abc import Sequence
|
|
17
|
-
from typing import Literal,
|
|
18
|
+
from typing import Literal, TypeVar
|
|
18
19
|
|
|
19
20
|
import numpy as np
|
|
20
21
|
from numpy.typing import NBitBase, NDArray
|
|
@@ -101,12 +102,12 @@ def propn_ci(
|
|
|
101
102
|
case "Agresti-Coull":
|
|
102
103
|
_zsc = norm.ppf(1 - alpha / 2)
|
|
103
104
|
_zscsq = _zsc * _zsc
|
|
104
|
-
|
|
105
|
-
_est_phat = (_npos +
|
|
105
|
+
_adjmt = 4 if alpha == 0.05 else _zscsq
|
|
106
|
+
_est_phat = (_npos + _adjmt / 2) / (_nobs + _adjmt)
|
|
106
107
|
_est_ci_l, _est_ci_u = (
|
|
107
108
|
_est_phat + _g
|
|
108
109
|
for _g in [
|
|
109
|
-
_f * _zsc * np.sqrt(_est_phat * (1 - _est_phat) / (_nobs +
|
|
110
|
+
_f * _zsc * np.sqrt(_est_phat * (1 - _est_phat) / (_nobs + _adjmt))
|
|
110
111
|
for _f in (-1, 1)
|
|
111
112
|
]
|
|
112
113
|
)
|
|
@@ -441,7 +442,7 @@ def _propn_diff_chisq_mn(
|
|
|
441
442
|
)
|
|
442
443
|
|
|
443
444
|
|
|
444
|
-
def
|
|
445
|
+
def propn_diff_ci_multinomial(
|
|
445
446
|
_counts: NDArray[np.integer[TI]], /, *, alpha: float = 0.05
|
|
446
447
|
) -> NDArray[np.float64]:
|
|
447
448
|
"""Estimate confidence intervals of pair-wise differences in multinomial proportions
|
|
@@ -475,16 +476,17 @@ def propn_ci_diff_multinomial(
|
|
|
475
476
|
return np.column_stack([_d + _f * _d_cr * np.sqrt(_var) for _f in (-1, 1)])
|
|
476
477
|
|
|
477
478
|
|
|
478
|
-
|
|
479
|
+
@dataclass(slots=True, frozen=True)
|
|
480
|
+
class MultinomialPropnsTest:
|
|
479
481
|
estimate: np.float64
|
|
480
482
|
dof: int
|
|
481
483
|
critical_value: np.float64
|
|
482
484
|
p_value: np.float64
|
|
483
485
|
|
|
484
486
|
|
|
485
|
-
def
|
|
487
|
+
def propn_test_multinomial(
|
|
486
488
|
_counts: NDArray[np.integer[TI]], /, *, alpha: float = 0.05
|
|
487
|
-
) ->
|
|
489
|
+
) -> MultinomialPropnsTest:
|
|
488
490
|
"""Chi-square test for homogeneity of differences in multinomial proportions.
|
|
489
491
|
|
|
490
492
|
Differences in multinomial proportions sum to zero.
|
|
@@ -510,9 +512,9 @@ def propn_diff_multinomial_chisq(
|
|
|
510
512
|
_p_bar = _n / np.einsum("jk->j", _n_k / _prob)
|
|
511
513
|
|
|
512
514
|
_y_sq = _n * ((1 / np.einsum("j->", _p_bar)) - 1)
|
|
513
|
-
_dof = np.array([
|
|
515
|
+
_dof = np.array([_s - 1 for _s in _counts.shape]).prod()
|
|
514
516
|
_chi_rv = chi2(_dof)
|
|
515
517
|
|
|
516
|
-
return
|
|
518
|
+
return MultinomialPropnsTest(
|
|
517
519
|
_y_sq, _dof, _chi_rv.ppf(1 - alpha), 1 - _chi_rv.cdf(_y_sq)
|
|
518
520
|
)
|