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.

@@ -0,0 +1,626 @@
1
+ """
2
+ Non-public functions called in data_generation.py
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from importlib.metadata import version
8
+
9
+ from .. import _PKG_NAME # noqa: TID252
10
+
11
+ __version__ = version(_PKG_NAME)
12
+
13
+ from typing import Literal
14
+
15
+ import numpy as np
16
+ from numpy.random import SeedSequence
17
+ from numpy.typing import NDArray
18
+
19
+ from ..core.damodaran_margin_data import resample_mgn_data # noqa: TID252
20
+ from ..core.pseudorandom_numbers import ( # noqa: TID252
21
+ DIST_PARMS_DEFAULT,
22
+ MultithreadedRNG,
23
+ prng,
24
+ )
25
+ from . import (
26
+ FCOUNT_WTS_DEFAULT,
27
+ TF,
28
+ FM2Constants,
29
+ MarginDataSample,
30
+ MarketSampleSpec,
31
+ PCMConstants,
32
+ PriceDataSample,
33
+ PRIConstants,
34
+ RECConstants,
35
+ ShareDataSample,
36
+ SHRConstants,
37
+ SSZConstants,
38
+ )
39
+
40
+
41
+ def _gen_share_data(
42
+ _mkt_sample_spec: MarketSampleSpec,
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
+ _mkt_sample_spec
53
+ Class specifying parameters for share-, price-, and margin-data generation
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_spec, _dist_type_mktshr, _dist_parms_mktshr, _firm_count_prob_wts_raw = (
68
+ getattr(_mkt_sample_spec.share_spec, _f)
69
+ for _f in ("recapture_spec", "dist_type", "dist_parms", "firm_counts_weights")
70
+ )
71
+
72
+ _ssz = _mkt_sample_spec.sample_size
73
+
74
+ _r_bar = _mkt_sample_spec.recapture_rate or 0.80
75
+
76
+ match _dist_type_mktshr:
77
+ case SHRConstants.UNI:
78
+ _mkt_share_sample = _gen_market_shares_uniform(
79
+ _ssz, _dist_parms_mktshr, _mktshr_rng_seed_seq, _nthreads
80
+ )
81
+
82
+ case _ if _dist_type_mktshr.name.startswith("DIR_"):
83
+ _firm_count_prob_wts = (
84
+ None
85
+ if _firm_count_prob_wts_raw is None
86
+ else np.array(_firm_count_prob_wts_raw, dtype=np.float64)
87
+ )
88
+ _mkt_share_sample = _gen_market_shares_dirichlet_multisample(
89
+ _ssz,
90
+ _recapture_spec,
91
+ _dist_type_mktshr,
92
+ _dist_parms_mktshr,
93
+ _firm_count_prob_wts,
94
+ _fcount_rng_seed_seq,
95
+ _mktshr_rng_seed_seq,
96
+ _nthreads,
97
+ )
98
+
99
+ case _:
100
+ raise ValueError(
101
+ f'Unexpected type, "{_dist_type_mktshr}" for share distribution.'
102
+ )
103
+
104
+ # If recapture_spec == "inside-out", recalculate _aggregate_purchase_prob
105
+ _frmshr_array = _mkt_share_sample.mktshr_array[:, :2]
106
+ if _recapture_spec == RECConstants.INOUT:
107
+ _mkt_share_sample = ShareDataSample(
108
+ _mkt_share_sample.mktshr_array,
109
+ _mkt_share_sample.fcounts,
110
+ _mkt_share_sample.nth_firm_share,
111
+ _r_bar / (1 - (1 - _r_bar) * _frmshr_array.min(axis=1, keepdims=True)),
112
+ )
113
+
114
+ return _mkt_share_sample
115
+
116
+
117
+ def _gen_market_shares_uniform(
118
+ _s_size: int = 10**6,
119
+ _dist_parms_mktshr: NDArray[np.floating[TF]] | None = DIST_PARMS_DEFAULT,
120
+ _mktshr_rng_seed_seq: SeedSequence | None = None,
121
+ _nthreads: int = 16,
122
+ /,
123
+ ) -> ShareDataSample:
124
+ """Generate merging-firm shares from Uniform distribution on the 3-D simplex.
125
+
126
+ Parameters
127
+ ----------
128
+ _s_size
129
+ size of sample to be drawn
130
+ _r_bar
131
+ market recapture rate
132
+ _mktshr_rng_seed_seq
133
+ seed for rng, so results can be made replicable
134
+ _nthreads
135
+ number of threads for random number generation
136
+
137
+ Returns
138
+ -------
139
+ market shares and other market statistics for each draw (market)
140
+
141
+ """
142
+
143
+ _frmshr_array = np.empty((_s_size, 2), dtype=np.float64)
144
+ _dist_parms_mktshr = (
145
+ DIST_PARMS_DEFAULT if _dist_parms_mktshr is None else _dist_parms_mktshr # type: ignore
146
+ )
147
+ _mrng = MultithreadedRNG(
148
+ _frmshr_array,
149
+ dist_type="Uniform",
150
+ dist_parms=_dist_parms_mktshr,
151
+ seed_sequence=_mktshr_rng_seed_seq,
152
+ nthreads=_nthreads,
153
+ )
154
+ _mrng.fill()
155
+ # Convert draws on U[0, 1] to Uniformly-distributed draws on simplex, s_1 + s_2 < 1
156
+ _frmshr_array = np.sort(_frmshr_array, axis=1)
157
+ _frmshr_array = np.column_stack((
158
+ _frmshr_array[:, 0],
159
+ _frmshr_array[:, 1] - _frmshr_array[:, 0],
160
+ ))
161
+
162
+ # Keep only share combinations representing feasible mergers
163
+ _frmshr_array = _frmshr_array[_frmshr_array.min(axis=1) > 0]
164
+
165
+ # Let a third column have values of "np.nan", so HHI calculations return "np.nan"
166
+ _mktshr_array = np.pad(
167
+ _frmshr_array, ((0, 0), (0, 1)), "constant", constant_values=np.nan
168
+ )
169
+
170
+ _fcounts: NDArray[np.int64] = np.ones((_s_size, 1), np.int64) * np.nan # type: ignore
171
+ _nth_firm_share, _aggregate_purchase_prob = (
172
+ np.nan * np.ones((_s_size, 1), np.float64) for _ in range(2)
173
+ )
174
+
175
+ return ShareDataSample(
176
+ _mktshr_array, _fcounts, _nth_firm_share, _aggregate_purchase_prob
177
+ )
178
+
179
+
180
+ def _gen_market_shares_dirichlet_multisample(
181
+ _s_size: int = 10**6,
182
+ _recapture_spec: RECConstants = RECConstants.INOUT,
183
+ _dist_type_dir: SHRConstants = SHRConstants.DIR_FLAT,
184
+ _dist_parms_dir: NDArray[np.floating[TF]] | None = None,
185
+ _firm_count_wts: NDArray[np.floating[TF]] | None = None, # type: ignore
186
+ _fcount_rng_seed_seq: SeedSequence | None = None,
187
+ _mktshr_rng_seed_seq: SeedSequence | None = None,
188
+ _nthreads: int = 16,
189
+ /,
190
+ ) -> ShareDataSample:
191
+ """Dirichlet-distributed shares with multiple firm-counts.
192
+
193
+ Firm-counts may be specified as having Uniform distribution over the range
194
+ of firm counts, or a set of probability weights may be specified. In the
195
+ latter case the proportion of draws for each firm-count matches the
196
+ specified probability weight.
197
+
198
+ Parameters
199
+ ----------
200
+ _s_size
201
+ sample size to be drawn
202
+ _r_bar
203
+ market recapture rate
204
+ _firm_count_wts
205
+ firm count weights array for sample to be drawn
206
+ _dist_type_dir
207
+ Whether Dirichlet is Flat or Asymmetric
208
+ _recapture_spec
209
+ r_1 = r_2 if "proportional", otherwise MNL-consistent
210
+ _fcount_rng_seed_seq
211
+ seed firm count rng, for replicable results
212
+ _mktshr_rng_seed_seq
213
+ seed market share rng, for replicable results
214
+ _nthreads
215
+ number of threads for parallelized random number generation
216
+
217
+ Returns
218
+ -------
219
+ array of market shares and other market statistics
220
+
221
+ """
222
+
223
+ _firm_count_wts: np.float64 = (
224
+ FCOUNT_WTS_DEFAULT if _firm_count_wts is None else _firm_count_wts
225
+ ) # type: ignore
226
+
227
+ _min_choice_wt = 0.03 if _dist_type_dir == SHRConstants.DIR_FLAT_CONSTR else 0.00
228
+ _fcount_keys, _choice_wts = zip(
229
+ *(
230
+ _f
231
+ for _f in zip(
232
+ 2 + np.arange(len(_firm_count_wts)), # type: ignore
233
+ _firm_count_wts / _firm_count_wts.sum(),
234
+ strict=True,
235
+ )
236
+ if _f[1] > _min_choice_wt
237
+ )
238
+ )
239
+ _choice_wts = _choice_wts / sum(_choice_wts)
240
+
241
+ _fc_max = _fcount_keys[-1]
242
+ _dir_alphas_full = (
243
+ [1.0] * _fc_max if _dist_parms_dir is None else _dist_parms_dir[:_fc_max]
244
+ )
245
+ if _dist_type_dir == SHRConstants.DIR_ASYM:
246
+ _dir_alphas_full = [2.0] * 6 + [1.5] * 5 + [1.25] * min(7, _fc_max)
247
+
248
+ if _dist_type_dir == SHRConstants.DIR_COND:
249
+
250
+ def _gen_dir_alphas(_fcv: int) -> NDArray[np.float64]:
251
+ _dat = [2.5] * 2
252
+ if _fcv > len(_dat):
253
+ _dat += [1.0 / (_fcv - 2)] * (_fcv - 2)
254
+ return np.array(_dat, dtype=np.float64)
255
+
256
+ else:
257
+
258
+ def _gen_dir_alphas(_fcv: int) -> NDArray[np.float64]:
259
+ return np.array(_dir_alphas_full[:_fcv], dtype=np.float64)
260
+
261
+ _fcounts = prng(_fcount_rng_seed_seq).choice(
262
+ _fcount_keys, size=(_s_size, 1), p=_choice_wts
263
+ )
264
+
265
+ _mktshr_seed_seq_ch = (
266
+ _mktshr_rng_seed_seq.spawn(len(_fcount_keys))
267
+ if isinstance(_mktshr_rng_seed_seq, SeedSequence)
268
+ else SeedSequence(pool_size=8).spawn(len(_fcounts))
269
+ )
270
+
271
+ _aggregate_purchase_prob, _nth_firm_share = (
272
+ np.empty((_s_size, 1)) for _ in range(2)
273
+ )
274
+ _mktshr_array = np.empty((_s_size, _fc_max), dtype=np.float64)
275
+ for _f_val, _f_sseq in zip(_fcount_keys, _mktshr_seed_seq_ch, strict=True):
276
+ _fcounts_match_rows = np.where(_fcounts == _f_val)[0]
277
+ _dir_alphas_test = _gen_dir_alphas(_f_val)
278
+
279
+ try:
280
+ _mktshr_sample_f = _gen_market_shares_dirichlet(
281
+ _dir_alphas_test,
282
+ len(_fcounts_match_rows),
283
+ _recapture_spec,
284
+ _f_sseq,
285
+ _nthreads,
286
+ )
287
+ except ValueError as _err:
288
+ print(_f_val, len(_fcounts_match_rows))
289
+ raise _err
290
+
291
+ # Push data for present sample to parent
292
+ _mktshr_array[_fcounts_match_rows] = np.pad(
293
+ _mktshr_sample_f.mktshr_array,
294
+ ((0, 0), (0, _fc_max - _mktshr_sample_f.mktshr_array.shape[1])),
295
+ "constant",
296
+ )
297
+ _aggregate_purchase_prob[_fcounts_match_rows] = (
298
+ _mktshr_sample_f.aggregate_purchase_prob
299
+ )
300
+ _nth_firm_share[_fcounts_match_rows] = _mktshr_sample_f.nth_firm_share
301
+
302
+ if (_iss := np.round(np.einsum("ij->", _mktshr_array))) != _s_size or _iss != len(
303
+ _mktshr_array
304
+ ):
305
+ raise ValueError(
306
+ "DATA GENERATION ERROR: {} {} {}".format(
307
+ "Generation of sample shares is inconsistent:",
308
+ "array of drawn shares must some to the number of draws",
309
+ "i.e., the sample size, which condition is not met.",
310
+ )
311
+ )
312
+
313
+ return ShareDataSample(
314
+ _mktshr_array, _fcounts, _nth_firm_share, _aggregate_purchase_prob
315
+ )
316
+
317
+
318
+ def _gen_market_shares_dirichlet(
319
+ _dir_alphas: NDArray[np.floating[TF]],
320
+ _s_size: int = 10**6,
321
+ _recapture_spec: RECConstants = RECConstants.INOUT,
322
+ _mktshr_rng_seed_seq: SeedSequence | None = None,
323
+ _nthreads: int = 16,
324
+ /,
325
+ ) -> ShareDataSample:
326
+ """Dirichlet-distributed shares with fixed firm-count.
327
+
328
+ Parameters
329
+ ----------
330
+ _dir_alphas
331
+ Shape parameters for Dirichlet distribution
332
+ _s_size
333
+ sample size to be drawn
334
+ _r_bar
335
+ market recapture rate
336
+ _recapture_spec
337
+ r_1 = r_2 if RECConstants.FIXED, otherwise MNL-consistent. If
338
+ RECConstants.OUTIN; the number of columns in the output share array
339
+ is len(_dir_alphas) - 1.
340
+ _mktshr_rng_seed_seq
341
+ seed market share rng, for replicable results
342
+ _nthreads
343
+ number of threads for parallelized random number generation
344
+
345
+ Returns
346
+ -------
347
+ array of market shares and other market statistics
348
+
349
+ """
350
+
351
+ if not isinstance(_dir_alphas, np.ndarray):
352
+ _dir_alphas = np.array(_dir_alphas)
353
+
354
+ if _recapture_spec == RECConstants.OUTIN:
355
+ _dir_alphas = np.concatenate((_dir_alphas, _dir_alphas[-1:]))
356
+
357
+ _mktshr_seed_seq_ch = (
358
+ _mktshr_rng_seed_seq
359
+ if isinstance(_mktshr_rng_seed_seq, SeedSequence)
360
+ else SeedSequence(pool_size=8)
361
+ )
362
+
363
+ _mktshr_array = np.empty((_s_size, len(_dir_alphas)))
364
+ _mrng = MultithreadedRNG(
365
+ _mktshr_array,
366
+ dist_type="Dirichlet",
367
+ dist_parms=_dir_alphas,
368
+ seed_sequence=_mktshr_seed_seq_ch,
369
+ nthreads=_nthreads,
370
+ )
371
+ _mrng.fill()
372
+
373
+ if (_iss := np.round(np.einsum("ij->", _mktshr_array))) != _s_size or _iss != len(
374
+ _mktshr_array
375
+ ):
376
+ print(_dir_alphas, _iss, repr(_s_size), len(_mktshr_array))
377
+ print(repr(_mktshr_array[-10:, :]))
378
+ raise ValueError(
379
+ "DATA GENERATION ERROR: {} {} {}".format(
380
+ "Generation of sample shares is inconsistent:",
381
+ "array of drawn shares must sum to the number of draws",
382
+ "i.e., the sample size, which condition is not met.",
383
+ )
384
+ )
385
+
386
+ # If recapture_spec == 'inside_out', further calculations downstream
387
+ _aggregate_purchase_prob = np.nan * np.empty((_s_size, 1))
388
+ if _recapture_spec == RECConstants.OUTIN:
389
+ _aggregate_purchase_prob = 1 - _mktshr_array[:, [-1]]
390
+ _mktshr_array = _mktshr_array[:, :-1] / _aggregate_purchase_prob
391
+
392
+ return ShareDataSample(
393
+ _mktshr_array,
394
+ (_mktshr_array.shape[-1] * np.ones((_s_size, 1))).astype(np.int64),
395
+ _mktshr_array[:, [-1]],
396
+ _aggregate_purchase_prob,
397
+ )
398
+
399
+
400
+ def _gen_pr_data(
401
+ _frmshr_array: NDArray[np.floating[TF]],
402
+ _nth_firm_share: NDArray[np.floating[TF]],
403
+ _mkt_sample_spec: MarketSampleSpec,
404
+ _seed_seq: SeedSequence | None = None,
405
+ /,
406
+ ) -> PriceDataSample:
407
+ _ssz = len(_frmshr_array)
408
+
409
+ _hsr_filing_test_type = _mkt_sample_spec.hsr_filing_test_type
410
+
411
+ _price_array, _price_ratio_array, _hsr_filing_test = (
412
+ np.ones_like(_frmshr_array),
413
+ np.empty_like(_frmshr_array),
414
+ np.empty(_ssz, dtype=bool),
415
+ )
416
+
417
+ _pr_max_ratio = 5.0
418
+ match _mkt_sample_spec.pr_sym_spec:
419
+ case PRIConstants.SYM:
420
+ _nth_firm_price = np.ones((_ssz, 1))
421
+ case PRIConstants.POS:
422
+ _price_array, _nth_firm_price = (
423
+ np.ceil(_p * _pr_max_ratio) for _p in (_frmshr_array, _nth_firm_share)
424
+ )
425
+ case PRIConstants.NEG:
426
+ _price_array, _nth_firm_price = (
427
+ np.ceil((1 - _p) * _pr_max_ratio)
428
+ for _p in (_frmshr_array, _nth_firm_share)
429
+ )
430
+ case PRIConstants.ZERO:
431
+ _price_array_gen = prng(_seed_seq).choice(
432
+ 1 + np.arange(_pr_max_ratio), size=(len(_frmshr_array), 3)
433
+ )
434
+ _price_array = _price_array_gen[:, :2]
435
+ _nth_firm_price = _price_array_gen[:, [2]]
436
+ # del _price_array_gen
437
+ case _:
438
+ raise ValueError(
439
+ f"Condition regarding price symmetry"
440
+ f' "{_mkt_sample_spec.pr_sym_spec.value}" is invalid.'
441
+ )
442
+ # del _pr_max_ratio
443
+
444
+ _price_ratio_array = _price_array / _price_array[:, ::-1]
445
+ _rev_array = _price_array * _frmshr_array
446
+ _nth_firm_rev = _nth_firm_price * _nth_firm_share
447
+
448
+ # Although `_test_rev_ratio_inv` is not fixed at 10%,
449
+ # the ratio has not changed since inception of the HSR filing test,
450
+ # so we treat it as a constant of merger policy.
451
+ _test_rev_ratio, _test_rev_ratio_inv = 10, 1 / 10
452
+
453
+ match _hsr_filing_test_type:
454
+ case SSZConstants.HSR_TEN:
455
+ # See, https://www.ftc.gov/enforcement/premerger-notification-program/
456
+ # -> Procedures For Submitting Post-Consummation Filings
457
+ # -> Key Elements to Determine Whether a Post Consummation Filing is Required
458
+ # under heading, "Historical Thresholds"
459
+ # Revenue ratio has been 10-to-1 since inception
460
+ # Thus, a simple form of the HSR filing test would impose a 10-to-1
461
+ # ratio restriction on the merging firms' revenues
462
+ _rev_ratio = (_rev_array.min(axis=1) / _rev_array.max(axis=1)).round(4)
463
+ _hsr_filing_test = _rev_ratio >= _test_rev_ratio_inv
464
+ # del _rev_array, _rev_ratio
465
+ case SSZConstants.HSR_NTH:
466
+ # To get around the 10-to-1 ratio restriction, specify that the nth firm
467
+ # matches the smaller firm in the size test; then if the smaller merging firm
468
+ # matches the n-th firm in size, and the larger merging firm has at least
469
+ # 10 times the size of the nth firm, the size test is considered met.
470
+ # Alternatively, if the smaller merging firm has 10% or greater share,
471
+ # the value of transaction test is considered met.
472
+ _rev_ratio_to_nth = np.round(np.sort(_rev_array, axis=1) / _nth_firm_rev, 4)
473
+ _hsr_filing_test = (
474
+ np.einsum(
475
+ "ij->i",
476
+ 1 * (_rev_ratio_to_nth > [1, _test_rev_ratio]),
477
+ dtype=np.int64,
478
+ )
479
+ == _rev_ratio_to_nth.shape[1]
480
+ ) | (_frmshr_array.min(axis=1) >= _test_rev_ratio_inv)
481
+
482
+ # del _nth_firm_rev, _rev_ratio_to_nth
483
+ case _:
484
+ # Otherwise, all draws meet the filing test
485
+ _hsr_filing_test = np.ones(_ssz, dtype=bool)
486
+
487
+ return PriceDataSample(_price_array, _hsr_filing_test)
488
+
489
+
490
+ def _gen_pcm_data(
491
+ _frmshr_array: NDArray[np.floating[TF]],
492
+ _mkt_sample_spec: MarketSampleSpec,
493
+ _price_array: NDArray[np.floating[TF]],
494
+ _aggregate_purchase_prob: NDArray[np.floating[TF]],
495
+ _pcm_rng_seed_seq: SeedSequence,
496
+ _nthreads: int = 16,
497
+ /,
498
+ ) -> MarginDataSample:
499
+ _recapture_spec = _mkt_sample_spec.share_spec.recapture_spec
500
+ _dist_type_pcm, _dist_firm2_pcm, _dist_parms_pcm = (
501
+ getattr(_mkt_sample_spec.pcm_spec, _f)
502
+ for _f in ("dist_type", "firm2_pcm_constraint", "dist_parms")
503
+ )
504
+ _dist_type: Literal["Beta", "Uniform"] = (
505
+ "Uniform" if _dist_type_pcm == PCMConstants.UNI else "Beta"
506
+ )
507
+
508
+ _pcm_array = np.empty((len(_frmshr_array), 2), dtype=np.float64)
509
+ _mnl_test_array = np.empty((len(_frmshr_array), 2), dtype=int)
510
+
511
+ _beta_min, _beta_max = [None] * 2 # placeholder
512
+ _dist_parms = np.ones(2, np.float64)
513
+ if _dist_type_pcm == PCMConstants.EMPR:
514
+ _pcm_array = resample_mgn_data(
515
+ _pcm_array.shape, # type: ignore
516
+ seed_sequence=_pcm_rng_seed_seq,
517
+ )
518
+ else:
519
+ if _dist_type_pcm == PCMConstants.UNI:
520
+ _dist_parms = (
521
+ DIST_PARMS_DEFAULT if _dist_parms_pcm is None else _dist_parms_pcm
522
+ )
523
+ elif _dist_type_pcm == PCMConstants.BETA:
524
+ # Error-checking (could move to validators in definition of MarketSampleSpec)
525
+
526
+ if _dist_parms_pcm is None:
527
+ _dist_parms_pcm = _dist_parms
528
+
529
+ elif _dist_type_pcm == PCMConstants.BETA_BND: # Bounded beta
530
+ if _dist_parms_pcm is None:
531
+ _dist_parms_pcm = np.array([0, 1, 0, 1], np.float64)
532
+ _dist_parms = beta_located_bound(_dist_parms_pcm)
533
+
534
+ _pcm_rng = MultithreadedRNG(
535
+ _pcm_array,
536
+ dist_type=_dist_type,
537
+ dist_parms=_dist_parms,
538
+ seed_sequence=_pcm_rng_seed_seq,
539
+ nthreads=_nthreads,
540
+ )
541
+ _pcm_rng.fill()
542
+ del _pcm_rng
543
+
544
+ if _dist_type_pcm == PCMConstants.BETA_BND:
545
+ _beta_min, _beta_max = _dist_parms_pcm[2:] # type: ignore
546
+ _pcm_array = (_beta_max - _beta_min) * _pcm_array + _beta_min
547
+ del _beta_min, _beta_max
548
+
549
+ if _dist_firm2_pcm == FM2Constants.MNL:
550
+ # Impose FOCs from profit-maximization with MNL demand
551
+ _purchprob_array = _aggregate_purchase_prob * _frmshr_array
552
+
553
+ _pcm_array[:, [1]] = np.divide(
554
+ np.einsum(
555
+ "ij,ij,ij->ij",
556
+ _price_array[:, [0]],
557
+ _pcm_array[:, [0]],
558
+ 1 - _purchprob_array[:, [0]],
559
+ ),
560
+ np.einsum("ij,ij->ij", _price_array[:, [1]], 1 - _purchprob_array[:, [1]]),
561
+ )
562
+
563
+ _mnl_test_array = _pcm_array[:, 1].__ge__(0) & _pcm_array[:, 1].__le__(1)
564
+ else:
565
+ _mnl_test_array = np.ones(len(_pcm_array), dtype=bool)
566
+ if _dist_firm2_pcm == FM2Constants.SYM:
567
+ _pcm_array[:, [1]] = _pcm_array[:, [0]]
568
+
569
+ return MarginDataSample(_pcm_array, _mnl_test_array)
570
+
571
+
572
+ def _beta_located(
573
+ _mu: float | NDArray[np.float64], _sigma: float | NDArray[np.float64], /
574
+ ) -> NDArray[np.float64]:
575
+ """
576
+ Given mean and stddev, return shape parameters for corresponding Beta distribution
577
+
578
+ Solve the first two moments of the standard Beta to get the shape parameters.
579
+
580
+ Parameters
581
+ ----------
582
+ _mu
583
+ mean
584
+ _sigma
585
+ standardd deviation
586
+
587
+ Returns
588
+ -------
589
+ shape parameters for Beta distribution
590
+
591
+ """
592
+
593
+ _mul = -1 + _mu * (1 - _mu) / _sigma**2
594
+ return np.array([_mu * _mul, (1 - _mu) * _mul], dtype=np.float64)
595
+
596
+
597
+ def beta_located_bound(_dist_parms: NDArray[np.floating[TF]], /) -> NDArray[np.float64]:
598
+ R"""
599
+ Return shape parameters for a non-standard beta, given the mean, stddev, range
600
+
601
+
602
+ Recover the r.v.s as
603
+ :math:`\min + (\max - \min) \cdot \symup{Β}(a, b)`,
604
+ with `a` and `b` calculated from the specified mean (:math:`\mu`) and
605
+ variance (:math:`\sigma`). [8]_
606
+
607
+ Parameters
608
+ ----------
609
+ _dist_parms
610
+ vector of :math:`\mu`, :math:`\sigma`, :math:`\mathtt{\min}`, and :math:`\mathtt{\max}` values
611
+
612
+ Returns
613
+ -------
614
+ shape parameters for Beta distribution
615
+
616
+ Notes
617
+ -----
618
+ For example, ``beta_located_bound(np.array([0.5, 0.2887, 0.0, 1.0]))``.
619
+
620
+ References
621
+ ----------
622
+ .. [8] NIST, Beta Distribution. https://www.itl.nist.gov/div898/handbook/eda/section3/eda366h.htm
623
+ """ # noqa: RUF002
624
+
625
+ _bmu, _bsigma, _bmin, _bmax = _dist_parms
626
+ return _beta_located((_bmu - _bmin) / (_bmax - _bmin), _bsigma / (_bmax - _bmin))