interox 0.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.
Files changed (84) hide show
  1. interox-0.1.0/.gitignore +18 -0
  2. interox-0.1.0/PKG-INFO +189 -0
  3. interox-0.1.0/README.md +163 -0
  4. interox-0.1.0/interox/__init__.py +37 -0
  5. interox-0.1.0/interox/explanation_status_widget.py +22 -0
  6. interox-0.1.0/interox/helpers.py +131 -0
  7. interox-0.1.0/interox/interox.py +1425 -0
  8. interox-0.1.0/interox/rule_analyzer.py +300 -0
  9. interox-0.1.0/interox/rule_builder_widget.py +324 -0
  10. interox-0.1.0/interox/rule_detail_widget.py +165 -0
  11. interox-0.1.0/interox/ruleset_table_widget.py +109 -0
  12. interox-0.1.0/interox/static/explanation_status.js +238 -0
  13. interox-0.1.0/interox/static/rule_builder.js +1523 -0
  14. interox-0.1.0/interox/static/rule_detail.js +534 -0
  15. interox-0.1.0/interox/static/ruleset_table.js +662 -0
  16. interox-0.1.0/js/build.mjs +30 -0
  17. interox-0.1.0/js/package-lock.json +630 -0
  18. interox-0.1.0/js/package.json +21 -0
  19. interox-0.1.0/js/src/common/data.ts +122 -0
  20. interox-0.1.0/js/src/common/types.ts +66 -0
  21. interox-0.1.0/js/src/common/utils.ts +56 -0
  22. interox-0.1.0/js/src/explanation_status/App.tsx +25 -0
  23. interox-0.1.0/js/src/explanation_status/ExplanationStatus.tsx +159 -0
  24. interox-0.1.0/js/src/explanation_status/Flow.tsx +462 -0
  25. interox-0.1.0/js/src/explanation_status/data.ts +57 -0
  26. interox-0.1.0/js/src/explanation_status/geometry.ts +200 -0
  27. interox-0.1.0/js/src/explanation_status/index.tsx +24 -0
  28. interox-0.1.0/js/src/explanation_status/styles.css +184 -0
  29. interox-0.1.0/js/src/explanation_status/tokens.ts +27 -0
  30. interox-0.1.0/js/src/explanation_status/types.ts +20 -0
  31. interox-0.1.0/js/src/global.d.ts +4 -0
  32. interox-0.1.0/js/src/rule_builder/App.tsx +324 -0
  33. interox-0.1.0/js/src/rule_builder/AttrRail.tsx +309 -0
  34. interox-0.1.0/js/src/rule_builder/AttrRow.tsx +71 -0
  35. interox-0.1.0/js/src/rule_builder/CatBars.tsx +35 -0
  36. interox-0.1.0/js/src/rule_builder/EvalFooter.tsx +236 -0
  37. interox-0.1.0/js/src/rule_builder/ImpactPanel.tsx +83 -0
  38. interox-0.1.0/js/src/rule_builder/OptimizePanel.tsx +195 -0
  39. interox-0.1.0/js/src/rule_builder/RailSplitter.tsx +73 -0
  40. interox-0.1.0/js/src/rule_builder/RangeHist.tsx +170 -0
  41. interox-0.1.0/js/src/rule_builder/StorePopover.tsx +65 -0
  42. interox-0.1.0/js/src/rule_builder/atoms.tsx +83 -0
  43. interox-0.1.0/js/src/rule_builder/data.ts +129 -0
  44. interox-0.1.0/js/src/rule_builder/engine.ts +0 -0
  45. interox-0.1.0/js/src/rule_builder/index.tsx +401 -0
  46. interox-0.1.0/js/src/rule_builder/railGroups.ts +142 -0
  47. interox-0.1.0/js/src/rule_builder/styles.css +1468 -0
  48. interox-0.1.0/js/src/rule_builder/tokens.ts +12 -0
  49. interox-0.1.0/js/src/rule_builder/types.ts +141 -0
  50. interox-0.1.0/js/src/rule_detail/App.tsx +49 -0
  51. interox-0.1.0/js/src/rule_detail/ExperimentPanel.tsx +101 -0
  52. interox-0.1.0/js/src/rule_detail/ExperimentRow.tsx +85 -0
  53. interox-0.1.0/js/src/rule_detail/FeatureSummaryCell.tsx +145 -0
  54. interox-0.1.0/js/src/rule_detail/ImpactCell.tsx +31 -0
  55. interox-0.1.0/js/src/rule_detail/PrecisionRecallCell.tsx +33 -0
  56. interox-0.1.0/js/src/rule_detail/RuleDetailTable.tsx +206 -0
  57. interox-0.1.0/js/src/rule_detail/RuleMetrics.tsx +31 -0
  58. interox-0.1.0/js/src/rule_detail/data.ts +54 -0
  59. interox-0.1.0/js/src/rule_detail/index.tsx +37 -0
  60. interox-0.1.0/js/src/rule_detail/styles.css +480 -0
  61. interox-0.1.0/js/src/rule_detail/tokens.ts +15 -0
  62. interox-0.1.0/js/src/rule_detail/types.ts +67 -0
  63. interox-0.1.0/js/src/ruleset_table/App.tsx +38 -0
  64. interox-0.1.0/js/src/ruleset_table/FeatureSummaryCell.tsx +115 -0
  65. interox-0.1.0/js/src/ruleset_table/MetricCell.tsx +21 -0
  66. interox-0.1.0/js/src/ruleset_table/RuleRow.tsx +294 -0
  67. interox-0.1.0/js/src/ruleset_table/RulesetTable.tsx +218 -0
  68. interox-0.1.0/js/src/ruleset_table/SplitGlyphCell.tsx +86 -0
  69. interox-0.1.0/js/src/ruleset_table/data.ts +40 -0
  70. interox-0.1.0/js/src/ruleset_table/index.tsx +70 -0
  71. interox-0.1.0/js/src/ruleset_table/styles.css +608 -0
  72. interox-0.1.0/js/src/ruleset_table/tokens.ts +29 -0
  73. interox-0.1.0/js/src/ruleset_table/types.ts +56 -0
  74. interox-0.1.0/js/tsconfig.json +15 -0
  75. interox-0.1.0/pyproject.toml +63 -0
  76. interox-0.1.0/tests/conftest.py +64 -0
  77. interox-0.1.0/tests/test_helpers.py +84 -0
  78. interox-0.1.0/tests/test_interox.py +288 -0
  79. interox-0.1.0/tests/test_invariants.py +76 -0
  80. interox-0.1.0/tests/test_rule_analyzer.py +108 -0
  81. interox-0.1.0/tests/test_rule_builder_eval.py +35 -0
  82. interox-0.1.0/tests/test_rule_builder_optimize.py +87 -0
  83. interox-0.1.0/tests/test_rule_edit_transfer.py +180 -0
  84. interox-0.1.0/tests/test_spec_rule_evaluation.py +54 -0
@@ -0,0 +1,18 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ .python-version
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+
10
+ # Test / type-check artefacts
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+
14
+ # Frontend
15
+ js/node_modules/
16
+
17
+ # NOTE: interox/static/*.js is committed on purpose -- these are the built
18
+ # bundles the widgets load at import, so `pip install interox` needs no Node.
interox-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.5
2
+ Name: interox
3
+ Version: 0.1.0
4
+ Summary: Interactive, rule-based outcome explanation for process-mining case logs, as notebook widgets
5
+ Project-URL: Homepage, https://github.com/aaronkurz/interox
6
+ Project-URL: Repository, https://github.com/aaronkurz/interox
7
+ Project-URL: Issues, https://github.com/aaronkurz/interox/issues
8
+ Author: Aaron Kurz
9
+ Keywords: anywidget,case-log,explainability,jupyter,marimo,process-mining,rule-induction
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: Jupyter
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
16
+ Requires-Python: >=3.13
17
+ Requires-Dist: anywidget<0.12,>=0.11
18
+ Requires-Dist: coolname>=5.0
19
+ Requires-Dist: eclear<0.2,>=0.1.0
20
+ Requires-Dist: numpy>=2.0
21
+ Requires-Dist: pandas<4,>=3.0
22
+ Requires-Dist: rulelearn-ripper-interactive<0.3,>=0.2.3
23
+ Requires-Dist: scikit-learn>=1.5
24
+ Requires-Dist: traitlets>=5.14
25
+ Description-Content-Type: text/markdown
26
+
27
+ # interox
28
+
29
+ Interactive outcome explanation for process-mining case logs.
30
+
31
+ `interox` helps you find out *why* some cases of a process end the way they do.
32
+ You hand it a case log and a boolean outcome; it suggests DNF rules (via a fork
33
+ of RIPPER rule induction) that tell the outcome cases apart from the rest, and
34
+ gives you notebook widgets to inspect, store, edit and build rules until the
35
+ outcome is explained.
36
+
37
+ ```bash
38
+ uv add interox # or: pip install interox
39
+ ```
40
+
41
+ The widgets ship pre-built inside the package, so using interox needs no Node
42
+ or JavaScript toolchain.
43
+
44
+ ## Use
45
+
46
+ ```python
47
+ from interox import InterOX
48
+
49
+ iox = InterOX(outcome, case_log) # outcome: bool Series, case_log: DataFrame, same index
50
+
51
+ iox.get_exp_status() # how much of the outcome the stored rules explain
52
+ iox.get_suggested_ruleset() # induced suggestions, as a table widget
53
+ iox.get_stored_ruleset() # the rules you kept
54
+ iox.get_rule_builder() # build a rule from scratch
55
+ ```
56
+
57
+ Rendering needs a notebook that speaks [anywidget](https://anywidget.dev):
58
+ marimo, Jupyter, VS Code. The API reference is generated from the docstrings:
59
+
60
+ ```bash
61
+ uv run pdoc interox
62
+ ```
63
+
64
+ A complete walkthrough on a public event log lives in
65
+ [interox-nodebook-sepsis](https://github.com/aaronkurz/interox-nodebook-sepsis).
66
+
67
+ ## Requirements
68
+
69
+ Python 3.13+ and pandas 3. Case-log enrichment comes from
70
+ [eclear](https://pypi.org/project/eclear/) and rule induction from
71
+ [rulelearn-ripper-interactive](https://pypi.org/project/rulelearn-ripper-interactive/)
72
+ (imported as `rulelearn_ri`); both install as dependencies.
73
+
74
+ ## Development
75
+
76
+ Everything Python goes through uv:
77
+
78
+ ```bash
79
+ uv sync # creates .venv with interox (editable) + the dev group
80
+ ./code-check.sh # pylint + mypy + pytest
81
+ ```
82
+
83
+ The widgets are React 18 + TypeScript in `js/`, bundled with esbuild into
84
+ `interox/static/*.js`:
85
+
86
+ ```bash
87
+ cd js
88
+ npm ci # install React, esbuild, TypeScript tooling
89
+ npm run build # minified production bundles → ../interox/static/
90
+ npm run dev # watch mode, unminified, for working on a widget
91
+ npm run typecheck # esbuild does not typecheck; run this separately
92
+ ```
93
+
94
+ See [DEVELOPMENT.md](DEVELOPMENT.md) for the architecture,
95
+ [DECISIONS.md](DECISIONS.md) for why it is shaped this way, and
96
+ [ubiquitous-language.md](ubiquitous-language.md) for the agreed vocabulary.
97
+
98
+ ### How the compiled widgets get into the package
99
+
100
+ Each widget class points `_esm` at a file next to it, e.g.
101
+ `pathlib.Path(__file__).parent / "static" / "rule_builder.js"`. anywidget reads
102
+ that file when the widget is created, so the bundle has to be *inside* the
103
+ installed package. Two things make sure it is:
104
+
105
+ - `interox/static/*.js` is **committed on purpose** (the same choice as eclear).
106
+ Whatever is in git is what a build from a clean checkout ships, and a notebook
107
+ user never runs npm.
108
+ - `[tool.hatch.build.targets.wheel] packages = ["interox"]` puts the whole
109
+ `interox/` directory, `static/` included, into the wheel. `js/` and `tests/`
110
+ stay out of the wheel; the sdist carries them so it can be rebuilt.
111
+
112
+ So the rule is: **whenever anything under `js/src` changes, run
113
+ `npm run build` and commit the regenerated `interox/static/*.js` together with
114
+ the source change.** A stale bundle is the one packaging mistake that goes
115
+ unnoticed until someone opens a notebook.
116
+
117
+ ## Releasing to PyPI
118
+
119
+ Run from the repo root. Nothing here is automated: an upload is irreversible,
120
+ so each step is yours to run.
121
+
122
+ **1. Rebuild the widgets and re-verify** against exactly what you will ship:
123
+
124
+ ```bash
125
+ (cd js && npm ci && npm run typecheck && npm run build)
126
+ git status --short interox/static # changed bundles? commit them first
127
+ ./code-check.sh
128
+ ```
129
+
130
+ **2. Bump the version** in `pyproject.toml` (`version = "0.1.0"`). A version
131
+ number can never be reused on PyPI, even after deleting the release.
132
+
133
+ **3. Build the distributions:**
134
+
135
+ ```bash
136
+ rm -rf dist/
137
+ uv build
138
+ uvx twine check dist/*
139
+ unzip -l dist/interox-*.whl
140
+ ```
141
+
142
+ The wheel listing must contain the four bundles
143
+ `interox/static/{rule_detail,ruleset_table,explanation_status,rule_builder}.js`
144
+ and must **not** contain `js/`, `node_modules` or `tests/`.
145
+
146
+ **4. Smoke-test the built wheel** from a directory that is *not* this repo,
147
+ otherwise Python imports the local `interox/` folder and you have tested
148
+ nothing:
149
+
150
+ ```bash
151
+ cd /tmp
152
+ uv run --isolated --no-project --with ~/Developer/interox/dist/interox-0.1.0-py3-none-any.whl \
153
+ python -c "import interox, interox.rule_builder_widget as w; print(interox.__file__); print(len(str(w.RuleBuilderWidget._esm)))"
154
+ ```
155
+
156
+ The path must be inside a `site-packages`, and the length must be well above
157
+ zero: that is the bundle anywidget will send to the notebook.
158
+
159
+ **5. Dry run on TestPyPI** (separate site, separate account and token:
160
+ <https://test.pypi.org/manage/account/token/>):
161
+
162
+ ```bash
163
+ uv publish --publish-url https://test.pypi.org/legacy/
164
+ cd /tmp
165
+ uv run --isolated --no-project \
166
+ --index https://test.pypi.org/simple/ --index-strategy unsafe-best-match \
167
+ --with interox python -c "import interox; print(interox.__file__)"
168
+ ```
169
+
170
+ The extra index flags are needed because the dependencies come from real PyPI.
171
+
172
+ **6. Publish for real.** Create a token at <https://pypi.org/manage/account/token/>.
173
+ For a project that does not exist yet it must be **account-scoped**; after the
174
+ first upload, replace it with a project-scoped one.
175
+
176
+ ```bash
177
+ read -rs UV_PUBLISH_TOKEN && export UV_PUBLISH_TOKEN && uv publish
178
+ ```
179
+
180
+ **7. Tag the release:**
181
+
182
+ ```bash
183
+ git tag v0.1.0
184
+ git push origin main --tags
185
+ ```
186
+
187
+ Later, PyPI trusted publishing (a GitHub Actions workflow that uploads on a
188
+ pushed tag, no stored token) removes the token step:
189
+ <https://pypi.org/manage/project/interox/settings/publishing/>.
@@ -0,0 +1,163 @@
1
+ # interox
2
+
3
+ Interactive outcome explanation for process-mining case logs.
4
+
5
+ `interox` helps you find out *why* some cases of a process end the way they do.
6
+ You hand it a case log and a boolean outcome; it suggests DNF rules (via a fork
7
+ of RIPPER rule induction) that tell the outcome cases apart from the rest, and
8
+ gives you notebook widgets to inspect, store, edit and build rules until the
9
+ outcome is explained.
10
+
11
+ ```bash
12
+ uv add interox # or: pip install interox
13
+ ```
14
+
15
+ The widgets ship pre-built inside the package, so using interox needs no Node
16
+ or JavaScript toolchain.
17
+
18
+ ## Use
19
+
20
+ ```python
21
+ from interox import InterOX
22
+
23
+ iox = InterOX(outcome, case_log) # outcome: bool Series, case_log: DataFrame, same index
24
+
25
+ iox.get_exp_status() # how much of the outcome the stored rules explain
26
+ iox.get_suggested_ruleset() # induced suggestions, as a table widget
27
+ iox.get_stored_ruleset() # the rules you kept
28
+ iox.get_rule_builder() # build a rule from scratch
29
+ ```
30
+
31
+ Rendering needs a notebook that speaks [anywidget](https://anywidget.dev):
32
+ marimo, Jupyter, VS Code. The API reference is generated from the docstrings:
33
+
34
+ ```bash
35
+ uv run pdoc interox
36
+ ```
37
+
38
+ A complete walkthrough on a public event log lives in
39
+ [interox-nodebook-sepsis](https://github.com/aaronkurz/interox-nodebook-sepsis).
40
+
41
+ ## Requirements
42
+
43
+ Python 3.13+ and pandas 3. Case-log enrichment comes from
44
+ [eclear](https://pypi.org/project/eclear/) and rule induction from
45
+ [rulelearn-ripper-interactive](https://pypi.org/project/rulelearn-ripper-interactive/)
46
+ (imported as `rulelearn_ri`); both install as dependencies.
47
+
48
+ ## Development
49
+
50
+ Everything Python goes through uv:
51
+
52
+ ```bash
53
+ uv sync # creates .venv with interox (editable) + the dev group
54
+ ./code-check.sh # pylint + mypy + pytest
55
+ ```
56
+
57
+ The widgets are React 18 + TypeScript in `js/`, bundled with esbuild into
58
+ `interox/static/*.js`:
59
+
60
+ ```bash
61
+ cd js
62
+ npm ci # install React, esbuild, TypeScript tooling
63
+ npm run build # minified production bundles → ../interox/static/
64
+ npm run dev # watch mode, unminified, for working on a widget
65
+ npm run typecheck # esbuild does not typecheck; run this separately
66
+ ```
67
+
68
+ See [DEVELOPMENT.md](DEVELOPMENT.md) for the architecture,
69
+ [DECISIONS.md](DECISIONS.md) for why it is shaped this way, and
70
+ [ubiquitous-language.md](ubiquitous-language.md) for the agreed vocabulary.
71
+
72
+ ### How the compiled widgets get into the package
73
+
74
+ Each widget class points `_esm` at a file next to it, e.g.
75
+ `pathlib.Path(__file__).parent / "static" / "rule_builder.js"`. anywidget reads
76
+ that file when the widget is created, so the bundle has to be *inside* the
77
+ installed package. Two things make sure it is:
78
+
79
+ - `interox/static/*.js` is **committed on purpose** (the same choice as eclear).
80
+ Whatever is in git is what a build from a clean checkout ships, and a notebook
81
+ user never runs npm.
82
+ - `[tool.hatch.build.targets.wheel] packages = ["interox"]` puts the whole
83
+ `interox/` directory, `static/` included, into the wheel. `js/` and `tests/`
84
+ stay out of the wheel; the sdist carries them so it can be rebuilt.
85
+
86
+ So the rule is: **whenever anything under `js/src` changes, run
87
+ `npm run build` and commit the regenerated `interox/static/*.js` together with
88
+ the source change.** A stale bundle is the one packaging mistake that goes
89
+ unnoticed until someone opens a notebook.
90
+
91
+ ## Releasing to PyPI
92
+
93
+ Run from the repo root. Nothing here is automated: an upload is irreversible,
94
+ so each step is yours to run.
95
+
96
+ **1. Rebuild the widgets and re-verify** against exactly what you will ship:
97
+
98
+ ```bash
99
+ (cd js && npm ci && npm run typecheck && npm run build)
100
+ git status --short interox/static # changed bundles? commit them first
101
+ ./code-check.sh
102
+ ```
103
+
104
+ **2. Bump the version** in `pyproject.toml` (`version = "0.1.0"`). A version
105
+ number can never be reused on PyPI, even after deleting the release.
106
+
107
+ **3. Build the distributions:**
108
+
109
+ ```bash
110
+ rm -rf dist/
111
+ uv build
112
+ uvx twine check dist/*
113
+ unzip -l dist/interox-*.whl
114
+ ```
115
+
116
+ The wheel listing must contain the four bundles
117
+ `interox/static/{rule_detail,ruleset_table,explanation_status,rule_builder}.js`
118
+ and must **not** contain `js/`, `node_modules` or `tests/`.
119
+
120
+ **4. Smoke-test the built wheel** from a directory that is *not* this repo,
121
+ otherwise Python imports the local `interox/` folder and you have tested
122
+ nothing:
123
+
124
+ ```bash
125
+ cd /tmp
126
+ uv run --isolated --no-project --with ~/Developer/interox/dist/interox-0.1.0-py3-none-any.whl \
127
+ python -c "import interox, interox.rule_builder_widget as w; print(interox.__file__); print(len(str(w.RuleBuilderWidget._esm)))"
128
+ ```
129
+
130
+ The path must be inside a `site-packages`, and the length must be well above
131
+ zero: that is the bundle anywidget will send to the notebook.
132
+
133
+ **5. Dry run on TestPyPI** (separate site, separate account and token:
134
+ <https://test.pypi.org/manage/account/token/>):
135
+
136
+ ```bash
137
+ uv publish --publish-url https://test.pypi.org/legacy/
138
+ cd /tmp
139
+ uv run --isolated --no-project \
140
+ --index https://test.pypi.org/simple/ --index-strategy unsafe-best-match \
141
+ --with interox python -c "import interox; print(interox.__file__)"
142
+ ```
143
+
144
+ The extra index flags are needed because the dependencies come from real PyPI.
145
+
146
+ **6. Publish for real.** Create a token at <https://pypi.org/manage/account/token/>.
147
+ For a project that does not exist yet it must be **account-scoped**; after the
148
+ first upload, replace it with a project-scoped one.
149
+
150
+ ```bash
151
+ read -rs UV_PUBLISH_TOKEN && export UV_PUBLISH_TOKEN && uv publish
152
+ ```
153
+
154
+ **7. Tag the release:**
155
+
156
+ ```bash
157
+ git tag v0.1.0
158
+ git push origin main --tags
159
+ ```
160
+
161
+ Later, PyPI trusted publishing (a GitHub Actions workflow that uploads on a
162
+ pushed tag, no stored token) removes the token step:
163
+ <https://pypi.org/manage/project/interox/settings/publishing/>.
@@ -0,0 +1,37 @@
1
+ """
2
+ # InterOX: Interactive Outcome Explanation for Case Logs
3
+
4
+ `interox` exposes the `InterOX` class, which supports iterative, rule-based
5
+ outcome explanation over a case log. The workflow is typically:
6
+
7
+ 1. Inspect explanation progress.
8
+ 2. Compare stored and suggested rules.
9
+ 3. Store rules that improve coverage.
10
+ 4. Optionally hide or unhide features and re-induce suggestions.
11
+
12
+ Start here
13
+ ----------
14
+ - `InterOX.get_exp_status` — where the explanation stands.
15
+ - `InterOX.get_stored_ruleset` / `InterOX.get_suggested_ruleset` — the two
16
+ rulesets, with metrics, as table widgets.
17
+ - `InterOX.get_rule_builder` — build a rule from scratch instead of taking a
18
+ suggested one.
19
+ - `InterOX.send_rule_to_builder` — take a suggested rule into that builder to
20
+ adjust it before storing (the ✎ button on a suggested row).
21
+ - `InterOX.store_rule_by_id` / `InterOX.remove_rule_by_id` — change the stored
22
+ ruleset; both re-induce suggestions.
23
+
24
+ A rule is addressed by its **stable id**, which survives re-induction and
25
+ reordering: prefer the `*_by_id` methods over the positional ones of the same
26
+ name. `InterOX.get_stored_rule_options` maps rule names to those ids.
27
+
28
+ The rest of the class -- rule detail widgets, annotations, hidden features, and
29
+ the `get_*_case_log` / `get_data_*` accessors for analysis outside the provided
30
+ widgets -- is documented on the methods themselves, below.
31
+ """
32
+ import logging
33
+ from .interox import InterOX
34
+
35
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
36
+
37
+ __all__ = ["InterOX"]
@@ -0,0 +1,22 @@
1
+ # pylint: disable=missing-module-docstring
2
+ import pathlib
3
+ import anywidget
4
+ import traitlets
5
+
6
+
7
+ class ExplanationStatusWidget(anywidget.AnyWidget): # pylint: disable=too-few-public-methods, abstract-method
8
+ """A widget for displaying the status of an outcome explanation
9
+ in terms of remaining (FN), explained (TP) and wrongly claimed (FP) cases,
10
+ the negatives the explanation leaves alone (TN), and the number of rules the
11
+ explanation is currently made of.
12
+
13
+ The four case counts partition the case log, which lets the frontend draw
14
+ the flow from the case log through the rule set to the prediction. All
15
+ traits are Python-owned; the frontend never writes back."""
16
+ _esm = pathlib.Path(__file__).parent / "static" / "explanation_status.js"
17
+
18
+ cases_tbe = traitlets.Int(default_value=0).tag(sync=True)
19
+ exp_tp = traitlets.Int(default_value=0).tag(sync=True)
20
+ exp_fp = traitlets.Int(default_value=0).tag(sync=True)
21
+ exp_tn = traitlets.Int(default_value=0).tag(sync=True)
22
+ stored_rules = traitlets.Int(default_value=0).tag(sync=True)
@@ -0,0 +1,131 @@
1
+ """Supporting functions for InterOX."""
2
+ import operator
3
+ from collections import defaultdict
4
+ from typing import Any, Callable, Dict, List, Tuple
5
+ import logging
6
+ from rulelearn_ri.trxf.core import Conjunction, DnfRuleSet, Feature
7
+ from rulelearn_ri.trxf.core.predicate import Relation
8
+ from pandas import Series
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ #: The comparison each trxf relation stands for. Rule evaluation compares values
13
+ #: directly rather than rendering them into an expression to `eval`, so a value
14
+ #: is only ever compared, never parsed.
15
+ _RELATION_OPS = {
16
+ Relation.EQ: operator.eq,
17
+ Relation.NEQ: operator.ne,
18
+ Relation.LT: operator.lt,
19
+ Relation.LE: operator.le,
20
+ Relation.GT: operator.gt,
21
+ Relation.GE: operator.ge,
22
+ }
23
+
24
+
25
+ def is_usable_feature_name(name) -> bool:
26
+ """Whether a column name can carry a rule condition.
27
+
28
+ `trxf` parses a feature name as an arithmetic expression, so a name is only
29
+ usable if it happens to be a valid one. Most punctuation survives -- `.`,
30
+ `:` and `::` are absorbed into a single token -- but `+ - * /` and
31
+ parentheses are read as operators, and a name that then fails to parse
32
+ (`*.concept:name::append`, `Payment_(late)::count`) cannot become a
33
+ `Feature` at all.
34
+
35
+ The failure modes are varied enough (`ValueError` from the expression
36
+ validator, `TokenError` from the tokenizer, `IndexError` from an unbalanced
37
+ parenthesis) that the only useful question is whether construction works.
38
+ """
39
+ try:
40
+ Feature(str(name))
41
+ return True
42
+ except Exception: # pylint: disable=broad-except
43
+ return False
44
+
45
+
46
+ def rule_to_lambda(rule: Conjunction,
47
+ skip_feature=None,
48
+ only_feature=None) -> Callable[[Series], bool]:
49
+ """Convert a rule (conjunction of predicates) to a lambda function
50
+ that evaluates the rule on a case.
51
+
52
+ A predicate becomes a column lookup and a comparison against the value the
53
+ predicate carries, whatever that value is. Nothing is rendered into source
54
+ text, so a category that looks like a number (`'nan'`, `'1e3'`), a value
55
+ holding a quote, and a column name holding an apostrophe all compare as
56
+ themselves. An empty conjunction -- every predicate filtered out, or none to
57
+ begin with -- matches every case.
58
+
59
+ Raises:
60
+ KeyError: When evaluating a case that has no column for a predicate's
61
+ feature.
62
+ """
63
+ if skip_feature is not None and only_feature is not None:
64
+ raise ValueError("skip_feature and only_feature are mutually exclusive")
65
+ tests: List[Tuple[str, Callable[[Any, Any], bool], Any]] = []
66
+ for predicate in rule.predicates:
67
+ feature = str(predicate.feature)
68
+ if skip_feature is not None and feature == skip_feature:
69
+ continue
70
+ if only_feature is not None and feature != only_feature:
71
+ continue
72
+ logger.debug("Processing predicate: %s %s %s",
73
+ feature, predicate.relation.value, predicate.value)
74
+ try:
75
+ op = _RELATION_OPS[predicate.relation]
76
+ except KeyError as exc:
77
+ raise ValueError(f'Unknown relation {predicate.relation!r}') from exc
78
+ tests.append((feature, op, predicate.value))
79
+
80
+ return lambda case: all(op(case[feature], value) for feature, op, value in tests)
81
+
82
+
83
+ def conjunction_to_conditions(conjunction: Conjunction) -> List[Dict[str, Any]]:
84
+ """Convert a rule into the condition list the rule builder canvas speaks.
85
+
86
+ The builder holds at most one condition per attribute, so the predicates are
87
+ grouped by feature first: a GE/LE pair on the same feature is the two sides
88
+ of one `range` condition, and a lone bound keeps its own operator. Predicates
89
+ beyond the first of each relation on a feature are dropped -- the canvas has
90
+ nowhere to put them. So are whole relations the canvas cannot express: NEQ,
91
+ LT and GT have no branch here, because a categorical condition is `==` only
92
+ (RIPPER induces no negation) and a numeric one is an inclusive bound. Note
93
+ this is narrower than `_RELATION_OPS` above, which still evaluates all six --
94
+ a rule can carry a relation the builder would not let you author.
95
+
96
+ This is the inverse of `RuleBuilderWidget._conditions_to_predicates`, and it
97
+ is what lets an already-induced rule be loaded back into the builder (see
98
+ `InterOX.send_rule_to_builder`).
99
+ """
100
+ by_attr: Dict[str, List[Any]] = defaultdict(list)
101
+ for predicate in conjunction.predicates:
102
+ # `str`, as in `rule_to_lambda`: a predicate's feature is a `Feature` when
103
+ # it comes from the inducer but a bare column name when it was built by
104
+ # hand, and both render as the column name.
105
+ by_attr[str(predicate.feature)].append(predicate)
106
+
107
+ conditions: List[Dict[str, Any]] = []
108
+ for attr_name, preds in by_attr.items():
109
+ ge = next((p for p in preds if p.relation == Relation.GE), None)
110
+ le = next((p for p in preds if p.relation == Relation.LE), None)
111
+ eq = next((p for p in preds if p.relation == Relation.EQ), None)
112
+ if ge and le:
113
+ conditions.append({'attr': attr_name, 'op': 'range',
114
+ 'lo': float(ge.value), 'hi': float(le.value)})
115
+ elif ge:
116
+ conditions.append({'attr': attr_name, 'op': '>=', 'value': float(ge.value)})
117
+ elif le:
118
+ conditions.append({'attr': attr_name, 'op': '<=', 'value': float(le.value)})
119
+ elif eq:
120
+ conditions.append({'attr': attr_name, 'op': '==', 'value': str(eq.value)})
121
+ return conditions
122
+
123
+
124
+ def ruleset_to_lambda(ruleset: DnfRuleSet, skip_feature=None) -> Callable[[Series], bool]:
125
+ """Convert a ruleset (disjunction of conjunctions) to a lambda function
126
+ that evaluates the ruleset on a case."""
127
+ rule_lambdas = []
128
+ for conji in ruleset.conjunctions:
129
+ rule_lambdas.append(rule_to_lambda(conji, skip_feature))
130
+
131
+ return lambda case: any(rule_lambda(case) for rule_lambda in rule_lambdas)