mergeron 2024.739127.1__py3-none-any.whl → 2024.739145.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
@@ -8,7 +8,7 @@ from numpy.typing import NDArray
8
8
 
9
9
  _PKG_NAME: str = Path(__file__).parent.stem
10
10
 
11
- VERSION = "2024.739127.1"
11
+ VERSION = "2024.739145.0"
12
12
 
13
13
  __version__ = VERSION
14
14
 
@@ -21,14 +21,11 @@ If the subdirectory doesn't exist, it is created on package invocation.
21
21
  if not DATA_DIR.is_dir():
22
22
  DATA_DIR.mkdir(parents=False)
23
23
 
24
- np.set_printoptions(precision=18)
25
-
26
-
27
- type ArrayINT = NDArray[np.intp]
28
- type ArrayFloat = NDArray[np.half | np.single | np.double]
29
-
24
+ np.set_printoptions(precision=24, floatmode="fixed")
30
25
 
31
26
  type ArrayBoolean = NDArray[np.bool_]
27
+ type ArrayFloat = NDArray[np.half | np.single | np.double]
28
+ type ArrayINT = NDArray[np.intp]
32
29
 
33
30
  type ArrayDouble = NDArray[np.double]
34
31
  type ArrayBIGINT = NDArray[np.int64]
@@ -38,7 +35,7 @@ DEFAULT_REC_RATE = 0.85
38
35
 
39
36
  @enum.unique
40
37
  class RECForm(enum.StrEnum):
41
- """For derivation of recapture rate from market shares."""
38
+ """For derivation of recapture ratio from market shares."""
42
39
 
43
40
  INOUT = "inside-out"
44
41
  OUTIN = "outside-in"
mergeron/core/__init__.py CHANGED
@@ -1,3 +1,8 @@
1
+ from mpmath import mp # type: ignore
2
+
1
3
  from .. import VERSION # noqa: TID252
2
4
 
3
5
  __version__ = VERSION
6
+
7
+ type MPFloat = mp.mpf # pyright: ignore
8
+ type MPMatrix = mp.matrix # pyright: ignore
@@ -2,6 +2,9 @@
2
2
  Functions to parse margin data compiled by
3
3
  Prof. Aswath Damodaran, Stern School of Business, NYU.
4
4
 
5
+ Provides :func:`mgn_data_resampler` for generating margin data
6
+ from an estimated Gaussian KDE from the source (margin) data.
7
+
5
8
  Data are downloaded or reused from a local copy, on demand.
6
9
 
7
10
  For terms of use of Prof. Damodaran's data, please see:
@@ -103,7 +106,7 @@ def mgn_data_getter( # noqa: PLR0912
103
106
  if not _mgn_path.is_file():
104
107
  with resources.as_file(
105
108
  resources.files(f"{_PKG_NAME}.data").joinpath(
106
- "damodaran_margin_data.xls"
109
+ "empirical_margin_distribution.xls"
107
110
  )
108
111
  ) as _mgn_data_archive_path:
109
112
  shutil.copy2(_mgn_data_archive_path, _mgn_path)
@@ -129,7 +132,7 @@ def mgn_data_getter( # noqa: PLR0912
129
132
  _xl_row[1] = int(_xl_row[1])
130
133
  _mgn_dict[_xl_row[0]] = dict(zip(_mgn_row_keys[1:], _xl_row[1:], strict=True))
131
134
 
132
- _ = _data_archive_path.write_bytes(msgpack.packb(_mgn_dict))
135
+ _ = _data_archive_path.write_bytes(msgpack.packb(_mgn_dict)) # pyright: ignore
133
136
 
134
137
  return MappingProxyType(_mgn_dict)
135
138
 
@@ -218,7 +221,7 @@ def mgn_data_resampler(
218
221
  _x, _w, _ = mgn_data_builder(mgn_data_getter())
219
222
 
220
223
  _mgn_kde = stats.gaussian_kde(_x, weights=_w, bw_method="silverman")
221
- _mgn_kde.set_bandwidth(bw_method=_mgn_kde.factor / 3.0)
224
+ _mgn_kde.set_bandwidth(bw_method=_mgn_kde.factor / 3.0) # pyright: ignore
222
225
 
223
226
  if isinstance(_sample_size, int):
224
227
  return np.array(
@@ -96,10 +96,11 @@ class INVTableData(NamedTuple):
96
96
 
97
97
 
98
98
  type INVData = Mapping[str, Mapping[str, Mapping[str, INVTableData]]]
99
+ type _INVData_in = dict[str, dict[str, dict[str, INVTableData]]]
99
100
 
100
101
 
101
102
  def construct_data(
102
- _archive_path: Path | None = None,
103
+ _archive_path: Path = INVDATA_ARCHIVE_PATH,
103
104
  *,
104
105
  flag_backward_compatibility: bool = True,
105
106
  flag_pharma_for_exclusion: bool = True,
@@ -134,11 +135,11 @@ def construct_data(
134
135
  A dictionary of merger investigations data keyed to reporting periods
135
136
 
136
137
  """
137
- _archive_path = _archive_path or INVDATA_ARCHIVE_PATH
138
+
138
139
  if _archive_path.is_file() and not rebuild_data:
139
140
  _archived_data = msgpack.unpackb(_archive_path.read_bytes(), use_list=False)
140
141
 
141
- _invdata: dict[str, dict[str, dict[str, INVTableData]]] = {}
142
+ _invdata: _INVData_in = {}
142
143
  for _period in _archived_data:
143
144
  _invdata[_period] = {}
144
145
  for _table_type in _archived_data[_period]:
@@ -149,7 +150,7 @@ def construct_data(
149
150
  )
150
151
  return MappingProxyType(_invdata)
151
152
 
152
- _invdata = dict(_parse_invdata()) # Convert immutable to mutable
153
+ _invdata = dict(_parse_invdata()) # type: ignore # Convert immutable to mutable
153
154
 
154
155
  # Add some data periods (
155
156
  # only periods ending in 2011, others have few observations and
@@ -197,12 +198,12 @@ def construct_data(
197
198
  )
198
199
  }
199
200
 
200
- _ = INVDATA_ARCHIVE_PATH.write_bytes(msgpack.packb(_invdata))
201
+ _ = INVDATA_ARCHIVE_PATH.write_bytes(msgpack.packb(_invdata)) # pyright: ignore
201
202
 
202
203
  return MappingProxyType(_invdata)
203
204
 
204
205
 
205
- def _construct_no_evidence_data(_invdata: INVData, _data_period: str, /) -> None:
206
+ def _construct_no_evidence_data(_invdata: _INVData_in, _data_period: str, /) -> None:
206
207
  _invdata_ind_grp = "All Markets"
207
208
  _table_nos_map = dict(
208
209
  zip(
@@ -231,18 +232,20 @@ def _construct_no_evidence_data(_invdata: INVData, _data_period: str, /) -> None
231
232
  _stn0 = "Table 4.1" if _stats_grp == "ByFirmCount" else "Table 3.1"
232
233
  _stn1, _stn2 = (_dtn.replace(".X", f".{_i}") for _i in ("1", "2"))
233
234
 
234
- _invdata_sub_evid_cond_conc[_dtn] = INVTableData(
235
- _invdata_ind_grp,
236
- _invdata_evid_cond,
237
- np.column_stack((
238
- _invdata_sub_evid_cond_conc[_stn0].data_array[:, :2],
239
- (
240
- _invdata_sub_evid_cond_conc[_stn0].data_array[:, 2:]
241
- - _invdata_sub_evid_cond_conc[_stn1].data_array[:, 2:]
242
- - _invdata_sub_evid_cond_conc[_stn2].data_array[:, 2:]
243
- ),
244
- )),
245
- )
235
+ _invdata_sub_evid_cond_conc |= {
236
+ _dtn: INVTableData(
237
+ _invdata_ind_grp,
238
+ _invdata_evid_cond,
239
+ np.column_stack((
240
+ _invdata_sub_evid_cond_conc[_stn0].data_array[:, :2],
241
+ (
242
+ _invdata_sub_evid_cond_conc[_stn0].data_array[:, 2:]
243
+ - _invdata_sub_evid_cond_conc[_stn1].data_array[:, 2:]
244
+ - _invdata_sub_evid_cond_conc[_stn2].data_array[:, 2:]
245
+ ),
246
+ )),
247
+ )
248
+ }
246
249
 
247
250
 
248
251
  def _construct_new_period_data(
@@ -494,7 +497,7 @@ def _parse_invdata() -> INVData:
494
497
 
495
498
 
496
499
  def _parse_page_blocks(
497
- _invdata: INVData, _data_period: str, _doc_pg_blocks: Sequence[Sequence[Any]], /
500
+ _invdata: _INVData_in, _data_period: str, _doc_pg_blocks: Sequence[Sequence[Any]], /
498
501
  ) -> None:
499
502
  if _data_period != "1996-2011":
500
503
  _parse_table_blocks(_invdata, _data_period, _doc_pg_blocks)
@@ -521,7 +524,7 @@ def _parse_page_blocks(
521
524
 
522
525
 
523
526
  def _parse_table_blocks(
524
- _invdata: INVData, _data_period: str, _table_blocks: Sequence[Sequence[str]], /
527
+ _invdata: _INVData_in, _data_period: str, _table_blocks: Sequence[Sequence[str]], /
525
528
  ) -> None:
526
529
  _invdata_evid_cond = "Unrestricted on additional evidence"
527
530
  _table_num, _table_ser, _table_type = _identify_table_type(
@@ -20,12 +20,13 @@ from .. import ( # noqa: TID252
20
20
  RECForm,
21
21
  UPPAggrSelector,
22
22
  )
23
+ from . import MPFloat
23
24
  from . import guidelines_boundary_functions as gbfn
24
25
 
25
26
  __version__ = VERSION
26
27
 
27
28
 
28
- mp.prec = 80
29
+ mp.dps = 32
29
30
  mp.trap_complex = True
30
31
 
31
32
  type HMGPubYear = Literal[1992, 2004, 2010, 2023]
@@ -47,7 +48,7 @@ class GuidelinesThresholds:
47
48
  """
48
49
  Guidelines threholds by Guidelines publication year
49
50
 
50
- ΔHHI, Recapture Rate, GUPPI, Diversion ratio, CMCR, and IPR thresholds
51
+ ΔHHI, Recapture Ratio, GUPPI, Diversion ratio, CMCR, and IPR thresholds
51
52
  constructed from concentration standards in Guidelines published in
52
53
  1992, 2004, 2010, and 2023.
53
54
 
@@ -70,7 +71,7 @@ class GuidelinesThresholds:
70
71
  """
71
72
  Negative presumption quantified on various measures
72
73
 
73
- ΔHHI safeharbor bound, default recapture rate, GUPPI bound,
74
+ ΔHHI safeharbor bound, default recapture ratio, GUPPI bound,
74
75
  diversion ratio limit, CMCR, and IPR
75
76
  """
76
77
 
@@ -78,7 +79,7 @@ class GuidelinesThresholds:
78
79
  """
79
80
  Presumption of harm defined in HMG
80
81
 
81
- ΔHHI bound and corresponding default recapture rate, GUPPI bound,
82
+ ΔHHI bound and corresponding default recapture ratio, GUPPI bound,
82
83
  diversion ratio limit, CMCR, and IPR
83
84
  """
84
85
 
@@ -87,7 +88,7 @@ class GuidelinesThresholds:
87
88
  Presumption of harm imputed from guidelines
88
89
 
89
90
  ΔHHI bound inferred from strict numbers-equivalent
90
- of (post-merger) HHI presumption, and corresponding default recapture rate,
91
+ of (post-merger) HHI presumption, and corresponding default recapture ratio,
91
92
  GUPPI bound, diversion ratio limit, CMCR, and IPR
92
93
  """
93
94
 
@@ -162,38 +163,40 @@ class GuidelinesThresholds:
162
163
  )
163
164
 
164
165
 
165
- def _concentration_threshold_validator(
166
- _instance: ConcentrationBoundary, _attribute: Attribute[float], _value: float, /
167
- ) -> None:
168
- if not 0 <= _value <= 1:
169
- raise ValueError("Concentration threshold must lie between 0 and 1.")
170
-
171
-
172
- def _concentration_measure_name_validator(
173
- _instance: ConcentrationBoundary, _attribute: Attribute[str], _value: str, /
174
- ) -> None:
175
- if _value not in ("ΔHHI", "Combined share", "Pre-merger HHI", "Post-merger HHI"):
176
- raise ValueError(f"Invalid name for a concentration measure, {_value!r}.")
177
-
178
-
179
166
  @frozen
180
167
  class ConcentrationBoundary:
181
168
  """Concentration parameters, boundary coordinates, and area under concentration boundary."""
182
169
 
183
- threshold: float = field(
184
- kw_only=False,
185
- default=0.01,
186
- validator=(validators.instance_of(float), _concentration_threshold_validator),
187
- )
188
- precision: int = field(
189
- kw_only=False, default=5, validator=validators.instance_of(int)
190
- )
191
170
  measure_name: Literal[
192
171
  "ΔHHI", "Combined share", "Pre-merger HHI", "Post-merger HHI"
193
172
  ] = field(
194
173
  kw_only=False,
195
- default="ΔHHI",
196
- validator=(validators.instance_of(str), _concentration_measure_name_validator),
174
+ default="ΔHHI", # pyright: ignore
175
+ )
176
+
177
+ @measure_name.validator # pyright: ignore
178
+ def __mnv(
179
+ _instance: ConcentrationBoundary, _attribute: Attribute[str], _value: str, /
180
+ ) -> None:
181
+ if _value not in (
182
+ "ΔHHI",
183
+ "Combined share",
184
+ "Pre-merger HHI",
185
+ "Post-merger HHI",
186
+ ):
187
+ raise ValueError(f"Invalid name for a concentration measure, {_value!r}.")
188
+
189
+ threshold: float = field(kw_only=False, default=0.01)
190
+
191
+ @threshold.validator # pyright: ignore
192
+ def __tv(
193
+ _instance: ConcentrationBoundary, _attribute: Attribute[float], _value: float, /
194
+ ) -> None:
195
+ if not 0 <= _value <= 1:
196
+ raise ValueError("Concentration threshold must lie between 0 and 1.") # pyright: ignore
197
+
198
+ precision: int = field(
199
+ kw_only=False, default=5, validator=validators.instance_of(int)
197
200
  )
198
201
 
199
202
  coordinates: ArrayDouble = field(init=False, kw_only=True)
@@ -213,45 +216,17 @@ class ConcentrationBoundary:
213
216
  case "Post-merger HHI":
214
217
  _conc_fn = gbfn.hhi_post_contrib_boundary
215
218
 
216
- _boundary = _conc_fn(self.threshold, prec=self.precision)
219
+ _boundary = _conc_fn(self.threshold, dps=self.precision)
217
220
  object.__setattr__(self, "coordinates", _boundary.coordinates)
218
221
  object.__setattr__(self, "area", _boundary.area)
219
222
 
220
223
 
221
- def _divr_value_validator(
222
- _instance: DiversionRatioBoundary, _attribute: Attribute[float], _value: float, /
223
- ) -> None:
224
- if not 0 <= _value <= 1:
225
- raise ValueError(
226
- "Margin-adjusted benchmark share ratio must lie between 0 and 1."
227
- )
228
-
229
-
230
- def _rec_spec_validator(
231
- _instance: DiversionRatioBoundary,
232
- _attribute: Attribute[RECForm],
233
- _value: RECForm,
234
- /,
235
- ) -> None:
236
- if _value == RECForm.OUTIN and _instance.recapture_rate:
237
- raise ValueError(
238
- f"Invalid recapture specification, {_value!r}. "
239
- "You may consider specifying `mergeron.RECForm.INOUT` here, and "
240
- 'assigning the default recapture rate as attribute, "recapture_rate" of '
241
- "this `DiversionRatioBoundarySpec` object."
242
- )
243
- if _value is None and _instance.agg_method != UPPAggrSelector.MAX:
244
- raise ValueError(
245
- f"Specified aggregation method, {_instance.agg_method} requires a recapture specification."
246
- )
247
-
248
-
249
224
  @frozen
250
225
  class DiversionRatioBoundary:
251
226
  """
252
227
  Diversion ratio specification, boundary coordinates, and area under boundary.
253
228
 
254
- Along with the default diversion ratio and recapture rate,
229
+ Along with the default diversion ratio and recapture ratio,
255
230
  a diversion ratio boundary specification includes the recapture form --
256
231
  whether fixed for both merging firms' products ("proportional") or
257
232
  consistent with share-proportionality, i.e., "inside-out";
@@ -261,29 +236,33 @@ class DiversionRatioBoundary:
261
236
 
262
237
  """
263
238
 
264
- diversion_ratio: float = field(
265
- kw_only=False,
266
- default=0.065,
267
- validator=(validators.instance_of(float), _divr_value_validator),
268
- )
269
-
270
- recapture_rate: float = field(
239
+ diversion_ratio: float = field(kw_only=False, default=0.065)
240
+
241
+ @diversion_ratio.validator # pyright: ignore
242
+ def __dvv(
243
+ _instance: DiversionRatioBoundary,
244
+ _attribute: Attribute[float],
245
+ _value: float,
246
+ /,
247
+ ) -> None:
248
+ if not 0 <= _value <= 1:
249
+ raise ValueError(
250
+ "Margin-adjusted benchmark share ratio must lie between 0 and 1."
251
+ )
252
+
253
+ recapture_ratio: float = field(
271
254
  kw_only=False, default=DEFAULT_REC_RATE, validator=validators.instance_of(float)
272
255
  )
273
256
 
274
- recapture_form: RECForm | None = field(
275
- kw_only=True,
276
- default=RECForm.INOUT,
277
- validator=(validators.instance_of((type(None), RECForm)), _rec_spec_validator),
278
- )
257
+ recapture_form: RECForm | None = field(kw_only=True, default=RECForm.INOUT)
279
258
  """
280
- The form of the recapture rate.
259
+ The form of the recapture ratio.
281
260
 
282
- When :attr:`mergeron.RECForm.INOUT`, the recapture rate for
261
+ When :attr:`mergeron.RECForm.INOUT`, the recapture ratio for
283
262
  he product having the smaller market-share is assumed to equal the default,
284
- and the recapture rate for the product with the larger market-share is
285
- computed assuming MNL demand. Fixed recapture rates are specified as
286
- :attr:`mergeron.RECForm.FIXED`. (To specify that recapture rates be
263
+ and the recapture ratio for the product with the larger market-share is
264
+ computed assuming MNL demand. Fixed recapture ratios are specified as
265
+ :attr:`mergeron.RECForm.FIXED`. (To specify that recapture ratios be
287
266
  constructed from the generated purchase-probabilities for products in
288
267
  the market and for the outside good, specify :attr:`mergeron.RECForm.OUTIN`.)
289
268
 
@@ -296,6 +275,27 @@ class DiversionRatioBoundary:
296
275
 
297
276
  """
298
277
 
278
+ @recapture_form.validator # pyright: ignore
279
+ def __rsv(
280
+ _instance: DiversionRatioBoundary,
281
+ _attribute: Attribute[RECForm],
282
+ _value: RECForm,
283
+ /,
284
+ ) -> None:
285
+ if _value and not (isinstance(_value, RECForm)):
286
+ raise ValueError(f"Invalid recapture specification, {_value!r}.")
287
+ if _value == RECForm.OUTIN and _instance.recapture_ratio:
288
+ raise ValueError(
289
+ f"Invalid recapture specification, {_value!r}. "
290
+ "You may consider specifying `mergeron.RECForm.INOUT` here, and "
291
+ 'assigning the default recapture ratio as attribute, "recapture_ratio" of '
292
+ "this `DiversionRatioBoundarySpec` object."
293
+ )
294
+ if _value is None and _instance.agg_method != UPPAggrSelector.MAX:
295
+ raise ValueError(
296
+ f"Specified aggregation method, {_instance.agg_method} requires a recapture specification."
297
+ )
298
+
299
299
  agg_method: UPPAggrSelector = field(
300
300
  kw_only=True,
301
301
  default=UPPAggrSelector.MAX,
@@ -331,11 +331,11 @@ class DiversionRatioBoundary:
331
331
 
332
332
  def __attrs_post_init__(self, /) -> None:
333
333
  _share_ratio = critical_share_ratio(
334
- self.diversion_ratio, r_bar=self.recapture_rate
334
+ self.diversion_ratio, r_bar=self.recapture_ratio
335
335
  )
336
336
  _upp_agg_kwargs: gbfn.ShareRatioBoundaryKeywords = {
337
337
  "recapture_form": getattr(self.recapture_form, "value", "inside-out"),
338
- "prec": self.precision,
338
+ "dps": self.precision,
339
339
  }
340
340
  match self.agg_method:
341
341
  case UPPAggrSelector.DIS:
@@ -345,10 +345,10 @@ class DiversionRatioBoundary:
345
345
  _upp_agg_fn = gbfn.shrratio_boundary_xact_avg # type: ignore
346
346
  case UPPAggrSelector.MAX:
347
347
  _upp_agg_fn = gbfn.shrratio_boundary_max # type: ignore
348
- _upp_agg_kwargs = {"prec": 10} # replace here
348
+ _upp_agg_kwargs = {"dps": 10} # replace here
349
349
  case UPPAggrSelector.MIN:
350
350
  _upp_agg_fn = gbfn.shrratio_boundary_min # type: ignore
351
- _upp_agg_kwargs |= {"prec": 10} # update here
351
+ _upp_agg_kwargs |= {"dps": 10} # update here
352
352
  case _:
353
353
  _upp_agg_fn = gbfn.shrratio_boundary_wtd_avg
354
354
 
@@ -370,7 +370,7 @@ class DiversionRatioBoundary:
370
370
 
371
371
  _upp_agg_kwargs |= {"agg_method": _aggregator, "weighting": _wgt_type}
372
372
 
373
- _boundary = _upp_agg_fn(_share_ratio, self.recapture_rate, **_upp_agg_kwargs)
373
+ _boundary = _upp_agg_fn(_share_ratio, self.recapture_ratio, **_upp_agg_kwargs) # pyright: ignore # TypedDict redefinition
374
374
  object.__setattr__(self, "coordinates", _boundary.coordinates)
375
375
  object.__setattr__(self, "area", _boundary.area)
376
376
 
@@ -392,11 +392,11 @@ def guppi_from_delta(
392
392
  m_star
393
393
  Parametric price-cost margin.
394
394
  r_bar
395
- Default recapture rate.
395
+ Default recapture ratio.
396
396
 
397
397
  Returns
398
398
  -------
399
- GUPPI bound corresponding to ∆HHI bound, at given margin and recapture rate.
399
+ GUPPI bound corresponding to ∆HHI bound, at given margin and recapture ratio.
400
400
 
401
401
  """
402
402
  return gbfn.round_cust(
@@ -413,7 +413,7 @@ def critical_share_ratio(
413
413
  m_star: float = 1.00,
414
414
  r_bar: float = 1.00,
415
415
  frac: float = 1e-16,
416
- ) -> mpf:
416
+ ) -> MPFloat:
417
417
  """
418
418
  Corollary to GUPPI bound.
419
419
 
@@ -424,12 +424,12 @@ def critical_share_ratio(
424
424
  m_star
425
425
  Parametric price-cost margin.
426
426
  r_bar
427
- Default recapture rate.
427
+ Default recapture ratio.
428
428
 
429
429
  Returns
430
430
  -------
431
431
  Critical share ratio (share ratio bound) corresponding to the GUPPI bound
432
- for given margin and recapture rate.
432
+ for given margin and recapture ratio.
433
433
 
434
434
  """
435
435
  return gbfn.round_cust(
@@ -445,7 +445,7 @@ def share_from_guppi(
445
445
  r_bar: float = DEFAULT_REC_RATE,
446
446
  ) -> float:
447
447
  """
448
- Symmetric-firm share for given GUPPI, margin, and recapture rate.
448
+ Symmetric-firm share for given GUPPI, margin, and recapture ratio.
449
449
 
450
450
  Parameters
451
451
  ----------
@@ -454,13 +454,13 @@ def share_from_guppi(
454
454
  m_star
455
455
  Parametric price-cost margin.
456
456
  r_bar
457
- Default recapture rate.
457
+ Default recapture ratio.
458
458
 
459
459
  Returns
460
460
  -------
461
461
  float
462
462
  Symmetric firm market share on GUPPI boundary, for given margin and
463
- recapture rate.
463
+ recapture ratio.
464
464
 
465
465
  """
466
466