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
@@ -1,35 +1,71 @@
1
1
  """
2
- Specialized routines for defining and analyzing boundaries for Guidelines standards.
2
+ Specialized methods for defining and analyzing boundaries for Guidelines standards.
3
3
 
4
- These routines provide improved precision or demonstrate additional methods, but tend
5
- to have poor performance
4
+ These methods (functions) provide rely on scipy of sympy for core computations,
5
+ and may provide improved precision than core functions, but tend to have
6
+ poor performance
6
7
 
7
8
  """
8
9
 
9
- from importlib.metadata import version
10
+ from collections.abc import Callable
11
+ from dataclasses import dataclass
10
12
  from typing import Literal
11
13
 
12
14
  import numpy as np
13
15
  from mpmath import mp, mpf # type: ignore
14
16
  from scipy.spatial.distance import minkowski as distance_function # type: ignore
15
- from sympy import lambdify, simplify, solve, symbols
17
+ from sympy import lambdify, simplify, solve, symbols # type: ignore
16
18
 
17
- from .. import _PKG_NAME # noqa: TID252
18
- from .guidelines_boundaries import (
19
- GuidelinesBoundary,
20
- GuidelinesBoundaryCallable,
21
- _shrratio_boundary_intcpt,
22
- lerp,
23
- )
19
+ from .. import DEFAULT_REC_RATIO, VERSION, ArrayDouble # noqa: TID252
20
+ from . import guidelines_boundary_functions as gbfn
24
21
 
25
- __version__ = version(_PKG_NAME)
22
+ __version__ = VERSION
26
23
 
27
24
 
28
- mp.prec = 80
25
+ mp.dps = 32
29
26
  mp.trap_complex = True
30
27
 
31
28
 
32
- def delta_hhi_boundary_qdtr(_dh_val: float = 0.01) -> GuidelinesBoundaryCallable:
29
+ @dataclass(slots=True, frozen=True)
30
+ class GuidelinesBoundaryCallable:
31
+ boundary_function: Callable[[ArrayDouble], ArrayDouble]
32
+ area: float
33
+ s_naught: float = 0
34
+
35
+
36
+ def dh_area_quad(_dh_val: float = 0.01, /, *, dps: int = 9) -> float:
37
+ """
38
+ Area under the ΔHHI boundary.
39
+
40
+ When the given ΔHHI bound matches a Guidelines safeharbor,
41
+ the area under the boundary is half the intrinsic clearance rate
42
+ for the ΔHHI safeharbor.
43
+
44
+ Parameters
45
+ ----------
46
+ _dh_val
47
+ Merging-firms' ΔHHI bound.
48
+ dps
49
+ Specified precision in decimal places.
50
+
51
+ Returns
52
+ -------
53
+ Area under ΔHHI boundary.
54
+
55
+ """
56
+
57
+ _dh_val = mpf(f"{_dh_val}")
58
+ _s_naught = (1 - mp.sqrt(1 - 2 * _dh_val)) / 2
59
+
60
+ return round(
61
+ float(
62
+ _s_naught + mp.quad(lambda x: _dh_val / (2 * x), [_s_naught, 1 - _s_naught])
63
+ ),
64
+ dps,
65
+ )
66
+
67
+
68
+ def hhi_delta_boundary_qdtr(_dh_val: float = 0.01, /) -> GuidelinesBoundaryCallable:
33
69
  """
34
70
  Generate the list of share combination on the ΔHHI boundary.
35
71
 
@@ -37,8 +73,6 @@ def delta_hhi_boundary_qdtr(_dh_val: float = 0.01) -> GuidelinesBoundaryCallable
37
73
  ----------
38
74
  _dh_val:
39
75
  Merging-firms' ΔHHI bound.
40
- prec
41
- Number of decimal places for rounding reported shares.
42
76
 
43
77
  Returns
44
78
  -------
@@ -52,12 +86,12 @@ def delta_hhi_boundary_qdtr(_dh_val: float = 0.01) -> GuidelinesBoundaryCallable
52
86
 
53
87
  _hhi_eqn = _s_2 - 0.01 / (2 * _s_1)
54
88
 
55
- _hhi_bdry = solve(_hhi_eqn, _s_2)[0] # type: ignore
56
- _s_nought = float(solve(_hhi_eqn.subs({_s_2: 1 - _s_1}), _s_1)[0]) # type: ignore
89
+ _hhi_bdry = solve(_hhi_eqn, _s_2)[0]
90
+ _s_nought = float(solve(_hhi_eqn.subs({_s_2: 1 - _s_1}), _s_1)[0])
57
91
 
58
92
  _hhi_bdry_area = 2 * (
59
93
  _s_nought
60
- + mp.quad(lambdify(_s_1, _hhi_bdry, "mpmath"), (_s_nought, 1 - _s_nought))
94
+ + mp.quad(lambdify(_s_1, _hhi_bdry, "mpmath"), (_s_nought, 1 - _s_nought)) # pyright: ignore
61
95
  )
62
96
 
63
97
  return GuidelinesBoundaryCallable(
@@ -67,11 +101,11 @@ def delta_hhi_boundary_qdtr(_dh_val: float = 0.01) -> GuidelinesBoundaryCallable
67
101
 
68
102
  def shrratio_boundary_qdtr_wtd_avg(
69
103
  _delta_star: float = 0.075,
70
- _r_val: float = 0.80,
104
+ _r_val: float = DEFAULT_REC_RATIO,
71
105
  /,
72
106
  *,
73
107
  weighting: Literal["own-share", "cross-product-share"] | None = "own-share",
74
- recapture_spec: Literal["inside-out", "proportional"] = "inside-out",
108
+ recapture_form: Literal["inside-out", "proportional"] = "inside-out",
75
109
  ) -> GuidelinesBoundaryCallable:
76
110
  """
77
111
  Share combinations for the share-weighted average GUPPI boundary with symmetric
@@ -85,7 +119,7 @@ def shrratio_boundary_qdtr_wtd_avg(
85
119
  recapture ratio
86
120
  weighting
87
121
  Whether "own-share" or "cross-product-share" (or None for simple, unweighted average)
88
- recapture_spec
122
+ recapture_form
89
123
  Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
90
124
  value for both merging firms ("proportional").
91
125
 
@@ -109,23 +143,23 @@ def shrratio_boundary_qdtr_wtd_avg(
109
143
  * _s_1
110
144
  / (
111
145
  (1 - (_r_val * _s_2 + (1 - _r_val) * _s_1))
112
- if recapture_spec == "inside-out"
146
+ if recapture_form == "inside-out"
113
147
  else (1 - _s_2)
114
148
  )
115
149
  - (_s_1 + _s_2) * _delta_star
116
150
  )
117
151
 
118
- _bdry_func = solve(_bdry_eqn, _s_2)[0] # type: ignore
152
+ _bdry_func = solve(_bdry_eqn, _s_2)[0]
119
153
  _s_naught = (
120
154
  float(solve(simplify(_bdry_eqn.subs({_s_2: 1 - _s_1})), _s_1)[0]) # type: ignore
121
- if recapture_spec == "inside-out"
155
+ if recapture_form == "inside-out"
122
156
  else 0
123
157
  )
124
158
  _bdry_area = float(
125
159
  2
126
160
  * (
127
161
  _s_naught
128
- + mp.quad(lambdify(_s_1, _bdry_func, "mpmath"), (_s_naught, _s_mid))
162
+ + mp.quad(lambdify(_s_1, _bdry_func, "mpmath"), (_s_naught, _s_mid)) # pyright: ignore
129
163
  )
130
164
  - (_s_mid**2 + _s_naught**2)
131
165
  )
@@ -139,13 +173,13 @@ def shrratio_boundary_qdtr_wtd_avg(
139
173
  * _s_1
140
174
  / (
141
175
  (1 - (_r_val * _s_2 + (1 - _r_val) * _s_1))
142
- if recapture_spec == "inside-out"
176
+ if recapture_form == "inside-out"
143
177
  else (1 - _s_2)
144
178
  )
145
179
  - (_s_1 + _s_2) * _d_star
146
180
  )
147
181
 
148
- _bdry_func = solve(_bdry_eqn, _s_2)[1] # type: ignore
182
+ _bdry_func = solve(_bdry_eqn, _s_2)[1]
149
183
  _bdry_area = float(
150
184
  2
151
185
  * (
@@ -155,7 +189,7 @@ def shrratio_boundary_qdtr_wtd_avg(
155
189
  ),
156
190
  (0, _s_mid),
157
191
  )
158
- ).real
192
+ ).real # pyright: ignore
159
193
  - _s_mid**2
160
194
  )
161
195
 
@@ -167,15 +201,15 @@ def shrratio_boundary_qdtr_wtd_avg(
167
201
  * _s_1
168
202
  / (
169
203
  (1 - (_r_val * _s_2 + (1 - _r_val) * _s_1))
170
- if recapture_spec == "inside-out"
204
+ if recapture_form == "inside-out"
171
205
  else (1 - _s_2)
172
206
  )
173
207
  - _delta_star
174
208
  )
175
209
 
176
- _bdry_func = solve(_bdry_eqn, _s_2)[0] # type: ignore
210
+ _bdry_func = solve(_bdry_eqn, _s_2)[0]
177
211
  _bdry_area = float(
178
- 2 * (mp.quad(lambdify(_s_1, _bdry_func, "mpmath"), (0, _s_mid)))
212
+ 2 * (mp.quad(lambdify(_s_1, _bdry_func, "mpmath"), (0, _s_mid))) # pyright: ignore
179
213
  - _s_mid**2
180
214
  )
181
215
 
@@ -186,20 +220,20 @@ def shrratio_boundary_qdtr_wtd_avg(
186
220
 
187
221
  def shrratio_boundary_distance(
188
222
  _delta_star: float = 0.075,
189
- _r_val: float = 0.80,
223
+ _r_val: float = DEFAULT_REC_RATIO,
190
224
  /,
191
225
  *,
192
- agg_method: Literal["arithmetic", "distance"] = "arithmetic",
226
+ agg_method: Literal["arithmetic mean", "distance"] = "arithmetic mean",
193
227
  weighting: Literal["own-share", "cross-product-share"] | None = "own-share",
194
- recapture_spec: Literal["inside-out", "proportional"] = "inside-out",
195
- prec: int = 5,
196
- ) -> GuidelinesBoundary:
228
+ recapture_form: Literal["inside-out", "proportional"] = "inside-out",
229
+ dps: int = 5,
230
+ ) -> gbfn.GuidelinesBoundary:
197
231
  """
198
232
  Share combinations for the GUPPI boundaries using various aggregators with
199
233
  symmetric merging-firm margins.
200
234
 
201
235
  Reimplements the arithmetic-averages and distance estimations from function,
202
- `shrratio_boundary_wtd_avg`but uses the Minkowski-distance function,
236
+ `shrratio_boundary_wtd_avg` but uses the Minkowski-distance function,
203
237
  `scipy.spatial.distance.minkowski` for all aggregators. This reimplementation
204
238
  is useful for testing the output of `shrratio_boundary_wtd_avg`
205
239
  but runs considerably slower.
@@ -211,13 +245,13 @@ def shrratio_boundary_distance(
211
245
  _r_val
212
246
  recapture ratio
213
247
  agg_method
214
- Whether "arithmetic", "geometric", or "distance".
248
+ Whether "arithmetic mean" or "distance".
215
249
  weighting
216
250
  Whether "own-share" or "cross-product-share".
217
- recapture_spec
251
+ recapture_form
218
252
  Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
219
253
  value for both merging firms ("proportional").
220
- prec
254
+ dps
221
255
  Number of decimal places for rounding returned shares and area.
222
256
 
223
257
  Returns
@@ -232,11 +266,11 @@ def shrratio_boundary_distance(
232
266
  # initial conditions
233
267
  _gbdry_points = [(_s_mid, _s_mid)]
234
268
  _s_1_pre, _s_2_pre = _s_mid, _s_mid
235
- _s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0, 0
269
+ _s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0.0, 0.0
236
270
 
237
271
  # parameters for iteration
238
272
  _weights_base = (mpf("0.5"),) * 2
239
- _gbd_step_sz = mp.power(10, -prec)
273
+ _gbd_step_sz = mp.power(10, -dps)
240
274
  _theta = _gbd_step_sz * (10 if weighting == "cross-product-share" else 1)
241
275
  for _s_1 in mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz):
242
276
  # The wtd. avg. GUPPI is not always convex to the origin, so we
@@ -251,8 +285,8 @@ def shrratio_boundary_distance(
251
285
  while True:
252
286
  _de_1 = _s_2 / (1 - _s_1)
253
287
  _de_2 = (
254
- _s_1 / (1 - lerp(_s_1, _s_2, _r_val))
255
- if recapture_spec == "inside-out"
288
+ _s_1 / (1 - gbfn.lerp(_s_1, _s_2, _r_val))
289
+ if recapture_form == "inside-out"
256
290
  else _s_1 / (1 - _s_2)
257
291
  )
258
292
 
@@ -269,7 +303,7 @@ def shrratio_boundary_distance(
269
303
  )
270
304
 
271
305
  match agg_method:
272
- case "arithmetic":
306
+ case "arithmetic mean":
273
307
  _delta_test = distance_function(
274
308
  (_de_1, _de_2), (0.0, 0.0), p=1, w=_weights_i
275
309
  )
@@ -306,11 +340,11 @@ def shrratio_boundary_distance(
306
340
  else:
307
341
  _s_2_oddsum -= _s_1_pre
308
342
 
309
- _s_intcpt = _shrratio_boundary_intcpt(
343
+ _s_intcpt = gbfn._shrratio_boundary_intcpt(
310
344
  _s_1_pre,
311
345
  _delta_star,
312
346
  _r_val,
313
- recapture_spec=recapture_spec,
347
+ recapture_form=recapture_form,
314
348
  agg_method=agg_method,
315
349
  weighting=weighting,
316
350
  )
@@ -331,11 +365,9 @@ def shrratio_boundary_distance(
331
365
  # Area under boundary
332
366
  _gbdry_area_total = 2 * _gbd_prtlarea - mp.power(_s_mid, "2")
333
367
 
334
- _gbdry_points = np.row_stack((_gbdry_points, (mpf("0.0"), _s_intcpt))).astype(
335
- np.float64
336
- )
368
+ _gbdry_points.append((mpf("0.0"), _s_intcpt))
337
369
  # Points defining boundary to point-of-symmetry
338
- return GuidelinesBoundary(
339
- np.row_stack((np.flip(_gbdry_points, 0), np.flip(_gbdry_points[1:], 1))),
340
- round(float(_gbdry_area_total), prec),
370
+ return gbfn.GuidelinesBoundary(
371
+ np.vstack((_gbdry_points[::-1], np.flip(_gbdry_points[1:], 1))),
372
+ round(float(_gbdry_area_total), dps),
341
373
  )
@@ -8,24 +8,18 @@ https://github.com/numpy/numpy/issues/16313.
8
8
 
9
9
  import concurrent.futures
10
10
  from collections.abc import Sequence
11
- from importlib.metadata import version
12
11
  from multiprocessing import cpu_count
13
- from typing import Literal, TypeVar
12
+ from typing import Literal
14
13
 
15
14
  import numpy as np
16
15
  from numpy.random import PCG64DXSM, Generator, SeedSequence
17
- from numpy.typing import NBitBase, NDArray
18
16
 
19
- from .. import _PKG_NAME # noqa: TID252
17
+ from .. import VERSION, ArrayDouble # noqa: TID252
20
18
 
21
- __version__ = version(_PKG_NAME)
22
-
23
-
24
- TF = TypeVar("TF", bound=NBitBase)
25
- TI = TypeVar("TI", bound=NBitBase)
19
+ __version__ = VERSION
26
20
 
27
21
  NTHREADS = 2 * cpu_count()
28
- DIST_PARMS_DEFAULT = np.array([0.0, 1.0], np.float64)
22
+ DEFAULT_DIST_PARMS = np.array([0.0, 1.0], np.float64)
29
23
 
30
24
 
31
25
  def prng(_s: SeedSequence | None = None, /) -> np.random.Generator:
@@ -123,7 +117,7 @@ class MultithreadedRNG:
123
117
 
124
118
  Parameters
125
119
  ----------
126
- _out_array
120
+ __out_array
127
121
  The output array to which generated data are written.
128
122
  Its dimensions define the size of the sample.
129
123
  dist_type
@@ -139,24 +133,24 @@ class MultithreadedRNG:
139
133
 
140
134
  def __init__(
141
135
  self,
142
- _out_array: NDArray[np.float64],
136
+ __out_array: ArrayDouble,
143
137
  /,
144
138
  *,
145
139
  dist_type: Literal[
146
140
  "Beta", "Dirichlet", "Gaussian", "Normal", "Random", "Uniform"
147
141
  ] = "Uniform",
148
- dist_parms: NDArray[np.floating[TF]] | None = DIST_PARMS_DEFAULT, # type: ignore
142
+ dist_parms: ArrayDouble | None = DEFAULT_DIST_PARMS,
149
143
  seed_sequence: SeedSequence | None = None,
150
144
  nthreads: int = NTHREADS,
151
145
  ):
152
146
  self.thread_count = nthreads
153
147
 
154
- _seed_sequence = seed_sequence or SeedSequence(pool_size=8)
148
+ __seed_sequence = seed_sequence or SeedSequence(pool_size=8)
155
149
  self._random_generators = [
156
- prng(_t) for _t in _seed_sequence.spawn(self.thread_count)
150
+ prng(_t) for _t in __seed_sequence.spawn(self.thread_count)
157
151
  ]
158
152
 
159
- self.sample_sz = len(_out_array)
153
+ self.sample_sz = len(__out_array)
160
154
 
161
155
  if dist_type not in (_rdts := ("Beta", "Dirichlet", "Normal", "Uniform")):
162
156
  raise ValueError("Specified distribution must be one of {_rdts}")
@@ -172,7 +166,7 @@ class MultithreadedRNG:
172
166
 
173
167
  self.dist_type = dist_type
174
168
 
175
- if dist_parms is None or np.array_equal(dist_parms, DIST_PARMS_DEFAULT):
169
+ if dist_parms is None or np.array_equal(dist_parms, DEFAULT_DIST_PARMS):
176
170
  match dist_type:
177
171
  case "Uniform":
178
172
  self.dist_type = "Random"
@@ -189,10 +183,10 @@ class MultithreadedRNG:
189
183
  )
190
184
 
191
185
  elif dist_type == "Dirichlet":
192
- if len(dist_parms) != _out_array.shape[1]:
186
+ if len(dist_parms) != __out_array.shape[1]:
193
187
  raise ValueError(
194
188
  f"Insufficient shape parameters for requested Dirichlet sample "
195
- f"of size, {_out_array.shape}"
189
+ f"of size, {__out_array.shape}"
196
190
  )
197
191
 
198
192
  elif (_lrdp := len(dist_parms)) != 2:
@@ -200,7 +194,7 @@ class MultithreadedRNG:
200
194
 
201
195
  self.dist_parms = dist_parms
202
196
 
203
- self.values = _out_array
197
+ self.values = __out_array
204
198
  self.executor = concurrent.futures.ThreadPoolExecutor(self.thread_count)
205
199
 
206
200
  self.step_size = (len(self.values) / self.thread_count).__ceil__()
@@ -211,8 +205,8 @@ class MultithreadedRNG:
211
205
  def _fill(
212
206
  _rng: np.random.Generator,
213
207
  _dist_type: str,
214
- _dist_parms: NDArray[np.floating[TF]],
215
- _out: NDArray[np.float64],
208
+ _dist_parms: ArrayDouble,
209
+ _out: ArrayDouble,
216
210
  _first: int,
217
211
  _last: int,
218
212
  /,
@@ -0,0 +1,3 @@
1
+ from .. import VERSION # noqa: TID252
2
+
3
+ __version__ = VERSION
Binary file
@@ -0,0 +1,3 @@
1
+ from .. import VERSION # noqa: TID252
2
+
3
+ __version__ = VERSION
@@ -0,0 +1,86 @@
1
+ """
2
+ Plot the empirical distribution derived using the Gaussian KDE with
3
+ margin data downloaded from Prof. Damodaran's website at NYU.
4
+
5
+ """
6
+
7
+ import warnings
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+ from matplotlib.ticker import StrMethodFormatter
12
+ from numpy.random import PCG64DXSM, Generator, SeedSequence
13
+ from scipy import stats # type: ignore
14
+
15
+ import mergeron.core.empirical_margin_distribution as dmgn
16
+ from mergeron import DATA_DIR
17
+ from mergeron.core.guidelines_boundary_functions import boundary_plot
18
+
19
+ SAMPLE_SIZE = 10**6
20
+ BIN_COUNT = 25
21
+ mgn_data_obs, mgn_data_wts, mgn_data_stats = dmgn.mgn_data_builder()
22
+ print(repr(mgn_data_obs))
23
+ print(repr(mgn_data_stats))
24
+
25
+ plt, mgn_fig, mgn_ax, set_axis_def = boundary_plot(mktshares_plot_flag=False)
26
+ mgn_fig.set_figheight(6.5)
27
+ mgn_fig.set_figwidth(9.0)
28
+
29
+ _, mgn_bins, _ = mgn_ax.hist(
30
+ x=mgn_data_obs,
31
+ weights=mgn_data_wts,
32
+ bins=BIN_COUNT,
33
+ alpha=0.4,
34
+ density=True,
35
+ label="Downloaded data",
36
+ color="#004488", # Paul Tol's High Contrast Blue
37
+ )
38
+
39
+ with warnings.catch_warnings():
40
+ warnings.filterwarnings("ignore", category=UserWarning)
41
+ # Don't warn regarding the below; ticklabels have been fixed before this point
42
+ mgn_ax.set_yticklabels([
43
+ f"{float(_g.get_text()) * np.diff(mgn_bins)[-1]:.0%}"
44
+ for _g in mgn_ax.get_yticklabels()
45
+ ])
46
+
47
+ mgn_kde = stats.gaussian_kde(mgn_data_obs, weights=mgn_data_wts, bw_method="silverman")
48
+ mgn_kde.set_bandwidth(bw_method=mgn_kde.factor / 3.0) # pyright: ignore
49
+
50
+ mgn_ax.plot(
51
+ (_xv := np.linspace(0, BIN_COUNT, 10**5) / BIN_COUNT),
52
+ mgn_kde(_xv),
53
+ color="#004488",
54
+ rasterized=True,
55
+ label="Estimated Density",
56
+ )
57
+
58
+ mgn_ax.hist(
59
+ x=mgn_kde.resample(
60
+ SAMPLE_SIZE, seed=Generator(PCG64DXSM(SeedSequence(pool_size=8)))
61
+ )[0],
62
+ color="#DDAA33", # Paul Tol's High Contrast Yellow
63
+ alpha=0.6,
64
+ bins=BIN_COUNT,
65
+ density=True,
66
+ label="Generated data",
67
+ )
68
+
69
+ mgn_ax.legend(
70
+ loc="best",
71
+ fancybox=False,
72
+ shadow=False,
73
+ frameon=True,
74
+ facecolor="white",
75
+ edgecolor="white",
76
+ framealpha=1,
77
+ fontsize="small",
78
+ )
79
+
80
+ mgn_ax.set_xlim(0.0, 1.0)
81
+ mgn_ax.xaxis.set_major_formatter(StrMethodFormatter("{x:>3.0%}"))
82
+ mgn_ax.set_xlabel("Price Cost Margin", fontsize=10)
83
+ mgn_ax.set_ylabel("Relative Frequency", fontsize=10)
84
+
85
+ mgn_fig.tight_layout()
86
+ plt.savefig(DATA_DIR / f"{Path(__file__).stem}.pdf")