varselpfn 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Omar Melikechi
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,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: varselpfn
3
+ Version: 0.1.0
4
+ Summary: Variable selection with prior-data fitted networks: CD-PFN and PC-PFN
5
+ Author-email: Omar Melikechi <omar.melikechi@duke.edu>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/omelikechi/varselpfn
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ License-File: THIRD_PARTY_NOTICES
12
+ Requires-Dist: numpy>=1.24
13
+ Requires-Dist: safetensors>=0.4
14
+ Requires-Dist: scipy>=1.10
15
+ Requires-Dist: threadpoolctl>=3.0
16
+ Requires-Dist: torch>=2.1
17
+ Provides-Extra: pandas
18
+ Requires-Dist: pandas>=1.5; extra == "pandas"
19
+ Dynamic: license-file
20
+
21
+ # Prior-data fitted networks for variable selection
22
+
23
+ > Two pretrained prior-data fitted networks that return posterior inclusion probabilities for every covariate in one forward pass: **CD-PFN** estimates whether each covariate is conditionally dependent on the response, and **PC-PFN** estimates whether each covariate is a parent or child of the response in a directed acyclic graph.
24
+
25
+ ## Associated paper
26
+
27
+ - **Prior-data fitted networks for variable selection**
28
+ [arXiv](https://arxiv.org/abs/2611.05966)
29
+
30
+ ## Installation
31
+ ```
32
+ pip install varselpfn
33
+ ```
34
+
35
+ ## Usage
36
+ ```python
37
+ import numpy as np
38
+ from varselpfn import CDPFN, PCPFN
39
+
40
+ rng = np.random.default_rng(0)
41
+ X = rng.normal(size=(200, 50))
42
+ y = X[:, 0] + np.sin(2 * X[:, 1]) + rng.normal(size=200)
43
+
44
+ # CD-PFN
45
+ cd = CDPFN()
46
+ pips = cd.predict(X, y) # shape (50,)
47
+ selected = cd.select(X, y, alpha=0.1) # indices selected at target FDR 0.1
48
+
49
+ # PC-PFN
50
+ pc = PCPFN()
51
+ pips = pc.predict(X, y) # shape (50, 2): parent and child PIPs
52
+ selected = pc.select(X, y, alpha=0.1) # {'parents': [...], 'children': [...]}
53
+ ```
54
+
55
+ - `X` is an `(n, p)` array or DataFrame of numeric covariates; `y` is continuous or binary (0/1). No preprocessing is needed, but missing values are not allowed.
56
+ - The networks were trained on datasets with 16 to 1024 observations and 8 to 512 covariates.
57
+ - Pass `device='cuda'` (or `'mps'`) to run on a GPU.
58
+
59
+ ## Fine-tuning
60
+
61
+ To adapt a network to your data, copy its config ([`cd_pfn.json`](https://github.com/omelikechi/varselpfn/blob/main/varselpfn/configs/cd_pfn.json) or [`pc_pfn.json`](https://github.com/omelikechi/varselpfn/blob/main/varselpfn/configs/pc_pfn.json)), edit the `prior` block (options in [`prior.py`](https://github.com/omelikechi/varselpfn/blob/main/varselpfn/prior.py)), and continue training from the released weights:
62
+
63
+ ```
64
+ python -m varselpfn.train --config my_prior.json --init cd_pfn --run-dir my_run --max-minutes 120
65
+ ```
66
+
67
+ Then load it with `CDPFN(weights='my_run/best.pt')`. For PC-PFN, use `--init pc_pfn` and `PCPFN(weights=...)`.
@@ -0,0 +1,47 @@
1
+ # Prior-data fitted networks for variable selection
2
+
3
+ > Two pretrained prior-data fitted networks that return posterior inclusion probabilities for every covariate in one forward pass: **CD-PFN** estimates whether each covariate is conditionally dependent on the response, and **PC-PFN** estimates whether each covariate is a parent or child of the response in a directed acyclic graph.
4
+
5
+ ## Associated paper
6
+
7
+ - **Prior-data fitted networks for variable selection**
8
+ [arXiv](https://arxiv.org/abs/2611.05966)
9
+
10
+ ## Installation
11
+ ```
12
+ pip install varselpfn
13
+ ```
14
+
15
+ ## Usage
16
+ ```python
17
+ import numpy as np
18
+ from varselpfn import CDPFN, PCPFN
19
+
20
+ rng = np.random.default_rng(0)
21
+ X = rng.normal(size=(200, 50))
22
+ y = X[:, 0] + np.sin(2 * X[:, 1]) + rng.normal(size=200)
23
+
24
+ # CD-PFN
25
+ cd = CDPFN()
26
+ pips = cd.predict(X, y) # shape (50,)
27
+ selected = cd.select(X, y, alpha=0.1) # indices selected at target FDR 0.1
28
+
29
+ # PC-PFN
30
+ pc = PCPFN()
31
+ pips = pc.predict(X, y) # shape (50, 2): parent and child PIPs
32
+ selected = pc.select(X, y, alpha=0.1) # {'parents': [...], 'children': [...]}
33
+ ```
34
+
35
+ - `X` is an `(n, p)` array or DataFrame of numeric covariates; `y` is continuous or binary (0/1). No preprocessing is needed, but missing values are not allowed.
36
+ - The networks were trained on datasets with 16 to 1024 observations and 8 to 512 covariates.
37
+ - Pass `device='cuda'` (or `'mps'`) to run on a GPU.
38
+
39
+ ## Fine-tuning
40
+
41
+ To adapt a network to your data, copy its config ([`cd_pfn.json`](https://github.com/omelikechi/varselpfn/blob/main/varselpfn/configs/cd_pfn.json) or [`pc_pfn.json`](https://github.com/omelikechi/varselpfn/blob/main/varselpfn/configs/pc_pfn.json)), edit the `prior` block (options in [`prior.py`](https://github.com/omelikechi/varselpfn/blob/main/varselpfn/prior.py)), and continue training from the released weights:
42
+
43
+ ```
44
+ python -m varselpfn.train --config my_prior.json --init cd_pfn --run-dir my_run --max-minutes 120
45
+ ```
46
+
47
+ Then load it with `CDPFN(weights='my_run/best.pt')`. For PC-PFN, use `--init pc_pfn` and `PCPFN(weights=...)`.
@@ -0,0 +1,33 @@
1
+ Third-party notices
2
+
3
+ The Cauchy-DAG graph sampler in varselpfn/prior.py (sample_graph) is adapted from TabICL
4
+ (https://github.com/soda-inria/tabicl, commit c91f00d), distributed under the following license.
5
+
6
+ BSD 3-Clause License
7
+
8
+ Copyright (c) 2025, Soda team @ Inria
9
+
10
+ Redistribution and use in source and binary forms, with or without
11
+ modification, are permitted provided that the following conditions are met:
12
+
13
+ 1. Redistributions of source code must retain the above copyright notice, this
14
+ list of conditions and the following disclaimer.
15
+
16
+ 2. Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+
20
+ 3. Neither the name of the copyright holder nor the names of its
21
+ contributors may be used to endorse or promote products derived from
22
+ this software without specific prior written permission.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ['setuptools>=77']
3
+ build-backend = 'setuptools.build_meta'
4
+
5
+ [project]
6
+ name = 'varselpfn'
7
+ version = '0.1.0'
8
+ description = 'Variable selection with prior-data fitted networks: CD-PFN and PC-PFN'
9
+ readme = 'README.md'
10
+ license = 'MIT'
11
+ license-files = ['LICENSE', 'THIRD_PARTY_NOTICES']
12
+ authors = [{name = 'Omar Melikechi', email = 'omar.melikechi@duke.edu'}]
13
+ requires-python = '>=3.10'
14
+ dependencies = ['numpy>=1.24', 'safetensors>=0.4', 'scipy>=1.10', 'threadpoolctl>=3.0', 'torch>=2.1']
15
+
16
+ [project.optional-dependencies]
17
+ pandas = ['pandas>=1.5']
18
+
19
+ [project.urls]
20
+ Repository = 'https://github.com/omelikechi/varselpfn'
21
+
22
+ [tool.setuptools]
23
+ packages = ['varselpfn']
24
+
25
+ [tool.setuptools.package-data]
26
+ varselpfn = ['configs/*.json', 'weights/*.safetensors']
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ # varselpfn: variable selection with prior-data fitted networks
2
+ from varselpfn.api import CDPFN, PCPFN, bayes_select
3
+
4
+ __version__ = '0.1.0'
@@ -0,0 +1,131 @@
1
+ # CD-PFN and PC-PFN
2
+ '''
3
+ CDPFN and PCPFN wrap the pretrained networks. predict(X, y) returns estimated posterior inclusion probabilities (PIPs)
4
+ in one forward pass, with nothing fitted to the data; select(X, y, alpha) applies the Bayesian FDR rule to them.
5
+
6
+ Weights are the bundled ones ('cd_pfn', 'pc_pfn') unless a path is given: a .safetensors file or a checkpoint written
7
+ by varselpfn.train (best.pt), for example after fine-tuning.
8
+ '''
9
+
10
+ from functools import lru_cache
11
+ import json
12
+ from pathlib import Path
13
+
14
+ import numpy as np
15
+ from safetensors import safe_open
16
+ from safetensors.torch import load_file
17
+ import torch
18
+
19
+ from varselpfn.model import Network
20
+
21
+ package_dir = Path(__file__).resolve().parent
22
+ bundled_weights = {'cd_pfn': package_dir / 'weights' / 'cd_pfn.safetensors', 'pc_pfn': package_dir / 'weights' / 'pc_pfn.safetensors'}
23
+
24
+ #----------------------------------------------------------------
25
+ # Weights
26
+ #----------------------------------------------------------------
27
+ def load_weights(weights):
28
+ '''Returns (model config, state dict) for a bundled name or a path to a .safetensors file or a training checkpoint.'''
29
+ path = bundled_weights.get(weights, Path(weights).expanduser())
30
+ if path.suffix == '.safetensors':
31
+ with safe_open(path, 'pt') as f:
32
+ model_config = json.loads(f.metadata()['model'])
33
+ return model_config, load_file(path)
34
+ checkpoint = torch.load(path, map_location='cpu', weights_only=False)
35
+ return checkpoint['config']['model'], checkpoint['model']
36
+
37
+ @lru_cache(maxsize=None)
38
+ def load_network(weights, device):
39
+ '''Loaded once per (weights, device) and reused.'''
40
+ model_config, state = load_weights(weights)
41
+ model = Network(**(model_config | {'grad_checkpoint': False}))
42
+ model.load_state_dict(state)
43
+ return model.to(device).eval()
44
+
45
+ #----------------------------------------------------------------
46
+ # Selection
47
+ #----------------------------------------------------------------
48
+ def bayes_select(pips, alpha=0.1):
49
+ '''
50
+ Bayesian FDR rule: the largest set of covariates with the highest PIPs whose mean (1 - PIP), the estimated FDR, is
51
+ at most alpha. Returns their indices, in decreasing order of PIP (ties by index).
52
+ '''
53
+ pips = np.asarray(pips, dtype=float)
54
+ p = len(pips)
55
+ order = np.lexsort((np.arange(p), -pips))
56
+ running = np.cumsum(1 - pips[order]) / np.arange(1, p + 1)
57
+ ok = np.flatnonzero(running <= alpha)
58
+ k = ok.max() + 1 if len(ok) else 0
59
+ return order[:k]
60
+
61
+ #----------------------------------------------------------------
62
+ # Models
63
+ #----------------------------------------------------------------
64
+ class PFN:
65
+ '''Shared by CDPFN and PCPFN: input checks, the forward pass, and covariate names for pandas input.'''
66
+ default_weights = None
67
+ outputs = None
68
+
69
+ def __init__(self, weights=None, device='cpu'):
70
+ self.weights = str(weights) if weights is not None else self.default_weights
71
+ self.device = torch.device(device)
72
+
73
+ def probabilities(self, X, y):
74
+ names = list(X.columns) if hasattr(X, 'columns') else None
75
+ X = np.asarray(X, dtype=np.float32)
76
+ y = np.asarray(y, dtype=np.float32).ravel()
77
+ if X.ndim != 2 or len(X) != len(y):
78
+ raise ValueError(f'X must be (n, p) and y (n,); got X {X.shape} and y {y.shape}')
79
+ if not (np.isfinite(X).all() and np.isfinite(y).all()):
80
+ raise ValueError('X and y must not contain missing or infinite values')
81
+ model = load_network(self.weights, self.device)
82
+ with torch.no_grad():
83
+ logits = model(torch.from_numpy(X)[None].to(self.device), torch.from_numpy(y)[None].to(self.device))[0]
84
+ pips = torch.sigmoid(logits[:, self.outputs].float()).cpu().numpy().astype(float)
85
+ return pips, names
86
+
87
+ class CDPFN(PFN):
88
+ '''
89
+ CD-PFN: for each covariate, the estimated posterior probability that y depends on it given the other covariates.
90
+
91
+ X: (n, p) array or DataFrame of numeric covariates (continuous, binary, or ordinal). y: (n,) continuous or binary
92
+ response. Trained on 16 <= n <= 1024 and 8 <= p <= 512; columns are standardized inside the model.
93
+ '''
94
+ default_weights = 'cd_pfn'
95
+ outputs = 0
96
+
97
+ def predict(self, X, y):
98
+ '''PIPs, shape (p,); a Series indexed by the column names if X is a DataFrame.'''
99
+ pips, names = self.probabilities(X, y)
100
+ if names is None:
101
+ return pips
102
+ import pandas as pd
103
+ return pd.Series(pips, index=names, name='pip')
104
+
105
+ def select(self, X, y, alpha=0.1):
106
+ '''Covariates selected at target FDR alpha: indices, or column names if X is a DataFrame.'''
107
+ pips, names = self.probabilities(X, y)
108
+ selected = bayes_select(pips, alpha)
109
+ return selected if names is None else [names[j] for j in selected]
110
+
111
+ class PCPFN(PFN):
112
+ '''
113
+ PC-PFN: for each covariate, the estimated posterior probabilities that it is a parent (direct cause) and a child
114
+ (direct effect) of y. Inputs as for CDPFN.
115
+ '''
116
+ default_weights = 'pc_pfn'
117
+ outputs = [1, 2]
118
+
119
+ def predict(self, X, y):
120
+ '''PIPs, shape (p, 2) with columns (parent, child); a DataFrame indexed by the column names if X is one.'''
121
+ pips, names = self.probabilities(X, y)
122
+ if names is None:
123
+ return pips
124
+ import pandas as pd
125
+ return pd.DataFrame(pips, index=names, columns=['parent', 'child'])
126
+
127
+ def select(self, X, y, alpha=0.1):
128
+ '''Parents and children selected at target FDR alpha, each by the Bayesian FDR rule: {'parents': ..., 'children': ...}.'''
129
+ pips, names = self.probabilities(X, y)
130
+ out = {'parents': bayes_select(pips[:, 0], alpha), 'children': bayes_select(pips[:, 1], alpha)}
131
+ return out if names is None else {role: [names[j] for j in selected] for role, selected in out.items()}
@@ -0,0 +1,174 @@
1
+ {
2
+ "seed": 20261017,
3
+ "prior": {
4
+ "family": "general",
5
+ "p_range": [
6
+ 8,
7
+ 256
8
+ ],
9
+ "n_range": [
10
+ 16,
11
+ 1024
12
+ ],
13
+ "latent_zero_prob": 0.3,
14
+ "latent_fraction_range": [
15
+ 0.0,
16
+ 0.3
17
+ ],
18
+ "indegree_range": [
19
+ 0.4,
20
+ 2.5
21
+ ],
22
+ "cauchy_graph_prob": 0.5,
23
+ "mechanism_weights": {
24
+ "linear": 0.3,
25
+ "additive": 0.3,
26
+ "mlp": 0.12,
27
+ "tree": 0.12,
28
+ "gp": 0.08,
29
+ "product": 0.08
30
+ },
31
+ "additive_set": [
32
+ "identity",
33
+ "tanh",
34
+ "sin",
35
+ "cos",
36
+ "square",
37
+ "abs",
38
+ "bump",
39
+ "relu",
40
+ "softplus",
41
+ "sigmoid"
42
+ ],
43
+ "noise_families": [
44
+ "gaussian",
45
+ "laplace",
46
+ "uniform",
47
+ "student_t",
48
+ "mixture"
49
+ ],
50
+ "noise_fraction_range": [
51
+ 0.05,
52
+ 0.95
53
+ ],
54
+ "binary_feature_prob": 0.1,
55
+ "ordinal_feature_prob": 0.1,
56
+ "ordinal_levels": [
57
+ 3,
58
+ 7
59
+ ],
60
+ "binary_y_prob": 0.3,
61
+ "measurement_discretization_prob": 0.2,
62
+ "n_ref": 512,
63
+ "component_weights": {
64
+ "scm": 0.25,
65
+ "xfirst": 0.75
66
+ },
67
+ "xfirst": {
68
+ "structure_weights": {
69
+ "independent": 0.1,
70
+ "factor": 0.25,
71
+ "hierarchical": 0.35,
72
+ "ar": 0.1,
73
+ "scm": 0.2
74
+ },
75
+ "transform_prob": 0.7,
76
+ "transform_weights": {
77
+ "exp": 0.4,
78
+ "zero_inflated": 0.2,
79
+ "cube": 0.1,
80
+ "sqrt": 0.1,
81
+ "uniform": 0.1,
82
+ "bimodal": 0.1
83
+ },
84
+ "all_discrete_prob": 0.15,
85
+ "all_transform_prob": 0.25,
86
+ "continuous_prob": 0.4,
87
+ "null_prob": 0.1,
88
+ "max_parent_fraction": 0.5,
89
+ "children_prob": 0.3,
90
+ "max_children": 3,
91
+ "max_spouses": 2,
92
+ "spouses_near_child": true,
93
+ "descendant_prob": 0.0,
94
+ "max_descendants": 2,
95
+ "child_share": 0.0,
96
+ "parent_uniform_prob": 0.5,
97
+ "max_product_terms": 3
98
+ },
99
+ "p_uniform_prob": 0.5,
100
+ "n_uniform_prob": 0.5,
101
+ "additive_weights": {
102
+ "identity": 0.12,
103
+ "tanh": 0.12,
104
+ "sigmoid": 0.12,
105
+ "bump": 0.12,
106
+ "relu": 0.1,
107
+ "softplus": 0.1,
108
+ "square": 0.1,
109
+ "abs": 0.1,
110
+ "step": 0.08,
111
+ "sin": 0.02,
112
+ "cos": 0.02
113
+ },
114
+ "mlp_activations": [
115
+ "tanh",
116
+ "relu",
117
+ "sigmoid",
118
+ "bump",
119
+ "softplus",
120
+ "abs"
121
+ ],
122
+ "gp_length_range": [
123
+ 1.0,
124
+ 4.0
125
+ ],
126
+ "product_functions": [
127
+ "identity",
128
+ "tanh",
129
+ "sigmoid",
130
+ "bump",
131
+ "relu"
132
+ ],
133
+ "max_in_degree": 10,
134
+ "p_large_prob": 0.2,
135
+ "p_large_range": [
136
+ 257,
137
+ 512
138
+ ]
139
+ },
140
+ "model": {
141
+ "dim": 192,
142
+ "heads": 6,
143
+ "ff_dim": 768,
144
+ "blocks": 8,
145
+ "dropout": 0.0,
146
+ "pool": "mean_max",
147
+ "grad_checkpoint": false
148
+ },
149
+ "train": {
150
+ "cells_per_batch": 65536,
151
+ "max_batch_size": 64,
152
+ "lr": 0.0001,
153
+ "min_lr_ratio": 0.1,
154
+ "weight_decay": 0.0001,
155
+ "grad_clip": 1.0,
156
+ "warmup_steps": 2000,
157
+ "max_steps": 100000000,
158
+ "max_train_minutes": 1440,
159
+ "val_datasets": 512,
160
+ "val_every": 1000,
161
+ "log_every": 50,
162
+ "prevalence_datasets": 4000,
163
+ "workers": 15,
164
+ "schedule": "wsd",
165
+ "decay_fraction": 0.15,
166
+ "checkpoint_cells": 150000,
167
+ "head_weights": [
168
+ 1.0,
169
+ 0.0,
170
+ 0.0
171
+ ],
172
+ "val_match_training": true
173
+ }
174
+ }