spmkit-learn 1.0.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shota Inoue
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,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: spmkit-learn
3
+ Version: 1.0.0
4
+ Summary: Meta-Sparse Modeling Library for Chemical Data Prediction.
5
+ Author-email: Shota Inoue <inoue.shota@st.kitasato-u.ac.jp>
6
+ License: Proprietary
7
+ Classifier: License :: Other/Proprietary License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy<=1.26.4,>=1.25.2
18
+ Requires-Dist: scikit-learn==1.6.0
19
+ Requires-Dist: matplotlib>=3.7.0
20
+ Dynamic: license-file
21
+
22
+ # spmkit
23
+
24
+ **spmkit** is meta-sparse modeling library for chemical data prediction.
25
+
26
+ -----
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ # via wheel binary
32
+ pip install spmkit-***.whl
33
+
34
+ # via gzip source
35
+ pip install spmkit-***.tar.gz
36
+ ```
@@ -0,0 +1,15 @@
1
+ # spmkit
2
+
3
+ **spmkit** is meta-sparse modeling library for chemical data prediction.
4
+
5
+ -----
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # via wheel binary
11
+ pip install spmkit-***.whl
12
+
13
+ # via gzip source
14
+ pip install spmkit-***.tar.gz
15
+ ```
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "spmkit-learn"
7
+ version = "1.0.0"
8
+ description = "Meta-Sparse Modeling Library for Chemical Data Prediction."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+
12
+ authors = [
13
+ { name = "Shota Inoue", email = "inoue.shota@st.kitasato-u.ac.jp" }
14
+ ]
15
+
16
+ dependencies = [
17
+ "numpy >= 1.25.2, <= 1.26.4",
18
+ "scikit-learn == 1.6.0",
19
+ "matplotlib >= 3.7.0",
20
+ ]
21
+
22
+ license = { text = "Proprietary" }
23
+
24
+ classifiers = [
25
+ "License :: Other/Proprietary License",
26
+
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+
31
+ "Operating System :: OS Independent",
32
+
33
+ "Intended Audience :: Science/Research",
34
+
35
+ "Topic :: Scientific/Engineering :: Chemistry",
36
+ ]
37
+
38
+ [tool.setuptools.packages.find]
39
+ include = ["spmkit*"]
40
+
41
+ [tool.setuptools]
42
+ include-package-data = true
43
+
44
+ [tool.setuptools.package-data]
45
+ spmkit = [
46
+ "assets/*.ttf",
47
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ from . import models, plots
@@ -0,0 +1,4 @@
1
+ from .exhaustive_search import ExhaustiveSearch
2
+ from .stability_selection import StabilitySelection
3
+
4
+ __all__ = ["ExhaustiveSearch", "StabilitySelection"]
@@ -0,0 +1,314 @@
1
+ import multiprocessing as mp
2
+ from dataclasses import dataclass
3
+ from functools import partial
4
+ from itertools import combinations
5
+ from typing import Literal
6
+
7
+ import numpy as np
8
+ from sklearn.linear_model import LinearRegression
9
+ from sklearn.metrics import (
10
+ mean_squared_error,
11
+ )
12
+ from sklearn.model_selection import KFold
13
+ from tqdm.auto import tqdm
14
+
15
+
16
+ @dataclass
17
+ class Result:
18
+ score: float
19
+ weights: np.ndarray
20
+ feature: np.ndarray
21
+ model: LinearRegression | list[LinearRegression]
22
+
23
+
24
+ # 回帰器
25
+ def _regressor(combo, X, y, sort_criteria):
26
+ # 特徴量を切り出す
27
+ idx = np.array(combo)
28
+ X_slice = X[:, idx]
29
+
30
+ # モデル定義
31
+ model = LinearRegression()
32
+
33
+ # fit
34
+ model.fit(X_slice, y)
35
+
36
+ # predict
37
+ y_pred = model.predict(X_slice)
38
+
39
+ # score
40
+ score = (
41
+ _get_aic(X_slice, y, y_pred)
42
+ if sort_criteria == "aic"
43
+ else _get_bic(X_slice, y, y_pred)
44
+ )
45
+
46
+ # idx, coef は元の特徴量ベクトル (size p) へ埋め込む
47
+ weights = np.zeros(X.shape[1], dtype=float)
48
+ weights[idx] = model.coef_
49
+ feature = np.zeros(X.shape[1], dtype=int)
50
+ feature[idx] = 1
51
+
52
+ return Result(
53
+ score=score,
54
+ weights=weights,
55
+ feature=feature,
56
+ model=model,
57
+ )
58
+
59
+
60
+ def _regressor_cv(combo, X, y, split):
61
+ # 特徴量を切り出す
62
+ idx = np.array(combo)
63
+ X_slice = X[:, idx]
64
+
65
+ # モデル定義
66
+ model = LinearRegression()
67
+
68
+ # internal k-fold split
69
+ cves = []
70
+ coefs = []
71
+ models = []
72
+
73
+ for train_index, test_index in split:
74
+ # split
75
+ X_train, X_test = X_slice[train_index], X_slice[test_index]
76
+ y_train, y_test = y[train_index], y[test_index]
77
+
78
+ # fit
79
+ model.fit(X_train, y_train)
80
+
81
+ # save model
82
+ models.append(model)
83
+
84
+ # predict
85
+ y_pred = model.predict(X_test)
86
+
87
+ # compute cve
88
+ cve = mean_squared_error(y_test, y_pred)
89
+ cves.append(cve)
90
+
91
+ # coefs
92
+ coefs.append(model.coef_)
93
+
94
+ # fold 平均 CVE, coefs
95
+ cve = np.mean(cves)
96
+ coefs = np.mean(coefs)
97
+
98
+ # idx, coef は元の特徴量ベクトル (size p) へ埋め込む
99
+ weights = np.zeros(X.shape[1], dtype=float)
100
+ weights[idx] = coefs
101
+ feature = np.zeros(X.shape[1], dtype=int)
102
+ feature[idx] = 1
103
+
104
+ return Result(
105
+ score=cve,
106
+ weights=weights,
107
+ feature=feature,
108
+ model=models,
109
+ )
110
+
111
+
112
+ # スコア計算関数
113
+ def _get_aic(X, y, y_pred):
114
+ rss = np.sum((y - y_pred) ** 2)
115
+ n = len(y)
116
+ k = X.shape[1] + 1
117
+ aic = n * np.log(rss / n) + 2 * k
118
+ return aic
119
+
120
+
121
+ def _get_bic(X, y, y_pred):
122
+ rss = np.sum((y - y_pred) ** 2)
123
+ n = len(y)
124
+ k = X.shape[1] + 1
125
+ bic = n * np.log(rss / n) + k * np.log(n)
126
+ return bic
127
+
128
+
129
+ # exhaustive search
130
+ class ExhaustiveSearch:
131
+ """線形重回帰モデルの全探索
132
+
133
+ Parameters
134
+ ----------
135
+ sort_criteria : {"cve", "aic", "bic"}, default="cve"
136
+ 学習済みモデルをソートする基準 (交差検証誤差もしくは AIC, BIC)
137
+ random_state : int, default=42
138
+ ランダムシード
139
+ n_fold : int, default=5
140
+ 交差検証を用いる場合の分割数
141
+
142
+ Attributes
143
+ ----------
144
+ result_ : ndarray
145
+ 基準に基づてソートされた学習済みモデルの重みとスコア
146
+
147
+ """
148
+
149
+ def __init__(
150
+ self,
151
+ sort_criteria: Literal["cve", "aic", "bic"] = "cve",
152
+ random_state: int = 42,
153
+ n_fold: int = 5,
154
+ ):
155
+ if sort_criteria not in {"cve", "aic", "bic"}:
156
+ raise ValueError(
157
+ f"sort_criteria must be one of 'cve', 'aic', 'bic', got {sort_criteria!r}",
158
+ )
159
+
160
+ self.sort_criteria = sort_criteria
161
+ self.n_fold = n_fold
162
+ self.random_state = random_state
163
+
164
+ self.results_ = None
165
+ self._best_mask = None
166
+
167
+ # ランダムシード固定
168
+ np.random.seed(self.random_state)
169
+
170
+ def fit(self, X, y, n_jobs: int = -1):
171
+ """モデルを学習させる
172
+
173
+ Parameters
174
+ ----------
175
+ X : array-like of shape (n_samples, n_features)
176
+ 訓練データのデザイン行列
177
+ y : array-like of shape (n_samples,)
178
+ 教師ラベルの配列
179
+ n_jobs : int, default=-1
180
+ CPU プロセス数 (-1 を指定すると全ての CPU コアを消費)
181
+
182
+ Returns
183
+ -------
184
+ self
185
+ 学習済みインスタンス
186
+
187
+ """
188
+ # ndarray にキャスト
189
+ X, y = np.asarray(X), np.asarray(y)
190
+
191
+ # 全ての特徴量の組み合わせを生成する
192
+ combos = []
193
+ for k in range(1, X.shape[1] + 1):
194
+ combos.extend(combinations(range(X.shape[1]), k))
195
+
196
+ print(f"model sorting criteria to be used: {self.sort_criteria.upper()}")
197
+
198
+ if self.sort_criteria == "cve":
199
+ # k-fold
200
+ kf = KFold(
201
+ n_splits=self.n_fold,
202
+ random_state=self.random_state,
203
+ shuffle=True,
204
+ )
205
+ split = list(kf.split(X, y))
206
+ worker = partial(
207
+ _regressor_cv,
208
+ X=X,
209
+ y=y,
210
+ split=split,
211
+ )
212
+ else:
213
+ worker = partial(
214
+ _regressor,
215
+ X=X,
216
+ y=y,
217
+ sort_criteria=self.sort_criteria,
218
+ )
219
+
220
+ # 並列実行
221
+ with mp.Pool(processes=mp.cpu_count() if n_jobs == -1 else n_jobs) as pool:
222
+ results = list(
223
+ tqdm(
224
+ pool.imap_unordered(worker, combos),
225
+ total=len(combos),
226
+ desc="progress",
227
+ ),
228
+ )
229
+
230
+ # sort
231
+ results.sort(key=lambda res: res.score)
232
+ self.results_ = np.array(results)
233
+ return self
234
+
235
+ def get_scores(self):
236
+ """ソート済みのスコアを取得する
237
+
238
+ Returns
239
+ -------
240
+ ndarray of shape (2**n_features - 1,)
241
+ ソートされたスコアの配列
242
+
243
+ """
244
+ return np.array([res.score for res in self.results_])
245
+
246
+ def get_weights(self):
247
+ """基準に基づいてソートされた学習済みモデルの重みを取得する
248
+
249
+ Returns
250
+ -------
251
+ ndarray of shape (n_features, 2**n_features - 1)
252
+ ソートされた学習済みモデルの重み行列
253
+
254
+ """
255
+ return np.array([res.weights for res in self.results_]).T
256
+
257
+ def get_features(self):
258
+ """基準に基づいてソートされた、各特徴量の有無を表す One-hot ベクトルを取得する
259
+
260
+ Returns
261
+ -------
262
+ ndarray of shape (2**n_features - 1,)
263
+ 各特徴量の有無を表す One-hot ベクトルの配列
264
+
265
+ """
266
+ return np.array([res.feature for res in self.results_])
267
+
268
+ def predict(self, X, rank: int = 0):
269
+ """指定したランクの学習済みモデルによる予測を行う
270
+
271
+ Parameters
272
+ ----------
273
+ X : array-like of shape (n_samples, n_features)
274
+ デザイン行列
275
+ rank : int, default=0
276
+ モデルランク
277
+
278
+ Returns
279
+ -------
280
+ ndarray of shape (n_samples,)
281
+ 指定したランクの学習済みモデルによる予測値
282
+
283
+ """
284
+ # rank を指定してモデルを取り出す
285
+ model = self.results_[rank].model
286
+
287
+ # ndarray にキャスト
288
+ X = np.asarray(X)
289
+
290
+ # スライス
291
+ X_slice = X[:, self.results_[rank].feature.astype(bool)]
292
+
293
+ if self.sort_criteria == "cve":
294
+ y_preds = []
295
+ for fold in model:
296
+ y_pred = fold.predict(X_slice)
297
+ y_preds.append(y_pred)
298
+ return np.mean(y_preds, axis=0)
299
+
300
+ y_pred = model.predict(X_slice)
301
+ return y_pred
302
+
303
+ def get_ave_importances(self):
304
+ """特徴量重要度 (平均絶対標準化回帰係数) とその標準偏差を取得する
305
+
306
+ Returns
307
+ -------
308
+ ndarray of shape (n_features, 2)
309
+ 特徴量重要度 (平均絶対標準化回帰係数) とその標準偏差
310
+
311
+ """
312
+ weights = self.get_weights()
313
+ weights_abs = np.abs(weights)
314
+ return np.vstack([weights_abs.mean(axis=1), weights_abs.std(axis=1)]).T
@@ -0,0 +1,335 @@
1
+ import multiprocessing as mp
2
+ from functools import partial
3
+ from itertools import product
4
+ from typing import Literal
5
+
6
+ import numpy as np
7
+ from sklearn.linear_model import Lasso
8
+ from sklearn.preprocessing import StandardScaler
9
+ from tqdm.auto import tqdm
10
+
11
+
12
+ # 各イテレーションで特徴量選択を行い、特徴量ごとに選択されたかどうかを返すメソッド
13
+ def _selector(iters, X, y, alphas, eps, max_iter, random_state):
14
+ # イテレータ
15
+ subsample_idx, alpha_idx = iters
16
+
17
+ # サブサンプル
18
+ X_subsample = X[subsample_idx]
19
+ y_subsample = y[subsample_idx]
20
+
21
+ # alpha
22
+ alpha = alphas[alpha_idx]
23
+
24
+ # 標準化
25
+ scaler = StandardScaler()
26
+ X_subsample_scaled = scaler.fit_transform(X_subsample)
27
+
28
+ # モデル定義
29
+ model = Lasso(
30
+ alpha=alpha,
31
+ random_state=random_state,
32
+ max_iter=max_iter,
33
+ )
34
+
35
+ # 学習
36
+ model.fit(X_subsample_scaled, y_subsample)
37
+
38
+ # 選択された特徴量を 0/1 で収集
39
+ selection = (np.abs(model.coef_) > eps).astype(int)
40
+
41
+ return selection, alpha_idx
42
+
43
+
44
+ # Stability Selection を行うクラス
45
+ class StabilitySelection:
46
+ """Stability Selection (N. Meinshausen and P. Bühlmann, J. R. Stat. Soc. B (2010).).
47
+
48
+ Parameters
49
+ ----------
50
+ subsample_iter : int, default=100
51
+ サブサンプリング回数
52
+ subsample_frac : float, default=0.5
53
+ サブサンプリング比率 (0.0 から 1.0 の値)
54
+ n_alphas : int, default=100
55
+ 正則化係数 (alpha) の値をサンプリングする回数
56
+ min_alpha : float, default=1e-6
57
+ サンプリングされる alpha の最小値
58
+ max_alpha : float, default=1.0
59
+ サンプリングされる alpha の最大値
60
+ eps : float, default=1e-6
61
+ LASSO が特徴量選択を行う際の閾値
62
+ lasso_max_iter : int, default=1000
63
+ LASSO の最大反復回数
64
+ random_state : int, default=42
65
+ ランダムシード
66
+
67
+ Attributes
68
+ ----------
69
+ alphas_ : ndarray of shape (n_alphas,)
70
+ サンプリングされた alpha の配列
71
+ selection_path_ : ndarray of shape (n_features, n_alphas)
72
+ 各特徴量における alpha ごとの選択確率プロファイル
73
+
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ subsample_iter: int = 100,
79
+ subsample_frac: float = 0.5,
80
+ n_alphas: int = 100,
81
+ min_alpha: float = 1e-6,
82
+ max_alpha: float = 1.0,
83
+ eps: float = 1e-6,
84
+ lasso_max_iter: int = 1000,
85
+ random_state: int = 42,
86
+ ):
87
+ self.subsample_iter = subsample_iter # サブサンプリング回数
88
+ self.subsample_frac = subsample_frac # サブサンプリング比率
89
+ self.n_alphas = n_alphas # LASSO の正則化係数の探索回数
90
+ self.min_alpha = min_alpha # LASSO の正則化係数の最小値
91
+ self.max_alpha = max_alpha # LASSO の正則化係数の最大値
92
+ self.eps = eps # スパースとみなす閾値
93
+ self.lasso_max_iter = lasso_max_iter # LASSO の最大反復回数
94
+ self.random_state = random_state # ランダムシード
95
+
96
+ self.alphas_ = None # alpha のプロファイル
97
+ self.selection_path_ = None # 選択確率
98
+
99
+ # ランダムシード固定
100
+ np.random.seed(self.random_state)
101
+
102
+ # 学習を実行する関数
103
+ def fit(self, X, y, n_jobs: int = -1):
104
+ """モデルを学習させる
105
+
106
+ Parameters
107
+ ----------
108
+ X : array-like of shape (n_samples, n_features)
109
+ 訓練データのデザイン行列
110
+ y : array-like of shape (n_samples,)
111
+ 教師ラベルの配列
112
+ n_jobs : int, default=-1
113
+ CPU プロセス数 (-1 を指定すると全ての CPU コアを消費)
114
+
115
+ Returns
116
+ -------
117
+ self
118
+ 学習済みインスタンス
119
+
120
+ """
121
+ # ndarray にキャスト
122
+ X, y = np.asarray(X), np.asarray(y)
123
+
124
+ # データサイズ
125
+ n_sample = X.shape[0]
126
+ n_subsample = int(n_sample * self.subsample_frac)
127
+
128
+ # サブサンプリング用の index を生成
129
+ # 重複を許さない
130
+ subsample_indices = []
131
+ seen = set()
132
+ subsample_count = 0
133
+ subsample_max_iter = int(self.subsample_iter * 10)
134
+
135
+ while len(subsample_indices) < self.subsample_iter:
136
+ if subsample_count > subsample_max_iter:
137
+ break
138
+
139
+ subsample_count += 1
140
+
141
+ indice = np.random.choice(n_sample, n_subsample, replace=False)
142
+ key = tuple(sorted(indice))
143
+
144
+ if key not in seen:
145
+ subsample_indices.append(indice)
146
+ seen.add(key)
147
+
148
+ # alpha を走査
149
+ self.alphas_ = np.logspace(
150
+ np.log10(self.min_alpha),
151
+ np.log10(self.max_alpha),
152
+ self.n_alphas,
153
+ )
154
+ alpha_indices = list(range(len(self.alphas_)))
155
+
156
+ # 特徴量選択器の初期化
157
+ worker = partial(
158
+ _selector,
159
+ X=X,
160
+ y=y,
161
+ alphas=self.alphas_,
162
+ eps=self.eps,
163
+ max_iter=self.lasso_max_iter,
164
+ random_state=self.random_state,
165
+ )
166
+
167
+ # ループするイテレータ
168
+ iters = product(subsample_indices, alpha_indices)
169
+
170
+ # 並列で選択を実行
171
+ tot_iters = len(subsample_indices) * len(alpha_indices)
172
+ with mp.Pool(processes=mp.cpu_count() if n_jobs == -1 else n_jobs) as pool:
173
+ selections = list(
174
+ tqdm(
175
+ pool.imap_unordered(worker, iters),
176
+ total=tot_iters,
177
+ desc="progress",
178
+ ),
179
+ )
180
+
181
+ # 特徴量・alpha ごとの選択回数
182
+ selection_counts = np.zeros((X.shape[1], len(alpha_indices)), dtype=int)
183
+ for sele in selections:
184
+ selection_counts[:, sele[1]] += sele[0]
185
+
186
+ # 特徴量・alpha ごとの選択確率
187
+ self.selection_path_ = selection_counts / self.subsample_iter
188
+ return self
189
+
190
+ # 選択確率を取得
191
+ def get_selection_path(self):
192
+ """各特徴量における alpha ごとの選択確率プロファイルを取得する
193
+
194
+ Returns
195
+ -------
196
+ selection_path_ : ndarray of shape (n_features, n_alphas)
197
+ 各特徴量における alpha ごとの選択確率プロファイル
198
+
199
+ """
200
+ return self.selection_path_
201
+
202
+ # alphas を取得
203
+ def get_alphas(self):
204
+ """サンプリングされた alpha の配列を取得する
205
+
206
+ Returns
207
+ -------
208
+ alphas_ : ndarray of shape (n_alphas,)
209
+ サンプリングされた alpha の配列
210
+
211
+ """
212
+ return self.alphas_
213
+
214
+ # alpha のスライス
215
+ def _get_alpha_domain(
216
+ self,
217
+ lower_alpha: float | None,
218
+ upper_alpha: float | None,
219
+ ):
220
+ if lower_alpha is None:
221
+ lower_alpha = self.min_alpha
222
+ if upper_alpha is None:
223
+ upper_alpha = self.max_alpha
224
+
225
+ if lower_alpha < self.min_alpha:
226
+ return ValueError(
227
+ "'lower_alpha' must be greater than or equal to 'min_alpha'.",
228
+ )
229
+ if upper_alpha > self.max_alpha:
230
+ return ValueError(
231
+ "'upper_alpha' must be less than or equal to 'max_alpha'.",
232
+ )
233
+
234
+ # alpha を指定した領域に制限
235
+ alpha_indice = np.where(
236
+ (self.alphas_ >= lower_alpha) & (self.alphas_ < upper_alpha),
237
+ )[0]
238
+ domain = self.selection_path_[:, alpha_indice]
239
+ return domain
240
+
241
+ def get_ave_selection_probs(
242
+ self,
243
+ lower_alpha: float | None = None,
244
+ upper_alpha: float | None = None,
245
+ ):
246
+ """指定した alpha の領域について、特徴量ごとの平均選択率とその標準偏差を取得する
247
+
248
+ Parameters
249
+ ----------
250
+ lower_alpha : float, optional
251
+ alpha の下界
252
+ upper_alpha : float, optional
253
+ alpha の上界
254
+
255
+ Returns
256
+ -------
257
+ ndarray of shape (n_features, 2)
258
+ 特徴量ごとの平均選択率とその標準偏差
259
+
260
+ """
261
+ domain = self._get_alpha_domain(
262
+ lower_alpha=lower_alpha,
263
+ upper_alpha=upper_alpha,
264
+ )
265
+
266
+ # 平均選択率とその標準偏差
267
+ return np.vstack([domain.mean(axis=1), domain.std(axis=1)]).T
268
+
269
+ def get_mask(
270
+ self,
271
+ lower_alpha: float | None = None,
272
+ upper_alpha: float | None = None,
273
+ selection_th: float = 0.5,
274
+ astype: Literal["bool", "int"] = "bool",
275
+ ):
276
+ """指定した alpha の領域について、特徴量が選択されたかどうかを表すマスク配列を取得する
277
+
278
+ Parameters
279
+ ----------
280
+ lower_alpha : float, optional
281
+ alpha の下界
282
+ upper_alpha : float, optional
283
+ alpha の上界
284
+ selection_th : float, default=0.5
285
+ 特徴量選択の閾値 (0.0 から 1.0 の値)
286
+ astype : {"bool", "int"}, default="bool"
287
+ マスク配列の型
288
+
289
+ Returns
290
+ -------
291
+ ndarray of shape (n_features,)
292
+ 特徴量が選択されたかどうかを表すマスク配列
293
+
294
+ """
295
+ selection_probs = self.get_ave_selection_probs(
296
+ lower_alpha=lower_alpha,
297
+ upper_alpha=upper_alpha,
298
+ )
299
+ mask = selection_probs[:, 0] >= selection_th
300
+ if astype == "int":
301
+ mask = mask.astype(int)
302
+ return mask
303
+
304
+ def transform(
305
+ self,
306
+ X,
307
+ lower_alpha: float | None = None,
308
+ upper_alpha: float | None = None,
309
+ selection_th: float = 0.5,
310
+ ):
311
+ """指定した alpha の領域について、デザイン行列をスパース表現に変換する
312
+
313
+ Parameters
314
+ ----------
315
+ lower_alpha : float, optional
316
+ alpha の下界
317
+ upper_alpha : float, optional
318
+ alpha の上界
319
+ selection_th : float, default=0.5
320
+ 特徴量選択の閾値 (0.0 から 1.0 の値)
321
+
322
+ Returns
323
+ -------
324
+ ndarray of shape (n_samples, n_features)
325
+ デザイン行列のスパース表現
326
+
327
+ """
328
+ X = np.asarray(X)
329
+ mask = self.get_mask(
330
+ lower_alpha=lower_alpha,
331
+ upper_alpha=upper_alpha,
332
+ selection_th=selection_th,
333
+ )
334
+ X[:, ~mask] = 0.0
335
+ return X
@@ -0,0 +1,320 @@
1
+ from importlib.resources import files
2
+ from typing import Literal
3
+
4
+ import matplotlib.colors as cl
5
+ import matplotlib.font_manager as fm
6
+ import matplotlib.pyplot as plt
7
+ import matplotlib.ticker as tick
8
+ import numpy as np
9
+
10
+
11
+ def _init():
12
+ arial_path = files("spmkit.assets") / "Arial.ttf"
13
+ arial = fm.FontProperties(fname=arial_path)
14
+ plt.rcParams["font.family"] = arial.get_name()
15
+ plt.rcParams["mathtext.fontset"] = "cm"
16
+ plt.rcParams["xtick.direction"] = "in"
17
+ plt.rcParams["ytick.direction"] = "in"
18
+ plt.rcParams["font.size"] = 14
19
+ plt.gca().spines["right"].set_visible(True)
20
+ plt.gca().spines["top"].set_visible(True)
21
+ plt.gca().spines["bottom"].set_visible(True)
22
+ plt.gca().spines["left"].set_visible(True)
23
+ plt.gca().spines["top"].set_linewidth(2)
24
+ plt.gca().spines["left"].set_linewidth(2)
25
+ plt.gca().spines["bottom"].set_linewidth(2)
26
+ plt.gca().spines["right"].set_linewidth(2)
27
+ plt.xticks(fontproperties=arial.get_name())
28
+ plt.yticks(fontproperties=arial.get_name())
29
+ plt.tick_params(left=True, width=2, length=5)
30
+
31
+
32
+ def selection_path_plot(ax, selection_path, alphas, feature_names=None):
33
+ """各特徴量における alpha ごとの選択確率プロファイルを表示する
34
+
35
+ Parameters
36
+ ----------
37
+ ax : matplotlib.axes.Axes
38
+ `matplotlib.axes.Axes`
39
+ selection_path : array-like of shape (n_features, n_alphas)
40
+ 各特徴量における alpha ごとの選択確率プロファイル
41
+ alphas_ : array-like of shape (n_alphas,)
42
+ サンプリングされた alpha の配列
43
+ feature_names : array-like of shape (n_features,), optional
44
+ 各特徴量の名称
45
+
46
+ Returns
47
+ -------
48
+ matplotlib.axes.Axes
49
+ 更新済みの `matplotlib.axes.Axes`
50
+
51
+ """
52
+ _init()
53
+ selection_path, alphas = np.asarray(selection_path), np.asarray(alphas)
54
+ if feature_names is None:
55
+ feature_names = [f"feature {l + 1}" for l in range(selection_path.shape[0])]
56
+
57
+ for i, feat in enumerate(selection_path):
58
+ ax.plot(alphas, feat, label=feature_names[i])
59
+ ax.set_xscale("log", base=10)
60
+ ax.set_ylim(0.0, 1.0)
61
+ ax.set_xlabel(r"$\alpha$", fontsize=16)
62
+ ax.set_ylabel("Selection probability", fontsize=16)
63
+ ax.legend()
64
+ return ax
65
+
66
+
67
+ def ave_selection_probs_bar_plot(
68
+ ax,
69
+ selection_probs,
70
+ selection_th=None,
71
+ feature_names=None,
72
+ bar_color="limegreen",
73
+ vline_color="red",
74
+ errorbar=False,
75
+ ):
76
+ """特徴量ごとの平均選択率を表示する
77
+
78
+ Parameters
79
+ ----------
80
+ ax : matplotlib.axes.Axes
81
+ `matplotlib.axes.Axes`
82
+ selection_probs : ndarray of shape (n_features, 2)
83
+ 特徴量ごとの平均選択率とその標準偏差
84
+ selection_th : float, optional
85
+ 特徴量選択の閾値 (0.0 から 1.0 の値)
86
+ feature_names : array-like of shape (n_features,), optional
87
+ 各特徴量の名称
88
+ bar_color : str, default="limegreen"
89
+ バーの色
90
+ vline_color : str, default="red"
91
+ 分割線の色
92
+ errorbar : bool, default=False
93
+ エラーバーを表示するかどうか
94
+
95
+ Returns
96
+ -------
97
+ matplotlib.axes.Axes
98
+ 更新済みの `matplotlib.axes.Axes`
99
+
100
+ """
101
+ _init()
102
+ selection_probs = np.asarray(selection_probs)
103
+ if feature_names is None:
104
+ feature_names = [f"feature {l + 1}" for l in range(selection_probs.shape[0])]
105
+
106
+ # ソート
107
+ sort_indice = np.argsort(selection_probs[:, 0])
108
+ selection_probs_sorted = selection_probs[sort_indice]
109
+ feature_names_sorted = np.array(feature_names)[sort_indice]
110
+
111
+ ax.barh(
112
+ feature_names_sorted,
113
+ selection_probs_sorted[:, 0],
114
+ xerr=selection_probs_sorted[:, 1] if errorbar else 0,
115
+ capsize=2,
116
+ height=1.0,
117
+ edgecolor="black",
118
+ color=bar_color,
119
+ )
120
+
121
+ if selection_th is not None:
122
+ ax.axvline(
123
+ selection_th,
124
+ linestyle="dashed",
125
+ linewidth=1.5,
126
+ label="Threshold",
127
+ color=vline_color,
128
+ )
129
+ ax.legend()
130
+
131
+ ax.set_xlim(0.0, 1.0)
132
+ ax.set_xlabel("Average of selection probability", fontsize=16)
133
+ ax.set_ylabel("Features", fontsize=16)
134
+ return ax
135
+
136
+
137
+ def weight_diagram(
138
+ ax,
139
+ weights,
140
+ feature_names=None,
141
+ pos_color="red",
142
+ neg_color="dodgerblue",
143
+ mid_color="whitesmoke",
144
+ xscale: Literal["linear", "log2"] = "linear",
145
+ ):
146
+ """全ての学習済みモデルの重みを表示する
147
+
148
+ Parameters
149
+ ----------
150
+ ax : matplotlib.axes.Axes
151
+ `matplotlib.axes.Axes`
152
+ weights : array-like of shape (n_features, 2**n_features - 1)
153
+ ソートされた学習済みモデルの重み行列
154
+ feature_names : array-like of shape (n_features,), optional
155
+ 各特徴量の名称
156
+ pos_color : str, default="red"
157
+ 重みが正となる場合の色
158
+ neg_color : str, default="dodgerblue"
159
+ 重みが負となる場合の色
160
+ mid_color : str, default="lightgray"
161
+ 重みがゼロとなる場合の色
162
+ xscale : {"linear", "log2"}, default="linear"
163
+ 横軸のスケール
164
+
165
+ Returns
166
+ -------
167
+ matplotlib.collections.QuadMesh
168
+ 更新済みの `matplotlib.collections.QuadMesh`
169
+
170
+ """
171
+ _init()
172
+ weights = np.asarray(weights)
173
+ n_features, n_models = weights.shape
174
+
175
+ if feature_names is None:
176
+ feature_names = [f"feature {l + 1}" for l in range(n_features)]
177
+
178
+ # ranks = np.arange(1, n_models + 1)
179
+ x = np.arange(0.5, n_models + 1.5)
180
+ y = np.arange(n_features + 1)
181
+
182
+ im = ax.pcolormesh(
183
+ x,
184
+ y,
185
+ weights,
186
+ cmap=cl.LinearSegmentedColormap.from_list(
187
+ "weight_diagram",
188
+ [neg_color, mid_color, pos_color],
189
+ ),
190
+ norm=cl.TwoSlopeNorm(
191
+ vcenter=0.0,
192
+ ),
193
+ )
194
+
195
+ ax.set_xlim(1, n_models + 1)
196
+ if xscale == "log2":
197
+ ax.set_xscale("log", base=2)
198
+ ax.xaxis.set_major_locator(tick.LogLocator(base=2))
199
+ ax.xaxis.set_major_formatter(tick.LogFormatterMathtext(base=2))
200
+ ax.set_yticklabels(feature_names)
201
+ ax.set_yticks(np.arange(len(feature_names)) + 0.5)
202
+ ax.set_xlabel("Rank", fontsize=16)
203
+ ax.set_ylabel("Features", fontsize=16)
204
+ return im
205
+
206
+
207
+ def coefs_waterfall_plot(
208
+ ax,
209
+ weights,
210
+ rank=0,
211
+ feature_names=None,
212
+ pos_color="red",
213
+ neg_color="dodgerblue",
214
+ ):
215
+ """指定したランクのモデルの重みを絶対値順にソートして表示する
216
+
217
+ Parameters
218
+ ----------
219
+ ax : matplotlib.axes.Axes
220
+ `matplotlib.axes.Axes`
221
+ weights : array-like of shape (n_features, 2**n_features - 1)
222
+ ソートされた学習済みモデルの重み行列
223
+ rank : int, default=0
224
+ モデルランク
225
+ feature_names : array-like of shape (n_features,), optional
226
+ 各特徴量の名称
227
+ pos_color : str, default="red"
228
+ 重みが正となる場合の色
229
+ neg_color : str, default="dodgerblue"
230
+ 重みが負となる場合の色
231
+
232
+ Returns
233
+ -------
234
+ matplotlib.axes.Axes
235
+ 更新済みの `matplotlib.axes.Axes`
236
+
237
+ """
238
+ _init()
239
+ weights = np.asarray(weights)
240
+ coefs = weights[:, rank]
241
+ n_features = coefs.shape[0]
242
+
243
+ if feature_names is None:
244
+ feature_names = [f"feature {l + 1}" for l in range(n_features)]
245
+
246
+ colors = [pos_color if v >= 0 else neg_color for v in coefs]
247
+
248
+ # 回帰係数がゼロとなる特徴量を落とす
249
+ filter_indice = np.where(coefs != 0)[0]
250
+ coefs_filtered = coefs[filter_indice]
251
+ feature_names_filtered = np.array(feature_names)[filter_indice]
252
+
253
+ # ソート
254
+ sort_indice = np.argsort(np.abs(coefs_filtered))
255
+ coefs_sorted = coefs_filtered[sort_indice]
256
+ feature_names_sorted = feature_names_filtered[sort_indice]
257
+
258
+ ax.barh(
259
+ feature_names_sorted,
260
+ coefs_sorted,
261
+ color=colors,
262
+ height=1.0,
263
+ edgecolor="black",
264
+ )
265
+ ax.axvline(x=0, color="black", linewidth=2)
266
+ ax.set_xlabel("Standardized coefficient", fontsize=16)
267
+ ax.set_ylabel("Features", fontsize=16)
268
+ return ax
269
+
270
+
271
+ def ave_importances_bar_plot(
272
+ ax,
273
+ importances,
274
+ feature_names=None,
275
+ bar_color="limegreen",
276
+ errorbar=False,
277
+ ):
278
+ """特徴量重要度 (平均絶対標準化回帰係数) を表示する
279
+
280
+ Parameters
281
+ ----------
282
+ ax : matplotlib.axes.Axes
283
+ `matplotlib.axes.Axes`
284
+ importances : array-like of shape (n_features, 2)
285
+ 特徴量重要度 (平均絶対標準化回帰係数) とその標準偏差
286
+ feature_names : array-like of shape (n_features,), optional
287
+ 各特徴量の名称
288
+ bar_color : str, default="limegreen"
289
+ バーの色
290
+ errorbar : bool, default=False
291
+ エラーバーを表示するかどうか
292
+
293
+ Returns
294
+ -------
295
+ matplotlib.axes.Axes
296
+ 更新済みの `matplotlib.axes.Axes`
297
+
298
+ """
299
+ _init()
300
+ importances = np.asarray(importances)
301
+ if feature_names is None:
302
+ feature_names = [f"feature {l + 1}" for l in range(importances.shape[0])]
303
+
304
+ # ソート
305
+ sort_indice = np.argsort(importances[:, 0])
306
+ importances_sorted = importances[sort_indice]
307
+ feature_names_sorted = np.array(feature_names)[sort_indice]
308
+
309
+ ax.barh(
310
+ feature_names_sorted,
311
+ importances_sorted[:, 0],
312
+ xerr=importances_sorted[:, 1] if errorbar else 0,
313
+ capsize=2,
314
+ height=1.0,
315
+ edgecolor="black",
316
+ color=bar_color,
317
+ )
318
+ ax.set_xlabel("Feature importance", fontsize=16)
319
+ ax.set_ylabel("Features", fontsize=16)
320
+ return ax
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: spmkit-learn
3
+ Version: 1.0.0
4
+ Summary: Meta-Sparse Modeling Library for Chemical Data Prediction.
5
+ Author-email: Shota Inoue <inoue.shota@st.kitasato-u.ac.jp>
6
+ License: Proprietary
7
+ Classifier: License :: Other/Proprietary License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy<=1.26.4,>=1.25.2
18
+ Requires-Dist: scikit-learn==1.6.0
19
+ Requires-Dist: matplotlib>=3.7.0
20
+ Dynamic: license-file
21
+
22
+ # spmkit
23
+
24
+ **spmkit** is meta-sparse modeling library for chemical data prediction.
25
+
26
+ -----
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ # via wheel binary
32
+ pip install spmkit-***.whl
33
+
34
+ # via gzip source
35
+ pip install spmkit-***.tar.gz
36
+ ```
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ spmkit/__init__.py
5
+ spmkit/plots.py
6
+ spmkit/assets/Arial.ttf
7
+ spmkit/models/__init__.py
8
+ spmkit/models/exhaustive_search.py
9
+ spmkit/models/stability_selection.py
10
+ spmkit_learn.egg-info/PKG-INFO
11
+ spmkit_learn.egg-info/SOURCES.txt
12
+ spmkit_learn.egg-info/dependency_links.txt
13
+ spmkit_learn.egg-info/requires.txt
14
+ spmkit_learn.egg-info/top_level.txt
15
+ test/test.py
@@ -0,0 +1,3 @@
1
+ numpy<=1.26.4,>=1.25.2
2
+ scikit-learn==1.6.0
3
+ matplotlib>=3.7.0
@@ -0,0 +1,29 @@
1
+ import pandas as pd
2
+
3
+ # Wine Quality データセットをダウンロードする
4
+ from sklearn.datasets import fetch_openml
5
+ from spmkit.models import StabilitySelection
6
+
7
+ # データフェッチ
8
+ dataset = fetch_openml(data_id=43257, as_frame=True)
9
+
10
+ # 不要な列を削除し、X, y に分割
11
+ X = dataset.data.drop(["quality", "id"], axis=1)
12
+ y = dataset.data["quality"]
13
+
14
+ # StabilitySelection モデルを定義
15
+ model = StabilitySelection(
16
+ subsample_iter=1000,
17
+ n_alphas=100,
18
+ min_alpha=1e-6,
19
+ max_alpha=1.0,
20
+ random_state=42,
21
+ )
22
+
23
+ # 特徴量選択を実行
24
+ model.fit(X, y)
25
+
26
+ # alpha ごとの選択確率を算出
27
+ selection_path = pd.DataFrame(model.selection_path_, index=X.columns)
28
+
29
+ print(selection_path)