mergeron 2025.739290.9__tar.gz → 2025.739319.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 (22) hide show
  1. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/PKG-INFO +1 -1
  2. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/pyproject.toml +1 -1
  3. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/__init__.py +1 -1
  4. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/core/empirical_margin_distribution.py +26 -30
  5. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/gen/__init__.py +26 -12
  6. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/gen/data_generation_functions.py +3 -3
  7. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/README.rst +0 -0
  8. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/core/__init__.py +0 -0
  9. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/core/ftc_merger_investigations_data.py +0 -0
  10. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/core/guidelines_boundaries.py +0 -0
  11. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/core/guidelines_boundary_functions.py +0 -0
  12. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/core/guidelines_boundary_functions_extra.py +0 -0
  13. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/core/pseudorandom_numbers.py +0 -0
  14. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/data/__init__.py +0 -0
  15. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/data/damodaran_margin_data.xls +0 -0
  16. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/data/ftc_merger_investigations_data.zip +0 -0
  17. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/demo/__init__.py +0 -0
  18. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/demo/visualize_empirical_margin_distribution.py +0 -0
  19. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/gen/data_generation.py +0 -0
  20. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/gen/enforcement_stats.py +0 -0
  21. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/gen/upp_tests.py +0 -0
  22. {mergeron-2025.739290.9 → mergeron-2025.739319.0}/src/mergeron/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mergeron
3
- Version: 2025.739290.9
3
+ Version: 2025.739319.0
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.9"
16
+ version = "2025.739319.0"
17
17
 
18
18
  # Classifiers list: https://pypi.org/classifiers/
19
19
  classifiers = [
@@ -12,7 +12,7 @@ from ruamel import yaml
12
12
 
13
13
  _PKG_NAME: str = Path(__file__).parent.stem
14
14
 
15
- VERSION = "2025.739290.9"
15
+ VERSION = "2025.739319.0"
16
16
 
17
17
  __version__ = VERSION
18
18
 
@@ -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
- ) -> MappingProxyType[str, MappingProxyType[str, float | int]]:
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
- zipfile.ZipFile(data_archive_path) as _yzip,
83
- _yzip.open(f"{data_archive_path.stem}.yaml") as _yfh,
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: Mapping[str, Mapping[str, float | int]] | None = None, /
158
- ) -> tuple[ArrayDouble, 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
- margin_data_obs,
199
- margin_data_wts,
200
- np.round(
201
- (
202
- margin_wtd_avg,
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
- _sample_size: int | tuple[int, ...] = (10**6, 2),
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
  """
@@ -238,24 +232,26 @@ def margin_data_resampler(
238
232
 
239
233
  """
240
234
 
235
+ _dist_parms = margin_data_builder()[0] if _dist_parms is None else _dist_parms
236
+
241
237
  _seed = seed_sequence or SeedSequence(pool_size=8)
242
238
 
243
- _x, _w, _ = margin_data_builder(margin_data_getter())
239
+ _x, _w = _dist_parms[:, 0], _dist_parms[:, 1]
244
240
 
245
241
  margin_kde = stats.gaussian_kde(_x, weights=_w, bw_method="silverman")
246
242
  margin_kde.set_bandwidth(bw_method=margin_kde.factor / 3.0)
247
243
 
248
- if isinstance(_sample_size, int):
244
+ if isinstance(sample_size, int):
249
245
  return np.array(
250
- margin_kde.resample(_sample_size, seed=Generator(PCG64DXSM(_seed)))[0]
246
+ margin_kde.resample(sample_size, seed=Generator(PCG64DXSM(_seed)))[0]
251
247
  )
252
- elif isinstance(_sample_size, tuple) and len(_sample_size) == 2:
253
- _ssz, _ncol = _sample_size
254
- ret_array = np.empty(_sample_size, float)
248
+ elif isinstance(sample_size, tuple) and len(sample_size) == 2:
249
+ _ssz, _ncol = sample_size
250
+ ret_array = np.empty(sample_size, float)
255
251
  for idx, _col_seed in enumerate(_seed.spawn(_ncol)):
256
252
  ret_array[:, idx] = margin_kde.resample(
257
253
  _ssz, seed=Generator(PCG64DXSM(_col_seed))
258
254
  )[0]
259
255
  return ret_array
260
256
  else:
261
- raise ValueError(f"Invalid sample size: {_sample_size!r}")
257
+ 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] | ArrayFloat | None, _i: PCMSpec
311
- ) -> ArrayFloat | None:
312
- if _i.dist_type == PCMDistribution.EMPR:
313
- return None
314
- elif _v is None or len(_v) == 0 or np.array_equal(_v, DEFAULT_DIST_PARMS):
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, Sequence of floats, or Numpy ndarray."
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 | None = field(
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(_i: PCMSpec) -> ArrayFloat | None:
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, _a: Attribute[ArrayFloat | None], _v: ArrayFloat | None
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 _v is not None and _i.dist_type == PCMDistribution.EMPR:
406
+ elif (
407
+ _i.dist_type == PCMDistribution.EMPR
408
+ and not isinstance(_v, np.ndarray)
409
+ ):
395
410
  raise ValueError(
396
- f"Empirical distribution does not require additional parameters; "
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)
@@ -671,16 +671,16 @@ def _gen_margin_data(
671
671
  )
672
672
 
673
673
  pcm_array = (
674
- np.empty((len(_frmshr_array), 1), float)
674
+ np.empty_like(_frmshr_array[:, :1])
675
675
  if _pcm_spec.firm2_pcm_constraint == FM2Constraint.SYM
676
- else np.empty_like(_frmshr_array, float)
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"]