tppis 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.
tppis-0.1.0/.gitignore ADDED
@@ -0,0 +1,43 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg-info/
7
+ .eggs/
8
+ dist/
9
+ build/
10
+ .venv/
11
+ venv/
12
+ .env
13
+
14
+ # Tests and typing
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .coverage
19
+ htmlcov/
20
+ .hypothesis/
21
+
22
+ # Editors
23
+ .idea/
24
+ .vscode/
25
+ *.swp
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Local data caches (real datasets are fetched, not shipped)
32
+ .tppis_data/
33
+
34
+ # Working notes and paper copies, not part of the library
35
+ discussions/
36
+ research_paper_pdf/
37
+ yet_to_do.md
38
+ test_results/
39
+ tools/
40
+
41
+ # Simulation outputs
42
+ reproduction/results/*
43
+ !reproduction/results/.gitkeep
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2026-09-26
8
+
9
+ ### Added
10
+
11
+ - SIS, FPSIS, FPSIS-BIC, PPIS and TPPIS estimators implementing Tanaka and Matsui (2023).
12
+ - Shared spectral kernel, BIC-type criterion, and joint grid search over `d`, `alpha` and `k`.
13
+ - Simulation generators for the paper's Examples 1 to 4, plus screening metrics.
14
+ - scikit-learn compatible `fit` / `transform` / `get_support` API.
15
+
@@ -0,0 +1,25 @@
1
+ cff-version: 1.2.0
2
+ title: tppis
3
+ message: If you use this software, please cite both the method paper and this package.
4
+ type: software
5
+ authors:
6
+ - family-names: M
7
+ given-names: Mohammed Aswath
8
+ email: mohammed.aswath07@gmail.com
9
+ repository-code: https://github.com/Mohammed-Aswath/tppis
10
+ license: MIT
11
+ version: 0.1.0
12
+ abstract: >
13
+ Open-source Python implementation of Truncated Preconditioned Profiled
14
+ Independence Screening (TPPIS) as specified by Tanaka and Matsui (2023).
15
+ references:
16
+ - type: article
17
+ title: Variable screening using factor analysis for high-dimensional data with multicollinearity
18
+ authors:
19
+ - family-names: Tanaka
20
+ given-names: Shuntaro
21
+ - family-names: Matsui
22
+ given-names: Hidetoshi
23
+ year: 2023
24
+ doi: 10.48550/arXiv.2306.05702
25
+ url: https://arxiv.org/abs/2306.05702
tppis-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammed Aswath M
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.
tppis-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.5
2
+ Name: tppis
3
+ Version: 0.1.0
4
+ Summary: Truncated Preconditioned Profiled Independence Screening for high-dimensional data with multicollinearity.
5
+ Project-URL: Homepage, https://github.com/Mohammed-Aswath/tppis
6
+ Project-URL: Documentation, https://github.com/Mohammed-Aswath/tppis
7
+ Project-URL: Source, https://github.com/Mohammed-Aswath/tppis
8
+ Project-URL: Issues, https://github.com/Mohammed-Aswath/tppis/issues
9
+ Author-email: Mohammed Aswath M <mohammed.aswath07@gmail.com>
10
+ Maintainer-email: Mohammed Aswath M <mohammed.aswath07@gmail.com>
11
+ License: MIT License
12
+
13
+ Copyright (c) 2026 Mohammed Aswath M
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ License-File: LICENSE
33
+ Keywords: SIS,TPPIS,factor-analysis,feature-selection,high-dimensional,multicollinearity,variable-screening
34
+ Classifier: Development Status :: 3 - Alpha
35
+ Classifier: Intended Audience :: Science/Research
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
43
+ Classifier: Typing :: Typed
44
+ Requires-Python: >=3.10
45
+ Requires-Dist: numpy>=1.24
46
+ Requires-Dist: scikit-learn>=1.6
47
+ Requires-Dist: scipy>=1.10
48
+ Provides-Extra: datasets
49
+ Requires-Dist: pandas>=2.0; extra == 'datasets'
50
+ Provides-Extra: dev
51
+ Requires-Dist: hypothesis>=6.82; extra == 'dev'
52
+ Requires-Dist: mypy>=1.8; extra == 'dev'
53
+ Requires-Dist: pandas>=2.0; extra == 'dev'
54
+ Requires-Dist: pre-commit>=3.6; extra == 'dev'
55
+ Requires-Dist: pytest-cov>=4.1; extra == 'dev'
56
+ Requires-Dist: pytest>=7.4; extra == 'dev'
57
+ Requires-Dist: ruff>=0.5; extra == 'dev'
58
+ Provides-Extra: docs
59
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
60
+ Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
61
+ Provides-Extra: repro
62
+ Requires-Dist: joblib>=1.3; extra == 'repro'
63
+ Requires-Dist: matplotlib>=3.7; extra == 'repro'
64
+ Requires-Dist: pandas>=2.0; extra == 'repro'
65
+ Description-Content-Type: text/markdown
66
+
67
+ # tppis
68
+
69
+ Open-source Python implementation of **Truncated Preconditioned Profiled Independence Screening** (TPPIS) from Tanaka and Matsui (2023), plus the baselines SIS, FPSIS (and FPSIS-BIC), and PPIS.
70
+
71
+ The method screens variables in high-dimensional regression when predictors are strongly multicollinear. A factor-analysis transform removes the common factors; TPPIS then *truncates* the tail of that transform so the unique-factor signal is not washed out.
72
+
73
+ **Maintainer:** [Mohammed Aswath M](https://github.com/Mohammed-Aswath) · [mohammed.aswath07@gmail.com](mailto:mohammed.aswath07@gmail.com) · issues: [github.com/Mohammed-Aswath/tppis](https://github.com/Mohammed-Aswath/tppis/issues)
74
+
75
+ ## Install
76
+
77
+ ```bash
78
+ pip install tppis
79
+ ```
80
+
81
+ The package requires Python 3.10+, NumPy, SciPy, and **scikit-learn 1.6+**.
82
+
83
+ From a clone of this repository:
84
+
85
+ ```bash
86
+ python -m venv .venv
87
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
88
+ pip install -e ".[dev]"
89
+ ```
90
+
91
+ Requires Python 3.10+, NumPy, SciPy, and scikit-learn 1.6+.
92
+
93
+ ## Quickstart
94
+
95
+ ```python
96
+ from tppis import TPPIS, make_example1
97
+
98
+ data = make_example1(n=100, p=80, phi=0.7, seed=0)
99
+ est = TPPIS().fit(data.X, data.y)
100
+
101
+ est.get_support(indices=True) # selected columns, ranking order
102
+ est.d_, est.alpha_, est.k_ # BIC-chosen parameters
103
+ est.bic_
104
+ est.grid_ # every (d, alpha, k, bic) evaluated
105
+ ```
106
+
107
+ Fixed parameters, no search:
108
+
109
+ ```python
110
+ TPPIS(d=20, alpha=0.4, k=10).fit(data.X, data.y)
111
+ ```
112
+
113
+ Inside a scikit-learn pipeline:
114
+
115
+ ```python
116
+ from sklearn.linear_model import LinearRegression
117
+ from sklearn.pipeline import Pipeline
118
+ from tppis import TPPIS
119
+
120
+ pipe = Pipeline([
121
+ ("select", TPPIS(d=8, alpha=0.6, k=10)),
122
+ ("lm", LinearRegression()),
123
+ ])
124
+ pipe.fit(data.X, data.y)
125
+ ```
126
+
127
+ The functional entry point `screen(X, y, method="tppis")` returns the same fitted estimator.
128
+
129
+ ## When to use which method
130
+
131
+ | Method | Transform | How `d` is chosen |
132
+ | --- | --- | --- |
133
+ | SIS | none (`X.T @ y`) | — |
134
+ | FPSIS | project out leading `d` factors | singular-value ratio (3) |
135
+ | FPSIS-BIC | same | BIC grid |
136
+ | PPIS | Puffer whitening of the tail | singular-value ratio (3) |
137
+ | TPPIS | PPIS with the tail truncated at `[n α]` | joint BIC over `d`, `α`, `k` |
138
+
139
+ TPPIS at `alpha=1` is exactly PPIS. That identity is tested.
140
+
141
+ ## Citation
142
+
143
+ If you use the method, cite the paper. If you use this software, cite the package as well.
144
+
145
+ ```
146
+ Tanaka, S. and Matsui, H. (2023). Variable screening using factor analysis
147
+ for high-dimensional data with multicollinearity. arXiv:2306.05702.
148
+ ```
149
+
150
+ See [`CITATION.cff`](CITATION.cff).
151
+
152
+ ## License
153
+
154
+ MIT. See [`LICENSE`](LICENSE).
tppis-0.1.0/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # tppis
2
+
3
+ Open-source Python implementation of **Truncated Preconditioned Profiled Independence Screening** (TPPIS) from Tanaka and Matsui (2023), plus the baselines SIS, FPSIS (and FPSIS-BIC), and PPIS.
4
+
5
+ The method screens variables in high-dimensional regression when predictors are strongly multicollinear. A factor-analysis transform removes the common factors; TPPIS then *truncates* the tail of that transform so the unique-factor signal is not washed out.
6
+
7
+ **Maintainer:** [Mohammed Aswath M](https://github.com/Mohammed-Aswath) · [mohammed.aswath07@gmail.com](mailto:mohammed.aswath07@gmail.com) · issues: [github.com/Mohammed-Aswath/tppis](https://github.com/Mohammed-Aswath/tppis/issues)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install tppis
13
+ ```
14
+
15
+ The package requires Python 3.10+, NumPy, SciPy, and **scikit-learn 1.6+**.
16
+
17
+ From a clone of this repository:
18
+
19
+ ```bash
20
+ python -m venv .venv
21
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
22
+ pip install -e ".[dev]"
23
+ ```
24
+
25
+ Requires Python 3.10+, NumPy, SciPy, and scikit-learn 1.6+.
26
+
27
+ ## Quickstart
28
+
29
+ ```python
30
+ from tppis import TPPIS, make_example1
31
+
32
+ data = make_example1(n=100, p=80, phi=0.7, seed=0)
33
+ est = TPPIS().fit(data.X, data.y)
34
+
35
+ est.get_support(indices=True) # selected columns, ranking order
36
+ est.d_, est.alpha_, est.k_ # BIC-chosen parameters
37
+ est.bic_
38
+ est.grid_ # every (d, alpha, k, bic) evaluated
39
+ ```
40
+
41
+ Fixed parameters, no search:
42
+
43
+ ```python
44
+ TPPIS(d=20, alpha=0.4, k=10).fit(data.X, data.y)
45
+ ```
46
+
47
+ Inside a scikit-learn pipeline:
48
+
49
+ ```python
50
+ from sklearn.linear_model import LinearRegression
51
+ from sklearn.pipeline import Pipeline
52
+ from tppis import TPPIS
53
+
54
+ pipe = Pipeline([
55
+ ("select", TPPIS(d=8, alpha=0.6, k=10)),
56
+ ("lm", LinearRegression()),
57
+ ])
58
+ pipe.fit(data.X, data.y)
59
+ ```
60
+
61
+ The functional entry point `screen(X, y, method="tppis")` returns the same fitted estimator.
62
+
63
+ ## When to use which method
64
+
65
+ | Method | Transform | How `d` is chosen |
66
+ | --- | --- | --- |
67
+ | SIS | none (`X.T @ y`) | — |
68
+ | FPSIS | project out leading `d` factors | singular-value ratio (3) |
69
+ | FPSIS-BIC | same | BIC grid |
70
+ | PPIS | Puffer whitening of the tail | singular-value ratio (3) |
71
+ | TPPIS | PPIS with the tail truncated at `[n α]` | joint BIC over `d`, `α`, `k` |
72
+
73
+ TPPIS at `alpha=1` is exactly PPIS. That identity is tested.
74
+
75
+ ## Citation
76
+
77
+ If you use the method, cite the paper. If you use this software, cite the package as well.
78
+
79
+ ```
80
+ Tanaka, S. and Matsui, H. (2023). Variable screening using factor analysis
81
+ for high-dimensional data with multicollinearity. arXiv:2306.05702.
82
+ ```
83
+
84
+ See [`CITATION.cff`](CITATION.cff).
85
+
86
+ ## License
87
+
88
+ MIT. See [`LICENSE`](LICENSE).
@@ -0,0 +1,127 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tppis"
7
+ version = "0.1.0"
8
+ description = "Truncated Preconditioned Profiled Independence Screening for high-dimensional data with multicollinearity."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Mohammed Aswath M", email = "mohammed.aswath07@gmail.com" },
14
+ ]
15
+ maintainers = [
16
+ { name = "Mohammed Aswath M", email = "mohammed.aswath07@gmail.com" },
17
+ ]
18
+ keywords = [
19
+ "feature-selection",
20
+ "variable-screening",
21
+ "high-dimensional",
22
+ "multicollinearity",
23
+ "factor-analysis",
24
+ "SIS",
25
+ "TPPIS",
26
+ ]
27
+ classifiers = [
28
+ "Development Status :: 3 - Alpha",
29
+ "Intended Audience :: Science/Research",
30
+ "License :: OSI Approved :: MIT License",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
36
+ "Topic :: Scientific/Engineering :: Mathematics",
37
+ "Typing :: Typed",
38
+ ]
39
+ dependencies = [
40
+ "numpy>=1.24",
41
+ "scipy>=1.10",
42
+ "scikit-learn>=1.6",
43
+ ]
44
+
45
+ [project.optional-dependencies]
46
+ datasets = [
47
+ "pandas>=2.0",
48
+ ]
49
+ repro = [
50
+ "pandas>=2.0",
51
+ "matplotlib>=3.7",
52
+ "joblib>=1.3",
53
+ ]
54
+ dev = [
55
+ "pytest>=7.4",
56
+ "pytest-cov>=4.1",
57
+ "hypothesis>=6.82",
58
+ "ruff>=0.5",
59
+ "mypy>=1.8",
60
+ "pre-commit>=3.6",
61
+ "pandas>=2.0",
62
+ ]
63
+ docs = [
64
+ "mkdocs-material>=9.5",
65
+ "mkdocstrings[python]>=0.24",
66
+ ]
67
+
68
+ [project.urls]
69
+ Homepage = "https://github.com/Mohammed-Aswath/tppis"
70
+ Documentation = "https://github.com/Mohammed-Aswath/tppis"
71
+ Source = "https://github.com/Mohammed-Aswath/tppis"
72
+ Issues = "https://github.com/Mohammed-Aswath/tppis/issues"
73
+
74
+ [tool.hatch.build.targets.sdist]
75
+ include = [
76
+ "/tppis",
77
+ "/tests",
78
+ "/README.md",
79
+ "/LICENSE",
80
+ "/CHANGELOG.md",
81
+ "/CITATION.cff",
82
+ ]
83
+
84
+ [tool.hatch.build.targets.wheel]
85
+ packages = ["tppis"]
86
+
87
+ [tool.ruff]
88
+ line-length = 88
89
+ target-version = "py310"
90
+ src = ["tppis", "tests"]
91
+
92
+ [tool.ruff.lint]
93
+ select = ["E", "F", "I", "UP", "B", "SIM"]
94
+
95
+ [tool.ruff.format]
96
+ quote-style = "double"
97
+
98
+ [tool.mypy]
99
+ python_version = "3.12"
100
+ strict = true
101
+ packages = ["tppis"]
102
+ warn_unused_ignores = true
103
+
104
+ [[tool.mypy.overrides]]
105
+ module = ["sklearn.*", "scipy.*"]
106
+ ignore_missing_imports = true
107
+
108
+ [tool.pytest.ini_options]
109
+ testpaths = ["tests"]
110
+ addopts = "-q --strict-markers"
111
+ filterwarnings = [
112
+ "ignore::UserWarning",
113
+ "ignore::FutureWarning",
114
+ "ignore::DeprecationWarning",
115
+ ]
116
+
117
+ [tool.coverage.run]
118
+ source = ["tppis"]
119
+ branch = true
120
+
121
+ [tool.coverage.report]
122
+ fail_under = 90
123
+ show_missing = true
124
+ exclude_lines = [
125
+ "pragma: no cover",
126
+ "if TYPE_CHECKING:",
127
+ ]
File without changes
@@ -0,0 +1,39 @@
1
+ """Shared fixtures.
2
+
3
+ Every random draw here is seeded explicitly, so that a failure is reproducible from the
4
+ test name alone.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+ import pytest
11
+
12
+
13
+ def multicollinear_design(n_samples, n_features, n_factors=3, seed=0):
14
+ """A design with a few strong common factors above a noise floor."""
15
+ rng = np.random.default_rng(seed)
16
+ Z = rng.standard_normal((n_samples, n_factors))
17
+ B = rng.standard_normal((n_factors, n_features))
18
+ return Z @ B + rng.standard_normal((n_samples, n_features))
19
+
20
+
21
+ @pytest.fixture
22
+ def small_problem():
23
+ """A wide, strongly multicollinear problem with a known active set."""
24
+ rng = np.random.default_rng(11)
25
+ X = multicollinear_design(40, 120, seed=11)
26
+ X = (X - X.mean(axis=0)) / X.std(axis=0)
27
+ y = 5.0 * X[:, 0] - 4.0 * X[:, 1] + 3.0 * X[:, 2] + 0.1 * rng.standard_normal(40)
28
+ return X, y - y.mean()
29
+
30
+
31
+ @pytest.fixture(params=[(20, 60), (30, 30), (40, 15)], ids=["wide", "square", "tall"])
32
+ def shaped_problem(request):
33
+ """The same problem across the three shape regimes n < p, n == p and n > p."""
34
+ n_samples, n_features = request.param
35
+ rng = np.random.default_rng(hash(request.param) % 2**32)
36
+ X = multicollinear_design(n_samples, n_features, n_factors=2, seed=3)
37
+ X = (X - X.mean(axis=0)) / X.std(axis=0)
38
+ y = 4.0 * X[:, 0] - 3.0 * X[:, 1] + 0.2 * rng.standard_normal(n_samples)
39
+ return X, y - y.mean()
@@ -0,0 +1,103 @@
1
+ """Literal transcription of equations (4), (6), (7), (8) and (10).
2
+
3
+ These routines form the explicit n-by-n projection matrices printed in the
4
+ paper. They are the yardstick for the fast path, not a public API.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+ from numpy.typing import NDArray
11
+
12
+
13
+ def svd_parts(
14
+ X: NDArray[np.float64],
15
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
16
+ U, mu, Vt = np.linalg.svd(X, full_matrices=False)
17
+ return U, mu, Vt.T
18
+
19
+
20
+ def q_f(U1: NDArray[np.float64]) -> NDArray[np.float64]:
21
+ """Equation (4): ``Q_F = I - U1 (U1.T U1)^{-1} U1.T``."""
22
+ n = U1.shape[0]
23
+ if U1.shape[1] == 0:
24
+ return np.eye(n)
25
+ gram = U1.T @ U1
26
+ return np.eye(n) - U1 @ np.linalg.inv(gram) @ U1.T
27
+
28
+
29
+ def q_p(
30
+ U1: NDArray[np.float64], U2: NDArray[np.float64], mu2: NDArray[np.float64]
31
+ ) -> NDArray[np.float64]:
32
+ """Equation (6) as printed, without an inverse on ``U1.T U1`` (A-3)."""
33
+ n = U1.shape[0]
34
+ if U2.shape[1] == 0:
35
+ return np.zeros((n, n))
36
+ tail = U2 @ np.diag(1.0 / mu2) @ U2.T
37
+ if U1.shape[1] == 0:
38
+ return tail
39
+ return tail @ (np.eye(n) - U1 @ (U1.T @ U1) @ U1.T)
40
+
41
+
42
+ def q_t(
43
+ U1: NDArray[np.float64], U2a: NDArray[np.float64], mu2a: NDArray[np.float64]
44
+ ) -> NDArray[np.float64]:
45
+ """Equation (7) as printed."""
46
+ return q_p(U1, U2a, mu2a)
47
+
48
+
49
+ def omega_from_q(
50
+ X: NDArray[np.float64],
51
+ y: NDArray[np.float64],
52
+ Q: NDArray[np.float64],
53
+ ) -> NDArray[np.float64]:
54
+ """``omega = (Q X).T @ (Q y)``."""
55
+ X_hat = Q @ X
56
+ y_hat = Q @ y
57
+ return X_hat.T @ y_hat
58
+
59
+
60
+ def sis_omega(X: NDArray[np.float64], y: NDArray[np.float64]) -> NDArray[np.float64]:
61
+ return X.T @ y
62
+
63
+
64
+ def fpsis_omega(
65
+ X: NDArray[np.float64],
66
+ y: NDArray[np.float64],
67
+ d: int,
68
+ ) -> NDArray[np.float64]:
69
+ U, _mu, _V = svd_parts(X)
70
+ return omega_from_q(X, y, q_f(U[:, :d]))
71
+
72
+
73
+ def ppis_omega(
74
+ X: NDArray[np.float64],
75
+ y: NDArray[np.float64],
76
+ d: int,
77
+ ) -> NDArray[np.float64]:
78
+ U, mu, _V = svd_parts(X)
79
+ return omega_from_q(X, y, q_p(U[:, :d], U[:, d:], mu[d:]))
80
+
81
+
82
+ def tppis_omega(
83
+ X: NDArray[np.float64],
84
+ y: NDArray[np.float64],
85
+ d: int,
86
+ m: int,
87
+ ) -> NDArray[np.float64]:
88
+ U, mu, _V = svd_parts(X)
89
+ return omega_from_q(X, y, q_t(U[:, :d], U[:, d:m], mu[d:m]))
90
+
91
+
92
+ def refit_beta(
93
+ X_hat_m: NDArray[np.float64],
94
+ y_hat: NDArray[np.float64],
95
+ ) -> NDArray[np.float64]:
96
+ """Equation (8): ``beta = (X_hat_M.T X_hat_M)^{-1} X_hat_M.T y_hat``."""
97
+ gram = X_hat_m.T @ X_hat_m
98
+ return np.linalg.solve(gram, X_hat_m.T @ y_hat)
99
+
100
+
101
+ def bic_paper(rss: float, k: int, n: int, p: int) -> float:
102
+ """Equation (10): natural log, unnormalized RSS (A-4)."""
103
+ return float(np.log(rss) + (np.log(p) / n) * k * np.log(n))
@@ -0,0 +1,59 @@
1
+ """Refit (8), BIC (10), and the analytic check of specification Section 5.1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ from tppis.criteria import bic_value, incremental_refit
7
+ from tppis.datasets import make_example1
8
+
9
+ from tests.reference import bic_paper, refit_beta
10
+
11
+
12
+ def test_bic_matches_reference_transcription() -> None:
13
+ got = bic_value(100.0, 4, 100, 1000, kind="paper")
14
+ assert got == bic_paper(100.0, 4, 100, 1000)
15
+
16
+
17
+ def test_section_51_analytic_check() -> None:
18
+ # RSS ~ n * sigma^2 = 100 at the true model; penalty ~ 1.272; BIC ~ 5.877.
19
+ value = bic_value(100.0, 4, 100, 1000, kind="paper")
20
+ assert 5.85 < value < 5.90
21
+
22
+
23
+ def test_incremental_matches_direct_solve() -> None:
24
+ rng = np.random.default_rng(0)
25
+ n, K, s = 30, 6, 8
26
+ F = rng.standard_normal((K, s))
27
+ X_sel = rng.standard_normal((n, K))
28
+ y = rng.standard_normal(n)
29
+ rhs = rng.standard_normal(K)
30
+ sweep = incremental_refit(F, rhs, X_sel, y, p=20, kind="paper")
31
+ for k in range(1, K + 1):
32
+ gram = F[:k] @ F[:k].T
33
+ beta_k = np.linalg.solve(gram, rhs[:k])
34
+ resid = y - X_sel[:, :k] @ beta_k
35
+ rss = float(resid @ resid)
36
+ np.testing.assert_allclose(sweep.rss[k - 1], rss, rtol=1e-10, atol=1e-10)
37
+ if k == sweep.best_k:
38
+ np.testing.assert_allclose(sweep.best_beta, beta_k, rtol=1e-10, atol=1e-10)
39
+
40
+
41
+ def test_direct_refit_matches_equation_8() -> None:
42
+ rng = np.random.default_rng(1)
43
+ n, k = 25, 4
44
+ X_hat = rng.standard_normal((n, k))
45
+ y_hat = rng.standard_normal(n)
46
+ beta = refit_beta(X_hat, y_hat)
47
+ gram = X_hat.T @ X_hat
48
+ np.testing.assert_allclose(gram @ beta, X_hat.T @ y_hat, rtol=1e-10, atol=1e-10)
49
+
50
+
51
+ def test_example1_true_model_bic_is_near_588() -> None:
52
+ data = make_example1(n=100, p=80, phi=0.5, seed=0)
53
+ # Residual of the true four-variable model on this one draw.
54
+ X4 = data.X[:, :4]
55
+ beta, *_ = np.linalg.lstsq(X4, data.y, rcond=None)
56
+ rss = float(np.sum((data.y - X4 @ beta) ** 2))
57
+ value = bic_value(rss, 4, 100, 80, kind="paper")
58
+ # One draw, not the expectation: just check the criterion is O(1) not O(10).
59
+ assert 3.0 < value < 9.0