mergeron 2025.739290.2__py3-none-any.whl → 2025.739290.4__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
@@ -8,14 +8,11 @@ from __future__ import annotations
8
8
 
9
9
  import enum
10
10
  from collections.abc import Sequence
11
- from dataclasses import dataclass
12
- from typing import ClassVar, Protocol
11
+ from operator import attrgetter
13
12
 
14
13
  import numpy as np
15
- from attrs import Attribute, Converter, cmp_using, field, validators
16
- from attrs import frozen as frozen_attrs
14
+ from attrs import Attribute, Converter, cmp_using, field, frozen, validators
17
15
  from numpy.random import SeedSequence
18
- from ruamel import yaml
19
16
 
20
17
  from .. import ( # noqa: TID252
21
18
  DEFAULT_REC_RATIO,
@@ -25,9 +22,11 @@ from .. import ( # noqa: TID252
25
22
  ArrayDouble,
26
23
  ArrayFloat,
27
24
  ArrayINT,
25
+ EnumYAMLized,
28
26
  RECForm,
29
27
  UPPAggrSelector,
30
28
  this_yaml,
29
+ yamelize_attrs,
31
30
  )
32
31
  from ..core.pseudorandom_numbers import ( # noqa: TID252
33
32
  DEFAULT_BETA_DIST_PARMS,
@@ -36,28 +35,29 @@ from ..core.pseudorandom_numbers import ( # noqa: TID252
36
35
 
37
36
  __version__ = VERSION
38
37
 
39
-
40
38
  DEFAULT_FCOUNT_WTS = np.asarray((_nr := np.arange(6, 0, -1)) / _nr.sum(), float)
41
39
 
40
+ DEFAULT_BETA_BND_DIST_PARMS = np.array([0.5, 1.0, 0.0, 1.0], float)
41
+
42
42
 
43
- @dataclass(slots=True, frozen=True)
43
+ @frozen
44
44
  class SeedSequenceData:
45
- share: SeedSequence
46
- pcm: SeedSequence
47
- fcounts: SeedSequence | None
48
- price: SeedSequence | None
45
+ share: SeedSequence = field(eq=attrgetter("state"))
46
+ pcm: SeedSequence = field(eq=attrgetter("state"))
47
+ fcounts: SeedSequence | None = field(eq=lambda x: x if x is None else x.state)
48
+ price: SeedSequence | None = field(eq=lambda x: x if x is None else x.state)
49
49
 
50
50
 
51
51
  @this_yaml.register_class
52
52
  @enum.unique
53
- class PriceSpec(tuple[bool, str | None], enum.ReprEnum):
53
+ class PriceSpec(tuple[bool, str | None], EnumYAMLized):
54
54
  """Price specification.
55
55
 
56
56
  Whether prices are symmetric and, if not, the direction of correlation, if any.
57
57
  """
58
58
 
59
59
  SYM = (True, None)
60
- ZERO = (False, None)
60
+ RNG = (False, None)
61
61
  NEG = (False, "negative share-correlation")
62
62
  POS = (False, "positive share-correlation")
63
63
  CSY = (False, "market-wide cost-symmetry")
@@ -65,7 +65,7 @@ class PriceSpec(tuple[bool, str | None], enum.ReprEnum):
65
65
 
66
66
  @this_yaml.register_class
67
67
  @enum.unique
68
- class SHRDistribution(enum.StrEnum):
68
+ class SHRDistribution(str, EnumYAMLized):
69
69
  """Market share distributions."""
70
70
 
71
71
  UNI = "Uniform"
@@ -104,30 +104,28 @@ def _fc_wts_conv(
104
104
  ) -> ArrayFloat | None:
105
105
  if _i.dist_type == SHRDistribution.UNI:
106
106
  return None
107
- elif _v is None or np.array_equal(_v, DEFAULT_FCOUNT_WTS):
107
+ elif _v is None or len(_v) == 0 or np.array_equal(_v, DEFAULT_FCOUNT_WTS):
108
108
  return DEFAULT_FCOUNT_WTS
109
109
  else:
110
110
  return _tv if (_tv := np.asarray(_v, float)).sum() == 1 else _tv / _tv.sum()
111
111
 
112
112
 
113
113
  def _shr_dp_conv(_v: Sequence[float] | ArrayFloat | None, _i: ShareSpec) -> ArrayFloat:
114
- match _v:
115
- case None if _i.dist_type == SHRDistribution.UNI:
114
+ if _v is None or len(_v) == 0 or np.array_equal(_v, DEFAULT_DIST_PARMS):
115
+ if _i.dist_type == SHRDistribution.UNI:
116
116
  return DEFAULT_DIST_PARMS
117
- case None:
118
- _fc_max = 1 + (
117
+ else:
118
+ fc_max = 1 + (
119
119
  len(DEFAULT_FCOUNT_WTS)
120
- if not hasattr(_i, "firm_counts_weights")
121
- or _i.firm_counts_weights is None
122
- or len(_i.firm_counts_weights) == 0
120
+ if _i.firm_counts_weights is None
123
121
  else len(_i.firm_counts_weights)
124
122
  )
125
123
 
126
124
  match _i.dist_type:
127
125
  case SHRDistribution.DIR_FLAT | SHRDistribution.DIR_FLAT_CONSTR:
128
- return np.ones(_fc_max, float)
126
+ return np.ones(fc_max, float)
129
127
  case SHRDistribution.DIR_ASYM:
130
- return np.array([2.0] * 6 + [1.5] * 5 + [1.25] * _fc_max, float)
128
+ return np.array([2.0] * 6 + [1.5] * 5 + [1.25] * fc_max, float)
131
129
  case SHRDistribution.DIR_COND:
132
130
  return np.array([], float)
133
131
  case _ if isinstance(_i.dist_type, SHRDistribution):
@@ -138,16 +136,15 @@ def _shr_dp_conv(_v: Sequence[float] | ArrayFloat | None, _i: ShareSpec) -> Arra
138
136
  raise ValueError(
139
137
  f"Unsupported distribution for market share generation, {_i.dist_type!r}"
140
138
  )
141
- case _ if isinstance(_v, Sequence | np.ndarray):
142
- return np.asarray(_v, float)
143
- case _:
144
- raise ValueError(
145
- f"Input, {_v!r} has invalid type. Must be None, Sequence of floats, or Numpy ndarray."
146
- )
139
+ elif isinstance(_v, Sequence | np.ndarray):
140
+ return np.asarray(_v, float)
141
+ else:
142
+ raise ValueError(
143
+ f"Input, {_v!r} has invalid type. Must be None, Sequence of floats, or Numpy ndarray."
144
+ )
147
145
 
148
146
 
149
- @this_yaml.register_class
150
- @frozen_attrs
147
+ @frozen
151
148
  class ShareSpec:
152
149
  """Market share specification
153
150
 
@@ -188,11 +185,11 @@ class ShareSpec:
188
185
  """
189
186
 
190
187
  @firm_counts_weights.default
191
- def _fcwd(_i: ShareSpec) -> ArrayFloat | None:
188
+ def __fcwd(_i: ShareSpec) -> ArrayFloat | None:
192
189
  return _fc_wts_conv(None, _i)
193
190
 
194
191
  @firm_counts_weights.validator
195
- def _fcv(_i: ShareSpec, _a: Attribute[ArrayFloat], _v: ArrayFloat) -> None:
192
+ def __fcv(_i: ShareSpec, _a: Attribute[ArrayFloat], _v: ArrayFloat) -> None:
196
193
  if _i.dist_type != SHRDistribution.UNI and not len(_v):
197
194
  raise ValueError(
198
195
  f"Attribute, {'"firm_counts_weights"'} must be populated if the share distribution is "
@@ -214,13 +211,13 @@ class ShareSpec:
214
211
  """
215
212
 
216
213
  @dist_parms.default
217
- def _dpd(_i: ShareSpec) -> ArrayFloat:
214
+ def __dpd(_i: ShareSpec) -> ArrayFloat:
218
215
  # converters run after defaults, and we
219
216
  # avoid redundancy and confusion here
220
217
  return _shr_dp_conv(None, _i)
221
218
 
222
219
  @dist_parms.validator
223
- def _dpv(_i: ShareSpec, _a: Attribute[ArrayFloat], _v: ArrayFloat) -> None:
220
+ def __dpv(_i: ShareSpec, _a: Attribute[ArrayFloat], _v: ArrayFloat) -> None:
224
221
  if (
225
222
  _i.firm_counts_weights is not None
226
223
  and _v is not None
@@ -236,7 +233,7 @@ class ShareSpec:
236
233
  """See :class:`mergeron.RECForm`"""
237
234
 
238
235
  @recapture_form.validator
239
- def _rfv(_i: ShareSpec, _a: Attribute[RECForm], _v: RECForm) -> None:
236
+ def __rfv(_i: ShareSpec, _a: Attribute[RECForm], _v: RECForm) -> None:
240
237
  if _i.dist_type == SHRDistribution.UNI and _v == RECForm.OUTIN:
241
238
  raise ValueError(
242
239
  "Outside-good choice probabilities cannot be generated if the "
@@ -271,11 +268,11 @@ class ShareSpec:
271
268
  """
272
269
 
273
270
  @recapture_ratio.default
274
- def _rrd(_i: ShareSpec) -> float | None:
271
+ def __rrd(_i: ShareSpec) -> float | None:
275
272
  return None if _i.recapture_form == RECForm.OUTIN else DEFAULT_REC_RATIO
276
273
 
277
274
  @recapture_ratio.validator
278
- def _rrv(_i: ShareSpec, _a: Attribute[float], _v: float) -> None:
275
+ def __rrv(_i: ShareSpec, _a: Attribute[float], _v: float) -> None:
279
276
  if _v and not (0 < _v <= 1):
280
277
  raise ValueError("Recapture ratio must lie in the interval, [0, 1).")
281
278
  elif _v is None and _i.recapture_form != RECForm.OUTIN:
@@ -285,25 +282,10 @@ class ShareSpec:
285
282
  "interval [0, 1)."
286
283
  )
287
284
 
288
- @classmethod
289
- def to_yaml(
290
- cls, _r: yaml.representer.SafeRepresenter, _d: ShareSpec
291
- ) -> yaml.MappingNode:
292
- _ret: yaml.MappingNode = _r.represent_mapping(
293
- f"!{cls.__name__}",
294
- {_a.name: getattr(_d, _a.name) for _a in _d.__attrs_attrs__},
295
- )
296
- return _ret
297
-
298
- @classmethod
299
- def from_yaml(
300
- cls, _c: yaml.constructor.SafeConstructor, _n: yaml.MappingNode
301
- ) -> ShareSpec:
302
- return cls(**_c.construct_mapping(_n))
303
-
304
285
 
286
+ @this_yaml.register_class
305
287
  @enum.unique
306
- class PCMDistribution(enum.StrEnum):
288
+ class PCMDistribution(str, EnumYAMLized):
307
289
  """Margin distributions."""
308
290
 
309
291
  UNI = "Uniform"
@@ -312,8 +294,9 @@ class PCMDistribution(enum.StrEnum):
312
294
  EMPR = "Damodaran margin data, resampled"
313
295
 
314
296
 
297
+ @this_yaml.register_class
315
298
  @enum.unique
316
- class FM2Constraint(enum.StrEnum):
299
+ class FM2Constraint(str, EnumYAMLized):
317
300
  """Firm 2 margins - derivation methods."""
318
301
 
319
302
  IID = "i.i.d"
@@ -321,8 +304,28 @@ class FM2Constraint(enum.StrEnum):
321
304
  SYM = "symmetric"
322
305
 
323
306
 
324
- @this_yaml.register_class
325
- @frozen_attrs
307
+ def _pcm_dp_conv(
308
+ _v: Sequence[float] | ArrayFloat | None, _i: PCMSpec
309
+ ) -> ArrayFloat | None:
310
+ if _i.dist_type == PCMDistribution.EMPR:
311
+ return None
312
+ elif _v is None or len(_v) == 0 or np.array_equal(_v, DEFAULT_DIST_PARMS):
313
+ match _i.dist_type:
314
+ case PCMDistribution.BETA:
315
+ return DEFAULT_BETA_DIST_PARMS
316
+ case PCMDistribution.BETA_BND:
317
+ return DEFAULT_BETA_BND_DIST_PARMS
318
+ case _:
319
+ return DEFAULT_DIST_PARMS
320
+ elif isinstance(_v, Sequence | np.ndarray):
321
+ return np.asarray(_v, float)
322
+ else:
323
+ raise ValueError(
324
+ f"Input, {_v!r} has invalid type. Must be None, Sequence of floats, or Numpy ndarray."
325
+ )
326
+
327
+
328
+ @frozen
326
329
  class PCMSpec:
327
330
  """Price-cost margin (PCM) specification
328
331
 
@@ -342,10 +345,14 @@ class PCMSpec:
342
345
  """See :class:`PCMDistribution`"""
343
346
 
344
347
  @dist_type.default
345
- def _dtd(_i: PCMSpec) -> PCMDistribution:
348
+ def __dtd(_i: PCMSpec) -> PCMDistribution:
346
349
  return PCMDistribution.UNI
347
350
 
348
- dist_parms: ArrayFloat | None = field(kw_only=True, eq=np.array_repr)
351
+ dist_parms: ArrayFloat | None = field(
352
+ kw_only=True,
353
+ eq=cmp_using(eq=np.array_equal),
354
+ converter=Converter(_pcm_dp_conv, takes_self=True), # type: ignore
355
+ )
349
356
  """Parameter specification for tailoring PCM distribution
350
357
 
351
358
  For Uniform distribution, bounds of the distribution; defaults to `(0, 1)`;
@@ -356,19 +363,11 @@ class PCMSpec:
356
363
  """
357
364
 
358
365
  @dist_parms.default
359
- def _dpwd(_i: PCMSpec) -> ArrayFloat | None:
360
- match _i.dist_type:
361
- case PCMDistribution.EMPR:
362
- return None
363
- case PCMDistribution.BETA:
364
- return DEFAULT_BETA_DIST_PARMS
365
- case PCMDistribution.BETA_BND:
366
- return np.array([0.5, 1.0, 0.0, 0.1], float)
367
- case _:
368
- return DEFAULT_DIST_PARMS
366
+ def __dpwd(_i: PCMSpec) -> ArrayFloat | None:
367
+ return _pcm_dp_conv(None, _i)
369
368
 
370
369
  @dist_parms.validator
371
- def _dpv(
370
+ def __dpv(
372
371
  _i: PCMSpec, _a: Attribute[ArrayFloat | None], _v: ArrayFloat | None
373
372
  ) -> None:
374
373
  if _i.dist_type.name.startswith("BETA"):
@@ -399,25 +398,10 @@ class PCMSpec:
399
398
  firm2_pcm_constraint: FM2Constraint = field(kw_only=True, default=FM2Constraint.IID)
400
399
  """See :class:`FM2Constraint`"""
401
400
 
402
- @classmethod
403
- def to_yaml(
404
- cls, _r: yaml.representer.SafeRepresenter, _d: PCMSpec
405
- ) -> yaml.MappingNode:
406
- _ret: yaml.MappingNode = _r.represent_mapping(
407
- f"!{cls.__name__}",
408
- {_a.name: getattr(_d, _a.name) for _a in _d.__attrs_attrs__},
409
- )
410
- return _ret
411
-
412
- @classmethod
413
- def from_yaml(
414
- cls, _c: yaml.constructor.SafeConstructor, _n: yaml.MappingNode
415
- ) -> PCMSpec:
416
- return cls(**_c.construct_mapping(_n))
417
-
418
401
 
402
+ @this_yaml.register_class
419
403
  @enum.unique
420
- class SSZConstant(float, enum.ReprEnum):
404
+ class SSZConstant(float, EnumYAMLized):
421
405
  """
422
406
  Scale factors to offset sample size reduction.
423
407
 
@@ -456,47 +440,69 @@ class SSZConstant(float, enum.ReprEnum):
456
440
  """When initial set of draws is not restricted in any way."""
457
441
 
458
442
 
459
- @dataclass(slots=True, frozen=True)
443
+ @frozen
460
444
  class MarketSampleData:
461
445
  """Container for generated markets data sample."""
462
446
 
463
- frmshr_array: ArrayDouble
447
+ frmshr_array: ArrayDouble = field(eq=cmp_using(np.array_equal))
464
448
  """Merging-firm shares (with two merging firms)"""
465
449
 
466
- pcm_array: ArrayDouble
450
+ pcm_array: ArrayDouble = field(eq=cmp_using(np.array_equal))
467
451
  """Merging-firms' prices (normalized to 1, in default specification)"""
468
452
 
469
- price_array: ArrayDouble
453
+ price_array: ArrayDouble = field(eq=cmp_using(np.array_equal))
470
454
  """Merging-firms' price-cost margins (PCM)"""
471
455
 
472
- fcounts: ArrayBIGINT
473
- """Number of firms in market"""
456
+ divr_array: ArrayDouble = field(eq=cmp_using(np.array_equal))
457
+ """Diversion ratio between the merging firms"""
474
458
 
475
- aggregate_purchase_prob: ArrayDouble
459
+ hhi_delta: ArrayDouble = field(eq=cmp_using(np.array_equal))
460
+ """Change in HHI from combination of merging firms"""
461
+
462
+ aggregate_purchase_prob: ArrayDouble = field(eq=cmp_using(np.array_equal))
476
463
  """
477
464
  One (1) minus probability that the outside good is chosen
478
465
 
479
466
  Converts market shares to choice probabilities by multiplication.
480
467
  """
481
468
 
482
- nth_firm_share: ArrayDouble
469
+ @aggregate_purchase_prob.default
470
+ def __appd(_i: MarketSampleData) -> ArrayINT:
471
+ e_ = np.empty_like(_i.frmshr_array[:, :1], float)
472
+ e_.fill(np.nan)
473
+ return e_
474
+
475
+ fcounts: ArrayINT = field(eq=cmp_using(np.array_equal))
476
+ """Number of firms in market"""
477
+
478
+ @fcounts.default
479
+ def __fcd(_i: MarketSampleData) -> ArrayINT:
480
+ return np.zeros_like(_i.frmshr_array[:, :1], np.uint8)
481
+
482
+ nth_firm_share: ArrayDouble = field(eq=cmp_using(np.array_equal))
483
483
  """Market-share of n-th firm
484
484
 
485
- Relevant for testing for draws the do or
485
+ Relevant for testing draws that do or
486
486
  do not meet HSR filing thresholds.
487
487
  """
488
488
 
489
- divr_array: ArrayDouble
490
- """Diversion ratio between the merging firms"""
489
+ @nth_firm_share.default
490
+ def __nfsd(_i: MarketSampleData) -> ArrayINT:
491
+ e_ = np.empty_like(_i.frmshr_array[:, :1], float)
492
+ e_.fill(np.nan)
493
+ return e_
491
494
 
492
- hhi_post: ArrayDouble
495
+ hhi_post: ArrayDouble = field(eq=cmp_using(np.array_equal))
493
496
  """Post-merger change in Herfindahl-Hirschmann Index (HHI)"""
494
497
 
495
- hhi_delta: ArrayDouble
496
- """Change in HHI from combination of merging firms"""
498
+ @hhi_post.default
499
+ def __hpd(_i: MarketSampleData) -> ArrayINT:
500
+ e_ = np.empty_like(_i.frmshr_array[:, :1], float)
501
+ e_.fill(np.nan)
502
+ return e_
497
503
 
498
504
 
499
- @dataclass(slots=True, frozen=True)
505
+ @frozen
500
506
  class ShareDataSample:
501
507
  """Container for generated market shares.
502
508
 
@@ -507,7 +513,7 @@ class ShareDataSample:
507
513
  mktshr_array: ArrayDouble
508
514
  """All-firm shares (with two merging firms)"""
509
515
 
510
- fcounts: ArrayBIGINT
516
+ fcounts: ArrayINT
511
517
  """All-firm-count for each draw"""
512
518
 
513
519
  nth_firm_share: ArrayDouble
@@ -517,7 +523,7 @@ class ShareDataSample:
517
523
  """Converts market shares to choice probabilities by multiplication."""
518
524
 
519
525
 
520
- @dataclass(slots=True, frozen=True)
526
+ @frozen
521
527
  class PriceDataSample:
522
528
  """Container for generated price array, and related."""
523
529
 
@@ -528,7 +534,7 @@ class PriceDataSample:
528
534
  """Flags draws as meeting HSR filing thresholds or not"""
529
535
 
530
536
 
531
- @dataclass(slots=True, frozen=True)
537
+ @frozen
532
538
  class MarginDataSample:
533
539
  """Container for generated margin array and related MNL test array."""
534
540
 
@@ -549,14 +555,15 @@ class MarginDataSample:
549
555
  """
550
556
 
551
557
 
558
+ @this_yaml.register_class
552
559
  @enum.unique
553
- class INVResolution(enum.StrEnum):
560
+ class INVResolution(str, EnumYAMLized):
554
561
  CLRN = "clearance"
555
562
  ENFT = "enforcement"
556
563
  BOTH = "investigation"
557
564
 
558
565
 
559
- @frozen_attrs
566
+ @frozen
560
567
  class UPPTestRegime:
561
568
  """Configuration for UPP tests."""
562
569
 
@@ -576,7 +583,7 @@ class UPPTestRegime:
576
583
  """Aggregator for diversion ratio test."""
577
584
 
578
585
 
579
- @dataclass(slots=True, frozen=True)
586
+ @frozen
580
587
  class UPPTestsRaw:
581
588
  """Container for arrays marking test failures and successes
582
589
 
@@ -601,7 +608,7 @@ class UPPTestsRaw:
601
608
  """True if IPR (partial price-simulation) estimate meets criterion"""
602
609
 
603
610
 
604
- @dataclass(slots=True, frozen=True)
611
+ @frozen
605
612
  class UPPTestsCounts:
606
613
  """Counts of markets resolved as specified
607
614
 
@@ -612,9 +619,9 @@ class UPPTestsCounts:
612
619
 
613
620
  """
614
621
 
615
- by_firm_count: ArrayBIGINT
616
- by_delta: ArrayBIGINT
617
- by_conczone: ArrayBIGINT
622
+ by_firm_count: ArrayBIGINT = field(eq=cmp_using(eq=np.array_equal))
623
+ by_delta: ArrayBIGINT = field(eq=cmp_using(eq=np.array_equal))
624
+ by_conczone: ArrayBIGINT = field(eq=cmp_using(eq=np.array_equal))
618
625
  """Zones are "unoncentrated", "moderately concentrated", and "highly concentrated",
619
626
  with futher detail by HHI and ΔHHI for mergers in the "unconcentrated" and
620
627
  "moderately concentrated" zones. See
@@ -624,50 +631,5 @@ class UPPTestsCounts:
624
631
  """
625
632
 
626
633
 
627
- # https://stackoverflow.com/questions/54668000
628
- class DataclassInstance(Protocol):
629
- """Generic dataclass-instance"""
630
-
631
- __dataclass_fields__: ClassVar
632
-
633
-
634
- for _typ in (
635
- PriceSpec,
636
- SHRDistribution,
637
- PCMDistribution,
638
- FM2Constraint,
639
- SSZConstant,
640
- INVResolution,
641
- ):
642
- # NOTE: If additional enums are defined in this module,
643
- # add themn to the list above
644
-
645
- _, _ = (
646
- this_yaml.representer.add_representer(
647
- _typ,
648
- lambda _r, _d: _r.represent_scalar(f"!{_d.__class__.__name__}", _d.name),
649
- ),
650
- this_yaml.constructor.add_constructor(
651
- f"!{_typ.__name__}",
652
- lambda _c, _n, /: getattr(
653
- globals().get(_n.tag.lstrip("!")), _c.construct_scalar(_n)
654
- ),
655
- ),
656
- )
657
-
658
- for _typ in (MarketSampleData, SeedSequenceData, UPPTestsCounts):
659
- _, _ = (
660
- this_yaml.representer.add_representer(
661
- _typ,
662
- lambda _r, _d: _r.rpresent_mapping(
663
- f"!{_d.__class__.__name__}",
664
- {_a: getattr(_d, _a) for _a in _d.__dataclass_fields__},
665
- ),
666
- ),
667
- this_yaml.constructor.add_constructor(
668
- _typ,
669
- lambda _c, _n: globals().get(
670
- _n.tag.lstrip("!")(**_c.construct_mapping(_n))
671
- ),
672
- ),
673
- )
634
+ for _typ in (SeedSequenceData, ShareSpec, PCMSpec, UPPTestsCounts, UPPTestRegime):
635
+ yamelize_attrs(_typ)