mergeron 2024.738936.0__py3-none-any.whl → 2024.738940.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.

@@ -11,400 +11,29 @@ from .. import _PKG_NAME # noqa: TID252
11
11
 
12
12
  __version__ = version(_PKG_NAME)
13
13
 
14
- import enum
15
- from typing import Literal, NamedTuple, TypeVar
16
-
17
14
  import attrs
18
15
  import numpy as np
19
16
  from numpy.random import SeedSequence
20
- from numpy.typing import NBitBase, NDArray
21
-
22
- from ..core.damodaran_margin_data import resample_mgn_data # noqa: TID252
23
- from ..core.pseudorandom_numbers import ( # noqa: TID252
24
- DIST_PARMS_DEFAULT,
25
- MultithreadedRNG,
26
- prng,
17
+ from numpy.typing import NDArray
18
+
19
+ from . import (
20
+ EMPTY_ARRAY_DEFAULT,
21
+ TF,
22
+ FM2Constants,
23
+ MarketDataSample,
24
+ MarketSampleSpec,
25
+ PRIConstants,
26
+ RECConstants,
27
+ SHRConstants,
28
+ SSZConstants,
29
+ )
30
+ from ._data_generation_functions_nonpublic import (
31
+ _gen_market_shares_dirichlet, # noqa: F401 easter-egg for external modules
32
+ _gen_market_shares_uniform, # noqa: F401 easter-egg for external modules
33
+ _gen_pcm_data,
34
+ _gen_pr_data,
35
+ _gen_share_data,
27
36
  )
28
-
29
- EMPTY_ARRAY_DEFAULT = np.zeros(2)
30
- FCOUNT_WTS_DEFAULT = ((_nr := np.arange(1, 6)[::-1]) / _nr.sum()).astype(np.float64)
31
-
32
- TF = TypeVar("TF", bound=NBitBase)
33
-
34
-
35
- @enum.unique
36
- class PRIConstants(tuple[bool, str | None], enum.ReprEnum):
37
- """Price specification.
38
-
39
- Whether prices are symmetric and, if not, the direction of correlation, if any.
40
- """
41
-
42
- SYM = (True, None)
43
- ZERO = (False, None)
44
- NEG = (False, "negative share-correlation")
45
- POS = (False, "positive share-correlation")
46
- CSY = (False, "market-wide cost-symmetry")
47
-
48
-
49
- @enum.unique
50
- class SHRConstants(enum.StrEnum):
51
- """Market share distributions."""
52
-
53
- UNI = "Uniform"
54
- """Uniform distribution over the 3-simplex"""
55
-
56
- DIR_FLAT = "Flat Dirichlet"
57
- """Shape parameter for all merging-firm-shares is unity (1)"""
58
-
59
- DIR_FLAT_CONSTR = "Flat Dirichlet - Constrained"
60
- """Impose minimum probablility weight on each firm-count
61
-
62
- Only firm-counts with probability weight of no less than 3%
63
- are included for data generation.
64
- """
65
-
66
- DIR_ASYM = "Asymmetric Dirichlet"
67
- """Share distribution for merging-firm shares has a higher peak share
68
-
69
- Shape parameter for merging-firm-share is 2.5, and 1.0 for all others.
70
- """
71
-
72
- DIR_COND = "Conditional Dirichlet"
73
- """Shape parameters for non-merging firms is proportional
74
-
75
- Shape parameters for merging-firm-share are 2.0 each; and
76
- are equiproportional and add to 2.0 for all non-merging-firm-shares.
77
- """
78
-
79
-
80
- @enum.unique
81
- class RECConstants(enum.StrEnum):
82
- """Recapture rate - derivation methods."""
83
-
84
- INOUT = "inside-out"
85
- OUTIN = "outside-in"
86
- FIXED = "proportional"
87
-
88
-
89
- class ShareSpec(NamedTuple):
90
- """Market share specification
91
-
92
- Notes
93
- -----
94
- If recapture is determined "outside-in", market shares cannot have
95
- Uniform distribution.
96
-
97
- If sample with varying firm counts is required, market shares must
98
- be specified as having a supported Dirichlet distribution.
99
-
100
- """
101
-
102
- recapture_spec: RECConstants
103
- """see RECConstants"""
104
-
105
- dist_type: SHRConstants
106
- """see SHRConstants"""
107
-
108
- dist_parms: NDArray[np.float64] | None
109
- """Parameters for tailoring market-share distribution
110
-
111
- For Uniform distribution, bounds of the distribution; defaults to `(0, 1)`;
112
- for Beta distribution, shape parameters, defaults to `(1, 1)`;
113
- for Bounded-Beta distribution, vector of (min, max, mean, std. deviation), non-optional;
114
- for Dirichlet-type distributions, a vector of shape parameters of length
115
- no less than the length of firm-count weights below; defaults depend on
116
- type of Dirichlet-distribution specified.
117
-
118
- """
119
- firm_counts_prob_weights: NDArray[np.float64 | np.int64] | None
120
- """relative or absolute frequencies of firm counts
121
-
122
-
123
- Given frequencies are exogenous to generated market data sample;
124
- defaults to FCOUNT_WTS_DEFAULT, which specifies firm-counts of 2 to 6
125
- with weights in descending order from 5 to 1."""
126
-
127
-
128
- @enum.unique
129
- class PCMConstants(enum.StrEnum):
130
- """Margin distributions."""
131
-
132
- UNI = "Uniform"
133
- BETA = "Beta"
134
- BETA_BND = "Bounded Beta"
135
- EMPR = "Damodaran margin data"
136
-
137
-
138
- @enum.unique
139
- class FM2Constants(enum.StrEnum):
140
- """Firm 2 margins - derivation methods."""
141
-
142
- IID = "i.i.d"
143
- MNL = "MNL-dep"
144
- SYM = "symmetric"
145
-
146
-
147
- class PCMSpec(NamedTuple):
148
- """Price-cost margin (PCM) specification
149
-
150
- If price-cost margins are specified as having Beta distribution,
151
- `dist_parms` is specified as a pair of positive, non-zero shape parameters of
152
- the standard Beta distribution. Specifying shape parameters :code:`np.array([1, 1])`
153
- is known equivalent to specifying uniform distribution over
154
- the interval :math:`[0, 1]`. If price-cost margins are specified as having
155
- Bounded-Beta distribution, `dist_parms` is specified as
156
- the tuple, (`mean`, `std deviation`, `min`, `max`), where `min` and `max`
157
- are lower- and upper-bounds respectively within the interval :math:`[0, 1]`.
158
-
159
-
160
- """
161
-
162
- dist_type: PCMConstants
163
- """See PCMConstants"""
164
-
165
- firm2_margin_restrictions: FM2Constants
166
- """See FM2Constants"""
167
-
168
- dist_parms: NDArray[np.float64] | None
169
- """Parameter specification for tailoring PCM distribution
170
-
171
- For Uniform distribution, bounds of the distribution; defaults to `(0, 1)`;
172
- for Beta distribution, shape parameters, defaults to `(1, 1)`;
173
- for Bounded-Beta distribution, vector of (min, max, mean, std. deviation), non-optional;
174
- for empirical distribution based on Damodaran margin data, optional, ignored
175
- """
176
-
177
-
178
- @enum.unique
179
- class SSZConstants(float, enum.ReprEnum):
180
- """
181
- Scale factors to offset sample size reduction.
182
-
183
- Sample size reduction occurs when imposing a HSR filing test
184
- or equilibrium condition under MNL demand.
185
- """
186
-
187
- HSR_NTH = 1.666667
188
- """
189
- For HSR filing requirement.
190
-
191
- When filing requirement is assumed met if maximum merging-firm shares exceeds
192
- ten (10) times the n-th firm's share and minimum merging-firm share is
193
- no less than n-th firm's share. To assure that the number of draws available
194
- after applying the given restriction, the initial number of draws is larger than
195
- the sample size by the given scale factor.
196
- """
197
-
198
- HSR_TEN = 1.234567
199
- """
200
- For alternative HSR filing requirement,
201
-
202
- When filing requirement is assumed met if merging-firm shares exceed 10:1 ratio
203
- to each other.
204
- """
205
-
206
- MNL_DEP = 1.25
207
- """
208
- For restricted PCM's.
209
-
210
- When merging firm's PCMs are constrained for consistency with f.o.c.s from
211
- profit maximization under Nash-Bertrand oligopoly with MNL demand.
212
- """
213
-
214
- ONE = 1.00
215
- """When initial set of draws is not restricted in any way."""
216
-
217
-
218
- # share_spec dist_type validator:
219
- def _share_spec_validator(
220
- _instance: MarketSampleSpec, _attribute: attrs.Attribute, _value: ShareSpec, /
221
- ) -> None:
222
- _r_bar = _instance.recapture_rate
223
- if _value.dist_type == SHRConstants.UNI:
224
- if _value.recapture_spec == RECConstants.OUTIN:
225
- raise ValueError(
226
- f"Invalid recapture specification, {_value.recapture_spec!r} "
227
- "for market share specification with Uniform distribution. "
228
- "Redefine the market-sample specification, modifying the ."
229
- "market-share specification or the recapture specification."
230
- )
231
- elif _value.firm_counts_prob_weights is not None:
232
- raise ValueError(
233
- "Generated data for markets with specified firm-counts or "
234
- "varying firm counts are not feasible with market shares "
235
- "with Uniform distribution. Consider revising the "
236
- r"distribution type to {SHRConstants.DIR_FLAT}, which gives "
237
- "uniformly distributed draws on the :math:`n+1` simplex "
238
- "for firm-count, :math:`n`."
239
- )
240
- # Outside-in calibration only valid for Dir-distributed shares
241
- elif _value.recapture_spec != RECConstants.OUTIN and (
242
- _r_bar is None or not isinstance(_r_bar, float)
243
- ):
244
- raise ValueError(
245
- f"Recapture specification, {_value.recapture_spec!r} requires that "
246
- "the market sample specification inclues a recapture rate."
247
- )
248
-
249
-
250
- def _pcm_spec_validator(
251
- _instance: MarketSampleSpec, _attribute: attrs.Attribute, _value: PCMSpec, /
252
- ) -> None:
253
- if (
254
- _instance.share_spec.recapture_spec == RECConstants.FIXED
255
- and _value.firm2_margin_restrictions == FM2Constants.MNL
256
- ):
257
- raise ValueError(
258
- "{} {} {}".format(
259
- f'Specification of "recapture_spec", "{_instance.share_spec.recapture_spec}"',
260
- "requires Firm 2 margin must have property, ",
261
- f'"{FM2Constants.IID}" or "{FM2Constants.SYM}".',
262
- )
263
- )
264
- elif _value.dist_type.name.startswith("BETA"):
265
- if _value.dist_parms is None:
266
- pass
267
- elif np.array_equal(_value.dist_parms, DIST_PARMS_DEFAULT):
268
- raise ValueError(
269
- f"The distribution parameters, {DIST_PARMS_DEFAULT!r} "
270
- "are not valid with margin distribution, {_dist_type_pcm!r}"
271
- )
272
- elif (
273
- _value.dist_type == PCMConstants.BETA
274
- and len(_value.dist_parms) != len(("max", "min"))
275
- ) or (
276
- _value.dist_type == PCMConstants.BETA_BND
277
- and len(_value.dist_parms) != len(("mu", "sigma", "max", "min"))
278
- ):
279
- raise ValueError(
280
- f"Given number, {len(_value.dist_parms)} of parameters "
281
- f'for PCM with distribution, "{_value.dist_type}" is incorrect.'
282
- )
283
-
284
-
285
- @attrs.define(slots=True, frozen=True)
286
- class MarketSampleSpec:
287
- """Parameter specification for market data generation."""
288
-
289
- sample_size: int = 10**6
290
- """sample size generated"""
291
-
292
- recapture_rate: float | None = None
293
- """market recapture rate
294
-
295
- Is None if market share specification includes
296
- generation of outside good choice probabilities (RECConstants.OUTIN).
297
- """
298
- pr_sym_spec: PRIConstants = PRIConstants.SYM
299
- """Price specification, see PRIConstants"""
300
-
301
- share_spec: ShareSpec = attrs.field(
302
- kw_only=True,
303
- default=ShareSpec(RECConstants.INOUT, SHRConstants.UNI, None, None),
304
- validator=_share_spec_validator,
305
- )
306
- """See definition of ShareSpec"""
307
-
308
- pcm_spec: PCMSpec = attrs.field(
309
- kw_only=True,
310
- default=PCMSpec(PCMConstants.UNI, FM2Constants.IID, None),
311
- validator=_pcm_spec_validator,
312
- )
313
- """See definition of PCMSpec"""
314
-
315
- hsr_filing_test_type: SSZConstants = attrs.field(
316
- kw_only=True, default=SSZConstants.ONE
317
- )
318
- """Method for modeling HSR filing threholds, see SSZConstants"""
319
-
320
-
321
- class MarketsSample(NamedTuple):
322
- """Container for generated markets data sample."""
323
-
324
- frmshr_array: NDArray[np.floating]
325
- """Merging-firm shares (with two merging firms)"""
326
-
327
- pcm_array: NDArray[np.floating]
328
- """Merging-firms' prices (normalized to 1, in default specification)"""
329
-
330
- price_array: NDArray[np.floating]
331
- """Merging-firms' price-cost margins (PCM)"""
332
-
333
- fcounts: NDArray[np.integer]
334
- """Number of firms in market"""
335
-
336
- ratio_choice_prob_to_mktshr: NDArray[np.floating]
337
- """
338
- One (1) minus probability that the outside good is chosen
339
-
340
- Converts market shares to choice probabilities by multiplication.
341
- """
342
-
343
- nth_firm_share: NDArray[np.floating]
344
- """Market-share of n-th firm
345
-
346
- Relevant for testing for draws the do or
347
- do not meet HSR filing thresholds.
348
- """
349
-
350
- divr_array: NDArray[np.floating]
351
- """Diversion ratio between the merging firms"""
352
-
353
- hhi_post: NDArray[np.floating]
354
- """Post-merger change in Herfindahl-Hirschmann Index (HHI)"""
355
-
356
- hhi_delta: NDArray[np.floating]
357
- """Change in HHI from combination of merging firms"""
358
-
359
-
360
- class ShareDataSample(NamedTuple):
361
- """Container for generated market shares.
362
-
363
- Includes related measures of market structure
364
- and aggregate purchase probability.
365
- """
366
-
367
- mktshr_array: NDArray[np.float64]
368
- """All-firm shares (with two merging firms)"""
369
-
370
- fcounts: NDArray[np.int64]
371
- """All-firm-count for each draw"""
372
-
373
- nth_firm_share: NDArray[np.float64]
374
- """Market-share of n-th firm"""
375
-
376
- aggregate_purchase_prob: NDArray[np.float64]
377
- """Converts market shares to choice probabilities by multiplication."""
378
-
379
-
380
- class PriceDataSample(NamedTuple):
381
- """Container for generated price array, and related."""
382
-
383
- price_array: NDArray[np.floating]
384
- """Merging-firms' prices"""
385
-
386
- hsr_filing_test: NDArray[np.bool_]
387
- """Flags draws as meeting HSR filing thresholds or not"""
388
-
389
-
390
- class MarginDataSample(NamedTuple):
391
- """Container for generated margin array and related MNL test array."""
392
-
393
- pcm_array: NDArray[np.float64]
394
- """Merging-firms' PCMs"""
395
-
396
- mnl_test_array: NDArray[np.bool_]
397
- """Flags infeasible observations as False and rest as True
398
-
399
- Applying restrictions from Bertrand-Nash oligopoly
400
- with MNL demand results in draws of Firm 2 PCM falling
401
- outside the feabile interval,:math:`[0, 1]`, depending
402
- on the configuration of merging firm shares. Such draws
403
- are are flagged as infeasible (False)in :code:`mnl_test_array` while
404
- draws with PCM values within the feasible range are
405
- flagged as True. Used from filtering-out draws with
406
- infeasible PCM.
407
- """
408
37
 
409
38
 
410
39
  def gen_market_sample(
@@ -413,7 +42,7 @@ def gen_market_sample(
413
42
  *,
414
43
  seed_seq_list: list[SeedSequence] | None = None,
415
44
  nthreads: int = 16,
416
- ) -> MarketsSample:
45
+ ) -> MarketDataSample:
417
46
  """
418
47
  Generate share, diversion ratio, price, and margin data based on supplied parameters
419
48
 
@@ -425,8 +54,8 @@ def gen_market_sample(
425
54
  for generating the relevant random variates:
426
55
  1.) quantity shares
427
56
  2.) price-cost margins
428
- 3.) firm-counts, from :code:`[2, 2 + len(firm_counts_prob_weights)]`,
429
- weighted by :code:`firm_counts_prob_weights`, where relevant
57
+ 3.) firm-counts, from :code:`[2, 2 + len(firm_counts_weights)]`,
58
+ weighted by :code:`firm_counts_weights`, where relevant
430
59
  4.) prices, if :code:`pr_sym_spec == PRIConstants.ZERO`.
431
60
 
432
61
  Parameters
@@ -448,8 +77,9 @@ def gen_market_sample(
448
77
 
449
78
  _mkt_sample_spec = _mkt_sample_spec or MarketSampleSpec()
450
79
 
451
- _recapture_spec, _dist_type_mktshr, _, _ = _mkt_sample_spec.share_spec
452
- _, _dist_firm2_pcm, _ = _mkt_sample_spec.pcm_spec
80
+ _recapture_spec = _mkt_sample_spec.share_spec.recapture_spec
81
+ _dist_type_mktshr = _mkt_sample_spec.share_spec.dist_type
82
+ _dist_firm2_pcm = _mkt_sample_spec.pcm_spec.firm2_pcm_constraint
453
83
  _hsr_filing_test_type = _mkt_sample_spec.hsr_filing_test_type
454
84
 
455
85
  (
@@ -476,21 +106,28 @@ def gen_market_sample(
476
106
  _mkt_sample_spec_here, _fcount_rng_seed_seq, _mktshr_rng_seed_seq, nthreads
477
107
  )
478
108
 
109
+ _mktshr_array, _fcounts, _aggregate_purchase_prob, _nth_firm_share = (
110
+ getattr(_mktshr_data, _f)
111
+ for _f in (
112
+ "mktshr_array",
113
+ "fcounts",
114
+ "aggregate_purchase_prob",
115
+ "nth_firm_share",
116
+ )
117
+ )
118
+
479
119
  # Generate merging-firm price data
480
- _price_data = _gen_pr_ratio(
120
+ _price_data = _gen_pr_data(
481
121
  _mktshr_data.mktshr_array[:, :2],
482
122
  _mktshr_data.nth_firm_share,
483
123
  _mkt_sample_spec_here,
484
124
  _pr_rng_seed_seq,
485
125
  )
486
126
 
487
- _mktshr_array = _mktshr_data.mktshr_array
488
- _fcounts = _mktshr_data.fcounts
489
- _aggregate_purchase_prob = _mktshr_data.aggregate_purchase_prob
490
- _nth_firm_share = _mktshr_data.nth_firm_share
491
- _price_array = _price_data.price_array
492
- _hsr_filing_test = _price_data.hsr_filing_test
493
- del _mktshr_data, _price_data
127
+ _price_array, _hsr_filing_test = (
128
+ getattr(_price_data, _f) for _f in ("price_array", "hsr_filing_test")
129
+ )
130
+
494
131
  if _hsr_filing_test_type != SSZConstants.ONE:
495
132
  _mktshr_array = _mktshr_array[_hsr_filing_test]
496
133
  _fcounts = _fcounts[_hsr_filing_test]
@@ -507,7 +144,7 @@ def gen_market_sample(
507
144
  )
508
145
 
509
146
  # Generate margin data
510
- _pcm_array, _mnl_test_rows = _gen_pcm_data(
147
+ _pcm_data = _gen_pcm_data(
511
148
  _mktshr_array[:, :2],
512
149
  _mkt_sample_spec_here,
513
150
  _price_array,
@@ -515,6 +152,9 @@ def gen_market_sample(
515
152
  _pcm_rng_seed_seq,
516
153
  nthreads,
517
154
  )
155
+ _pcm_array, _mnl_test_rows = (
156
+ getattr(_pcm_data, _f) for _f in ("pcm_array", "mnl_test_array")
157
+ )
518
158
 
519
159
  _s_size = _mkt_sample_spec.sample_size # originally-specified sample size
520
160
  if _dist_firm2_pcm == FM2Constants.MNL:
@@ -535,7 +175,7 @@ def gen_market_sample(
535
175
  _hhi_delta + np.einsum("ij,ij->i", _mktshr_array, _mktshr_array)[:, None]
536
176
  )
537
177
 
538
- return MarketsSample(
178
+ return MarketDataSample(
539
179
  _frmshr_array,
540
180
  _pcm_array,
541
181
  _price_array,
@@ -585,454 +225,6 @@ def parse_seed_seq_list(
585
225
  )
586
226
 
587
227
 
588
- def _gen_share_data(
589
- _mkt_sample_spec: MarketSampleSpec,
590
- _fcount_rng_seed_seq: SeedSequence | None,
591
- _mktshr_rng_seed_seq: SeedSequence,
592
- _nthreads: int = 16,
593
- /,
594
- ) -> ShareDataSample:
595
- """Helper function for generating share data.
596
-
597
- Parameters
598
- ----------
599
- _mkt_sample_spec
600
- Class specifying parameters for share-, price-, and margin-data generation
601
- _fcount_rng_seed_seq
602
- Seed sequence for assuring independent and, optionally, redundant streams
603
- _mktshr_rng_seed_seq
604
- Seed sequence for assuring independent and, optionally, redundant streams
605
- _nthreads
606
- Must be specified for generating repeatable random streams
607
-
608
- Returns
609
- -------
610
- Arrays representing shares, diversion ratios, etc. structured as a :ShareDataSample:
611
-
612
- """
613
-
614
- _recapture_spec, _dist_type_mktshr, _dist_parms_mktshr, _firm_count_prob_wts_raw = (
615
- _mkt_sample_spec.share_spec
616
- )
617
-
618
- _ssz = _mkt_sample_spec.sample_size
619
-
620
- _r_bar = _mkt_sample_spec.recapture_rate or 0.80
621
-
622
- match _dist_type_mktshr:
623
- case SHRConstants.UNI:
624
- _mkt_share_sample = _gen_market_shares_uniform(
625
- _ssz, _dist_parms_mktshr, _mktshr_rng_seed_seq, _nthreads
626
- )
627
-
628
- case _ if _dist_type_mktshr.name.startswith("DIR_"):
629
- _firm_count_prob_wts = (
630
- None
631
- if _firm_count_prob_wts_raw is None
632
- else np.array(_firm_count_prob_wts_raw, dtype=np.float64)
633
- )
634
- _mkt_share_sample = _gen_market_shares_dirichlet_multisample(
635
- _ssz,
636
- _recapture_spec,
637
- _dist_type_mktshr,
638
- _dist_parms_mktshr,
639
- _firm_count_prob_wts,
640
- _fcount_rng_seed_seq,
641
- _mktshr_rng_seed_seq,
642
- _nthreads,
643
- )
644
-
645
- case _:
646
- raise ValueError(
647
- f'Unexpected type, "{_dist_type_mktshr}" for share distribution.'
648
- )
649
-
650
- # If recapture_spec == "inside-out", recalculate _aggregate_purchase_prob
651
- _frmshr_array = _mkt_share_sample.mktshr_array[:, :2]
652
- if _recapture_spec == RECConstants.INOUT:
653
- _mkt_share_sample = ShareDataSample(
654
- _mkt_share_sample.mktshr_array,
655
- _mkt_share_sample.fcounts,
656
- _mkt_share_sample.nth_firm_share,
657
- _r_bar / (1 - (1 - _r_bar) * _frmshr_array.min(axis=1, keepdims=True)),
658
- )
659
-
660
- return _mkt_share_sample
661
-
662
-
663
- def _gen_market_shares_uniform(
664
- _s_size: int = 10**6,
665
- _dist_parms_mktshr: NDArray[np.floating[TF]] | None = DIST_PARMS_DEFAULT,
666
- _mktshr_rng_seed_seq: SeedSequence | None = None,
667
- _nthreads: int = 16,
668
- /,
669
- ) -> ShareDataSample:
670
- """Generate merging-firm shares from Uniform distribution on the 3-D simplex.
671
-
672
- Parameters
673
- ----------
674
- _s_size
675
- size of sample to be drawn
676
- _r_bar
677
- market recapture rate
678
- _mktshr_rng_seed_seq
679
- seed for rng, so results can be made replicable
680
- _nthreads
681
- number of threads for random number generation
682
-
683
- Returns
684
- -------
685
- market shares and other market statistics for each draw (market)
686
-
687
- """
688
-
689
- _frmshr_array = np.empty((_s_size, 2), dtype=np.float64)
690
- _dist_parms_mktshr = (
691
- DIST_PARMS_DEFAULT if _dist_parms_mktshr is None else _dist_parms_mktshr # type: ignore
692
- )
693
- _mrng = MultithreadedRNG(
694
- _frmshr_array,
695
- dist_type="Uniform",
696
- dist_parms=_dist_parms_mktshr,
697
- seed_sequence=_mktshr_rng_seed_seq,
698
- nthreads=_nthreads,
699
- )
700
- _mrng.fill()
701
- # Convert draws on U[0, 1] to Uniformly-distributed draws on simplex, s_1 + s_2 < 1
702
- _frmshr_array = np.sort(_frmshr_array, axis=1)
703
- _frmshr_array = np.column_stack((
704
- _frmshr_array[:, 0],
705
- _frmshr_array[:, 1] - _frmshr_array[:, 0],
706
- ))
707
-
708
- # Keep only share combinations representing feasible mergers
709
- _frmshr_array = _frmshr_array[_frmshr_array.min(axis=1) > 0]
710
-
711
- # Let a third column have values of "np.nan", so HHI calculations return "np.nan"
712
- _mktshr_array = np.pad(
713
- _frmshr_array, ((0, 0), (0, 1)), "constant", constant_values=np.nan
714
- )
715
-
716
- _fcounts: NDArray[np.int64] = np.ones((_s_size, 1), np.int64) * np.nan # type: ignore
717
- _nth_firm_share, _aggregate_purchase_prob = (
718
- np.nan * np.ones((_s_size, 1), np.float64) for _ in range(2)
719
- )
720
-
721
- return ShareDataSample(
722
- _mktshr_array, _fcounts, _nth_firm_share, _aggregate_purchase_prob
723
- )
724
-
725
-
726
- def _gen_market_shares_dirichlet_multisample(
727
- _s_size: int = 10**6,
728
- _recapture_spec: RECConstants = RECConstants.INOUT,
729
- _dist_type_dir: SHRConstants = SHRConstants.DIR_FLAT,
730
- _dist_parms_dir: NDArray[np.floating[TF]] | None = None,
731
- _firm_count_wts: NDArray[np.floating[TF]] | None = None, # type: ignore
732
- _fcount_rng_seed_seq: SeedSequence | None = None,
733
- _mktshr_rng_seed_seq: SeedSequence | None = None,
734
- _nthreads: int = 16,
735
- /,
736
- ) -> ShareDataSample:
737
- """Dirichlet-distributed shares with multiple firm-counts.
738
-
739
- Firm-counts may be specified as having Uniform distribution over the range
740
- of firm counts, or a set of probability weights may be specified. In the
741
- latter case the proportion of draws for each firm-count matches the
742
- specified probability weight.
743
-
744
- Parameters
745
- ----------
746
- _s_size
747
- sample size to be drawn
748
- _r_bar
749
- market recapture rate
750
- _firm_count_wts
751
- firm count weights array for sample to be drawn
752
- _dist_type_dir
753
- Whether Dirichlet is Flat or Asymmetric
754
- _recapture_spec
755
- r_1 = r_2 if "proportional", otherwise MNL-consistent
756
- _fcount_rng_seed_seq
757
- seed firm count rng, for replicable results
758
- _mktshr_rng_seed_seq
759
- seed market share rng, for replicable results
760
- _nthreads
761
- number of threads for parallelized random number generation
762
-
763
- Returns
764
- -------
765
- array of market shares and other market statistics
766
-
767
- """
768
-
769
- _firm_count_wts: np.float64 = (
770
- FCOUNT_WTS_DEFAULT if _firm_count_wts is None else _firm_count_wts
771
- ) # type: ignore
772
-
773
- _min_choice_wt = 0.03 if _dist_type_dir == SHRConstants.DIR_FLAT_CONSTR else 0.00
774
- _fcount_keys, _choice_wts = zip(
775
- *(
776
- _f
777
- for _f in zip(
778
- 2 + np.arange(len(_firm_count_wts)), # type: ignore
779
- _firm_count_wts / _firm_count_wts.sum(),
780
- strict=True,
781
- )
782
- if _f[1] > _min_choice_wt
783
- )
784
- )
785
- _choice_wts = _choice_wts / sum(_choice_wts)
786
-
787
- _fc_max = _fcount_keys[-1]
788
- _dir_alphas_full = (
789
- [1.0] * _fc_max if _dist_parms_dir is None else _dist_parms_dir[:_fc_max]
790
- )
791
- if _dist_type_dir == SHRConstants.DIR_ASYM:
792
- _dir_alphas_full = [2.0] * 6 + [1.5] * 5 + [1.25] * min(7, _fc_max)
793
-
794
- if _dist_type_dir == SHRConstants.DIR_COND:
795
-
796
- def _gen_dir_alphas(_fcv: int) -> NDArray[np.float64]:
797
- _dat = [2.5] * 2
798
- if _fcv > len(_dat):
799
- _dat += [1.0 / (_fcv - 2)] * (_fcv - 2)
800
- return np.array(_dat, dtype=np.float64)
801
-
802
- else:
803
-
804
- def _gen_dir_alphas(_fcv: int) -> NDArray[np.float64]:
805
- return np.array(_dir_alphas_full[:_fcv], dtype=np.float64)
806
-
807
- _fcounts = prng(_fcount_rng_seed_seq).choice(
808
- _fcount_keys, size=(_s_size, 1), p=_choice_wts
809
- )
810
-
811
- _mktshr_seed_seq_ch = (
812
- _mktshr_rng_seed_seq.spawn(len(_fcount_keys))
813
- if isinstance(_mktshr_rng_seed_seq, SeedSequence)
814
- else SeedSequence(pool_size=8).spawn(len(_fcounts))
815
- )
816
-
817
- _aggregate_purchase_prob, _nth_firm_share = (
818
- np.empty((_s_size, 1)) for _ in range(2)
819
- )
820
- _mktshr_array = np.empty((_s_size, _fc_max), dtype=np.float64)
821
- for _f_val, _f_sseq in zip(_fcount_keys, _mktshr_seed_seq_ch, strict=True):
822
- _fcounts_match_rows = np.where(_fcounts == _f_val)[0]
823
- _dir_alphas_test = _gen_dir_alphas(_f_val)
824
-
825
- try:
826
- _mktshr_sample_f = _gen_market_shares_dirichlet(
827
- _dir_alphas_test,
828
- len(_fcounts_match_rows),
829
- _recapture_spec,
830
- _f_sseq,
831
- _nthreads,
832
- )
833
- except ValueError as _err:
834
- print(_f_val, len(_fcounts_match_rows))
835
- raise _err
836
-
837
- # Push data for present sample to parent
838
- _mktshr_array[_fcounts_match_rows] = np.pad(
839
- _mktshr_sample_f.mktshr_array,
840
- ((0, 0), (0, _fc_max - _mktshr_sample_f.mktshr_array.shape[1])),
841
- "constant",
842
- )
843
- _aggregate_purchase_prob[_fcounts_match_rows] = (
844
- _mktshr_sample_f.aggregate_purchase_prob
845
- )
846
- _nth_firm_share[_fcounts_match_rows] = _mktshr_sample_f.nth_firm_share
847
-
848
- if (_iss := np.round(np.einsum("ij->", _mktshr_array))) != _s_size or _iss != len(
849
- _mktshr_array
850
- ):
851
- raise ValueError(
852
- "DATA GENERATION ERROR: {} {} {}".format(
853
- "Generation of sample shares is inconsistent:",
854
- "array of drawn shares must some to the number of draws",
855
- "i.e., the sample size, which condition is not met.",
856
- )
857
- )
858
-
859
- return ShareDataSample(
860
- _mktshr_array, _fcounts, _nth_firm_share, _aggregate_purchase_prob
861
- )
862
-
863
-
864
- def _gen_market_shares_dirichlet(
865
- _dir_alphas: NDArray[np.floating[TF]],
866
- _s_size: int = 10**6,
867
- _recapture_spec: RECConstants = RECConstants.INOUT,
868
- _mktshr_rng_seed_seq: SeedSequence | None = None,
869
- _nthreads: int = 16,
870
- /,
871
- ) -> ShareDataSample:
872
- """Dirichlet-distributed shares with fixed firm-count.
873
-
874
- Parameters
875
- ----------
876
- _dir_alphas
877
- Shape parameters for Dirichlet distribution
878
- _s_size
879
- sample size to be drawn
880
- _r_bar
881
- market recapture rate
882
- _recapture_spec
883
- r_1 = r_2 if RECConstants.FIXED, otherwise MNL-consistent. If
884
- RECConstants.OUTIN; the number of columns in the output share array
885
- is len(_dir_alphas) - 1.
886
- _mktshr_rng_seed_seq
887
- seed market share rng, for replicable results
888
- _nthreads
889
- number of threads for parallelized random number generation
890
-
891
- Returns
892
- -------
893
- array of market shares and other market statistics
894
-
895
- """
896
-
897
- if not isinstance(_dir_alphas, np.ndarray):
898
- _dir_alphas = np.array(_dir_alphas)
899
-
900
- if _recapture_spec == RECConstants.OUTIN:
901
- _dir_alphas = np.concatenate((_dir_alphas, _dir_alphas[-1:]))
902
-
903
- _mktshr_seed_seq_ch = (
904
- _mktshr_rng_seed_seq
905
- if isinstance(_mktshr_rng_seed_seq, SeedSequence)
906
- else SeedSequence(pool_size=8)
907
- )
908
-
909
- _mktshr_array = np.empty((_s_size, len(_dir_alphas)))
910
- _mrng = MultithreadedRNG(
911
- _mktshr_array,
912
- dist_type="Dirichlet",
913
- dist_parms=_dir_alphas,
914
- seed_sequence=_mktshr_seed_seq_ch,
915
- nthreads=_nthreads,
916
- )
917
- _mrng.fill()
918
-
919
- if (_iss := np.round(np.einsum("ij->", _mktshr_array))) != _s_size or _iss != len(
920
- _mktshr_array
921
- ):
922
- print(_dir_alphas, _iss, repr(_s_size), len(_mktshr_array))
923
- print(repr(_mktshr_array[-10:, :]))
924
- raise ValueError(
925
- "DATA GENERATION ERROR: {} {} {}".format(
926
- "Generation of sample shares is inconsistent:",
927
- "array of drawn shares must sum to the number of draws",
928
- "i.e., the sample size, which condition is not met.",
929
- )
930
- )
931
-
932
- # If recapture_spec == 'inside_out', further calculations downstream
933
- _aggregate_purchase_prob = np.nan * np.empty((_s_size, 1))
934
- if _recapture_spec == RECConstants.OUTIN:
935
- _aggregate_purchase_prob = 1 - _mktshr_array[:, [-1]]
936
- _mktshr_array = _mktshr_array[:, :-1] / _aggregate_purchase_prob
937
-
938
- return ShareDataSample(
939
- _mktshr_array,
940
- (_mktshr_array.shape[-1] * np.ones((_s_size, 1))).astype(np.int64),
941
- _mktshr_array[:, [-1]],
942
- _aggregate_purchase_prob,
943
- )
944
-
945
-
946
- def _gen_pr_ratio(
947
- _frmshr_array: NDArray[np.floating[TF]],
948
- _nth_firm_share: NDArray[np.floating[TF]],
949
- _mkt_sample_spec: MarketSampleSpec,
950
- _seed_seq: SeedSequence | None = None,
951
- /,
952
- ) -> PriceDataSample:
953
- _ssz = len(_frmshr_array)
954
-
955
- _hsr_filing_test_type = _mkt_sample_spec.hsr_filing_test_type
956
-
957
- _price_array, _price_ratio_array, _hsr_filing_test = (
958
- np.ones_like(_frmshr_array),
959
- np.empty_like(_frmshr_array),
960
- np.empty(_ssz, dtype=bool),
961
- )
962
-
963
- _pr_max_ratio = 5.0
964
- match _mkt_sample_spec.pr_sym_spec:
965
- case PRIConstants.SYM:
966
- _nth_firm_price = np.ones((_ssz, 1))
967
- case PRIConstants.POS:
968
- _price_array, _nth_firm_price = (
969
- np.ceil(_p * _pr_max_ratio) for _p in (_frmshr_array, _nth_firm_share)
970
- )
971
- case PRIConstants.NEG:
972
- _price_array, _nth_firm_price = (
973
- np.ceil((1 - _p) * _pr_max_ratio)
974
- for _p in (_frmshr_array, _nth_firm_share)
975
- )
976
- case PRIConstants.ZERO:
977
- _price_array_gen = prng(_seed_seq).choice(
978
- 1 + np.arange(_pr_max_ratio), size=(len(_frmshr_array), 3)
979
- )
980
- _price_array = _price_array_gen[:, :2]
981
- _nth_firm_price = _price_array_gen[:, [2]]
982
- # del _price_array_gen
983
- case _:
984
- raise ValueError(
985
- f"Condition regarding price symmetry"
986
- f' "{_mkt_sample_spec.pr_sym_spec.value}" is invalid.'
987
- )
988
- # del _pr_max_ratio
989
-
990
- _price_ratio_array = _price_array / _price_array[:, ::-1]
991
- _rev_array = _price_array * _frmshr_array
992
- _nth_firm_rev = _nth_firm_price * _nth_firm_share
993
-
994
- # Although `_test_rev_ratio_inv` is not fixed at 10%,
995
- # the ratio has not changed since inception of the HSR filing test,
996
- # so we treat it as a constant of merger policy.
997
- _test_rev_ratio, _test_rev_ratio_inv = 10, 1 / 10
998
-
999
- match _hsr_filing_test_type:
1000
- case SSZConstants.HSR_TEN:
1001
- # See, https://www.ftc.gov/enforcement/premerger-notification-program/
1002
- # -> Procedures For Submitting Post-Consummation Filings
1003
- # -> Key Elements to Determine Whether a Post Consummation Filing is Required
1004
- # under heading, "Historical Thresholds"
1005
- # Revenue ratio has been 10-to-1 since inception
1006
- # Thus, a simple form of the HSR filing test would impose a 10-to-1
1007
- # ratio restriction on the merging firms' revenues
1008
- _rev_ratio = (_rev_array.min(axis=1) / _rev_array.max(axis=1)).round(4)
1009
- _hsr_filing_test = _rev_ratio >= _test_rev_ratio_inv
1010
- # del _rev_array, _rev_ratio
1011
- case SSZConstants.HSR_NTH:
1012
- # To get around the 10-to-1 ratio restriction, specify that the nth firm
1013
- # matches the smaller firm in the size test; then if the smaller merging firm
1014
- # matches the n-th firm in size, and the larger merging firm has at least
1015
- # 10 times the size of the nth firm, the size test is considered met.
1016
- # Alternatively, if the smaller merging firm has 10% or greater share,
1017
- # the value of transaction test is considered met.
1018
- _rev_ratio_to_nth = np.round(np.sort(_rev_array, axis=1) / _nth_firm_rev, 4)
1019
- _hsr_filing_test = (
1020
- np.einsum(
1021
- "ij->i",
1022
- 1 * (_rev_ratio_to_nth > [1, _test_rev_ratio]),
1023
- dtype=np.int64,
1024
- )
1025
- == _rev_ratio_to_nth.shape[1]
1026
- ) | (_frmshr_array.min(axis=1) >= _test_rev_ratio_inv)
1027
-
1028
- # del _nth_firm_rev, _rev_ratio_to_nth
1029
- case _:
1030
- # Otherwise, all draws meet the filing test
1031
- _hsr_filing_test = np.ones(_ssz, dtype=bool)
1032
-
1033
- return PriceDataSample(_price_array, _hsr_filing_test)
1034
-
1035
-
1036
228
  def gen_divr_array(
1037
229
  _frmshr_array: NDArray[np.floating[TF]],
1038
230
  _r_bar: float,
@@ -1091,142 +283,3 @@ def gen_divr_array(
1091
283
  )
1092
284
 
1093
285
  return _divr_array
1094
-
1095
-
1096
- def _gen_pcm_data(
1097
- _frmshr_array: NDArray[np.floating[TF]],
1098
- _mkt_sample_spec: MarketSampleSpec,
1099
- _price_array: NDArray[np.floating[TF]],
1100
- _aggregate_purchase_prob: NDArray[np.floating[TF]],
1101
- _pcm_rng_seed_seq: SeedSequence,
1102
- _nthreads: int = 16,
1103
- /,
1104
- ) -> MarginDataSample:
1105
- _recapture_spec, _, _, _ = _mkt_sample_spec.share_spec
1106
- _dist_type_pcm, _dist_firm2_pcm, _dist_parms_pcm = _mkt_sample_spec.pcm_spec
1107
- _dist_type: Literal["Beta", "Uniform"] = (
1108
- "Uniform" if _dist_type_pcm == PCMConstants.UNI else "Beta"
1109
- )
1110
-
1111
- _pcm_array = np.empty((len(_frmshr_array), 2), dtype=np.float64)
1112
- _mnl_test_array = np.empty((len(_frmshr_array), 2), dtype=int)
1113
-
1114
- _beta_min, _beta_max = [None] * 2 # placeholder
1115
- _dist_parms = np.ones(2, np.float64)
1116
- if _dist_type_pcm == PCMConstants.EMPR:
1117
- _pcm_array = resample_mgn_data(
1118
- _pcm_array.shape, # type: ignore
1119
- seed_sequence=_pcm_rng_seed_seq,
1120
- )
1121
- else:
1122
- if _dist_type_pcm == PCMConstants.UNI:
1123
- _dist_parms = (
1124
- DIST_PARMS_DEFAULT if _dist_parms_pcm is None else _dist_parms_pcm
1125
- )
1126
- elif _dist_type_pcm == PCMConstants.BETA:
1127
- # Error-checking (could move to validators in definition of MarketSampleSpec)
1128
-
1129
- if _dist_parms_pcm is None:
1130
- _dist_parms_pcm = _dist_parms
1131
-
1132
- elif _dist_type_pcm == PCMConstants.BETA_BND: # Bounded beta
1133
- if _dist_parms_pcm is None:
1134
- _dist_parms_pcm = np.array([0, 1, 0, 1], np.float64)
1135
- _dist_parms = beta_located_bound(_dist_parms_pcm)
1136
-
1137
- _pcm_rng = MultithreadedRNG(
1138
- _pcm_array,
1139
- dist_type=_dist_type,
1140
- dist_parms=_dist_parms,
1141
- seed_sequence=_pcm_rng_seed_seq,
1142
- nthreads=_nthreads,
1143
- )
1144
- _pcm_rng.fill()
1145
- del _pcm_rng
1146
-
1147
- if _dist_type_pcm == PCMConstants.BETA_BND:
1148
- _beta_min, _beta_max = _dist_parms_pcm[2:] # type: ignore
1149
- _pcm_array = (_beta_max - _beta_min) * _pcm_array + _beta_min
1150
- del _beta_min, _beta_max
1151
-
1152
- if _dist_firm2_pcm == FM2Constants.MNL:
1153
- # Impose FOCs from profit-maximization with MNL demand
1154
- _purchprob_array = _aggregate_purchase_prob * _frmshr_array
1155
-
1156
- _pcm_array[:, [1]] = np.divide(
1157
- np.einsum(
1158
- "ij,ij,ij->ij",
1159
- _price_array[:, [0]],
1160
- _pcm_array[:, [0]],
1161
- 1 - _purchprob_array[:, [0]],
1162
- ),
1163
- np.einsum("ij,ij->ij", _price_array[:, [1]], 1 - _purchprob_array[:, [1]]),
1164
- )
1165
-
1166
- _mnl_test_array = _pcm_array[:, 1].__ge__(0) & _pcm_array[:, 1].__le__(1)
1167
- else:
1168
- _mnl_test_array = np.ones(len(_pcm_array), dtype=bool)
1169
- if _dist_firm2_pcm == FM2Constants.SYM:
1170
- _pcm_array[:, [1]] = _pcm_array[:, [0]]
1171
-
1172
- return MarginDataSample(_pcm_array, _mnl_test_array)
1173
-
1174
-
1175
- def _beta_located(
1176
- _mu: float | NDArray[np.float64], _sigma: float | NDArray[np.float64], /
1177
- ) -> NDArray[np.float64]:
1178
- """
1179
- Given mean and stddev, return shape parameters for corresponding Beta distribution
1180
-
1181
- Solve the first two moments of the standard Beta to get the shape parameters. [1]_
1182
-
1183
- Parameters
1184
- ----------
1185
- _mu
1186
- mean
1187
- _sigma
1188
- standardd deviation
1189
-
1190
- Returns
1191
- -------
1192
- shape parameters for Beta distribution
1193
-
1194
- References
1195
- ----------
1196
- .. [1] NIST. https://www.itl.nist.gov/div898/handbook/eda/section3/eda366h.htm
1197
-
1198
- """
1199
- _mul = (_mu - _mu**2 - _sigma**2) / _sigma**2
1200
- return np.array([_mu * _mul, (1 - _mu) * _mul], dtype=np.float64)
1201
-
1202
-
1203
- def beta_located_bound(_dist_parms: NDArray[np.floating[TF]], /) -> NDArray[np.float64]:
1204
- R"""
1205
- Return shape parameters for a non-standard beta, given the mean, stddev, range
1206
-
1207
-
1208
- Recover the r.v.s as
1209
- :math:`\min + (\max - \min) \cdot \symup{Β}(a, b)`,
1210
- with `a` and `b` calculated from the specified mean (:math:`\mu`) and
1211
- variance (:math:`\sigma`). [7]_
1212
-
1213
- Parameters
1214
- ----------
1215
- _dist_parms
1216
- vector of :math:`\mu`, :math:`\sigma`, :math:`\mathtt{\min}`, and :math:`\mathtt{\max}` values
1217
-
1218
- Returns
1219
- -------
1220
- shape parameters for Beta distribution
1221
-
1222
- Notes
1223
- -----
1224
- For example, ``beta_located_bound(np.array([0.5, 0.2887, 0.0, 1.0]))``.
1225
-
1226
- References
1227
- ----------
1228
- .. [7] NIST. https://www.itl.nist.gov/div898/handbook/eda/section3/eda366h.htm
1229
- """ # noqa: RUF002
1230
-
1231
- _bmu, _bsigma, _bmin, _bmax = _dist_parms
1232
- return _beta_located((_bmu - _bmin) / (_bmax - _bmin), _bsigma / (_bmax - _bmin))