mergeron 2024.738953.1__py3-none-any.whl → 2025.739265.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.

Files changed (39) hide show
  1. mergeron/__init__.py +26 -6
  2. mergeron/core/__init__.py +5 -65
  3. mergeron/core/{damodaran_margin_data.py → empirical_margin_distribution.py} +74 -58
  4. mergeron/core/ftc_merger_investigations_data.py +147 -101
  5. mergeron/core/guidelines_boundaries.py +290 -1078
  6. mergeron/core/guidelines_boundary_functions.py +1128 -0
  7. mergeron/core/{guidelines_boundaries_specialized_functions.py → guidelines_boundary_functions_extra.py} +87 -55
  8. mergeron/core/pseudorandom_numbers.py +16 -22
  9. mergeron/data/__init__.py +3 -0
  10. mergeron/data/damodaran_margin_data.xls +0 -0
  11. mergeron/data/damodaran_margin_data_dict.msgpack +0 -0
  12. mergeron/demo/__init__.py +3 -0
  13. mergeron/demo/visualize_empirical_margin_distribution.py +86 -0
  14. mergeron/gen/__init__.py +258 -246
  15. mergeron/gen/data_generation.py +473 -224
  16. mergeron/gen/data_generation_functions.py +876 -0
  17. mergeron/gen/enforcement_stats.py +355 -0
  18. mergeron/gen/upp_tests.py +171 -259
  19. mergeron-2025.739265.0.dist-info/METADATA +115 -0
  20. mergeron-2025.739265.0.dist-info/RECORD +23 -0
  21. {mergeron-2024.738953.1.dist-info → mergeron-2025.739265.0.dist-info}/WHEEL +1 -1
  22. mergeron/License.txt +0 -16
  23. mergeron/core/InCommon RSA Server CA cert chain.pem +0 -68
  24. mergeron/core/excel_helper.py +0 -257
  25. mergeron/core/proportions_tests.py +0 -520
  26. mergeron/ext/__init__.py +0 -5
  27. mergeron/ext/tol_colors.py +0 -851
  28. mergeron/gen/_data_generation_functions_nonpublic.py +0 -623
  29. mergeron/gen/investigations_stats.py +0 -709
  30. mergeron/jinja_LaTex_templates/clrrate_cis_summary_table_template.tex.jinja2 +0 -121
  31. mergeron/jinja_LaTex_templates/ftcinvdata_byhhianddelta_table_template.tex.jinja2 +0 -82
  32. mergeron/jinja_LaTex_templates/ftcinvdata_summary_table_template.tex.jinja2 +0 -57
  33. mergeron/jinja_LaTex_templates/ftcinvdata_summarypaired_table_template.tex.jinja2 +0 -104
  34. mergeron/jinja_LaTex_templates/mergeron.cls +0 -161
  35. mergeron/jinja_LaTex_templates/mergeron_table_collection_template.tex.jinja2 +0 -90
  36. mergeron/jinja_LaTex_templates/setup_tikz_tables.tex.jinja2 +0 -84
  37. mergeron-2024.738953.1.dist-info/METADATA +0 -93
  38. mergeron-2024.738953.1.dist-info/RECORD +0 -30
  39. /mergeron/{core → data}/ftc_invdata.msgpack +0 -0
@@ -0,0 +1,876 @@
1
+ """
2
+ Non-public functions called in data_generation.py
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Sequence
8
+ from typing import Literal
9
+
10
+ import numpy as np
11
+ from attrs import evolve
12
+ from numpy.random import SeedSequence
13
+
14
+ from .. import DEFAULT_REC_RATIO, VERSION, ArrayDouble, RECForm # noqa: TID252
15
+ from ..core.empirical_margin_distribution import mgn_data_resampler # noqa: TID252
16
+ from ..core.pseudorandom_numbers import ( # noqa: TID252
17
+ DEFAULT_DIST_PARMS,
18
+ MultithreadedRNG,
19
+ prng,
20
+ )
21
+ from . import (
22
+ DEFAULT_EMPTY_ARRAY,
23
+ DEFAULT_FCOUNT_WTS,
24
+ FM2Constraint,
25
+ MarginDataSample,
26
+ PCMDistribution,
27
+ PCMSpec,
28
+ PriceDataSample,
29
+ PriceSpec,
30
+ SeedSequenceData,
31
+ ShareDataSample,
32
+ ShareSpec,
33
+ SHRDistribution,
34
+ SSZConstant,
35
+ )
36
+
37
+ __version__ = VERSION
38
+
39
+
40
+ def gen_share_data(
41
+ _sample_size: int,
42
+ _share_spec: ShareSpec,
43
+ _fcount_rng_seed_seq: SeedSequence | None,
44
+ _mktshr_rng_seed_seq: SeedSequence,
45
+ _nthreads: int = 16,
46
+ /,
47
+ ) -> ShareDataSample:
48
+ """Helper function for generating share data.
49
+
50
+ Parameters
51
+ ----------
52
+ _share_spec
53
+ Class specifying parameters for generating market share data
54
+ _fcount_rng_seed_seq
55
+ Seed sequence for assuring independent and, optionally, redundant streams
56
+ _mktshr_rng_seed_seq
57
+ Seed sequence for assuring independent and, optionally, redundant streams
58
+ _nthreads
59
+ Must be specified for generating repeatable random streams
60
+
61
+ Returns
62
+ -------
63
+ Arrays representing shares, diversion ratios, etc. structured as a :ShareDataSample:
64
+
65
+ """
66
+
67
+ _recapture_form, _dist_type_mktshr, _dist_parms_mktshr, _firm_count_prob_wts = (
68
+ getattr(_share_spec, _f)
69
+ for _f in ("recapture_form", "dist_type", "dist_parms", "firm_counts_weights")
70
+ )
71
+
72
+ _ssz = _sample_size
73
+
74
+ if _dist_type_mktshr == SHRDistribution.UNI:
75
+ _mkt_share_sample = gen_market_shares_uniform(
76
+ _ssz, _dist_parms_mktshr, _mktshr_rng_seed_seq, _nthreads
77
+ )
78
+
79
+ elif _dist_type_mktshr.name.startswith("DIR_"):
80
+ _firm_count_prob_wts = (
81
+ None
82
+ if _firm_count_prob_wts is None
83
+ else np.array(_firm_count_prob_wts, dtype=np.float64)
84
+ )
85
+ _mkt_share_sample = gen_market_shares_dirichlet_multimarket(
86
+ _ssz,
87
+ _recapture_form,
88
+ _dist_type_mktshr,
89
+ _dist_parms_mktshr,
90
+ _firm_count_prob_wts,
91
+ _fcount_rng_seed_seq,
92
+ _mktshr_rng_seed_seq,
93
+ _nthreads,
94
+ )
95
+
96
+ else:
97
+ raise ValueError(
98
+ f'Unexpected type, "{_dist_type_mktshr}" for share distribution.'
99
+ )
100
+
101
+ # If recapture_form == "inside-out", recalculate _aggregate_purchase_prob
102
+ _frmshr_array = _mkt_share_sample.mktshr_array[:, :2]
103
+ _r_bar = _share_spec.recapture_ratio or DEFAULT_REC_RATIO
104
+ if _recapture_form == RECForm.INOUT:
105
+ _mkt_share_sample = ShareDataSample(
106
+ _mkt_share_sample.mktshr_array,
107
+ _mkt_share_sample.fcounts,
108
+ _mkt_share_sample.nth_firm_share,
109
+ _r_bar / (1 - (1 - _r_bar) * _frmshr_array.min(axis=1, keepdims=True)),
110
+ )
111
+
112
+ return _mkt_share_sample
113
+
114
+
115
+ def gen_market_shares_uniform(
116
+ _s_size: int = 10**6,
117
+ _dist_parms_mktshr: ArrayDouble | None = DEFAULT_DIST_PARMS,
118
+ _mktshr_rng_seed_seq: SeedSequence | None = None,
119
+ _nthreads: int = 16,
120
+ /,
121
+ ) -> ShareDataSample:
122
+ """Generate merging-firm shares from Uniform distribution on the 3-D simplex.
123
+
124
+ Parameters
125
+ ----------
126
+ _s_size
127
+ size of sample to be drawn
128
+
129
+ _mktshr_rng_seed_seq
130
+ seed for rng, so results can be made replicable
131
+
132
+ _nthreads
133
+ number of threads for random number generation
134
+
135
+ Returns
136
+ -------
137
+ market shares and other market statistics for each draw (market)
138
+
139
+ """
140
+
141
+ _frmshr_array = np.empty((_s_size, 2), dtype=np.float64)
142
+
143
+ _dist_parms_mktshr = _dist_parms_mktshr or DEFAULT_DIST_PARMS
144
+ _mrng = MultithreadedRNG(
145
+ _frmshr_array,
146
+ dist_type="Uniform",
147
+ dist_parms=_dist_parms_mktshr,
148
+ seed_sequence=_mktshr_rng_seed_seq,
149
+ nthreads=_nthreads,
150
+ )
151
+ _mrng.fill()
152
+ # Convert draws on U[0, 1] to Uniformly-distributed draws on simplex, s_1 + s_2 <= 1
153
+ _frmshr_array.sort(axis=1)
154
+ _frmshr_array = np.column_stack((
155
+ _frmshr_array[:, 0],
156
+ _frmshr_array[:, 1] - _frmshr_array[:, 0],
157
+ ))
158
+
159
+ # Keep only share combinations representing feasible mergers
160
+ # This is a no-op for 64-bit floats, but is necessary for 32-bit floats
161
+ _frmshr_array = _frmshr_array[_frmshr_array.min(axis=1) > 0]
162
+
163
+ # Let a third column have values of "np.nan", so HHI calculations return "np.nan"
164
+ _mktshr_array = np.pad(
165
+ _frmshr_array, ((0, 0), (0, 1)), "constant", constant_values=np.nan
166
+ )
167
+
168
+ _fcounts = np.empty((_s_size, 1), np.int64)
169
+ _nth_firm_share, _aggregate_purchase_prob = (
170
+ np.empty(_fcounts.shape, np.float64)
171
+ for _ in ("nth_firm_share", "aggregate_purchase_prob")
172
+ )
173
+
174
+ # This array is meant to be ignored, so a sentinel value is fine
175
+ _fcounts.fill(-9999)
176
+
177
+ _nth_firm_share.fill(np.nan)
178
+ _aggregate_purchase_prob.fill(np.nan)
179
+
180
+ return ShareDataSample(
181
+ _mktshr_array, _fcounts, _nth_firm_share, _aggregate_purchase_prob
182
+ )
183
+
184
+
185
+ def gen_market_shares_dirichlet_multimarket(
186
+ _s_size: int = 10**6,
187
+ _recapture_form: RECForm = RECForm.INOUT,
188
+ _dist_type_dir: SHRDistribution = SHRDistribution.DIR_FLAT,
189
+ _dist_parms_dir: ArrayDouble | None = None,
190
+ _firm_count_wts: ArrayDouble | None = None,
191
+ _fcount_rng_seed_seq: SeedSequence | None = None,
192
+ _mktshr_rng_seed_seq: SeedSequence | None = None,
193
+ _nthreads: int = 16,
194
+ /,
195
+ ) -> ShareDataSample:
196
+ """Dirichlet-distributed shares with multiple firm-counts.
197
+
198
+ Firm-counts may be specified as having Uniform distribution over the range
199
+ of firm counts, or a set of probability weights may be specified. In the
200
+ latter case the proportion of draws for each firm-count matches the
201
+ specified probability weight.
202
+
203
+ Parameters
204
+ ----------
205
+ _s_size
206
+ sample size to be drawn
207
+
208
+ _firm_count_wts
209
+ firm count weights array for sample to be drawn
210
+
211
+ _dist_type_dir
212
+ Whether Dirichlet is Flat or Asymmetric
213
+
214
+ _recapture_form
215
+ r_1 = r_2 if "proportional", otherwise MNL-consistent
216
+
217
+ _fcount_rng_seed_seq
218
+ seed firm count rng, for replicable results
219
+
220
+ _mktshr_rng_seed_seq
221
+ seed market share rng, for replicable results
222
+
223
+ _nthreads
224
+ number of threads for parallelized random number generation
225
+
226
+ Returns
227
+ -------
228
+ array of market shares and other market statistics
229
+
230
+ """
231
+
232
+ # _firm_count_wts: ArrayDouble = (
233
+ # DEFAULT_FCOUNT_WTS if _firm_count_wts is None else _firm_count_wts
234
+ # )
235
+ _firm_count_wts = DEFAULT_FCOUNT_WTS if _firm_count_wts is None else _firm_count_wts
236
+
237
+ _min_choice_wt = 0.03 if _dist_type_dir == SHRDistribution.DIR_FLAT_CONSTR else 0.00
238
+ _fcount_keys, _choice_wts = zip(
239
+ *(
240
+ _f
241
+ for _f in zip(
242
+ 2 + np.arange(len(_firm_count_wts)),
243
+ _firm_count_wts / _firm_count_wts.sum(),
244
+ strict=True,
245
+ )
246
+ if _f[1] > _min_choice_wt
247
+ )
248
+ )
249
+ _choice_wts = _choice_wts / sum(_choice_wts)
250
+
251
+ _fc_max = _fcount_keys[-1]
252
+ _dir_alphas_full = (
253
+ [1.0] * _fc_max if _dist_parms_dir is None else _dist_parms_dir[:_fc_max]
254
+ )
255
+ if _dist_type_dir == SHRDistribution.DIR_ASYM:
256
+ _dir_alphas_full = [2.0] * 6 + [1.5] * 5 + [1.25] * min(7, _fc_max)
257
+
258
+ if _dist_type_dir == SHRDistribution.DIR_COND:
259
+
260
+ def _gen_dir_alphas(_fcv: int) -> ArrayDouble:
261
+ _dat = [2.5] * 2
262
+ if _fcv > len(_dat):
263
+ _dat += [1.0 / (_fcv - 2)] * (_fcv - 2)
264
+ return np.array(_dat, dtype=np.float64)
265
+
266
+ else:
267
+
268
+ def _gen_dir_alphas(_fcv: int) -> ArrayDouble:
269
+ return np.array(_dir_alphas_full[:_fcv], dtype=np.float64)
270
+
271
+ _fcounts = prng(_fcount_rng_seed_seq).choice(
272
+ _fcount_keys, size=(_s_size, 1), p=_choice_wts
273
+ )
274
+
275
+ _mktshr_seed_seq_ch = (
276
+ _mktshr_rng_seed_seq.spawn(len(_fcount_keys))
277
+ if isinstance(_mktshr_rng_seed_seq, SeedSequence)
278
+ else SeedSequence(pool_size=8).spawn(len(_fcounts))
279
+ )
280
+
281
+ _aggregate_purchase_prob, _nth_firm_share = (
282
+ np.empty((_s_size, 1)) for _ in range(2)
283
+ )
284
+ _mktshr_array = np.empty((_s_size, _fc_max), dtype=np.float64)
285
+ for _f_val, _f_sseq in zip(_fcount_keys, _mktshr_seed_seq_ch, strict=True):
286
+ _fcounts_match_rows = np.where(_fcounts == _f_val)[0]
287
+ _dir_alphas_test = _gen_dir_alphas(_f_val)
288
+
289
+ try:
290
+ _mktshr_sample_f = gen_market_shares_dirichlet(
291
+ _dir_alphas_test,
292
+ len(_fcounts_match_rows),
293
+ _recapture_form,
294
+ _f_sseq,
295
+ _nthreads,
296
+ )
297
+ except ValueError as _err:
298
+ print(_f_val, len(_fcounts_match_rows))
299
+ raise _err
300
+
301
+ # Push data for present sample to parent
302
+ _mktshr_array[_fcounts_match_rows] = np.pad(
303
+ _mktshr_sample_f.mktshr_array,
304
+ ((0, 0), (0, _fc_max - _mktshr_sample_f.mktshr_array.shape[1])),
305
+ "constant",
306
+ )
307
+ _aggregate_purchase_prob[_fcounts_match_rows] = (
308
+ _mktshr_sample_f.aggregate_purchase_prob
309
+ )
310
+ _nth_firm_share[_fcounts_match_rows] = _mktshr_sample_f.nth_firm_share
311
+
312
+ if (_iss := np.round(np.einsum("ij->", _mktshr_array))) != _s_size or _iss != len(
313
+ _mktshr_array
314
+ ):
315
+ raise ValueError(
316
+ "DATA GENERATION ERROR: {} {} {}".format(
317
+ "Generation of sample shares is inconsistent:",
318
+ "array of drawn shares must some to the number of draws",
319
+ "i.e., the sample size, which condition is not met.",
320
+ )
321
+ )
322
+
323
+ return ShareDataSample(
324
+ _mktshr_array, _fcounts, _nth_firm_share, _aggregate_purchase_prob
325
+ )
326
+
327
+
328
+ def gen_market_shares_dirichlet(
329
+ _dir_alphas: ArrayDouble,
330
+ _s_size: int = 10**6,
331
+ _recapture_form: RECForm = RECForm.INOUT,
332
+ _mktshr_rng_seed_seq: SeedSequence | None = None,
333
+ _nthreads: int = 16,
334
+ /,
335
+ ) -> ShareDataSample:
336
+ """Dirichlet-distributed shares with fixed firm-count.
337
+
338
+ Parameters
339
+ ----------
340
+ _dir_alphas
341
+ Shape parameters for Dirichlet distribution
342
+
343
+ _s_size
344
+ sample size to be drawn
345
+
346
+ _recapture_form
347
+ r_1 = r_2 if RECForm.FIXED, otherwise MNL-consistent. If
348
+ RECForm.OUTIN; the number of columns in the output share array
349
+ is len(_dir_alphas) - 1.
350
+
351
+ _mktshr_rng_seed_seq
352
+ seed market share rng, for replicable results
353
+
354
+ _nthreads
355
+ number of threads for parallelized random number generation
356
+
357
+ Returns
358
+ -------
359
+ array of market shares and other market statistics
360
+
361
+ """
362
+
363
+ if not isinstance(_dir_alphas, np.ndarray):
364
+ _dir_alphas = np.array(_dir_alphas)
365
+
366
+ if _recapture_form == RECForm.OUTIN:
367
+ _dir_alphas = np.concatenate((_dir_alphas, _dir_alphas[-1:]))
368
+
369
+ _mktshr_seed_seq_ch = (
370
+ _mktshr_rng_seed_seq
371
+ if isinstance(_mktshr_rng_seed_seq, SeedSequence)
372
+ else SeedSequence(pool_size=8)
373
+ )
374
+
375
+ _mktshr_array = np.empty((_s_size, len(_dir_alphas)), dtype=np.float64)
376
+ _mrng = MultithreadedRNG(
377
+ _mktshr_array,
378
+ dist_type="Dirichlet",
379
+ dist_parms=_dir_alphas,
380
+ seed_sequence=_mktshr_seed_seq_ch,
381
+ nthreads=_nthreads,
382
+ )
383
+ _mrng.fill()
384
+
385
+ if (_iss := np.round(np.einsum("ij->", _mktshr_array))) != _s_size or _iss != len(
386
+ _mktshr_array
387
+ ):
388
+ print(_dir_alphas, _iss, repr(_s_size), len(_mktshr_array))
389
+ print(repr(_mktshr_array[-10:, :]))
390
+ raise ValueError(
391
+ "DATA GENERATION ERROR: {} {} {}".format(
392
+ "Generation of sample shares is inconsistent:",
393
+ "array of drawn shares must sum to the number of draws",
394
+ "i.e., the sample size, which condition is not met.",
395
+ )
396
+ )
397
+
398
+ # If recapture_form == 'inside_out', further calculations downstream
399
+ _aggregate_purchase_prob = np.empty((_s_size, 1), dtype=np.float64)
400
+ _aggregate_purchase_prob.fill(np.nan)
401
+ if _recapture_form == RECForm.OUTIN:
402
+ _aggregate_purchase_prob = 1 - _mktshr_array[:, [-1]] # type: ignore
403
+ _mktshr_array = _mktshr_array[:, :-1] / _aggregate_purchase_prob # type: ignore
404
+
405
+ return ShareDataSample(
406
+ _mktshr_array,
407
+ (_mktshr_array.shape[-1] * np.ones((_s_size, 1))).astype(np.int64),
408
+ _mktshr_array[:, [-1]],
409
+ _aggregate_purchase_prob,
410
+ )
411
+
412
+
413
+ def gen_divr_array(
414
+ _recapture_form: RECForm,
415
+ _recapture_ratio: float | None,
416
+ _frmshr_array: ArrayDouble,
417
+ _aggregate_purchase_prob: ArrayDouble = DEFAULT_EMPTY_ARRAY,
418
+ /,
419
+ ) -> ArrayDouble:
420
+ """
421
+ Given merging-firm shares and related parameters, return diverion ratios.
422
+
423
+ If recapture is specified as :attr:`mergeron.RECForm.OUTIN`, then the
424
+ choice-probability for the outside good must be supplied.
425
+
426
+ Parameters
427
+ ----------
428
+ _recapture_form
429
+ Enum specifying Fixed (proportional), Inside-out, or Outside-in
430
+
431
+ _recapture_ratio
432
+ If recapture is proportional or inside-out, the recapture ratio
433
+ for the firm with the smaller share.
434
+
435
+ _frmshr_array
436
+ Merging-firm shares.
437
+
438
+ _aggregate_purchase_prob
439
+ 1 minus probability that the outside good is chosen; converts
440
+ market shares to choice probabilities by multiplication.
441
+
442
+ Raises
443
+ ------
444
+ ValueError
445
+ If the firm with the smaller share does not have the larger
446
+ diversion ratio between the merging firms.
447
+
448
+ Returns
449
+ -------
450
+ Merging-firm diversion ratios for mergers in the sample.
451
+
452
+ """
453
+
454
+ _divr_array: ArrayDouble
455
+ if _recapture_form == RECForm.FIXED:
456
+ _divr_array = _recapture_ratio * _frmshr_array[:, ::-1] / (1 - _frmshr_array) # type: ignore
457
+
458
+ else:
459
+ _purchprob_array = _aggregate_purchase_prob * _frmshr_array
460
+ _divr_array = _purchprob_array[:, ::-1] / (1 - _purchprob_array)
461
+
462
+ _divr_assert_test = (
463
+ (np.round(np.einsum("ij->i", _frmshr_array), 15) == 1)
464
+ | (np.argmin(_frmshr_array, axis=1) == np.argmax(_divr_array, axis=1))
465
+ )[:, None]
466
+ if not all(_divr_assert_test):
467
+ raise ValueError(
468
+ "{} {} {} {}".format(
469
+ "Data construction fails tests:",
470
+ "the index of min(s_1, s_2) must equal",
471
+ "the index of max(d_12, d_21), for all draws.",
472
+ "unless frmshr_array sums to 1.00.",
473
+ )
474
+ )
475
+
476
+ return _divr_array
477
+
478
+
479
+ def gen_margin_price_data(
480
+ _frmshr_array: ArrayDouble,
481
+ _nth_firm_share: ArrayDouble,
482
+ _aggregate_purchase_prob: ArrayDouble,
483
+ _pcm_spec: PCMSpec,
484
+ _price_spec: PriceSpec,
485
+ _hsr_filing_test_type: SSZConstant,
486
+ _pcm_rng_seed_seq: SeedSequence,
487
+ _pr_rng_seed_seq: SeedSequence | None = None,
488
+ _nthreads: int = 16,
489
+ /,
490
+ ) -> tuple[MarginDataSample, PriceDataSample]:
491
+ """Generate margin and price data for mergers in the sample.
492
+
493
+ Parameters
494
+ ----------
495
+ _frmshr_array
496
+ Merging-firm shares; see :class:`mergeron.gen.ShareSpec`.
497
+
498
+ _nth_firm_share
499
+ Share of the nth firm in the sample.
500
+
501
+ _aggregate_purchase_prob
502
+ 1 minus probability that the outside good is chosen; converts
503
+ market shares to choice probabilities by multiplication.
504
+
505
+ _pcm_spec
506
+ Enum specifying whether to use asymmetric or flat margins. see
507
+ :class:`mergeron.gen.PCMSpec`.
508
+
509
+ _price_spec
510
+ Enum specifying whether to use symmetric, positive, or negative
511
+ margins; see :class:`mergeron.gen.PriceSpec`.
512
+
513
+ _hsr_filing_test_type
514
+ Enum specifying restriction, if any, to impose on market data sample
515
+ to model HSR filing requirements; see :class:`mergeron.gen.SSZConstant`.
516
+
517
+ _pcm_rng_seed_seq
518
+ Seed sequence for generating margin data.
519
+
520
+ _pr_rng_seed_seq
521
+ Seed sequence for generating price data.
522
+
523
+ _nthreads
524
+ Number of threads to use in generating price data.
525
+
526
+ Returns
527
+ -------
528
+ Simulated margin- and price-data arrays for mergers in the sample.
529
+ """
530
+
531
+ _margin_data = MarginDataSample(
532
+ np.empty_like(_frmshr_array), np.ones(len(_frmshr_array)) == 0
533
+ )
534
+
535
+ _price_array, _price_ratio_array = (
536
+ np.ones_like(_frmshr_array, np.float64),
537
+ np.empty_like(_frmshr_array, np.float64),
538
+ )
539
+
540
+ _pr_max_ratio = 5.0
541
+ match _price_spec:
542
+ case PriceSpec.SYM:
543
+ _nth_firm_price = np.ones((len(_frmshr_array), 1), np.float64)
544
+ case PriceSpec.POS:
545
+ _price_array, _nth_firm_price = (
546
+ np.ceil(_p * _pr_max_ratio) for _p in (_frmshr_array, _nth_firm_share)
547
+ )
548
+ case PriceSpec.NEG:
549
+ _price_array, _nth_firm_price = (
550
+ np.ceil((1 - _p) * _pr_max_ratio)
551
+ for _p in (_frmshr_array, _nth_firm_share)
552
+ )
553
+ case PriceSpec.ZERO:
554
+ _price_array_gen = prng(_pr_rng_seed_seq).choice(
555
+ 1 + np.arange(_pr_max_ratio), size=(len(_frmshr_array), 3)
556
+ )
557
+ _price_array = _price_array_gen[:, :2]
558
+ _nth_firm_price = _price_array_gen[:, [2]] # type: ignore
559
+ # del _price_array_gen
560
+ case PriceSpec.CSY:
561
+ # TODO:
562
+ # evolve FM2Constraint (save running MNL test twice); evolve copy of _mkt_sample_spec=1q
563
+ # generate the margin data
564
+ # generate price and margin data
565
+ _frmshr_array_plus = np.hstack((_frmshr_array, _nth_firm_share))
566
+ _pcm_spec_here = evolve(_pcm_spec, firm2_pcm_constraint=FM2Constraint.IID)
567
+ _margin_data = _gen_margin_data(
568
+ _frmshr_array_plus,
569
+ np.ones_like(_frmshr_array_plus, np.float64),
570
+ _aggregate_purchase_prob,
571
+ _pcm_spec_here,
572
+ _pcm_rng_seed_seq,
573
+ _nthreads,
574
+ )
575
+
576
+ _pcm_array, _mnl_test_array = (
577
+ getattr(_margin_data, _f) for _f in ("pcm_array", "mnl_test_array")
578
+ )
579
+
580
+ _price_array_here = 1 / (1 - _pcm_array)
581
+ _price_array = _price_array_here[:, :2]
582
+ _nth_firm_price = _price_array_here[:, [-1]]
583
+ if _pcm_spec.firm2_pcm_constraint == FM2Constraint.MNL:
584
+ # Generate i.i.d. PCMs then take PCM0 and construct PCM1
585
+ # Regenerate MNL test
586
+ _purchase_prob_array = _aggregate_purchase_prob * _frmshr_array
587
+ _pcm_array[:, 1] = np.divide(
588
+ (
589
+ _m1_nr := np.divide(
590
+ np.einsum(
591
+ "ij,ij,ij->ij",
592
+ _price_array[:, [0]],
593
+ _pcm_array[:, [0]],
594
+ 1 - _purchase_prob_array[:, [0]],
595
+ ),
596
+ 1 - _purchase_prob_array[:, [1]],
597
+ )
598
+ ),
599
+ 1 + _m1_nr,
600
+ )
601
+ _mnl_test_array = (_pcm_array[:, [1]] >= 0) & (_pcm_array[:, [1]] <= 1)
602
+
603
+ _margin_data = MarginDataSample(_pcm_array[:, :2], _mnl_test_array)
604
+ del _price_array_here
605
+ case _:
606
+ raise ValueError(
607
+ f'Specification of price distribution, "{_price_spec.value}" is invalid.'
608
+ )
609
+ if _price_spec != PriceSpec.CSY:
610
+ _margin_data = _gen_margin_data(
611
+ _frmshr_array,
612
+ _price_array,
613
+ _aggregate_purchase_prob,
614
+ _pcm_spec,
615
+ _pcm_rng_seed_seq,
616
+ _nthreads,
617
+ )
618
+
619
+ _price_array = _price_array.astype(np.float64)
620
+ _rev_array = _price_array * _frmshr_array
621
+ _nth_firm_rev = _nth_firm_price * _nth_firm_share
622
+
623
+ # Although `_test_rev_ratio_inv` is not fixed at 10%,
624
+ # the ratio has not changed since inception of the HSR filing test,
625
+ # so we treat it as a constant of merger enforcement policy.
626
+ _test_rev_ratio, _test_rev_ratio_inv = 10, 1 / 10
627
+
628
+ match _hsr_filing_test_type:
629
+ case SSZConstant.HSR_TEN:
630
+ # See, https://www.ftc.gov/enforcement/premerger-notification-program/
631
+ # -> Procedures For Submitting Post-Consummation Filings
632
+ # -> Key Elements to Determine Whether a Post Consummation Filing is Required
633
+ # under heading, "Historical Thresholds"
634
+ # Revenue ratio has been 10-to-1 since inception
635
+ # Thus, a simple form of the HSR filing test would impose a 10-to-1
636
+ # ratio restriction on the merging firms' revenues
637
+ _rev_ratio = (_rev_array.min(axis=1) / _rev_array.max(axis=1)).round(4)
638
+ _hsr_filing_test = _rev_ratio >= _test_rev_ratio_inv
639
+ # del _rev_array, _rev_ratio
640
+ case SSZConstant.HSR_NTH:
641
+ # To get around the 10-to-1 ratio restriction, specify that the nth firm test:
642
+ # if the smaller merging firm matches or exceeds the n-th firm in size, and
643
+ # the larger merging firm has at least 10 times the size of the nth firm,
644
+ # the size test is considered met.
645
+ # Alternatively, if the smaller merging firm has 10% or greater share,
646
+ # the value of transaction test is considered met.
647
+ _rev_ratio_to_nth = np.round(np.sort(_rev_array, axis=1) / _nth_firm_rev, 4)
648
+ _hsr_filing_test = (
649
+ np.einsum(
650
+ "ij->i",
651
+ 1 * (_rev_ratio_to_nth > [1, _test_rev_ratio]),
652
+ dtype=np.int64,
653
+ )
654
+ == _rev_ratio_to_nth.shape[1]
655
+ )
656
+
657
+ # del _nth_firm_rev, _rev_ratio_to_nth
658
+ case _:
659
+ # Otherwise, all draws meet the filing test
660
+ _hsr_filing_test = np.ones(len(_frmshr_array), dtype=bool)
661
+ _hsr_filing_test = _hsr_filing_test | (
662
+ _frmshr_array.min(axis=1) >= _test_rev_ratio_inv
663
+ )
664
+
665
+ return _margin_data, PriceDataSample(_price_array, _hsr_filing_test)
666
+
667
+
668
+ def _gen_margin_data(
669
+ _frmshr_array: ArrayDouble,
670
+ _price_array: ArrayDouble,
671
+ _aggregate_purchase_prob: ArrayDouble,
672
+ _pcm_spec: PCMSpec,
673
+ _pcm_rng_seed_seq: SeedSequence,
674
+ _nthreads: int = 16,
675
+ /,
676
+ ) -> MarginDataSample:
677
+ _dist_type_pcm, _dist_firm2_pcm, _dist_parms_pcm = (
678
+ getattr(_pcm_spec, _f)
679
+ for _f in ("dist_type", "firm2_pcm_constraint", "dist_parms")
680
+ )
681
+
682
+ _pcm_array = (
683
+ np.empty((len(_frmshr_array), 1), dtype=np.float64)
684
+ if _pcm_spec.firm2_pcm_constraint == FM2Constraint.SYM
685
+ else np.empty_like(_frmshr_array, dtype=np.float64)
686
+ )
687
+
688
+ _beta_min, _beta_max = [None] * 2 # placeholder
689
+ if _dist_type_pcm == PCMDistribution.EMPR:
690
+ _pcm_array = mgn_data_resampler(
691
+ _pcm_array.shape, seed_sequence=_pcm_rng_seed_seq
692
+ )
693
+ else:
694
+ _dist_type: Literal["Beta", "Uniform"]
695
+ if _dist_type_pcm in (PCMDistribution.BETA, PCMDistribution.BETA_BND):
696
+ _dist_type = "Beta"
697
+ _dist_parms_pcm = (
698
+ (
699
+ np.array([0, 1, 0, 1], np.float64)
700
+ if _dist_parms_pcm == PCMDistribution.BETA_BND
701
+ else np.ones(2, np.float64)
702
+ )
703
+ if _dist_parms_pcm is None
704
+ else _dist_parms_pcm
705
+ )
706
+ _dist_parms = beta_located_bound(_dist_parms_pcm)
707
+
708
+ else:
709
+ _dist_type = "Uniform"
710
+ _dist_parms = (
711
+ DEFAULT_DIST_PARMS if _dist_parms_pcm is None else _dist_parms_pcm
712
+ )
713
+
714
+ _pcm_rng = MultithreadedRNG(
715
+ _pcm_array,
716
+ dist_type=_dist_type,
717
+ dist_parms=_dist_parms,
718
+ seed_sequence=_pcm_rng_seed_seq,
719
+ nthreads=_nthreads,
720
+ )
721
+ _pcm_rng.fill()
722
+ del _pcm_rng
723
+
724
+ if _dist_type_pcm == PCMDistribution.BETA_BND:
725
+ _beta_min, _beta_max = _dist_parms_pcm[2:]
726
+ _pcm_array = (_beta_max - _beta_min) * _pcm_array + _beta_min
727
+ del _beta_min, _beta_max
728
+
729
+ if _dist_firm2_pcm == FM2Constraint.SYM:
730
+ _pcm_array = np.column_stack((_pcm_array,) * _frmshr_array.shape[1])
731
+ if _dist_firm2_pcm == FM2Constraint.MNL:
732
+ # Impose FOCs from profit-maximization with MNL demand
733
+ if _dist_type_pcm == PCMDistribution.EMPR:
734
+ print(
735
+ "NOTE: Estimated Firm 2 parameters will not be consistent with "
736
+ "the empirical distribution of margins in the source data. For "
737
+ "consistency, respecify pcm_spec.firm2_pcm_constraint = FM2Constraint.IID."
738
+ )
739
+ _purchase_prob_array = _aggregate_purchase_prob * _frmshr_array
740
+
741
+ _pcm_array[:, [1]] = np.divide(
742
+ np.einsum(
743
+ "ij,ij,ij->ij",
744
+ _price_array[:, [0]],
745
+ _pcm_array[:, [0]],
746
+ 1 - _purchase_prob_array[:, [0]],
747
+ ),
748
+ np.einsum(
749
+ "ij,ij->ij", _price_array[:, [1]], 1 - _purchase_prob_array[:, [1]]
750
+ ),
751
+ )
752
+
753
+ _mnl_test_array = _pcm_array[:, 1].__ge__(0) & _pcm_array[:, 1].__le__(1)
754
+ else:
755
+ _mnl_test_array = np.ones(len(_pcm_array), dtype=bool)
756
+
757
+ return MarginDataSample(_pcm_array, _mnl_test_array)
758
+
759
+
760
+ def _beta_located(
761
+ _mu: float | ArrayDouble, _sigma: float | ArrayDouble, /
762
+ ) -> ArrayDouble:
763
+ """
764
+ Given mean and stddev, return shape parameters for corresponding Beta distribution
765
+
766
+ Solve the first two moments of the standard Beta to get the shape parameters.
767
+
768
+ Parameters
769
+ ----------
770
+ _mu
771
+ mean
772
+ _sigma
773
+ standardd deviation
774
+
775
+ Returns
776
+ -------
777
+ shape parameters for Beta distribution
778
+
779
+ """
780
+
781
+ _mul = -1 + _mu * (1 - _mu) / _sigma**2
782
+ return np.array([_mu * _mul, (1 - _mu) * _mul], dtype=np.float64)
783
+
784
+
785
+ def beta_located_bound(_dist_parms: ArrayDouble, /) -> ArrayDouble:
786
+ R"""
787
+ Return shape parameters for a non-standard beta, given the mean, stddev, range
788
+
789
+
790
+ Recover the r.v.s as
791
+ :math:`\min + (\max - \min) \cdot \symup{Β}(a, b)`,
792
+ with `a` and `b` calculated from the specified mean (:math:`\mu`) and
793
+ variance (:math:`\sigma`). [8]_
794
+
795
+ Parameters
796
+ ----------
797
+ _dist_parms
798
+ vector of :math:`\mu`, :math:`\sigma`, :math:`\mathtt{\min}`, and :math:`\mathtt{\max}` values
799
+
800
+ Returns
801
+ -------
802
+ shape parameters for Beta distribution
803
+
804
+ Notes
805
+ -----
806
+ For example, ``beta_located_bound(np.array([0.5, 0.2887, 0.0, 1.0]))``.
807
+
808
+ References
809
+ ----------
810
+ .. [8] NIST, Beta Distribution. https://www.itl.nist.gov/div898/handbook/eda/section3/eda366h.htm
811
+ """ # noqa: RUF002
812
+
813
+ _bmu, _bsigma, _bmin, _bmax = _dist_parms
814
+ return _beta_located((_bmu - _bmin) / (_bmax - _bmin), _bsigma / (_bmax - _bmin))
815
+
816
+
817
+ def parse_seed_seq_list(
818
+ _sseq_list: Sequence[SeedSequence] | None,
819
+ _mktshr_dist_type: SHRDistribution,
820
+ _price_spec: PriceSpec,
821
+ /,
822
+ ) -> SeedSequenceData:
823
+ """Initialize RNG seed sequences to ensure independence of distinct random streams.
824
+
825
+ The tuple of SeedSequences, is parsed in the following order
826
+ for generating the relevant random variates:
827
+ 1.) quantity shares
828
+ 2.) price-cost margins
829
+ 3.) firm-counts, if :code:`MarketSpec.share_spec.dist_type` is a Dirichlet distribution
830
+ 4.) prices, if :code:`MarketSpec.price_spec ==`:attr:`mergeron.gen.PriceSpec.ZERO`.
831
+
832
+
833
+
834
+ Parameters
835
+ ----------
836
+ _sseq_list
837
+ List of RNG seed sequences
838
+
839
+ _mktshr_dist_type
840
+ Market share distribution type
841
+
842
+ _price_spec
843
+ Price specification
844
+
845
+ Returns
846
+ -------
847
+ Seed sequence data
848
+
849
+ """
850
+ _seed_count = 2 if _mktshr_dist_type == SHRDistribution.UNI else 3
851
+ _seed_count += 1 if _price_spec == PriceSpec.ZERO else 0
852
+
853
+ _fcount_rng_seed_seq: SeedSequence | None = None
854
+ _pr_rng_seed_seq: SeedSequence | None = None
855
+
856
+ _sseq_list = (
857
+ _sseq_list
858
+ if _sseq_list
859
+ else tuple(SeedSequence(pool_size=8) for _ in range(_seed_count))
860
+ )
861
+
862
+ if (_l := len(_sseq_list)) < _seed_count:
863
+ raise ValueError(
864
+ f"Seed sequence list must contain {_seed_count} seed sequences; "
865
+ f"only {_l} given."
866
+ )
867
+
868
+ _mktshr_rng_seed_seq, _pcm_rng_seed_seq = _sseq_list[:2]
869
+ _fcount_rng_seed_seq = (
870
+ None if _mktshr_dist_type == SHRDistribution.UNI else _sseq_list[2]
871
+ )
872
+ _pr_rng_seed_seq = _sseq_list[-1] if _price_spec == PriceSpec.ZERO else None
873
+
874
+ return SeedSequenceData(
875
+ _mktshr_rng_seed_seq, _pcm_rng_seed_seq, _fcount_rng_seed_seq, _pr_rng_seed_seq
876
+ )