mergeron 2024.738936.2__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.

mergeron/gen/__init__.py CHANGED
@@ -1,5 +1,468 @@
1
+ """
2
+ Defines constants and containers for industry data generation and testing
3
+
4
+ """
5
+
6
+ from __future__ import annotations
7
+
1
8
  from importlib.metadata import version
2
9
 
3
10
  from .. import _PKG_NAME # noqa: TID252
4
11
 
5
12
  __version__ = version(_PKG_NAME)
13
+
14
+
15
+ import enum
16
+ from dataclasses import dataclass
17
+ from typing import ClassVar, Protocol, TypeVar
18
+
19
+ import attrs
20
+ import numpy as np
21
+ from numpy.typing import NBitBase, NDArray
22
+
23
+ from ..core.pseudorandom_numbers import DIST_PARMS_DEFAULT # noqa: TID252
24
+
25
+ EMPTY_ARRAY_DEFAULT = np.zeros(2)
26
+ FCOUNT_WTS_DEFAULT = ((_nr := np.arange(1, 6)[::-1]) / _nr.sum()).astype(np.float64)
27
+
28
+ TF = TypeVar("TF", bound=NBitBase)
29
+ TI = TypeVar("TI", bound=NBitBase)
30
+
31
+
32
+ # https://stackoverflow.com/questions/54668000
33
+ class DataclassInstance(Protocol):
34
+ """For type-hinting dataclass objects"""
35
+
36
+ __dataclass_asdict__: ClassVar
37
+ __dataclass_fields__: ClassVar
38
+
39
+
40
+ @enum.unique
41
+ class PRIConstants(tuple[bool, str | None], enum.ReprEnum):
42
+ """Price specification.
43
+
44
+ Whether prices are symmetric and, if not, the direction of correlation, if any.
45
+ """
46
+
47
+ SYM = (True, None)
48
+ ZERO = (False, None)
49
+ NEG = (False, "negative share-correlation")
50
+ POS = (False, "positive share-correlation")
51
+ CSY = (False, "market-wide cost-symmetry")
52
+
53
+
54
+ @enum.unique
55
+ class SHRConstants(enum.StrEnum):
56
+ """Market share distributions."""
57
+
58
+ UNI = "Uniform"
59
+ """Uniform distribution over the 3-simplex"""
60
+
61
+ DIR_FLAT = "Flat Dirichlet"
62
+ """Shape parameter for all merging-firm-shares is unity (1)"""
63
+
64
+ DIR_FLAT_CONSTR = "Flat Dirichlet - Constrained"
65
+ """Impose minimum probablility weight on each firm-count
66
+
67
+ Only firm-counts with probability weight of no less than 3%
68
+ are included for data generation.
69
+ """
70
+
71
+ DIR_ASYM = "Asymmetric Dirichlet"
72
+ """Share distribution for merging-firm shares has a higher peak share
73
+
74
+ Shape parameter for merging-firm-share is 2.5, and 1.0 for all others.
75
+ """
76
+
77
+ DIR_COND = "Conditional Dirichlet"
78
+ """Shape parameters for non-merging firms is proportional
79
+
80
+ Shape parameters for merging-firm-share are 2.0 each; and
81
+ are equiproportional and add to 2.0 for all non-merging-firm-shares.
82
+ """
83
+
84
+
85
+ @enum.unique
86
+ class RECConstants(enum.StrEnum):
87
+ """Recapture rate - derivation methods."""
88
+
89
+ INOUT = "inside-out"
90
+ OUTIN = "outside-in"
91
+ FIXED = "proportional"
92
+
93
+
94
+ @attrs.define(slots=True, frozen=True)
95
+ class ShareSpec:
96
+ """Market share specification
97
+
98
+ Notes
99
+ -----
100
+ If recapture is determined "outside-in", market shares cannot have
101
+ Uniform distribution.
102
+
103
+ If sample with varying firm counts is required, market shares must
104
+ be specified as having a supported Dirichlet distribution.
105
+
106
+ """
107
+
108
+ recapture_spec: RECConstants
109
+ """see RECConstants"""
110
+
111
+ dist_type: SHRConstants
112
+ """see SHRConstants"""
113
+
114
+ dist_parms: NDArray[np.float64] | None
115
+ """Parameters for tailoring market-share distribution
116
+
117
+ For Uniform distribution, bounds of the distribution; defaults to `(0, 1)`;
118
+ for Beta distribution, shape parameters, defaults to `(1, 1)`;
119
+ for Bounded-Beta distribution, vector of (min, max, mean, std. deviation), non-optional;
120
+ for Dirichlet-type distributions, a vector of shape parameters of length
121
+ no less than the length of firm-count weights below; defaults depend on
122
+ type of Dirichlet-distribution specified.
123
+
124
+ """
125
+ firm_counts_weights: NDArray[np.float64 | np.int64] | None
126
+ """relative or absolute frequencies of firm counts
127
+
128
+
129
+ Given frequencies are exogenous to generated market data sample;
130
+ defaults to FCOUNT_WTS_DEFAULT, which specifies firm-counts of 2 to 6
131
+ with weights in descending order from 5 to 1."""
132
+
133
+
134
+ @enum.unique
135
+ class PCMConstants(enum.StrEnum):
136
+ """Margin distributions."""
137
+
138
+ UNI = "Uniform"
139
+ BETA = "Beta"
140
+ BETA_BND = "Bounded Beta"
141
+ EMPR = "Damodaran margin data"
142
+
143
+
144
+ @enum.unique
145
+ class FM2Constants(enum.StrEnum):
146
+ """Firm 2 margins - derivation methods."""
147
+
148
+ IID = "i.i.d"
149
+ MNL = "MNL-dep"
150
+ SYM = "symmetric"
151
+
152
+
153
+ @attrs.define(slots=True, frozen=True)
154
+ class PCMSpec:
155
+ """Price-cost margin (PCM) specification
156
+
157
+ If price-cost margins are specified as having Beta distribution,
158
+ `dist_parms` is specified as a pair of positive, non-zero shape parameters of
159
+ the standard Beta distribution. Specifying shape parameters :code:`np.array([1, 1])`
160
+ is known equivalent to specifying uniform distribution over
161
+ the interval :math:`[0, 1]`. If price-cost margins are specified as having
162
+ Bounded-Beta distribution, `dist_parms` is specified as
163
+ the tuple, (`mean`, `std deviation`, `min`, `max`), where `min` and `max`
164
+ are lower- and upper-bounds respectively within the interval :math:`[0, 1]`.
165
+
166
+
167
+ """
168
+
169
+ dist_type: PCMConstants
170
+ """See PCMConstants"""
171
+
172
+ firm2_pcm_constraint: FM2Constants
173
+ """See FM2Constants"""
174
+
175
+ dist_parms: NDArray[np.float64] | None
176
+ """Parameter specification for tailoring PCM distribution
177
+
178
+ For Uniform distribution, bounds of the distribution; defaults to `(0, 1)`;
179
+ for Beta distribution, shape parameters, defaults to `(1, 1)`;
180
+ for Bounded-Beta distribution, vector of (min, max, mean, std. deviation), non-optional;
181
+ for empirical distribution based on Damodaran margin data, optional, ignored
182
+ """
183
+
184
+
185
+ @enum.unique
186
+ class SSZConstants(float, enum.ReprEnum):
187
+ """
188
+ Scale factors to offset sample size reduction.
189
+
190
+ Sample size reduction occurs when imposing a HSR filing test
191
+ or equilibrium condition under MNL demand.
192
+ """
193
+
194
+ HSR_NTH = 1.666667
195
+ """
196
+ For HSR filing requirement.
197
+
198
+ When filing requirement is assumed met if maximum merging-firm shares exceeds
199
+ ten (10) times the n-th firm's share and minimum merging-firm share is
200
+ no less than n-th firm's share. To assure that the number of draws available
201
+ after applying the given restriction, the initial number of draws is larger than
202
+ the sample size by the given scale factor.
203
+ """
204
+
205
+ HSR_TEN = 1.234567
206
+ """
207
+ For alternative HSR filing requirement,
208
+
209
+ When filing requirement is assumed met if merging-firm shares exceed 10:1 ratio
210
+ to each other.
211
+ """
212
+
213
+ MNL_DEP = 1.25
214
+ """
215
+ For restricted PCM's.
216
+
217
+ When merging firm's PCMs are constrained for consistency with f.o.c.s from
218
+ profit maximization under Nash-Bertrand oligopoly with MNL demand.
219
+ """
220
+
221
+ ONE = 1.00
222
+ """When initial set of draws is not restricted in any way."""
223
+
224
+
225
+ # Validators for selected attributes of MarketSampleSpec
226
+ def _sample_size_validator(
227
+ _object: MarketSampleSpec, _attribute: attrs.Attribute, _value: int, /
228
+ ) -> None:
229
+ if _value < 10**6 or (_value % 10**5 != 0):
230
+ raise ValueError(
231
+ f"Sample size must be a multiple of {10** 4:,d} and not less than {10** 6:,d}."
232
+ )
233
+
234
+
235
+ def _recapture_rate_validator(
236
+ _object: MarketSampleSpec, _attribute: attrs.Attribute, _value: float | None, /
237
+ ) -> None:
238
+ # if _value < 10**6 or (np.log10(_value) % 1 != 0):
239
+ if _value and not (0 < _value <= 1):
240
+ raise ValueError("Recapture rate must lie in the interval, [0, 1).")
241
+
242
+ if _value and _object.share_spec.recapture_spec == RECConstants.OUTIN:
243
+ raise ValueError(
244
+ "Market share specification requires estimation of recapture rate from "
245
+ "generated data. Either delete recapture rate specification or set it to None."
246
+ )
247
+
248
+
249
+ def _share_spec_validator(
250
+ _instance: MarketSampleSpec, _attribute: attrs.Attribute, _value: ShareSpec, /
251
+ ) -> None:
252
+ _r_bar = _instance.recapture_rate
253
+ if _value.dist_type == SHRConstants.UNI:
254
+ if _value.recapture_spec == RECConstants.OUTIN:
255
+ raise ValueError(
256
+ f"Invalid recapture specification, {_value.recapture_spec!r} "
257
+ "for market share specification with Uniform distribution. "
258
+ "Redefine the market-sample specification, modifying the ."
259
+ "market-share specification or the recapture specification."
260
+ )
261
+ elif _value.firm_counts_weights is not None:
262
+ raise ValueError(
263
+ "Generated data for markets with specified firm-counts or "
264
+ "varying firm counts are not feasible with market shares "
265
+ "with Uniform distribution. Consider revising the "
266
+ r"distribution type to {SHRConstants.DIR_FLAT}, which gives "
267
+ "uniformly distributed draws on the :math:`n+1` simplex "
268
+ "for firm-count, :math:`n`."
269
+ )
270
+ # Outside-in calibration only valid for Dir-distributed shares
271
+ elif _value.recapture_spec != RECConstants.OUTIN and (
272
+ _r_bar is None or not isinstance(_r_bar, float)
273
+ ):
274
+ raise ValueError(
275
+ f"Recapture specification, {_value.recapture_spec!r} requires that "
276
+ "the market sample specification inclues a recapture rate."
277
+ )
278
+
279
+
280
+ def _pcm_spec_validator(
281
+ _instance: MarketSampleSpec, _attribute: attrs.Attribute, _value: PCMSpec, /
282
+ ) -> None:
283
+ if (
284
+ _instance.share_spec.recapture_spec == RECConstants.FIXED
285
+ and _value.firm2_pcm_constraint == FM2Constants.MNL
286
+ ):
287
+ raise ValueError(
288
+ "{} {} {}".format(
289
+ f'Specification of "recapture_spec", "{_instance.share_spec.recapture_spec}"',
290
+ "requires Firm 2 margin must have property, ",
291
+ f'"{FM2Constants.IID}" or "{FM2Constants.SYM}".',
292
+ )
293
+ )
294
+ elif _value.dist_type.name.startswith("BETA"):
295
+ if _value.dist_parms is None:
296
+ pass
297
+ elif np.array_equal(_value.dist_parms, DIST_PARMS_DEFAULT):
298
+ raise ValueError(
299
+ f"The distribution parameters, {DIST_PARMS_DEFAULT!r} "
300
+ "are not valid with margin distribution, {_dist_type_pcm!r}"
301
+ )
302
+ elif (
303
+ _value.dist_type == PCMConstants.BETA
304
+ and len(_value.dist_parms) != len(("max", "min"))
305
+ ) or (
306
+ _value.dist_type == PCMConstants.BETA_BND
307
+ and len(_value.dist_parms) != len(("mu", "sigma", "max", "min"))
308
+ ):
309
+ raise ValueError(
310
+ f"Given number, {len(_value.dist_parms)} of parameters "
311
+ f'for PCM with distribution, "{_value.dist_type}" is incorrect.'
312
+ )
313
+
314
+
315
+ @attrs.define(slots=True, frozen=True)
316
+ class MarketSampleSpec:
317
+ """Parameter specification for market data generation."""
318
+
319
+ sample_size: int = attrs.field(
320
+ default=10**6,
321
+ validator=(attrs.validators.instance_of(int), _sample_size_validator),
322
+ )
323
+ """sample size generated"""
324
+
325
+ recapture_rate: float | None = attrs.field(
326
+ default=None, validator=attrs.validators.instance_of(float | None)
327
+ )
328
+ """market recapture rate
329
+
330
+ Is None if market share specification requires generation of
331
+ outside good choice probabilities (RECConstants.OUTIN).
332
+ """
333
+
334
+ pr_sym_spec: PRIConstants = attrs.field( # type: ignore
335
+ kw_only=True,
336
+ default=PRIConstants.SYM,
337
+ validator=attrs.validators.instance_of(PRIConstants), # type: ignore
338
+ )
339
+ """Price specification, see PRIConstants"""
340
+
341
+ share_spec: ShareSpec = attrs.field(
342
+ kw_only=True,
343
+ default=ShareSpec(RECConstants.INOUT, SHRConstants.UNI, None, None),
344
+ validator=[attrs.validators.instance_of(ShareSpec), _share_spec_validator],
345
+ )
346
+ """See definition of ShareSpec"""
347
+
348
+ pcm_spec: PCMSpec = attrs.field(
349
+ kw_only=True,
350
+ default=PCMSpec(PCMConstants.UNI, FM2Constants.IID, None),
351
+ validator=[attrs.validators.instance_of(PCMSpec), _pcm_spec_validator],
352
+ )
353
+ """See definition of PCMSpec"""
354
+
355
+ hsr_filing_test_type: SSZConstants = attrs.field( # type: ignore
356
+ kw_only=True,
357
+ default=SSZConstants.ONE,
358
+ validator=attrs.validators.instance_of(SSZConstants), # type: ignore
359
+ )
360
+ """Method for modeling HSR filing threholds, see SSZConstants"""
361
+
362
+
363
+ @dataclass(slots=True, frozen=True)
364
+ class MarketDataSample:
365
+ """Container for generated markets data sample."""
366
+
367
+ frmshr_array: NDArray[np.floating]
368
+ """Merging-firm shares (with two merging firms)"""
369
+
370
+ pcm_array: NDArray[np.floating]
371
+ """Merging-firms' prices (normalized to 1, in default specification)"""
372
+
373
+ price_array: NDArray[np.floating]
374
+ """Merging-firms' price-cost margins (PCM)"""
375
+
376
+ fcounts: NDArray[np.integer]
377
+ """Number of firms in market"""
378
+
379
+ ratio_choice_prob_to_mktshr: NDArray[np.floating]
380
+ """
381
+ One (1) minus probability that the outside good is chosen
382
+
383
+ Converts market shares to choice probabilities by multiplication.
384
+ """
385
+
386
+ nth_firm_share: NDArray[np.floating]
387
+ """Market-share of n-th firm
388
+
389
+ Relevant for testing for draws the do or
390
+ do not meet HSR filing thresholds.
391
+ """
392
+
393
+ divr_array: NDArray[np.floating]
394
+ """Diversion ratio between the merging firms"""
395
+
396
+ hhi_post: NDArray[np.floating]
397
+ """Post-merger change in Herfindahl-Hirschmann Index (HHI)"""
398
+
399
+ hhi_delta: NDArray[np.floating]
400
+ """Change in HHI from combination of merging firms"""
401
+
402
+
403
+ @dataclass(slots=True, frozen=True)
404
+ class ShareDataSample:
405
+ """Container for generated market shares.
406
+
407
+ Includes related measures of market structure
408
+ and aggregate purchase probability.
409
+ """
410
+
411
+ mktshr_array: NDArray[np.float64]
412
+ """All-firm shares (with two merging firms)"""
413
+
414
+ fcounts: NDArray[np.int64]
415
+ """All-firm-count for each draw"""
416
+
417
+ nth_firm_share: NDArray[np.float64]
418
+ """Market-share of n-th firm"""
419
+
420
+ aggregate_purchase_prob: NDArray[np.float64]
421
+ """Converts market shares to choice probabilities by multiplication."""
422
+
423
+
424
+ @dataclass(slots=True, frozen=True)
425
+ class PriceDataSample:
426
+ """Container for generated price array, and related."""
427
+
428
+ price_array: NDArray[np.floating]
429
+ """Merging-firms' prices"""
430
+
431
+ hsr_filing_test: NDArray[np.bool_]
432
+ """Flags draws as meeting HSR filing thresholds or not"""
433
+
434
+
435
+ @dataclass(slots=True, frozen=True)
436
+ class MarginDataSample:
437
+ """Container for generated margin array and related MNL test array."""
438
+
439
+ pcm_array: NDArray[np.float64]
440
+ """Merging-firms' PCMs"""
441
+
442
+ mnl_test_array: NDArray[np.bool_]
443
+ """Flags infeasible observations as False and rest as True
444
+
445
+ Applying restrictions from Bertrand-Nash oligopoly
446
+ with MNL demand results in draws of Firm 2 PCM falling
447
+ outside the feabile interval,:math:`[0, 1]`, depending
448
+ on the configuration of merging firm shares. Such draws
449
+ are are flagged as infeasible (False)in :code:`mnl_test_array` while
450
+ draws with PCM values within the feasible range are
451
+ flagged as True. Used from filtering-out draws with
452
+ infeasible PCM.
453
+ """
454
+
455
+
456
+ @dataclass(slots=True, frozen=True)
457
+ class UPPTestsRaw:
458
+ guppi_test_simple: NDArray[np.bool_]
459
+ guppi_test_compound: NDArray[np.bool_]
460
+ cmcr_test: NDArray[np.bool_]
461
+ ipr_test: NDArray[np.bool_]
462
+
463
+
464
+ @dataclass(slots=True, frozen=True)
465
+ class UPPTestsCounts:
466
+ by_firm_count: NDArray[np.int64]
467
+ by_delta: NDArray[np.int64]
468
+ by_conczone: NDArray[np.int64]