mergeron 2024.738963.0__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 +142 -93
  5. mergeron/core/guidelines_boundaries.py +289 -1077
  6. mergeron/core/guidelines_boundary_functions.py +1128 -0
  7. mergeron/core/{guidelines_boundaries_specialized_functions.py → guidelines_boundary_functions_extra.py} +76 -42
  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 +257 -245
  15. mergeron/gen/data_generation.py +473 -221
  16. mergeron/gen/data_generation_functions.py +876 -0
  17. mergeron/gen/enforcement_stats.py +355 -0
  18. mergeron/gen/upp_tests.py +159 -259
  19. mergeron-2025.739265.0.dist-info/METADATA +115 -0
  20. mergeron-2025.739265.0.dist-info/RECORD +23 -0
  21. {mergeron-2024.738963.0.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 -259
  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 -621
  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.738963.0.dist-info/METADATA +0 -108
  38. mergeron-2024.738963.0.dist-info/RECORD +0 -30
  39. /mergeron/{core → data}/ftc_invdata.msgpack +0 -0
mergeron/gen/__init__.py CHANGED
@@ -1,36 +1,50 @@
1
1
  """
2
- Defines constants and containers for industry data generation and testing
2
+ Defines constants, specifications (classes with attributes defining varous parameters) and
3
+ containers for industry data generation and testing.
3
4
 
4
5
  """
5
6
 
6
7
  from __future__ import annotations
7
8
 
8
- from importlib.metadata import version
9
-
10
- from .. import _PKG_NAME, RECConstants, UPPAggrSelector # noqa: TID252
11
-
12
- __version__ = version(_PKG_NAME)
13
-
14
-
15
9
  import enum
16
10
  from dataclasses import dataclass
17
- from typing import ClassVar, Literal, Protocol, TypeVar, Union
11
+ from typing import ClassVar, NamedTuple, Protocol
18
12
 
19
13
  import numpy as np
20
- from attrs import Attribute, define, field, validators
21
- from numpy.typing import NBitBase, NDArray
14
+ from attrs import Attribute, cmp_using, field, frozen, validators
15
+ from numpy.random import SeedSequence
22
16
 
23
- from ..core.pseudorandom_numbers import DIST_PARMS_DEFAULT # noqa: TID252
17
+ from .. import ( # noqa: TID252
18
+ DEFAULT_REC_RATIO,
19
+ VERSION,
20
+ ArrayBIGINT,
21
+ ArrayBoolean,
22
+ ArrayDouble,
23
+ ArrayFloat,
24
+ ArrayINT,
25
+ RECForm,
26
+ UPPAggrSelector,
27
+ )
28
+ from ..core.pseudorandom_numbers import DEFAULT_DIST_PARMS # noqa: TID252
24
29
 
25
- EMPTY_ARRAY_DEFAULT = np.zeros(2)
26
- FCOUNT_WTS_DEFAULT = ((_nr := np.arange(1, 6)[::-1]) / _nr.sum()).astype(np.float64)
30
+ __version__ = VERSION
27
31
 
28
- TF = TypeVar("TF", bound=NBitBase)
29
- TI = TypeVar("TI", bound=NBitBase)
32
+
33
+ DEFAULT_EMPTY_ARRAY = np.zeros(2)
34
+ DEFAULT_FCOUNT_WTS = np.divide(
35
+ (_nr := np.arange(1, 7)[::-1]), _nr.sum(), dtype=np.float64
36
+ )
37
+
38
+
39
+ class SeedSequenceData(NamedTuple):
40
+ mktshr_rng_seed_seq: SeedSequence
41
+ pcm_rng_seed_seq: SeedSequence
42
+ fcount_rng_seed_seq: SeedSequence | None
43
+ pr_rng_seed_seq: SeedSequence | None
30
44
 
31
45
 
32
46
  @enum.unique
33
- class PRIConstants(tuple[bool, str | None], enum.ReprEnum):
47
+ class PriceSpec(tuple[bool, str | None], enum.ReprEnum):
34
48
  """Price specification.
35
49
 
36
50
  Whether prices are symmetric and, if not, the direction of correlation, if any.
@@ -44,11 +58,11 @@ class PRIConstants(tuple[bool, str | None], enum.ReprEnum):
44
58
 
45
59
 
46
60
  @enum.unique
47
- class SHRConstants(enum.StrEnum):
61
+ class SHRDistribution(enum.StrEnum):
48
62
  """Market share distributions."""
49
63
 
50
64
  UNI = "Uniform"
51
- """Uniform distribution over the 3-simplex"""
65
+ R"""Uniform distribution over :math:`s_1 + s_2 \leqslant 1`"""
52
66
 
53
67
  DIR_FLAT = "Flat Dirichlet"
54
68
  """Shape parameter for all merging-firm-shares is unity (1)"""
@@ -63,7 +77,11 @@ class SHRConstants(enum.StrEnum):
63
77
  DIR_ASYM = "Asymmetric Dirichlet"
64
78
  """Share distribution for merging-firm shares has a higher peak share
65
79
 
66
- Shape parameter for merging-firm-share is 2.5, and 1.0 for all others.
80
+ By default, shape parameter for merging-firm-share is 2.5, and
81
+ 1.0 for all others. Defining, :attr:`mergeron.ShareSpec.dist_parms`
82
+ as a vector of shape parameters with length matching
83
+ that of :attr:`mergeron.ShareSpec.dist_parms` allows flexible specification
84
+ of Dirichlet-distributed share-data generation.
67
85
  """
68
86
 
69
87
  DIR_COND = "Conditional Dirichlet"
@@ -74,77 +92,143 @@ class SHRConstants(enum.StrEnum):
74
92
  """
75
93
 
76
94
 
77
- @define(slots=True, frozen=True)
95
+ @frozen(kw_only=False)
78
96
  class ShareSpec:
79
97
  """Market share specification
80
98
 
99
+ A key feature of market-share specification in this package is that
100
+ the draws represent markets with multiple different firm-counts.
101
+ Firm-counts are unspecified if the share distribution is
102
+ :attr:`mergeron.SHRDistribution.UNI`, for Dirichlet-distributed market-shares,
103
+ the default specification is that firm-counts vary between
104
+ 2 and 7 firms with each value equally likely.
105
+
81
106
  Notes
82
107
  -----
83
- If recapture is determined "outside-in", market shares cannot have
84
- Uniform distribution.
108
+ If :attr:`mergeron.gen.ShareSpec.dist_type` == :attr:`mergeron.gen.SHRDistribution.UNI`,
109
+ then it is infeasible that
110
+ :attr:`mergeron.gen.ShareSpec.recapture_form` == :attr:`mergeron.RECForm.OUTIN`.
111
+ In other words, if the distribution of markets over firm-counts is unspecified,
112
+ recapture ratios cannot be estimated using outside-good choice probabilities.
85
113
 
86
- If sample with varying firm counts is required, market shares must
87
- be specified as having a supported Dirichlet distribution.
114
+ For a sample with explicit firm counts, market shares must be specified as
115
+ having a supported Dirichlet distribution (see :class:`mergeron.gen.SHRDistribution`).
88
116
 
89
117
  """
90
118
 
91
- recapture_form: RECConstants
92
- """see RECConstants"""
119
+ dist_type: SHRDistribution = field(default=SHRDistribution.DIR_FLAT)
120
+ """See :class:`SHRDistribution`"""
121
+
122
+ @dist_type.validator # pyright: ignore
123
+ def __dtv(
124
+ _i: ShareSpec, _a: Attribute[SHRDistribution], _v: SHRDistribution
125
+ ) -> None:
126
+ if _v == SHRDistribution.UNI:
127
+ if _i.firm_counts_weights is not None:
128
+ raise ValueError(
129
+ "The specified value is incompatible with "
130
+ " :code:`distypte`=:attr`:`SHRDistribution.UNI`. "
131
+ "Set value to None or Consider revising the "
132
+ r"distribution type to :attr:`SHRDistribution.DIR_FLAT`, which gives "
133
+ "uniformly distributed draws on the :math:`n+1` simplex "
134
+ "for firm-count, :math:`n`. "
135
+ ""
136
+ )
137
+ elif _i.recapture_form == RECForm.OUTIN:
138
+ raise ValueError(
139
+ "Market share specification requires estimation of recapture ratio from "
140
+ "generated data. Either delete recapture ratio specification or set it to None."
141
+ )
142
+
143
+ dist_parms: ArrayFloat | ArrayINT | None = field(
144
+ default=None, eq=cmp_using(eq=np.array_equal)
145
+ )
146
+ """Parameters for tailoring market-share distribution
93
147
 
94
- recapture_rate: float | None
95
- """A value between 0 and 1.
148
+ For Uniform distribution, bounds of the distribution; defaults to `(0, 1)`;
149
+ for Dirichlet-type distributions, a vector of shape parameters of length
150
+ no less than the length of firm-count weights below; defaults depend on
151
+ type of Dirichlet-distribution specified.
96
152
 
97
- None if market share specification requires direct generation of
98
- outside good choice probabilities (RECConstants.OUTIN).
153
+ """
154
+
155
+ @dist_parms.validator # pyright: ignore
156
+ def __dpv(
157
+ _i: ShareSpec,
158
+ _a: Attribute[ArrayFloat | ArrayINT | None],
159
+ _v: ArrayFloat | ArrayINT | None,
160
+ ) -> None:
161
+ if (
162
+ _i.firm_counts_weights is not None
163
+ and _v is not None
164
+ and len(_v) < 1 + len(_i.firm_counts_weights)
165
+ ):
166
+ raise ValueError(
167
+ "If specified, the number of distribution parameters must be at least "
168
+ "the maximum firm-count premerger, which is 1 plus the length of the "
169
+ "vector specifying firm-count weights."
170
+ )
99
171
 
100
- The recapture rate is usually calibrated to the numbers-equivalent of the
101
- HHI threshold for the presumtion of harm from unilateral compoetitive effects
102
- in published merger guidelines. Accordingly, values for the recapture rate may be:
172
+ firm_counts_weights: ArrayFloat | ArrayINT | None = field(
173
+ default=DEFAULT_FCOUNT_WTS, eq=cmp_using(eq=np.array_equal)
174
+ )
175
+ """Relative or absolute frequencies of firm counts
103
176
 
104
- * 0.855, **6-to-5 merger from symmetry**; US Guidelines, 1992, 2023
105
- * 0.855, 6-to-5 merger from symmetry; EU Guidelines for horizontal mergers, 2004
106
- * 0.82, **6-to-5 merger to symmetry**; EU Guidelines for horizontal mergers, 2004
107
- * 0.80, 5-to-4 merger from symmetry; US Guidelines, 2010
108
- * 0.78, **5-to-4 merger to symmetry**; US Guidelines, 2010
177
+ Given frequencies are exogenous to generated market data sample;
178
+ for Dirichlet-type distributions, defaults to DEFAULT_FCOUNT_WTS, which specifies
179
+ firm-counts of 2 to 6 with weights in descending order from 5 to 1.
109
180
 
110
- Highlighting indicates hypothetical mergers close to the boundary of the presumption.
111
181
  """
112
182
 
113
- dist_type: SHRConstants
114
- """see SHRConstants"""
183
+ recapture_form: RECForm = field(default=RECForm.INOUT)
184
+ """See :class:`mergeron.RECForm`"""
115
185
 
116
- dist_parms: NDArray[np.float64] | None
117
- """Parameters for tailoring market-share distribution
186
+ recapture_ratio: float | None = field(default=DEFAULT_REC_RATIO)
187
+ """A value between 0 and 1.
118
188
 
119
- For Uniform distribution, bounds of the distribution; defaults to `(0, 1)`;
120
- for Beta distribution, shape parameters, defaults to `(1, 1)`;
121
- for Bounded-Beta distribution, vector of (min, max, mean, std. deviation), non-optional;
122
- for Dirichlet-type distributions, a vector of shape parameters of length
123
- no less than the length of firm-count weights below; defaults depend on
124
- type of Dirichlet-distribution specified.
189
+ :code:`None` if market share specification requires direct generation of
190
+ outside good choice probabilities (:attr:`mergeron.RECForm.OUTIN`).
125
191
 
126
- """
127
- firm_counts_weights: NDArray[np.float64 | np.int64] | None
128
- """relative or absolute frequencies of firm counts
192
+ The recapture ratio is usually calibrated to the numbers-equivalent of the
193
+ HHI threshold for the presumtion of harm from unilateral competitive effects
194
+ in published merger guidelines. Accordingly, the recapture ratio rounded to
195
+ the nearest 5% is:
129
196
 
197
+ * 0.85, **7-to-6 merger from symmetry**; US Guidelines, 1982, 1984, 1992, 2023
198
+ * 0.80, 5-to-4 merger from symmetry
199
+ * 0.80, **5-to-4 merger to symmetry**; US Guidelines, 2010
130
200
 
131
- Given frequencies are exogenous to generated market data sample;
132
- defaults to FCOUNT_WTS_DEFAULT, which specifies firm-counts of 2 to 6
133
- with weights in descending order from 5 to 1."""
201
+ Highlighting indicates hypothetical mergers in the neighborhood of (the boundary of)
202
+ the Guidelines presumption of harm. (In the EU Guidelines, concentration measures serve as
203
+ screens for further investigation, rather than as the basis for presumptions of harm or
204
+ presumptions no harm.)
205
+
206
+ """
207
+
208
+ @recapture_ratio.validator # pyright: ignore
209
+ def __rrv(_i: ShareSpec, _a: Attribute[float], _v: float) -> None:
210
+ if _v and not (0 < _v <= 1):
211
+ raise ValueError("Recapture ratio must lie in the interval, [0, 1).")
212
+ elif _v is None and _i.recapture_form != RECForm.OUTIN:
213
+ raise ValueError(
214
+ f"Recapture specification, {_i.recapture_form!r} requires that "
215
+ "the market sample specification inclues a recapture ratio in the "
216
+ "interval [0, 1)."
217
+ )
134
218
 
135
219
 
136
220
  @enum.unique
137
- class PCMConstants(enum.StrEnum):
221
+ class PCMDistribution(enum.StrEnum):
138
222
  """Margin distributions."""
139
223
 
140
224
  UNI = "Uniform"
141
225
  BETA = "Beta"
142
226
  BETA_BND = "Bounded Beta"
143
- EMPR = "Damodaran margin data"
227
+ EMPR = "Damodaran margin data, resampled"
144
228
 
145
229
 
146
230
  @enum.unique
147
- class FM2Constants(enum.StrEnum):
231
+ class FM2Constraint(enum.StrEnum):
148
232
  """Firm 2 margins - derivation methods."""
149
233
 
150
234
  IID = "i.i.d"
@@ -152,7 +236,7 @@ class FM2Constants(enum.StrEnum):
152
236
  SYM = "symmetric"
153
237
 
154
238
 
155
- @define(slots=True, frozen=True)
239
+ @frozen
156
240
  class PCMSpec:
157
241
  """Price-cost margin (PCM) specification
158
242
 
@@ -168,24 +252,56 @@ class PCMSpec:
168
252
 
169
253
  """
170
254
 
171
- firm2_pcm_constraint: FM2Constants
172
- """See FM2Constants"""
173
-
174
- dist_type: PCMConstants
175
- """See PCMConstants"""
255
+ dist_type: PCMDistribution = field(kw_only=False, default=PCMDistribution.UNI)
256
+ """See :class:`PCMDistribution`"""
176
257
 
177
- dist_parms: NDArray[np.float64] | None
258
+ dist_parms: ArrayDouble | None = field(kw_only=False, default=None)
178
259
  """Parameter specification for tailoring PCM distribution
179
260
 
180
261
  For Uniform distribution, bounds of the distribution; defaults to `(0, 1)`;
181
262
  for Beta distribution, shape parameters, defaults to `(1, 1)`;
182
263
  for Bounded-Beta distribution, vector of (min, max, mean, std. deviation), non-optional;
183
264
  for empirical distribution based on Damodaran margin data, optional, ignored
265
+
184
266
  """
185
267
 
268
+ @dist_parms.validator # pyright: ignore
269
+ def __dpv(
270
+ _i: PCMSpec, _a: Attribute[ArrayDouble | None], _v: ArrayDouble | None
271
+ ) -> None:
272
+ if _i.dist_type.name.startswith("BETA"):
273
+ if _v is None:
274
+ pass
275
+ elif np.array_equal(_v, DEFAULT_DIST_PARMS):
276
+ raise ValueError(
277
+ f"The distribution parameters, {DEFAULT_DIST_PARMS!r} "
278
+ "are not valid with margin distribution, {_dist_type_pcm!r}"
279
+ )
280
+ elif (
281
+ _i.dist_type == PCMDistribution.BETA and len(_v) != len(("a", "b"))
282
+ ) or (
283
+ _i.dist_type == PCMDistribution.BETA_BND
284
+ and len(_v) != len(("mu", "sigma", "max", "min"))
285
+ ):
286
+ raise ValueError(
287
+ f"Given number, {len(_v)} of parameters "
288
+ f'for PCM with distribution, "{_i.dist_type}" is incorrect.'
289
+ )
290
+
291
+ elif _i.dist_type == PCMDistribution.EMPR and _v is not None:
292
+ raise ValueError(
293
+ f"Empirical distribution does not require additional parameters; "
294
+ f'"given value, {_v!r} is ignored."'
295
+ )
296
+
297
+ firm2_pcm_constraint: FM2Constraint = field(
298
+ kw_only=False, default=FM2Constraint.IID
299
+ )
300
+ """See :class:`FM2Constraint`"""
301
+
186
302
 
187
303
  @enum.unique
188
- class SSZConstants(float, enum.ReprEnum):
304
+ class SSZConstant(float, enum.ReprEnum):
189
305
  """
190
306
  Scale factors to offset sample size reduction.
191
307
 
@@ -224,193 +340,46 @@ class SSZConstants(float, enum.ReprEnum):
224
340
  """When initial set of draws is not restricted in any way."""
225
341
 
226
342
 
227
- # Validators for selected attributes of MarketSampleSpec
228
- def _sample_size_validator(
229
- _object: MarketSampleSpec, _attribute: Attribute[int], _value: int, /
230
- ) -> None:
231
- if _value < 10**6:
232
- raise ValueError(
233
- f"Sample size must be no less than {10**6:,d}; got, {_value:,d}."
234
- )
235
-
236
-
237
- def _share_spec_validator(
238
- _instance: MarketSampleSpec, _attribute: Attribute[ShareSpec], _value: ShareSpec, /
239
- ) -> None:
240
- _r_bar = _value.recapture_rate
241
- if _r_bar and not (0 < _r_bar <= 1):
242
- raise ValueError("Recapture rate must lie in the interval, [0, 1).")
243
-
244
- elif _r_bar and _value.recapture_form == RECConstants.OUTIN:
245
- raise ValueError(
246
- "Market share specification requires estimation of recapture rate from "
247
- "generated data. Either delete recapture rate specification or set it to None."
248
- )
249
-
250
- if _value.dist_type == SHRConstants.UNI:
251
- if _value.recapture_form == RECConstants.OUTIN:
252
- raise ValueError(
253
- f"Invalid recapture specification, {_value.recapture_form!r} "
254
- "for market share specification with Uniform distribution. "
255
- "Redefine the market-sample specification, modifying the ."
256
- "market-share specification or the recapture specification."
257
- )
258
- elif _value.firm_counts_weights is not None:
259
- raise ValueError(
260
- "Generated data for markets with specified firm-counts or "
261
- "varying firm counts are not feasible for market shares "
262
- "with Uniform distribution. Consider revising the "
263
- r"distribution type to {SHRConstants.DIR_FLAT}, which gives "
264
- "uniformly distributed draws on the :math:`n+1` simplex "
265
- "for firm-count, :math:`n`."
266
- )
267
- elif _value.recapture_form != RECConstants.OUTIN and (
268
- _r_bar is None or not isinstance(_r_bar, float)
269
- ):
270
- raise ValueError(
271
- f"Recapture specification, {_value.recapture_form!r} requires that "
272
- "the market sample specification inclues a recapture rate."
273
- )
274
-
275
-
276
- def _pcm_spec_validator(
277
- _instance: MarketSampleSpec, _attribute: Attribute[PCMSpec], _value: PCMSpec, /
278
- ) -> None:
279
- if (
280
- _instance.share_spec.recapture_form == RECConstants.FIXED
281
- and _value.firm2_pcm_constraint == FM2Constants.MNL
282
- ):
283
- raise ValueError(
284
- "{} {} {}".format(
285
- f'Specification of "recapture_form", "{_instance.share_spec.recapture_form}"',
286
- "requires Firm 2 margin must have property, ",
287
- f'"{FM2Constants.IID}" or "{FM2Constants.SYM}".',
288
- )
289
- )
290
- elif _value.dist_type.name.startswith("BETA"):
291
- if _value.dist_parms is None:
292
- pass
293
- elif np.array_equal(_value.dist_parms, DIST_PARMS_DEFAULT):
294
- raise ValueError(
295
- f"The distribution parameters, {DIST_PARMS_DEFAULT!r} "
296
- "are not valid with margin distribution, {_dist_type_pcm!r}"
297
- )
298
- elif (
299
- _value.dist_type == PCMConstants.BETA
300
- and len(_value.dist_parms) != len(("max", "min"))
301
- ) or (
302
- _value.dist_type == PCMConstants.BETA_BND
303
- and len(_value.dist_parms) != len(("mu", "sigma", "max", "min"))
304
- ):
305
- raise ValueError(
306
- f"Given number, {len(_value.dist_parms)} of parameters "
307
- f'for PCM with distribution, "{_value.dist_type}" is incorrect.'
308
- )
309
-
310
-
311
- @define(slots=True, frozen=True)
312
- class MarketSampleSpec:
313
- """Parameter specification for market data generation."""
314
-
315
- sample_size: int = field(
316
- default=10**6, validator=(validators.instance_of(int), _sample_size_validator)
317
- )
318
- """sample size generated"""
319
-
320
- share_spec: ShareSpec = field(
321
- kw_only=True,
322
- default=ShareSpec(RECConstants.INOUT, 0.855, SHRConstants.UNI, None, None),
323
- validator=[validators.instance_of(ShareSpec), _share_spec_validator],
324
- )
325
- """Market-share specification, see definition of ShareSpec"""
326
-
327
- pcm_spec: PCMSpec = field(
328
- kw_only=True,
329
- default=PCMSpec(FM2Constants.IID, PCMConstants.UNI, None),
330
- validator=[validators.instance_of(PCMSpec), _pcm_spec_validator],
331
- )
332
- """Margin specification, see definition of PCMSpec"""
333
-
334
- price_spec: PRIConstants = field(
335
- kw_only=True,
336
- default=PRIConstants.SYM,
337
- validator=validators.instance_of(PRIConstants),
338
- )
339
- """Price specification, see PRIConstants"""
340
-
341
- hsr_filing_test_type: SSZConstants = field(
342
- kw_only=True,
343
- default=SSZConstants.ONE,
344
- validator=validators.instance_of(SSZConstants),
345
- )
346
- """Method for modeling HSR filing threholds, see SSZConstants"""
347
-
348
-
349
- @enum.unique
350
- class INVResolution(enum.StrEnum):
351
- CLRN = "clearance"
352
- ENFT = "enforcement"
353
- BOTH = "both"
354
-
355
-
356
- @define(slots=True, frozen=True)
357
- class UPPTestRegime:
358
- resolution: INVResolution = field(
359
- default=INVResolution.ENFT, validator=validators.instance_of(INVResolution)
360
- )
361
- guppi_aggregator: UPPAggrSelector = field(
362
- default=UPPAggrSelector.MAX, validator=validators.instance_of(UPPAggrSelector)
363
- )
364
- divr_aggregator: UPPAggrSelector | None = field(
365
- default=guppi_aggregator,
366
- validator=validators.instance_of((UPPAggrSelector, type(None))),
367
- )
368
-
369
-
370
- # https://stackoverflow.com/questions/54668000
371
- class DataclassInstance(Protocol):
372
- """Generic dataclass-instance"""
373
-
374
- __dataclass_fields__: ClassVar
343
+ # Validators for selected attributes of MarketSpec
375
344
 
376
345
 
377
346
  @dataclass(slots=True, frozen=True)
378
347
  class MarketDataSample:
379
348
  """Container for generated markets data sample."""
380
349
 
381
- frmshr_array: NDArray[np.float64]
350
+ frmshr_array: ArrayDouble
382
351
  """Merging-firm shares (with two merging firms)"""
383
352
 
384
- pcm_array: NDArray[np.float64]
353
+ pcm_array: ArrayDouble
385
354
  """Merging-firms' prices (normalized to 1, in default specification)"""
386
355
 
387
- price_array: NDArray[np.float64]
356
+ price_array: ArrayDouble
388
357
  """Merging-firms' price-cost margins (PCM)"""
389
358
 
390
- fcounts: NDArray[np.int64]
359
+ fcounts: ArrayBIGINT
391
360
  """Number of firms in market"""
392
361
 
393
- aggregate_purchase_prob: NDArray[np.float64]
362
+ aggregate_purchase_prob: ArrayDouble
394
363
  """
395
364
  One (1) minus probability that the outside good is chosen
396
365
 
397
366
  Converts market shares to choice probabilities by multiplication.
398
367
  """
399
368
 
400
- nth_firm_share: NDArray[np.float64]
369
+ nth_firm_share: ArrayDouble
401
370
  """Market-share of n-th firm
402
371
 
403
372
  Relevant for testing for draws the do or
404
373
  do not meet HSR filing thresholds.
405
374
  """
406
375
 
407
- divr_array: NDArray[np.float64]
376
+ divr_array: ArrayDouble
408
377
  """Diversion ratio between the merging firms"""
409
378
 
410
- hhi_post: NDArray[np.float64]
379
+ hhi_post: ArrayDouble
411
380
  """Post-merger change in Herfindahl-Hirschmann Index (HHI)"""
412
381
 
413
- hhi_delta: NDArray[np.float64]
382
+ hhi_delta: ArrayDouble
414
383
  """Change in HHI from combination of merging firms"""
415
384
 
416
385
 
@@ -422,16 +391,16 @@ class ShareDataSample:
422
391
  and aggregate purchase probability.
423
392
  """
424
393
 
425
- mktshr_array: NDArray[np.float64]
394
+ mktshr_array: ArrayDouble
426
395
  """All-firm shares (with two merging firms)"""
427
396
 
428
- fcounts: NDArray[np.int64]
397
+ fcounts: ArrayBIGINT
429
398
  """All-firm-count for each draw"""
430
399
 
431
- nth_firm_share: NDArray[np.float64]
400
+ nth_firm_share: ArrayDouble
432
401
  """Market-share of n-th firm"""
433
402
 
434
- aggregate_purchase_prob: NDArray[np.float64]
403
+ aggregate_purchase_prob: ArrayDouble
435
404
  """Converts market shares to choice probabilities by multiplication."""
436
405
 
437
406
 
@@ -439,10 +408,10 @@ class ShareDataSample:
439
408
  class PriceDataSample:
440
409
  """Container for generated price array, and related."""
441
410
 
442
- price_array: NDArray[np.float64]
411
+ price_array: ArrayDouble
443
412
  """Merging-firms' prices"""
444
413
 
445
- hsr_filing_test: NDArray[np.bool_]
414
+ hsr_filing_test: ArrayBoolean
446
415
  """Flags draws as meeting HSR filing thresholds or not"""
447
416
 
448
417
 
@@ -450,10 +419,10 @@ class PriceDataSample:
450
419
  class MarginDataSample:
451
420
  """Container for generated margin array and related MNL test array."""
452
421
 
453
- pcm_array: NDArray[np.float64]
422
+ pcm_array: ArrayDouble
454
423
  """Merging-firms' PCMs"""
455
424
 
456
- mnl_test_array: NDArray[np.bool_]
425
+ mnl_test_array: ArrayBoolean
457
426
  """Flags infeasible observations as False and rest as True
458
427
 
459
428
  Applying restrictions from Bertrand-Nash oligopoly
@@ -467,40 +436,83 @@ class MarginDataSample:
467
436
  """
468
437
 
469
438
 
439
+ @enum.unique
440
+ class INVResolution(enum.StrEnum):
441
+ CLRN = "clearance"
442
+ ENFT = "enforcement"
443
+ BOTH = "both"
444
+
445
+
446
+ @frozen
447
+ class UPPTestRegime:
448
+ """Configuration for UPP tests."""
449
+
450
+ resolution: INVResolution = field(
451
+ kw_only=False,
452
+ default=INVResolution.ENFT,
453
+ validator=validators.in_([INVResolution.CLRN, INVResolution.ENFT]),
454
+ )
455
+ """Whether to test clearance, enforcement, or both."""
456
+
457
+ guppi_aggregator: UPPAggrSelector = field(
458
+ kw_only=False, default=UPPAggrSelector.MIN
459
+ )
460
+ """Aggregator for GUPPI test."""
461
+
462
+ divr_aggregator: UPPAggrSelector = field(kw_only=False, default=UPPAggrSelector.MIN)
463
+ """Aggregator for diversion ratio test."""
464
+
465
+
470
466
  @dataclass(slots=True, frozen=True)
471
467
  class UPPTestsRaw:
472
- """arrays marking test failures and successes
468
+ """Container for arrays marking test failures and successes
473
469
 
474
470
  A test success is a draw ("market") that meeets the
475
471
  specified test criterion, and a test failure is
476
- one that does not; test criteria are defined and
477
- evaluated in:code:`guidelines_stats.gen_upp_arrays`.
472
+ one that does not; test criteria are evaluated in
473
+ :func:`enforcement_stats.gen_upp_arrays`.
478
474
  """
479
475
 
480
- guppi_test_simple: NDArray[np.bool_]
476
+ guppi_test_simple: ArrayBoolean
481
477
  """True if GUPPI estimate meets criterion"""
482
478
 
483
- guppi_test_compound: NDArray[np.bool_]
479
+ guppi_test_compound: ArrayBoolean
484
480
  """True if both GUPPI estimate and diversion ratio estimate
485
481
  meet criterion
486
482
  """
487
483
 
488
- cmcr_test: NDArray[np.bool_]
484
+ cmcr_test: ArrayBoolean
489
485
  """True if CMCR estimate meets criterion"""
490
486
 
491
- ipr_test: NDArray[np.bool_]
487
+ ipr_test: ArrayBoolean
492
488
  """True if IPR (partial price-simulation) estimate meets criterion"""
493
489
 
494
490
 
495
491
  @dataclass(slots=True, frozen=True)
496
492
  class UPPTestsCounts:
497
- """counts of markets resolved as specified
493
+ """Counts of markets resolved as specified
494
+
495
+ Resolution may be either :attr:`INVResolution.ENFT`,
496
+ :attr:`INVResolution.CLRN`, or :attr:`INVResolution.BOTH`.
497
+ In the case of :attr:`INVResolution.BOTH`, two colums of counts
498
+ are returned: one for each resolution.
498
499
 
499
- Resolution is specified in a UPPTestRegime object.
500
500
  """
501
501
 
502
- by_firm_count: NDArray[np.int64]
503
- by_delta: NDArray[np.int64]
504
- by_conczone: NDArray[np.int64]
505
- """Zones are "unoncentrated", "moderately concentrated", and "highly concentrated"
502
+ by_firm_count: ArrayBIGINT
503
+ by_delta: ArrayBIGINT
504
+ by_conczone: ArrayBIGINT
505
+ """Zones are "unoncentrated", "moderately concentrated", and "highly concentrated",
506
+ with futher detail by HHI and ΔHHI for mergers in the "unconcentrated" and
507
+ "moderately concentrated" zones. See
508
+ :attr:`mergeron.gen.enforcement_stats.HMG_PRESUMPTION_ZONE_MAP` and
509
+ :attr:`mergeron.gen.enforcement_stats.ZONE_VALS` for more detail.
510
+
506
511
  """
512
+
513
+
514
+ # https://stackoverflow.com/questions/54668000
515
+ class DataclassInstance(Protocol):
516
+ """Generic dataclass-instance"""
517
+
518
+ __dataclass_fields__: ClassVar