mergeron 2024.738940.0__py3-none-any.whl → 2024.738949.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 (24) hide show
  1. mergeron/core/excel_helper.py +38 -25
  2. mergeron/core/ftc_merger_investigations_data.py +33 -30
  3. mergeron/core/{guidelines_standards.py → guidelines_boundaries.py} +35 -29
  4. mergeron/core/proportions_tests.py +12 -10
  5. mergeron/examples/concentration_as_diversion.py +30 -26
  6. mergeron/examples/{safeharbor_boundaries_for_mergers_with_asymmetric_shares.py → enforcement_boundaries_for_mergers_with_asymmetric_shares.py} +84 -90
  7. mergeron/examples/{safeharbor_boundaries_for_symmetric_firm_mergers.py → enforcement_boundaries_for_symmetric_firm_mergers.py} +3 -3
  8. mergeron/examples/guidelines_enforcement_patterns.py +18 -16
  9. mergeron/examples/investigations_stats_obs_tables.py +15 -14
  10. mergeron/examples/investigations_stats_sim_tables.py +49 -54
  11. mergeron/examples/plotSafeHarbs_symbolically.py +1 -1
  12. mergeron/examples/sound_guppi_safeharbor.py +59 -54
  13. mergeron/examples/summarize_ftc_investigations_data.py +4 -4
  14. mergeron/examples/visualize_empirical_margin_distribution.py +2 -2
  15. mergeron/examples/visualize_guidelines_tests.py +67 -65
  16. mergeron/gen/__init__.py +104 -42
  17. mergeron/gen/_data_generation_functions_nonpublic.py +6 -6
  18. mergeron/gen/data_generation.py +1 -4
  19. mergeron/gen/investigations_stats.py +21 -27
  20. mergeron/gen/{guidelines_tests.py → upp_tests.py} +98 -102
  21. {mergeron-2024.738940.0.dist-info → mergeron-2024.738949.0.dist-info}/METADATA +2 -5
  22. mergeron-2024.738949.0.dist-info/RECORD +42 -0
  23. {mergeron-2024.738940.0.dist-info → mergeron-2024.738949.0.dist-info}/WHEEL +1 -1
  24. mergeron-2024.738940.0.dist-info/RECORD +0 -42
@@ -14,16 +14,29 @@ from .. import _PKG_NAME # noqa: TID252
14
14
 
15
15
  __version__ = version(_PKG_NAME)
16
16
 
17
- from collections.abc import Sequence
18
- from typing import Any, ClassVar
17
+ import enum
18
+ from collections.abc import Mapping, Sequence
19
+ from types import MappingProxyType
20
+ from typing import Any
19
21
 
20
- import aenum # type: ignore
21
22
  import numpy as np
22
23
  import numpy.typing as npt
23
24
  import xlsxwriter # type: ignore
24
25
 
25
26
 
26
- class CFmt(aenum.UniqueEnum): # type: ignore
27
+ @enum.unique
28
+ class CFmtParent(dict[str, Any], enum.ReprEnum):
29
+ def merge(self, _other) -> CFmtParent:
30
+ if isinstance(_other, CFmtParent):
31
+ return self.value | _other.value
32
+ else:
33
+ raise RuntimeWarning(
34
+ f"Object {_other!r} not valid for merge(), returned original."
35
+ )
36
+ return self.value
37
+
38
+
39
+ class CFmt(CFmtParent): # type: ignore
27
40
  """
28
41
  Initialize cell formats for xlsxwriter.
29
42
 
@@ -35,31 +48,31 @@ class CFmt(aenum.UniqueEnum): # type: ignore
35
48
  See, https://xlsxwriter.readthedocs.io/format.html
36
49
  """
37
50
 
38
- XL_DEFAULT: ClassVar = {"font_name": "Calibri", "font_size": 11}
39
- XL_DEFAULT_2003: ClassVar = {"font_name": "Arial", "font_size": 10}
51
+ XL_DEFAULT = MappingProxyType({"font_name": "Calibri", "font_size": 11})
52
+ XL_DEFAULT_2003 = MappingProxyType({"font_name": "Arial", "font_size": 10})
40
53
 
41
- A_CTR: ClassVar = {"align": "center"}
42
- A_CTR_ACROSS: ClassVar = {"align": "center_across"}
43
- A_LEFT: ClassVar = {"align": "left"}
44
- A_RIGHT: ClassVar = {"align": "right"}
54
+ A_CTR = MappingProxyType({"align": "center"})
55
+ A_CTR_ACROSS = MappingProxyType({"align": "center_across"})
56
+ A_LEFT = MappingProxyType({"align": "left"})
57
+ A_RIGHT = MappingProxyType({"align": "right"})
45
58
 
46
- BOLD: ClassVar = {"bold": True}
47
- ITALIC: ClassVar = {"italic": True}
48
- ULINE: ClassVar = {"underline": True}
59
+ BOLD = MappingProxyType({"bold": True})
60
+ ITALIC = MappingProxyType({"italic": True})
61
+ ULINE = MappingProxyType({"underline": True})
49
62
 
50
- TEXT_WRAP: ClassVar = {"text_wrap": True}
51
- IND_1: ClassVar = {"indent": 1}
63
+ TEXT_WRAP = MappingProxyType({"text_wrap": True})
64
+ IND_1 = MappingProxyType({"indent": 1})
52
65
 
53
- DOLLAR_NUM: ClassVar = {"num_format": "[$$-409]#,##0.00"}
54
- DT_NUM: ClassVar = {"num_format": "mm/dd/yyyy"}
55
- QTY_NUM: ClassVar = {"num_format": "#,##0.0"}
56
- PCT_NUM: ClassVar = {"num_format": "##0.000000%"}
57
- AREA_NUM: ClassVar = {"num_format": "0.00000000"}
66
+ DOLLAR_NUM = MappingProxyType({"num_format": "[$$-409]#,##0.00"})
67
+ DT_NUM = MappingProxyType({"num_format": "mm/dd/yyyy"})
68
+ QTY_NUM = MappingProxyType({"num_format": "#,##0.0"})
69
+ PCT_NUM = MappingProxyType({"num_format": "##0.000000%"})
70
+ AREA_NUM = MappingProxyType({"num_format": "0.00000000"})
58
71
 
59
- BAR_FILL: ClassVar = {"pattern": 1, "bg_color": "dfeadf"}
60
- BOT_BORDER: ClassVar = {"bottom": 1, "bottom_color": "000000"}
61
- TOP_BORDER: ClassVar = {"top": 1, "top_color": "000000"}
62
- HDR_BORDER: ClassVar = TOP_BORDER | BOT_BORDER
72
+ BAR_FILL = MappingProxyType({"pattern": 1, "bg_color": "dfeadf"})
73
+ BOT_BORDER = MappingProxyType({"bottom": 1, "bottom_color": "000000"})
74
+ TOP_BORDER = MappingProxyType({"top": 1, "top_color": "000000"})
75
+ HDR_BORDER = TOP_BORDER | BOT_BORDER
63
76
 
64
77
 
65
78
  def matrix_to_sheet(
@@ -216,7 +229,7 @@ def xl_fmt(
216
229
  :code:`xlsxwriter` `Format` object
217
230
 
218
231
  """
219
- _cell_fmt_dict: dict[str, Any] = {}
232
+ _cell_fmt_dict: Mapping[str, Any] = MappingProxyType({})
220
233
  if isinstance(_cell_fmt, tuple):
221
234
  ensure_cell_format_spec_tuple(_cell_fmt)
222
235
  for _cf in _cell_fmt:
@@ -81,13 +81,13 @@ CNT_FCOUNT_DICT = {
81
81
  }
82
82
 
83
83
 
84
- class TableData(NamedTuple):
84
+ class INVTableData(NamedTuple):
85
85
  ind_grp: str
86
86
  evid_cond: str
87
87
  data_array: NDArray[np.int64]
88
88
 
89
89
 
90
- INVData: TypeAlias = Mapping[str, dict[str, dict[str, TableData]]]
90
+ INVData: TypeAlias = Mapping[str, dict[str, dict[str, INVTableData]]]
91
91
 
92
92
 
93
93
  def construct_data(
@@ -130,13 +130,13 @@ def construct_data(
130
130
  if _archive_path.is_file() and not rebuild_data:
131
131
  _archived_data = msgpack.unpackb(_archive_path.read_bytes(), use_list=False)
132
132
 
133
- _invdata: dict[str, dict[str, dict[str, TableData]]] = {}
133
+ _invdata: dict[str, dict[str, dict[str, INVTableData]]] = {}
134
134
  for _period in _archived_data:
135
135
  _invdata[_period] = {}
136
136
  for _table_type in _archived_data[_period]:
137
137
  _invdata[_period][_table_type] = {}
138
138
  for _table_no in _archived_data[_period][_table_type]:
139
- _invdata[_period][_table_type][_table_no] = TableData(
139
+ _invdata[_period][_table_type][_table_no] = INVTableData(
140
140
  *_archived_data[_period][_table_type][_table_no]
141
141
  )
142
142
  return MappingProxyType(_invdata)
@@ -197,7 +197,7 @@ def _construct_no_entry_evidence_data(_invdata: INVData, _data_period: str, /) -
197
197
  _invdata_evid_cond = "No Entry Evidence"
198
198
 
199
199
  _invdata_sub_evid_cond_conc = _invdata[_data_period]["ByHHIandDelta"]
200
- _invdata_sub_evid_cond_conc["Table 9.X"] = TableData(
200
+ _invdata_sub_evid_cond_conc["Table 9.X"] = INVTableData(
201
201
  _invdata_ind_grp,
202
202
  _invdata_evid_cond,
203
203
  np.column_stack((
@@ -211,7 +211,7 @@ def _construct_no_entry_evidence_data(_invdata: INVData, _data_period: str, /) -
211
211
  )
212
212
 
213
213
  _invdata_sub_evid_cond_fcount = _invdata[_data_period]["ByFirmCount"]
214
- _invdata_sub_evid_cond_fcount["Table 10.X"] = TableData(
214
+ _invdata_sub_evid_cond_fcount["Table 10.X"] = INVTableData(
215
215
  _invdata_ind_grp,
216
216
  _invdata_evid_cond,
217
217
  np.column_stack((
@@ -231,7 +231,7 @@ def _construct_new_period_data(
231
231
  /,
232
232
  *,
233
233
  flag_backward_compatibility: bool = False,
234
- ) -> dict[str, dict[str, TableData]]:
234
+ ) -> dict[str, dict[str, INVTableData]]:
235
235
  _cuml_period = "1996-{}".format(int(_data_period.split("-")[1]))
236
236
  if _cuml_period != "1996-2011":
237
237
  raise ValueError('Expected cumulative period, "1996-2011"')
@@ -249,7 +249,7 @@ def _construct_new_period_data(
249
249
  _data_typesubdict = {}
250
250
  for _table_no in _invdata_cuml[_table_type]:
251
251
  _invdata_cuml_sub_table = _invdata_cuml[_table_type][_table_no]
252
- _invdata_indugrp, _invdata_evid_cond, _invdata_cuml_array = (
252
+ _invdata_ind_group, _invdata_evid_cond, _invdata_cuml_array = (
253
253
  _invdata_cuml_sub_table.ind_grp,
254
254
  _invdata_cuml_sub_table.evid_cond,
255
255
  _invdata_cuml_sub_table.data_array,
@@ -257,16 +257,16 @@ def _construct_new_period_data(
257
257
 
258
258
  _invdata_base_sub_table = _invdata_base[_table_type].get(_table_no, None)
259
259
 
260
- (_invdata_base_indugrp, _invdata_base_evid_cond, _invdata_base_array) = (
260
+ (_invdata_base_ind_group, _invdata_base_evid_cond, _invdata_base_array) = (
261
261
  _invdata_base_sub_table or ("", "", None)
262
262
  )
263
263
 
264
264
  # Some tables can't be constructed due to inconsistencies in the data
265
265
  # across time periods
266
266
  if (
267
- (_data_period != "2004-2011" and _invdata_indugrp != "All Markets")
268
- or (_invdata_indugrp in ('"Other" Markets', "Industries in Common"))
269
- or (_invdata_base_indugrp in ('"Other" Markets', ""))
267
+ (_data_period != "2004-2011" and _invdata_ind_group != "All Markets")
268
+ or (_invdata_ind_group in ('"Other" Markets', "Industries in Common"))
269
+ or (_invdata_base_ind_group in ('"Other" Markets', ""))
270
270
  ):
271
271
  continue
272
272
 
@@ -334,7 +334,7 @@ def _construct_new_period_data(
334
334
  # )
335
335
  # if np.einsum('ij->', invdata_array_bld_tbc):
336
336
  # print(
337
- # f"{_data_period}, {_table_no}, {_invdata_indugrp}:",
337
+ # f"{_data_period}, {_table_no}, {_invdata_ind_group}:",
338
338
  # abs(np.einsum('ij->', invdata_array_bld_tbc))
339
339
  # )
340
340
 
@@ -350,22 +350,22 @@ def _construct_new_period_data(
350
350
  np.einsum("ij->i", _invdata_array_bld_enfcls),
351
351
  ))
352
352
 
353
- _data_typesubdict[_table_no] = TableData(
354
- _invdata_indugrp, _invdata_evid_cond, _invdata_array_bld
353
+ _data_typesubdict[_table_no] = INVTableData(
354
+ _invdata_ind_group, _invdata_evid_cond, _invdata_array_bld
355
355
  )
356
- del _invdata_indugrp, _invdata_evid_cond, _invdata_cuml_array
357
- del _invdata_base_indugrp, _invdata_base_evid_cond, _invdata_base_array
356
+ del _invdata_ind_group, _invdata_evid_cond, _invdata_cuml_array
357
+ del _invdata_base_ind_group, _invdata_base_evid_cond, _invdata_base_array
358
358
  del _invdata_array_bld
359
359
  _invdata_bld[_table_type] = _data_typesubdict
360
360
  return _invdata_bld
361
361
 
362
362
 
363
363
  def _invdata_build_aggregate_table(
364
- _data_typesub: dict[str, TableData], _aggr_table_list: Sequence[str]
365
- ) -> TableData:
364
+ _data_typesub: dict[str, INVTableData], _aggr_table_list: Sequence[str]
365
+ ) -> INVTableData:
366
366
  _hdr_table_no = _aggr_table_list[0]
367
367
 
368
- return TableData(
368
+ return INVTableData(
369
369
  "Industries in Common",
370
370
  "Unrestricted on additional evidence",
371
371
  np.column_stack((
@@ -403,7 +403,7 @@ def parse_invdata(
403
403
  by range of HHI and ∆HHI.
404
404
 
405
405
  """
406
- _invdata: dict[str, dict[str, dict[str, TableData]]] = {}
406
+ _invdata: dict[str, dict[str, dict[str, INVTableData]]] = {}
407
407
 
408
408
  for _invdata_docname in _invdata_docnames:
409
409
  _invdata_pdf_path = FTCDATA_DIR.joinpath(_invdata_docname)
@@ -508,7 +508,7 @@ def _parse_table_blocks(
508
508
  )
509
509
 
510
510
  if _data_period == "1996-2011":
511
- _invdata_indugrp = (
511
+ _invdata_ind_group = (
512
512
  _table_blocks[1][-3].split("\n")[1]
513
513
  if _table_num == "Table 4.8"
514
514
  else _table_blocks[2][-3].split("\n")[0]
@@ -524,19 +524,19 @@ def _parse_table_blocks(
524
524
  elif _data_period == "1996-2005":
525
525
  _table_blocks = sorted(_table_blocks, key=itemgetter(6))
526
526
 
527
- _invdata_indugrp = _table_blocks[3][-3].strip()
527
+ _invdata_ind_group = _table_blocks[3][-3].strip()
528
528
  if _table_ser > 4:
529
529
  _invdata_evid_cond = _table_blocks[5][-3].strip()
530
530
 
531
531
  elif _table_ser % 2 == 0:
532
- _invdata_indugrp = _table_blocks[1][-3].split("\n")[2]
532
+ _invdata_ind_group = _table_blocks[1][-3].split("\n")[2]
533
533
  if (_evid_cond_teststr := _table_blocks[2][-3].strip()) == "Outcome":
534
534
  _invdata_evid_cond = "Unrestricted on additional evidence"
535
535
  else:
536
536
  _invdata_evid_cond = _evid_cond_teststr
537
537
 
538
538
  elif _table_blocks[3][-3].startswith("FTC Horizontal Merger Investigations"):
539
- _invdata_indugrp = _table_blocks[3][-3].split("\n")[2]
539
+ _invdata_ind_group = _table_blocks[3][-3].split("\n")[2]
540
540
  _invdata_evid_cond = "Unrestricted on additional evidence"
541
541
 
542
542
  else:
@@ -546,10 +546,10 @@ def _parse_table_blocks(
546
546
  if _table_ser == 9
547
547
  else _table_blocks[3][-3].strip()
548
548
  )
549
- _invdata_indugrp = _table_blocks[4][-3].split("\n")[2]
549
+ _invdata_ind_group = _table_blocks[4][-3].split("\n")[2]
550
550
 
551
- if _invdata_indugrp == "Pharmaceutical Markets":
552
- _invdata_indugrp = "Pharmaceuticals Markets"
551
+ if _invdata_ind_group == "Pharmaceutical Markets":
552
+ _invdata_ind_group = "Pharmaceuticals Markets"
553
553
 
554
554
  process_table_func = (
555
555
  _process_table_blks_conc_type
@@ -563,7 +563,7 @@ def _parse_table_blocks(
563
563
  print(_table_blocks)
564
564
  raise ValueError
565
565
 
566
- _table_data = TableData(_invdata_indugrp, _invdata_evid_cond, _table_array)
566
+ _table_data = INVTableData(_invdata_ind_group, _invdata_evid_cond, _table_array)
567
567
  _invdata[_data_period][_table_type] |= {_table_num: _table_data}
568
568
 
569
569
 
@@ -579,7 +579,7 @@ def _process_table_blks_conc_type(
579
579
  _conc_row_pat = re.compile(r"((?:0|\d,\d{3}) (?:- \d+,\d{3}|\+)|TOTAL)")
580
580
 
581
581
  _col_titles_array = tuple(CONC_DELTA_DICT.values())
582
- # _col_totals: NDArray[np.int64] | None = None
582
+ _col_totals: NDArray[np.int64] = np.zeros(len(_col_titles_array), np.int64)
583
583
  _invdata_array: NDArray[np.int64] = np.array(None)
584
584
 
585
585
  for _tbl_blk in _table_blocks:
@@ -636,6 +636,9 @@ def _process_table_blks_cnt_type(
636
636
  _cnt_row_pat = re.compile(r"(\d+ (?:to \d+|\+)|TOTAL)")
637
637
 
638
638
  _invdata_array: NDArray[np.int64] = np.array(None)
639
+ _col_totals: NDArray[np.int64] = np.zeros(
640
+ 3, np.int64
641
+ ) # "enforced", "closed", "total"
639
642
 
640
643
  for _tbl_blk in _table_blocks:
641
644
  if _cnt_row_pat.match(_blk_str := _tbl_blk[-3]):
@@ -15,7 +15,7 @@ from dataclasses import dataclass
15
15
  from typing import Any, Literal, TypeAlias
16
16
 
17
17
  import numpy as np
18
- from attr import define, field
18
+ from attrs import define, field
19
19
  from mpmath import mp, mpf # type: ignore
20
20
  from numpy.typing import NDArray
21
21
  from scipy.spatial.distance import minkowski as distance_function
@@ -33,7 +33,7 @@ class GuidelinesBoundary:
33
33
 
34
34
 
35
35
  @define(slots=True, frozen=True)
36
- class GuidelinesSTD:
36
+ class HMGThresholds:
37
37
  delta: float
38
38
  rec: float
39
39
  guppi: float
@@ -43,13 +43,12 @@ class GuidelinesSTD:
43
43
 
44
44
 
45
45
  @define(slots=True, frozen=True)
46
- class GuidelinesStandards:
46
+ class GuidelinesThresholds:
47
47
  """
48
- Guidelines standards by Guidelines publication year
49
-
50
- Diversion ratio, GUPPI, CMCR, and IPR standards are constructed from
51
- concentration standards.
48
+ Guidelines threholds by Guidelines publication year
52
49
 
50
+ ΔHHI, Recapture Rate, GUPPI, Diversion ratio, CMCR, and IPR thresholds
51
+ constructed from concentration standards.
53
52
  """
54
53
 
55
54
  pub_year: HMGPubYear
@@ -57,26 +56,26 @@ class GuidelinesStandards:
57
56
  Year of publication of the U.S. Horizontal Merger Guidelines (HMG)
58
57
  """
59
58
 
60
- safeharbor: GuidelinesSTD = field(kw_only=True, default=None)
59
+ safeharbor: HMGThresholds = field(kw_only=True, default=None)
61
60
  """
62
- Negative presumption defined on various measures
61
+ Negative presumption quantified on various measures
63
62
 
64
63
  ΔHHI safeharbor bound, default recapture rate, GUPPI bound,
65
64
  diversion ratio limit, CMCR, and IPR
66
65
  """
67
66
 
68
- inferred_presumption: GuidelinesSTD = field(kw_only=True, default=None)
67
+ imputed_presumption: HMGThresholds = field(kw_only=True, default=None)
69
68
  """
70
- Inferred ΔHHI safeharbor presumption and related measures
69
+ Presumption of harm imputed from guidelines
71
70
 
72
71
  ΔHHI bound inferred from strict numbers-equivalent
73
72
  of (post-merger) HHI presumption, and corresponding default recapture rate,
74
73
  GUPPI bound, diversion ratio limit, CMCR, and IPR
75
74
  """
76
75
 
77
- presumption: GuidelinesSTD = field(kw_only=True, default=None)
76
+ presumption: HMGThresholds = field(kw_only=True, default=None)
78
77
  """
79
- Guidelines ΔHHI safeharbor presumption and related measures
78
+ Presumption of harm defined in HMG
80
79
 
81
80
  ΔHHI bound and corresponding default recapture rate, GUPPI bound,
82
81
  diversion ratio limit, CMCR, and IPR
@@ -98,7 +97,7 @@ class GuidelinesStandards:
98
97
  object.__setattr__(
99
98
  self,
100
99
  "safeharbor",
101
- GuidelinesSTD(
100
+ HMGThresholds(
102
101
  _dh_s,
103
102
  _r := round_cust((_fc := int(np.ceil(1 / _hhi_p))) / (_fc + 1)),
104
103
  _g_s := gbd_from_dsf(_dh_s, m_star=1.0, r_bar=_r),
@@ -108,12 +107,12 @@ class GuidelinesStandards:
108
107
  ),
109
108
  )
110
109
 
111
- # inferred_presumption is relevant for 2010 Guidelines
110
+ # imputed_presumption is relevant for 2010 Guidelines
112
111
  object.__setattr__(
113
112
  self,
114
- "inferred_presumption",
113
+ "imputed_presumption",
115
114
  (
116
- GuidelinesSTD(
115
+ HMGThresholds(
117
116
  _dh_i := 2 * (0.5 / _fc) ** 2,
118
117
  _r_i := round_cust((_fc - 1 / 2) / (_fc + 1 / 2)),
119
118
  _g_i := gbd_from_dsf(_dh_i, m_star=1.0, r_bar=_r_i),
@@ -122,7 +121,7 @@ class GuidelinesStandards:
122
121
  _g_i,
123
122
  )
124
123
  if self.pub_year == 2010
125
- else GuidelinesSTD(
124
+ else HMGThresholds(
126
125
  _dh_i := 2 * (1 / (_fc + 1)) ** 2,
127
126
  _r,
128
127
  _g_i := gbd_from_dsf(_dh_i, m_star=1.0, r_bar=_r),
@@ -136,7 +135,7 @@ class GuidelinesStandards:
136
135
  object.__setattr__(
137
136
  self,
138
137
  "presumption",
139
- GuidelinesSTD(
138
+ HMGThresholds(
140
139
  _dh_p,
141
140
  _r,
142
141
  _g_p := gbd_from_dsf(_dh_p, m_star=1.0, r_bar=_r),
@@ -391,7 +390,13 @@ def boundary_plot(*, mktshares_plot_flag: bool = True) -> tuple[Any, ...]:
391
390
  _fig = plt.figure(figsize=(5, 5), dpi=600)
392
391
  _ax_out = _fig.add_subplot()
393
392
 
394
- def _set_axis_def(_ax1: mpa.Axes, /, mktshares_plot_flag: bool = False) -> mpa.Axes:
393
+ def _set_axis_def(
394
+ _ax1: mpa.Axes,
395
+ /,
396
+ *,
397
+ mktshares_plot_flag: bool = False,
398
+ mktshares_axlbls_flag: bool = False,
399
+ ) -> mpa.Axes:
395
400
  # Set the width of axis gridlines, and tick marks:
396
401
  # both axes, both major and minor ticks
397
402
  # Frame, grid, and facecolor
@@ -402,7 +407,7 @@ def boundary_plot(*, mktshares_plot_flag: bool = True) -> tuple[Any, ...]:
402
407
  _ax1.spines[_spos1].set_linewidth(0.0)
403
408
  _ax1.spines[_spos1].set_zorder(0)
404
409
  _ax1.spines[_spos1].set_visible(False)
405
- _ax1.set_facecolor("#F6F6F6")
410
+ _ax1.set_facecolor("#E6E6E6")
406
411
 
407
412
  _ax1.grid(linewidth=0.5, linestyle=":", color="grey", zorder=1)
408
413
  _ax1.tick_params(axis="x", which="both", width=0.5)
@@ -421,15 +426,16 @@ def boundary_plot(*, mktshares_plot_flag: bool = True) -> tuple[Any, ...]:
421
426
  _ax1.yaxis.get_majorticklabels(), horizontalalignment="right", fontsize=6
422
427
  )
423
428
 
424
- # Axis labels
425
- # x-axis
426
- _ax1.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
427
- _ax1.xaxis.set_label_coords(0.75, -0.1)
428
- # y-axis
429
- _ax1.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
430
- _ax1.yaxis.set_label_coords(-0.1, 0.75)
431
-
432
429
  if mktshares_plot_flag:
430
+ # Axis labels
431
+ if mktshares_axlbls_flag:
432
+ # x-axis
433
+ _ax1.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
434
+ _ax1.xaxis.set_label_coords(0.75, -0.1)
435
+ # y-axis
436
+ _ax1.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
437
+ _ax1.yaxis.set_label_coords(-0.1, 0.75)
438
+
433
439
  # Plot the ray of symmetry
434
440
  _ax1.plot(
435
441
  [0, 1], [0, 1], linewidth=0.5, linestyle=":", color="grey", zorder=1
@@ -7,6 +7,7 @@ Functions to estimate confidence intervals for
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from dataclasses import dataclass
10
11
  from importlib.metadata import version
11
12
 
12
13
  from .. import _PKG_NAME # noqa: TID252
@@ -14,7 +15,7 @@ from .. import _PKG_NAME # noqa: TID252
14
15
  __version__ = version(_PKG_NAME)
15
16
 
16
17
  from collections.abc import Sequence
17
- from typing import Literal, NamedTuple, TypeVar
18
+ from typing import Literal, TypeVar
18
19
 
19
20
  import numpy as np
20
21
  from numpy.typing import NBitBase, NDArray
@@ -101,12 +102,12 @@ def propn_ci(
101
102
  case "Agresti-Coull":
102
103
  _zsc = norm.ppf(1 - alpha / 2)
103
104
  _zscsq = _zsc * _zsc
104
- _adjmt_t = 4 if alpha == 0.05 else _zscsq
105
- _est_phat = (_npos + _adjmt_t / 2) / (_nobs + _adjmt_t)
105
+ _adjmt = 4 if alpha == 0.05 else _zscsq
106
+ _est_phat = (_npos + _adjmt / 2) / (_nobs + _adjmt)
106
107
  _est_ci_l, _est_ci_u = (
107
108
  _est_phat + _g
108
109
  for _g in [
109
- _f * _zsc * np.sqrt(_est_phat * (1 - _est_phat) / (_nobs + 4))
110
+ _f * _zsc * np.sqrt(_est_phat * (1 - _est_phat) / (_nobs + _adjmt))
110
111
  for _f in (-1, 1)
111
112
  ]
112
113
  )
@@ -441,7 +442,7 @@ def _propn_diff_chisq_mn(
441
442
  )
442
443
 
443
444
 
444
- def propn_ci_diff_multinomial(
445
+ def propn_diff_ci_multinomial(
445
446
  _counts: NDArray[np.integer[TI]], /, *, alpha: float = 0.05
446
447
  ) -> NDArray[np.float64]:
447
448
  """Estimate confidence intervals of pair-wise differences in multinomial proportions
@@ -475,16 +476,17 @@ def propn_ci_diff_multinomial(
475
476
  return np.column_stack([_d + _f * _d_cr * np.sqrt(_var) for _f in (-1, 1)])
476
477
 
477
478
 
478
- class MultinomialDiffTest(NamedTuple):
479
+ @dataclass(slots=True, frozen=True)
480
+ class MultinomialPropnsTest:
479
481
  estimate: np.float64
480
482
  dof: int
481
483
  critical_value: np.float64
482
484
  p_value: np.float64
483
485
 
484
486
 
485
- def propn_diff_multinomial_chisq(
487
+ def propn_test_multinomial(
486
488
  _counts: NDArray[np.integer[TI]], /, *, alpha: float = 0.05
487
- ) -> MultinomialDiffTest:
489
+ ) -> MultinomialPropnsTest:
488
490
  """Chi-square test for homogeneity of differences in multinomial proportions.
489
491
 
490
492
  Differences in multinomial proportions sum to zero.
@@ -510,9 +512,9 @@ def propn_diff_multinomial_chisq(
510
512
  _p_bar = _n / np.einsum("jk->j", _n_k / _prob)
511
513
 
512
514
  _y_sq = _n * ((1 / np.einsum("j->", _p_bar)) - 1)
513
- _dof = np.array([_f - 1 for _f in _counts.shape]).prod()
515
+ _dof = np.array([_s - 1 for _s in _counts.shape]).prod()
514
516
  _chi_rv = chi2(_dof)
515
517
 
516
- return MultinomialDiffTest(
518
+ return MultinomialPropnsTest(
517
519
  _y_sq, _dof, _chi_rv.ppf(1 - alpha), 1 - _chi_rv.cdf(_y_sq)
518
520
  )