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.
Files changed (71) hide show
  1. {pyruleanalyzer-2.1.0/pyruleanalyzer.egg-info → pyruleanalyzer-2.1.2}/PKG-INFO +7 -7
  2. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/README.md +6 -6
  3. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyproject.toml +1 -1
  4. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpn_mutants.py +6 -2
  5. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/gbdt_analyzer.py +5 -1
  6. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/pyruleanalyzer.py +6 -2
  7. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2/pyruleanalyzer.egg-info}/PKG-INFO +7 -7
  8. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile.py +31 -0
  9. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_usage_engine.py +2 -1
  10. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/LICENSE +0 -0
  11. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/__init__.py +0 -0
  12. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_accel.py +0 -0
  13. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_arrays.py +0 -0
  14. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_binary_format.py +0 -0
  15. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_c_export.py +0 -0
  16. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_engine.py +0 -0
  17. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_extraction.py +0 -0
  18. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_labels.py +0 -0
  19. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_native_python.py +0 -0
  20. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_pycodegen.py +0 -0
  21. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_refinement.py +0 -0
  22. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_reporting.py +0 -0
  23. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_rules.py +0 -0
  24. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_serialization.py +0 -0
  25. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/_tree_traversal.c +0 -0
  26. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/compositional.py +0 -0
  27. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpn_profile.py +0 -0
  28. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpn_semantics.py +0 -0
  29. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpn_statespace.py +0 -0
  30. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpn_tools_exporter.py +0 -0
  31. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/cpntools_oracle.py +0 -0
  32. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/dt_analyzer.py +0 -0
  33. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/exporters.py +0 -0
  34. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/footprint.py +0 -0
  35. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/full_pipeline.py +0 -0
  36. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/model_checker.py +0 -0
  37. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/rf_analyzer.py +0 -0
  38. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/rule_classifier.py +0 -0
  39. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer/verified_pipeline.py +0 -0
  40. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer.egg-info/SOURCES.txt +0 -0
  41. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer.egg-info/dependency_links.txt +0 -0
  42. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer.egg-info/entry_points.txt +0 -0
  43. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer.egg-info/requires.txt +0 -0
  44. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/pyruleanalyzer.egg-info/top_level.txt +0 -0
  45. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/setup.cfg +0 -0
  46. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/setup.py +0 -0
  47. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cheap_fixes.py +0 -0
  48. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_compositional.py +0 -0
  49. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_coverage_and_precision.py +0 -0
  50. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_export.py +0 -0
  51. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile_counterexamples.py +0 -0
  52. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile_generic.py +0 -0
  53. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile_oracle.py +0 -0
  54. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile_reduction.py +0 -0
  55. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_profile_structural.py +0 -0
  56. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_cpn_safe_eval.py +0 -0
  57. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_dt_invariants.py +0 -0
  58. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_engine_hardening.py +0 -0
  59. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_footprint.py +0 -0
  60. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_full_pipeline.py +0 -0
  61. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_gbdt_invariants.py +0 -0
  62. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_json_serialization.py +0 -0
  63. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_labels_and_nan.py +0 -0
  64. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_model_checker.py +0 -0
  65. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_predict_batch.py +0 -0
  66. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_refinement_v2.py +0 -0
  67. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_regressions.py +0 -0
  68. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_rf_invariants.py +0 -0
  69. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_rule_fidelity.py +0 -0
  70. {pyruleanalyzer-2.1.0 → pyruleanalyzer-2.1.2}/tests/test_sklearn_api.py +0 -0
  71. {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.0
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=True`) | Stages of one class with an identical region are folded into one rule whose contribution is the sum. The other stages abstain there. | unchanged |
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 fold identical stages.
291
- model.execute_rule_refinement(X=X_val, y=y_val, remove_below_n_classifications=1,
292
- refine_between_trees=True)
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=False, ...)` | Refine; returns `rules_before`, `rules_after`, `rules_removed`, `reduction_percent` |
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.0; [CHANGELOG.md](https://github.com/GrupoCybersegurancaVirtus/pyruleanalyzer/blob/main/CHANGELOG.md)
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=True`) | Stages of one class with an identical region are folded into one rule whose contribution is the sum. The other stages abstain there. | unchanged |
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 fold identical stages.
249
- model.execute_rule_refinement(X=X_val, y=y_val, remove_below_n_classifications=1,
250
- refine_between_trees=True)
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=False, ...)` | Refine; returns `rules_before`, `rules_after`, `rules_removed`, `reduction_percent` |
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.0; [CHANGELOG.md](https://github.com/GrupoCybersegurancaVirtus/pyruleanalyzer/blob/main/CHANGELOG.md)
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.0"
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, "7")
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 7", _bad_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 = False,
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 = False,
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
- Default is False.
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.0
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=True`) | Stages of one class with an identical region are folded into one rule whose contribution is the sum. The other stages abstain there. | unchanged |
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 fold identical stages.
291
- model.execute_rule_refinement(X=X_val, y=y_val, remove_below_n_classifications=1,
292
- refine_between_trees=True)
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=False, ...)` | Refine; returns `rules_before`, `rules_after`, `rules_removed`, `reduction_percent` |
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.0; [CHANGELOG.md](https://github.com/GrupoCybersegurancaVirtus/pyruleanalyzer/blob/main/CHANGELOG.md)
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
- an = _refined(sk, X, y, 'batch')
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