steer-learn 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.
- steer_learn-0.1.0/PKG-INFO +321 -0
- steer_learn-0.1.0/README.md +310 -0
- steer_learn-0.1.0/pyproject.toml +24 -0
- steer_learn-0.1.0/setup.cfg +4 -0
- steer_learn-0.1.0/src/steer_learn/__init__.py +2 -0
- steer_learn-0.1.0/src/steer_learn/hidden_markov_model.py +119 -0
- steer_learn-0.1.0/src/steer_learn/markov_chain.py +59 -0
- steer_learn-0.1.0/src/steer_learn/transformer.py +42 -0
- steer_learn-0.1.0/src/steer_learn.egg-info/PKG-INFO +321 -0
- steer_learn-0.1.0/src/steer_learn.egg-info/SOURCES.txt +11 -0
- steer_learn-0.1.0/src/steer_learn.egg-info/dependency_links.txt +1 -0
- steer_learn-0.1.0/src/steer_learn.egg-info/requires.txt +2 -0
- steer_learn-0.1.0/src/steer_learn.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: steer-learn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SKLearn extention for Markov models
|
|
5
|
+
Author-email: Miguel <miguel.melo@driverevel.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: scikit-learn
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
|
|
12
|
+
# steer_learn
|
|
13
|
+
|
|
14
|
+
`steer_learn` is a lightweight **scikit-learn-compatible extension** for transition-based and sequence-aware learning.
|
|
15
|
+
It provides estimators for **absorbing Markov chains** and **hidden Markov models (HMMs)**, plus small preprocessing utilities for path-like sequential data.
|
|
16
|
+
|
|
17
|
+
The package is designed to feel familiar to scikit-learn users:
|
|
18
|
+
|
|
19
|
+
- estimators inherit from `BaseEstimator`
|
|
20
|
+
- predictive models follow the `fit` / `predict` / `predict_proba` API
|
|
21
|
+
- transformers implement `fit` / `transform`
|
|
22
|
+
- components are easy to compose in sklearn-style workflows where their input/output contracts fit the task
|
|
23
|
+
|
|
24
|
+
## Why `steer_learn`?
|
|
25
|
+
|
|
26
|
+
Many real-world systems are naturally described as **state transitions**:
|
|
27
|
+
|
|
28
|
+
- navigation funnels
|
|
29
|
+
- user journey analysis
|
|
30
|
+
- workflow completion paths
|
|
31
|
+
- process mining
|
|
32
|
+
- discrete state machines
|
|
33
|
+
- sequence classification with previous-state context
|
|
34
|
+
|
|
35
|
+
`steer_learn` focuses on these settings with a simple API and minimal dependencies.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
### Estimators
|
|
40
|
+
|
|
41
|
+
- **`MarkovAbsorbingModel`**
|
|
42
|
+
Learns transition probabilities for a Markov chain with absorbing states and predicts the most likely absorbing outcome.
|
|
43
|
+
|
|
44
|
+
- **`HiddenMarkovModel`**
|
|
45
|
+
A discrete-emission hidden Markov model classifier that combines transition probabilities with per-dimension categorical emission probabilities.
|
|
46
|
+
|
|
47
|
+
- **`GaussianHiddenMarkovModel`**
|
|
48
|
+
A continuous-emission hidden Markov model classifier that uses multivariate Gaussian emissions.
|
|
49
|
+
|
|
50
|
+
### Transformers
|
|
51
|
+
|
|
52
|
+
- **`PathSplitter`**
|
|
53
|
+
Splits string paths into tokenized state sequences.
|
|
54
|
+
|
|
55
|
+
- **`PathFlanker`**
|
|
56
|
+
Prefixes each sequence with a start token and appends the aligned target value from `y`, making it a **supervised sequence-preparation transformer**.
|
|
57
|
+
|
|
58
|
+
- **`TransitionRoller`**
|
|
59
|
+
Converts sequences into pairwise transitions.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
For local development:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install steer_learn
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
### 1. Absorbing Markov chain classification
|
|
72
|
+
|
|
73
|
+
Use `MarkovAbsorbingModel` when each sample represents a transition from one state to the next, encoded as one-hot vectors.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import numpy as np
|
|
77
|
+
from steer_learn import MarkovAbsorbingModel
|
|
78
|
+
|
|
79
|
+
# 3 states: A, B, C
|
|
80
|
+
# X = source state, y = target state
|
|
81
|
+
X = np.array([
|
|
82
|
+
[1, 0, 0],
|
|
83
|
+
[1, 0, 0],
|
|
84
|
+
[0, 1, 0],
|
|
85
|
+
[0, 1, 0],
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
y = np.array([
|
|
89
|
+
[0, 1, 0],
|
|
90
|
+
[0, 1, 0],
|
|
91
|
+
[0, 0, 1],
|
|
92
|
+
[0, 0, 1],
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
clf = MarkovAbsorbingModel()
|
|
96
|
+
clf.fit(X, y)
|
|
97
|
+
|
|
98
|
+
# Predict the final absorbing destination
|
|
99
|
+
clf.predict(np.array([
|
|
100
|
+
[1, 0, 0],
|
|
101
|
+
[0, 1, 0],
|
|
102
|
+
]))
|
|
103
|
+
|
|
104
|
+
# Probability of each absorbing state
|
|
105
|
+
clf.predict_proba(np.array([
|
|
106
|
+
[1, 0, 0],
|
|
107
|
+
[0, 1, 0],
|
|
108
|
+
]))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 2. Discrete hidden Markov model
|
|
112
|
+
|
|
113
|
+
Use `HiddenMarkovModel` when:
|
|
114
|
+
|
|
115
|
+
- the **first column** of `X` is the previous state
|
|
116
|
+
- the remaining columns are **discrete observed symbols**
|
|
117
|
+
- `y` is the current hidden state label
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import numpy as np
|
|
121
|
+
from steer_learn import HiddenMarkovModel
|
|
122
|
+
|
|
123
|
+
# X columns: [previous_state, symbol_1, symbol_2]
|
|
124
|
+
X = np.array([
|
|
125
|
+
[0, 1, 2],
|
|
126
|
+
[0, 1, 1],
|
|
127
|
+
[1, 0, 2],
|
|
128
|
+
[1, 0, 1],
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
y = np.array([0, 0, 1, 1])
|
|
132
|
+
|
|
133
|
+
clf = HiddenMarkovModel()
|
|
134
|
+
clf.fit(X, y)
|
|
135
|
+
|
|
136
|
+
pred = clf.predict(X)
|
|
137
|
+
proba = clf.predict_proba(X)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 3. Gaussian hidden Markov model
|
|
141
|
+
|
|
142
|
+
Use `GaussianHiddenMarkovModel` when observations are continuous.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import numpy as np
|
|
146
|
+
from steer_learn import GaussianHiddenMarkovModel
|
|
147
|
+
|
|
148
|
+
# X columns: [previous_state, feature_1, feature_2]
|
|
149
|
+
X = np.array([
|
|
150
|
+
[0, 0.2, 1.1],
|
|
151
|
+
[0, 0.1, 0.9],
|
|
152
|
+
[1, 2.2, 3.0],
|
|
153
|
+
[1, 2.0, 2.8],
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
y = np.array([0, 0, 1, 1])
|
|
157
|
+
|
|
158
|
+
clf = GaussianHiddenMarkovModel()
|
|
159
|
+
clf.fit(X, y)
|
|
160
|
+
|
|
161
|
+
pred = clf.predict(X)
|
|
162
|
+
proba = clf.predict_proba(X)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## API design
|
|
166
|
+
|
|
167
|
+
`steer_learn` follows the core conventions recommended for scikit-learn-compatible extensions:
|
|
168
|
+
|
|
169
|
+
- **Estimator interface**: predictive models implement `fit`, `predict`, and when available `predict_proba`.
|
|
170
|
+
- **Transformer interface**: preprocessing components implement `fit` and `transform`.
|
|
171
|
+
- **Composable objects**: estimators inherit from `BaseEstimator`, which provides parameter inspection utilities such as `get_params` and `set_params`.
|
|
172
|
+
- **Return `self` from `fit`**: all fitted estimators return the estimator instance.
|
|
173
|
+
- **NumPy-first inputs**: examples use NumPy arrays and sklearn-style tabular inputs.
|
|
174
|
+
|
|
175
|
+
This makes the project easier to understand for users already familiar with the scikit-learn ecosystem and simplifies future integration with tooling such as pipelines, search utilities, and model evaluation helpers.
|
|
176
|
+
|
|
177
|
+
## Available objects
|
|
178
|
+
|
|
179
|
+
### Top-level imports
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from steer_learn import (
|
|
183
|
+
MarkovAbsorbingModel,
|
|
184
|
+
HiddenMarkovModel,
|
|
185
|
+
GaussianHiddenMarkovModel,
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Transformer utilities
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from steer_learn.transformer import (
|
|
193
|
+
PathSplitter,
|
|
194
|
+
PathFlanker,
|
|
195
|
+
TransitionRoller,
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Input conventions
|
|
200
|
+
|
|
201
|
+
### `MarkovAbsorbingModel`
|
|
202
|
+
|
|
203
|
+
- `X`: 2D array of one-hot encoded **source** states
|
|
204
|
+
- `y`: 2D array of one-hot encoded **target** states
|
|
205
|
+
- `predict(X)`: returns the most likely absorbing state index
|
|
206
|
+
- `predict_proba(X)`: returns absorbing-state probabilities
|
|
207
|
+
|
|
208
|
+
### `HiddenMarkovModel`
|
|
209
|
+
|
|
210
|
+
- `X[:, 0]`: previous state index
|
|
211
|
+
- `X[:, 1:]`: discrete emission symbols
|
|
212
|
+
- `y`: target state index
|
|
213
|
+
- `predict_proba(X)`: returns unnormalized state scores from transition × emission terms
|
|
214
|
+
|
|
215
|
+
### `GaussianHiddenMarkovModel`
|
|
216
|
+
|
|
217
|
+
- `X[:, 0]`: previous state index
|
|
218
|
+
- `X[:, 1:]`: continuous-valued observation vector
|
|
219
|
+
- `y`: target state index
|
|
220
|
+
- `predict_proba(X)`: returns Gaussian emission density × transition scores
|
|
221
|
+
|
|
222
|
+
### `PathSplitter`
|
|
223
|
+
|
|
224
|
+
- `X`: iterable of path strings
|
|
225
|
+
- output: tokenized sequences split on `sep`
|
|
226
|
+
|
|
227
|
+
### `PathFlanker`
|
|
228
|
+
|
|
229
|
+
- parameter: `start_state="<START>"`
|
|
230
|
+
- `X`: iterable of tokenized sequences
|
|
231
|
+
- `y`: iterable of aligned target labels
|
|
232
|
+
- output: each sample becomes `[start_state] + list(x) + [target]`
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from steer_learn.transformer import PathFlanker
|
|
238
|
+
|
|
239
|
+
X = [["Landing", "Signup"], ["Landing", "Pricing"]]
|
|
240
|
+
y = ["Activated", "Churned"]
|
|
241
|
+
|
|
242
|
+
flanker = PathFlanker(start_state="<START>")
|
|
243
|
+
X_flanked = flanker.fit_transform(X, y)
|
|
244
|
+
# ["<START>", "Landing", "Signup", "Activated"]
|
|
245
|
+
# ["<START>", "Landing", "Pricing", "Churned"]
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### `TransitionRoller`
|
|
249
|
+
|
|
250
|
+
- `X`: iterable of sequences
|
|
251
|
+
- output: 2-column transition pairs extracted from consecutive positions
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
## Use cases
|
|
255
|
+
|
|
256
|
+
`steer_learn` is a good fit for problems such as:
|
|
257
|
+
|
|
258
|
+
- predicting terminal states in a process
|
|
259
|
+
- estimating next-state probabilities
|
|
260
|
+
- modeling user or system trajectories
|
|
261
|
+
- sequence-aware classification with previous-state context
|
|
262
|
+
- transforming string-based paths into transition datasets
|
|
263
|
+
- preparing supervised transition sequences where the final label is appended to the observed path
|
|
264
|
+
|
|
265
|
+
## Notes on `PathFlanker`
|
|
266
|
+
|
|
267
|
+
`PathFlanker` now behaves differently from a typical unsupervised preprocessing transformer:
|
|
268
|
+
|
|
269
|
+
- it uses `y` during `transform`, not just during `fit`
|
|
270
|
+
- it appends the aligned target label to the end of each sequence
|
|
271
|
+
- it is best understood as a **training-data preparation step** for labeled sequence problems
|
|
272
|
+
|
|
273
|
+
This is still compatible with the general sklearn estimator style, but it is less conventional than a pure `transform(X)` preprocessing step. In practice, it is most useful in custom preprocessing workflows, dataset construction code, or supervised sequence pipelines where `y` is intentionally available at transform time.
|
|
274
|
+
|
|
275
|
+
## Example workflow for path data
|
|
276
|
+
|
|
277
|
+
A typical workflow for journey or state-path data may look like this:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
from steer_learn.transformer import PathSplitter, PathFlanker, TransitionRoller
|
|
281
|
+
|
|
282
|
+
paths = [
|
|
283
|
+
"Landing -> Signup",
|
|
284
|
+
"Landing -> Pricing",
|
|
285
|
+
]
|
|
286
|
+
labels = ["Activated", "Churned"]
|
|
287
|
+
|
|
288
|
+
splitter = PathSplitter(sep="->")
|
|
289
|
+
flanker = PathFlanker(start_state="<START>")
|
|
290
|
+
roller = TransitionRoller()
|
|
291
|
+
|
|
292
|
+
X = splitter.fit_transform(paths)
|
|
293
|
+
X = flanker.fit_transform(X, labels)
|
|
294
|
+
transitions = roller.transform(X)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
This produces transitions over sequences that begin with `<START>` and end with the aligned target label, which is useful when the target should be modeled as the final state in the path.
|
|
298
|
+
|
|
299
|
+
## Notes and current scope
|
|
300
|
+
|
|
301
|
+
This project is intentionally compact and focused. It currently emphasizes:
|
|
302
|
+
|
|
303
|
+
- educational clarity
|
|
304
|
+
- sklearn-style APIs
|
|
305
|
+
- transition-based modeling primitives
|
|
306
|
+
- simple NumPy-backed implementations
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
## Contributing
|
|
310
|
+
|
|
311
|
+
Contributions are welcome. A good contribution should preserve the project's core design goals:
|
|
312
|
+
|
|
313
|
+
- keep the API intuitive for scikit-learn users
|
|
314
|
+
- document estimator inputs and outputs clearly
|
|
315
|
+
- prefer readable, testable implementations
|
|
316
|
+
- maintain consistent naming and import patterns
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
If you use `steer_learn` in research, analytics, or production experiments, consider documenting the exact state encoding, label semantics, and sequence assumptions used in your preprocessing pipeline.
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# steer_learn
|
|
2
|
+
|
|
3
|
+
`steer_learn` is a lightweight **scikit-learn-compatible extension** for transition-based and sequence-aware learning.
|
|
4
|
+
It provides estimators for **absorbing Markov chains** and **hidden Markov models (HMMs)**, plus small preprocessing utilities for path-like sequential data.
|
|
5
|
+
|
|
6
|
+
The package is designed to feel familiar to scikit-learn users:
|
|
7
|
+
|
|
8
|
+
- estimators inherit from `BaseEstimator`
|
|
9
|
+
- predictive models follow the `fit` / `predict` / `predict_proba` API
|
|
10
|
+
- transformers implement `fit` / `transform`
|
|
11
|
+
- components are easy to compose in sklearn-style workflows where their input/output contracts fit the task
|
|
12
|
+
|
|
13
|
+
## Why `steer_learn`?
|
|
14
|
+
|
|
15
|
+
Many real-world systems are naturally described as **state transitions**:
|
|
16
|
+
|
|
17
|
+
- navigation funnels
|
|
18
|
+
- user journey analysis
|
|
19
|
+
- workflow completion paths
|
|
20
|
+
- process mining
|
|
21
|
+
- discrete state machines
|
|
22
|
+
- sequence classification with previous-state context
|
|
23
|
+
|
|
24
|
+
`steer_learn` focuses on these settings with a simple API and minimal dependencies.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
### Estimators
|
|
29
|
+
|
|
30
|
+
- **`MarkovAbsorbingModel`**
|
|
31
|
+
Learns transition probabilities for a Markov chain with absorbing states and predicts the most likely absorbing outcome.
|
|
32
|
+
|
|
33
|
+
- **`HiddenMarkovModel`**
|
|
34
|
+
A discrete-emission hidden Markov model classifier that combines transition probabilities with per-dimension categorical emission probabilities.
|
|
35
|
+
|
|
36
|
+
- **`GaussianHiddenMarkovModel`**
|
|
37
|
+
A continuous-emission hidden Markov model classifier that uses multivariate Gaussian emissions.
|
|
38
|
+
|
|
39
|
+
### Transformers
|
|
40
|
+
|
|
41
|
+
- **`PathSplitter`**
|
|
42
|
+
Splits string paths into tokenized state sequences.
|
|
43
|
+
|
|
44
|
+
- **`PathFlanker`**
|
|
45
|
+
Prefixes each sequence with a start token and appends the aligned target value from `y`, making it a **supervised sequence-preparation transformer**.
|
|
46
|
+
|
|
47
|
+
- **`TransitionRoller`**
|
|
48
|
+
Converts sequences into pairwise transitions.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
For local development:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install steer_learn
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
### 1. Absorbing Markov chain classification
|
|
61
|
+
|
|
62
|
+
Use `MarkovAbsorbingModel` when each sample represents a transition from one state to the next, encoded as one-hot vectors.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import numpy as np
|
|
66
|
+
from steer_learn import MarkovAbsorbingModel
|
|
67
|
+
|
|
68
|
+
# 3 states: A, B, C
|
|
69
|
+
# X = source state, y = target state
|
|
70
|
+
X = np.array([
|
|
71
|
+
[1, 0, 0],
|
|
72
|
+
[1, 0, 0],
|
|
73
|
+
[0, 1, 0],
|
|
74
|
+
[0, 1, 0],
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
y = np.array([
|
|
78
|
+
[0, 1, 0],
|
|
79
|
+
[0, 1, 0],
|
|
80
|
+
[0, 0, 1],
|
|
81
|
+
[0, 0, 1],
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
clf = MarkovAbsorbingModel()
|
|
85
|
+
clf.fit(X, y)
|
|
86
|
+
|
|
87
|
+
# Predict the final absorbing destination
|
|
88
|
+
clf.predict(np.array([
|
|
89
|
+
[1, 0, 0],
|
|
90
|
+
[0, 1, 0],
|
|
91
|
+
]))
|
|
92
|
+
|
|
93
|
+
# Probability of each absorbing state
|
|
94
|
+
clf.predict_proba(np.array([
|
|
95
|
+
[1, 0, 0],
|
|
96
|
+
[0, 1, 0],
|
|
97
|
+
]))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 2. Discrete hidden Markov model
|
|
101
|
+
|
|
102
|
+
Use `HiddenMarkovModel` when:
|
|
103
|
+
|
|
104
|
+
- the **first column** of `X` is the previous state
|
|
105
|
+
- the remaining columns are **discrete observed symbols**
|
|
106
|
+
- `y` is the current hidden state label
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import numpy as np
|
|
110
|
+
from steer_learn import HiddenMarkovModel
|
|
111
|
+
|
|
112
|
+
# X columns: [previous_state, symbol_1, symbol_2]
|
|
113
|
+
X = np.array([
|
|
114
|
+
[0, 1, 2],
|
|
115
|
+
[0, 1, 1],
|
|
116
|
+
[1, 0, 2],
|
|
117
|
+
[1, 0, 1],
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
y = np.array([0, 0, 1, 1])
|
|
121
|
+
|
|
122
|
+
clf = HiddenMarkovModel()
|
|
123
|
+
clf.fit(X, y)
|
|
124
|
+
|
|
125
|
+
pred = clf.predict(X)
|
|
126
|
+
proba = clf.predict_proba(X)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3. Gaussian hidden Markov model
|
|
130
|
+
|
|
131
|
+
Use `GaussianHiddenMarkovModel` when observations are continuous.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import numpy as np
|
|
135
|
+
from steer_learn import GaussianHiddenMarkovModel
|
|
136
|
+
|
|
137
|
+
# X columns: [previous_state, feature_1, feature_2]
|
|
138
|
+
X = np.array([
|
|
139
|
+
[0, 0.2, 1.1],
|
|
140
|
+
[0, 0.1, 0.9],
|
|
141
|
+
[1, 2.2, 3.0],
|
|
142
|
+
[1, 2.0, 2.8],
|
|
143
|
+
])
|
|
144
|
+
|
|
145
|
+
y = np.array([0, 0, 1, 1])
|
|
146
|
+
|
|
147
|
+
clf = GaussianHiddenMarkovModel()
|
|
148
|
+
clf.fit(X, y)
|
|
149
|
+
|
|
150
|
+
pred = clf.predict(X)
|
|
151
|
+
proba = clf.predict_proba(X)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## API design
|
|
155
|
+
|
|
156
|
+
`steer_learn` follows the core conventions recommended for scikit-learn-compatible extensions:
|
|
157
|
+
|
|
158
|
+
- **Estimator interface**: predictive models implement `fit`, `predict`, and when available `predict_proba`.
|
|
159
|
+
- **Transformer interface**: preprocessing components implement `fit` and `transform`.
|
|
160
|
+
- **Composable objects**: estimators inherit from `BaseEstimator`, which provides parameter inspection utilities such as `get_params` and `set_params`.
|
|
161
|
+
- **Return `self` from `fit`**: all fitted estimators return the estimator instance.
|
|
162
|
+
- **NumPy-first inputs**: examples use NumPy arrays and sklearn-style tabular inputs.
|
|
163
|
+
|
|
164
|
+
This makes the project easier to understand for users already familiar with the scikit-learn ecosystem and simplifies future integration with tooling such as pipelines, search utilities, and model evaluation helpers.
|
|
165
|
+
|
|
166
|
+
## Available objects
|
|
167
|
+
|
|
168
|
+
### Top-level imports
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from steer_learn import (
|
|
172
|
+
MarkovAbsorbingModel,
|
|
173
|
+
HiddenMarkovModel,
|
|
174
|
+
GaussianHiddenMarkovModel,
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Transformer utilities
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from steer_learn.transformer import (
|
|
182
|
+
PathSplitter,
|
|
183
|
+
PathFlanker,
|
|
184
|
+
TransitionRoller,
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Input conventions
|
|
189
|
+
|
|
190
|
+
### `MarkovAbsorbingModel`
|
|
191
|
+
|
|
192
|
+
- `X`: 2D array of one-hot encoded **source** states
|
|
193
|
+
- `y`: 2D array of one-hot encoded **target** states
|
|
194
|
+
- `predict(X)`: returns the most likely absorbing state index
|
|
195
|
+
- `predict_proba(X)`: returns absorbing-state probabilities
|
|
196
|
+
|
|
197
|
+
### `HiddenMarkovModel`
|
|
198
|
+
|
|
199
|
+
- `X[:, 0]`: previous state index
|
|
200
|
+
- `X[:, 1:]`: discrete emission symbols
|
|
201
|
+
- `y`: target state index
|
|
202
|
+
- `predict_proba(X)`: returns unnormalized state scores from transition × emission terms
|
|
203
|
+
|
|
204
|
+
### `GaussianHiddenMarkovModel`
|
|
205
|
+
|
|
206
|
+
- `X[:, 0]`: previous state index
|
|
207
|
+
- `X[:, 1:]`: continuous-valued observation vector
|
|
208
|
+
- `y`: target state index
|
|
209
|
+
- `predict_proba(X)`: returns Gaussian emission density × transition scores
|
|
210
|
+
|
|
211
|
+
### `PathSplitter`
|
|
212
|
+
|
|
213
|
+
- `X`: iterable of path strings
|
|
214
|
+
- output: tokenized sequences split on `sep`
|
|
215
|
+
|
|
216
|
+
### `PathFlanker`
|
|
217
|
+
|
|
218
|
+
- parameter: `start_state="<START>"`
|
|
219
|
+
- `X`: iterable of tokenized sequences
|
|
220
|
+
- `y`: iterable of aligned target labels
|
|
221
|
+
- output: each sample becomes `[start_state] + list(x) + [target]`
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from steer_learn.transformer import PathFlanker
|
|
227
|
+
|
|
228
|
+
X = [["Landing", "Signup"], ["Landing", "Pricing"]]
|
|
229
|
+
y = ["Activated", "Churned"]
|
|
230
|
+
|
|
231
|
+
flanker = PathFlanker(start_state="<START>")
|
|
232
|
+
X_flanked = flanker.fit_transform(X, y)
|
|
233
|
+
# ["<START>", "Landing", "Signup", "Activated"]
|
|
234
|
+
# ["<START>", "Landing", "Pricing", "Churned"]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### `TransitionRoller`
|
|
238
|
+
|
|
239
|
+
- `X`: iterable of sequences
|
|
240
|
+
- output: 2-column transition pairs extracted from consecutive positions
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
## Use cases
|
|
244
|
+
|
|
245
|
+
`steer_learn` is a good fit for problems such as:
|
|
246
|
+
|
|
247
|
+
- predicting terminal states in a process
|
|
248
|
+
- estimating next-state probabilities
|
|
249
|
+
- modeling user or system trajectories
|
|
250
|
+
- sequence-aware classification with previous-state context
|
|
251
|
+
- transforming string-based paths into transition datasets
|
|
252
|
+
- preparing supervised transition sequences where the final label is appended to the observed path
|
|
253
|
+
|
|
254
|
+
## Notes on `PathFlanker`
|
|
255
|
+
|
|
256
|
+
`PathFlanker` now behaves differently from a typical unsupervised preprocessing transformer:
|
|
257
|
+
|
|
258
|
+
- it uses `y` during `transform`, not just during `fit`
|
|
259
|
+
- it appends the aligned target label to the end of each sequence
|
|
260
|
+
- it is best understood as a **training-data preparation step** for labeled sequence problems
|
|
261
|
+
|
|
262
|
+
This is still compatible with the general sklearn estimator style, but it is less conventional than a pure `transform(X)` preprocessing step. In practice, it is most useful in custom preprocessing workflows, dataset construction code, or supervised sequence pipelines where `y` is intentionally available at transform time.
|
|
263
|
+
|
|
264
|
+
## Example workflow for path data
|
|
265
|
+
|
|
266
|
+
A typical workflow for journey or state-path data may look like this:
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from steer_learn.transformer import PathSplitter, PathFlanker, TransitionRoller
|
|
270
|
+
|
|
271
|
+
paths = [
|
|
272
|
+
"Landing -> Signup",
|
|
273
|
+
"Landing -> Pricing",
|
|
274
|
+
]
|
|
275
|
+
labels = ["Activated", "Churned"]
|
|
276
|
+
|
|
277
|
+
splitter = PathSplitter(sep="->")
|
|
278
|
+
flanker = PathFlanker(start_state="<START>")
|
|
279
|
+
roller = TransitionRoller()
|
|
280
|
+
|
|
281
|
+
X = splitter.fit_transform(paths)
|
|
282
|
+
X = flanker.fit_transform(X, labels)
|
|
283
|
+
transitions = roller.transform(X)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
This produces transitions over sequences that begin with `<START>` and end with the aligned target label, which is useful when the target should be modeled as the final state in the path.
|
|
287
|
+
|
|
288
|
+
## Notes and current scope
|
|
289
|
+
|
|
290
|
+
This project is intentionally compact and focused. It currently emphasizes:
|
|
291
|
+
|
|
292
|
+
- educational clarity
|
|
293
|
+
- sklearn-style APIs
|
|
294
|
+
- transition-based modeling primitives
|
|
295
|
+
- simple NumPy-backed implementations
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
## Contributing
|
|
299
|
+
|
|
300
|
+
Contributions are welcome. A good contribution should preserve the project's core design goals:
|
|
301
|
+
|
|
302
|
+
- keep the API intuitive for scikit-learn users
|
|
303
|
+
- document estimator inputs and outputs clearly
|
|
304
|
+
- prefer readable, testable implementations
|
|
305
|
+
- maintain consistent naming and import patterns
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
If you use `steer_learn` in research, analytics, or production experiments, consider documenting the exact state encoding, label semantics, and sequence assumptions used in your preprocessing pipeline.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "steer-learn"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SKLearn extention for Markov models"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Miguel", email = "miguel.melo@driverevel.com" }
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"scikit-learn",
|
|
17
|
+
"numpy"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
package-dir = {"" = "src"}
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from sklearn.base import BaseEstimator, ClassifierMixin
|
|
6
|
+
|
|
7
|
+
class HiddenMarkovModelBase(BaseEstimator, ClassifierMixin, ABC):
|
|
8
|
+
_n_states: int
|
|
9
|
+
_n_dims: int
|
|
10
|
+
_transitions_matrix: np.ndarray[float, float]
|
|
11
|
+
|
|
12
|
+
def fit(self, X, y):
|
|
13
|
+
prev_states, _ = self._preprocess_x(X)
|
|
14
|
+
states = np.array(y)
|
|
15
|
+
self._n_states = max(prev_states.max(), states.max()) + 1
|
|
16
|
+
|
|
17
|
+
transitions = np.zeros((self._n_states, self._n_states))
|
|
18
|
+
for prev_state, state in zip(prev_states, states):
|
|
19
|
+
transitions[prev_state, state] += 1
|
|
20
|
+
|
|
21
|
+
totals = np.sum(transitions, axis=1, keepdims=True)
|
|
22
|
+
totals[totals == 0] = 1
|
|
23
|
+
self._transitions_matrix = transitions / totals
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def _emission(self, state: int, symbols: Iterable[int | float]) -> float: ...
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _preprocess_x(X: Iterable[Iterable[float | int]]) -> (np.ndarray[int], np.ndarray[float, float]):
|
|
31
|
+
X_processed = np.array(X)
|
|
32
|
+
x1 = X_processed[:, 0]
|
|
33
|
+
x2 = X_processed[:, 1:]
|
|
34
|
+
return x1, x2
|
|
35
|
+
|
|
36
|
+
def _transitions(self, state_a: int, state_b: int) -> float:
|
|
37
|
+
return self._transitions_matrix[state_a, state_b]
|
|
38
|
+
|
|
39
|
+
def predict_proba(self, X):
|
|
40
|
+
prev_states, symbols = self._preprocess_x(X)
|
|
41
|
+
transitions = np.zeros((len(prev_states), self._n_states))
|
|
42
|
+
emissions = np.zeros_like(transitions)
|
|
43
|
+
|
|
44
|
+
for i, (prev_state, symbol) in enumerate(zip(prev_states, symbols)):
|
|
45
|
+
for state in range(self._n_states):
|
|
46
|
+
emissions[i, state] = self._emission(state, symbol)
|
|
47
|
+
transitions[i, state] = self._transitions(prev_state, state)
|
|
48
|
+
|
|
49
|
+
return emissions * transitions
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def predict(self, X: Iterable[Iterable[float | int]]) -> np.ndarray[int]:
|
|
53
|
+
probs = self.predict_proba(X)
|
|
54
|
+
return np.argmax(probs, axis=1)
|
|
55
|
+
|
|
56
|
+
class HiddenMarkovModel(HiddenMarkovModelBase):
|
|
57
|
+
_n_symbols: int
|
|
58
|
+
_emission_matrix: np.ndarray[float, float]
|
|
59
|
+
|
|
60
|
+
def fit(self, X: Iterable[Iterable[int]], y: Iterable[int]):
|
|
61
|
+
super().fit(X, y)
|
|
62
|
+
_, symbols = self._preprocess_x(X)
|
|
63
|
+
states = np.array(y)
|
|
64
|
+
self._n_symbols = np.max(symbols) + 1
|
|
65
|
+
self._n_dims = symbols.shape[1]
|
|
66
|
+
e_matrix = np.zeros((
|
|
67
|
+
self._n_states,
|
|
68
|
+
self._n_dims,
|
|
69
|
+
self._n_symbols
|
|
70
|
+
))
|
|
71
|
+
for state, symbol in zip(states, symbols):
|
|
72
|
+
for d, symb in enumerate(symbol):
|
|
73
|
+
e_matrix[state, d, symb] += 1
|
|
74
|
+
totals = np.sum(e_matrix, axis=2, keepdims=True)
|
|
75
|
+
self._emission_matrix = e_matrix / totals
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def _emission(self, state: int, symbols: Iterable[int]) -> float:
|
|
79
|
+
result = 1
|
|
80
|
+
for d, symbol in enumerate(symbols):
|
|
81
|
+
result *= self._emission_matrix[state, d, symbol]
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
class GaussianHiddenMarkovModel(HiddenMarkovModelBase):
|
|
85
|
+
_mu: np.ndarray
|
|
86
|
+
_sigma: np.ndarray
|
|
87
|
+
|
|
88
|
+
def fit(self, X: Iterable[Iterable[float | int]], y: Iterable[int]):
|
|
89
|
+
super().fit(X, y)
|
|
90
|
+
_, symbols = self._preprocess_x(X)
|
|
91
|
+
states = np.array(y)
|
|
92
|
+
self._n_dims = symbols.shape[1]
|
|
93
|
+
mu = np.zeros((
|
|
94
|
+
self._n_states,
|
|
95
|
+
self._n_dims
|
|
96
|
+
))
|
|
97
|
+
sigma = np.zeros((
|
|
98
|
+
self._n_states,
|
|
99
|
+
self._n_dims,
|
|
100
|
+
self._n_dims
|
|
101
|
+
))
|
|
102
|
+
for i in range(self._n_states):
|
|
103
|
+
mu[i, :] = np.mean(symbols[states == i], axis=0)
|
|
104
|
+
for i in range(self._n_states):
|
|
105
|
+
symbol = symbols[states == i]
|
|
106
|
+
sigma[i, ...] = np.cov(symbol, rowvar=False, bias=True)
|
|
107
|
+
|
|
108
|
+
self._mu = mu
|
|
109
|
+
self._sigma = sigma
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
def _emission(self, state, symbols):
|
|
113
|
+
x = np.array(symbols)
|
|
114
|
+
diff = x - self._mu[state]
|
|
115
|
+
sigma = self._sigma[state]
|
|
116
|
+
inv_sigma = np.linalg.inv(sigma)
|
|
117
|
+
exponent = -0.5 * (diff.T @ inv_sigma @ diff)
|
|
118
|
+
den = np.sqrt(((2 * np.pi) ** self._n_dims) * np.linalg.det(sigma))
|
|
119
|
+
return np.exp(exponent) / den
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from sklearn.base import BaseEstimator, ClassifierMixin
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MarkovAbsorbingModel(BaseEstimator, ClassifierMixin):
|
|
8
|
+
_n_absorbing_states: int
|
|
9
|
+
_n_transient_states: int
|
|
10
|
+
_b_matrix: np.ndarray
|
|
11
|
+
_absorbing_states: np.ndarray
|
|
12
|
+
_transient_states: np.ndarray
|
|
13
|
+
_transient_map: np.ndarray
|
|
14
|
+
|
|
15
|
+
def fit(self, X: Iterable[Iterable[int]], y: Iterable[Iterable[int]]):
|
|
16
|
+
srcs = np.array(X)
|
|
17
|
+
trgs = np.array(y)
|
|
18
|
+
n_states = srcs.shape[1]
|
|
19
|
+
srcs = np.argmax(srcs, axis=1)
|
|
20
|
+
trgs = np.argmax(trgs, axis=1)
|
|
21
|
+
transitions = np.zeros((
|
|
22
|
+
n_states,
|
|
23
|
+
n_states
|
|
24
|
+
))
|
|
25
|
+
for src, trg in zip(srcs, trgs):
|
|
26
|
+
transitions[src, trg] += 1
|
|
27
|
+
totals = np.sum(transitions, axis=1)
|
|
28
|
+
abs_idx = totals == 0
|
|
29
|
+
trans_idx = totals > 0
|
|
30
|
+
safe_totals = totals.copy()
|
|
31
|
+
safe_totals[abs_idx] = 1
|
|
32
|
+
transitions = transitions / safe_totals[:, None]
|
|
33
|
+
self._n_absorbing_states = np.sum(abs_idx)
|
|
34
|
+
self._n_transient_states = np.sum(trans_idx)
|
|
35
|
+
self._absorbing_states = np.flatnonzero(abs_idx)
|
|
36
|
+
self._transient_states = np.flatnonzero(trans_idx)
|
|
37
|
+
self._transient_map = np.full(n_states, -1, dtype=int)
|
|
38
|
+
self._transient_map[self._transient_states] = np.arange(self._n_transient_states)
|
|
39
|
+
Q = transitions[np.ix_(trans_idx, trans_idx)]
|
|
40
|
+
R = transitions[np.ix_(trans_idx, abs_idx)]
|
|
41
|
+
N = np.linalg.inv(
|
|
42
|
+
np.identity(self._n_transient_states) - Q
|
|
43
|
+
)
|
|
44
|
+
self._b_matrix = N @ R
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
def predict(self, X: Iterable[Iterable[int]]) -> Iterable[int]:
|
|
48
|
+
probs = self.predict_proba(X)
|
|
49
|
+
return self._absorbing_states[np.argmax(probs, axis=1)]
|
|
50
|
+
|
|
51
|
+
def predict_proba(self, X: Iterable[Iterable[int]]) -> Iterable[Iterable[float]]:
|
|
52
|
+
idx = np.argmax(np.array(X), axis=1)
|
|
53
|
+
probs = np.zeros((len(idx), self._n_absorbing_states))
|
|
54
|
+
transient_mask = self._transient_map[idx] >= 0
|
|
55
|
+
probs[transient_mask] = self._b_matrix[self._transient_map[idx[transient_mask]]]
|
|
56
|
+
for i, state in enumerate(idx):
|
|
57
|
+
if not transient_mask[i]:
|
|
58
|
+
probs[i, self._absorbing_states == state] = 1.0
|
|
59
|
+
return probs
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from sklearn.base import BaseEstimator, TransformerMixin
|
|
3
|
+
|
|
4
|
+
class PathSplitter(BaseEstimator, TransformerMixin):
|
|
5
|
+
def __init__(self, sep = "->"):
|
|
6
|
+
self.sep = sep
|
|
7
|
+
|
|
8
|
+
def fit(self, X, y=None):
|
|
9
|
+
return self
|
|
10
|
+
|
|
11
|
+
def transform(self, X, y=None):
|
|
12
|
+
return np.array(
|
|
13
|
+
[state.strip() for state in x.split(self.sep)]
|
|
14
|
+
for x in X
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class PathFlanker(BaseEstimator, TransformerMixin):
|
|
18
|
+
def __init__(self, start_state = "<START>"):
|
|
19
|
+
self.start_state = start_state
|
|
20
|
+
|
|
21
|
+
def fit(self, X, y=None):
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def transform(self, X, y=None):
|
|
25
|
+
return np.array(
|
|
26
|
+
np.array(
|
|
27
|
+
[self.start_state] + list(x) + [_y]
|
|
28
|
+
)
|
|
29
|
+
for x, _y in zip(X, y)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
class TransitionRoller(BaseEstimator, TransformerMixin):
|
|
33
|
+
def fit(self, X, y=None):
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def transform(self, X, y=None):
|
|
38
|
+
transitions = []
|
|
39
|
+
for x in X:
|
|
40
|
+
for i in range(len(x) - 1):
|
|
41
|
+
transitions.append([x[i], x[i + 1]])
|
|
42
|
+
return np.array(transitions)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: steer-learn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SKLearn extention for Markov models
|
|
5
|
+
Author-email: Miguel <miguel.melo@driverevel.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: scikit-learn
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
|
|
12
|
+
# steer_learn
|
|
13
|
+
|
|
14
|
+
`steer_learn` is a lightweight **scikit-learn-compatible extension** for transition-based and sequence-aware learning.
|
|
15
|
+
It provides estimators for **absorbing Markov chains** and **hidden Markov models (HMMs)**, plus small preprocessing utilities for path-like sequential data.
|
|
16
|
+
|
|
17
|
+
The package is designed to feel familiar to scikit-learn users:
|
|
18
|
+
|
|
19
|
+
- estimators inherit from `BaseEstimator`
|
|
20
|
+
- predictive models follow the `fit` / `predict` / `predict_proba` API
|
|
21
|
+
- transformers implement `fit` / `transform`
|
|
22
|
+
- components are easy to compose in sklearn-style workflows where their input/output contracts fit the task
|
|
23
|
+
|
|
24
|
+
## Why `steer_learn`?
|
|
25
|
+
|
|
26
|
+
Many real-world systems are naturally described as **state transitions**:
|
|
27
|
+
|
|
28
|
+
- navigation funnels
|
|
29
|
+
- user journey analysis
|
|
30
|
+
- workflow completion paths
|
|
31
|
+
- process mining
|
|
32
|
+
- discrete state machines
|
|
33
|
+
- sequence classification with previous-state context
|
|
34
|
+
|
|
35
|
+
`steer_learn` focuses on these settings with a simple API and minimal dependencies.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
### Estimators
|
|
40
|
+
|
|
41
|
+
- **`MarkovAbsorbingModel`**
|
|
42
|
+
Learns transition probabilities for a Markov chain with absorbing states and predicts the most likely absorbing outcome.
|
|
43
|
+
|
|
44
|
+
- **`HiddenMarkovModel`**
|
|
45
|
+
A discrete-emission hidden Markov model classifier that combines transition probabilities with per-dimension categorical emission probabilities.
|
|
46
|
+
|
|
47
|
+
- **`GaussianHiddenMarkovModel`**
|
|
48
|
+
A continuous-emission hidden Markov model classifier that uses multivariate Gaussian emissions.
|
|
49
|
+
|
|
50
|
+
### Transformers
|
|
51
|
+
|
|
52
|
+
- **`PathSplitter`**
|
|
53
|
+
Splits string paths into tokenized state sequences.
|
|
54
|
+
|
|
55
|
+
- **`PathFlanker`**
|
|
56
|
+
Prefixes each sequence with a start token and appends the aligned target value from `y`, making it a **supervised sequence-preparation transformer**.
|
|
57
|
+
|
|
58
|
+
- **`TransitionRoller`**
|
|
59
|
+
Converts sequences into pairwise transitions.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
For local development:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install steer_learn
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
### 1. Absorbing Markov chain classification
|
|
72
|
+
|
|
73
|
+
Use `MarkovAbsorbingModel` when each sample represents a transition from one state to the next, encoded as one-hot vectors.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import numpy as np
|
|
77
|
+
from steer_learn import MarkovAbsorbingModel
|
|
78
|
+
|
|
79
|
+
# 3 states: A, B, C
|
|
80
|
+
# X = source state, y = target state
|
|
81
|
+
X = np.array([
|
|
82
|
+
[1, 0, 0],
|
|
83
|
+
[1, 0, 0],
|
|
84
|
+
[0, 1, 0],
|
|
85
|
+
[0, 1, 0],
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
y = np.array([
|
|
89
|
+
[0, 1, 0],
|
|
90
|
+
[0, 1, 0],
|
|
91
|
+
[0, 0, 1],
|
|
92
|
+
[0, 0, 1],
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
clf = MarkovAbsorbingModel()
|
|
96
|
+
clf.fit(X, y)
|
|
97
|
+
|
|
98
|
+
# Predict the final absorbing destination
|
|
99
|
+
clf.predict(np.array([
|
|
100
|
+
[1, 0, 0],
|
|
101
|
+
[0, 1, 0],
|
|
102
|
+
]))
|
|
103
|
+
|
|
104
|
+
# Probability of each absorbing state
|
|
105
|
+
clf.predict_proba(np.array([
|
|
106
|
+
[1, 0, 0],
|
|
107
|
+
[0, 1, 0],
|
|
108
|
+
]))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 2. Discrete hidden Markov model
|
|
112
|
+
|
|
113
|
+
Use `HiddenMarkovModel` when:
|
|
114
|
+
|
|
115
|
+
- the **first column** of `X` is the previous state
|
|
116
|
+
- the remaining columns are **discrete observed symbols**
|
|
117
|
+
- `y` is the current hidden state label
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import numpy as np
|
|
121
|
+
from steer_learn import HiddenMarkovModel
|
|
122
|
+
|
|
123
|
+
# X columns: [previous_state, symbol_1, symbol_2]
|
|
124
|
+
X = np.array([
|
|
125
|
+
[0, 1, 2],
|
|
126
|
+
[0, 1, 1],
|
|
127
|
+
[1, 0, 2],
|
|
128
|
+
[1, 0, 1],
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
y = np.array([0, 0, 1, 1])
|
|
132
|
+
|
|
133
|
+
clf = HiddenMarkovModel()
|
|
134
|
+
clf.fit(X, y)
|
|
135
|
+
|
|
136
|
+
pred = clf.predict(X)
|
|
137
|
+
proba = clf.predict_proba(X)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 3. Gaussian hidden Markov model
|
|
141
|
+
|
|
142
|
+
Use `GaussianHiddenMarkovModel` when observations are continuous.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import numpy as np
|
|
146
|
+
from steer_learn import GaussianHiddenMarkovModel
|
|
147
|
+
|
|
148
|
+
# X columns: [previous_state, feature_1, feature_2]
|
|
149
|
+
X = np.array([
|
|
150
|
+
[0, 0.2, 1.1],
|
|
151
|
+
[0, 0.1, 0.9],
|
|
152
|
+
[1, 2.2, 3.0],
|
|
153
|
+
[1, 2.0, 2.8],
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
y = np.array([0, 0, 1, 1])
|
|
157
|
+
|
|
158
|
+
clf = GaussianHiddenMarkovModel()
|
|
159
|
+
clf.fit(X, y)
|
|
160
|
+
|
|
161
|
+
pred = clf.predict(X)
|
|
162
|
+
proba = clf.predict_proba(X)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## API design
|
|
166
|
+
|
|
167
|
+
`steer_learn` follows the core conventions recommended for scikit-learn-compatible extensions:
|
|
168
|
+
|
|
169
|
+
- **Estimator interface**: predictive models implement `fit`, `predict`, and when available `predict_proba`.
|
|
170
|
+
- **Transformer interface**: preprocessing components implement `fit` and `transform`.
|
|
171
|
+
- **Composable objects**: estimators inherit from `BaseEstimator`, which provides parameter inspection utilities such as `get_params` and `set_params`.
|
|
172
|
+
- **Return `self` from `fit`**: all fitted estimators return the estimator instance.
|
|
173
|
+
- **NumPy-first inputs**: examples use NumPy arrays and sklearn-style tabular inputs.
|
|
174
|
+
|
|
175
|
+
This makes the project easier to understand for users already familiar with the scikit-learn ecosystem and simplifies future integration with tooling such as pipelines, search utilities, and model evaluation helpers.
|
|
176
|
+
|
|
177
|
+
## Available objects
|
|
178
|
+
|
|
179
|
+
### Top-level imports
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from steer_learn import (
|
|
183
|
+
MarkovAbsorbingModel,
|
|
184
|
+
HiddenMarkovModel,
|
|
185
|
+
GaussianHiddenMarkovModel,
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Transformer utilities
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from steer_learn.transformer import (
|
|
193
|
+
PathSplitter,
|
|
194
|
+
PathFlanker,
|
|
195
|
+
TransitionRoller,
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Input conventions
|
|
200
|
+
|
|
201
|
+
### `MarkovAbsorbingModel`
|
|
202
|
+
|
|
203
|
+
- `X`: 2D array of one-hot encoded **source** states
|
|
204
|
+
- `y`: 2D array of one-hot encoded **target** states
|
|
205
|
+
- `predict(X)`: returns the most likely absorbing state index
|
|
206
|
+
- `predict_proba(X)`: returns absorbing-state probabilities
|
|
207
|
+
|
|
208
|
+
### `HiddenMarkovModel`
|
|
209
|
+
|
|
210
|
+
- `X[:, 0]`: previous state index
|
|
211
|
+
- `X[:, 1:]`: discrete emission symbols
|
|
212
|
+
- `y`: target state index
|
|
213
|
+
- `predict_proba(X)`: returns unnormalized state scores from transition × emission terms
|
|
214
|
+
|
|
215
|
+
### `GaussianHiddenMarkovModel`
|
|
216
|
+
|
|
217
|
+
- `X[:, 0]`: previous state index
|
|
218
|
+
- `X[:, 1:]`: continuous-valued observation vector
|
|
219
|
+
- `y`: target state index
|
|
220
|
+
- `predict_proba(X)`: returns Gaussian emission density × transition scores
|
|
221
|
+
|
|
222
|
+
### `PathSplitter`
|
|
223
|
+
|
|
224
|
+
- `X`: iterable of path strings
|
|
225
|
+
- output: tokenized sequences split on `sep`
|
|
226
|
+
|
|
227
|
+
### `PathFlanker`
|
|
228
|
+
|
|
229
|
+
- parameter: `start_state="<START>"`
|
|
230
|
+
- `X`: iterable of tokenized sequences
|
|
231
|
+
- `y`: iterable of aligned target labels
|
|
232
|
+
- output: each sample becomes `[start_state] + list(x) + [target]`
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from steer_learn.transformer import PathFlanker
|
|
238
|
+
|
|
239
|
+
X = [["Landing", "Signup"], ["Landing", "Pricing"]]
|
|
240
|
+
y = ["Activated", "Churned"]
|
|
241
|
+
|
|
242
|
+
flanker = PathFlanker(start_state="<START>")
|
|
243
|
+
X_flanked = flanker.fit_transform(X, y)
|
|
244
|
+
# ["<START>", "Landing", "Signup", "Activated"]
|
|
245
|
+
# ["<START>", "Landing", "Pricing", "Churned"]
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### `TransitionRoller`
|
|
249
|
+
|
|
250
|
+
- `X`: iterable of sequences
|
|
251
|
+
- output: 2-column transition pairs extracted from consecutive positions
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
## Use cases
|
|
255
|
+
|
|
256
|
+
`steer_learn` is a good fit for problems such as:
|
|
257
|
+
|
|
258
|
+
- predicting terminal states in a process
|
|
259
|
+
- estimating next-state probabilities
|
|
260
|
+
- modeling user or system trajectories
|
|
261
|
+
- sequence-aware classification with previous-state context
|
|
262
|
+
- transforming string-based paths into transition datasets
|
|
263
|
+
- preparing supervised transition sequences where the final label is appended to the observed path
|
|
264
|
+
|
|
265
|
+
## Notes on `PathFlanker`
|
|
266
|
+
|
|
267
|
+
`PathFlanker` now behaves differently from a typical unsupervised preprocessing transformer:
|
|
268
|
+
|
|
269
|
+
- it uses `y` during `transform`, not just during `fit`
|
|
270
|
+
- it appends the aligned target label to the end of each sequence
|
|
271
|
+
- it is best understood as a **training-data preparation step** for labeled sequence problems
|
|
272
|
+
|
|
273
|
+
This is still compatible with the general sklearn estimator style, but it is less conventional than a pure `transform(X)` preprocessing step. In practice, it is most useful in custom preprocessing workflows, dataset construction code, or supervised sequence pipelines where `y` is intentionally available at transform time.
|
|
274
|
+
|
|
275
|
+
## Example workflow for path data
|
|
276
|
+
|
|
277
|
+
A typical workflow for journey or state-path data may look like this:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
from steer_learn.transformer import PathSplitter, PathFlanker, TransitionRoller
|
|
281
|
+
|
|
282
|
+
paths = [
|
|
283
|
+
"Landing -> Signup",
|
|
284
|
+
"Landing -> Pricing",
|
|
285
|
+
]
|
|
286
|
+
labels = ["Activated", "Churned"]
|
|
287
|
+
|
|
288
|
+
splitter = PathSplitter(sep="->")
|
|
289
|
+
flanker = PathFlanker(start_state="<START>")
|
|
290
|
+
roller = TransitionRoller()
|
|
291
|
+
|
|
292
|
+
X = splitter.fit_transform(paths)
|
|
293
|
+
X = flanker.fit_transform(X, labels)
|
|
294
|
+
transitions = roller.transform(X)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
This produces transitions over sequences that begin with `<START>` and end with the aligned target label, which is useful when the target should be modeled as the final state in the path.
|
|
298
|
+
|
|
299
|
+
## Notes and current scope
|
|
300
|
+
|
|
301
|
+
This project is intentionally compact and focused. It currently emphasizes:
|
|
302
|
+
|
|
303
|
+
- educational clarity
|
|
304
|
+
- sklearn-style APIs
|
|
305
|
+
- transition-based modeling primitives
|
|
306
|
+
- simple NumPy-backed implementations
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
## Contributing
|
|
310
|
+
|
|
311
|
+
Contributions are welcome. A good contribution should preserve the project's core design goals:
|
|
312
|
+
|
|
313
|
+
- keep the API intuitive for scikit-learn users
|
|
314
|
+
- document estimator inputs and outputs clearly
|
|
315
|
+
- prefer readable, testable implementations
|
|
316
|
+
- maintain consistent naming and import patterns
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
If you use `steer_learn` in research, analytics, or production experiments, consider documenting the exact state encoding, label semantics, and sequence assumptions used in your preprocessing pipeline.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/steer_learn/__init__.py
|
|
4
|
+
src/steer_learn/hidden_markov_model.py
|
|
5
|
+
src/steer_learn/markov_chain.py
|
|
6
|
+
src/steer_learn/transformer.py
|
|
7
|
+
src/steer_learn.egg-info/PKG-INFO
|
|
8
|
+
src/steer_learn.egg-info/SOURCES.txt
|
|
9
|
+
src/steer_learn.egg-info/dependency_links.txt
|
|
10
|
+
src/steer_learn.egg-info/requires.txt
|
|
11
|
+
src/steer_learn.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
steer_learn
|