layeredcompmodel 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,25 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ python-version: ["3.10", "3.12"]
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - name: Set up Python ${{ matrix.python-version }}
14
+ uses: actions/setup-python@v5
15
+ with:
16
+ python-version: ${{ matrix.python-version }}
17
+ - name: Install & Test
18
+ run: |
19
+ pip install -e .[dev]
20
+ pytest tests/ --cov=layeredcompmodel --cov-report=term-missing --cov-fail-under=75 -ra
21
+ mypy src/layeredcompmodel
22
+ ruff check src/ tests/
23
+ python -m build
24
+ pip install twine
25
+ twine check dist/*
@@ -0,0 +1,134 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .PyInstaller/
11
+ result/
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+
28
+ # PyInstaller
29
+ # Usually these files are covered by other .gitignore patterns already,
30
+ # but this one is useful for ensuring that the deployment directory is available
31
+ .PyInstaller/
32
+ build/
33
+ develop-eggs/
34
+ dist/
35
+ downloads/
36
+ eggs/
37
+ .eggs/
38
+ lib/
39
+ lib64/
40
+ parts/
41
+ sdist/
42
+ var/
43
+ wheels/
44
+ *.egg-info/
45
+ .installed.cfg
46
+ *.egg
47
+
48
+ # Unit test / coverage reports
49
+ htmlcov/
50
+ .tox/
51
+ .nox/
52
+ .coverage
53
+ .coverage.*
54
+ .cache
55
+ nosetests.xml
56
+ coverage.xml
57
+ *.cover
58
+ .pytest_cache/
59
+ hypothesis/
60
+
61
+ # Translations
62
+ *.mo
63
+ *.pot
64
+
65
+ # Django stuff:
66
+ *.log
67
+ local_settings.py
68
+ db.sqlite3
69
+ db.sqlite3-journal
70
+
71
+ # Flask stuff:
72
+ instance/
73
+ .webassets-cache
74
+
75
+ # Scrapy stuff:
76
+ .scrapy
77
+
78
+ # Sphinx documentation
79
+ docs/_build/
80
+
81
+ # PyBuilder
82
+ target/
83
+
84
+ # Jupyter Notebook
85
+ .ipynb_checkpoints
86
+
87
+ # pyenv
88
+ .python-version
89
+
90
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
91
+ __pypackages__/
92
+
93
+ # Celery stuff
94
+ celerybeat-schedule
95
+ celerybeat.pid
96
+
97
+ # SageMath parsed files
98
+ *.sage.py
99
+
100
+ # Environments
101
+ .venv/
102
+ env/
103
+ ENV/
104
+ env.bak/
105
+ venv/
106
+ ENV/
107
+ env.bak/
108
+
109
+ # Spyder project settings
110
+ .spyderproject
111
+ .spyproject
112
+
113
+ # Rope project settings
114
+ .ropeproject
115
+
116
+ # mkdocs documentation
117
+ /site
118
+
119
+ # mypy
120
+ .mypy_cache/
121
+ .dmypy.json
122
+ dmypy.json
123
+
124
+ # Pyre type checker
125
+ .pyre/
126
+
127
+ # VSCode
128
+ .vscode/
129
+
130
+ # JetBrains IDEs
131
+ .idea/
132
+
133
+ # uv
134
+ uv.lock
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-04-22
4
+ ### Added
5
+ - Initial release: Hierarchical tree-based regressor using path-weighted Wilson means (95% trimmed) for robust predictions (e.g., parcel sale prices).
6
+ - NaN handling: Categorical NaNs as distinct "NaN" category (`fillna("NaN").unique()`); numeric NaNs excluded from splits via `notna()` masks (per SPEC.md); target `y` must be finite (raises `ValueError`).
7
+ - Scikit-learn compliance: `BaseEstimator`/`RegressorMixin`; works with `Pipeline`, `GridSearchCV`, `cross_val_score`, pickling; partial `check_estimator` pass (intentional NaN trade-off).
8
+ - Development: Full type hints (`py.typed`, mypy-ready), 16+ unittest/pytest tests (splits/NaN/explain/pickle/sklearn), `examples/quickstart.py` (MAE ~127k), `src/` layout, Hatchling build, dev deps (ruff/black/mypy).
9
+
10
+ Future releases will include Sphinx docs, benchmarks (vs XGBoost/LinearR), CI/CD.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
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,48 @@
1
+ # Layered Comp Model
2
+
3
+ # Overview:
4
+
5
+ The idea is to build "hierarchical" predictions that start with a general predicted price, then refine the prediction to be more specific by adding information and narrowing the comparison group.
6
+
7
+ You take the "Wilson mean" of all your parcels, then you find a filter that splits them into the best submarkets you can find and produce a "child node" for each variant of that filter (using a one-vs-rest approach for categorical data). Then you repeat until you've filtered down to a single data point. The value is a weighted average that prioritizes comparing well against closer matches and comparing slightly less well against further matches.
8
+
9
+ To get the predictions back out, you find the most specific bucket your subject matches, then you trace its path up the tree, taking and weighting the Wilson means as you go.
10
+
11
+ # Method:
12
+
13
+ ## Training
14
+
15
+ 1. Build a tree.
16
+ 2. Plot sale prices.
17
+ 3. Take the Wilson mean: This is defined as the mean after trimming the top 2.5% and the bottom 2.5% (the middle 95%).
18
+ 4. Find a set of filters to use (segmentation score based on weighted MAE reduction (MAE of sale prices vs the mean of the subset)).
19
+ 5. Make child nodes (one-vs-rest for categorical, or binary split for numeric using binary search for the breakpoint). We choose the split that results in the lowest ratio of weighted child MAE to parent MAE.
20
+ 6. Repeat from step 3 until we've filtered down to a single parcel (leaf node) or cannot split further (minimum node size = 2).
21
+
22
+ ## Predicting
23
+
24
+ 1. Find the node furthest down in the hierarchy that matches your parcel.
25
+ 2. Note its Wilson mean and the Wilson means of all nodes above it in the hierarchy.
26
+ 3. Calculate weights for each node in the path:
27
+ - Use the formula $w(x)=(1−x)^{weight\_falloff}$.
28
+ - $x$ is normalized from 0 to 1, evenly spaced by the depth of the node.
29
+ - $x = 0$ for the most specific (leaf) node.
30
+ - $x = 1$ for the root node.
31
+ 4. Take the weighted average of the Wilson means.
32
+ 5. There's your prediction.
33
+
34
+ # Hyperparameters
35
+
36
+ weight_falloff: 0 to 1. Will be used in w(x)=(1−x)^weight_falloff where x is normalized from 0 to 1
37
+
38
+ # Nuances:
39
+
40
+ The Wilson Mean keeps the prediction from going too crazy on the large sets, and it also penalizes the small sets so when the test set gets specific, the value won't swing wildly.
41
+
42
+ No parcel will get mapped to its own sale price because the weight falloff of the means will add some noise to it, in the direction of the broader market.
43
+
44
+ Every parcel should compare well because this model is fundamentally doing a hierarchical version of comp analysis to determine the value.
45
+
46
+ If a predicted parcel has a feature that wasn't in the training set, that particular level of nuance will be missed, but the parcel will still slot into a node slightly higher up the tree, so the model should still perform reasonably well even for things we don't have representative sales for.
47
+
48
+ The function that the weighted medians follows will determine a lot of how this model handles accuracy vs equity. A fast falloff will give good accuracy but may miss broader market trends. A slow falloff will promote "normativity" in predictions, but may miss market nuance and not assign correct values to particularly rare but valuable features.
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: layeredcompmodel
3
+ Version: 0.1.0
4
+ Summary: Hierarchical tree-based model for robust parcel sale price predictions using weighted Wilson means.
5
+ Project-URL: Homepage, https://github.com/JohnKossa/layeredcompmodel
6
+ Project-URL: Repository, https://github.com/JohnKossa/layeredcompmodel.git
7
+ Project-URL: Bug Tracker, https://github.com/JohnKossa/layeredcompmodel/issues
8
+ Author: John Kossa
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Your Name
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: hierarchical-model,real-estate,regression,scikit-learn
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
42
+ Requires-Python: >=3.10
43
+ Requires-Dist: numpy>=1.24.0
44
+ Requires-Dist: pandas>=2.0.0
45
+ Requires-Dist: scikit-learn>=1.3.0
46
+ Requires-Dist: scipy>=1.10.0
47
+ Provides-Extra: dev
48
+ Requires-Dist: black; extra == 'dev'
49
+ Requires-Dist: build; extra == 'dev'
50
+ Requires-Dist: mypy; extra == 'dev'
51
+ Requires-Dist: pytest-cov; extra == 'dev'
52
+ Requires-Dist: pytest>=7.0; extra == 'dev'
53
+ Requires-Dist: ruff; extra == 'dev'
54
+ Requires-Dist: scikit-learn[dev]; extra == 'dev'
55
+ Description-Content-Type: text/markdown
56
+
57
+ # LayeredCompModel
58
+
59
+ [![PyPI version](https://badge.fury.io/py/layeredcompmodel.svg)](https://pypi.org/project/layeredcompmodel/)
60
+ [![Documentation Status](https://readthedocs.org/projects/layeredcompmodel/badge/?version=latest)](https://layeredcompmodel.readthedocs.io/en/latest/?badge=latest)
61
+ [![Tests](https://github.com/JohnKossa/layeredcompmodel/actions/workflows/ci.yml/badge.svg)](https://github.com/JohnKossa/layeredcompmodel/actions)
62
+ [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
63
+
64
+ Hierarchical tree-based regressor for robust predictions (e.g., parcel sale prices) using path-weighted Wilson means (95% trimmed means for outlier resistance).
65
+
66
+ * [MODEL_SPEC.md](MODEL_SPEC.md): High-level method.
67
+ * [SPEC.md](SPEC.md): Detailed implementation specs.
68
+
69
+ ## Features
70
+
71
+ - **Scikit-learn compatible**: Inherits `BaseEstimator`/`RegressorMixin`; works with `Pipeline`, `GridSearchCV`, `cross_val_score`, pickling.
72
+ - **Automatic feature handling**: Categorical (one-vs-rest splits), numeric (binary search breakpoints), NaNs/missing values.
73
+ - **Robust statistics**: Wilson means prevent outlier swings.
74
+ - **Configurable weighting**: `weight_falloff` balances local accuracy vs. market normativity.
75
+ - **Explainable**: `explain_value(row)` shows path, weights, means.
76
+ - **Serializable**: `to_json()`, `to_dict()`.
77
+ - **Parallel**: `n_jobs` support.
78
+
79
+ ### NaN Handling
80
+ - **Categorical**: Treated as distinct "NaN" category.
81
+ - **Numeric**: Excluded from splits (robust; per SPEC.md).
82
+ - **Target `y`**: Must be finite (raises `ValueError`).
83
+ - Strict checks: Use `Pipeline([('imputer', SimpleImputer()), ('model', LayeredCompModel())])`.
84
+
85
+ ## Installation
86
+
87
+ ```bash
88
+ pip install layeredcompmodel
89
+ ```
90
+
91
+ For development:
92
+
93
+ ```bash
94
+ git clone https://github.com/JohnKossa/layeredcompmodel.git
95
+ cd layeredcompmodel
96
+ pip install -e .[dev]
97
+ ```
98
+
99
+ ## Quickstart
100
+
101
+ ```python
102
+ import pandas as pd
103
+ import numpy as np
104
+ from layeredcompmodel import LayeredCompModel
105
+
106
+ # Synthetic real-estate-like data
107
+ rng = np.random.default_rng(42)
108
+ n_samples = 100
109
+ data = {
110
+ 'neighborhood': rng.choice(['North', 'South', 'East'], n_samples),
111
+ 'size_sqft': rng.normal(2000, 500, n_samples),
112
+ 'price': rng.normal(500000, 100000, n_samples) + 100 * rng.normal(0, 1, n_samples) * (rng.normal(0, 1, n_samples) * 2000)
113
+ }
114
+ df = pd.DataFrame(data)
115
+ X = df[['neighborhood', 'size_sqft']]
116
+ y = df['price']
117
+
118
+ # Train
119
+ model = LayeredCompModel(weight_falloff=0.8, n_jobs=1)
120
+ model.fit(X, y)
121
+
122
+ # Predict
123
+ predictions = model.predict(X)
124
+ print(f"Predictions shape: {predictions.shape}")
125
+ print(f"MAE: {np.mean(np.abs(predictions - y)):.0f}")
126
+
127
+ # Explain single prediction
128
+ explanation = model.explain_value(X.iloc[0:1].squeeze())
129
+ print(explanation)
130
+ ```
131
+
132
+ ## API Reference
133
+
134
+ ### LayeredCompModel(weight_falloff=0.5, split_metric='mae', n_jobs=1)
135
+
136
+ - `fit(X, y)`: Build tree from features `X` (DataFrame), target `y` (Series).
137
+ - `predict(X)`: Predict using path-weighted means.
138
+ - `explain_value(row)`: Dict with path nodes, depths, weights, wilson_means.
139
+ - `to_json(indent=4)`: JSON tree dump.
140
+ - `tree_`: Root `CompNode` (filter_col, filter_val, wilson_mean, children).
141
+
142
+ See [docs](https://layeredcompmodel.readthedocs.io) (TBD).
143
+
144
+ ## Examples
145
+
146
+ See [`examples/quickstart.py`](examples/quickstart.py) for a runnable example (code matches Quickstart above).
147
+
148
+ **Run it:**
149
+ ```bash
150
+ python examples/quickstart.py
151
+ ```
152
+
153
+ **Expected output:**
154
+ ```
155
+ Predictions shape: (100,)
156
+ MAE: 126914
157
+ {'final_prediction': 530354.0426294187, 'weight_falloff': 0.8, 'path': [{'depth': 0, 'wilson_mean': 476353.91361128056, 'count': 100, 'is_leaf': False, 'filter_col': 'size_sqft', 'filter_val': 2101.366485546922}, {'depth': 1, 'wilson_mean': 553953.0606894617, 'count': 42, 'is_leaf': False, 'filter_col': 'neighborhood', 'filter_val': 'North'}, {'depth': 2, 'wilson_mean': 525096.3185716979, 'count': 13, 'is_leaf': True}], 'calculation': '0.199*476354 + 0.512*553953 + 0.289*525096 = 530354'}
158
+ ```
159
+
160
+ ## Development & Testing
161
+
162
+ ```bash
163
+ pytest tests/ --cov=layeredcompmodel
164
+ black src/
165
+ mypy src/
166
+ ```
167
+
168
+ CI/CD, Sphinx docs: planned.
169
+
170
+ ## Citing
171
+
172
+ Kossa, J. (2026). LayeredCompModel. GitHub. https://github.com/JohnKossa/layeredcompmodel
173
+
174
+ ## License
175
+
176
+ [MIT](LICENSE)
@@ -0,0 +1,120 @@
1
+ # LayeredCompModel
2
+
3
+ [![PyPI version](https://badge.fury.io/py/layeredcompmodel.svg)](https://pypi.org/project/layeredcompmodel/)
4
+ [![Documentation Status](https://readthedocs.org/projects/layeredcompmodel/badge/?version=latest)](https://layeredcompmodel.readthedocs.io/en/latest/?badge=latest)
5
+ [![Tests](https://github.com/JohnKossa/layeredcompmodel/actions/workflows/ci.yml/badge.svg)](https://github.com/JohnKossa/layeredcompmodel/actions)
6
+ [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ Hierarchical tree-based regressor for robust predictions (e.g., parcel sale prices) using path-weighted Wilson means (95% trimmed means for outlier resistance).
9
+
10
+ * [MODEL_SPEC.md](MODEL_SPEC.md): High-level method.
11
+ * [SPEC.md](SPEC.md): Detailed implementation specs.
12
+
13
+ ## Features
14
+
15
+ - **Scikit-learn compatible**: Inherits `BaseEstimator`/`RegressorMixin`; works with `Pipeline`, `GridSearchCV`, `cross_val_score`, pickling.
16
+ - **Automatic feature handling**: Categorical (one-vs-rest splits), numeric (binary search breakpoints), NaNs/missing values.
17
+ - **Robust statistics**: Wilson means prevent outlier swings.
18
+ - **Configurable weighting**: `weight_falloff` balances local accuracy vs. market normativity.
19
+ - **Explainable**: `explain_value(row)` shows path, weights, means.
20
+ - **Serializable**: `to_json()`, `to_dict()`.
21
+ - **Parallel**: `n_jobs` support.
22
+
23
+ ### NaN Handling
24
+ - **Categorical**: Treated as distinct "NaN" category.
25
+ - **Numeric**: Excluded from splits (robust; per SPEC.md).
26
+ - **Target `y`**: Must be finite (raises `ValueError`).
27
+ - Strict checks: Use `Pipeline([('imputer', SimpleImputer()), ('model', LayeredCompModel())])`.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install layeredcompmodel
33
+ ```
34
+
35
+ For development:
36
+
37
+ ```bash
38
+ git clone https://github.com/JohnKossa/layeredcompmodel.git
39
+ cd layeredcompmodel
40
+ pip install -e .[dev]
41
+ ```
42
+
43
+ ## Quickstart
44
+
45
+ ```python
46
+ import pandas as pd
47
+ import numpy as np
48
+ from layeredcompmodel import LayeredCompModel
49
+
50
+ # Synthetic real-estate-like data
51
+ rng = np.random.default_rng(42)
52
+ n_samples = 100
53
+ data = {
54
+ 'neighborhood': rng.choice(['North', 'South', 'East'], n_samples),
55
+ 'size_sqft': rng.normal(2000, 500, n_samples),
56
+ 'price': rng.normal(500000, 100000, n_samples) + 100 * rng.normal(0, 1, n_samples) * (rng.normal(0, 1, n_samples) * 2000)
57
+ }
58
+ df = pd.DataFrame(data)
59
+ X = df[['neighborhood', 'size_sqft']]
60
+ y = df['price']
61
+
62
+ # Train
63
+ model = LayeredCompModel(weight_falloff=0.8, n_jobs=1)
64
+ model.fit(X, y)
65
+
66
+ # Predict
67
+ predictions = model.predict(X)
68
+ print(f"Predictions shape: {predictions.shape}")
69
+ print(f"MAE: {np.mean(np.abs(predictions - y)):.0f}")
70
+
71
+ # Explain single prediction
72
+ explanation = model.explain_value(X.iloc[0:1].squeeze())
73
+ print(explanation)
74
+ ```
75
+
76
+ ## API Reference
77
+
78
+ ### LayeredCompModel(weight_falloff=0.5, split_metric='mae', n_jobs=1)
79
+
80
+ - `fit(X, y)`: Build tree from features `X` (DataFrame), target `y` (Series).
81
+ - `predict(X)`: Predict using path-weighted means.
82
+ - `explain_value(row)`: Dict with path nodes, depths, weights, wilson_means.
83
+ - `to_json(indent=4)`: JSON tree dump.
84
+ - `tree_`: Root `CompNode` (filter_col, filter_val, wilson_mean, children).
85
+
86
+ See [docs](https://layeredcompmodel.readthedocs.io) (TBD).
87
+
88
+ ## Examples
89
+
90
+ See [`examples/quickstart.py`](examples/quickstart.py) for a runnable example (code matches Quickstart above).
91
+
92
+ **Run it:**
93
+ ```bash
94
+ python examples/quickstart.py
95
+ ```
96
+
97
+ **Expected output:**
98
+ ```
99
+ Predictions shape: (100,)
100
+ MAE: 126914
101
+ {'final_prediction': 530354.0426294187, 'weight_falloff': 0.8, 'path': [{'depth': 0, 'wilson_mean': 476353.91361128056, 'count': 100, 'is_leaf': False, 'filter_col': 'size_sqft', 'filter_val': 2101.366485546922}, {'depth': 1, 'wilson_mean': 553953.0606894617, 'count': 42, 'is_leaf': False, 'filter_col': 'neighborhood', 'filter_val': 'North'}, {'depth': 2, 'wilson_mean': 525096.3185716979, 'count': 13, 'is_leaf': True}], 'calculation': '0.199*476354 + 0.512*553953 + 0.289*525096 = 530354'}
102
+ ```
103
+
104
+ ## Development & Testing
105
+
106
+ ```bash
107
+ pytest tests/ --cov=layeredcompmodel
108
+ black src/
109
+ mypy src/
110
+ ```
111
+
112
+ CI/CD, Sphinx docs: planned.
113
+
114
+ ## Citing
115
+
116
+ Kossa, J. (2026). LayeredCompModel. GitHub. https://github.com/JohnKossa/layeredcompmodel
117
+
118
+ ## License
119
+
120
+ [MIT](LICENSE)
@@ -0,0 +1,76 @@
1
+ # Goal
2
+
3
+ The goal of the Layered Comp Model is to create a hierarchical prediction system that starts with a general predicted price and refines it by adding information and narrowing the comparison group. This is achieved by building a tree of nodes, each representing a filtered subset of the data, and calculating a weighted average of the Wilson means to produce a final prediction. The model aims to balance accuracy and equity by penalizing large sets and promoting normativity in predictions. The implementation should be a scikit-learn compatible estimator.
4
+
5
+ More details can be found in MODEL_SPEC.md
6
+
7
+ # Tech Stack
8
+
9
+ * pandas
10
+ * python
11
+ * numpy
12
+ * scipy
13
+ * scikit-learn
14
+
15
+ # Key Processes
16
+
17
+ ## Training
18
+
19
+ ### Accepts
20
+ A pandas dataframe
21
+
22
+ A target prediction field
23
+
24
+ A list of columns to use
25
+
26
+ A list of columns to use
27
+
28
+ ### Produces
29
+
30
+ A trained scikit-learn compatible model object (e.g., `LayeredCompModel`) with `fit` and `predict` methods.
31
+
32
+ ## Prediction
33
+
34
+ ### Accepts
35
+ A pandas dataframe
36
+
37
+ A weight_falloff hyperparameter.
38
+
39
+ ### Produces
40
+
41
+ A "prediction" field decorated on the dataframe and returns it.
42
+
43
+ # Variable classification
44
+
45
+ We will need to be able to determine whether a column is numeric or categorical so the correct segmentation test can be applied.
46
+
47
+ # Segmentation Scoring
48
+
49
+ Uses a simple linear regression under the hood to fit sale price vs mean and evaluates the split quality by calculating the reduction in Mean Absolute Error (MAE).
50
+
51
+ 1. Calculate the base MAE for the current set of data by fitting a linear regression (sale price vs mean) and calculating the MAE ($MAE_{total}$).
52
+
53
+ ## Categorical
54
+
55
+ 1. For each variant in the categorical (one-vs-rest), treating missing values as a distinct category:
56
+ 1. filter to only that variant, calculate its MAE vs its mean ($MAE_v$), and get its count ($N_v$).
57
+ 2. filter to the inverse of that variant, calculate its MAE vs its mean ($MAE_{inv}$), and get its count ($N_{inv}$).
58
+ 3. Calculate the weighted MAE for the split: $MAE_{weighted} = (MAE_v \times N_v + MAE_{inv} \times N_{inv}) / N_{total}$.
59
+ 4. The segmentation score is the ratio: $Score = MAE_{weighted} / MAE_{total}$.
60
+ 2. Find the lowest segmentation score among all variants. In case of ties, choose the variant that splits the count most evenly. If still tied, choose the first one.
61
+
62
+ ## Numeric
63
+ 1. Exclude NaNs from numeric features during this process.
64
+ 2. Set num_iterations to minimum of 10 and log2(current population size).
65
+ 3. Perform a binary search for an optimal breakpoint:
66
+ 1. Set the initial midpoint to the median of the feature values.
67
+ 2. filter to the entries below the midpoint, calculate its MAE ($MAE_{low}$) and count ($N_{low}$).
68
+ 3. filter to the entries above the midpoint, calculate its MAE ($MAE_{high}$) and count ($N_{high}$).
69
+ 4. Calculate the weighted MAE for the split: $MAE_{weighted} = (MAE_{low} \times N_{low} + MAE_{high} \times N_{high}) / N_{total}$.
70
+ 5. The segmentation score is the ratio: $Score = MAE_{weighted} / MAE_{total}$.
71
+ 6. Move the midpoint to the side that resulted in a better scoring split (lower ratio) and reform the "above" and "below" subsets for each step of the search.
72
+ 4. Return the lowest segmentation score and corresponding midpoint found. In case of ties, choose the split that splits the count most evenly. If still tied, choose the first one.
73
+
74
+ # Node Size Constraints
75
+
76
+ Minimum node size is 2. Do not attempt to split a node if its size is below this threshold.
@@ -0,0 +1,28 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ from layeredcompmodel import LayeredCompModel
4
+
5
+ # Synthetic real-estate-like data
6
+ rng = np.random.default_rng(42)
7
+ n_samples = 100
8
+ data = {
9
+ 'neighborhood': rng.choice(['North', 'South', 'East'], n_samples),
10
+ 'size_sqft': rng.normal(2000, 500, n_samples),
11
+ 'price': rng.normal(500000, 100000, n_samples) + 100 * rng.normal(0, 1, n_samples) * (rng.normal(0, 1, n_samples) * 2000)
12
+ }
13
+ df = pd.DataFrame(data)
14
+ X = df[['neighborhood', 'size_sqft']]
15
+ y = df['price']
16
+
17
+ # Train
18
+ model = LayeredCompModel(weight_falloff=0.8, n_jobs=1)
19
+ model.fit(X, y)
20
+
21
+ # Predict
22
+ predictions = model.predict(X)
23
+ print(f"Predictions shape: {predictions.shape}")
24
+ print(f"MAE: {np.mean(np.abs(predictions - y)):.0f}")
25
+
26
+ # Explain single prediction
27
+ explanation = model.explain_value(X.iloc[0:1].squeeze())
28
+ print(explanation)