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.
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .venv/
13
+ venv/
14
+ .idea/
15
+ .vscode/
@@ -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.
@@ -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)
@@ -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)