pyruleanalyzer 2.1.0__tar.gz → 2.1.2__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.
- {pyruleanalyzer-2.1.0/pyruleanalyzer.egg-info → pyruleanalyzer-2.1.2}/PKG-INFO +7 -7
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/README.md +6 -6
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyproject.toml +1 -1
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpn_mutants.py +6 -2
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/gbdt_analyzer.py +5 -1
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/pyruleanalyzer.py +6 -2
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2/pyruleanalyzer.egg-info}/PKG-INFO +7 -7
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile.py +31 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_usage_engine.py +2 -1
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/LICENSE +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/__init__.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_accel.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_arrays.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_binary_format.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_c_export.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_engine.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_extraction.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_labels.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_native_python.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_pycodegen.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_refinement.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_reporting.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_rules.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_serialization.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_tree_traversal.c +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/compositional.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpn_profile.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpn_semantics.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpn_statespace.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpn_tools_exporter.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpntools_oracle.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/dt_analyzer.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/exporters.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/footprint.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/full_pipeline.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/model_checker.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/rf_analyzer.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/rule_classifier.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/verified_pipeline.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer.egg-info/SOURCES.txt +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer.egg-info/dependency_links.txt +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer.egg-info/entry_points.txt +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer.egg-info/requires.txt +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer.egg-info/top_level.txt +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/setup.cfg +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/setup.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cheap_fixes.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_compositional.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_coverage_and_precision.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_export.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile_counterexamples.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile_generic.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile_oracle.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile_reduction.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile_structural.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_safe_eval.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_dt_invariants.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_engine_hardening.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_footprint.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_full_pipeline.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_gbdt_invariants.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_json_serialization.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_labels_and_nan.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_model_checker.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_predict_batch.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_refinement_v2.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_regressions.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_rf_invariants.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_rule_fidelity.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_sklearn_api.py +0 -0
- {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_verified_pipeline_gate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyruleanalyzer
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: Extract, refine and formally verify the rules of scikit-learn decision trees, random forests and gradient boosting, and export them as standalone Python, C and Arduino classifiers
|
|
5
5
|
Author: GrupoCybersegurancaVirtus
|
|
6
6
|
License-Expression: MIT
|
|
@@ -281,15 +281,15 @@ works for all three model types.
|
|
|
281
281
|
| **Usage count** | Runs the refinement samples through the rules and counts how often each fires. | unchanged |
|
|
282
282
|
| **Boundary merge** | Sibling leaves with the same output and complementary conditions (`<= T` and `> T`) are merged into their parent, repeatedly, until no pair is left. The merged rule carries the sum of the two usage counts. | unchanged |
|
|
283
283
|
| **Low-usage removal** | Rules used `remove_below_n_classifications` times or fewer are removed, deepest first; the sibling of a removed rule is promoted. `-1` disables it, `0` removes only never-used rules. | may change |
|
|
284
|
-
| **Inter-tree fusion** (GBDT, `refine_between_trees=
|
|
284
|
+
| **Inter-tree fusion** (GBDT, on by default; `refine_between_trees=False` turns it off) | Stages of one class with an identical region are folded into one rule whose contribution is the sum. The other stages abstain there. | unchanged |
|
|
285
285
|
|
|
286
286
|
```python
|
|
287
287
|
# Random Forest: low-usage removal only (see below for why not inter-tree).
|
|
288
288
|
model.execute_rule_refinement(X=X_val, y=y_val, remove_below_n_classifications=1)
|
|
289
289
|
|
|
290
|
-
# Gradient Boosting: also
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
# Gradient Boosting: identical stages are also folded (refine_between_trees,
|
|
291
|
+
# on by default for GBDT).
|
|
292
|
+
model.execute_rule_refinement(X=X_val, y=y_val, remove_below_n_classifications=1)
|
|
293
293
|
```
|
|
294
294
|
|
|
295
295
|
Merging siblings is a promotion upwards:
|
|
@@ -834,7 +834,7 @@ added condition) refuse NaN as before.
|
|
|
834
834
|
| `.fit(X, y)` | Train the scikit-learn model and extract its rules |
|
|
835
835
|
| `PyRuleAnalyzer.from_sklearn(model, feature_names=None, class_names=None)` | Wrap an already-fitted `DecisionTreeClassifier`, `RandomForestClassifier`, `ExtraTreesClassifier` (wrapped as a Random Forest; `get_params()["estimator"]` remembers the class, so refitting, also after `clone`, trains an `ExtraTreesClassifier` again) or `GradientBoostingClassifier` |
|
|
836
836
|
| `PyRuleAnalyzer.create(train_path, test_path, model, params, refine)` | Same, from CSV files |
|
|
837
|
-
| `.execute_rule_refinement(X, y, remove_below_n_classifications=-1, refine_between_trees=
|
|
837
|
+
| `.execute_rule_refinement(X, y, remove_below_n_classifications=-1, refine_between_trees=None, ...)` | Refine; returns `rules_before`, `rules_after`, `rules_removed`, `reduction_percent` |
|
|
838
838
|
| `.compare_initial_final_results(X, y)` | Metrics and disagreements, initial against refined |
|
|
839
839
|
| `.predict(X, use_refined=True)`, `.predict_proba(X, ...)` | Vectorised prediction |
|
|
840
840
|
| `.summary_report()` | A dict: model type, total rules, classes and every rule |
|
|
@@ -960,7 +960,7 @@ cd docs && make html
|
|
|
960
960
|
|
|
961
961
|
## Status
|
|
962
962
|
|
|
963
|
-
Version 2.1.
|
|
963
|
+
Version 2.1.2; [CHANGELOG.md](https://github.com/GrupoCybersegurancaVirtus/pyruleanalyzer/blob/main/CHANGELOG.md)
|
|
964
964
|
says what changed. The figures on this page are drawn from real runs by
|
|
965
965
|
[`docs/figures/make_figures.py`](https://github.com/GrupoCybersegurancaVirtus/pyruleanalyzer/blob/main/docs/figures/make_figures.py).
|
|
966
966
|
|
|
@@ -239,15 +239,15 @@ works for all three model types.
|
|
|
239
239
|
| **Usage count** | Runs the refinement samples through the rules and counts how often each fires. | unchanged |
|
|
240
240
|
| **Boundary merge** | Sibling leaves with the same output and complementary conditions (`<= T` and `> T`) are merged into their parent, repeatedly, until no pair is left. The merged rule carries the sum of the two usage counts. | unchanged |
|
|
241
241
|
| **Low-usage removal** | Rules used `remove_below_n_classifications` times or fewer are removed, deepest first; the sibling of a removed rule is promoted. `-1` disables it, `0` removes only never-used rules. | may change |
|
|
242
|
-
| **Inter-tree fusion** (GBDT, `refine_between_trees=
|
|
242
|
+
| **Inter-tree fusion** (GBDT, on by default; `refine_between_trees=False` turns it off) | Stages of one class with an identical region are folded into one rule whose contribution is the sum. The other stages abstain there. | unchanged |
|
|
243
243
|
|
|
244
244
|
```python
|
|
245
245
|
# Random Forest: low-usage removal only (see below for why not inter-tree).
|
|
246
246
|
model.execute_rule_refinement(X=X_val, y=y_val, remove_below_n_classifications=1)
|
|
247
247
|
|
|
248
|
-
# Gradient Boosting: also
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
# Gradient Boosting: identical stages are also folded (refine_between_trees,
|
|
249
|
+
# on by default for GBDT).
|
|
250
|
+
model.execute_rule_refinement(X=X_val, y=y_val, remove_below_n_classifications=1)
|
|
251
251
|
```
|
|
252
252
|
|
|
253
253
|
Merging siblings is a promotion upwards:
|
|
@@ -792,7 +792,7 @@ added condition) refuse NaN as before.
|
|
|
792
792
|
| `.fit(X, y)` | Train the scikit-learn model and extract its rules |
|
|
793
793
|
| `PyRuleAnalyzer.from_sklearn(model, feature_names=None, class_names=None)` | Wrap an already-fitted `DecisionTreeClassifier`, `RandomForestClassifier`, `ExtraTreesClassifier` (wrapped as a Random Forest; `get_params()["estimator"]` remembers the class, so refitting, also after `clone`, trains an `ExtraTreesClassifier` again) or `GradientBoostingClassifier` |
|
|
794
794
|
| `PyRuleAnalyzer.create(train_path, test_path, model, params, refine)` | Same, from CSV files |
|
|
795
|
-
| `.execute_rule_refinement(X, y, remove_below_n_classifications=-1, refine_between_trees=
|
|
795
|
+
| `.execute_rule_refinement(X, y, remove_below_n_classifications=-1, refine_between_trees=None, ...)` | Refine; returns `rules_before`, `rules_after`, `rules_removed`, `reduction_percent` |
|
|
796
796
|
| `.compare_initial_final_results(X, y)` | Metrics and disagreements, initial against refined |
|
|
797
797
|
| `.predict(X, use_refined=True)`, `.predict_proba(X, ...)` | Vectorised prediction |
|
|
798
798
|
| `.summary_report()` | A dict: model type, total rules, classes and every rule |
|
|
@@ -918,7 +918,7 @@ cd docs && make html
|
|
|
918
918
|
|
|
919
919
|
## Status
|
|
920
920
|
|
|
921
|
-
Version 2.1.
|
|
921
|
+
Version 2.1.2; [CHANGELOG.md](https://github.com/GrupoCybersegurancaVirtus/pyruleanalyzer/blob/main/CHANGELOG.md)
|
|
922
922
|
says what changed. The figures on this page are drawn from real runs by
|
|
923
923
|
[`docs/figures/make_figures.py`](https://github.com/GrupoCybersegurancaVirtus/pyruleanalyzer/blob/main/docs/figures/make_figures.py).
|
|
924
924
|
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "pyruleanalyzer"
|
|
8
|
-
version = "2.1.
|
|
8
|
+
version = "2.1.2"
|
|
9
9
|
description = "Extract, refine and formally verify the rules of scikit-learn decision trees, random forests and gradient boosting, and export them as standalone Python, C and Arduino classifiers"
|
|
10
10
|
authors = [{ name = "GrupoCybersegurancaVirtus" }]
|
|
11
11
|
readme = "README.md"
|
|
@@ -227,10 +227,14 @@ def _double_output(doc: CPNDocument, info) -> bool:
|
|
|
227
227
|
|
|
228
228
|
# Function to deposit a label outside the domain (A6).
|
|
229
229
|
def _bad_label(doc: CPNDocument, info) -> bool:
|
|
230
|
+
# One past the largest class the correct net can predict; a fixed label
|
|
231
|
+
# such as 7 is a valid class of any model with eight or more classes.
|
|
232
|
+
known = list(info.get("labels") or []) + list(info.get("prediction") or [])
|
|
233
|
+
bad = max(known) + 1 if info.get("labels") else max(known + [0]) + 1_000_000
|
|
230
234
|
page, name = info["producer_page"], info["producer"]
|
|
231
235
|
for arc in doc.arcs(page, name, "TtoP"):
|
|
232
236
|
if doc.place_of(page, arc) == "Prediction":
|
|
233
|
-
doc.set_inscription(arc,
|
|
237
|
+
doc.set_inscription(arc, str(bad))
|
|
234
238
|
return True
|
|
235
239
|
|
|
236
240
|
|
|
@@ -373,7 +377,7 @@ TREE_MUTANTS: List[Mutant] = [
|
|
|
373
377
|
"dormant leaf always enabled, produces nothing", _dead_branch),
|
|
374
378
|
Mutant("double_output", ("A5", "A7"), _ALL,
|
|
375
379
|
"prediction deposited twice", _double_output),
|
|
376
|
-
Mutant("bad_label", ("A6",), _ALL, "prediction replaced by label
|
|
380
|
+
Mutant("bad_label", ("A6",), _ALL, "prediction replaced by a label outside the domain", _bad_label),
|
|
377
381
|
Mutant("overlap", ("B1", "B4"), _ALL,
|
|
378
382
|
"dormant leaf enabled next to the live one", _overlap),
|
|
379
383
|
Mutant("hidden_overlap", ("B4",), _ALL,
|
|
@@ -250,7 +250,7 @@ class GBDTAnalyzer:
|
|
|
250
250
|
X=None,
|
|
251
251
|
y=None,
|
|
252
252
|
remove_below_n_classifications: int = -1,
|
|
253
|
-
refine_between_trees: bool =
|
|
253
|
+
refine_between_trees: bool = True,
|
|
254
254
|
save_final_model: bool = True,
|
|
255
255
|
save_report: bool = True,
|
|
256
256
|
max_error_increase: Optional[int] = None,
|
|
@@ -270,6 +270,10 @@ class GBDTAnalyzer:
|
|
|
270
270
|
y: True labels.
|
|
271
271
|
remove_below_n_classifications: Threshold for low-usage refinement
|
|
272
272
|
(-1 disables).
|
|
273
|
+
refine_between_trees: After the low-usage stage, merge the rules of
|
|
274
|
+
different stages of one class that cover the same region into
|
|
275
|
+
one rule carrying the sum of their values; the other stages
|
|
276
|
+
add 0 there, so predictions are unchanged. Default True.
|
|
273
277
|
save_final_model: Whether to save the final model to 'final_model.pkl'.
|
|
274
278
|
Default is True.
|
|
275
279
|
save_report: Whether to save the analysis report to 'output_classifier_gbdt.txt'.
|
|
@@ -750,7 +750,7 @@ class PyRuleAnalyzer(ClassifierMixin, BaseEstimator):
|
|
|
750
750
|
remove_below_n_classifications: int = -1,
|
|
751
751
|
save_final_model: bool = False,
|
|
752
752
|
save_report: bool = False,
|
|
753
|
-
refine_between_trees: bool =
|
|
753
|
+
refine_between_trees: Optional[bool] = None,
|
|
754
754
|
max_error_increase: Optional[int] = None,
|
|
755
755
|
usage_engine: str = 'auto',
|
|
756
756
|
) -> Dict[str, Any]:
|
|
@@ -773,7 +773,8 @@ class PyRuleAnalyzer(ClassifierMixin, BaseEstimator):
|
|
|
773
773
|
redundancy; the merged rule carries the sum of their values,
|
|
774
774
|
so the score is unchanged). Refused for Random Forest, where
|
|
775
775
|
it would change the soft vote; no effect on a Decision Tree.
|
|
776
|
-
|
|
776
|
+
None (default) turns it on for Gradient Boosting and off
|
|
777
|
+
otherwise.
|
|
777
778
|
max_error_increase: Error-aware refinement: remove a low-usage
|
|
778
779
|
rule only while the refinement-set errors stay within this
|
|
779
780
|
many samples of those before the stage (0 = never more
|
|
@@ -798,6 +799,9 @@ class PyRuleAnalyzer(ClassifierMixin, BaseEstimator):
|
|
|
798
799
|
>>> print(f"Removed {stats['rules_removed']} rules ({stats['reduction_percent']:.1f}%)")
|
|
799
800
|
"""
|
|
800
801
|
rules_before = len(self.classifier.initial_rules)
|
|
802
|
+
if refine_between_trees is None:
|
|
803
|
+
refine_between_trees = (self.classifier.algorithm_type
|
|
804
|
+
== 'Gradient Boosting Decision Trees')
|
|
801
805
|
|
|
802
806
|
# Execute refinement using the appropriate analyzer
|
|
803
807
|
# The analyzer handles save_final_model and save_report internally
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyruleanalyzer
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: Extract, refine and formally verify the rules of scikit-learn decision trees, random forests and gradient boosting, and export them as standalone Python, C and Arduino classifiers
|
|
5
5
|
Author: GrupoCybersegurancaVirtus
|
|
6
6
|
License-Expression: MIT
|
|
@@ -281,15 +281,15 @@ works for all three model types.
|
|
|
281
281
|
| **Usage count** | Runs the refinement samples through the rules and counts how often each fires. | unchanged |
|
|
282
282
|
| **Boundary merge** | Sibling leaves with the same output and complementary conditions (`<= T` and `> T`) are merged into their parent, repeatedly, until no pair is left. The merged rule carries the sum of the two usage counts. | unchanged |
|
|
283
283
|
| **Low-usage removal** | Rules used `remove_below_n_classifications` times or fewer are removed, deepest first; the sibling of a removed rule is promoted. `-1` disables it, `0` removes only never-used rules. | may change |
|
|
284
|
-
| **Inter-tree fusion** (GBDT, `refine_between_trees=
|
|
284
|
+
| **Inter-tree fusion** (GBDT, on by default; `refine_between_trees=False` turns it off) | Stages of one class with an identical region are folded into one rule whose contribution is the sum. The other stages abstain there. | unchanged |
|
|
285
285
|
|
|
286
286
|
```python
|
|
287
287
|
# Random Forest: low-usage removal only (see below for why not inter-tree).
|
|
288
288
|
model.execute_rule_refinement(X=X_val, y=y_val, remove_below_n_classifications=1)
|
|
289
289
|
|
|
290
|
-
# Gradient Boosting: also
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
# Gradient Boosting: identical stages are also folded (refine_between_trees,
|
|
291
|
+
# on by default for GBDT).
|
|
292
|
+
model.execute_rule_refinement(X=X_val, y=y_val, remove_below_n_classifications=1)
|
|
293
293
|
```
|
|
294
294
|
|
|
295
295
|
Merging siblings is a promotion upwards:
|
|
@@ -834,7 +834,7 @@ added condition) refuse NaN as before.
|
|
|
834
834
|
| `.fit(X, y)` | Train the scikit-learn model and extract its rules |
|
|
835
835
|
| `PyRuleAnalyzer.from_sklearn(model, feature_names=None, class_names=None)` | Wrap an already-fitted `DecisionTreeClassifier`, `RandomForestClassifier`, `ExtraTreesClassifier` (wrapped as a Random Forest; `get_params()["estimator"]` remembers the class, so refitting, also after `clone`, trains an `ExtraTreesClassifier` again) or `GradientBoostingClassifier` |
|
|
836
836
|
| `PyRuleAnalyzer.create(train_path, test_path, model, params, refine)` | Same, from CSV files |
|
|
837
|
-
| `.execute_rule_refinement(X, y, remove_below_n_classifications=-1, refine_between_trees=
|
|
837
|
+
| `.execute_rule_refinement(X, y, remove_below_n_classifications=-1, refine_between_trees=None, ...)` | Refine; returns `rules_before`, `rules_after`, `rules_removed`, `reduction_percent` |
|
|
838
838
|
| `.compare_initial_final_results(X, y)` | Metrics and disagreements, initial against refined |
|
|
839
839
|
| `.predict(X, use_refined=True)`, `.predict_proba(X, ...)` | Vectorised prediction |
|
|
840
840
|
| `.summary_report()` | A dict: model type, total rules, classes and every rule |
|
|
@@ -960,7 +960,7 @@ cd docs && make html
|
|
|
960
960
|
|
|
961
961
|
## Status
|
|
962
962
|
|
|
963
|
-
Version 2.1.
|
|
963
|
+
Version 2.1.2; [CHANGELOG.md](https://github.com/GrupoCybersegurancaVirtus/pyruleanalyzer/blob/main/CHANGELOG.md)
|
|
964
964
|
says what changed. The figures on this page are drawn from real runs by
|
|
965
965
|
[`docs/figures/make_figures.py`](https://github.com/GrupoCybersegurancaVirtus/pyruleanalyzer/blob/main/docs/figures/make_figures.py).
|
|
966
966
|
|
|
@@ -87,3 +87,34 @@ def test_the_askctl_script_is_written(tmp_path):
|
|
|
87
87
|
decl, queries = mc.ml_program()
|
|
88
88
|
assert set(queries) <= set(mc.check().oracle)
|
|
89
89
|
assert "EXIST_UNTIL" in text or "EV(" in text
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _StubDoc:
|
|
93
|
+
"""Records the label bad_label writes on the producer's Prediction arc."""
|
|
94
|
+
|
|
95
|
+
def __init__(self):
|
|
96
|
+
self.written = None
|
|
97
|
+
|
|
98
|
+
def arcs(self, page, name, direction):
|
|
99
|
+
return ["a1"]
|
|
100
|
+
|
|
101
|
+
def place_of(self, page, arc):
|
|
102
|
+
return "Prediction"
|
|
103
|
+
|
|
104
|
+
def set_inscription(self, arc, text):
|
|
105
|
+
self.written = text
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.mark.parametrize("labels,prediction,expected", [
|
|
109
|
+
(list(range(8)), [3], "8"), # 7 would be a valid class here
|
|
110
|
+
(list(range(19)), [7], "19"),
|
|
111
|
+
([], [2], str(2 + 1_000_000)), # no constant domain (forests)
|
|
112
|
+
])
|
|
113
|
+
def test_bad_label_writes_a_label_outside_the_domain(labels, prediction, expected):
|
|
114
|
+
from pyruleanalyzer.cpn_mutants import _bad_label
|
|
115
|
+
doc = _StubDoc()
|
|
116
|
+
info = {"labels": labels, "prediction": prediction,
|
|
117
|
+
"producer_page": "Top", "producer": "Decide_3"}
|
|
118
|
+
assert _bad_label(doc, info)
|
|
119
|
+
assert doc.written == expected
|
|
120
|
+
assert labels == [] or int(doc.written) not in labels
|
|
@@ -42,7 +42,8 @@ def test_batch_counts_every_leaf_once_per_sample():
|
|
|
42
42
|
X, y = make_classification(n_samples=300, n_features=5, n_classes=3,
|
|
43
43
|
n_informative=3, random_state=1)
|
|
44
44
|
sk = GradientBoostingClassifier(n_estimators=8, max_depth=3, random_state=0).fit(X, y)
|
|
45
|
-
|
|
45
|
+
# without inter-tree merging, which moves merged regions out of all but one stage
|
|
46
|
+
an = _refined(sk, X, y, 'batch', refine_between_trees=False)
|
|
46
47
|
clf = an.classifier
|
|
47
48
|
by_tree = {}
|
|
48
49
|
for r in clf.final_rules:
|
|
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
|
|
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
|