segmtree 0.3.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.
- segmtree-0.3.0/.gitignore +15 -0
- segmtree-0.3.0/CHANGELOG.md +54 -0
- segmtree-0.3.0/LICENSE +21 -0
- segmtree-0.3.0/PKG-INFO +218 -0
- segmtree-0.3.0/README.md +186 -0
- segmtree-0.3.0/README.zh-CN.md +166 -0
- segmtree-0.3.0/pyproject.toml +72 -0
- segmtree-0.3.0/src/segmtree/__init__.py +30 -0
- segmtree-0.3.0/src/segmtree/_base.py +94 -0
- segmtree-0.3.0/src/segmtree/rules.py +215 -0
- segmtree-0.3.0/src/segmtree/stability.py +100 -0
- segmtree-0.3.0/src/segmtree/tree.py +956 -0
- segmtree-0.3.0/tests/__init__.py +0 -0
- segmtree-0.3.0/tests/_naive_reference.py +159 -0
- segmtree-0.3.0/tests/conftest.py +44 -0
- segmtree-0.3.0/tests/test_categorical.py +233 -0
- segmtree-0.3.0/tests/test_equivalence.py +83 -0
- segmtree-0.3.0/tests/test_invariance.py +120 -0
- segmtree-0.3.0/tests/test_rules.py +71 -0
- segmtree-0.3.0/tests/test_stability.py +61 -0
- segmtree-0.3.0/tests/test_tree.py +235 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 — 2026-08-25
|
|
4
|
+
|
|
5
|
+
- **Encoding-invariant categorical impurity**: declared categorical features
|
|
6
|
+
now contribute *Gini impurity relative to the root Gini* to node
|
|
7
|
+
heterogeneity (continuous/binary keep normalized variance). Results no
|
|
8
|
+
longer depend on how categories happen to be integer-coded; any relabeling
|
|
9
|
+
yields an equivalent partition. Children Gini for every candidate split is
|
|
10
|
+
evaluated exactly via per-column contingency tables.
|
|
11
|
+
- **Exact categorical codes under any dtype**: category columns are stored as
|
|
12
|
+
int64/float64 internally, so codes like `16_777_216` vs `16_777_217` stay
|
|
13
|
+
distinct even with `dtype=numpy.float32`.
|
|
14
|
+
- **`handle_unknown` parameter** (`"complement"` default, `"error"`): control
|
|
15
|
+
whether unseen category codes at prediction time route deterministically to
|
|
16
|
+
the complement side or raise `ValueError` naming the feature and values.
|
|
17
|
+
New learned attribute ``categories_`` records the training-time code set of
|
|
18
|
+
every categorical column.
|
|
19
|
+
- **`profile_leaves` categorical profiles**: categorical columns are excluded
|
|
20
|
+
from z-scored ``top_features`` and instead reported through
|
|
21
|
+
``category_enrichment``: the most enriched ``(feature, code, leaf_share,
|
|
22
|
+
lift)`` pairs per segment.
|
|
23
|
+
- Test suite hardening: package-style imports (`tests/__init__.py`),
|
|
24
|
+
recoding-invariance tests across multiple permutations, float32 large-code
|
|
25
|
+
regression, unknown-category policy tests and categorical profile tests.
|
|
26
|
+
|
|
27
|
+
## 0.2.0 — 2026-08-25
|
|
28
|
+
|
|
29
|
+
- **Native categorical features**: pass integer-coded columns via
|
|
30
|
+
`categorical_features`; splits are category-subset memberships
|
|
31
|
+
(`color in {1, 2}`), enumerated exhaustively and scored with the same
|
|
32
|
+
normalized-heterogeneity criterion (`max_categories` guards the 2^(k-1)
|
|
33
|
+
enumeration).
|
|
34
|
+
- Candidate thresholds for numeric splits are now computed and located in the
|
|
35
|
+
original feature space, guaranteeing that the evaluated partition is exactly
|
|
36
|
+
the committed one (fixes a rare boundary-point flip when a quantile knot
|
|
37
|
+
coincided with a data point).
|
|
38
|
+
- `Condition` / `Cut` gained set-membership semantics (`op="in"`, `categories`);
|
|
39
|
+
rule replay, `predict` on unseen codes (deterministic right branch) and
|
|
40
|
+
profiling all support categorical columns.
|
|
41
|
+
|
|
42
|
+
## 0.1.0 — 2026-08-25
|
|
43
|
+
|
|
44
|
+
Initial release.
|
|
45
|
+
|
|
46
|
+
- `SegTree`: best-first unsupervised decision tree (clustering tree) with
|
|
47
|
+
normalized mixed-type heterogeneity criterion, quantile candidate splits,
|
|
48
|
+
automatic binary-feature detection, sklearn-compatible API and float32/64
|
|
49
|
+
internal precision.
|
|
50
|
+
- `extract_rules` / `replay_labels` / `replay_check`: rule extraction with
|
|
51
|
+
independent replay verification.
|
|
52
|
+
- `profile_leaves`: per-segment z-score feature profiles.
|
|
53
|
+
- `bootstrap_stability`: bootstrap ARI stability assessment.
|
|
54
|
+
- `adjusted_rand_score`: pure-numpy ARI matching scikit-learn numerics.
|
segmtree-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The segmtree authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
segmtree-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: segmtree
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Interpretable unsupervised decision tree for customer segmentation and rule-based clustering.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Unknownxu1/segmtree
|
|
6
|
+
Project-URL: Repository, https://github.com/Unknownxu1/segmtree
|
|
7
|
+
Project-URL: Issues, https://github.com/Unknownxu1/segmtree/issues
|
|
8
|
+
Author-email: Unknownxu1 <Unknownxu1@users.noreply.github.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: clustering,clustering-tree,customer-segmentation,decision-tree,interpretable-ml,unsupervised-learning
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: numpy>=1.21
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pandas>=1.5; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
30
|
+
Requires-Dist: scikit-learn>=1.1; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# segmtree
|
|
34
|
+
|
|
35
|
+
Interpretable unsupervised decision trees for segmentation — clusters rows of a
|
|
36
|
+
tabular dataset into segments, where every segment is described by a short,
|
|
37
|
+
human-readable rule chain. No target variable required.
|
|
38
|
+
|
|
39
|
+
`segmtree` implements a **clustering tree**: instead of predicting labels, it
|
|
40
|
+
recursively splits the data so that within-segment heterogeneity drops as much
|
|
41
|
+
as possible at every cut, growing best-first (highest-gain node expanded
|
|
42
|
+
first). Think of it as "a decision tree grown without `y`" — you get both the
|
|
43
|
+
partition *and* the rules that define it, which plain k-means or GMM cannot
|
|
44
|
+
give you.
|
|
45
|
+
|
|
46
|
+
## Why segmtree?
|
|
47
|
+
|
|
48
|
+
| | k-means / GMM | segmtree |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Segment definition | centroid coordinates | `income > 50k AND is_active = 0` style rules |
|
|
51
|
+
| New-row assignment | nearest centroid | evaluate ≤ 6 threshold tests |
|
|
52
|
+
| Mixed binary + continuous features | needs scaling tricks | one normalized criterion for both |
|
|
53
|
+
| Built-in self-checks | none | gain additivity + rule replay verification |
|
|
54
|
+
| Stability assessment | ad hoc | one-call bootstrap ARI |
|
|
55
|
+
|
|
56
|
+
Typical uses: customer segmentation with behavioral flags, patient phenotyping,
|
|
57
|
+
survey respondent typing — any "many 0/1 tags + a few counts" table.
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install segmtree
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Requires Python ≥ 3.9 and numpy only.
|
|
66
|
+
|
|
67
|
+
## Quickstart
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import numpy as np
|
|
71
|
+
from segmtree import SegTree, extract_rules, profile_leaves, replay_check, bootstrap_stability
|
|
72
|
+
|
|
73
|
+
rng = np.random.default_rng(0)
|
|
74
|
+
# 4 continuous features + 3 binary flags, three latent groups
|
|
75
|
+
X = np.vstack([
|
|
76
|
+
np.hstack([rng.normal([0, -1, 2, .5], .5, (250, 4)), (rng.random((250, 3)) < .1)]),
|
|
77
|
+
np.hstack([rng.normal([4, 3, -2, .5], .5, (200, 4)), (rng.random((200, 3)) < .5)]),
|
|
78
|
+
np.hstack([rng.normal([-4, 2, 0, 8], .5, (150, 4)), (rng.random((150, 3)) < .9)]),
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
tree = SegTree(min_gain=0.02, min_leaf_frac=0.05, min_leaf_abs=10)
|
|
82
|
+
tree.fit(X)
|
|
83
|
+
|
|
84
|
+
print(f"{tree.n_leaves_} segments, heterogeneity reduced {tree.reduction_:.0%}")
|
|
85
|
+
|
|
86
|
+
for rule in extract_rules(tree):
|
|
87
|
+
print(rule) # #0: x1 <= -1.24 AND x0 <= 1.83 ...
|
|
88
|
+
|
|
89
|
+
profile_leaves(tree, X, top_k=3) # most distinctive features per segment
|
|
90
|
+
replay_check(tree, X) # True: stored rules reproduce the partition exactly
|
|
91
|
+
bootstrap_stability(tree, X, n_replicates=12, random_state=0).mean # e.g. 0.98
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Binary columns (all values in `{0, 1}`) are detected automatically and split
|
|
95
|
+
with a single test; pandas DataFrames are accepted and column names flow into
|
|
96
|
+
the rules.
|
|
97
|
+
|
|
98
|
+
## Categorical features
|
|
99
|
+
|
|
100
|
+
Columns holding integer category codes (e.g. `0..k-1` from a nominal
|
|
101
|
+
encoding) can be declared with `categorical_features`. They are split by the
|
|
102
|
+
best **subset** of categories rather than by a threshold, and the rules read
|
|
103
|
+
as membership tests:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
tree = SegTree(categorical_features=["color"], max_categories=12).fit(df)
|
|
107
|
+
# rule example: #3: color in {1, 2} AND income > 42 ...
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Impurity contribution: categorical columns use **Gini impurity relative to
|
|
111
|
+
the root Gini**, while continuous/binary columns keep normalized variance.
|
|
112
|
+
Gini depends only on the category *distribution*, so any one-to-one recoding
|
|
113
|
+
of the codes yields an equivalent partition — results are encoding-invariant.
|
|
114
|
+
|
|
115
|
+
Notes:
|
|
116
|
+
|
|
117
|
+
- Codes must be integral; non-numeric labels should be factorized first
|
|
118
|
+
(`pd.factorize`, `df["color"].astype("category").cat.codes`).
|
|
119
|
+
- Codes are handled exactly at any magnitude (int64/float64 internally), even
|
|
120
|
+
with `dtype=np.float32` for the continuous columns.
|
|
121
|
+
- All `2^(k-1)-1` category partitions of a node are evaluated exactly, so
|
|
122
|
+
keep `k <= max_categories` (rare levels can be grouped beforehand); for
|
|
123
|
+
high-cardinality columns prefer one-hot encoding.
|
|
124
|
+
- Unseen codes at prediction time follow `handle_unknown`: `"complement"`
|
|
125
|
+
(default) routes them deterministically to the complement side and
|
|
126
|
+
`replay_check` stays exact; `"error"` raises `ValueError` naming the
|
|
127
|
+
feature and unknown values. The training-time code sets are exposed via
|
|
128
|
+
the ``categories_`` attribute.
|
|
129
|
+
- ``profile_leaves`` reports categorical structure through
|
|
130
|
+
``category_enrichment`` — ``(feature, code, leaf_share, lift)`` pairs —
|
|
131
|
+
and keeps z-scores for continuous/binary features only.
|
|
132
|
+
|
|
133
|
+
## How it works
|
|
134
|
+
|
|
135
|
+
Node heterogeneity averages per-feature variance relative to the root:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
H(S) = (1/m) · Σ_j Var_j(S) / Var_j(root) H(root) = 1
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Because Bernoulli variance p(1−p) fits the same formula, binary and continuous
|
|
142
|
+
features share one comparable criterion. A split into L/R earns:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
Gain = H(S) − [ n_L/n_S · H(L) + n_R/n_S · H(R) ]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Candidates are the in-node quantiles of each feature (`quantiles=8` → the 5%
|
|
149
|
+
… 95% quantiles; binary features get one candidate at 0.5). All candidates are
|
|
150
|
+
evaluated per feature via sort + prefix sums in O(n log n), and the globally
|
|
151
|
+
best leaf is always split next (best-first).
|
|
152
|
+
|
|
153
|
+
Splitting stops when no cut gains at least `min_gain`, a node is too small
|
|
154
|
+
(`max(min_leaf_abs, min_leaf_frac·n)` per child), or `max_depth` is reached.
|
|
155
|
+
|
|
156
|
+
Two properties make results trustworthy out of the box:
|
|
157
|
+
|
|
158
|
+
- **Gain additivity** — `total_gain_ == h_root_ − h_avg_` up to float error;
|
|
159
|
+
it accounts for exactly how much heterogeneity was explained.
|
|
160
|
+
- **Rule replay** — `replay_check()` re-evaluates every leaf's rule chain from
|
|
161
|
+
scratch and must reproduce `predict()` bit-for-bit.
|
|
162
|
+
|
|
163
|
+
The test suite additionally verifies exact agreement against an independent
|
|
164
|
+
naive reference implementation.
|
|
165
|
+
|
|
166
|
+
## API overview
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
SegTree(min_gain=0.01, min_leaf_frac=0.01, min_leaf_abs=50, max_depth=None,
|
|
170
|
+
quantiles=8, binary_features="auto", categorical_features=None,
|
|
171
|
+
max_categories=12, handle_unknown="complement", feature_names=None,
|
|
172
|
+
dtype=np.float64)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Member | Purpose |
|
|
176
|
+
|---|---|
|
|
177
|
+
| `.fit(X)` / `.fit_predict(X)` / `.predict(X)` | sklearn-style fit & assignment |
|
|
178
|
+
| `.labels_`, `.n_leaves_`, `.leaves_`, `.cuts_` | fitted structure |
|
|
179
|
+
| `.reduction_`, `.h_avg_`, `.total_gain_`, `.summary()` | quality statistics |
|
|
180
|
+
| `extract_rules(tree)` | list of `Rule` objects (`str(rule)` → readable chain) |
|
|
181
|
+
| `replay_labels(tree, X)` / `replay_check(tree, X)` | rule-replay validation |
|
|
182
|
+
| `profile_leaves(tree, X, top_k=3)` | z-score profiles per segment |
|
|
183
|
+
| `bootstrap_stability(tree, X, n_replicates, random_state)` | mean/std of bootstrap ARI |
|
|
184
|
+
| `adjusted_rand_score(a, b)` | pure-numpy ARI |
|
|
185
|
+
|
|
186
|
+
Tuning tips:
|
|
187
|
+
|
|
188
|
+
- Fewer, larger segments → raise `min_gain` (e.g. `0.02–0.05`) or lower
|
|
189
|
+
`quantiles`; cap `max_depth` (e.g. 4–6) to keep rules short and stable.
|
|
190
|
+
- Small datasets (< 5000 rows) → lower `min_leaf_abs` (it defaults to 50).
|
|
191
|
+
- Unstable segments (low bootstrap ARI) → fewer quantiles, higher `min_gain`,
|
|
192
|
+
or a depth cap usually stabilize the structure.
|
|
193
|
+
|
|
194
|
+
## Comparison with related tools
|
|
195
|
+
|
|
196
|
+
- **scikit-learn DecisionTree on k-means labels** ("surrogate tree"): two-step,
|
|
197
|
+
rules may contradict the clustering; `segmtree` optimizes split quality and
|
|
198
|
+
rule fidelity simultaneously.
|
|
199
|
+
- **forest-clustering / URF**: forest-proximity clustering — accurate but the
|
|
200
|
+
segments themselves are not directly interpretable.
|
|
201
|
+
- **CUBT (R)**: closest academic relative; `segmtree` adds best-first growth,
|
|
202
|
+
mixed-type normalization, additivity checks and bootstrap stability in a
|
|
203
|
+
numpy-only package.
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
git clone <your-fork-url> && cd segmtree
|
|
209
|
+
pip install -e .[dev]
|
|
210
|
+
pytest # run the test suite
|
|
211
|
+
ruff check src tests
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT — see [LICENSE](LICENSE).
|
|
217
|
+
|
|
218
|
+
[中文文档](README.zh-CN.md)
|
segmtree-0.3.0/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# segmtree
|
|
2
|
+
|
|
3
|
+
Interpretable unsupervised decision trees for segmentation — clusters rows of a
|
|
4
|
+
tabular dataset into segments, where every segment is described by a short,
|
|
5
|
+
human-readable rule chain. No target variable required.
|
|
6
|
+
|
|
7
|
+
`segmtree` implements a **clustering tree**: instead of predicting labels, it
|
|
8
|
+
recursively splits the data so that within-segment heterogeneity drops as much
|
|
9
|
+
as possible at every cut, growing best-first (highest-gain node expanded
|
|
10
|
+
first). Think of it as "a decision tree grown without `y`" — you get both the
|
|
11
|
+
partition *and* the rules that define it, which plain k-means or GMM cannot
|
|
12
|
+
give you.
|
|
13
|
+
|
|
14
|
+
## Why segmtree?
|
|
15
|
+
|
|
16
|
+
| | k-means / GMM | segmtree |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| Segment definition | centroid coordinates | `income > 50k AND is_active = 0` style rules |
|
|
19
|
+
| New-row assignment | nearest centroid | evaluate ≤ 6 threshold tests |
|
|
20
|
+
| Mixed binary + continuous features | needs scaling tricks | one normalized criterion for both |
|
|
21
|
+
| Built-in self-checks | none | gain additivity + rule replay verification |
|
|
22
|
+
| Stability assessment | ad hoc | one-call bootstrap ARI |
|
|
23
|
+
|
|
24
|
+
Typical uses: customer segmentation with behavioral flags, patient phenotyping,
|
|
25
|
+
survey respondent typing — any "many 0/1 tags + a few counts" table.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install segmtree
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requires Python ≥ 3.9 and numpy only.
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import numpy as np
|
|
39
|
+
from segmtree import SegTree, extract_rules, profile_leaves, replay_check, bootstrap_stability
|
|
40
|
+
|
|
41
|
+
rng = np.random.default_rng(0)
|
|
42
|
+
# 4 continuous features + 3 binary flags, three latent groups
|
|
43
|
+
X = np.vstack([
|
|
44
|
+
np.hstack([rng.normal([0, -1, 2, .5], .5, (250, 4)), (rng.random((250, 3)) < .1)]),
|
|
45
|
+
np.hstack([rng.normal([4, 3, -2, .5], .5, (200, 4)), (rng.random((200, 3)) < .5)]),
|
|
46
|
+
np.hstack([rng.normal([-4, 2, 0, 8], .5, (150, 4)), (rng.random((150, 3)) < .9)]),
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
tree = SegTree(min_gain=0.02, min_leaf_frac=0.05, min_leaf_abs=10)
|
|
50
|
+
tree.fit(X)
|
|
51
|
+
|
|
52
|
+
print(f"{tree.n_leaves_} segments, heterogeneity reduced {tree.reduction_:.0%}")
|
|
53
|
+
|
|
54
|
+
for rule in extract_rules(tree):
|
|
55
|
+
print(rule) # #0: x1 <= -1.24 AND x0 <= 1.83 ...
|
|
56
|
+
|
|
57
|
+
profile_leaves(tree, X, top_k=3) # most distinctive features per segment
|
|
58
|
+
replay_check(tree, X) # True: stored rules reproduce the partition exactly
|
|
59
|
+
bootstrap_stability(tree, X, n_replicates=12, random_state=0).mean # e.g. 0.98
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Binary columns (all values in `{0, 1}`) are detected automatically and split
|
|
63
|
+
with a single test; pandas DataFrames are accepted and column names flow into
|
|
64
|
+
the rules.
|
|
65
|
+
|
|
66
|
+
## Categorical features
|
|
67
|
+
|
|
68
|
+
Columns holding integer category codes (e.g. `0..k-1` from a nominal
|
|
69
|
+
encoding) can be declared with `categorical_features`. They are split by the
|
|
70
|
+
best **subset** of categories rather than by a threshold, and the rules read
|
|
71
|
+
as membership tests:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
tree = SegTree(categorical_features=["color"], max_categories=12).fit(df)
|
|
75
|
+
# rule example: #3: color in {1, 2} AND income > 42 ...
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Impurity contribution: categorical columns use **Gini impurity relative to
|
|
79
|
+
the root Gini**, while continuous/binary columns keep normalized variance.
|
|
80
|
+
Gini depends only on the category *distribution*, so any one-to-one recoding
|
|
81
|
+
of the codes yields an equivalent partition — results are encoding-invariant.
|
|
82
|
+
|
|
83
|
+
Notes:
|
|
84
|
+
|
|
85
|
+
- Codes must be integral; non-numeric labels should be factorized first
|
|
86
|
+
(`pd.factorize`, `df["color"].astype("category").cat.codes`).
|
|
87
|
+
- Codes are handled exactly at any magnitude (int64/float64 internally), even
|
|
88
|
+
with `dtype=np.float32` for the continuous columns.
|
|
89
|
+
- All `2^(k-1)-1` category partitions of a node are evaluated exactly, so
|
|
90
|
+
keep `k <= max_categories` (rare levels can be grouped beforehand); for
|
|
91
|
+
high-cardinality columns prefer one-hot encoding.
|
|
92
|
+
- Unseen codes at prediction time follow `handle_unknown`: `"complement"`
|
|
93
|
+
(default) routes them deterministically to the complement side and
|
|
94
|
+
`replay_check` stays exact; `"error"` raises `ValueError` naming the
|
|
95
|
+
feature and unknown values. The training-time code sets are exposed via
|
|
96
|
+
the ``categories_`` attribute.
|
|
97
|
+
- ``profile_leaves`` reports categorical structure through
|
|
98
|
+
``category_enrichment`` — ``(feature, code, leaf_share, lift)`` pairs —
|
|
99
|
+
and keeps z-scores for continuous/binary features only.
|
|
100
|
+
|
|
101
|
+
## How it works
|
|
102
|
+
|
|
103
|
+
Node heterogeneity averages per-feature variance relative to the root:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
H(S) = (1/m) · Σ_j Var_j(S) / Var_j(root) H(root) = 1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Because Bernoulli variance p(1−p) fits the same formula, binary and continuous
|
|
110
|
+
features share one comparable criterion. A split into L/R earns:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Gain = H(S) − [ n_L/n_S · H(L) + n_R/n_S · H(R) ]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Candidates are the in-node quantiles of each feature (`quantiles=8` → the 5%
|
|
117
|
+
… 95% quantiles; binary features get one candidate at 0.5). All candidates are
|
|
118
|
+
evaluated per feature via sort + prefix sums in O(n log n), and the globally
|
|
119
|
+
best leaf is always split next (best-first).
|
|
120
|
+
|
|
121
|
+
Splitting stops when no cut gains at least `min_gain`, a node is too small
|
|
122
|
+
(`max(min_leaf_abs, min_leaf_frac·n)` per child), or `max_depth` is reached.
|
|
123
|
+
|
|
124
|
+
Two properties make results trustworthy out of the box:
|
|
125
|
+
|
|
126
|
+
- **Gain additivity** — `total_gain_ == h_root_ − h_avg_` up to float error;
|
|
127
|
+
it accounts for exactly how much heterogeneity was explained.
|
|
128
|
+
- **Rule replay** — `replay_check()` re-evaluates every leaf's rule chain from
|
|
129
|
+
scratch and must reproduce `predict()` bit-for-bit.
|
|
130
|
+
|
|
131
|
+
The test suite additionally verifies exact agreement against an independent
|
|
132
|
+
naive reference implementation.
|
|
133
|
+
|
|
134
|
+
## API overview
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
SegTree(min_gain=0.01, min_leaf_frac=0.01, min_leaf_abs=50, max_depth=None,
|
|
138
|
+
quantiles=8, binary_features="auto", categorical_features=None,
|
|
139
|
+
max_categories=12, handle_unknown="complement", feature_names=None,
|
|
140
|
+
dtype=np.float64)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| Member | Purpose |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `.fit(X)` / `.fit_predict(X)` / `.predict(X)` | sklearn-style fit & assignment |
|
|
146
|
+
| `.labels_`, `.n_leaves_`, `.leaves_`, `.cuts_` | fitted structure |
|
|
147
|
+
| `.reduction_`, `.h_avg_`, `.total_gain_`, `.summary()` | quality statistics |
|
|
148
|
+
| `extract_rules(tree)` | list of `Rule` objects (`str(rule)` → readable chain) |
|
|
149
|
+
| `replay_labels(tree, X)` / `replay_check(tree, X)` | rule-replay validation |
|
|
150
|
+
| `profile_leaves(tree, X, top_k=3)` | z-score profiles per segment |
|
|
151
|
+
| `bootstrap_stability(tree, X, n_replicates, random_state)` | mean/std of bootstrap ARI |
|
|
152
|
+
| `adjusted_rand_score(a, b)` | pure-numpy ARI |
|
|
153
|
+
|
|
154
|
+
Tuning tips:
|
|
155
|
+
|
|
156
|
+
- Fewer, larger segments → raise `min_gain` (e.g. `0.02–0.05`) or lower
|
|
157
|
+
`quantiles`; cap `max_depth` (e.g. 4–6) to keep rules short and stable.
|
|
158
|
+
- Small datasets (< 5000 rows) → lower `min_leaf_abs` (it defaults to 50).
|
|
159
|
+
- Unstable segments (low bootstrap ARI) → fewer quantiles, higher `min_gain`,
|
|
160
|
+
or a depth cap usually stabilize the structure.
|
|
161
|
+
|
|
162
|
+
## Comparison with related tools
|
|
163
|
+
|
|
164
|
+
- **scikit-learn DecisionTree on k-means labels** ("surrogate tree"): two-step,
|
|
165
|
+
rules may contradict the clustering; `segmtree` optimizes split quality and
|
|
166
|
+
rule fidelity simultaneously.
|
|
167
|
+
- **forest-clustering / URF**: forest-proximity clustering — accurate but the
|
|
168
|
+
segments themselves are not directly interpretable.
|
|
169
|
+
- **CUBT (R)**: closest academic relative; `segmtree` adds best-first growth,
|
|
170
|
+
mixed-type normalization, additivity checks and bootstrap stability in a
|
|
171
|
+
numpy-only package.
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
git clone <your-fork-url> && cd segmtree
|
|
177
|
+
pip install -e .[dev]
|
|
178
|
+
pytest # run the test suite
|
|
179
|
+
ruff check src tests
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT — see [LICENSE](LICENSE).
|
|
185
|
+
|
|
186
|
+
[中文文档](README.zh-CN.md)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# segmtree
|
|
2
|
+
|
|
3
|
+
可解释的无监督决策树分群库 —— 把一张"客户标签"表自动划分成若干人群,每个人群都由一条
|
|
4
|
+
简短、人类可读的规则链定义,全程不需要任何标签 y。
|
|
5
|
+
|
|
6
|
+
`segmtree` 实现的是**聚类决策树**(clustering tree):不预测类别,而是自顶向下递归二分,
|
|
7
|
+
每一刀选择使"组内异质性下降最多"的单特征阈值规则,并按 Best-First(全局收益最大的叶子
|
|
8
|
+
先切)生长。可以理解为"没有 y 也能长的决策树"——同时给你划分结果和划分规则,这是
|
|
9
|
+
k-means / GMM 做不到的。
|
|
10
|
+
|
|
11
|
+
## 为什么选 segmtree?
|
|
12
|
+
|
|
13
|
+
| | k-means / GMM | segmtree |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| 人群定义 | 质心坐标 | `income > 50k AND is_active = 0` 式规则 |
|
|
16
|
+
| 新样本归类 | 最近质心 | 至多几条阈值判断即可回放 |
|
|
17
|
+
| 二值 + 连续混合特征 | 需要各种缩放技巧 | 归一化后同口径比较 |
|
|
18
|
+
| 内置自检 | 无 | 收益可加性 + 规则回放一致性校验 |
|
|
19
|
+
| 稳定性评估 | 自己想办法 | 一行调用的 Bootstrap ARI |
|
|
20
|
+
|
|
21
|
+
典型场景:带行为标签的客户分群、患者表型归类、问卷人群画像——一切"大量 0/1 标签 +
|
|
22
|
+
少量计数/金额列"的表。
|
|
23
|
+
|
|
24
|
+
## 安装
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install segmtree
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
仅要求 Python ≥ 3.9 与 numpy。
|
|
31
|
+
|
|
32
|
+
## 快速上手
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import numpy as np
|
|
36
|
+
from segmtree import SegTree, extract_rules, profile_leaves, replay_check, bootstrap_stability
|
|
37
|
+
|
|
38
|
+
rng = np.random.default_rng(0)
|
|
39
|
+
X = np.vstack([
|
|
40
|
+
np.hstack([rng.normal([0, -1, 2, .5], .5, (250, 4)), (rng.random((250, 3)) < .1)]),
|
|
41
|
+
np.hstack([rng.normal([4, 3, -2, .5], .5, (200, 4)), (rng.random((200, 3)) < .5)]),
|
|
42
|
+
np.hstack([rng.normal([-4, 2, 0, 8], .5, (150, 4)), (rng.random((150, 3)) < .9)]),
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
tree = SegTree(min_gain=0.02, min_leaf_frac=0.05, min_leaf_abs=10)
|
|
46
|
+
tree.fit(X)
|
|
47
|
+
|
|
48
|
+
print(f"{tree.n_leaves_} 类人群,异质性降低 {tree.reduction_:.0%}")
|
|
49
|
+
|
|
50
|
+
for rule in extract_rules(tree):
|
|
51
|
+
print(rule) # #0: x1 <= -1.24 AND x0 <= 1.83 ...
|
|
52
|
+
|
|
53
|
+
profile_leaves(tree, X, top_k=3) # 每类与全体差异最大的特征(z 分数)
|
|
54
|
+
replay_check(tree, X) # True:规则回放与 predict 完全一致
|
|
55
|
+
bootstrap_stability(tree, X, n_replicates=12, random_state=0).mean # 例如 0.98
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
取值只有 {0, 1} 的列会被自动识别为二值特征(只做一次"是/否"切分);直接传 pandas
|
|
59
|
+
DataFrame 也可以,列名会自动带入规则文本。
|
|
60
|
+
|
|
61
|
+
## 类别特征
|
|
62
|
+
|
|
63
|
+
整数编码的类别列(如名义变量的 0..k-1 码)可通过 `categorical_features` 声明。
|
|
64
|
+
它们不按阈值切分,而是按**类别子集**划分,规则呈现为成员判断:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
tree = SegTree(categorical_features=["color"], max_categories=12).fit(df)
|
|
68
|
+
# 规则示例: #3: color in {1, 2} AND income > 42 ...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
异质性贡献:类别列使用 **Gini 杂度相对根 Gini 的比值**,连续/二值列保持归一化
|
|
72
|
+
方差。Gini 只取决于类别*分布*,因此对编码做任何一一重映射都会得到等价的划分——
|
|
73
|
+
结果与编码方式无关。
|
|
74
|
+
|
|
75
|
+
注意:
|
|
76
|
+
|
|
77
|
+
- 编码必须为整数值;非数值标签请先因子化
|
|
78
|
+
(`pd.factorize` 或 `df["color"].astype("category").cat.codes`);
|
|
79
|
+
- 任意大小的编码都会被精确处理(内部 int64/float64),即使连续列选择
|
|
80
|
+
`dtype=np.float32` 也不会合并;
|
|
81
|
+
- 节点内会精确评估全部 `2^(k-1)-1` 种类别划分,请保持
|
|
82
|
+
`k <= max_categories`(稀有水平建议先归并);高基数列更适合 one-hot;
|
|
83
|
+
- 预测时未见过的编码由 `handle_unknown` 控制:`"complement"`(默认)确定性地
|
|
84
|
+
落入补集一侧且 `replay_check` 依然严格一致;`"error"` 抛出带特征名与未知值
|
|
85
|
+
的 `ValueError`。训练期类别集合通过 ``categories_`` 属性暴露;
|
|
86
|
+
- ``profile_leaves`` 通过 ``category_enrichment``(`(特征, 编码, 叶内占比,
|
|
87
|
+
lift)`)报告类别结构,z 分数仅用于连续/二值特征。
|
|
88
|
+
|
|
89
|
+
## 方法原理
|
|
90
|
+
|
|
91
|
+
节点异质性 = 各特征组内方差除以根方差后的平均:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
H(S) = (1/m) · Σ_j Var_j(S) / Var_j(root) H(root) = 1
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
二值特征的伯努利方差 p(1−p) 天然适用同一公式,因此两类特征可在同一口径下比较。
|
|
98
|
+
一刀切成 L/R 两块的收益:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
Gain = H(S) − [ n_L/n_S · H(L) + n_R/n_S · H(R) ]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
候选阈值取各特征在节点内的分位数(`quantiles=8` 即 5%~95% 共 8 个;二值特征仅 0.5
|
|
105
|
+
一刀)。每个特征用排序 + 前缀和 O(n log n) 批量评估全部候选,堆维护全局最优叶子,
|
|
106
|
+
Best-First 生长。满足任一条件即停止:最优刀收益低于 `min_gain`、子节点过小
|
|
107
|
+
(`max(min_leaf_abs, min_leaf_frac·n)`)、达到 `max_depth`。
|
|
108
|
+
|
|
109
|
+
两个开箱即用的可信度保证:
|
|
110
|
+
|
|
111
|
+
- **收益可加性** —— `total_gain_ == h_root_ − h_avg_`(浮点误差量级),精确核算这棵树
|
|
112
|
+
解释掉了多少总异质性;
|
|
113
|
+
- **规则回放** —— `replay_check()` 从零重新求值每条叶子规则链,必须与 `predict()`
|
|
114
|
+
逐行一致。
|
|
115
|
+
|
|
116
|
+
测试套件还包含与一个独立朴素参考实现逐位一致性的对照测试。
|
|
117
|
+
|
|
118
|
+
## API 一览
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
SegTree(min_gain=0.01, min_leaf_frac=0.01, min_leaf_abs=50, max_depth=None,
|
|
122
|
+
quantiles=8, binary_features="auto", categorical_features=None,
|
|
123
|
+
max_categories=12, handle_unknown="complement", feature_names=None,
|
|
124
|
+
dtype=np.float64)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| 成员 | 用途 |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `.fit(X)` / `.fit_predict(X)` / `.predict(X)` | sklearn 风格训练与归类 |
|
|
130
|
+
| `.labels_`, `.n_leaves_`, `.leaves_`, `.cuts_` | 拟合结构 |
|
|
131
|
+
| `.reduction_`, `.h_avg_`, `.total_gain_`, `.summary()` | 质量统计 |
|
|
132
|
+
| `extract_rules(tree)` | 返回 `Rule` 列表(`str(rule)` 即规则链文本) |
|
|
133
|
+
| `replay_labels(tree, X)` / `replay_check(tree, X)` | 规则回放校验 |
|
|
134
|
+
| `profile_leaves(tree, X, top_k=3)` | 每类 z 分数画像 |
|
|
135
|
+
| `bootstrap_stability(tree, X, ...)` | Bootstrap ARI(mean/std) |
|
|
136
|
+
| `adjusted_rand_score(a, b)` | 纯 numpy 实现的 ARI |
|
|
137
|
+
|
|
138
|
+
调参建议:
|
|
139
|
+
|
|
140
|
+
- 想要更少、更大的人群 → 提高 `min_gain`(如 0.02~0.05)或减少 `quantiles`;
|
|
141
|
+
- 规则想更短、结果想更稳 → 设 `max_depth`(4~6);
|
|
142
|
+
- 小数据集(< 5000 行)→ 降低 `min_leaf_abs`(默认 50 是为大数据设的下限);
|
|
143
|
+
- Bootstrap ARI 偏低 → 通常减少分位数、提高 `min_gain` 或限深即可稳定结构。
|
|
144
|
+
|
|
145
|
+
## 与相关工具的比较
|
|
146
|
+
|
|
147
|
+
- **先 k-means 再用 sklearn 决策树拟合**(代理树):两步走,规则可能与聚类结果矛盾;
|
|
148
|
+
segmtree 把切分质量与规则一致性放进同一个目标。
|
|
149
|
+
- **forest-clustering / URF**:基于森林邻近度的聚类,精度好但人群本身不可直接解释。
|
|
150
|
+
- **CUBT(R 包)**:学术上最接近的方法;segmtree 在纯 numpy 包内补齐了 Best-First
|
|
151
|
+
生长、混合类型归一化、可加性自检与 Bootstrap 稳定性。
|
|
152
|
+
|
|
153
|
+
## 开发
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
git clone <your-fork-url> && cd segmtree
|
|
157
|
+
pip install -e .[dev]
|
|
158
|
+
pytest # 运行测试
|
|
159
|
+
ruff check src tests
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## 许可证
|
|
163
|
+
|
|
164
|
+
MIT — 见 [LICENSE](LICENSE)。
|
|
165
|
+
|
|
166
|
+
[English](README.md)
|