mergeron 2024.738953.1__py3-none-any.whl → 2024.738972.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/gen/__init__.py CHANGED
@@ -5,23 +5,21 @@ Defines constants and containers for industry data generation and testing
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from importlib.metadata import version
9
-
10
- from .. import _PKG_NAME, RECConstants, UPPAggrSelector # noqa: TID252
11
-
12
- __version__ = version(_PKG_NAME)
13
-
14
-
15
8
  import enum
16
9
  from dataclasses import dataclass
10
+ from importlib.metadata import version
17
11
  from typing import ClassVar, Protocol, TypeVar
18
12
 
19
13
  import numpy as np
20
- from attrs import Attribute, define, field, validators
14
+ from attrs import Attribute, define, field, frozen, validators
21
15
  from numpy.typing import NBitBase, NDArray
22
16
 
17
+ from .. import _PKG_NAME, RECConstants, UPPAggrSelector # noqa: TID252
23
18
  from ..core.pseudorandom_numbers import DIST_PARMS_DEFAULT # noqa: TID252
24
19
 
20
+ __version__ = version(_PKG_NAME)
21
+
22
+
25
23
  EMPTY_ARRAY_DEFAULT = np.zeros(2)
26
24
  FCOUNT_WTS_DEFAULT = ((_nr := np.arange(1, 6)[::-1]) / _nr.sum()).astype(np.float64)
27
25
 
@@ -74,7 +72,7 @@ class SHRConstants(enum.StrEnum):
74
72
  """
75
73
 
76
74
 
77
- @define(slots=True, frozen=True)
75
+ @frozen
78
76
  class ShareSpec:
79
77
  """Market share specification
80
78
 
@@ -88,9 +86,28 @@ class ShareSpec:
88
86
 
89
87
  """
90
88
 
91
- recapture_spec: RECConstants
89
+ recapture_form: RECConstants
92
90
  """see RECConstants"""
93
91
 
92
+ recapture_rate: float | None
93
+ """A value between 0 and 1.
94
+
95
+ None if market share specification requires direct generation of
96
+ outside good choice probabilities (RECConstants.OUTIN).
97
+
98
+ The recapture rate is usually calibrated to the numbers-equivalent of the
99
+ HHI threshold for the presumtion of harm from unilateral compoetitive effects
100
+ in published merger guidelines. Accordingly, values for the recapture rate may be:
101
+
102
+ * 0.855, **6-to-5 merger from symmetry**; US Guidelines, 1992, 2023
103
+ * 0.855, 6-to-5 merger from symmetry; EU Guidelines for horizontal mergers, 2004
104
+ * 0.82, **6-to-5 merger to symmetry**; EU Guidelines for horizontal mergers, 2004
105
+ * 0.80, 5-to-4 merger from symmetry; US Guidelines, 2010
106
+ * 0.78, **5-to-4 merger to symmetry**; US Guidelines, 2010
107
+
108
+ Highlighting indicates hypothetical mergers close to the boundary of the presumption.
109
+ """
110
+
94
111
  dist_type: SHRConstants
95
112
  """see SHRConstants"""
96
113
 
@@ -133,7 +150,7 @@ class FM2Constants(enum.StrEnum):
133
150
  SYM = "symmetric"
134
151
 
135
152
 
136
- @define(slots=True, frozen=True)
153
+ @frozen
137
154
  class PCMSpec:
138
155
  """Price-cost margin (PCM) specification
139
156
 
@@ -149,12 +166,12 @@ class PCMSpec:
149
166
 
150
167
  """
151
168
 
152
- dist_type: PCMConstants
153
- """See PCMConstants"""
154
-
155
169
  firm2_pcm_constraint: FM2Constants
156
170
  """See FM2Constants"""
157
171
 
172
+ dist_type: PCMConstants
173
+ """See PCMConstants"""
174
+
158
175
  dist_parms: NDArray[np.float64] | None
159
176
  """Parameter specification for tailoring PCM distribution
160
177
 
@@ -205,40 +222,33 @@ class SSZConstants(float, enum.ReprEnum):
205
222
  """When initial set of draws is not restricted in any way."""
206
223
 
207
224
 
208
- # Validators for selected attributes of MarketSampleSpec
225
+ # Validators for selected attributes of MarketSpec
209
226
  def _sample_size_validator(
210
- _object: MarketSampleSpec, _attribute: Attribute[int], _value: int, /
227
+ _object: MarketSpec, _attribute: Attribute[int], _value: int, /
211
228
  ) -> None:
212
229
  if _value < 10**6:
213
230
  raise ValueError(
214
- f"Sample size must be not less than {10**6:,d}. Got, {_value:,d}."
231
+ f"Sample size must be no less than {10**6:,d}; got, {_value:,d}."
215
232
  )
216
233
 
217
234
 
218
- def _recapture_rate_validator(
219
- _object: MarketSampleSpec,
220
- _attribute: Attribute[float | None],
221
- _value: float | None,
222
- /,
235
+ def _share_spec_validator(
236
+ _instance: MarketSpec, _attribute: Attribute[ShareSpec], _value: ShareSpec, /
223
237
  ) -> None:
224
- if _value and not (0 < _value <= 1):
238
+ _r_bar = _value.recapture_rate
239
+ if _r_bar and not (0 < _r_bar <= 1):
225
240
  raise ValueError("Recapture rate must lie in the interval, [0, 1).")
226
241
 
227
- if _value and _object.share_spec.recapture_spec == RECConstants.OUTIN:
242
+ elif _r_bar and _value.recapture_form == RECConstants.OUTIN:
228
243
  raise ValueError(
229
244
  "Market share specification requires estimation of recapture rate from "
230
245
  "generated data. Either delete recapture rate specification or set it to None."
231
246
  )
232
247
 
233
-
234
- def _share_spec_validator(
235
- _instance: MarketSampleSpec, _attribute: Attribute[ShareSpec], _value: ShareSpec, /
236
- ) -> None:
237
- _r_bar = _instance.recapture_rate
238
248
  if _value.dist_type == SHRConstants.UNI:
239
- if _value.recapture_spec == RECConstants.OUTIN:
249
+ if _value.recapture_form == RECConstants.OUTIN:
240
250
  raise ValueError(
241
- f"Invalid recapture specification, {_value.recapture_spec!r} "
251
+ f"Invalid recapture specification, {_value.recapture_form!r} "
242
252
  "for market share specification with Uniform distribution. "
243
253
  "Redefine the market-sample specification, modifying the ."
244
254
  "market-share specification or the recapture specification."
@@ -246,32 +256,31 @@ def _share_spec_validator(
246
256
  elif _value.firm_counts_weights is not None:
247
257
  raise ValueError(
248
258
  "Generated data for markets with specified firm-counts or "
249
- "varying firm counts are not feasible with market shares "
259
+ "varying firm counts are not feasible for market shares "
250
260
  "with Uniform distribution. Consider revising the "
251
261
  r"distribution type to {SHRConstants.DIR_FLAT}, which gives "
252
262
  "uniformly distributed draws on the :math:`n+1` simplex "
253
263
  "for firm-count, :math:`n`."
254
264
  )
255
- # Outside-in calibration only valid for Dir-distributed shares
256
- elif _value.recapture_spec != RECConstants.OUTIN and (
265
+ elif _value.recapture_form != RECConstants.OUTIN and (
257
266
  _r_bar is None or not isinstance(_r_bar, float)
258
267
  ):
259
268
  raise ValueError(
260
- f"Recapture specification, {_value.recapture_spec!r} requires that "
269
+ f"Recapture specification, {_value.recapture_form!r} requires that "
261
270
  "the market sample specification inclues a recapture rate."
262
271
  )
263
272
 
264
273
 
265
274
  def _pcm_spec_validator(
266
- _instance: MarketSampleSpec, _attribute: Attribute[PCMSpec], _value: PCMSpec, /
275
+ _instance: MarketSpec, _attribute: Attribute[PCMSpec], _value: PCMSpec, /
267
276
  ) -> None:
268
277
  if (
269
- _instance.share_spec.recapture_spec == RECConstants.FIXED
278
+ _instance.share_spec.recapture_form == RECConstants.FIXED
270
279
  and _value.firm2_pcm_constraint == FM2Constants.MNL
271
280
  ):
272
281
  raise ValueError(
273
282
  "{} {} {}".format(
274
- f'Specification of "recapture_spec", "{_instance.share_spec.recapture_spec}"',
283
+ f'Specification of "recapture_form", "{_instance.share_spec.recapture_form}"',
275
284
  "requires Firm 2 margin must have property, ",
276
285
  f'"{FM2Constants.IID}" or "{FM2Constants.SYM}".',
277
286
  )
@@ -297,49 +306,35 @@ def _pcm_spec_validator(
297
306
  )
298
307
 
299
308
 
300
- @define(slots=True, frozen=True)
301
- class MarketSampleSpec:
309
+ @define(slots=False)
310
+ class MarketSpec:
302
311
  """Parameter specification for market data generation."""
303
312
 
304
- sample_size: int = field(
305
- default=10**6, validator=(validators.instance_of(int), _sample_size_validator)
306
- )
307
- """sample size generated"""
308
-
309
- recapture_rate: float | None = field(
310
- default=None, validator=_recapture_rate_validator
311
- )
312
- """market recapture rate
313
-
314
- Is None if market share specification requires generation of
315
- outside good choice probabilities (RECConstants.OUTIN).
316
- """
317
-
318
- pr_sym_spec: PRIConstants = field( # type: ignore
319
- kw_only=True,
320
- default=PRIConstants.SYM,
321
- validator=validators.instance_of(PRIConstants), # type: ignore
322
- )
323
- """Price specification, see PRIConstants"""
324
-
325
313
  share_spec: ShareSpec = field(
326
314
  kw_only=True,
327
- default=ShareSpec(RECConstants.INOUT, SHRConstants.UNI, None, None),
315
+ default=ShareSpec(RECConstants.INOUT, 0.855, SHRConstants.UNI, None, None),
328
316
  validator=[validators.instance_of(ShareSpec), _share_spec_validator],
329
317
  )
330
- """See definition of ShareSpec"""
318
+ """Market-share specification, see definition of ShareSpec"""
331
319
 
332
320
  pcm_spec: PCMSpec = field(
333
321
  kw_only=True,
334
- default=PCMSpec(PCMConstants.UNI, FM2Constants.IID, None),
322
+ default=PCMSpec(FM2Constants.IID, PCMConstants.UNI, None),
335
323
  validator=[validators.instance_of(PCMSpec), _pcm_spec_validator],
336
324
  )
337
- """See definition of PCMSpec"""
325
+ """Margin specification, see definition of PCMSpec"""
326
+
327
+ price_spec: PRIConstants = field(
328
+ kw_only=True,
329
+ default=PRIConstants.SYM,
330
+ validator=validators.instance_of(PRIConstants),
331
+ )
332
+ """Price specification, see PRIConstants"""
338
333
 
339
- hsr_filing_test_type: SSZConstants = field( # type: ignore
334
+ hsr_filing_test_type: SSZConstants = field(
340
335
  kw_only=True,
341
336
  default=SSZConstants.ONE,
342
- validator=validators.instance_of(SSZConstants), # type: ignore
337
+ validator=validators.instance_of(SSZConstants),
343
338
  )
344
339
  """Method for modeling HSR filing threholds, see SSZConstants"""
345
340
 
@@ -351,19 +346,16 @@ class INVResolution(enum.StrEnum):
351
346
  BOTH = "both"
352
347
 
353
348
 
354
- @define(slots=True, frozen=True)
349
+ @frozen
355
350
  class UPPTestRegime:
356
- resolution: INVResolution = field( # type: ignore
357
- default=INVResolution.ENFT,
358
- validator=validators.instance_of(INVResolution), # type: ignore
351
+ resolution: INVResolution = field(
352
+ default=INVResolution.ENFT, validator=validators.instance_of(INVResolution)
359
353
  )
360
- guppi_aggregator: UPPAggrSelector = field( # type: ignore
361
- default=UPPAggrSelector.MAX,
362
- validator=validators.instance_of(UPPAggrSelector), # type: ignore
354
+ guppi_aggregator: UPPAggrSelector = field(
355
+ default=UPPAggrSelector.MIN, validator=validators.instance_of(UPPAggrSelector)
363
356
  )
364
- divr_aggregator: UPPAggrSelector | None = field( # type: ignore
365
- default=guppi_aggregator,
366
- validator=validators.instance_of(UPPAggrSelector | None), # type: ignore
357
+ divr_aggregator: UPPAggrSelector | None = field(
358
+ default=None, validator=validators.instance_of((UPPAggrSelector, type(None)))
367
359
  )
368
360
 
369
361
 
@@ -469,7 +461,7 @@ class MarginDataSample:
469
461
 
470
462
  @dataclass(slots=True, frozen=True)
471
463
  class UPPTestsRaw:
472
- """arrays marking test failures and successes
464
+ """Container for arrays marking test failures and successes
473
465
 
474
466
  A test success is a draw ("market") that meeets the
475
467
  specified test criterion, and a test failure is
@@ -494,9 +486,9 @@ class UPPTestsRaw:
494
486
 
495
487
  @dataclass(slots=True, frozen=True)
496
488
  class UPPTestsCounts:
497
- """counts of markets resolved as specified
489
+ """Counts of markets resolved as specified
498
490
 
499
- Resolution is specified in a UPPTestRegime object.
491
+ Resolution may be either "enforcement" or "clearance".
500
492
  """
501
493
 
502
494
  by_firm_count: NDArray[np.int64]
@@ -19,11 +19,12 @@ from ..core.pseudorandom_numbers import ( # noqa: TID252
19
19
  prng,
20
20
  )
21
21
  from . import (
22
+ EMPTY_ARRAY_DEFAULT,
22
23
  FCOUNT_WTS_DEFAULT,
23
24
  TF,
24
25
  FM2Constants,
25
26
  MarginDataSample,
26
- MarketSampleSpec,
27
+ MarketSpec,
27
28
  PCMConstants,
28
29
  PriceDataSample,
29
30
  PRIConstants,
@@ -36,7 +37,8 @@ __version__ = version(_PKG_NAME)
36
37
 
37
38
 
38
39
  def _gen_share_data(
39
- _mkt_sample_spec: MarketSampleSpec,
40
+ _sample_size: int,
41
+ _mkt_sample_spec: MarketSpec,
40
42
  _fcount_rng_seed_seq: SeedSequence | None,
41
43
  _mktshr_rng_seed_seq: SeedSequence,
42
44
  _nthreads: int = 16,
@@ -61,14 +63,12 @@ def _gen_share_data(
61
63
 
62
64
  """
63
65
 
64
- _recapture_spec, _dist_type_mktshr, _dist_parms_mktshr, _firm_count_prob_wts_raw = (
66
+ _recapture_form, _dist_type_mktshr, _dist_parms_mktshr, _firm_count_prob_wts_raw = (
65
67
  getattr(_mkt_sample_spec.share_spec, _f)
66
- for _f in ("recapture_spec", "dist_type", "dist_parms", "firm_counts_weights")
68
+ for _f in ("recapture_form", "dist_type", "dist_parms", "firm_counts_weights")
67
69
  )
68
70
 
69
- _ssz = _mkt_sample_spec.sample_size
70
-
71
- _r_bar = _mkt_sample_spec.recapture_rate or 0.80
71
+ _ssz = _sample_size
72
72
 
73
73
  match _dist_type_mktshr:
74
74
  case SHRConstants.UNI:
@@ -84,7 +84,7 @@ def _gen_share_data(
84
84
  )
85
85
  _mkt_share_sample = _gen_market_shares_dirichlet_multisample(
86
86
  _ssz,
87
- _recapture_spec,
87
+ _recapture_form,
88
88
  _dist_type_mktshr,
89
89
  _dist_parms_mktshr,
90
90
  _firm_count_prob_wts,
@@ -98,9 +98,10 @@ def _gen_share_data(
98
98
  f'Unexpected type, "{_dist_type_mktshr}" for share distribution.'
99
99
  )
100
100
 
101
- # If recapture_spec == "inside-out", recalculate _aggregate_purchase_prob
101
+ # If recapture_form == "inside-out", recalculate _aggregate_purchase_prob
102
102
  _frmshr_array = _mkt_share_sample.mktshr_array[:, :2]
103
- if _recapture_spec == RECConstants.INOUT:
103
+ _r_bar = _mkt_sample_spec.share_spec.recapture_rate or 0.855
104
+ if _recapture_form == RECConstants.INOUT:
104
105
  _mkt_share_sample = ShareDataSample(
105
106
  _mkt_share_sample.mktshr_array,
106
107
  _mkt_share_sample.fcounts,
@@ -176,10 +177,10 @@ def _gen_market_shares_uniform(
176
177
 
177
178
  def _gen_market_shares_dirichlet_multisample(
178
179
  _s_size: int = 10**6,
179
- _recapture_spec: RECConstants = RECConstants.INOUT,
180
+ _recapture_form: RECConstants = RECConstants.INOUT,
180
181
  _dist_type_dir: SHRConstants = SHRConstants.DIR_FLAT,
181
182
  _dist_parms_dir: NDArray[np.floating[TF]] | None = None,
182
- _firm_count_wts: NDArray[np.floating[TF]] | None = None, # type: ignore
183
+ _firm_count_wts: NDArray[np.floating[TF]] | None = None,
183
184
  _fcount_rng_seed_seq: SeedSequence | None = None,
184
185
  _mktshr_rng_seed_seq: SeedSequence | None = None,
185
186
  _nthreads: int = 16,
@@ -202,7 +203,7 @@ def _gen_market_shares_dirichlet_multisample(
202
203
  firm count weights array for sample to be drawn
203
204
  _dist_type_dir
204
205
  Whether Dirichlet is Flat or Asymmetric
205
- _recapture_spec
206
+ _recapture_form
206
207
  r_1 = r_2 if "proportional", otherwise MNL-consistent
207
208
  _fcount_rng_seed_seq
208
209
  seed firm count rng, for replicable results
@@ -226,7 +227,7 @@ def _gen_market_shares_dirichlet_multisample(
226
227
  *(
227
228
  _f
228
229
  for _f in zip(
229
- 2 + np.arange(len(_firm_count_wts)), # type: ignore
230
+ 2 + np.arange(len(_firm_count_wts)),
230
231
  _firm_count_wts / _firm_count_wts.sum(),
231
232
  strict=True,
232
233
  )
@@ -277,7 +278,7 @@ def _gen_market_shares_dirichlet_multisample(
277
278
  _mktshr_sample_f = _gen_market_shares_dirichlet(
278
279
  _dir_alphas_test,
279
280
  len(_fcounts_match_rows),
280
- _recapture_spec,
281
+ _recapture_form,
281
282
  _f_sseq,
282
283
  _nthreads,
283
284
  )
@@ -315,7 +316,7 @@ def _gen_market_shares_dirichlet_multisample(
315
316
  def _gen_market_shares_dirichlet(
316
317
  _dir_alphas: NDArray[np.floating[TF]],
317
318
  _s_size: int = 10**6,
318
- _recapture_spec: RECConstants = RECConstants.INOUT,
319
+ _recapture_form: RECConstants = RECConstants.INOUT,
319
320
  _mktshr_rng_seed_seq: SeedSequence | None = None,
320
321
  _nthreads: int = 16,
321
322
  /,
@@ -330,7 +331,7 @@ def _gen_market_shares_dirichlet(
330
331
  sample size to be drawn
331
332
  _r_bar
332
333
  market recapture rate
333
- _recapture_spec
334
+ _recapture_form
334
335
  r_1 = r_2 if RECConstants.FIXED, otherwise MNL-consistent. If
335
336
  RECConstants.OUTIN; the number of columns in the output share array
336
337
  is len(_dir_alphas) - 1.
@@ -348,7 +349,7 @@ def _gen_market_shares_dirichlet(
348
349
  if not isinstance(_dir_alphas, np.ndarray):
349
350
  _dir_alphas = np.array(_dir_alphas)
350
351
 
351
- if _recapture_spec == RECConstants.OUTIN:
352
+ if _recapture_form == RECConstants.OUTIN:
352
353
  _dir_alphas = np.concatenate((_dir_alphas, _dir_alphas[-1:]))
353
354
 
354
355
  _mktshr_seed_seq_ch = (
@@ -380,9 +381,9 @@ def _gen_market_shares_dirichlet(
380
381
  )
381
382
  )
382
383
 
383
- # If recapture_spec == 'inside_out', further calculations downstream
384
+ # If recapture_form == 'inside_out', further calculations downstream
384
385
  _aggregate_purchase_prob = np.nan * np.empty((_s_size, 1))
385
- if _recapture_spec == RECConstants.OUTIN:
386
+ if _recapture_form == RECConstants.OUTIN:
386
387
  _aggregate_purchase_prob = 1 - _mktshr_array[:, [-1]]
387
388
  _mktshr_array = _mktshr_array[:, :-1] / _aggregate_purchase_prob
388
389
 
@@ -394,27 +395,25 @@ def _gen_market_shares_dirichlet(
394
395
  )
395
396
 
396
397
 
397
- def _gen_pr_data(
398
- _frmshr_array: NDArray[np.floating[TF]],
399
- _nth_firm_share: NDArray[np.floating[TF]],
400
- _mkt_sample_spec: MarketSampleSpec,
398
+ def _gen_price_data(
399
+ _frmshr_array: NDArray[np.float64],
400
+ _nth_firm_share: NDArray[np.float64],
401
+ _mkt_sample_spec: MarketSpec,
401
402
  _seed_seq: SeedSequence | None = None,
402
403
  /,
403
404
  ) -> PriceDataSample:
404
- _ssz = len(_frmshr_array)
405
-
406
405
  _hsr_filing_test_type = _mkt_sample_spec.hsr_filing_test_type
407
406
 
408
407
  _price_array, _price_ratio_array, _hsr_filing_test = (
409
- np.ones_like(_frmshr_array),
410
- np.empty_like(_frmshr_array),
411
- np.empty(_ssz, dtype=bool),
408
+ np.ones_like(_frmshr_array, np.float64),
409
+ np.empty_like(_frmshr_array, np.float64),
410
+ np.empty(len(_frmshr_array), bool),
412
411
  )
413
412
 
414
413
  _pr_max_ratio = 5.0
415
- match _mkt_sample_spec.pr_sym_spec:
414
+ match _mkt_sample_spec.price_spec:
416
415
  case PRIConstants.SYM:
417
- _nth_firm_price = np.ones((_ssz, 1))
416
+ _nth_firm_price = np.ones((len(_frmshr_array), 1))
418
417
  case PRIConstants.POS:
419
418
  _price_array, _nth_firm_price = (
420
419
  np.ceil(_p * _pr_max_ratio) for _p in (_frmshr_array, _nth_firm_share)
@@ -434,9 +433,8 @@ def _gen_pr_data(
434
433
  case _:
435
434
  raise ValueError(
436
435
  f"Condition regarding price symmetry"
437
- f' "{_mkt_sample_spec.pr_sym_spec.value}" is invalid.'
436
+ f' "{_mkt_sample_spec.price_spec.value}" is invalid.'
438
437
  )
439
- # del _pr_max_ratio
440
438
 
441
439
  _price_array = _price_array.astype(np.float64)
442
440
  _rev_array = _price_array * _frmshr_array
@@ -444,7 +442,7 @@ def _gen_pr_data(
444
442
 
445
443
  # Although `_test_rev_ratio_inv` is not fixed at 10%,
446
444
  # the ratio has not changed since inception of the HSR filing test,
447
- # so we treat it as a constant of merger policy.
445
+ # so we treat it as a constant of merger enforcement policy.
448
446
  _test_rev_ratio, _test_rev_ratio_inv = 10, 1 / 10
449
447
 
450
448
  match _hsr_filing_test_type:
@@ -479,21 +477,21 @@ def _gen_pr_data(
479
477
  # del _nth_firm_rev, _rev_ratio_to_nth
480
478
  case _:
481
479
  # Otherwise, all draws meet the filing test
482
- _hsr_filing_test = np.ones(_ssz, dtype=bool)
480
+ _hsr_filing_test = np.ones(len(_frmshr_array), dtype=bool)
483
481
 
484
482
  return PriceDataSample(_price_array, _hsr_filing_test)
485
483
 
486
484
 
487
485
  def _gen_pcm_data(
488
486
  _frmshr_array: NDArray[np.floating[TF]],
489
- _mkt_sample_spec: MarketSampleSpec,
487
+ _mkt_sample_spec: MarketSpec,
490
488
  _price_array: NDArray[np.floating[TF]],
491
489
  _aggregate_purchase_prob: NDArray[np.floating[TF]],
492
490
  _pcm_rng_seed_seq: SeedSequence,
493
491
  _nthreads: int = 16,
494
492
  /,
495
493
  ) -> MarginDataSample:
496
- _recapture_spec = _mkt_sample_spec.share_spec.recapture_spec
494
+ _recapture_form = _mkt_sample_spec.share_spec.recapture_form
497
495
  _dist_type_pcm, _dist_firm2_pcm, _dist_parms_pcm = (
498
496
  getattr(_mkt_sample_spec.pcm_spec, _f)
499
497
  for _f in ("dist_type", "firm2_pcm_constraint", "dist_parms")
@@ -506,27 +504,25 @@ def _gen_pcm_data(
506
504
  _mnl_test_array = np.empty((len(_frmshr_array), 2), dtype=int)
507
505
 
508
506
  _beta_min, _beta_max = [None] * 2 # placeholder
509
- _dist_parms = np.ones(2, np.float64)
510
507
  if _dist_type_pcm == PCMConstants.EMPR:
511
508
  _pcm_array = resample_mgn_data(
512
509
  _pcm_array.shape, # type: ignore
513
510
  seed_sequence=_pcm_rng_seed_seq,
514
511
  )
515
512
  else:
516
- if _dist_type_pcm == PCMConstants.UNI:
517
- _dist_parms = (
518
- DIST_PARMS_DEFAULT if _dist_parms_pcm is None else _dist_parms_pcm
519
- )
520
- elif _dist_type_pcm == PCMConstants.BETA:
521
- # Error-checking (could move to validators in definition of MarketSampleSpec)
522
-
513
+ if _dist_type_pcm == PCMConstants.BETA:
523
514
  if _dist_parms_pcm is None:
524
- _dist_parms_pcm = _dist_parms
515
+ _dist_parms_pcm = np.ones(2, np.float64)
525
516
 
526
517
  elif _dist_type_pcm == PCMConstants.BETA_BND: # Bounded beta
527
518
  if _dist_parms_pcm is None:
528
519
  _dist_parms_pcm = np.array([0, 1, 0, 1], np.float64)
529
520
  _dist_parms = beta_located_bound(_dist_parms_pcm)
521
+ else:
522
+ # _dist_type_pcm == PCMConstants.UNI
523
+ _dist_parms = (
524
+ DIST_PARMS_DEFAULT if _dist_parms_pcm is None else _dist_parms_pcm
525
+ )
530
526
 
531
527
  _pcm_rng = MultithreadedRNG(
532
528
  _pcm_array,
@@ -539,7 +535,7 @@ def _gen_pcm_data(
539
535
  del _pcm_rng
540
536
 
541
537
  if _dist_type_pcm == PCMConstants.BETA_BND:
542
- _beta_min, _beta_max = _dist_parms_pcm[2:] # type: ignore
538
+ _beta_min, _beta_max = _dist_parms_pcm[2:]
543
539
  _pcm_array = (_beta_max - _beta_min) * _pcm_array + _beta_min
544
540
  del _beta_min, _beta_max
545
541
 
@@ -566,6 +562,66 @@ def _gen_pcm_data(
566
562
  return MarginDataSample(_pcm_array, _mnl_test_array)
567
563
 
568
564
 
565
+ def _gen_divr_array(
566
+ _recapture_form: RECConstants,
567
+ _recapture_rate: float | None,
568
+ _frmshr_array: NDArray[np.float64],
569
+ _aggregate_purchase_prob: NDArray[np.float64] = EMPTY_ARRAY_DEFAULT,
570
+ /,
571
+ ) -> NDArray[np.float64]:
572
+ """
573
+ Given merging-firm shares and related parameters, return diverion ratios.
574
+
575
+ If recapture is specified as "Outside-in" (RECConstants.OUTIN), then the
576
+ choice-probability for the outside good must be supplied.
577
+
578
+ Parameters
579
+ ----------
580
+ _recapture_form
581
+ Enum specifying Fixed (proportional), Inside-out, or Outside-in
582
+
583
+ _recapture_rate
584
+ If recapture is proportional or inside-out, the recapture rate
585
+ for the firm with the smaller share.
586
+
587
+ _frmshr_array
588
+ Merging-firm shares.
589
+
590
+ _aggregate_purchase_prob
591
+ 1 minus probability that the outside good is chosen; converts
592
+ market shares to choice probabilities by multiplication.
593
+
594
+ Returns
595
+ -------
596
+ Merging-firm diversion ratios for mergers in the sample.
597
+
598
+ """
599
+
600
+ _divr_array: NDArray[np.float64]
601
+ if _recapture_form == RECConstants.FIXED:
602
+ _divr_array = _recapture_rate * _frmshr_array[:, ::-1] / (1 - _frmshr_array) # type: ignore
603
+
604
+ else:
605
+ _purchprob_array = _aggregate_purchase_prob * _frmshr_array
606
+ _divr_array = _purchprob_array[:, ::-1] / (1 - _purchprob_array)
607
+
608
+ _divr_assert_test = (
609
+ (np.round(np.einsum("ij->i", _frmshr_array), 15) == 1)
610
+ | (np.argmin(_frmshr_array, axis=1) == np.argmax(_divr_array, axis=1))
611
+ )[:, None]
612
+ if not all(_divr_assert_test):
613
+ raise ValueError(
614
+ "{} {} {} {}".format(
615
+ "Data construction fails tests:",
616
+ "the index of min(s_1, s_2) must equal",
617
+ "the index of max(d_12, d_21), for all draws.",
618
+ "unless frmshr_array sums to 1.00.",
619
+ )
620
+ )
621
+
622
+ return _divr_array
623
+
624
+
569
625
  def _beta_located(
570
626
  _mu: float | NDArray[np.float64], _sigma: float | NDArray[np.float64], /
571
627
  ) -> NDArray[np.float64]: