action-rules 2.0.1__tar.gz → 2.1.0__tar.gz
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.
- {action_rules-2.0.1 → action_rules-2.1.0}/PKG-INFO +3 -2
- {action_rules-2.0.1 → action_rules-2.1.0}/pyproject.toml +2 -2
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/__init__.py +1 -1
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/action_rules.py +47 -9
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/inference/analytic.py +86 -19
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/inference/base.py +28 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/inference/bayesian.py +15 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/inference/bootstrap.py +78 -33
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/rules/rules.py +9 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/inference/test_analytic.py +36 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/inference/test_base.py +13 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/inference/test_bayesian.py +11 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/inference/test_bootstrap.py +13 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/rules/test_rules.py +21 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/test_confidence_intervals.py +20 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/LICENSE +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/README.md +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/candidates/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/candidates/candidate_generator.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/cli.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/evaluation/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/evaluation/cv.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/evaluation/metrics.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/inference/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/input/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/input/input.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/output/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/output/output.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/rules/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/visualization/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/visualization/plots.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/candidates/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/candidates/test_candidate_generator.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/evaluation/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/evaluation/test_action_rules_method.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/evaluation/test_cv.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/evaluation/test_metrics.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/inference/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/input/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/input/test_input.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/output/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/output/test_output.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/rules/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/simulation/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/simulation/coverage_simulation.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/simulation/test_coverage_simulation.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/test_action_rules.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/visualization/__init__.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/visualization/test_plot_data.py +0 -0
- {action_rules-2.0.1 → action_rules-2.1.0}/tests/visualization/test_plots.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: action-rules
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: The package for action rules mining using Action-Apriori (Apriori Modified for Action Rules Mining)..
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
21
22
|
Provides-Extra: dev
|
|
22
23
|
Provides-Extra: doc
|
|
23
24
|
Provides-Extra: inference
|
|
@@ -42,7 +43,7 @@ Requires-Dist: pandas (>=2.2.2,<3.0.0)
|
|
|
42
43
|
Requires-Dist: pip (>=24.0,<25.0) ; extra == "dev"
|
|
43
44
|
Requires-Dist: pre-commit (>=3.7.1,<4.0.0) ; extra == "dev"
|
|
44
45
|
Requires-Dist: pytest (>=8.2.1,<9.0.0) ; extra == "test"
|
|
45
|
-
Requires-Dist: pytest-cov (>=5.0.0,<
|
|
46
|
+
Requires-Dist: pytest-cov (>=5.0.0,<8.0.0) ; extra == "test"
|
|
46
47
|
Requires-Dist: scipy (>=1.10,<2.0) ; extra == "test" or extra == "inference" or extra == "viz"
|
|
47
48
|
Requires-Dist: toml (>=0.10.2,<0.11.0) ; extra == "dev"
|
|
48
49
|
Requires-Dist: tox (>=4.15.0,<5.0.0) ; extra == "dev"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[tool]
|
|
2
2
|
[tool.poetry]
|
|
3
3
|
name = "action-rules"
|
|
4
|
-
version = "2.0
|
|
4
|
+
version = "2.1.0"
|
|
5
5
|
homepage = "https://github.com/lukassykora/action-rules"
|
|
6
6
|
description = "The package for action rules mining using Action-Apriori (Apriori Modified for Action Rules Mining).."
|
|
7
7
|
authors = ["Lukas Sykora <lukas.sykora@vse.cz>"]
|
|
@@ -31,7 +31,7 @@ flake8 = { version = "^7.0.0", optional = true}
|
|
|
31
31
|
flake8-docstrings = { version = "^1.7.0", optional = true }
|
|
32
32
|
mypy = {version = "^1.10.0", optional = true}
|
|
33
33
|
pytest = { version = "^8.2.1", optional = true}
|
|
34
|
-
pytest-cov = { version = "
|
|
34
|
+
pytest-cov = { version = ">=5.0.0,<8.0.0", optional = true}
|
|
35
35
|
scipy = { version = "^1.10", optional = true}
|
|
36
36
|
matplotlib = { version = "^3.7", optional = true}
|
|
37
37
|
tox = { version = "^4.15.0", optional = true}
|
|
@@ -908,6 +908,11 @@ class ActionRules:
|
|
|
908
908
|
random_state: Optional[int] = None,
|
|
909
909
|
analytic_type: str = "wald",
|
|
910
910
|
bootstrap_type: str = "percentile",
|
|
911
|
+
*,
|
|
912
|
+
prior: Optional[str] = None,
|
|
913
|
+
simultaneous_m: Optional[int] = None,
|
|
914
|
+
resample_mode: str = "rows",
|
|
915
|
+
discovery_fraction: float = 0.5,
|
|
911
916
|
):
|
|
912
917
|
r"""Compute confidence intervals for all fitted action rules.
|
|
913
918
|
|
|
@@ -999,7 +1004,15 @@ class ActionRules:
|
|
|
999
1004
|
raise ValueError("n_bootstrap must be >= 1.")
|
|
1000
1005
|
if n_mc < 1:
|
|
1001
1006
|
raise ValueError("n_mc must be >= 1.")
|
|
1002
|
-
valid_analytic_types = {
|
|
1007
|
+
valid_analytic_types = {
|
|
1008
|
+
"wald",
|
|
1009
|
+
"wald_rd",
|
|
1010
|
+
"wald_random",
|
|
1011
|
+
"wilson",
|
|
1012
|
+
"newcombe_wilson",
|
|
1013
|
+
"clopper_pearson",
|
|
1014
|
+
"auto",
|
|
1015
|
+
}
|
|
1003
1016
|
if analytic_type not in valid_analytic_types:
|
|
1004
1017
|
raise ValueError(f"Unknown analytic_type '{analytic_type}'. Choose from {valid_analytic_types}.")
|
|
1005
1018
|
valid_bootstrap_types = {"percentile", "bca"}
|
|
@@ -1012,27 +1025,52 @@ class ActionRules:
|
|
|
1012
1025
|
from .inference.base import categorize_rule, extract_rule_masks
|
|
1013
1026
|
|
|
1014
1027
|
masks = extract_rule_masks(self.output)
|
|
1028
|
+
eval_data = data
|
|
1015
1029
|
|
|
1016
1030
|
engine: Any
|
|
1017
|
-
if method
|
|
1031
|
+
if method in ("bootstrap", "bca"):
|
|
1018
1032
|
from .inference.bootstrap import BootstrapEngine
|
|
1019
1033
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1034
|
+
eff_btype = "bca" if method == "bca" else bootstrap_type
|
|
1035
|
+
engine = BootstrapEngine(
|
|
1036
|
+
n_bootstrap,
|
|
1037
|
+
random_state,
|
|
1038
|
+
bootstrap_type=eff_btype,
|
|
1039
|
+
resample_mode=resample_mode,
|
|
1040
|
+
)
|
|
1041
|
+
elif method in ("analytic", "wald", "wald_rd", "wald_random", "wilson", "newcombe_wilson", "clopper_pearson", "auto"):
|
|
1022
1042
|
from .inference.analytic import AnalyticEngine
|
|
1023
1043
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1044
|
+
eff_atype = analytic_type if method in ("analytic", "wald") else method
|
|
1045
|
+
engine = AnalyticEngine(analytic_type=eff_atype, simultaneous_m=simultaneous_m)
|
|
1046
|
+
elif method in ("bayesian", "jeffreys"):
|
|
1026
1047
|
from .inference.bayesian import BayesianEngine
|
|
1027
1048
|
|
|
1028
|
-
|
|
1049
|
+
eff_prior = "jeffreys" if method == "jeffreys" else prior
|
|
1050
|
+
engine = BayesianEngine(n_mc, random_state=random_state, prior=eff_prior)
|
|
1051
|
+
elif method == "honest_split":
|
|
1052
|
+
if not (0.0 < discovery_fraction < 1.0):
|
|
1053
|
+
raise ValueError("discovery_fraction must be strictly between 0 and 1.")
|
|
1054
|
+
import numpy as _np
|
|
1055
|
+
|
|
1056
|
+
from .inference.analytic import AnalyticEngine
|
|
1057
|
+
|
|
1058
|
+
rng_split = _np.random.default_rng(random_state)
|
|
1059
|
+
n_rows = len(data)
|
|
1060
|
+
perm = rng_split.permutation(n_rows)
|
|
1061
|
+
n_disc = int(round(discovery_fraction * n_rows))
|
|
1062
|
+
est_idx = perm[n_disc:]
|
|
1063
|
+
eval_data = data.iloc[est_idx].reset_index(drop=True) if len(est_idx) > 0 else data
|
|
1064
|
+
eff_atype = "newcombe_wilson" if analytic_type == "wald" else analytic_type
|
|
1065
|
+
engine = AnalyticEngine(analytic_type=eff_atype, simultaneous_m=simultaneous_m)
|
|
1029
1066
|
else:
|
|
1030
1067
|
raise ValueError(
|
|
1031
|
-
f"Unknown method '{method}'. Supported methods: 'bootstrap', 'analytic', 'wald',
|
|
1068
|
+
f"Unknown method '{method}'. Supported methods: 'bootstrap', 'bca', 'analytic', 'wald', "
|
|
1069
|
+
f"'wald_rd', 'newcombe_wilson', 'clopper_pearson', 'auto', 'bayesian', 'jeffreys', 'honest_split'."
|
|
1032
1070
|
)
|
|
1033
1071
|
|
|
1034
1072
|
results = engine.compute(
|
|
1035
|
-
data=
|
|
1073
|
+
data=eval_data,
|
|
1036
1074
|
rules=masks,
|
|
1037
1075
|
confidence_level=confidence_level,
|
|
1038
1076
|
intrinsic_utility_table=self._original_intrinsic_utility_table or None,
|
|
@@ -88,7 +88,7 @@ class AnalyticEngine(InferenceEngine):
|
|
|
88
88
|
# Initialisation
|
|
89
89
|
# ------------------------------------------------------------------
|
|
90
90
|
|
|
91
|
-
def __init__(self, analytic_type: str = "wald") -> None:
|
|
91
|
+
def __init__(self, analytic_type: str = "wald", simultaneous_m: Optional[int] = None) -> None:
|
|
92
92
|
r"""Initialise the engine with the chosen interval type.
|
|
93
93
|
|
|
94
94
|
Parameters
|
|
@@ -97,31 +97,50 @@ class AnalyticEngine(InferenceEngine):
|
|
|
97
97
|
Which closed-form interval to use. One of:
|
|
98
98
|
|
|
99
99
|
``'wald'``
|
|
100
|
-
Standard Wald (normal approximation) interval
|
|
101
|
-
backward-compatible choice.
|
|
100
|
+
Standard Wald (normal approximation) interval under fixed-
|
|
101
|
+
antecedent conditioning. Default and backward-compatible choice.
|
|
102
|
+
``'wald_rd'`` or ``'wald_random'``
|
|
103
|
+
Random-design (RD) delta-method Wald interval that propagates
|
|
104
|
+
uncertainty in the antecedent prevalence :math:`\hat{q}_u = n_u / N`
|
|
105
|
+
alongside the conditional proportions :math:`\hat{p}_u, \hat{p}_d`.
|
|
102
106
|
``'newcombe_wilson'`` (preferred) or ``'wilson'`` (alias)
|
|
103
107
|
Newcombe-Wilson interval for the unscaled rule contrast
|
|
104
|
-
:math
|
|
108
|
+
:math:`\delta = p_d + p_u - 1` (Newcombe, 1998), built by
|
|
105
109
|
combining two single-proportion Wilson score intervals.
|
|
106
110
|
More accurate for small samples or proportions near 0 or 1.
|
|
107
111
|
``'wilson'`` is retained as a backward-compatible alias and
|
|
108
112
|
resolves to the same Newcombe-Wilson construction.
|
|
113
|
+
``'clopper_pearson'``
|
|
114
|
+
Exact Clopper-Pearson single-proportion intervals combined via
|
|
115
|
+
Newcombe's square-and-add root.
|
|
109
116
|
``'auto'``
|
|
110
117
|
Selects the Newcombe-Wilson branch when either side has
|
|
111
118
|
``n < 40`` or ``p < 0.05`` / ``p > 0.95``; falls back to
|
|
112
119
|
Wald otherwise (Agresti & Coull, 1998).
|
|
120
|
+
simultaneous_m : int, optional
|
|
121
|
+
When provided (``>= 1``), applies a Bonferroni simultaneous
|
|
122
|
+
multiplicity correction over *simultaneous_m* candidate rules by
|
|
123
|
+
setting :math:`\alpha_{\mathrm{eff}} = (1 - \text{confidence\_level}) / m`.
|
|
113
124
|
|
|
114
125
|
Raises
|
|
115
126
|
------
|
|
116
127
|
ValueError
|
|
117
|
-
If *analytic_type* is not one of the recognised values
|
|
128
|
+
If *analytic_type* is not one of the recognised values or
|
|
129
|
+
*simultaneous_m* is less than 1.
|
|
118
130
|
"""
|
|
119
|
-
valid = {"wald", "wilson", "newcombe_wilson", "auto"}
|
|
131
|
+
valid = {"wald", "wald_rd", "wald_random", "wilson", "newcombe_wilson", "clopper_pearson", "auto"}
|
|
120
132
|
if analytic_type not in valid:
|
|
121
133
|
raise ValueError(f"Unknown analytic_type '{analytic_type}'. Choose from {valid}.")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
if simultaneous_m is not None and simultaneous_m < 1:
|
|
135
|
+
raise ValueError(f"simultaneous_m must be >= 1, got {simultaneous_m}.")
|
|
136
|
+
# Canonicalise aliases
|
|
137
|
+
if analytic_type == "wilson":
|
|
138
|
+
self.analytic_type = "newcombe_wilson"
|
|
139
|
+
elif analytic_type == "wald_random":
|
|
140
|
+
self.analytic_type = "wald_rd"
|
|
141
|
+
else:
|
|
142
|
+
self.analytic_type = analytic_type
|
|
143
|
+
self.simultaneous_m = simultaneous_m
|
|
125
144
|
|
|
126
145
|
# ------------------------------------------------------------------
|
|
127
146
|
# Private helpers
|
|
@@ -206,6 +225,38 @@ class AnalyticEngine(InferenceEngine):
|
|
|
206
225
|
upper = delta_hat + sqrt((u_d - p_d) ** 2 + (u_u - p_u) ** 2)
|
|
207
226
|
return delta_hat, lower, upper
|
|
208
227
|
|
|
228
|
+
@staticmethod
|
|
229
|
+
def _clopper_pearson_bounds(x: float, n: float, alpha: float) -> Tuple[float, float, float]:
|
|
230
|
+
"""Single-proportion exact Clopper-Pearson interval (Clopper & Pearson, 1934).
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
x : float
|
|
235
|
+
Number of successes.
|
|
236
|
+
n : float
|
|
237
|
+
Number of trials.
|
|
238
|
+
alpha : float
|
|
239
|
+
Two-sided significance level ``1 - confidence_level``.
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
p_hat : float
|
|
244
|
+
Maximum-likelihood proportion ``x / n``.
|
|
245
|
+
lower : float
|
|
246
|
+
Exact Clopper-Pearson lower bound.
|
|
247
|
+
upper : float
|
|
248
|
+
Exact Clopper-Pearson upper bound.
|
|
249
|
+
"""
|
|
250
|
+
if n <= 0:
|
|
251
|
+
nan = float('nan')
|
|
252
|
+
return nan, nan, nan
|
|
253
|
+
from scipy.stats import beta
|
|
254
|
+
|
|
255
|
+
p_hat = x / n
|
|
256
|
+
lower = 0.0 if x <= 0 else float(beta.ppf(alpha / 2.0, x, n - x + 1.0))
|
|
257
|
+
upper = 1.0 if x >= n else float(beta.ppf(1.0 - alpha / 2.0, x + 1.0, n - x))
|
|
258
|
+
return p_hat, lower, upper
|
|
259
|
+
|
|
209
260
|
# ------------------------------------------------------------------
|
|
210
261
|
# Public API
|
|
211
262
|
# ------------------------------------------------------------------
|
|
@@ -265,6 +316,8 @@ class AnalyticEngine(InferenceEngine):
|
|
|
265
316
|
compute_gain = (intrinsic_utility_table is not None) or (transition_utility_table is not None)
|
|
266
317
|
|
|
267
318
|
alpha = 1.0 - confidence_level
|
|
319
|
+
if self.simultaneous_m is not None and self.simultaneous_m > 1:
|
|
320
|
+
alpha = alpha / float(self.simultaneous_m)
|
|
268
321
|
z = norm.ppf(1.0 - alpha / 2.0)
|
|
269
322
|
|
|
270
323
|
results: List[ConfidenceIntervalResult] = []
|
|
@@ -313,7 +366,17 @@ class AnalyticEngine(InferenceEngine):
|
|
|
313
366
|
and (n_u_ante < 40 or n_d_ante < 40 or p_u < 0.05 or p_u > 0.95 or p_d < 0.05 or p_d > 0.95)
|
|
314
367
|
)
|
|
315
368
|
|
|
316
|
-
|
|
369
|
+
var_p_u = p_u * (1.0 - p_u) / n_u_ante
|
|
370
|
+
var_p_d = p_d * (1.0 - p_d) / n_d_ante
|
|
371
|
+
var_delta = var_p_u + var_p_d
|
|
372
|
+
|
|
373
|
+
if self.analytic_type == "clopper_pearson":
|
|
374
|
+
_, l_u, u_u = self._clopper_pearson_bounds(n_u_match, n_u_ante, alpha)
|
|
375
|
+
_, l_d, u_d = self._clopper_pearson_bounds(n_d_match, n_d_ante, alpha)
|
|
376
|
+
_, delta_lower, delta_upper = self._newcombe_delta_interval(
|
|
377
|
+
p_u, l_u, u_u, p_d, l_d, u_d
|
|
378
|
+
)
|
|
379
|
+
elif use_wilson:
|
|
317
380
|
# Newcombe (1998) combination of single-proportion Wilson intervals.
|
|
318
381
|
_, l_u, u_u = self._wilson_bounds(n_u_match, n_u_ante, z)
|
|
319
382
|
_, l_d, u_d = self._wilson_bounds(n_d_match, n_d_ante, z)
|
|
@@ -322,21 +385,25 @@ class AnalyticEngine(InferenceEngine):
|
|
|
322
385
|
)
|
|
323
386
|
else:
|
|
324
387
|
# Wald delta-method interval for delta = p_d + p_u - 1.
|
|
325
|
-
var_p_u = p_u * (1.0 - p_u) / n_u_ante
|
|
326
|
-
var_p_d = p_d * (1.0 - p_d) / n_d_ante
|
|
327
|
-
var_delta = var_p_u + var_p_d
|
|
328
388
|
delta_se = sqrt(var_delta)
|
|
329
389
|
delta_lower = delta_hat - z * delta_se
|
|
330
390
|
delta_upper = delta_hat + z * delta_se
|
|
331
391
|
|
|
332
392
|
# Transform delta -> population-scaled uplift.
|
|
333
393
|
uplift_point = delta_hat * scale
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
394
|
+
if self.analytic_type == "wald_rd":
|
|
395
|
+
# Random-design delta-method variance: accounts for random antecedent prevalence q_u = n_u / N.
|
|
396
|
+
var_uplift = (scale ** 2) * var_delta + (delta_hat ** 2) * (scale * (1.0 - scale) / n_total)
|
|
397
|
+
se_uplift = sqrt(var_uplift)
|
|
398
|
+
uplift_lower = uplift_point - z * se_uplift
|
|
399
|
+
uplift_upper = uplift_point + z * se_uplift
|
|
400
|
+
else:
|
|
401
|
+
uplift_lower = delta_lower * scale
|
|
402
|
+
uplift_upper = delta_upper * scale
|
|
403
|
+
# Average half-width divided by z gives an SE-equivalent for an
|
|
404
|
+
# asymmetric interval; reduces to the exact Wald SE in the Wald
|
|
405
|
+
# branch.
|
|
406
|
+
se_uplift = (uplift_upper - uplift_lower) / (2.0 * z) if z > 0 else 0.0
|
|
340
407
|
|
|
341
408
|
support = n_u_ante
|
|
342
409
|
confidence = p_u
|
|
@@ -7,6 +7,7 @@ functions consumed by all CI engines (analytic, bootstrap, Bayesian).
|
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from enum import Enum
|
|
10
|
+
import math
|
|
10
11
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
|
11
12
|
|
|
12
13
|
import numpy as np
|
|
@@ -32,11 +33,14 @@ class RuleCategory(Enum):
|
|
|
32
33
|
The entire CI is below the threshold — rule should be discarded.
|
|
33
34
|
UNCERTAIN : str
|
|
34
35
|
The CI straddles the threshold — insufficient evidence to decide.
|
|
36
|
+
UNSUPPORTED : str
|
|
37
|
+
Zero antecedent support or undefined CI bounds (NaN).
|
|
35
38
|
"""
|
|
36
39
|
|
|
37
40
|
ACCEPT = 'accept'
|
|
38
41
|
REJECT = 'reject'
|
|
39
42
|
UNCERTAIN = 'uncertain'
|
|
43
|
+
UNSUPPORTED = 'unsupported'
|
|
40
44
|
|
|
41
45
|
|
|
42
46
|
# ---------------------------------------------------------------------------
|
|
@@ -147,6 +151,27 @@ class ConfidenceIntervalResult:
|
|
|
147
151
|
samples_uplift: Optional[np.ndarray] = field(default=None, repr=False)
|
|
148
152
|
samples_gain: Optional[np.ndarray] = field(default=None, repr=False)
|
|
149
153
|
|
|
154
|
+
@property
|
|
155
|
+
def decision_status(self) -> str:
|
|
156
|
+
"""Return a four-way decision status string.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
str
|
|
161
|
+
One of ``'accept'``, ``'reject'``, ``'uncertain'``, or
|
|
162
|
+
``'unsupported'`` (when ``support == 0`` or CI bounds are ``NaN``).
|
|
163
|
+
"""
|
|
164
|
+
if (
|
|
165
|
+
self.support == 0
|
|
166
|
+
or math.isnan(self.uplift_ci_lower)
|
|
167
|
+
or math.isnan(self.uplift_ci_upper)
|
|
168
|
+
or self.category == RuleCategory.UNSUPPORTED
|
|
169
|
+
):
|
|
170
|
+
return RuleCategory.UNSUPPORTED.value
|
|
171
|
+
if self.category is not None:
|
|
172
|
+
return self.category.value
|
|
173
|
+
return RuleCategory.UNCERTAIN.value
|
|
174
|
+
|
|
150
175
|
def to_dict(self, include_samples: bool = False) -> dict:
|
|
151
176
|
"""Return a plain dict representation of this result.
|
|
152
177
|
|
|
@@ -601,10 +626,13 @@ def categorize_rule(ci_lower: float, ci_upper: float, threshold: float) -> RuleC
|
|
|
601
626
|
Returns
|
|
602
627
|
-------
|
|
603
628
|
RuleCategory
|
|
629
|
+
- :attr:`RuleCategory.UNSUPPORTED` when either CI bound is ``NaN``
|
|
604
630
|
- :attr:`RuleCategory.ACCEPT` when ``ci_lower >= threshold``
|
|
605
631
|
- :attr:`RuleCategory.REJECT` when ``ci_upper < threshold``
|
|
606
632
|
- :attr:`RuleCategory.UNCERTAIN` otherwise
|
|
607
633
|
"""
|
|
634
|
+
if math.isnan(ci_lower) or math.isnan(ci_upper):
|
|
635
|
+
return RuleCategory.UNSUPPORTED
|
|
608
636
|
if ci_lower >= threshold:
|
|
609
637
|
return RuleCategory.ACCEPT
|
|
610
638
|
if ci_upper < threshold:
|
|
@@ -74,11 +74,26 @@ class BayesianEngine(InferenceEngine):
|
|
|
74
74
|
prior_alpha: float = 1.0,
|
|
75
75
|
prior_beta: float = 1.0,
|
|
76
76
|
random_state: Optional[int] = None,
|
|
77
|
+
*,
|
|
78
|
+
prior: Optional[str] = None,
|
|
77
79
|
):
|
|
80
|
+
if prior is not None:
|
|
81
|
+
prior_norm = prior.lower()
|
|
82
|
+
if prior_norm in ("uniform", "laplace"):
|
|
83
|
+
prior_alpha, prior_beta = 1.0, 1.0
|
|
84
|
+
elif prior_norm == "jeffreys":
|
|
85
|
+
prior_alpha, prior_beta = 0.5, 0.5
|
|
86
|
+
elif prior_norm == "haldane":
|
|
87
|
+
prior_alpha, prior_beta = 0.01, 0.01
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Unknown prior '{prior}'. Choose from {{'uniform', 'laplace', 'jeffreys', 'haldane'}}."
|
|
91
|
+
)
|
|
78
92
|
self.n_mc = n_mc
|
|
79
93
|
self.prior_alpha = prior_alpha
|
|
80
94
|
self.prior_beta = prior_beta
|
|
81
95
|
self.random_state = random_state
|
|
96
|
+
self.prior = prior
|
|
82
97
|
|
|
83
98
|
# ------------------------------------------------------------------
|
|
84
99
|
# Public API
|
|
@@ -134,13 +134,18 @@ class BootstrapEngine(InferenceEngine):
|
|
|
134
134
|
random_state: Optional[int] = None,
|
|
135
135
|
*,
|
|
136
136
|
bootstrap_type: str = "percentile",
|
|
137
|
+
resample_mode: str = "rows",
|
|
137
138
|
):
|
|
138
139
|
valid = {"percentile", "bca"}
|
|
139
140
|
if bootstrap_type not in valid:
|
|
140
141
|
raise ValueError(f"Unknown bootstrap_type '{bootstrap_type}'. Choose from {valid}.")
|
|
142
|
+
valid_modes = {"rows", "multinomial"}
|
|
143
|
+
if resample_mode not in valid_modes:
|
|
144
|
+
raise ValueError(f"Unknown resample_mode '{resample_mode}'. Choose from {valid_modes}.")
|
|
141
145
|
self.n_bootstrap = n_bootstrap
|
|
142
146
|
self.random_state = random_state
|
|
143
147
|
self.bootstrap_type = bootstrap_type
|
|
148
|
+
self.resample_mode = resample_mode
|
|
144
149
|
|
|
145
150
|
# ------------------------------------------------------------------
|
|
146
151
|
# Static helpers
|
|
@@ -433,41 +438,81 @@ class BootstrapEngine(InferenceEngine):
|
|
|
433
438
|
gain_samples[:] = np.nan
|
|
434
439
|
|
|
435
440
|
n_undefined = 0
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
441
|
+
if self.resample_mode == "multinomial" and n > 0:
|
|
442
|
+
c_u_match = int(u_match.sum())
|
|
443
|
+
c_u_nonmatch = max(int(u_ante.sum()) - c_u_match, 0)
|
|
444
|
+
c_d_match = int(d_match.sum())
|
|
445
|
+
c_d_nonmatch = max(int(d_ante.sum()) - c_d_match, 0)
|
|
446
|
+
c_other = max(n - (c_u_match + c_u_nonmatch + c_d_match + c_d_nonmatch), 0)
|
|
447
|
+
probs = np.array(
|
|
448
|
+
[c_u_match, c_u_nonmatch, c_d_match, c_d_nonmatch, c_other],
|
|
449
|
+
dtype=float,
|
|
450
|
+
)
|
|
451
|
+
total_p = probs.sum()
|
|
452
|
+
if total_p > 0:
|
|
453
|
+
probs /= total_p
|
|
454
|
+
draws = rng.multinomial(n, probs, size=self.n_bootstrap)
|
|
455
|
+
nu_arr = draws[:, 0] + draws[:, 1]
|
|
456
|
+
num_arr = draws[:, 0]
|
|
457
|
+
nd_arr = draws[:, 2] + draws[:, 3]
|
|
458
|
+
ndm_arr = draws[:, 2]
|
|
459
|
+
valid_mask = (nu_arr > 0) & (nd_arr > 0)
|
|
460
|
+
n_undefined = int((~valid_mask).sum())
|
|
461
|
+
if np.any(valid_mask):
|
|
462
|
+
conf_u_arr = num_arr[valid_mask] / nu_arr[valid_mask]
|
|
463
|
+
conf_d_arr = ndm_arr[valid_mask] / nd_arr[valid_mask]
|
|
464
|
+
d_arr = conf_d_arr + conf_u_arr - 1.0
|
|
465
|
+
uplift_samples[valid_mask] = d_arr * (nu_arr[valid_mask] / float(n))
|
|
466
|
+
if gain_samples is not None and column_values is not None:
|
|
467
|
+
for idx_b in np.where(valid_mask)[0]:
|
|
468
|
+
gain_samples[idx_b] = compute_realistic_gain(
|
|
469
|
+
rule,
|
|
470
|
+
float(num_arr[idx_b] / nu_arr[idx_b]),
|
|
471
|
+
float(ndm_arr[idx_b] / nd_arr[idx_b]),
|
|
472
|
+
intrinsic_utility_table,
|
|
473
|
+
transition_utility_table,
|
|
474
|
+
column_values,
|
|
475
|
+
)
|
|
476
|
+
else:
|
|
477
|
+
n_undefined = self.n_bootstrap
|
|
478
|
+
else:
|
|
479
|
+
for b in range(self.n_bootstrap):
|
|
480
|
+
# Draw a bootstrap resample (with replacement) using integer
|
|
481
|
+
# index sampling, which is compatible with default_rng.
|
|
482
|
+
indices = rng.integers(0, n, size=n)
|
|
483
|
+
|
|
484
|
+
# Fancy-index the pre-computed boolean masks instead of
|
|
485
|
+
# slicing the DataFrame — avoids Python-level iteration and
|
|
486
|
+
# repeated string comparisons.
|
|
487
|
+
n_u_ante = int(u_ante[indices].sum())
|
|
488
|
+
n_u_match = int(u_match[indices].sum())
|
|
489
|
+
n_d_ante = int(d_ante[indices].sum())
|
|
490
|
+
n_d_match = int(d_match[indices].sum())
|
|
491
|
+
n_total = n
|
|
492
|
+
|
|
493
|
+
# Skip resamples where antecedent support is zero — uplift is
|
|
494
|
+
# undefined for these draws. Track the fraction so the caller
|
|
495
|
+
# can diagnose unstable percentile intervals on low-support rules.
|
|
496
|
+
if n_u_ante == 0 or n_d_ante == 0:
|
|
497
|
+
n_undefined += 1
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
uplift_samples[b] = compute_uplift_from_counts(
|
|
501
|
+
n_u_ante, n_u_match, n_d_ante, n_d_match, n_total
|
|
469
502
|
)
|
|
470
503
|
|
|
504
|
+
if gain_samples is not None and column_values is not None:
|
|
505
|
+
conf_u = n_u_match / n_u_ante
|
|
506
|
+
conf_d = n_d_match / n_d_ante
|
|
507
|
+
gain_samples[b] = compute_realistic_gain(
|
|
508
|
+
rule,
|
|
509
|
+
conf_u,
|
|
510
|
+
conf_d,
|
|
511
|
+
intrinsic_utility_table,
|
|
512
|
+
transition_utility_table,
|
|
513
|
+
column_values,
|
|
514
|
+
)
|
|
515
|
+
|
|
471
516
|
# Compute full-dataset counts from pre-computed masks.
|
|
472
517
|
# These are needed by both percentile and BCa branches.
|
|
473
518
|
n_u_ante_full = int(u_ante.sum())
|
|
@@ -183,6 +183,13 @@ class Rules:
|
|
|
183
183
|
for attribute_prefix, rules in self.classification_rules.items():
|
|
184
184
|
for desired_rule in rules['desired']:
|
|
185
185
|
for undesired_rule in rules['undesired']:
|
|
186
|
+
# Skip degenerate self-transitions when a real flexible prefix is present
|
|
187
|
+
if (
|
|
188
|
+
len(attribute_prefix) > 0
|
|
189
|
+
and undesired_rule.get('itemset') is not None
|
|
190
|
+
and undesired_rule.get('itemset') == desired_rule.get('itemset')
|
|
191
|
+
):
|
|
192
|
+
continue
|
|
186
193
|
# Uplift
|
|
187
194
|
uplift = self.calculate_uplift(
|
|
188
195
|
undesired_rule['support'],
|
|
@@ -299,6 +306,8 @@ class Rules:
|
|
|
299
306
|
Uplift measures the increase in the probability of achieving the desired state when applying the action rule
|
|
300
307
|
compared to not applying it. It is used to assess the effectiveness of the rule.
|
|
301
308
|
"""
|
|
309
|
+
if undesired_confidence == 0 or self.count_transactions == 0:
|
|
310
|
+
return 0.0
|
|
302
311
|
return (
|
|
303
312
|
(desired_confidence - (1 - undesired_confidence)) * (undesired_support / undesired_confidence)
|
|
304
313
|
) / self.count_transactions
|
|
@@ -821,3 +821,39 @@ class TestAutoMode:
|
|
|
821
821
|
|
|
822
822
|
assert auto.uplift_point == pytest.approx(wald.uplift_point, rel=1e-12)
|
|
823
823
|
assert auto.uplift_se == pytest.approx(wald.uplift_se, rel=1e-12)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
class TestRandomDesignAndClopperPearson:
|
|
827
|
+
"""Tests for v2.1.0 random-design Wald ('wald_rd'), 'clopper_pearson', and 'simultaneous_m'."""
|
|
828
|
+
|
|
829
|
+
def test_wald_rd_wider_than_fixed_wald(self):
|
|
830
|
+
"""Random-design Wald propagates prevalence uncertainty and is at least as wide as fixed Wald."""
|
|
831
|
+
data = _make_data(n=200)
|
|
832
|
+
extra = pd.DataFrame({'age': ['0'] * 10, 'class': ['0'] * 10, 'target': ['1'] * 10})
|
|
833
|
+
mixed = pd.concat([data, extra], ignore_index=True)
|
|
834
|
+
|
|
835
|
+
w_fixed = AnalyticEngine(analytic_type="wald").compute(mixed, [_make_rule()])[0]
|
|
836
|
+
w_rd = AnalyticEngine(analytic_type="wald_rd").compute(mixed, [_make_rule()])[0]
|
|
837
|
+
assert w_rd.uplift_point == pytest.approx(w_fixed.uplift_point, rel=1e-12)
|
|
838
|
+
assert w_rd.uplift_se >= w_fixed.uplift_se
|
|
839
|
+
|
|
840
|
+
def test_clopper_pearson_conservative(self):
|
|
841
|
+
"""Clopper-Pearson combined via Newcombe produces valid conservative bounds."""
|
|
842
|
+
data = _make_data(n=100)
|
|
843
|
+
extra = pd.DataFrame({'age': ['0'] * 5, 'class': ['0'] * 5, 'target': ['1'] * 5})
|
|
844
|
+
mixed = pd.concat([data, extra], ignore_index=True)
|
|
845
|
+
|
|
846
|
+
cp = AnalyticEngine(analytic_type="clopper_pearson").compute(mixed, [_make_rule()])[0]
|
|
847
|
+
nw = AnalyticEngine(analytic_type="newcombe_wilson").compute(mixed, [_make_rule()])[0]
|
|
848
|
+
assert cp.uplift_point == pytest.approx(nw.uplift_point, rel=1e-12)
|
|
849
|
+
assert (cp.uplift_ci_upper - cp.uplift_ci_lower) >= (nw.uplift_ci_upper - nw.uplift_ci_lower)
|
|
850
|
+
|
|
851
|
+
def test_simultaneous_m_widens_interval(self):
|
|
852
|
+
"""Bonferroni multiplicity simultaneous_m > 1 widens the confidence interval."""
|
|
853
|
+
data = _make_data(n=200)
|
|
854
|
+
extra = pd.DataFrame({'age': ['0'] * 10, 'class': ['0'] * 10, 'target': ['1'] * 10})
|
|
855
|
+
mixed = pd.concat([data, extra], ignore_index=True)
|
|
856
|
+
|
|
857
|
+
r1 = AnalyticEngine(analytic_type="newcombe_wilson", simultaneous_m=1).compute(mixed, [_make_rule()])[0]
|
|
858
|
+
r10 = AnalyticEngine(analytic_type="newcombe_wilson", simultaneous_m=10).compute(mixed, [_make_rule()])[0]
|
|
859
|
+
assert (r10.uplift_ci_upper - r10.uplift_ci_lower) > (r1.uplift_ci_upper - r1.uplift_ci_lower)
|
|
@@ -171,3 +171,16 @@ class TestCategorizeRule:
|
|
|
171
171
|
"""Verify a CI lower bound equal to the threshold yields ACCEPT."""
|
|
172
172
|
# Boundary case: ci_lower exactly at threshold counts as ACCEPT.
|
|
173
173
|
assert categorize_rule(0.0, 0.2, threshold=0.0) is RuleCategory.ACCEPT
|
|
174
|
+
|
|
175
|
+
def test_unsupported_when_bounds_nan(self):
|
|
176
|
+
"""Verify NaN CI bounds yield RuleCategory.UNSUPPORTED."""
|
|
177
|
+
assert categorize_rule(float('nan'), float('nan'), threshold=0.0) is RuleCategory.UNSUPPORTED
|
|
178
|
+
assert categorize_rule(float('nan'), 0.2, threshold=0.0) is RuleCategory.UNSUPPORTED
|
|
179
|
+
|
|
180
|
+
def test_decision_status_property(self):
|
|
181
|
+
"""Verify ConfidenceIntervalResult.decision_status returns 4-way status."""
|
|
182
|
+
r_acc = _make_result(0, category=RuleCategory.ACCEPT)
|
|
183
|
+
assert r_acc.decision_status == 'accept'
|
|
184
|
+
r_unsup = _make_result(1, category=None)
|
|
185
|
+
r_unsup.support = 0
|
|
186
|
+
assert r_unsup.decision_status == 'unsupported'
|
|
@@ -451,3 +451,14 @@ class TestMultipleRules:
|
|
|
451
451
|
result = BayesianEngine(n_mc=5000, random_state=0).compute(data, [_make_rule()])[0]
|
|
452
452
|
# The synthetic data gives perfect uplift; the posterior CI should be fully > 0.
|
|
453
453
|
assert result.category == RuleCategory.ACCEPT
|
|
454
|
+
|
|
455
|
+
def test_named_priors_jeffreys_and_uniform(self):
|
|
456
|
+
"""Named priors 'jeffreys', 'uniform', and 'haldane' set the expected Beta hyperparameters."""
|
|
457
|
+
eng_j = BayesianEngine(n_mc=100, random_state=0, prior="jeffreys")
|
|
458
|
+
assert eng_j.prior_alpha == 0.5 and eng_j.prior_beta == 0.5
|
|
459
|
+
eng_u = BayesianEngine(n_mc=100, random_state=0, prior="uniform")
|
|
460
|
+
assert eng_u.prior_alpha == 1.0 and eng_u.prior_beta == 1.0
|
|
461
|
+
eng_h = BayesianEngine(n_mc=100, random_state=0, prior="haldane")
|
|
462
|
+
assert eng_h.prior_alpha == 0.01 and eng_h.prior_beta == 0.01
|
|
463
|
+
with pytest.raises(ValueError, match="Unknown prior"):
|
|
464
|
+
BayesianEngine(prior="invalid_prior")
|
|
@@ -643,3 +643,16 @@ class TestDegenerateResampleReporting:
|
|
|
643
643
|
# miss them entirely.
|
|
644
644
|
assert result.undefined_bootstrap_fraction is not None
|
|
645
645
|
assert result.undefined_bootstrap_fraction > 0.01
|
|
646
|
+
|
|
647
|
+
def test_multinomial_resample_mode_matches_rows_distribution(self):
|
|
648
|
+
"""Vectorised 5-cell multinomial resampling agrees closely with row resampling."""
|
|
649
|
+
data = _make_data(n=400)
|
|
650
|
+
r_rows = BootstrapEngine(n_bootstrap=500, random_state=42, resample_mode="rows").compute(
|
|
651
|
+
data, [_make_rule()]
|
|
652
|
+
)[0]
|
|
653
|
+
r_mult = BootstrapEngine(n_bootstrap=500, random_state=42, resample_mode="multinomial").compute(
|
|
654
|
+
data, [_make_rule()]
|
|
655
|
+
)[0]
|
|
656
|
+
assert r_mult.uplift_point == pytest.approx(r_rows.uplift_point, abs=0.03)
|
|
657
|
+
assert r_mult.uplift_ci_lower == pytest.approx(r_rows.uplift_ci_lower, abs=0.05)
|
|
658
|
+
assert r_mult.uplift_ci_upper == pytest.approx(r_rows.uplift_ci_upper, abs=0.05)
|
|
@@ -167,3 +167,24 @@ def test_compute_rule_utility(rules):
|
|
|
167
167
|
assert abs(result[0] - expected[0]) < tol, f"max_rule_gain: expected {expected[0]}, got {result[0]}"
|
|
168
168
|
assert abs(result[1] - expected[1]) < tol, f"realistic_rule_gain: expected {expected[1]}, got {result[1]}"
|
|
169
169
|
assert abs(result[2] - expected[2]) < tol, f"realistic_rule_gain_dataset: expected {expected[2]}, got {result[2]}"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_calculate_uplift_zero_confidence_and_zero_transactions(rules):
|
|
173
|
+
"""calculate_uplift returns 0.0 instead of raising ZeroDivisionError when confidence or transactions are 0."""
|
|
174
|
+
assert rules.calculate_uplift(0, 0.0, 0.8) == 0.0
|
|
175
|
+
rules.count_transactions = 0
|
|
176
|
+
assert rules.calculate_uplift(10, 0.5, 0.8) == 0.0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_generate_action_rules_skips_identical_itemsets_with_prefix(rules):
|
|
180
|
+
"""generate_action_rules skips self-paired identical itemsets when attribute_prefix is non-empty."""
|
|
181
|
+
rules.add_classification_rules(
|
|
182
|
+
('flex_attr',),
|
|
183
|
+
(0,),
|
|
184
|
+
[{'item': 1, 'support': 10, 'confidence': 0.4}, {'item': 2, 'support': 12, 'confidence': 0.7}],
|
|
185
|
+
[{'item': 1, 'support': 15, 'confidence': 0.6}],
|
|
186
|
+
)
|
|
187
|
+
rules.generate_action_rules()
|
|
188
|
+
assert len(rules.action_rules) == 1
|
|
189
|
+
assert rules.action_rules[0]['undesired']['itemset'] == (0, 2)
|
|
190
|
+
assert rules.action_rules[0]['desired']['itemset'] == (0, 1)
|
|
@@ -1150,3 +1150,23 @@ class TestCLI:
|
|
|
1150
1150
|
finally:
|
|
1151
1151
|
os.unlink(csv_path)
|
|
1152
1152
|
os.unlink(json_path)
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
class TestV210Extensions:
|
|
1156
|
+
"""Integration tests for v2.1.0 confidence_intervals methods and options."""
|
|
1157
|
+
|
|
1158
|
+
def test_wald_rd_and_clopper_pearson_and_jeffreys_and_honest_split(self, fitted_ar):
|
|
1159
|
+
"""Verify new method aliases ('wald_rd', 'clopper_pearson', 'jeffreys', 'honest_split') run cleanly."""
|
|
1160
|
+
ar, data = fitted_ar
|
|
1161
|
+
res_rd = ar.confidence_intervals(data, method="wald_rd")
|
|
1162
|
+
assert len(res_rd) == len(ar.output.action_rules)
|
|
1163
|
+
res_cp = ar.confidence_intervals(data, method="clopper_pearson", simultaneous_m=len(res_rd))
|
|
1164
|
+
assert len(res_cp) == len(ar.output.action_rules)
|
|
1165
|
+
res_j = ar.confidence_intervals(data, method="jeffreys", n_mc=200, random_state=42)
|
|
1166
|
+
assert len(res_j) == len(ar.output.action_rules)
|
|
1167
|
+
res_hs = ar.confidence_intervals(data, method="honest_split", discovery_fraction=0.5, random_state=42)
|
|
1168
|
+
assert len(res_hs) == len(ar.output.action_rules)
|
|
1169
|
+
res_mult = ar.confidence_intervals(
|
|
1170
|
+
data, method="bootstrap", n_bootstrap=50, random_state=42, resample_mode="multinomial"
|
|
1171
|
+
)
|
|
1172
|
+
assert len(res_mult) == len(ar.output.action_rules)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{action_rules-2.0.1 → action_rules-2.1.0}/src/action_rules/candidates/candidate_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|