aging-metaclock 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.
- aging_metaclock-0.1.0/PKG-INFO +135 -0
- aging_metaclock-0.1.0/README.md +112 -0
- aging_metaclock-0.1.0/aging_metaclock.egg-info/PKG-INFO +135 -0
- aging_metaclock-0.1.0/aging_metaclock.egg-info/SOURCES.txt +13 -0
- aging_metaclock-0.1.0/aging_metaclock.egg-info/dependency_links.txt +1 -0
- aging_metaclock-0.1.0/aging_metaclock.egg-info/requires.txt +16 -0
- aging_metaclock-0.1.0/aging_metaclock.egg-info/top_level.txt +1 -0
- aging_metaclock-0.1.0/metaclock/__init__.py +17 -0
- aging_metaclock-0.1.0/metaclock/_version.py +3 -0
- aging_metaclock-0.1.0/metaclock/api.py +165 -0
- aging_metaclock-0.1.0/metaclock/components.py +210 -0
- aging_metaclock-0.1.0/metaclock/models.py +134 -0
- aging_metaclock-0.1.0/metaclock/presets.py +26 -0
- aging_metaclock-0.1.0/pyproject.toml +34 -0
- aging_metaclock-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aging_metaclock
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reproducible analysis and plotting workflows for MetaClock
|
|
5
|
+
Requires-Python: <3.12,>=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: anndata==0.12.7
|
|
8
|
+
Requires-Dist: joblib==1.5.2
|
|
9
|
+
Requires-Dist: lightgbm==4.6.0
|
|
10
|
+
Requires-Dist: lifelines==0.30.3
|
|
11
|
+
Requires-Dist: matplotlib==3.10.8
|
|
12
|
+
Requires-Dist: mrmr-selection==0.2.8
|
|
13
|
+
Requires-Dist: numpy==2.3.5
|
|
14
|
+
Requires-Dist: pandas==2.3.3
|
|
15
|
+
Requires-Dist: pymupdf==1.26.6
|
|
16
|
+
Requires-Dist: pyarrow==23.0.1
|
|
17
|
+
Requires-Dist: pyaging==0.1.30
|
|
18
|
+
Requires-Dist: rdata==1.0.0
|
|
19
|
+
Requires-Dist: scikit-learn==1.8.0
|
|
20
|
+
Requires-Dist: scipy==1.17.0
|
|
21
|
+
Requires-Dist: statsmodels==0.14.6
|
|
22
|
+
Requires-Dist: torch==2.9.1
|
|
23
|
+
|
|
24
|
+
# MetaClock workflow
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
This repository contains workflows for chronological-age
|
|
29
|
+
prediction, cross-domain generalization, biological-age and disease analysis,
|
|
30
|
+
functional enrichment, and intervention analysis. GEO workflows download inputs
|
|
31
|
+
into repository-local data directories. The workflow READMEs describe the required files and preparation
|
|
32
|
+
steps.
|
|
33
|
+
|
|
34
|
+
## Project overview
|
|
35
|
+
|
|
36
|
+
MetaClock is an integrated framework that combines
|
|
37
|
+
predictions from multiple established epigenetic clocks into meta
|
|
38
|
+
models. This repository organizes the reproducible downstream workflows used
|
|
39
|
+
to evaluate chronological-age prediction, quantify generalization across array
|
|
40
|
+
platforms, tissues, and species, and study biological-age associations with
|
|
41
|
+
disease and interventions.
|
|
42
|
+
|
|
43
|
+
## Environment
|
|
44
|
+
|
|
45
|
+
Install [uv](https://docs.astral.sh/uv/) and create the locked Python 3.11
|
|
46
|
+
environment from the repository root:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv sync --frozen
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## MetaClock Python package
|
|
53
|
+
|
|
54
|
+
The `aging_metaclock` distribution provides the importable `metaclock` package.
|
|
55
|
+
Install it from PyPI using Python 3.11:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python -m pip install aging_metaclock
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For local development, install from the repository root:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install -e .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Input contract
|
|
68
|
+
|
|
69
|
+
`metaclock.fit()` accepts an in-memory `anndata.AnnData` object or an `.h5ad`
|
|
70
|
+
file in pyaging's standard orientation:
|
|
71
|
+
|
|
72
|
+
- rows are samples;
|
|
73
|
+
- columns are DNAm or phenotype features;
|
|
74
|
+
- feature names are stored in `adata.var_names`;
|
|
75
|
+
- the numeric training response is stored in `adata.obs["age"]` by default.
|
|
76
|
+
|
|
77
|
+
Clocks that use phenotype covariates, such as GrimAge, require those covariates
|
|
78
|
+
in the input matrix and `adata.var_names` according to pyaging's input rules.
|
|
79
|
+
|
|
80
|
+
### Fit and predict
|
|
81
|
+
|
|
82
|
+
Pass any ordered list of pyaging component-model names. The fitted result is an
|
|
83
|
+
sklearn-compatible pipeline whose `predict()` method first runs the same
|
|
84
|
+
components in the same order and then applies the trained meta-regressor:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
import metaclock
|
|
88
|
+
|
|
89
|
+
component_model_list = [
|
|
90
|
+
"horvath2013",
|
|
91
|
+
"hannum",
|
|
92
|
+
"dnamphenoage",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
model = metaclock.fit(
|
|
96
|
+
train_adata,
|
|
97
|
+
component_model_list,
|
|
98
|
+
"elastic_net",
|
|
99
|
+
target="age",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
predicted_age = model.predict(test_adata)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The three positional arguments are the input data, component-model list, and
|
|
106
|
+
model architecture. Set `pyaging_dir` when using a specific local pyaging
|
|
107
|
+
weight cache. The component ordering used for fitting is retained for every
|
|
108
|
+
later prediction.
|
|
109
|
+
|
|
110
|
+
To run the component clocks without fitting a meta-model:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
component_outputs = metaclock.predict_components(
|
|
114
|
+
test_adata,
|
|
115
|
+
component_model_list,
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Supported architectures are `elastic_net`, `lasso`, `ridge`,
|
|
120
|
+
`linear_regression`, `random_forest`, `svr`, `mlp`, and `lightgbm`. The `mlp`
|
|
121
|
+
architecture uses `StandardScaler` followed by sklearn's `MLPRegressor`; its
|
|
122
|
+
network parameters can be overridden through `model_params`. LightGBM is part
|
|
123
|
+
of the standard locked environment and uses its sklearn-compatible
|
|
124
|
+
`LGBMRegressor` interface.
|
|
125
|
+
|
|
126
|
+
Fitted models can be saved with joblib and loaded for later prediction:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import joblib
|
|
130
|
+
|
|
131
|
+
joblib.dump(model, model_output_path)
|
|
132
|
+
loaded_model = joblib.load(model_output_path)
|
|
133
|
+
predicted_age = loaded_model.predict(test_adata)
|
|
134
|
+
```
|
|
135
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# MetaClock workflow
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
This repository contains workflows for chronological-age
|
|
6
|
+
prediction, cross-domain generalization, biological-age and disease analysis,
|
|
7
|
+
functional enrichment, and intervention analysis. GEO workflows download inputs
|
|
8
|
+
into repository-local data directories. The workflow READMEs describe the required files and preparation
|
|
9
|
+
steps.
|
|
10
|
+
|
|
11
|
+
## Project overview
|
|
12
|
+
|
|
13
|
+
MetaClock is an integrated framework that combines
|
|
14
|
+
predictions from multiple established epigenetic clocks into meta
|
|
15
|
+
models. This repository organizes the reproducible downstream workflows used
|
|
16
|
+
to evaluate chronological-age prediction, quantify generalization across array
|
|
17
|
+
platforms, tissues, and species, and study biological-age associations with
|
|
18
|
+
disease and interventions.
|
|
19
|
+
|
|
20
|
+
## Environment
|
|
21
|
+
|
|
22
|
+
Install [uv](https://docs.astral.sh/uv/) and create the locked Python 3.11
|
|
23
|
+
environment from the repository root:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv sync --frozen
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## MetaClock Python package
|
|
30
|
+
|
|
31
|
+
The `aging_metaclock` distribution provides the importable `metaclock` package.
|
|
32
|
+
Install it from PyPI using Python 3.11:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -m pip install aging_metaclock
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For local development, install from the repository root:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python -m pip install -e .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Input contract
|
|
45
|
+
|
|
46
|
+
`metaclock.fit()` accepts an in-memory `anndata.AnnData` object or an `.h5ad`
|
|
47
|
+
file in pyaging's standard orientation:
|
|
48
|
+
|
|
49
|
+
- rows are samples;
|
|
50
|
+
- columns are DNAm or phenotype features;
|
|
51
|
+
- feature names are stored in `adata.var_names`;
|
|
52
|
+
- the numeric training response is stored in `adata.obs["age"]` by default.
|
|
53
|
+
|
|
54
|
+
Clocks that use phenotype covariates, such as GrimAge, require those covariates
|
|
55
|
+
in the input matrix and `adata.var_names` according to pyaging's input rules.
|
|
56
|
+
|
|
57
|
+
### Fit and predict
|
|
58
|
+
|
|
59
|
+
Pass any ordered list of pyaging component-model names. The fitted result is an
|
|
60
|
+
sklearn-compatible pipeline whose `predict()` method first runs the same
|
|
61
|
+
components in the same order and then applies the trained meta-regressor:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
import metaclock
|
|
65
|
+
|
|
66
|
+
component_model_list = [
|
|
67
|
+
"horvath2013",
|
|
68
|
+
"hannum",
|
|
69
|
+
"dnamphenoage",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
model = metaclock.fit(
|
|
73
|
+
train_adata,
|
|
74
|
+
component_model_list,
|
|
75
|
+
"elastic_net",
|
|
76
|
+
target="age",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
predicted_age = model.predict(test_adata)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The three positional arguments are the input data, component-model list, and
|
|
83
|
+
model architecture. Set `pyaging_dir` when using a specific local pyaging
|
|
84
|
+
weight cache. The component ordering used for fitting is retained for every
|
|
85
|
+
later prediction.
|
|
86
|
+
|
|
87
|
+
To run the component clocks without fitting a meta-model:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
component_outputs = metaclock.predict_components(
|
|
91
|
+
test_adata,
|
|
92
|
+
component_model_list,
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Supported architectures are `elastic_net`, `lasso`, `ridge`,
|
|
97
|
+
`linear_regression`, `random_forest`, `svr`, `mlp`, and `lightgbm`. The `mlp`
|
|
98
|
+
architecture uses `StandardScaler` followed by sklearn's `MLPRegressor`; its
|
|
99
|
+
network parameters can be overridden through `model_params`. LightGBM is part
|
|
100
|
+
of the standard locked environment and uses its sklearn-compatible
|
|
101
|
+
`LGBMRegressor` interface.
|
|
102
|
+
|
|
103
|
+
Fitted models can be saved with joblib and loaded for later prediction:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import joblib
|
|
107
|
+
|
|
108
|
+
joblib.dump(model, model_output_path)
|
|
109
|
+
loaded_model = joblib.load(model_output_path)
|
|
110
|
+
predicted_age = loaded_model.predict(test_adata)
|
|
111
|
+
```
|
|
112
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aging_metaclock
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reproducible analysis and plotting workflows for MetaClock
|
|
5
|
+
Requires-Python: <3.12,>=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: anndata==0.12.7
|
|
8
|
+
Requires-Dist: joblib==1.5.2
|
|
9
|
+
Requires-Dist: lightgbm==4.6.0
|
|
10
|
+
Requires-Dist: lifelines==0.30.3
|
|
11
|
+
Requires-Dist: matplotlib==3.10.8
|
|
12
|
+
Requires-Dist: mrmr-selection==0.2.8
|
|
13
|
+
Requires-Dist: numpy==2.3.5
|
|
14
|
+
Requires-Dist: pandas==2.3.3
|
|
15
|
+
Requires-Dist: pymupdf==1.26.6
|
|
16
|
+
Requires-Dist: pyarrow==23.0.1
|
|
17
|
+
Requires-Dist: pyaging==0.1.30
|
|
18
|
+
Requires-Dist: rdata==1.0.0
|
|
19
|
+
Requires-Dist: scikit-learn==1.8.0
|
|
20
|
+
Requires-Dist: scipy==1.17.0
|
|
21
|
+
Requires-Dist: statsmodels==0.14.6
|
|
22
|
+
Requires-Dist: torch==2.9.1
|
|
23
|
+
|
|
24
|
+
# MetaClock workflow
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
This repository contains workflows for chronological-age
|
|
29
|
+
prediction, cross-domain generalization, biological-age and disease analysis,
|
|
30
|
+
functional enrichment, and intervention analysis. GEO workflows download inputs
|
|
31
|
+
into repository-local data directories. The workflow READMEs describe the required files and preparation
|
|
32
|
+
steps.
|
|
33
|
+
|
|
34
|
+
## Project overview
|
|
35
|
+
|
|
36
|
+
MetaClock is an integrated framework that combines
|
|
37
|
+
predictions from multiple established epigenetic clocks into meta
|
|
38
|
+
models. This repository organizes the reproducible downstream workflows used
|
|
39
|
+
to evaluate chronological-age prediction, quantify generalization across array
|
|
40
|
+
platforms, tissues, and species, and study biological-age associations with
|
|
41
|
+
disease and interventions.
|
|
42
|
+
|
|
43
|
+
## Environment
|
|
44
|
+
|
|
45
|
+
Install [uv](https://docs.astral.sh/uv/) and create the locked Python 3.11
|
|
46
|
+
environment from the repository root:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv sync --frozen
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## MetaClock Python package
|
|
53
|
+
|
|
54
|
+
The `aging_metaclock` distribution provides the importable `metaclock` package.
|
|
55
|
+
Install it from PyPI using Python 3.11:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python -m pip install aging_metaclock
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For local development, install from the repository root:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install -e .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Input contract
|
|
68
|
+
|
|
69
|
+
`metaclock.fit()` accepts an in-memory `anndata.AnnData` object or an `.h5ad`
|
|
70
|
+
file in pyaging's standard orientation:
|
|
71
|
+
|
|
72
|
+
- rows are samples;
|
|
73
|
+
- columns are DNAm or phenotype features;
|
|
74
|
+
- feature names are stored in `adata.var_names`;
|
|
75
|
+
- the numeric training response is stored in `adata.obs["age"]` by default.
|
|
76
|
+
|
|
77
|
+
Clocks that use phenotype covariates, such as GrimAge, require those covariates
|
|
78
|
+
in the input matrix and `adata.var_names` according to pyaging's input rules.
|
|
79
|
+
|
|
80
|
+
### Fit and predict
|
|
81
|
+
|
|
82
|
+
Pass any ordered list of pyaging component-model names. The fitted result is an
|
|
83
|
+
sklearn-compatible pipeline whose `predict()` method first runs the same
|
|
84
|
+
components in the same order and then applies the trained meta-regressor:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
import metaclock
|
|
88
|
+
|
|
89
|
+
component_model_list = [
|
|
90
|
+
"horvath2013",
|
|
91
|
+
"hannum",
|
|
92
|
+
"dnamphenoage",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
model = metaclock.fit(
|
|
96
|
+
train_adata,
|
|
97
|
+
component_model_list,
|
|
98
|
+
"elastic_net",
|
|
99
|
+
target="age",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
predicted_age = model.predict(test_adata)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The three positional arguments are the input data, component-model list, and
|
|
106
|
+
model architecture. Set `pyaging_dir` when using a specific local pyaging
|
|
107
|
+
weight cache. The component ordering used for fitting is retained for every
|
|
108
|
+
later prediction.
|
|
109
|
+
|
|
110
|
+
To run the component clocks without fitting a meta-model:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
component_outputs = metaclock.predict_components(
|
|
114
|
+
test_adata,
|
|
115
|
+
component_model_list,
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Supported architectures are `elastic_net`, `lasso`, `ridge`,
|
|
120
|
+
`linear_regression`, `random_forest`, `svr`, `mlp`, and `lightgbm`. The `mlp`
|
|
121
|
+
architecture uses `StandardScaler` followed by sklearn's `MLPRegressor`; its
|
|
122
|
+
network parameters can be overridden through `model_params`. LightGBM is part
|
|
123
|
+
of the standard locked environment and uses its sklearn-compatible
|
|
124
|
+
`LGBMRegressor` interface.
|
|
125
|
+
|
|
126
|
+
Fitted models can be saved with joblib and loaded for later prediction:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import joblib
|
|
130
|
+
|
|
131
|
+
joblib.dump(model, model_output_path)
|
|
132
|
+
loaded_model = joblib.load(model_output_path)
|
|
133
|
+
predicted_age = loaded_model.predict(test_adata)
|
|
134
|
+
```
|
|
135
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
aging_metaclock.egg-info/PKG-INFO
|
|
4
|
+
aging_metaclock.egg-info/SOURCES.txt
|
|
5
|
+
aging_metaclock.egg-info/dependency_links.txt
|
|
6
|
+
aging_metaclock.egg-info/requires.txt
|
|
7
|
+
aging_metaclock.egg-info/top_level.txt
|
|
8
|
+
metaclock/__init__.py
|
|
9
|
+
metaclock/_version.py
|
|
10
|
+
metaclock/api.py
|
|
11
|
+
metaclock/components.py
|
|
12
|
+
metaclock/models.py
|
|
13
|
+
metaclock/presets.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
anndata==0.12.7
|
|
2
|
+
joblib==1.5.2
|
|
3
|
+
lightgbm==4.6.0
|
|
4
|
+
lifelines==0.30.3
|
|
5
|
+
matplotlib==3.10.8
|
|
6
|
+
mrmr-selection==0.2.8
|
|
7
|
+
numpy==2.3.5
|
|
8
|
+
pandas==2.3.3
|
|
9
|
+
pymupdf==1.26.6
|
|
10
|
+
pyarrow==23.0.1
|
|
11
|
+
pyaging==0.1.30
|
|
12
|
+
rdata==1.0.0
|
|
13
|
+
scikit-learn==1.8.0
|
|
14
|
+
scipy==1.17.0
|
|
15
|
+
statsmodels==0.14.6
|
|
16
|
+
torch==2.9.1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
metaclock
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""MetaClock: fit meta-regressors from pyaging component clocks."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .api import fit
|
|
5
|
+
from .components import PyAgingTransformer, predict_components
|
|
6
|
+
from .models import available_architectures, make_estimator
|
|
7
|
+
from .presets import CHRONOLOGICAL_16_COMPONENT_MODELS
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"PyAgingTransformer",
|
|
11
|
+
"CHRONOLOGICAL_16_COMPONENT_MODELS",
|
|
12
|
+
"__version__",
|
|
13
|
+
"available_architectures",
|
|
14
|
+
"fit",
|
|
15
|
+
"make_estimator",
|
|
16
|
+
"predict_components",
|
|
17
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Public MetaClock fitting API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import platform
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
7
|
+
from os import PathLike
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Mapping, Sequence
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from sklearn.pipeline import Pipeline
|
|
13
|
+
|
|
14
|
+
from ._version import __version__
|
|
15
|
+
from .components import (
|
|
16
|
+
InputData,
|
|
17
|
+
PyAgingTransformer,
|
|
18
|
+
load_anndata,
|
|
19
|
+
normalize_component_models,
|
|
20
|
+
)
|
|
21
|
+
from .models import make_estimator, normalize_architecture
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from anndata import AnnData
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _package_version(distribution: str) -> str:
|
|
28
|
+
try:
|
|
29
|
+
return version(distribution)
|
|
30
|
+
except PackageNotFoundError:
|
|
31
|
+
return "not-installed"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_target(adata: AnnData, target: str) -> np.ndarray:
|
|
35
|
+
if not isinstance(target, str) or not target.strip():
|
|
36
|
+
raise TypeError("target must be a non-empty adata.obs column name.")
|
|
37
|
+
if target not in adata.obs.columns:
|
|
38
|
+
raise KeyError(f"AnnData obs does not contain target column {target!r}.")
|
|
39
|
+
series = pd.to_numeric(adata.obs[target], errors="coerce")
|
|
40
|
+
values = series.to_numpy(dtype=np.float64, copy=True)
|
|
41
|
+
if not np.isfinite(values).all():
|
|
42
|
+
affected = adata.obs_names[~np.isfinite(values)].astype(str).tolist()[:5]
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"Target column {target!r} contains missing or non-finite values; "
|
|
45
|
+
f"first affected samples: {affected}"
|
|
46
|
+
)
|
|
47
|
+
return values
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _safe_params(value: Any) -> Any:
|
|
51
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
52
|
+
return value
|
|
53
|
+
if isinstance(value, Mapping):
|
|
54
|
+
return {str(key): _safe_params(item) for key, item in value.items()}
|
|
55
|
+
if isinstance(value, (list, tuple)):
|
|
56
|
+
return [_safe_params(item) for item in value]
|
|
57
|
+
if isinstance(value, np.ndarray):
|
|
58
|
+
return value.tolist()
|
|
59
|
+
return repr(value)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def fit(
|
|
63
|
+
input_data: InputData,
|
|
64
|
+
component_models: Sequence[str] | str,
|
|
65
|
+
architecture: str,
|
|
66
|
+
*,
|
|
67
|
+
target: str = "age",
|
|
68
|
+
pyaging_dir: str | PathLike[str] | None = "pyaging_data",
|
|
69
|
+
batch_size: int = 1024,
|
|
70
|
+
clean: bool = True,
|
|
71
|
+
verbose: bool = False,
|
|
72
|
+
copy: bool = False,
|
|
73
|
+
random_state: int = 42,
|
|
74
|
+
n_jobs: int | None = 1,
|
|
75
|
+
model_params: Mapping[str, Any] | None = None,
|
|
76
|
+
) -> Pipeline:
|
|
77
|
+
"""Fit a MetaClock from a pyaging-standard AnnData input.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
input_data
|
|
82
|
+
An ``AnnData`` object or an ``.h5ad`` path. Rows are samples, columns
|
|
83
|
+
are pyaging features, and ``adata.obs[target]`` supplies the response.
|
|
84
|
+
component_models
|
|
85
|
+
One or more pyaging clock names. Their prediction order is preserved.
|
|
86
|
+
architecture
|
|
87
|
+
A name returned by ``metaclock.available_architectures()``.
|
|
88
|
+
target
|
|
89
|
+
Numeric ``adata.obs`` target column. Defaults to ``"age"``.
|
|
90
|
+
pyaging_dir
|
|
91
|
+
PyAging model-cache directory, or ``None`` to use PyAging's default.
|
|
92
|
+
batch_size, clean, verbose
|
|
93
|
+
Values forwarded to PyAging component-clock inference.
|
|
94
|
+
copy
|
|
95
|
+
If true, copy in-memory ``AnnData`` before PyAging inference. With the
|
|
96
|
+
default false, PyAging may update the supplied object in place.
|
|
97
|
+
random_state
|
|
98
|
+
Seed used by regressors and shuffled cross-validation where supported.
|
|
99
|
+
n_jobs
|
|
100
|
+
Worker count forwarded to regressors that support parallel execution.
|
|
101
|
+
model_params
|
|
102
|
+
Overrides passed directly to the final regression estimator.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
sklearn.pipeline.Pipeline
|
|
107
|
+
A fitted pipeline. Calling ``predict()`` with another pyaging-standard
|
|
108
|
+
``AnnData`` object or ``.h5ad`` path reruns the same component clocks
|
|
109
|
+
before applying the fitted regressor.
|
|
110
|
+
"""
|
|
111
|
+
adata = load_anndata(input_data)
|
|
112
|
+
components = normalize_component_models(component_models)
|
|
113
|
+
architecture_name = normalize_architecture(architecture)
|
|
114
|
+
if target.lower() in components:
|
|
115
|
+
raise ValueError("target cannot also be a component-model output column.")
|
|
116
|
+
y = _extract_target(adata, target)
|
|
117
|
+
|
|
118
|
+
transformer = PyAgingTransformer(
|
|
119
|
+
components,
|
|
120
|
+
pyaging_dir=pyaging_dir,
|
|
121
|
+
batch_size=batch_size,
|
|
122
|
+
clean=clean,
|
|
123
|
+
verbose=verbose,
|
|
124
|
+
copy=copy,
|
|
125
|
+
)
|
|
126
|
+
estimator = make_estimator(
|
|
127
|
+
architecture_name,
|
|
128
|
+
random_state=random_state,
|
|
129
|
+
n_jobs=n_jobs,
|
|
130
|
+
model_params=model_params,
|
|
131
|
+
)
|
|
132
|
+
model = Pipeline([("components", transformer), ("regressor", estimator)])
|
|
133
|
+
model.fit(adata, y)
|
|
134
|
+
|
|
135
|
+
model.metaclock_architecture_ = architecture_name
|
|
136
|
+
model.metaclock_component_models_ = components
|
|
137
|
+
model.metaclock_target_ = target
|
|
138
|
+
model.metaclock_provenance_ = {
|
|
139
|
+
"package_versions": {
|
|
140
|
+
"python": platform.python_version(),
|
|
141
|
+
"metaclock": __version__,
|
|
142
|
+
"pyaging": _package_version("pyaging"),
|
|
143
|
+
"anndata": _package_version("anndata"),
|
|
144
|
+
"numpy": _package_version("numpy"),
|
|
145
|
+
"pandas": _package_version("pandas"),
|
|
146
|
+
"scikit-learn": _package_version("scikit-learn"),
|
|
147
|
+
"lightgbm": _package_version("lightgbm")
|
|
148
|
+
if architecture_name == "lightgbm"
|
|
149
|
+
else None,
|
|
150
|
+
},
|
|
151
|
+
"fit_parameters": {
|
|
152
|
+
"target": target,
|
|
153
|
+
"component_models": list(components),
|
|
154
|
+
"architecture": architecture_name,
|
|
155
|
+
"pyaging_dir": None if pyaging_dir is None else str(pyaging_dir),
|
|
156
|
+
"batch_size": batch_size,
|
|
157
|
+
"clean": clean,
|
|
158
|
+
"random_state": random_state,
|
|
159
|
+
"n_jobs": n_jobs,
|
|
160
|
+
"model_params": _safe_params(dict(model_params or {})),
|
|
161
|
+
},
|
|
162
|
+
"training_samples": int(adata.n_obs),
|
|
163
|
+
"component_missing_feature_percent": dict(transformer.missing_feature_percent_),
|
|
164
|
+
}
|
|
165
|
+
return model
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""pyaging adapter for generating MetaClock component-model features."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Sequence, TypeAlias, Union
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from sklearn.base import BaseEstimator, TransformerMixin
|
|
12
|
+
from sklearn.utils.validation import check_is_fitted
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from anndata import AnnData
|
|
17
|
+
|
|
18
|
+
InputData: TypeAlias = Union["AnnData", str, PathLike[str]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_component_models(
|
|
22
|
+
component_models: Sequence[str] | str,
|
|
23
|
+
) -> tuple[str, ...]:
|
|
24
|
+
"""Return non-empty, unique, lower-case pyaging clock names."""
|
|
25
|
+
if isinstance(component_models, str):
|
|
26
|
+
raw_models = [component_models]
|
|
27
|
+
else:
|
|
28
|
+
try:
|
|
29
|
+
raw_models = list(component_models)
|
|
30
|
+
except TypeError as exc:
|
|
31
|
+
raise TypeError(
|
|
32
|
+
"component_models must be a clock name or a sequence of names."
|
|
33
|
+
) from exc
|
|
34
|
+
|
|
35
|
+
normalized: list[str] = []
|
|
36
|
+
for model in raw_models:
|
|
37
|
+
if not isinstance(model, str):
|
|
38
|
+
raise TypeError("Every component model name must be a string.")
|
|
39
|
+
name = model.strip().lower()
|
|
40
|
+
if not name:
|
|
41
|
+
raise ValueError("Component model names cannot be empty.")
|
|
42
|
+
normalized.append(name)
|
|
43
|
+
|
|
44
|
+
if not normalized:
|
|
45
|
+
raise ValueError("At least one component model is required.")
|
|
46
|
+
duplicates = sorted({name for name in normalized if normalized.count(name) > 1})
|
|
47
|
+
if duplicates:
|
|
48
|
+
raise ValueError(f"component_models contains duplicates: {duplicates}")
|
|
49
|
+
return tuple(normalized)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_anndata(input_data: InputData) -> AnnData:
|
|
53
|
+
"""Load a pyaging-standard AnnData object from memory or an .h5ad file."""
|
|
54
|
+
try:
|
|
55
|
+
import anndata as ad
|
|
56
|
+
except ImportError as exc: # pragma: no cover - declared core dependency
|
|
57
|
+
raise ImportError("anndata is required to load MetaClock input data.") from exc
|
|
58
|
+
|
|
59
|
+
if isinstance(input_data, ad.AnnData):
|
|
60
|
+
adata = input_data
|
|
61
|
+
elif isinstance(input_data, (str, PathLike)):
|
|
62
|
+
path = Path(input_data).expanduser()
|
|
63
|
+
if not path.is_file():
|
|
64
|
+
raise FileNotFoundError(f"AnnData input file does not exist: {path}")
|
|
65
|
+
try:
|
|
66
|
+
adata = ad.read_h5ad(path)
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
raise ValueError(f"Could not read AnnData .h5ad input: {path}") from exc
|
|
69
|
+
else:
|
|
70
|
+
raise TypeError(
|
|
71
|
+
"input_data must be an anndata.AnnData object or a path to an .h5ad file."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if adata.n_obs == 0:
|
|
75
|
+
raise ValueError("AnnData input has no samples.")
|
|
76
|
+
if adata.n_vars == 0:
|
|
77
|
+
raise ValueError("AnnData input has no features.")
|
|
78
|
+
if not adata.obs_names.is_unique:
|
|
79
|
+
raise ValueError("AnnData obs_names must be unique.")
|
|
80
|
+
if not adata.var_names.is_unique:
|
|
81
|
+
raise ValueError("AnnData var_names must be unique.")
|
|
82
|
+
return adata.copy() if adata.is_view else adata
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PyAgingTransformer(TransformerMixin, BaseEstimator):
|
|
86
|
+
"""Transform a pyaging-standard AnnData object into component predictions.
|
|
87
|
+
|
|
88
|
+
The transformer deliberately delegates clock loading, feature alignment,
|
|
89
|
+
preprocessing, inference, and postprocessing to pyaging. Its output column
|
|
90
|
+
order always matches ``component_models``.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
component_models: Sequence[str] | str,
|
|
96
|
+
*,
|
|
97
|
+
pyaging_dir: str | PathLike[str] | None = "pyaging_data",
|
|
98
|
+
batch_size: int = 1024,
|
|
99
|
+
clean: bool = True,
|
|
100
|
+
verbose: bool = False,
|
|
101
|
+
copy: bool = False,
|
|
102
|
+
) -> None:
|
|
103
|
+
self.component_models = component_models
|
|
104
|
+
self.pyaging_dir = pyaging_dir
|
|
105
|
+
self.batch_size = batch_size
|
|
106
|
+
self.clean = clean
|
|
107
|
+
self.verbose = verbose
|
|
108
|
+
self.copy = copy
|
|
109
|
+
|
|
110
|
+
def fit(self, X: InputData, y: Any = None) -> PyAgingTransformer:
|
|
111
|
+
"""Validate clock names and record their output order without fitting clocks."""
|
|
112
|
+
del X, y
|
|
113
|
+
self.component_models_ = normalize_component_models(self.component_models)
|
|
114
|
+
if not isinstance(self.batch_size, int) or self.batch_size < 1:
|
|
115
|
+
raise ValueError("batch_size must be a positive integer.")
|
|
116
|
+
self.n_features_out_ = len(self.component_models_)
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def transform(self, X: InputData) -> pd.DataFrame:
|
|
120
|
+
"""Run PyAging in place (on a copy if requested) and return finite predictions."""
|
|
121
|
+
check_is_fitted(self, "component_models_")
|
|
122
|
+
adata = load_anndata(X)
|
|
123
|
+
if self.copy:
|
|
124
|
+
adata = adata.copy()
|
|
125
|
+
|
|
126
|
+
kwargs: dict[str, Any] = {
|
|
127
|
+
"clock_names": list(self.component_models_),
|
|
128
|
+
"batch_size": self.batch_size,
|
|
129
|
+
"clean": self.clean,
|
|
130
|
+
"verbose": self.verbose,
|
|
131
|
+
}
|
|
132
|
+
if self.pyaging_dir is not None:
|
|
133
|
+
kwargs["dir"] = str(self.pyaging_dir)
|
|
134
|
+
|
|
135
|
+
import pyaging as pya
|
|
136
|
+
|
|
137
|
+
# PyAging 0.1.30 populates adata.obs in place and returns None.
|
|
138
|
+
pya.pred.predict_age(adata, **kwargs)
|
|
139
|
+
|
|
140
|
+
column_lookup: dict[str, Any] = {}
|
|
141
|
+
for column in adata.obs.columns:
|
|
142
|
+
lowered = str(column).lower()
|
|
143
|
+
if lowered in column_lookup and column_lookup[lowered] != column:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"AnnData obs has ambiguous case-insensitive column {lowered!r}."
|
|
146
|
+
)
|
|
147
|
+
column_lookup[lowered] = column
|
|
148
|
+
missing = [name for name in self.component_models_ if name not in column_lookup]
|
|
149
|
+
if missing:
|
|
150
|
+
raise RuntimeError(
|
|
151
|
+
f"pyaging did not create component output columns: {missing}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
ordered_columns = [column_lookup[name] for name in self.component_models_]
|
|
155
|
+
frame = adata.obs.loc[:, ordered_columns].apply(pd.to_numeric, errors="coerce")
|
|
156
|
+
values = frame.to_numpy(dtype=np.float64, copy=True)
|
|
157
|
+
if not np.isfinite(values).all():
|
|
158
|
+
bad_rows, bad_columns = np.where(~np.isfinite(values))
|
|
159
|
+
examples = [
|
|
160
|
+
(str(adata.obs_names[row]), self.component_models_[column])
|
|
161
|
+
for row, column in zip(bad_rows[:5], bad_columns[:5])
|
|
162
|
+
]
|
|
163
|
+
raise ValueError(
|
|
164
|
+
"pyaging component outputs contain missing or non-finite values; "
|
|
165
|
+
f"first affected (sample, component) pairs: {examples}"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
self.missing_feature_percent_ = {
|
|
169
|
+
name: float(adata.uns.get(f"{name}_percent_na", np.nan))
|
|
170
|
+
for name in self.component_models_
|
|
171
|
+
}
|
|
172
|
+
return pd.DataFrame(
|
|
173
|
+
values,
|
|
174
|
+
index=adata.obs_names.copy(),
|
|
175
|
+
columns=self.get_feature_names_out(),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def get_feature_names_out(self, input_features: Any = None) -> np.ndarray:
|
|
179
|
+
"""Return fitted component names in prediction-column order."""
|
|
180
|
+
del input_features
|
|
181
|
+
check_is_fitted(self, "component_models_")
|
|
182
|
+
return np.asarray(self.component_models_, dtype=object)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def predict_components(
|
|
186
|
+
input_data: InputData,
|
|
187
|
+
component_models: Sequence[str] | str,
|
|
188
|
+
*,
|
|
189
|
+
pyaging_dir: str | PathLike[str] | None = "pyaging_data",
|
|
190
|
+
batch_size: int = 1024,
|
|
191
|
+
clean: bool = True,
|
|
192
|
+
verbose: bool = False,
|
|
193
|
+
copy: bool = False,
|
|
194
|
+
) -> pd.DataFrame:
|
|
195
|
+
"""Return ordered pyaging component predictions as a DataFrame."""
|
|
196
|
+
adata = load_anndata(input_data)
|
|
197
|
+
transformer = PyAgingTransformer(
|
|
198
|
+
component_models,
|
|
199
|
+
pyaging_dir=pyaging_dir,
|
|
200
|
+
batch_size=batch_size,
|
|
201
|
+
clean=clean,
|
|
202
|
+
verbose=verbose,
|
|
203
|
+
copy=copy,
|
|
204
|
+
)
|
|
205
|
+
values = transformer.fit_transform(adata)
|
|
206
|
+
return pd.DataFrame(
|
|
207
|
+
values,
|
|
208
|
+
index=adata.obs_names.copy(),
|
|
209
|
+
columns=transformer.get_feature_names_out(),
|
|
210
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Maintained sklearn and LightGBM MetaClock estimator factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from sklearn.base import BaseEstimator
|
|
10
|
+
from sklearn.ensemble import RandomForestRegressor
|
|
11
|
+
from sklearn.linear_model import ElasticNetCV, LassoCV, LinearRegression, RidgeCV
|
|
12
|
+
from sklearn.model_selection import KFold
|
|
13
|
+
from sklearn.neural_network import MLPRegressor
|
|
14
|
+
from sklearn.pipeline import Pipeline
|
|
15
|
+
from sklearn.preprocessing import StandardScaler
|
|
16
|
+
from sklearn.svm import SVR
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_ARCHITECTURES = (
|
|
20
|
+
"elastic_net",
|
|
21
|
+
"lasso",
|
|
22
|
+
"ridge",
|
|
23
|
+
"linear_regression",
|
|
24
|
+
"random_forest",
|
|
25
|
+
"svr",
|
|
26
|
+
"mlp",
|
|
27
|
+
"lightgbm",
|
|
28
|
+
)
|
|
29
|
+
_ALIASES = {
|
|
30
|
+
"elasticnet": "elastic_net",
|
|
31
|
+
"enet": "elastic_net",
|
|
32
|
+
"linear": "linear_regression",
|
|
33
|
+
"ols": "linear_regression",
|
|
34
|
+
"rf": "random_forest",
|
|
35
|
+
"randomforest": "random_forest",
|
|
36
|
+
"svm": "svr",
|
|
37
|
+
"mlp_regressor": "mlp",
|
|
38
|
+
"neural_network": "mlp",
|
|
39
|
+
"lgbm": "lightgbm",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def available_architectures() -> tuple[str, ...]:
|
|
44
|
+
"""Return supported canonical architecture names."""
|
|
45
|
+
return _ARCHITECTURES
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize_architecture(architecture: str) -> str:
|
|
49
|
+
"""Normalize architecture spelling and aliases."""
|
|
50
|
+
if not isinstance(architecture, str):
|
|
51
|
+
raise TypeError("architecture must be a string.")
|
|
52
|
+
name = architecture.strip().lower().replace("-", "_").replace(" ", "_")
|
|
53
|
+
name = _ALIASES.get(name, name)
|
|
54
|
+
if name not in _ARCHITECTURES:
|
|
55
|
+
choices = ", ".join(_ARCHITECTURES)
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Unknown architecture {architecture!r}. Choose one of: {choices}."
|
|
58
|
+
)
|
|
59
|
+
return name
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _scaled(model: BaseEstimator) -> Pipeline:
|
|
63
|
+
return Pipeline([("scaler", StandardScaler()), ("model", model)])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def make_estimator(
|
|
67
|
+
architecture: str,
|
|
68
|
+
*,
|
|
69
|
+
random_state: int = 42,
|
|
70
|
+
n_jobs: int | None = 1,
|
|
71
|
+
model_params: Mapping[str, Any] | None = None,
|
|
72
|
+
) -> BaseEstimator:
|
|
73
|
+
"""Build an unfitted regressor using maintained package implementations.
|
|
74
|
+
|
|
75
|
+
``model_params`` are passed directly to the final estimator. Scaled linear
|
|
76
|
+
models are returned as sklearn Pipelines with ``scaler`` and ``model``
|
|
77
|
+
steps. LightGBM is imported only when requested.
|
|
78
|
+
"""
|
|
79
|
+
name = normalize_architecture(architecture)
|
|
80
|
+
params = dict(model_params or {})
|
|
81
|
+
alphas = np.logspace(-5, 0, 30)
|
|
82
|
+
cv = KFold(n_splits=5, shuffle=True, random_state=random_state)
|
|
83
|
+
|
|
84
|
+
if name == "elastic_net":
|
|
85
|
+
defaults: dict[str, Any] = {
|
|
86
|
+
"alphas": alphas,
|
|
87
|
+
"cv": cv,
|
|
88
|
+
"max_iter": 20_000,
|
|
89
|
+
"n_jobs": n_jobs,
|
|
90
|
+
}
|
|
91
|
+
defaults.update(params)
|
|
92
|
+
return _scaled(ElasticNetCV(**defaults))
|
|
93
|
+
if name == "lasso":
|
|
94
|
+
defaults = {
|
|
95
|
+
"alphas": alphas,
|
|
96
|
+
"cv": cv,
|
|
97
|
+
"max_iter": 20_000,
|
|
98
|
+
"n_jobs": n_jobs,
|
|
99
|
+
"random_state": random_state,
|
|
100
|
+
}
|
|
101
|
+
defaults.update(params)
|
|
102
|
+
return _scaled(LassoCV(**defaults))
|
|
103
|
+
if name == "ridge":
|
|
104
|
+
defaults = {"alphas": alphas, "cv": cv}
|
|
105
|
+
defaults.update(params)
|
|
106
|
+
return _scaled(RidgeCV(**defaults))
|
|
107
|
+
if name == "linear_regression":
|
|
108
|
+
defaults = {"n_jobs": n_jobs}
|
|
109
|
+
defaults.update(params)
|
|
110
|
+
return _scaled(LinearRegression(**defaults))
|
|
111
|
+
if name == "random_forest":
|
|
112
|
+
defaults = {"random_state": random_state, "n_jobs": n_jobs}
|
|
113
|
+
defaults.update(params)
|
|
114
|
+
return RandomForestRegressor(**defaults)
|
|
115
|
+
if name == "svr":
|
|
116
|
+
defaults = {"kernel": "linear"}
|
|
117
|
+
defaults.update(params)
|
|
118
|
+
return _scaled(SVR(**defaults))
|
|
119
|
+
if name == "mlp":
|
|
120
|
+
defaults = {"max_iter": 2_000, "random_state": random_state}
|
|
121
|
+
defaults.update(params)
|
|
122
|
+
return _scaled(MLPRegressor(**defaults))
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
module = import_module("lightgbm")
|
|
126
|
+
regressor = module.LGBMRegressor
|
|
127
|
+
except (ImportError, AttributeError) as exc:
|
|
128
|
+
raise ImportError(
|
|
129
|
+
"architecture='lightgbm' requires LightGBM, which is a core "
|
|
130
|
+
"metaclock-bio dependency. Reinstall the package dependencies."
|
|
131
|
+
) from exc
|
|
132
|
+
defaults = {"random_state": random_state, "n_jobs": n_jobs, "verbosity": -1}
|
|
133
|
+
defaults.update(params)
|
|
134
|
+
return regressor(**defaults)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Published MetaClock component-model presets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
CHRONOLOGICAL_16_COMPONENT_MODELS: tuple[str, ...] = (
|
|
7
|
+
"horvath2013",
|
|
8
|
+
"hannum",
|
|
9
|
+
"dnamphenoage",
|
|
10
|
+
"altumage",
|
|
11
|
+
"han",
|
|
12
|
+
"encen40",
|
|
13
|
+
"hrsinchphenoage",
|
|
14
|
+
"lin",
|
|
15
|
+
"skinandblood",
|
|
16
|
+
"yingcausage",
|
|
17
|
+
"zhangblup",
|
|
18
|
+
"zhangen",
|
|
19
|
+
"pchorvath2013",
|
|
20
|
+
"pchannum",
|
|
21
|
+
"pcphenoage",
|
|
22
|
+
"pcskinandblood",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["CHRONOLOGICAL_16_COMPONENT_MODELS"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aging_metaclock"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Reproducible analysis and plotting workflows for MetaClock"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11,<3.12"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"anndata==0.12.7",
|
|
13
|
+
"joblib==1.5.2",
|
|
14
|
+
"lightgbm==4.6.0",
|
|
15
|
+
"lifelines==0.30.3",
|
|
16
|
+
"matplotlib==3.10.8",
|
|
17
|
+
"mrmr-selection==0.2.8",
|
|
18
|
+
"numpy==2.3.5",
|
|
19
|
+
"pandas==2.3.3",
|
|
20
|
+
"pymupdf==1.26.6",
|
|
21
|
+
"pyarrow==23.0.1",
|
|
22
|
+
"pyaging==0.1.30",
|
|
23
|
+
"rdata==1.0.0",
|
|
24
|
+
"scikit-learn==1.8.0",
|
|
25
|
+
"scipy==1.17.0",
|
|
26
|
+
"statsmodels==0.14.6",
|
|
27
|
+
"torch==2.9.1",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.uv]
|
|
31
|
+
package = true
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
packages = ["metaclock"]
|