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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ from .markov_chain import MarkovAbsorbingModel
2
+ from .hidden_markov_model import HiddenMarkovModel, GaussianHiddenMarkovModel
@@ -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,2 @@
1
+ scikit-learn
2
+ numpy
@@ -0,0 +1 @@
1
+ steer_learn