mergeron 2025.739290.7__py3-none-any.whl → 2025.739319.0__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,7 +12,7 @@ from ruamel import yaml
12
12
 
13
13
  _PKG_NAME: str = Path(__file__).parent.stem
14
14
 
15
- VERSION = "2025.739290.7"
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,26 +232,26 @@ def margin_data_resampler(
238
232
 
239
233
  """
240
234
 
241
- seed_sequence_ = seed_sequence or SeedSequence(pool_size=8)
235
+ _dist_parms = margin_data_builder()[0] if _dist_parms is None else _dist_parms
242
236
 
243
- _x, _w, _ = margin_data_builder(margin_data_getter())
237
+ _seed = seed_sequence or SeedSequence(pool_size=8)
238
+
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(
251
- _sample_size, seed=Generator(PCG64DXSM(seed_sequence_))
252
- )[0]
246
+ margin_kde.resample(sample_size, seed=Generator(PCG64DXSM(_seed)))[0]
253
247
  )
254
- elif isinstance(_sample_size, tuple) and len(_sample_size) == 2:
255
- ssz, num_cols = _sample_size
256
- ret_array = np.empty(_sample_size, np.float64)
257
- for idx, seed_seq in enumerate(seed_sequence_.spawn(num_cols)):
248
+ elif isinstance(sample_size, tuple) and len(sample_size) == 2:
249
+ _ssz, _ncol = sample_size
250
+ ret_array = np.empty(sample_size, float)
251
+ for idx, _col_seed in enumerate(_seed.spawn(_ncol)):
258
252
  ret_array[:, idx] = margin_kde.resample(
259
- ssz, seed=Generator(PCG64DXSM(seed_seq))
253
+ _ssz, seed=Generator(PCG64DXSM(_col_seed))
260
254
  )[0]
261
255
  return ret_array
262
256
  else:
263
- raise ValueError(f"Invalid sample size: {_sample_size!r}")
257
+ raise ValueError(f"Invalid sample size: {sample_size!r}")
@@ -243,7 +243,7 @@ def _construct_no_evidence_data(_invdata: INVData_in, _data_period: str, /) -> N
243
243
  dtn: INVTableData(
244
244
  invdata_ind_grp,
245
245
  invdata_evid_cond,
246
- np.column_stack((
246
+ np.hstack((
247
247
  invdata_sub_evid_cond_conc[stn0].data_array[:, :2],
248
248
  (
249
249
  invdata_sub_evid_cond_conc[stn0].data_array[:, 2:]
@@ -377,10 +377,10 @@ def _construct_new_period_data(
377
377
  np.zeros_like(invdata_array_bld_enfcls),
378
378
  )).max(axis=0)
379
379
 
380
- invdata_array_bld = np.column_stack((
380
+ invdata_array_bld = np.hstack((
381
381
  invdata_cuml_array[:, :-3],
382
382
  invdata_array_bld_enfcls,
383
- np.einsum("ij->i", invdata_array_bld_enfcls),
383
+ np.einsum("ij->i", invdata_array_bld_enfcls)[:, None],
384
384
  ))
385
385
 
386
386
  data_typesubdict[table_no] = INVTableData(
@@ -401,7 +401,7 @@ def invdata_build_aggregate_table(
401
401
  return INVTableData(
402
402
  "Industries in Common",
403
403
  "Unrestricted on additional evidence",
404
- np.column_stack((
404
+ np.hstack((
405
405
  _data_typesub[hdr_table_no].data_array[:, :-3],
406
406
  np.einsum(
407
407
  "ijk->jk",
@@ -99,7 +99,7 @@ def hhi_delta_boundary(
99
99
 
100
100
  # Boundary points
101
101
  half_bdry = np.vstack((
102
- np.column_stack((_s_1, _delta_bound / (2 * _s_1))).astype(float),
102
+ np.stack((_s_1, _delta_bound / (2 * _s_1)), axis=1).astype(float),
103
103
  np.array([(mpf("0.0"), mpf("1.0"))], float),
104
104
  ))
105
105
  bdry = np.vstack((half_bdry[::-1], half_bdry[1:, ::-1]), dtype=float)
@@ -132,7 +132,7 @@ def hhi_pre_contrib_boundary(
132
132
  # Range-limit is 0 less a step, which is -1 * step-size
133
133
  s_1 = np.array(mp.arange(_s_mid, -step_size, -step_size))
134
134
  s_2 = np.sqrt(_hhi_bound - s_1**2)
135
- half_bdry = np.column_stack((s_1, s_2)).astype(float)
135
+ half_bdry = np.stack((s_1, s_2), axis=1).astype(float)
136
136
 
137
137
  return GuidelinesBoundary(
138
138
  np.vstack((half_bdry[::-1], half_bdry[1:, ::-1]), dtype=float),
@@ -536,7 +536,7 @@ def shrratio_boundary_xact_avg( # noqa: PLR0914
536
536
  )
537
537
  )
538
538
 
539
- bdry_inner = np.column_stack((_s_1, s_2))
539
+ bdry_inner = np.stack((_s_1, s_2), axis=1)
540
540
  bdry_end = np.array([(mpf("0.0"), _s_intcpt)], float)
541
541
 
542
542
  bdry = np.vstack((
@@ -506,7 +506,7 @@ def shrratio_boundary_xact_avg_mp( # noqa: PLR0914
506
506
  )
507
507
  )
508
508
 
509
- bdry_inner = np.column_stack((_s_1, s_2))
509
+ bdry_inner = np.stack((_s_1, s_2), axis=1)
510
510
  bdry_end = np.array([(mpf("0.0"), s_intcpt)])
511
511
 
512
512
  bdry = np.vstack((
mergeron/gen/__init__.py CHANGED
@@ -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)
@@ -27,6 +27,7 @@ from ..core import guidelines_boundaries as gbl # noqa: TID252
27
27
  from ..core.guidelines_boundaries import HMGThresholds # noqa: TID252
28
28
  from . import (
29
29
  FM2Constraint,
30
+ INVResolution, # noqa: F401
30
31
  MarketSampleData,
31
32
  PCMDistribution,
32
33
  PCMSpec,
@@ -396,7 +397,7 @@ class MarketSample:
396
397
  for _k in ("by_firm_count", "by_delta", "by_conczone")
397
398
  ])
398
399
  upp_test_results = UPPTestsCounts(*[
399
- np.column_stack((
400
+ np.hstack((
400
401
  (_gv := getattr(res_list_stacks, _g.name))[0, :, :_h],
401
402
  np.einsum("ijk->jk", _gv[:, :, _h:], dtype=np.int64),
402
403
  ))
@@ -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"]
@@ -722,7 +722,7 @@ def _gen_margin_data(
722
722
  del beta_min, beta_max
723
723
 
724
724
  if dist_firm2_pcm == FM2Constraint.SYM:
725
- pcm_array = np.column_stack((pcm_array,) * _frmshr_array.shape[1])
725
+ pcm_array = np.hstack((pcm_array,) * _frmshr_array.shape[1])
726
726
  if dist_firm2_pcm == FM2Constraint.MNL:
727
727
  # Impose FOCs from profit-maximization with MNL demand
728
728
  if dist_type_pcm == PCMDistribution.EMPR:
@@ -193,7 +193,7 @@ def enf_cnts_obs_byfirmcount(
193
193
  case INVResolution.BOTH:
194
194
  stats_kept_indxs = [-1, -3, -2]
195
195
 
196
- return np.column_stack([cnts_array[:, :ndim_in], cnts_array[:, stats_kept_indxs]])
196
+ return np.hstack([cnts_array[:, :ndim_in], cnts_array[:, stats_kept_indxs]])
197
197
 
198
198
 
199
199
  def enf_cnts_obs_byhhianddelta(
@@ -226,7 +226,7 @@ def enf_cnts_obs_byhhianddelta(
226
226
  case INVResolution.BOTH:
227
227
  stats_kept_indxs = [-1, -3, -2]
228
228
 
229
- return np.column_stack([cnts_array[:, :ndim_in], cnts_array[:, stats_kept_indxs]])
229
+ return np.hstack([cnts_array[:, :ndim_in], cnts_array[:, stats_kept_indxs]])
230
230
 
231
231
 
232
232
  def table_no_lku(
@@ -335,10 +335,10 @@ def enf_cnts_byconczone(_cnts_array: ArrayBIGINT, /) -> ArrayBIGINT:
335
335
  # Logical-and of multiple vectors:
336
336
  hhi_zone_test = (
337
337
  1
338
- * np.column_stack([
338
+ * np.stack([
339
339
  cnts_byhhipostanddelta[:, _idx] == _val
340
340
  for _idx, _val in enumerate(zone_val)
341
- ])
341
+ ], axis=1)
342
342
  ).prod(axis=1) == 1
343
343
 
344
344
  cnts_byconczone = np.vstack((
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.3
2
+ Name: mergeron
3
+ Version: 2025.739319.0
4
+ Summary: Analyze merger enforcement policy using Python
5
+ License: MIT
6
+ Keywords: merger policy analysis,merger guidelines,merger screening,policy presumptions,concentration standards,upward pricing pressure,GUPPI
7
+ Author: Murthy Kambhampaty
8
+ Author-email: smk@capeconomics.com
9
+ Requires-Python: >=3.12,<4.0
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: Implementation :: CPython
22
+ Requires-Dist: aenum (>=3.1.15,<4.0.0)
23
+ Requires-Dist: attrs (>=23.2)
24
+ Requires-Dist: bs4 (>=0.0.1)
25
+ Requires-Dist: certifi (>=2023.11.17)
26
+ Requires-Dist: h5py (>=3.13.0,<4.0.0)
27
+ Requires-Dist: jinja2 (>=3.1)
28
+ Requires-Dist: joblib (>=1.3)
29
+ Requires-Dist: matplotlib (>=3.8)
30
+ Requires-Dist: mpmath (>=1.3)
31
+ Requires-Dist: msgpack (>=1.0)
32
+ Requires-Dist: msgpack-numpy (>=0.4)
33
+ Requires-Dist: ruamel-yaml (>=0.18.10,<0.19.0)
34
+ Requires-Dist: scipy (>=1.12)
35
+ Requires-Dist: sympy (>=1.12)
36
+ Requires-Dist: types-beautifulsoup4 (>=4.11.2)
37
+ Requires-Dist: urllib3 (>=2.2.2,<3.0.0)
38
+ Requires-Dist: xlrd (>=2.0.1,<3.0.0)
39
+ Requires-Dist: xlsxwriter (>=3.1)
40
+ Description-Content-Type: text/x-rst
41
+
42
+ mergeron: Merger Policy Analysis with Python
43
+ ============================================
44
+
45
+ Usage
46
+ -----
47
+
48
+ *Visualizing Guidelines boundaries*
49
+
50
+ .. code:: python
51
+
52
+ %matplotlib inline
53
+ from mergeron.core import guidelines_boundaries as gbl
54
+ from mergeron.core import guidelines_boundary_functions as gbf
55
+ from math import sqrt
56
+
57
+ delta_bound = 0.01
58
+ conc_boundary = gbl.ConcentrationBoundary(delta_bound, "ΔHHI")
59
+ share_boundary = gbl.ConcentrationBoundary(2 * sqrt(delta_bound / 2), "Combined share")
60
+
61
+ divr_boundary_a = gbl.DiversionRatioBoundary(
62
+ gbl.guppi_from_delta(delta_bound, m_star=1.0, r_bar=0.85),
63
+ agg_method=gbl.UPPAggrSelector.AVG
64
+ )
65
+
66
+ divr_boundary_i = gbl.DiversionRatioBoundary(
67
+ gbl.guppi_from_delta(delta_bound, m_star=1.0, r_bar=0.85),
68
+ agg_method=gbl.UPPAggrSelector.MIN
69
+ )
70
+
71
+ divr_boundary_x = gbl.DiversionRatioBoundary(
72
+ gbl.guppi_from_delta(delta_bound, m_star=1.0, r_bar=0.85),
73
+ agg_method=gbl.UPPAggrSelector.MAX
74
+ )
75
+
76
+
77
+ Plots are written to PDF, typically, with ``backend="pgf"`` as the
78
+ default backend in the function, ``gbf.boundary_plot``. Here, we set the
79
+ backend to ``None`` to skip fine-tuning plots for PDF generation.
80
+
81
+ .. code:: python
82
+
83
+ plt, fig, ax, layout_axis = gbf.boundary_plot(backend=None)
84
+
85
+ ax.set_title("Concentration and Diversion Ratio Boundaries")
86
+
87
+ ax.plot(conc_boundary.coordinates[:, 0], conc_boundary.coordinates[:, 1], color="black", linestyle="-", label="ΔHHI")
88
+ ax.plot(share_boundary.coordinates[:, 0], share_boundary.coordinates[:, 1], color="black", linestyle=":", label="Combined share")
89
+ ax.plot(divr_boundary_a.coordinates[:, 0], divr_boundary_a.coordinates[:, 1], "b-", label="Average Diversion Ratio")
90
+ ax.plot(divr_boundary_i.coordinates[:, 0], divr_boundary_i.coordinates[:, 1], "r-", label="Minimum Diversion Ratio")
91
+ ax.plot(divr_boundary_x.coordinates[:, 0], divr_boundary_x.coordinates[:, 1], "g-", label="Maximum Diversion Ratio")
92
+
93
+ _ = fig.legend(loc=(0.4, 0.7), frameon=False)
94
+
95
+
96
+
97
+ .. image:: ./docs/readme_content/output_5_0.png
98
+
99
+
100
+ *Analyzing FTC Merger Investigations Data*
101
+
102
+ .. code:: python
103
+
104
+ from mergeron.core import ftc_merger_investigations_data as fid
105
+ import tabulate
106
+
107
+ inv_data = fid.construct_data(fid.INVDATA_ARCHIVE_PATH)
108
+
109
+
110
+ We can now analyze counts of markets reported in the source data, by
111
+ table number. Note that odd-numbered tables report FTC investigations
112
+ data organized by HHI and ΔHHI, while even-numbered tables report by
113
+ firm-count.
114
+
115
+ .. code:: python
116
+
117
+ from mergeron.gen import enforcement_stats as esl
118
+
119
+ print("Enforcement Rates in Markets with Entry Barriers, 1996-2003 vs 2004-2011")
120
+ print()
121
+ counts_by_delta_1 = esl.enf_cnts_bydelta(
122
+ inv_data["1996-2003"]["ByHHIandDelta"]["Table 9.2"].data_array
123
+ )
124
+ counts_by_delta_2 = esl.enf_cnts_bydelta(
125
+ inv_data["2004-2011"]["ByHHIandDelta"]["Table 9.2"].data_array
126
+ )
127
+ observed_enforcement_rates = list(zip(
128
+ (
129
+ {_v: _k for _k, _v in fid.CONC_DELTA_DICT.items()}[i]
130
+ for i in counts_by_delta_1[:, 0]
131
+ ),
132
+ (
133
+ f"{_a[1] / _a[-1]: <12.2%}" if _a[-1] else "--"
134
+ for _a in counts_by_delta_1
135
+ ),
136
+ (
137
+ f"{_e[1] / _e[-1]: <12.2%}" if _e[-1] else "--"
138
+ for _e in counts_by_delta_2
139
+ ),
140
+ ))
141
+
142
+ observed_enforcement_rates.append([
143
+ "Total",
144
+ f"{counts_by_delta_1[:, 1].sum() / counts_by_delta_1[:, -1].sum(): <12.2%}",
145
+ f"{counts_by_delta_2[:, 1].sum() / counts_by_delta_2[:, -1].sum(): <12.2%}",
146
+ ])
147
+
148
+ print(tabulate.tabulate(
149
+ observed_enforcement_rates,
150
+ tablefmt="simple",
151
+ headers=("ΔHHI", "1996-2003", "2004-2011"),
152
+ stralign="center",
153
+ maxcolwidths=36,
154
+ maxheadercolwidths=36,
155
+ ))
156
+
157
+
158
+ .. parsed-literal::
159
+
160
+ Enforcement Rates in Markets with Entry Barriers, 1996-2003 vs 2004-2011
161
+
162
+ ΔHHI 1996-2003 2004-2011
163
+ ------------- ----------- -----------
164
+ 0 - 100 -- 100.00%
165
+ 100 - 200 33.33% 50.00%
166
+ 200 - 300 33.33% 50.00%
167
+ 300 - 500 75.00% 77.78%
168
+ 500 - 800 59.09% 54.55%
169
+ 800 - 1,200 93.33% 81.82%
170
+ 1,200 - 2,500 90.91% 84.38%
171
+ 2,500 + 96.00% 100.00%
172
+ Total 81.65% 82.86%
173
+
174
+
175
+ Generating synthetic market data and analyzing enforcement rates
176
+
177
+
178
+
@@ -0,0 +1,22 @@
1
+ mergeron/__init__.py,sha256=Wd2SFL2g3DbyGBAv7la2ELFnxs6EQBHawwDVLgqg808,5549
2
+ mergeron/core/__init__.py,sha256=BzL_bXHyOQG8cvo76OP3K48LkeHQCJQN7ZFPRhoOdcE,2850
3
+ mergeron/core/empirical_margin_distribution.py,sha256=O-GD5X86tqf4kznELBzIcICueQxcL1igxHLJR9E7cU8,9229
4
+ mergeron/core/ftc_merger_investigations_data.py,sha256=DYqtyxGPnpGyaWbQ8dPHmHmQBOeHbmU5h9snk-YwSN4,28575
5
+ mergeron/core/guidelines_boundaries.py,sha256=srCEWzSuv7cDFCf-ity-9C0NtFCdZznn5dgiUS9Ndpo,15246
6
+ mergeron/core/guidelines_boundary_functions.py,sha256=wQdIQcEga888vVe3cAwYsDa7HTaxgqe3RbV48UklmzQ,29064
7
+ mergeron/core/guidelines_boundary_functions_extra.py,sha256=i2CmEpYRUVnMbPakSjlyWodMP11JW-rkVImhKOQlV6g,22355
8
+ mergeron/core/pseudorandom_numbers.py,sha256=YqcVwU-Pgc0F_pKzG9Osn14RnIuYOwE-q7GVDpCUtpI,9998
9
+ mergeron/data/__init__.py,sha256=4yOOvERJ28JIT5KRkIa_t2y9aYmuFdStPM4P38BsufM,1806
10
+ mergeron/data/damodaran_margin_data.xls,sha256=Qggl1p5nkOMJI8YUXhkwXQRz-OhRSqBTzz57N0JQyYA,79360
11
+ mergeron/data/ftc_merger_investigations_data.zip,sha256=tiB2TLFyS9LMSFIv8DBA_oEEx12DU4MyjHni4NlsRMU,24002
12
+ mergeron/demo/__init__.py,sha256=KtjBlZOl7jwBCAUhrTJB9PdrN39YLYytNiSUSM_gRmA,62
13
+ mergeron/demo/visualize_empirical_margin_distribution.py,sha256=17awsa188r7uVDJuHuCWTYwlQbfaq4n8HEHF5jK-0Ic,2532
14
+ mergeron/gen/__init__.py,sha256=JD8hP5QVgNTlGTwBnozN0xHg97mXn99WckCuFfVcCNQ,23071
15
+ mergeron/gen/data_generation.py,sha256=ED0ShgPL0sgJceLzJu1BcNs8yC-EBzNGHW1oj8_g_so,17092
16
+ mergeron/gen/data_generation_functions.py,sha256=VySWh-Jvnj0zSVvkq12ck3SUFJ-42udODKzeop2ZWvs,26418
17
+ mergeron/gen/enforcement_stats.py,sha256=UrsZWZNYy7DXWsCu96qmOQINipFbf9qLX0W8iNGgE_Y,11073
18
+ mergeron/gen/upp_tests.py,sha256=Czub4njLESkV5LRwb1lByEI4nHBK0AwT96LYp_bG12s,6968
19
+ mergeron/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
20
+ mergeron-2025.739319.0.dist-info/METADATA,sha256=7foBaEl6Nmoi5QFLnQSG45O1-JJROXXlyVxG0qfFS7E,6107
21
+ mergeron-2025.739319.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
22
+ mergeron-2025.739319.0.dist-info/RECORD,,
@@ -1,115 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: mergeron
3
- Version: 2025.739290.7
4
- Summary: Analyze merger enforcement policy using Python
5
- License: MIT
6
- Keywords: merger policy analysis,merger guidelines,merger screening,policy presumptions,concentration standards,upward pricing pressure,GUPPI
7
- Author: Murthy Kambhampaty
8
- Author-email: smk@capeconomics.com
9
- Requires-Python: >=3.12,<4.0
10
- Classifier: Development Status :: 4 - Beta
11
- Classifier: Environment :: Console
12
- Classifier: Intended Audience :: End Users/Desktop
13
- Classifier: Intended Audience :: Science/Research
14
- Classifier: License :: OSI Approved :: MIT License
15
- Classifier: Operating System :: OS Independent
16
- Classifier: Programming Language :: Python
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Programming Language :: Python :: 3.13
20
- Classifier: Programming Language :: Python :: 3 :: Only
21
- Classifier: Programming Language :: Python :: Implementation :: CPython
22
- Requires-Dist: aenum (>=3.1.15,<4.0.0)
23
- Requires-Dist: attrs (>=23.2)
24
- Requires-Dist: bs4 (>=0.0.1)
25
- Requires-Dist: certifi (>=2023.11.17)
26
- Requires-Dist: h5py (>=3.13.0,<4.0.0)
27
- Requires-Dist: jinja2 (>=3.1)
28
- Requires-Dist: joblib (>=1.3)
29
- Requires-Dist: matplotlib (>=3.8)
30
- Requires-Dist: mpmath (>=1.3)
31
- Requires-Dist: msgpack (>=1.0)
32
- Requires-Dist: msgpack-numpy (>=0.4)
33
- Requires-Dist: ruamel-yaml (>=0.18.10,<0.19.0)
34
- Requires-Dist: scipy (>=1.12)
35
- Requires-Dist: sympy (>=1.12)
36
- Requires-Dist: types-beautifulsoup4 (>=4.11.2)
37
- Requires-Dist: urllib3 (>=2.2.2,<3.0.0)
38
- Requires-Dist: xlrd (>=2.0.1,<3.0.0)
39
- Requires-Dist: xlsxwriter (>=3.1)
40
- Description-Content-Type: text/x-rst
41
-
42
- mergeron: Merger Policy Analysis using Python
43
- =============================================
44
-
45
- Visualize the sets of mergers conforming to concentration and diversion-ratio standards. Estimate intrinsic enforcement rates, and intrinsic clearance rates, under concentration, diversion ratio, GUPPI, CMCR, and IPR bounds using generated data with specified distributions of market shares, price-cost margins, firm counts, and prices, optionally imposing restrictions implied by statutory filing thresholds and/or Bertrand-Nash oligopoly with MNL demand. Download and analyze merger investigations data published by the U.S. Federal Trade Commission in various reports on extended merger investigations (Second Requests) during 1996 to 2011.
46
-
47
- Here, enforcement rates derived with merger enforcement as being exogenous to firm conduct are defined as intrinsic enforcement rates, and similarly intrinsic clearance rates. Depending on the merger enforcement regime, or merger control regime, intrinsic enforcement rates may also not be the complement of intrinsic clearance rates, i.e, it is not necessarily true that the intrinsic clearance rate estimate for a given enforcement regime is 1 minus the intrinsic enforcement rate. In contrast, observed enforcement rates reflect the deterrent effects of merger enforcement on firm conduct as well as the effects of merger screening on the level of enforcement; and, by definition, the observed clearance rate is 1 minus the observed enforcement rate.
48
-
49
- Introduction
50
- ------------
51
-
52
- Module :code:`.core.guidelines_boundaries` includes classes for specifying concentration bounds (:code:`.core.guidelines_boundaries.ConcentrationBoundary`) and diversion-ratio bounds (:code:`.core.guidelines_boundaries.DiversionRatioBoundary`), with automatic generation of boundary, as an array of share-pairs, and area. This module also includes a function for generating plots of concentration and diversion-ratio boundaries, and functions for mapping GUPPI standards to concentration (ΔHHI) standards, and vice-versa.
53
-
54
- Module :code:`.gen.data_generation` includes the :code:`.gen.data_generation.MarketSample` which provides for a rich specification of shares and diversion ratios (:code:`.gen.data_generation.MarketSample.share_spec`), margins (:code:`.gen.data_generation.MarketSample.pcm_spec`, prices (:code:`.gen.data_generation.MarketSample.price_spec`), and HSR filing requirements (:code:`.gen.data_generation.MarketSample.hsr_filing_test_type`), and with methods for, (i) generating sample data (:code:`.gen.data_generation.MarketSample.generate_sample`), and (ii) computing the intrinsic enforcement rate and intrinsic clearance rate for the generated sample, given a method (:code:`.UPPAggrSelector`) of aggregating diversion ratio or GUPPI estimates for the firms in a merger (:code:`.gen.data_generation.MarketSample.estimate_enf_counts`). While the latter populate the properties, :code:`.gen.data_generation.MarketSample.data`
55
- and :code:`.gen.data_generation.MarketSample.enf_counts`, respectively, the underlying methods for generating standalone :code:`MarketDataSample` and :code:`UPPTestCounts` objects are included in the class definition, with helper functions defined in the modules, :code:`.gen.data_generation_functions` and :code:`.gen.upp_tests`. Notably, market shares are generated for a sample of markets with firm-count distributed as specified in :code:`.gen.data_generation.MarketSample.share_spec.firm_count_weights`, with defaults as discussed below (also see, :code:`.gen.ShareSpec.firm_count_weights`.
56
-
57
- By default, merging-firm shares are drawn with uniform distribution over the space :math:`s_1 + s_2 \leqslant 1` for an unspecified number of firms. Alternatively, shares may be drawn from the Dirichlet distribution (see property `dist_type` of :code:`.gen.data_generation.MarketSample.share_spec`, of type, :code:`.gen.SHRDistribution`), with specified shape parameters (property `dist_parms` of :code:`.gen.data_generation.MarketSample.share_spec`. When drawing shares from the Dirichlet distribution, the user specifies the `firm_count_weights` property of :code:`.gen.data_generation.MarketSample.share_spec`, as a vector of weights specifying the frequency distribution over sequential firm counts, e.g., :code:`[133, 184, 134, 52, 32, 10, 12, 4, 3]` to specify shares drawn from Dirichlet distributions with 2 to 10 pre-merger firms distributed as in data for FTC merger investigations during 1996--2003 (See, for example, Table 4.1 of `FTC, Horizontal Merger Investigations Data, Fiscal Years 1996--2003 (Revised: August 31, 2004) <https://www.ftc.gov/sites/default/files/documents/reports/horizontal-merger-investigation-data-fiscal-years-1996-2003/040831horizmergersdata96-03.pdf>`_). If the property `firm_count_weights` is not explicitly assigned a value when defining :code:`.gen.data_generation.MarketSample.share_spec`, the default values is used, which results in a sample of markets with 2 to 7 firms with relative frequency in inverse proportion to firm-count, with 2-firm markets being 6 times as likely to be drawn as 7-firm markets.
58
-
59
- Recapture ratios can be specified as, "proportional", "inside-out", or "outside-in" (see :code:`.RECForm`). The "inside-out" specification (assigning :code:`.RECForm.INOUT` to the `recapture_form` property of :code:`.gen.data_generation.MarketSample.share_spec`) results in recapture ratios consistent with MNL demand, given merging-firms' in-market shares and a default recapture ratio. The "outside-in" specification (assigning :code:`.RECForm.INOUT` to the `recapture_form` property of :code:`.gen.data_generation.MarketSample.share_spec`) yields diversion ratios from purchase probabilities drawn at random for :math:`N+1` goods, with market shares and recapture ratios for the :math:`N` goods in the putative market (see, :code:`.gen.ShareSpec`) computed from the simulated choice probabilities. The "outside-in" specification requires specification of the distribution of markets over firm counts (the default being uniform distirbution over markets with 2 to 7 firms pre-merger), and Dirichlet-distributed shares, with optional parameters (the default being a "flat" Dirichlet distribution, i.e., one with all parameters being 1). The parameters of the Dirichlet distribution can, for example, be specified to increase (decrease) the probability of drawing mergers to monopoly relative to that probability associated with the Flat Dirichlet specification, by setting the first 2 specified parameters at higher (lower) values relative to the others. Lastly, the "proportional" form of recapture ratio (`recapture_form` = :code:`.RECForm.FIXED`) is often used in the literature, as an approximation to the "inside-out" calibration. See, for example, Coate (2011).
60
-
61
- Price-cost-margins may be specified as having uniform distribution, Beta distribution (including a bounded Beta distribution with specified mean and variance), or a built-in empirical distribution (see, :code:`.gen.PCMSpec`). The in-built empirical margin distribution is based on resampling margin data published by Prof. Damodaran of NYU Stern School of Business (see Notes), using an estimated Gaussian KDE. The second merging firm's margin (per the property `firm2_pcm_constraint` of :code:`.gen.data_generation.MarketSample.pcm_spec`) may be specified as symmetric, i.i.d., or subject to equilibrium conditions for (profit-maximization in) Bertrand-Nash oligopoly with MNL demand (:code:`.gen.FM2Constraint`).
62
-
63
- 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 (see, :code:`.gen.PriceSpec`). Prices may also be defined by imposing cost symmetry on firms in the sample, with fixed unit marginal costs normalized to 1 unit, such that prices equal :math:`1 / (1 - \pmb{m})`, where :math:`\pmb{m}` represents the array of margins for firms in the sample.
64
-
65
- The market sample may be restricted to mergers meeting the HSR filing requirement under two alternative approaches: in the one, the smaller of the two merging firms meets the lower HSR size threshold ($10 million, as adjusted) and the larger of the two merging firms meets the size test if it's share is no less than 10 times the share of the smaller firm. In the other, the :math:`n`-th firm's size is maintained as $10 million, as adjusted (see, :code:`.gen.SSZConstant`), and a merger meets the HSR filing test if either, (a.) the smaller merging firm is no smaller than the n-th firm and the larger merging firm is at 10-times as large as the n-th firm, or (b.) the smaller merging firm's market share is in excess of 10%; in effect this version of the test maintains that if the smaller merging firm's market share exceeds 10%, the value of the transaction exceeds $200 million, as adjusted, and the size-of-person test is eliminated (see, FTC (2008, p. 12); the above are simplifications of the statutory HSR filing requirements). The second assumption avoids the unfortunate assumption in the first that, within the resulting sample, the larger merging firm be at least 10 times as large as the smaller merging firm, as a consequence of the full definition of the HSR filing requirement.
66
-
67
- The full specification of a market sample is given in a :code:`.gen.data_generation.MarketSample` object, including the above parameters. Data are drawn by invoking :code:`.gen.data_generation.MarketSample.generate_sample` which adds a :code:`data` property of class, :code:`.gen.MarketDataSample`. Enforcement or clearance counts are computed by invoking :code:`.gen.data_generation.MarketSample.estimate_enf_counts`, which adds an :code:`enf_counts` property of class :code:`.gen.UPPTestsCounts`. For fast, parallel generation of enforcement or clearance counts over large market data samples that ordinarily would exceed available limits on machine memory, the user can invoke the method :code:`.gen.data_generation.MarketSample.estimate_enf_counts` on a :code:`.gen.data_generation.MarketSample` object without first invoking :code:`.gen.data_generation.MarketSample.generate_sample`. Note, however, that this strategy does not retain the market sample in memory in the interests of conserving memory and maintaining high performance (the user can specify that the market sample and enforcement statistics be stored to permanent storage; when saving to current PCIe NVMe storage, the performance penalty is slight, but can be considerable if saving to SATA storage).
68
-
69
- Enforcement statistics based on FTC investigations data and test data are tabulated using methods provided in :code:`.gen.enforcement_stats`.
70
-
71
- Programs demonstrating the use of this package are included in the sub-package, :code:`.demo`.
72
-
73
- This package includes a class, :code:`.core.pseudorandom_numbers.MultithreadedRNG` for generating random numbers with selected continuous distribution over specified parameters, and with CPU multithreading on machines with multiple CPU cores, be they virtual, logical, or physical cores. This class is an adaptation from the documentation for the external :code:`numpy.random` subpackage, from the discussion on, "`Multithreaded generation <https://numpy.org/doc/stable/reference/random/multithreading.html>`_"; the version included here permits selection of the distribution with pre-tests to catch and inform on common errors. To access these directly:
74
-
75
- .. code-block:: python
76
-
77
- import mergeron.core.pseudorandom_numbers as prng
78
-
79
- Documentation for this package is in the form of the API Reference. Documentation for individual functions and classes is accessible within a python shell. For example:
80
-
81
- .. code-block:: python
82
-
83
- import mergeron.core.data_generation as dgl
84
-
85
- help(dgl.MarketSample)
86
-
87
- .. rubric:: References
88
-
89
- .. _coate2011:
90
-
91
- Coate, M. B. (2011). Benchmarking the upward pricing pressure model with Federal Trade
92
- Commission evidence. Journal of Competition Law & Economics, 7(4), 825--846. URL: https://doi.org/10.1093/joclec/nhr014.
93
-
94
- .. _ftc_premerger_guide2:
95
-
96
- FTC Premerger Notification Office. “To File or Not to File: When You Must File a Premerger Notification Report Form”. 2008 (September, revised). URL: https://www.ftc.gov/sites/default/files/attachments/premerger-introductory-guides/guide2.pdf
97
-
98
-
99
- .. image:: https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json
100
- :alt: Poetry
101
- :target: https://python-poetry.org/
102
-
103
- .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
104
- :alt: Ruff
105
- :target: https://github.com/astral-sh/ruff/
106
-
107
- .. image:: https://www.mypy-lang.org/static/mypy_badge.svg
108
- :alt: Checked with mypy
109
- :target: https://mypy-lang.org/
110
-
111
- .. image:: https://img.shields.io/badge/License-MIT-yellow.svg
112
- :alt: License: MIT
113
- :target: https://opensource.org/licenses/MIT/
114
-
115
-
@@ -1,22 +0,0 @@
1
- mergeron/__init__.py,sha256=36OCTzt0VcRWkgR3gwKrpsbhIijQnK9C0PNrm27Zpio,5549
2
- mergeron/core/__init__.py,sha256=BzL_bXHyOQG8cvo76OP3K48LkeHQCJQN7ZFPRhoOdcE,2850
3
- mergeron/core/empirical_margin_distribution.py,sha256=ktX0r5EHooXFkTed6iTzqkHw0DRv7KIDIcX3h2ukm2I,9313
4
- mergeron/core/ftc_merger_investigations_data.py,sha256=VP0qpBbr-CARD41U7VGVckmtq5qWgreEeop2pvUTmJU,28584
5
- mergeron/core/guidelines_boundaries.py,sha256=srCEWzSuv7cDFCf-ity-9C0NtFCdZznn5dgiUS9Ndpo,15246
6
- mergeron/core/guidelines_boundary_functions.py,sha256=jbGTwFXoHgsIQNp2ZotAsoy0Ja0bRoJMocBwWGpsylY,29061
7
- mergeron/core/guidelines_boundary_functions_extra.py,sha256=CwoYu6jvQFLq-9rYneDJjKu5MHG88WkICXT3e2zskss,22354
8
- mergeron/core/pseudorandom_numbers.py,sha256=YqcVwU-Pgc0F_pKzG9Osn14RnIuYOwE-q7GVDpCUtpI,9998
9
- mergeron/data/__init__.py,sha256=4yOOvERJ28JIT5KRkIa_t2y9aYmuFdStPM4P38BsufM,1806
10
- mergeron/data/damodaran_margin_data.xls,sha256=Qggl1p5nkOMJI8YUXhkwXQRz-OhRSqBTzz57N0JQyYA,79360
11
- mergeron/data/ftc_merger_investigations_data.zip,sha256=tiB2TLFyS9LMSFIv8DBA_oEEx12DU4MyjHni4NlsRMU,24002
12
- mergeron/demo/__init__.py,sha256=KtjBlZOl7jwBCAUhrTJB9PdrN39YLYytNiSUSM_gRmA,62
13
- mergeron/demo/visualize_empirical_margin_distribution.py,sha256=17awsa188r7uVDJuHuCWTYwlQbfaq4n8HEHF5jK-0Ic,2532
14
- mergeron/gen/__init__.py,sha256=JvyF3WyujbDzJNKtJb_GwqGwv7lydCkubsOMDsCND3w,22579
15
- mergeron/gen/data_generation.py,sha256=4EnNTOUEOVde-7SL2l0v8W2kDfKixnyKOChBajoMijk,17065
16
- mergeron/gen/data_generation_functions.py,sha256=UDh3B4FPwh4SxTdJs7-faLouf7cWUUjHarRkfJc9gjI,26408
17
- mergeron/gen/enforcement_stats.py,sha256=axojhpdjp_ovAWsL1ZX4q5tnO8FgwwRl9Hw3RU1tP-U,11084
18
- mergeron/gen/upp_tests.py,sha256=Czub4njLESkV5LRwb1lByEI4nHBK0AwT96LYp_bG12s,6968
19
- mergeron/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
20
- mergeron-2025.739290.7.dist-info/METADATA,sha256=RXKEm0CLvb4GkgE7NEA6U_1fR10JIsWxOiUkbhL_Kls,14512
21
- mergeron-2025.739290.7.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
22
- mergeron-2025.739290.7.dist-info/RECORD,,