mergeron 2025.739290.9__tar.gz → 2025.739319.1__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.9 → mergeron-2025.739319.1}/PKG-INFO +1 -1
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/pyproject.toml +1 -1
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/__init__.py +1 -1
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/core/empirical_margin_distribution.py +34 -33
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/gen/__init__.py +26 -12
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/gen/data_generation_functions.py +3 -3
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/README.rst +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/core/__init__.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/core/ftc_merger_investigations_data.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/core/guidelines_boundaries.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/core/guidelines_boundary_functions.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/core/guidelines_boundary_functions_extra.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/core/pseudorandom_numbers.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/data/__init__.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/data/damodaran_margin_data.xls +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/data/ftc_merger_investigations_data.zip +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/demo/__init__.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/demo/visualize_empirical_margin_distribution.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/gen/data_generation.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/gen/enforcement_stats.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/gen/upp_tests.py +0 -0
- {mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mergeron
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.739319.1
|
|
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
|
{mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/core/empirical_margin_distribution.py
RENAMED
|
@@ -38,7 +38,6 @@ price-cost margins fall in the interval :math:`[0, 1]`.
|
|
|
38
38
|
|
|
39
39
|
import shutil
|
|
40
40
|
import zipfile
|
|
41
|
-
from collections.abc import Mapping
|
|
42
41
|
from pathlib import Path
|
|
43
42
|
from types import MappingProxyType
|
|
44
43
|
|
|
@@ -60,6 +59,7 @@ WORK_DIR = globals().get("WORK_DIR", PKG_WORK_DIR)
|
|
|
60
59
|
|
|
61
60
|
MGNDATA_ARCHIVE_PATH = WORK_DIR / "damodaran_margin_data_serialized.zip"
|
|
62
61
|
|
|
62
|
+
type DamodaranMarginData = MappingProxyType[str, MappingProxyType[str, float | int]]
|
|
63
63
|
|
|
64
64
|
u3pm = urllib3.PoolManager()
|
|
65
65
|
|
|
@@ -69,7 +69,7 @@ def margin_data_getter( # noqa: PLR0912
|
|
|
69
69
|
*,
|
|
70
70
|
data_archive_path: Path | None = None,
|
|
71
71
|
data_download_flag: bool = False,
|
|
72
|
-
) ->
|
|
72
|
+
) -> DamodaranMarginData:
|
|
73
73
|
if _table_name != "margin": # Not validated for other tables
|
|
74
74
|
raise ValueError(
|
|
75
75
|
"This code is designed for parsing Prof. Damodaran's margin tables."
|
|
@@ -78,13 +78,10 @@ def margin_data_getter( # noqa: PLR0912
|
|
|
78
78
|
data_archive_path = data_archive_path or MGNDATA_ARCHIVE_PATH
|
|
79
79
|
workbook_path = data_archive_path.parent / f"damodaran_{_table_name}_data.xls"
|
|
80
80
|
if data_archive_path.is_file() and not data_download_flag:
|
|
81
|
-
with (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
margin_data_dict: MappingProxyType[
|
|
86
|
-
str, MappingProxyType[str, float | int]
|
|
87
|
-
] = this_yaml.load(_yfh)
|
|
81
|
+
with zipfile.ZipFile(data_archive_path) as _yzip:
|
|
82
|
+
margin_data_dict = this_yaml.load(
|
|
83
|
+
_yzip.read(data_archive_path.with_suffix(".yaml").name)
|
|
84
|
+
)
|
|
88
85
|
return margin_data_dict
|
|
89
86
|
elif workbook_path.is_file():
|
|
90
87
|
workbook_path.unlink()
|
|
@@ -154,8 +151,8 @@ def margin_data_getter( # noqa: PLR0912
|
|
|
154
151
|
|
|
155
152
|
|
|
156
153
|
def margin_data_builder(
|
|
157
|
-
_src_data_dict:
|
|
158
|
-
) -> tuple[ArrayDouble, ArrayDouble
|
|
154
|
+
_src_data_dict: DamodaranMarginData | None = None, /
|
|
155
|
+
) -> tuple[ArrayDouble, ArrayDouble]:
|
|
159
156
|
if _src_data_dict is None:
|
|
160
157
|
_src_data_dict = margin_data_getter()
|
|
161
158
|
|
|
@@ -194,25 +191,22 @@ def margin_data_builder(
|
|
|
194
191
|
* (len(margin_data_wts) / (len(margin_data_wts) - 1))
|
|
195
192
|
)
|
|
196
193
|
|
|
197
|
-
return (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
(
|
|
202
|
-
|
|
203
|
-
margin_wtd_stderr,
|
|
204
|
-
margin_data_obs.min(),
|
|
205
|
-
margin_data_obs.max(),
|
|
206
|
-
),
|
|
207
|
-
8,
|
|
194
|
+
return np.stack([margin_data_obs, margin_data_wts], axis=1, dtype=float), np.round(
|
|
195
|
+
(
|
|
196
|
+
margin_wtd_avg,
|
|
197
|
+
margin_wtd_stderr,
|
|
198
|
+
margin_data_obs.min(),
|
|
199
|
+
margin_data_obs.max(),
|
|
208
200
|
),
|
|
201
|
+
8,
|
|
209
202
|
)
|
|
210
203
|
|
|
211
204
|
|
|
212
205
|
def margin_data_resampler(
|
|
213
|
-
|
|
206
|
+
_dist_parms: tuple[ArrayDouble, ArrayDouble] | None = None,
|
|
214
207
|
/,
|
|
215
208
|
*,
|
|
209
|
+
sample_size: int | tuple[int, ...] = (10**6, 2),
|
|
216
210
|
seed_sequence: SeedSequence | None = None,
|
|
217
211
|
) -> ArrayDouble:
|
|
218
212
|
"""
|
|
@@ -220,12 +214,17 @@ def margin_data_resampler(
|
|
|
220
214
|
|
|
221
215
|
The empirical distribution is estimated using a Gaussian KDE; the bandwidth
|
|
222
216
|
selected using Silverman's rule is narrowed to reflect that the margin data
|
|
223
|
-
are multimodal. Margins for firms in finance, investment, insurance,
|
|
224
|
-
REITs are excluded from the sample used to estimate the
|
|
217
|
+
are multimodal. Margins for firms in finance, investment, insurance,
|
|
218
|
+
reinsurance, and REITs are excluded from the sample used to estimate the
|
|
219
|
+
empirical distribution.
|
|
225
220
|
|
|
226
221
|
Parameters
|
|
227
222
|
----------
|
|
228
|
-
|
|
223
|
+
|
|
224
|
+
_dist_parms
|
|
225
|
+
Array of margins and firm counts extracted from Prof. Damodaran's margin data
|
|
226
|
+
|
|
227
|
+
sample_size
|
|
229
228
|
Number of draws; if tuple, (number of draws, number of columns)
|
|
230
229
|
|
|
231
230
|
seed_sequence
|
|
@@ -238,24 +237,26 @@ def margin_data_resampler(
|
|
|
238
237
|
|
|
239
238
|
"""
|
|
240
239
|
|
|
240
|
+
_dist_parms = margin_data_builder()[0] if _dist_parms is None else _dist_parms
|
|
241
|
+
|
|
241
242
|
_seed = seed_sequence or SeedSequence(pool_size=8)
|
|
242
243
|
|
|
243
|
-
_x, _w,
|
|
244
|
+
_x, _w = _dist_parms[:, 0], _dist_parms[:, 1]
|
|
244
245
|
|
|
245
246
|
margin_kde = stats.gaussian_kde(_x, weights=_w, bw_method="silverman")
|
|
246
247
|
margin_kde.set_bandwidth(bw_method=margin_kde.factor / 3.0)
|
|
247
248
|
|
|
248
|
-
if isinstance(
|
|
249
|
+
if isinstance(sample_size, int):
|
|
249
250
|
return np.array(
|
|
250
|
-
margin_kde.resample(
|
|
251
|
+
margin_kde.resample(sample_size, seed=Generator(PCG64DXSM(_seed)))[0]
|
|
251
252
|
)
|
|
252
|
-
elif isinstance(
|
|
253
|
-
_ssz, _ncol =
|
|
254
|
-
ret_array = np.empty(
|
|
253
|
+
elif isinstance(sample_size, tuple) and len(sample_size) == 2:
|
|
254
|
+
_ssz, _ncol = sample_size
|
|
255
|
+
ret_array = np.empty(sample_size, float)
|
|
255
256
|
for idx, _col_seed in enumerate(_seed.spawn(_ncol)):
|
|
256
257
|
ret_array[:, idx] = margin_kde.resample(
|
|
257
258
|
_ssz, seed=Generator(PCG64DXSM(_col_seed))
|
|
258
259
|
)[0]
|
|
259
260
|
return ret_array
|
|
260
261
|
else:
|
|
261
|
-
raise ValueError(f"Invalid sample size: {
|
|
262
|
+
raise ValueError(f"Invalid sample size: {sample_size!r}")
|
|
@@ -30,6 +30,7 @@ from .. import ( # noqa: TID252
|
|
|
30
30
|
this_yaml,
|
|
31
31
|
yamelize_attrs,
|
|
32
32
|
)
|
|
33
|
+
from ..core.empirical_margin_distribution import margin_data_builder # noqa: TID252
|
|
33
34
|
from ..core.pseudorandom_numbers import ( # noqa: TID252
|
|
34
35
|
DEFAULT_BETA_DIST_PARMS,
|
|
35
36
|
DEFAULT_DIST_PARMS,
|
|
@@ -307,11 +308,11 @@ class FM2Constraint(str, Enameled):
|
|
|
307
308
|
|
|
308
309
|
|
|
309
310
|
def _pcm_dp_conv(
|
|
310
|
-
_v: Sequence[float] |
|
|
311
|
-
) -> ArrayFloat
|
|
312
|
-
if
|
|
313
|
-
|
|
314
|
-
|
|
311
|
+
_v: ArrayFloat | Sequence[float] | None, _i: PCMSpec
|
|
312
|
+
) -> ArrayFloat:
|
|
313
|
+
if _v is None or len(_v) == 0 or np.array_equal(_v, DEFAULT_DIST_PARMS):
|
|
314
|
+
if _i.dist_type == PCMDistribution.EMPR:
|
|
315
|
+
return margin_data_builder()[0]
|
|
315
316
|
match _i.dist_type:
|
|
316
317
|
case PCMDistribution.BETA:
|
|
317
318
|
return DEFAULT_BETA_DIST_PARMS
|
|
@@ -319,13 +320,20 @@ def _pcm_dp_conv(
|
|
|
319
320
|
return DEFAULT_BETA_BND_DIST_PARMS
|
|
320
321
|
case _:
|
|
321
322
|
return DEFAULT_DIST_PARMS
|
|
323
|
+
elif (_i.dist_type == PCMDistribution.EMPR and not isinstance(_v, np.ndarray)):
|
|
324
|
+
raise ValueError(
|
|
325
|
+
"Invalid specification; use output of mergeron.core.empriical_margin_distribution.margin_data_builider()."
|
|
326
|
+
)
|
|
322
327
|
elif isinstance(_v, Sequence | np.ndarray):
|
|
323
328
|
return np.asarray(_v, float)
|
|
324
329
|
else:
|
|
325
330
|
raise ValueError(
|
|
326
|
-
f"Input, {_v!r} has invalid type. Must be None,
|
|
331
|
+
f"Input, {_v!r} has invalid type. Must be None, sequence of floats,"
|
|
332
|
+
"sequence of Numpy arrays, or Numpy ndarray."
|
|
327
333
|
)
|
|
328
334
|
|
|
335
|
+
return _v
|
|
336
|
+
|
|
329
337
|
|
|
330
338
|
@frozen
|
|
331
339
|
class PCMSpec:
|
|
@@ -350,7 +358,7 @@ class PCMSpec:
|
|
|
350
358
|
def __dtd(_i: PCMSpec) -> PCMDistribution:
|
|
351
359
|
return PCMDistribution.UNI
|
|
352
360
|
|
|
353
|
-
dist_parms: ArrayFloat
|
|
361
|
+
dist_parms: ArrayFloat = field(
|
|
354
362
|
kw_only=True,
|
|
355
363
|
eq=cmp_using(eq=np.array_equal),
|
|
356
364
|
converter=Converter(_pcm_dp_conv, takes_self=True), # type: ignore
|
|
@@ -365,12 +373,16 @@ class PCMSpec:
|
|
|
365
373
|
"""
|
|
366
374
|
|
|
367
375
|
@dist_parms.default
|
|
368
|
-
def __dpwd(
|
|
376
|
+
def __dpwd(
|
|
377
|
+
_i: PCMSpec,
|
|
378
|
+
) -> ArrayFloat:
|
|
369
379
|
return _pcm_dp_conv(None, _i)
|
|
370
380
|
|
|
371
381
|
@dist_parms.validator
|
|
372
382
|
def __dpv(
|
|
373
|
-
_i: PCMSpec,
|
|
383
|
+
_i: PCMSpec,
|
|
384
|
+
_a: Attribute[ArrayFloat | Sequence[ArrayDouble] | None],
|
|
385
|
+
_v: ArrayFloat | Sequence[ArrayDouble] | None,
|
|
374
386
|
) -> None:
|
|
375
387
|
if _i.dist_type.name.startswith("BETA"):
|
|
376
388
|
if _v is None or not any(_v.shape):
|
|
@@ -391,10 +403,12 @@ class PCMSpec:
|
|
|
391
403
|
f'for PCM with distribution, "{_i.dist_type}" is incorrect.'
|
|
392
404
|
)
|
|
393
405
|
|
|
394
|
-
elif
|
|
406
|
+
elif (
|
|
407
|
+
_i.dist_type == PCMDistribution.EMPR
|
|
408
|
+
and not isinstance(_v, np.ndarray)
|
|
409
|
+
):
|
|
395
410
|
raise ValueError(
|
|
396
|
-
|
|
397
|
-
f'"given value, {_v!r} is ignored."'
|
|
411
|
+
"Empirical distribution requires deserialzed margin data from Prof. Damodaran, NYU"
|
|
398
412
|
)
|
|
399
413
|
|
|
400
414
|
firm2_pcm_constraint: FM2Constraint = field(kw_only=True, default=FM2Constraint.IID)
|
{mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/gen/data_generation_functions.py
RENAMED
|
@@ -671,16 +671,16 @@ def _gen_margin_data(
|
|
|
671
671
|
)
|
|
672
672
|
|
|
673
673
|
pcm_array = (
|
|
674
|
-
np.
|
|
674
|
+
np.empty_like(_frmshr_array[:, :1])
|
|
675
675
|
if _pcm_spec.firm2_pcm_constraint == FM2Constraint.SYM
|
|
676
|
-
else np.empty_like(_frmshr_array
|
|
676
|
+
else np.empty_like(_frmshr_array)
|
|
677
677
|
)
|
|
678
678
|
|
|
679
679
|
dist_parms_: ArrayFloat
|
|
680
680
|
beta_min, beta_max = [0.0] * 2 # placeholder
|
|
681
681
|
if dist_type_pcm == PCMDistribution.EMPR:
|
|
682
682
|
pcm_array = margin_data_resampler(
|
|
683
|
-
pcm_array.shape, seed_sequence=_pcm_rng_seed_seq
|
|
683
|
+
dist_parms_pcm, sample_size=pcm_array.shape, seed_sequence=_pcm_rng_seed_seq
|
|
684
684
|
)
|
|
685
685
|
else:
|
|
686
686
|
dist_type_: Literal["Beta", "Uniform"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/core/guidelines_boundaries.py
RENAMED
|
File without changes
|
{mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/core/guidelines_boundary_functions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mergeron-2025.739290.9 → mergeron-2025.739319.1}/src/mergeron/data/damodaran_margin_data.xls
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|