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
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, Protocol, TypeVar
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
16
+
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
29
+
30
+ __version__ = VERSION
31
+
22
32
 
23
- from ..core.pseudorandom_numbers import DIST_PARMS_DEFAULT # noqa: TID252
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
+ )
24
37
 
25
- EMPTY_ARRAY_DEFAULT = np.zeros(2)
26
- FCOUNT_WTS_DEFAULT = ((_nr := np.arange(1, 6)[::-1]) / _nr.sum()).astype(np.float64)
27
38
 
28
- TF = TypeVar("TF", bound=NBitBase)
29
- TI = TypeVar("TI", bound=NBitBase)
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,58 +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_spec: RECConstants
92
- """see RECConstants"""
93
-
94
- dist_type: SHRConstants
95
- """see SHRConstants"""
96
-
97
- dist_parms: NDArray[np.float64] | None
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
+ )
98
146
  """Parameters for tailoring market-share distribution
99
147
 
100
148
  For Uniform distribution, bounds of the distribution; defaults to `(0, 1)`;
101
- for Beta distribution, shape parameters, defaults to `(1, 1)`;
102
- for Bounded-Beta distribution, vector of (min, max, mean, std. deviation), non-optional;
103
149
  for Dirichlet-type distributions, a vector of shape parameters of length
104
150
  no less than the length of firm-count weights below; defaults depend on
105
151
  type of Dirichlet-distribution specified.
106
152
 
107
153
  """
108
- firm_counts_weights: NDArray[np.float64 | np.int64] | None
109
- """relative or absolute frequencies of firm counts
110
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
+ )
171
+
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
111
176
 
112
177
  Given frequencies are exogenous to generated market data sample;
113
- defaults to FCOUNT_WTS_DEFAULT, which specifies firm-counts of 2 to 6
114
- with weights in descending order from 5 to 1."""
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.
180
+
181
+ """
182
+
183
+ recapture_form: RECForm = field(default=RECForm.INOUT)
184
+ """See :class:`mergeron.RECForm`"""
185
+
186
+ recapture_ratio: float | None = field(default=DEFAULT_REC_RATIO)
187
+ """A value between 0 and 1.
188
+
189
+ :code:`None` if market share specification requires direct generation of
190
+ outside good choice probabilities (:attr:`mergeron.RECForm.OUTIN`).
191
+
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:
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
200
+
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
+ )
115
218
 
116
219
 
117
220
  @enum.unique
118
- class PCMConstants(enum.StrEnum):
221
+ class PCMDistribution(enum.StrEnum):
119
222
  """Margin distributions."""
120
223
 
121
224
  UNI = "Uniform"
122
225
  BETA = "Beta"
123
226
  BETA_BND = "Bounded Beta"
124
- EMPR = "Damodaran margin data"
227
+ EMPR = "Damodaran margin data, resampled"
125
228
 
126
229
 
127
230
  @enum.unique
128
- class FM2Constants(enum.StrEnum):
231
+ class FM2Constraint(enum.StrEnum):
129
232
  """Firm 2 margins - derivation methods."""
130
233
 
131
234
  IID = "i.i.d"
@@ -133,7 +236,7 @@ class FM2Constants(enum.StrEnum):
133
236
  SYM = "symmetric"
134
237
 
135
238
 
136
- @define(slots=True, frozen=True)
239
+ @frozen
137
240
  class PCMSpec:
138
241
  """Price-cost margin (PCM) specification
139
242
 
@@ -149,24 +252,56 @@ class PCMSpec:
149
252
 
150
253
  """
151
254
 
152
- dist_type: PCMConstants
153
- """See PCMConstants"""
154
-
155
- firm2_pcm_constraint: FM2Constants
156
- """See FM2Constants"""
255
+ dist_type: PCMDistribution = field(kw_only=False, default=PCMDistribution.UNI)
256
+ """See :class:`PCMDistribution`"""
157
257
 
158
- dist_parms: NDArray[np.float64] | None
258
+ dist_parms: ArrayDouble | None = field(kw_only=False, default=None)
159
259
  """Parameter specification for tailoring PCM distribution
160
260
 
161
261
  For Uniform distribution, bounds of the distribution; defaults to `(0, 1)`;
162
262
  for Beta distribution, shape parameters, defaults to `(1, 1)`;
163
263
  for Bounded-Beta distribution, vector of (min, max, mean, std. deviation), non-optional;
164
264
  for empirical distribution based on Damodaran margin data, optional, ignored
265
+
165
266
  """
166
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
+
167
302
 
168
303
  @enum.unique
169
- class SSZConstants(float, enum.ReprEnum):
304
+ class SSZConstant(float, enum.ReprEnum):
170
305
  """
171
306
  Scale factors to offset sample size reduction.
172
307
 
@@ -205,212 +340,46 @@ class SSZConstants(float, enum.ReprEnum):
205
340
  """When initial set of draws is not restricted in any way."""
206
341
 
207
342
 
208
- # Validators for selected attributes of MarketSampleSpec
209
- def _sample_size_validator(
210
- _object: MarketSampleSpec, _attribute: Attribute[int], _value: int, /
211
- ) -> None:
212
- if _value < 10**6:
213
- raise ValueError(
214
- f"Sample size must be not less than {10**6:,d}. Got, {_value:,d}."
215
- )
216
-
217
-
218
- def _recapture_rate_validator(
219
- _object: MarketSampleSpec,
220
- _attribute: Attribute[float | None],
221
- _value: float | None,
222
- /,
223
- ) -> None:
224
- if _value and not (0 < _value <= 1):
225
- raise ValueError("Recapture rate must lie in the interval, [0, 1).")
226
-
227
- if _value and _object.share_spec.recapture_spec == RECConstants.OUTIN:
228
- raise ValueError(
229
- "Market share specification requires estimation of recapture rate from "
230
- "generated data. Either delete recapture rate specification or set it to None."
231
- )
232
-
233
-
234
- def _share_spec_validator(
235
- _instance: MarketSampleSpec, _attribute: Attribute[ShareSpec], _value: ShareSpec, /
236
- ) -> None:
237
- _r_bar = _instance.recapture_rate
238
- if _value.dist_type == SHRConstants.UNI:
239
- if _value.recapture_spec == RECConstants.OUTIN:
240
- raise ValueError(
241
- f"Invalid recapture specification, {_value.recapture_spec!r} "
242
- "for market share specification with Uniform distribution. "
243
- "Redefine the market-sample specification, modifying the ."
244
- "market-share specification or the recapture specification."
245
- )
246
- elif _value.firm_counts_weights is not None:
247
- raise ValueError(
248
- "Generated data for markets with specified firm-counts or "
249
- "varying firm counts are not feasible with market shares "
250
- "with Uniform distribution. Consider revising the "
251
- r"distribution type to {SHRConstants.DIR_FLAT}, which gives "
252
- "uniformly distributed draws on the :math:`n+1` simplex "
253
- "for firm-count, :math:`n`."
254
- )
255
- # Outside-in calibration only valid for Dir-distributed shares
256
- elif _value.recapture_spec != RECConstants.OUTIN and (
257
- _r_bar is None or not isinstance(_r_bar, float)
258
- ):
259
- raise ValueError(
260
- f"Recapture specification, {_value.recapture_spec!r} requires that "
261
- "the market sample specification inclues a recapture rate."
262
- )
263
-
264
-
265
- def _pcm_spec_validator(
266
- _instance: MarketSampleSpec, _attribute: Attribute[PCMSpec], _value: PCMSpec, /
267
- ) -> None:
268
- if (
269
- _instance.share_spec.recapture_spec == RECConstants.FIXED
270
- and _value.firm2_pcm_constraint == FM2Constants.MNL
271
- ):
272
- raise ValueError(
273
- "{} {} {}".format(
274
- f'Specification of "recapture_spec", "{_instance.share_spec.recapture_spec}"',
275
- "requires Firm 2 margin must have property, ",
276
- f'"{FM2Constants.IID}" or "{FM2Constants.SYM}".',
277
- )
278
- )
279
- elif _value.dist_type.name.startswith("BETA"):
280
- if _value.dist_parms is None:
281
- pass
282
- elif np.array_equal(_value.dist_parms, DIST_PARMS_DEFAULT):
283
- raise ValueError(
284
- f"The distribution parameters, {DIST_PARMS_DEFAULT!r} "
285
- "are not valid with margin distribution, {_dist_type_pcm!r}"
286
- )
287
- elif (
288
- _value.dist_type == PCMConstants.BETA
289
- and len(_value.dist_parms) != len(("max", "min"))
290
- ) or (
291
- _value.dist_type == PCMConstants.BETA_BND
292
- and len(_value.dist_parms) != len(("mu", "sigma", "max", "min"))
293
- ):
294
- raise ValueError(
295
- f"Given number, {len(_value.dist_parms)} of parameters "
296
- f'for PCM with distribution, "{_value.dist_type}" is incorrect.'
297
- )
298
-
299
-
300
- @define(slots=True, frozen=True)
301
- class MarketSampleSpec:
302
- """Parameter specification for market data generation."""
303
-
304
- sample_size: int = field(
305
- default=10**6, validator=(validators.instance_of(int), _sample_size_validator)
306
- )
307
- """sample size generated"""
308
-
309
- recapture_rate: float | None = field(
310
- default=None, validator=_recapture_rate_validator
311
- )
312
- """market recapture rate
313
-
314
- Is None if market share specification requires generation of
315
- outside good choice probabilities (RECConstants.OUTIN).
316
- """
317
-
318
- pr_sym_spec: PRIConstants = field( # type: ignore
319
- kw_only=True,
320
- default=PRIConstants.SYM,
321
- validator=validators.instance_of(PRIConstants), # type: ignore
322
- )
323
- """Price specification, see PRIConstants"""
324
-
325
- share_spec: ShareSpec = field(
326
- kw_only=True,
327
- default=ShareSpec(RECConstants.INOUT, SHRConstants.UNI, None, None),
328
- validator=[validators.instance_of(ShareSpec), _share_spec_validator],
329
- )
330
- """See definition of ShareSpec"""
331
-
332
- pcm_spec: PCMSpec = field(
333
- kw_only=True,
334
- default=PCMSpec(PCMConstants.UNI, FM2Constants.IID, None),
335
- validator=[validators.instance_of(PCMSpec), _pcm_spec_validator],
336
- )
337
- """See definition of PCMSpec"""
338
-
339
- hsr_filing_test_type: SSZConstants = field( # type: ignore
340
- kw_only=True,
341
- default=SSZConstants.ONE,
342
- validator=validators.instance_of(SSZConstants), # type: ignore
343
- )
344
- """Method for modeling HSR filing threholds, see SSZConstants"""
345
-
346
-
347
- @enum.unique
348
- class INVResolution(enum.StrEnum):
349
- CLRN = "clearance"
350
- ENFT = "enforcement"
351
- BOTH = "both"
352
-
353
-
354
- @define(slots=True, frozen=True)
355
- class UPPTestRegime:
356
- resolution: INVResolution = field( # type: ignore
357
- default=INVResolution.ENFT,
358
- validator=validators.instance_of(INVResolution), # type: ignore
359
- )
360
- guppi_aggregator: UPPAggrSelector = field( # type: ignore
361
- default=UPPAggrSelector.MAX,
362
- validator=validators.instance_of(UPPAggrSelector), # type: ignore
363
- )
364
- divr_aggregator: UPPAggrSelector | None = field( # type: ignore
365
- default=guppi_aggregator,
366
- validator=validators.instance_of(UPPAggrSelector | None), # type: ignore
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