pyaging 0.0.2__tar.gz → 0.0.4__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.
Potentially problematic release.
This version of pyaging might be problematic. Click here for more details.
- {pyaging-0.0.2 → pyaging-0.0.4}/PKG-INFO +3 -38
- {pyaging-0.0.2 → pyaging-0.0.4}/README.md +2 -37
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/logger/_logger.py +4 -3
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/predict/_pred.py +18 -17
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/predict/_pred_utils.py +91 -45
- pyaging-0.0.4/pyaging/predict/_preprocessing.py +57 -0
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/preprocess/_preprocess.py +1 -1
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/preprocess/_preprocess_utils.py +3 -1
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/utils/_utils.py +9 -10
- {pyaging-0.0.2 → pyaging-0.0.4}/pyproject.toml +1 -1
- pyaging-0.0.2/pyaging/predict/_preprocessing.py +0 -28
- {pyaging-0.0.2 → pyaging-0.0.4}/LICENSE +0 -0
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/__init__.py +0 -0
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/data/__init__.py +0 -0
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/data/_data.py +0 -0
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/logger/__init__.py +0 -0
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/models/__init__.py +0 -0
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/models/_models.py +0 -0
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/predict/__init__.py +0 -0
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/predict/_postprocessing.py +0 -0
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/preprocess/__init__.py +0 -0
- {pyaging-0.0.2 → pyaging-0.0.4}/pyaging/utils/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyaging
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: A Python-based compendium of GPU-optimized aging clocks.
|
|
5
5
|
License: BSD
|
|
6
6
|
Author: Lucas Paulo de Lima Camillo
|
|
@@ -43,53 +43,18 @@ Description-Content-Type: text/markdown
|
|
|
43
43
|
|
|
44
44
|
`pyaging` is a cutting-edge Python package designed for the longevity research community, offering a comprehensive suite of GPU-optimized biological aging clocks.
|
|
45
45
|
|
|
46
|
-
[Installation](https://pyaging.readthedocs.io/en/latest/installation.html) - [Search, cite, and get metadata](https://pyaging.readthedocs.io/en/latest/tutorial_utils.html) - [Bulk DNA methylation](https://pyaging.readthedocs.io/en/latest/tutorial_dnam.html) - [Bulk histone mark ChIP-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_histonemarkchipseq.html) - [Bulk ATAC-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_atacseq.html) - [Bulk RNA-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_rnaseq.html) - [API Reference](https://pyaging.readthedocs.io/en/latest/pyaging.html)
|
|
46
|
+
[Installation](https://pyaging.readthedocs.io/en/latest/installation.html) - [Search, cite, and get metadata](https://pyaging.readthedocs.io/en/latest/tutorial_utils.html) - [Bulk DNA methylation](https://pyaging.readthedocs.io/en/latest/tutorial_dnam.html) - [Bulk histone mark ChIP-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_histonemarkchipseq.html) - [Bulk ATAC-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_atacseq.html) - [Bulk RNA-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_rnaseq.html) - [Blood chemistry](https://pyaging.readthedocs.io/en/latest/tutorial_bloodchemistry.html) - [API Reference](https://pyaging.readthedocs.io/en/latest/pyaging.html)
|
|
47
47
|
|
|
48
48
|
With a growing number of aging clocks, comparing and analyzing them can be challenging. `pyaging` simplifies this process, allowing researchers to input various molecular layers (DNA methylation, histone ChIP-Seq, ATAC-seq, transcriptomics, etc.) and quickly analyze them using multiple aging clocks, thanks to its GPU-backed infrastructure. This makes it an ideal tool for large datasets and multi-layered analysis.
|
|
49
49
|
|
|
50
50
|
## 📝 To-Do List
|
|
51
51
|
|
|
52
|
-
- [X] Integrate `black` for PEP8 compliant code formatting
|
|
53
|
-
- [X] Improve and expand `readthedocs` documentation
|
|
54
|
-
- [X] Add option to specify download directory for datasets
|
|
55
|
-
- [X] Rename datasets to reflect source publications
|
|
56
|
-
- [X] Include mammalian array example datasets
|
|
57
|
-
- [X] Add feature to control logging verbosity
|
|
58
|
-
- [X] Publish to PyPi
|
|
59
|
-
- [X] Add percentage of missing features in anndata after age prediction for each clock
|
|
60
|
-
- [X] Add and populate "Notes" tab in metadata
|
|
61
|
-
- [X] Add PhenoAge based on blood chemistry analysis (and datasets)
|
|
62
|
-
- [X] Add table in readthedocs with all clock metadata
|
|
63
52
|
- [ ] Integrate scAge and scRNAseq clocks (and datasets)
|
|
64
53
|
- [ ] Incorporate murine DNA methylation and proteomic clocks (and datasets)
|
|
65
54
|
|
|
66
|
-
## ⚙️ Current update pipeline
|
|
67
|
-
|
|
68
|
-
For version update:
|
|
69
|
-
|
|
70
|
-
- [ ] Update package version in pyproject.toml
|
|
71
|
-
- [ ] Update package version in docs/source/conf.py
|
|
72
|
-
- [ ] `poetry update`
|
|
73
|
-
- [ ] `poetry build`
|
|
74
|
-
- [ ] `poetry run pytest --nbmake tutorials/`
|
|
75
|
-
- [ ] `make html` in docs folder
|
|
76
|
-
- [ ] `git add .`
|
|
77
|
-
- [ ] `git commit -m "commit message"`
|
|
78
|
-
- [ ] `git push`
|
|
79
|
-
- [ ] `git tag -a vX.X.X -m "release message"`
|
|
80
|
-
- [ ] `git push origin vX.X.X`
|
|
81
|
-
|
|
82
|
-
For adding a new clock:
|
|
83
|
-
|
|
84
|
-
- [ ] Add clock notebook
|
|
85
|
-
- [ ] Run `join_metadata.ipynb`
|
|
86
|
-
- [ ] Upload new weights to AWS
|
|
87
|
-
- [ ] Upload new `all_clock_metadata.pt` to AWS
|
|
88
|
-
|
|
89
|
-
|
|
90
55
|
## ❓ Can't find an aging clock?
|
|
91
56
|
|
|
92
|
-
If you have recently developed an aging clock and would like it to be integrated into `pyaging`, please [email us](lucas_camillo@alumni.brown.edu). We aim to incorporate it within two weeks!
|
|
57
|
+
If you have recently developed an aging clock and would like it to be integrated into `pyaging`, please [email us](lucas_camillo@alumni.brown.edu). We aim to incorporate it within two weeks! We are also happy to adapt to any licensing terms for commercial entities.
|
|
93
58
|
|
|
94
59
|
## 💬 Community Discussion
|
|
95
60
|
For coding-related queries, feedback, and discussions, please visit our [GitHub Issues](https://github.com/rsinghlab/pyaging/issues) page.
|
|
@@ -18,53 +18,18 @@
|
|
|
18
18
|
|
|
19
19
|
`pyaging` is a cutting-edge Python package designed for the longevity research community, offering a comprehensive suite of GPU-optimized biological aging clocks.
|
|
20
20
|
|
|
21
|
-
[Installation](https://pyaging.readthedocs.io/en/latest/installation.html) - [Search, cite, and get metadata](https://pyaging.readthedocs.io/en/latest/tutorial_utils.html) - [Bulk DNA methylation](https://pyaging.readthedocs.io/en/latest/tutorial_dnam.html) - [Bulk histone mark ChIP-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_histonemarkchipseq.html) - [Bulk ATAC-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_atacseq.html) - [Bulk RNA-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_rnaseq.html) - [API Reference](https://pyaging.readthedocs.io/en/latest/pyaging.html)
|
|
21
|
+
[Installation](https://pyaging.readthedocs.io/en/latest/installation.html) - [Search, cite, and get metadata](https://pyaging.readthedocs.io/en/latest/tutorial_utils.html) - [Bulk DNA methylation](https://pyaging.readthedocs.io/en/latest/tutorial_dnam.html) - [Bulk histone mark ChIP-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_histonemarkchipseq.html) - [Bulk ATAC-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_atacseq.html) - [Bulk RNA-Seq](https://pyaging.readthedocs.io/en/latest/tutorial_rnaseq.html) - [Blood chemistry](https://pyaging.readthedocs.io/en/latest/tutorial_bloodchemistry.html) - [API Reference](https://pyaging.readthedocs.io/en/latest/pyaging.html)
|
|
22
22
|
|
|
23
23
|
With a growing number of aging clocks, comparing and analyzing them can be challenging. `pyaging` simplifies this process, allowing researchers to input various molecular layers (DNA methylation, histone ChIP-Seq, ATAC-seq, transcriptomics, etc.) and quickly analyze them using multiple aging clocks, thanks to its GPU-backed infrastructure. This makes it an ideal tool for large datasets and multi-layered analysis.
|
|
24
24
|
|
|
25
25
|
## 📝 To-Do List
|
|
26
26
|
|
|
27
|
-
- [X] Integrate `black` for PEP8 compliant code formatting
|
|
28
|
-
- [X] Improve and expand `readthedocs` documentation
|
|
29
|
-
- [X] Add option to specify download directory for datasets
|
|
30
|
-
- [X] Rename datasets to reflect source publications
|
|
31
|
-
- [X] Include mammalian array example datasets
|
|
32
|
-
- [X] Add feature to control logging verbosity
|
|
33
|
-
- [X] Publish to PyPi
|
|
34
|
-
- [X] Add percentage of missing features in anndata after age prediction for each clock
|
|
35
|
-
- [X] Add and populate "Notes" tab in metadata
|
|
36
|
-
- [X] Add PhenoAge based on blood chemistry analysis (and datasets)
|
|
37
|
-
- [X] Add table in readthedocs with all clock metadata
|
|
38
27
|
- [ ] Integrate scAge and scRNAseq clocks (and datasets)
|
|
39
28
|
- [ ] Incorporate murine DNA methylation and proteomic clocks (and datasets)
|
|
40
29
|
|
|
41
|
-
## ⚙️ Current update pipeline
|
|
42
|
-
|
|
43
|
-
For version update:
|
|
44
|
-
|
|
45
|
-
- [ ] Update package version in pyproject.toml
|
|
46
|
-
- [ ] Update package version in docs/source/conf.py
|
|
47
|
-
- [ ] `poetry update`
|
|
48
|
-
- [ ] `poetry build`
|
|
49
|
-
- [ ] `poetry run pytest --nbmake tutorials/`
|
|
50
|
-
- [ ] `make html` in docs folder
|
|
51
|
-
- [ ] `git add .`
|
|
52
|
-
- [ ] `git commit -m "commit message"`
|
|
53
|
-
- [ ] `git push`
|
|
54
|
-
- [ ] `git tag -a vX.X.X -m "release message"`
|
|
55
|
-
- [ ] `git push origin vX.X.X`
|
|
56
|
-
|
|
57
|
-
For adding a new clock:
|
|
58
|
-
|
|
59
|
-
- [ ] Add clock notebook
|
|
60
|
-
- [ ] Run `join_metadata.ipynb`
|
|
61
|
-
- [ ] Upload new weights to AWS
|
|
62
|
-
- [ ] Upload new `all_clock_metadata.pt` to AWS
|
|
63
|
-
|
|
64
|
-
|
|
65
30
|
## ❓ Can't find an aging clock?
|
|
66
31
|
|
|
67
|
-
If you have recently developed an aging clock and would like it to be integrated into `pyaging`, please [email us](lucas_camillo@alumni.brown.edu). We aim to incorporate it within two weeks!
|
|
32
|
+
If you have recently developed an aging clock and would like it to be integrated into `pyaging`, please [email us](lucas_camillo@alumni.brown.edu). We aim to incorporate it within two weeks! We are also happy to adapt to any licensing terms for commercial entities.
|
|
68
33
|
|
|
69
34
|
## 💬 Community Discussion
|
|
70
35
|
For coding-related queries, feedback, and discussions, please visit our [GitHub Issues](https://github.com/rsinghlab/pyaging/issues) page.
|
|
@@ -273,7 +273,8 @@ class Logger:
|
|
|
273
273
|
self.logger_stream_handler.terminator = saved_terminator
|
|
274
274
|
|
|
275
275
|
def start_progress(self, message, indent_level=1, *args, **kwargs):
|
|
276
|
-
|
|
276
|
+
now = time.time()
|
|
277
|
+
self.previous_timestamp = now
|
|
277
278
|
message = format_logging_message(
|
|
278
279
|
"⚙️ " + message, logging.INFO, indent_level=indent_level
|
|
279
280
|
)
|
|
@@ -281,8 +282,8 @@ class Logger:
|
|
|
281
282
|
self.logger_stream_handler.flush()
|
|
282
283
|
|
|
283
284
|
def finish_progress(self, progress_name="", time_unit="s", indent_level=1):
|
|
284
|
-
|
|
285
|
-
|
|
285
|
+
now = time.time()
|
|
286
|
+
self.time_passed = now - self.previous_timestamp
|
|
286
287
|
|
|
287
288
|
saved_terminator = self.logger_stream_handler.terminator
|
|
288
289
|
self.logger_stream_handler.terminator = ""
|
|
@@ -19,15 +19,15 @@ def predict_age(
|
|
|
19
19
|
|
|
20
20
|
Parameters
|
|
21
21
|
----------
|
|
22
|
-
adata
|
|
22
|
+
adata: AnnData
|
|
23
23
|
An AnnData object. The object should have .X attribute for the
|
|
24
24
|
data matrix and .var_names for feature names.
|
|
25
25
|
|
|
26
|
-
clock_names
|
|
26
|
+
clock_names: str or list of str, optional
|
|
27
27
|
Names of the aging clocks to be applied. It can be a single clock name as a string or a list
|
|
28
28
|
of clock names, by default "horvath2013".
|
|
29
29
|
|
|
30
|
-
dir
|
|
30
|
+
dir: str
|
|
31
31
|
The directory to deposit the downloaded file. Defaults to "pyaging_data".
|
|
32
32
|
|
|
33
33
|
verbose: bool
|
|
@@ -45,8 +45,8 @@ def predict_age(
|
|
|
45
45
|
The predicted ages are appended to the .obs attribute of the AnnData object with the clock name as
|
|
46
46
|
the key. The metadata of each clock used in the prediction is stored in the .uns attribute.
|
|
47
47
|
|
|
48
|
-
It is important that the input AnnData object's .X attribute contains data
|
|
49
|
-
|
|
48
|
+
It is important that the input AnnData object's .X attribute contains data suitable for age
|
|
49
|
+
prediction.
|
|
50
50
|
|
|
51
51
|
The function automatically handles the transfer of data and models to the appropriate compute
|
|
52
52
|
device (CPU or GPU) based on system configuration.
|
|
@@ -71,30 +71,30 @@ def predict_age(
|
|
|
71
71
|
device = set_torch_device(logger)
|
|
72
72
|
|
|
73
73
|
for clock_name in clock_names:
|
|
74
|
-
logger.info(f"Processing clock: {clock_name}", indent_level=1)
|
|
74
|
+
logger.info(f"🕒 Processing clock: {clock_name}", indent_level=1)
|
|
75
75
|
|
|
76
76
|
# Load and prepare the clock
|
|
77
77
|
clock_name = clock_name.lower()
|
|
78
|
-
features, weight_dict, preprocessing, postprocessing,
|
|
78
|
+
features, weight_dict, preprocessing, postprocessing, preprocessing_helper, postprocessing_helper = load_clock(
|
|
79
79
|
clock_name, dir, logger, indent_level=2
|
|
80
80
|
)
|
|
81
81
|
|
|
82
82
|
# Check and update adata for missing features
|
|
83
|
-
|
|
83
|
+
adata = check_features_in_adata(
|
|
84
84
|
adata, clock_name, features, logger, indent_level=2
|
|
85
85
|
)
|
|
86
86
|
|
|
87
|
+
# Apply preprocessing
|
|
88
|
+
if preprocessing:
|
|
89
|
+
adata = preprocess_data(
|
|
90
|
+
adata, preprocessing, preprocessing_helper, features, logger, indent_level=2
|
|
91
|
+
)
|
|
92
|
+
|
|
87
93
|
# Filter features and then extract data matrix
|
|
88
94
|
x_numpy = filter_features_and_extract_data(
|
|
89
|
-
|
|
95
|
+
adata, preprocessing, features, logger, indent_level=2
|
|
90
96
|
)
|
|
91
97
|
|
|
92
|
-
# Apply preprocessing if specified
|
|
93
|
-
if preprocessing:
|
|
94
|
-
x_numpy = preprocess_data(
|
|
95
|
-
preprocessing, x_numpy, clock_dict, logger, indent_level=2
|
|
96
|
-
)
|
|
97
|
-
|
|
98
98
|
# Convert numpy array to tensor
|
|
99
99
|
x_tensor = convert_numpy_array_to_tensor(
|
|
100
100
|
x_numpy, device, logger, indent_level=2
|
|
@@ -118,7 +118,7 @@ def predict_age(
|
|
|
118
118
|
# Apply postprocessing if specified
|
|
119
119
|
if postprocessing:
|
|
120
120
|
predicted_ages = postprocess_data(
|
|
121
|
-
|
|
121
|
+
predicted_ages, postprocessing, postprocessing_helper, logger, indent_level=2
|
|
122
122
|
)
|
|
123
123
|
|
|
124
124
|
# Add predicted ages to adata
|
|
@@ -126,11 +126,12 @@ def predict_age(
|
|
|
126
126
|
|
|
127
127
|
# Load all clocks metadata
|
|
128
128
|
all_clock_metadata = load_clock_metadata(dir, logger, indent_level=2)
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
# Add clock metadata to adata object
|
|
131
131
|
add_clock_metadata_adata(
|
|
132
132
|
adata, clock_name, all_clock_metadata, logger, indent_level=2
|
|
133
133
|
)
|
|
134
|
+
|
|
134
135
|
|
|
135
136
|
logger.done()
|
|
136
137
|
return adata
|
|
@@ -8,7 +8,7 @@ import ntpath
|
|
|
8
8
|
from urllib.request import urlretrieve
|
|
9
9
|
|
|
10
10
|
from ..models import *
|
|
11
|
-
from ..utils import
|
|
11
|
+
from ..utils import progress, load_clock_metadata, download
|
|
12
12
|
from ..logger import LoggerManager, main_tqdm, silence_logger
|
|
13
13
|
from ._preprocessing import *
|
|
14
14
|
from ._postprocessing import *
|
|
@@ -48,7 +48,8 @@ def load_clock(clock_name: str, dir: str, logger, indent_level: int = 2) -> Tupl
|
|
|
48
48
|
- weight_dict: A dictionary of weights used in the clock's model.
|
|
49
49
|
- preprocessing: Any preprocessing steps required for the clock's input data.
|
|
50
50
|
- postprocessing: Any postprocessing steps applied to the clock's output.
|
|
51
|
-
-
|
|
51
|
+
- preprocessing_helper: Any preprocessing helper file.
|
|
52
|
+
- postprocessing_helper: Any postprocessing helper file.
|
|
52
53
|
|
|
53
54
|
Notes
|
|
54
55
|
-----
|
|
@@ -62,13 +63,13 @@ def load_clock(clock_name: str, dir: str, logger, indent_level: int = 2) -> Tupl
|
|
|
62
63
|
|
|
63
64
|
Examples
|
|
64
65
|
--------
|
|
65
|
-
>>> features, weight_dict,
|
|
66
|
+
>>> features, weight_dict, _, _, _, _ = load_clock("clock1", "pyaging_data", logger)
|
|
66
67
|
>>> print(features)
|
|
67
68
|
['feature1', 'feature2', ...]
|
|
68
69
|
|
|
69
70
|
"""
|
|
70
71
|
url = f"https://pyaging.s3.amazonaws.com/clocks/weights/{clock_name}.pt"
|
|
71
|
-
download(url, dir, logger, indent_level=
|
|
72
|
+
download(url, dir, logger, indent_level=indent_level)
|
|
72
73
|
|
|
73
74
|
# Define the path to the clock weights file
|
|
74
75
|
weights_path = os.path.join(dir, f"{clock_name}.pt")
|
|
@@ -81,8 +82,10 @@ def load_clock(clock_name: str, dir: str, logger, indent_level: int = 2) -> Tupl
|
|
|
81
82
|
weight_dict = clock_dict["weight_dict"]
|
|
82
83
|
preprocessing = clock_dict.get("preprocessing", None)
|
|
83
84
|
postprocessing = clock_dict.get("postprocessing", None)
|
|
85
|
+
preprocessing_helper = clock_dict.get("preprocessing_helper", None)
|
|
86
|
+
postprocessing_helper = clock_dict.get("postprocessing_helper", None)
|
|
84
87
|
|
|
85
|
-
return features, weight_dict, preprocessing, postprocessing,
|
|
88
|
+
return features, weight_dict, preprocessing, postprocessing, preprocessing_helper, postprocessing_helper
|
|
86
89
|
|
|
87
90
|
|
|
88
91
|
@progress("Check features in adata")
|
|
@@ -176,24 +179,27 @@ def check_features_in_adata(
|
|
|
176
179
|
f"{num_missing_features} out of {total_features} features "
|
|
177
180
|
f"({percent_missing:.2f}%) are missing and will be "
|
|
178
181
|
f"added with default value 0: {missing_features[:np.min([3, num_missing_features])]}, etc.",
|
|
179
|
-
indent_level=
|
|
182
|
+
indent_level=indent_level+1,
|
|
180
183
|
)
|
|
181
184
|
|
|
182
185
|
# Create an empty AnnData object for missing features
|
|
186
|
+
empty_data = np.zeros((adata.n_obs, num_missing_features))
|
|
183
187
|
adata_empty = anndata.AnnData(
|
|
184
|
-
|
|
188
|
+
X=empty_data,
|
|
185
189
|
obs=adata.obs,
|
|
186
190
|
var=pd.DataFrame(index=missing_features),
|
|
191
|
+
layers=dict(zip(adata.layers.keys(), [empty_data] * len(adata.layers)))
|
|
187
192
|
)
|
|
188
193
|
|
|
189
194
|
# Concatenate original adata with the empty adata
|
|
190
|
-
adata = anndata.concat([adata, adata_empty], axis=1)
|
|
195
|
+
adata = anndata.concat([adata, adata_empty], axis=1, merge='same')
|
|
196
|
+
|
|
191
197
|
logger.info(
|
|
192
198
|
f"Expanded adata with {num_missing_features} missing features.",
|
|
193
|
-
indent_level=
|
|
199
|
+
indent_level=indent_level+1,
|
|
194
200
|
)
|
|
195
201
|
else:
|
|
196
|
-
logger.info("All features are present in adata.var_names.", indent_level=
|
|
202
|
+
logger.info("All features are present in adata.var_names.", indent_level=indent_level+1)
|
|
197
203
|
|
|
198
204
|
return adata
|
|
199
205
|
|
|
@@ -285,6 +291,10 @@ def initialize_model(
|
|
|
285
291
|
"ocampoatac1",
|
|
286
292
|
"ocampoatac2",
|
|
287
293
|
"bitage",
|
|
294
|
+
"zhang",
|
|
295
|
+
"leecpc",
|
|
296
|
+
"leerpc",
|
|
297
|
+
"leerefinedrpc",
|
|
288
298
|
]:
|
|
289
299
|
model = LinearModel(len(features))
|
|
290
300
|
elif clock_name in [
|
|
@@ -328,34 +338,36 @@ def initialize_model(
|
|
|
328
338
|
|
|
329
339
|
@progress("Preprocess data")
|
|
330
340
|
def preprocess_data(
|
|
341
|
+
adata: anndata.AnnData,
|
|
331
342
|
preprocessing: str,
|
|
332
|
-
|
|
333
|
-
|
|
343
|
+
preprocessing_helper,
|
|
344
|
+
features: List[str],
|
|
334
345
|
logger,
|
|
335
346
|
indent_level: int = 2,
|
|
336
|
-
) ->
|
|
347
|
+
) -> anndata.AnnData:
|
|
337
348
|
"""
|
|
338
349
|
Preprocess the input data based on the specified method and the clock dictionary.
|
|
339
350
|
|
|
340
351
|
This function applies a specified preprocessing method to the input data, which is necessary
|
|
341
352
|
for aligning the data format with the requirements of the aging clock models. The function
|
|
342
353
|
supports multiple preprocessing methods, including scaling, log transformation, binarization, etc.
|
|
343
|
-
The specific preprocessing method to be used is indicated by the `preprocessing` parameter
|
|
344
|
-
|
|
354
|
+
The specific preprocessing method to be used is indicated by the `preprocessing` parameter. A new
|
|
355
|
+
layers with the format X_{preprocessing} is added to the adata object.
|
|
345
356
|
|
|
346
357
|
Parameters
|
|
347
358
|
----------
|
|
359
|
+
adata : anndata.AnnData
|
|
360
|
+
The input data to be preprocessed in AnnData format.
|
|
361
|
+
|
|
348
362
|
preprocessing : str
|
|
349
363
|
The name of the preprocessing method to apply. Supported methods include 'scale', 'log1p',
|
|
350
|
-
'binarize',
|
|
364
|
+
'binarize', and 'quantile_normalization_with_gold_standard'.
|
|
351
365
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
preprocessing method.
|
|
366
|
+
preprocessing_helper:
|
|
367
|
+
Any preprocessing helper file, from an scikit-learn scaler object to a list of standard values.
|
|
355
368
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
parameters needed for preprocessing.
|
|
369
|
+
features : List[str]
|
|
370
|
+
The features used by the clock.
|
|
359
371
|
|
|
360
372
|
logger : Logger
|
|
361
373
|
A logger object for logging the progress and any information or warnings during preprocessing.
|
|
@@ -366,7 +378,7 @@ def preprocess_data(
|
|
|
366
378
|
|
|
367
379
|
Returns
|
|
368
380
|
-------
|
|
369
|
-
|
|
381
|
+
adata : anndata.AnnData
|
|
370
382
|
The preprocessed data, ready to be input into the aging clock models.
|
|
371
383
|
|
|
372
384
|
Notes
|
|
@@ -375,31 +387,50 @@ def preprocess_data(
|
|
|
375
387
|
For instance, 'scale' may expect a numeric tensor, while 'log1p' requires non-negative values.
|
|
376
388
|
Users must ensure that the input data is appropriate for the selected preprocessing method.
|
|
377
389
|
|
|
378
|
-
The `clock_dict` may include specific parameters or helper functions like `preprocessing_helper`
|
|
379
|
-
for the 'scale' method, which should be defined elsewhere in the codebase.
|
|
380
|
-
|
|
381
390
|
Examples
|
|
382
391
|
--------
|
|
383
|
-
>>>
|
|
384
|
-
>>> print(
|
|
385
|
-
|
|
392
|
+
>>> processed_adata = preprocess_data(adata, "scale", preprocessing_helper, features, logger)
|
|
393
|
+
>>> print(processed_adata.shape)
|
|
394
|
+
np.array([...])
|
|
386
395
|
|
|
387
396
|
"""
|
|
397
|
+
# Skip if it has already found the preprocessing layer
|
|
398
|
+
if f"X_{preprocessing}" in adata.layers:
|
|
399
|
+
logger.info(f"Layer with {preprocessing} preprocessing is already in adata", indent_level=3)
|
|
400
|
+
return adata
|
|
401
|
+
|
|
402
|
+
# Move to adata.X for preprocessing
|
|
403
|
+
adata.X = adata.layers["X_imputed"].copy() if "X_imputed" in adata.layers else adata.layers["X_original"].copy()
|
|
404
|
+
|
|
388
405
|
logger.info(f"Preprocessing data with function {preprocessing}", indent_level=3)
|
|
389
406
|
# Apply specified preprocessing method
|
|
390
407
|
if preprocessing == "scale":
|
|
391
|
-
|
|
408
|
+
X = adata[:, features].X
|
|
409
|
+
X = scale(X, preprocessing_helper)
|
|
410
|
+
adata[:, features].X = X
|
|
392
411
|
elif preprocessing == "log1p":
|
|
393
|
-
|
|
412
|
+
X = adata.X
|
|
413
|
+
X = np.log1p(X)
|
|
414
|
+
adata.X = X
|
|
394
415
|
elif preprocessing == "binarize":
|
|
395
|
-
|
|
396
|
-
|
|
416
|
+
X = adata.X
|
|
417
|
+
X = binarize(X)
|
|
418
|
+
adata.X = X
|
|
419
|
+
elif preprocessing == "quantile_normalization_with_gold_standard":
|
|
420
|
+
gold_standard_df = pd.DataFrame(dict(zip(preprocessing_helper['gold_standard_probes'], preprocessing_helper['gold_standard_means'])), index=['means']).T
|
|
421
|
+
common_features = np.intersect1d(adata.var_names, gold_standard_df.index.tolist())
|
|
422
|
+
X = adata[:, common_features].X
|
|
423
|
+
X = quantile_normalize_with_gold_standard(X, gold_standard_df.loc[common_features, 'means'].tolist())
|
|
424
|
+
adata[:, common_features].X = X
|
|
425
|
+
adata.layers[f"X_{preprocessing}"] = adata.X
|
|
426
|
+
|
|
427
|
+
return adata
|
|
397
428
|
|
|
398
429
|
|
|
399
430
|
@progress("Postprocess data")
|
|
400
431
|
def postprocess_data(
|
|
401
|
-
postprocessing: str,
|
|
402
432
|
data: np.ndarray,
|
|
433
|
+
postprocessing: str,
|
|
403
434
|
clock_dict: dict,
|
|
404
435
|
logger,
|
|
405
436
|
indent_level: int = 2,
|
|
@@ -414,16 +445,16 @@ def postprocess_data(
|
|
|
414
445
|
|
|
415
446
|
Parameters
|
|
416
447
|
----------
|
|
417
|
-
postprocessing : str
|
|
418
|
-
The name of the postprocessing method to be applied. Supported methods include
|
|
419
|
-
'anti_log_linear', 'anti_logp2', 'anti_log', etc.
|
|
420
448
|
|
|
421
449
|
data : array-like
|
|
422
450
|
The data to be postprocessed. It should be compatible with the specified postprocessing method.
|
|
423
451
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
452
|
+
postprocessing : str
|
|
453
|
+
The name of the postprocessing method to be applied. Supported methods include
|
|
454
|
+
'anti_log_linear', 'anti_logp2', 'anti_log', etc.
|
|
455
|
+
|
|
456
|
+
postprocessing_helper:
|
|
457
|
+
Any postprocessing helper file, from an scikit-learn scaler object to a list of standard values.
|
|
427
458
|
|
|
428
459
|
logger : Logger
|
|
429
460
|
A logger object for logging the progress, information, or warnings during postprocessing.
|
|
@@ -446,7 +477,7 @@ def postprocess_data(
|
|
|
446
477
|
|
|
447
478
|
Examples
|
|
448
479
|
--------
|
|
449
|
-
>>> postprocessed_data = postprocess_data("anti_log_linear",
|
|
480
|
+
>>> postprocessed_data = postprocess_data(data, "anti_log_linear", postprocessing_helper, logger)
|
|
450
481
|
>>> print(postprocessed_data.shape)
|
|
451
482
|
(num_samples, num_features)
|
|
452
483
|
|
|
@@ -620,10 +651,10 @@ def convert_numpy_array_to_tensor(
|
|
|
620
651
|
|
|
621
652
|
@progress("Filter features and extract data matrix")
|
|
622
653
|
def filter_features_and_extract_data(
|
|
623
|
-
adata: anndata.AnnData, features: List[str], logger, indent_level: int = 2
|
|
654
|
+
adata: anndata.AnnData, preprocessing: str, features: List[str], logger, indent_level: int = 2
|
|
624
655
|
) -> np.ndarray:
|
|
625
656
|
"""
|
|
626
|
-
Filter features from an AnnData object
|
|
657
|
+
Filter features from an AnnData object
|
|
627
658
|
|
|
628
659
|
This function processes an AnnData object, which is commonly used in bioinformatics for storing large
|
|
629
660
|
gene expression datasets. It extracts the data matrix corresponding to a specified set of features (genes or
|
|
@@ -635,6 +666,9 @@ def filter_features_and_extract_data(
|
|
|
635
666
|
The AnnData object containing the dataset. Its `.X` attribute is expected to be a matrix where rows
|
|
636
667
|
correspond to samples and columns correspond to features.
|
|
637
668
|
|
|
669
|
+
preprocessing : str
|
|
670
|
+
The name of the preprocessing method used (if any). Used to search adata.layers for data matrix.
|
|
671
|
+
|
|
638
672
|
features : list of str
|
|
639
673
|
A list of feature names to be included in the output array. Only these features from the AnnData object will
|
|
640
674
|
be extracted and converted.
|
|
@@ -663,12 +697,24 @@ def filter_features_and_extract_data(
|
|
|
663
697
|
--------
|
|
664
698
|
>>> adata = anndata.AnnData(np.random.rand(5, 10))
|
|
665
699
|
>>> features = ['gene1', 'gene2', 'gene3']
|
|
666
|
-
>>> numpy_array = filter_features_and_extract_data(adata, features, logger)
|
|
700
|
+
>>> numpy_array = filter_features_and_extract_data(adata, None, features, logger)
|
|
667
701
|
>>> numpy_array.shape
|
|
668
702
|
array([5, 3])
|
|
669
703
|
|
|
670
704
|
"""
|
|
671
|
-
|
|
705
|
+
if preprocessing is not None:
|
|
706
|
+
layer_name = f"X_{preprocessing}"
|
|
707
|
+
elif "X_imputed" in adata.layers:
|
|
708
|
+
layer_name = "X_imputed"
|
|
709
|
+
else:
|
|
710
|
+
layer_name = "X_original"
|
|
711
|
+
#x_numpy = np.array(adata[:, features].layers[layer_name])
|
|
712
|
+
layer_data = adata[:, features].layers[layer_name]
|
|
713
|
+
|
|
714
|
+
if not isinstance(layer_data, np.ndarray):
|
|
715
|
+
layer_data = layer_data.toarray() if hasattr(layer_data, 'toarray') else np.array(layer_data, dtype=np.float32)
|
|
716
|
+
|
|
717
|
+
return layer_data
|
|
672
718
|
|
|
673
719
|
|
|
674
720
|
@progress("Add predicted ages to adata")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def scale(x, scaler):
|
|
5
|
+
"""
|
|
6
|
+
Scales the input data using the provided scaler.
|
|
7
|
+
"""
|
|
8
|
+
# Apply the scaling transformation to the NumPy array
|
|
9
|
+
x_scaled = scaler.transform(x)
|
|
10
|
+
|
|
11
|
+
return x_scaled
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def binarize(x):
|
|
15
|
+
"""
|
|
16
|
+
Binarizes an array based on the median of each row, excluding zeros.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Create a mask for non-zero elements
|
|
20
|
+
non_zero_mask = x != 0
|
|
21
|
+
|
|
22
|
+
# Apply mask, calculate median for each row, and change data
|
|
23
|
+
medians = np.zeros(x.shape[0])
|
|
24
|
+
for i, row in enumerate(x):
|
|
25
|
+
non_zero_elements = row[non_zero_mask[i]]
|
|
26
|
+
x[i] = x[i] > np.median(non_zero_elements)
|
|
27
|
+
|
|
28
|
+
return x
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def quantile_normalize_with_gold_standard(x, gold_standard_means):
|
|
32
|
+
"""
|
|
33
|
+
Apply quantile normalization on x using gold standard means.
|
|
34
|
+
"""
|
|
35
|
+
# Create a copy of x to avoid modifying a view
|
|
36
|
+
x_normalized = x.copy()
|
|
37
|
+
|
|
38
|
+
# Sort the gold standard means
|
|
39
|
+
sorted_gold_standard = np.sort(gold_standard_means)
|
|
40
|
+
|
|
41
|
+
# Iterate through each row in x_normalized
|
|
42
|
+
for i in range(x_normalized.shape[0]):
|
|
43
|
+
# Sort the row data and store the original indices
|
|
44
|
+
sorted_indices = np.argsort(x_normalized[i, :])
|
|
45
|
+
sorted_data = x_normalized[i, sorted_indices]
|
|
46
|
+
|
|
47
|
+
# Map the sorted data to their quantile values in the gold standard
|
|
48
|
+
quantile_indices = np.round(
|
|
49
|
+
np.linspace(0, len(sorted_gold_standard) - 1, len(sorted_data))
|
|
50
|
+
).astype(int)
|
|
51
|
+
normalized_data = sorted_gold_standard[quantile_indices]
|
|
52
|
+
|
|
53
|
+
# Re-order the normalized data to the original order
|
|
54
|
+
original_order_indices = np.argsort(sorted_indices)
|
|
55
|
+
x_normalized[i, :] = normalized_data[original_order_indices]
|
|
56
|
+
|
|
57
|
+
return x_normalized
|
|
@@ -127,7 +127,7 @@ def df_to_adata(
|
|
|
127
127
|
The DataFrame containing biological data. Rows represent samples, and columns represent features.
|
|
128
128
|
|
|
129
129
|
metadata_cols : List[str]
|
|
130
|
-
A list with the name of the columns in 'df' which are part of the metadata. They will be added
|
|
130
|
+
A list with the name of the columns in 'df' which are part of the metadata. They will be added
|
|
131
131
|
to adata.obs rather than adata.X.
|
|
132
132
|
|
|
133
133
|
imputer_strategy : str, optional
|
|
@@ -96,6 +96,7 @@ def impute_missing_values(
|
|
|
96
96
|
logger.info(f"Imputing missing values using {strategy} strategy", indent_level=2)
|
|
97
97
|
X = imputer.fit_transform(X)
|
|
98
98
|
adata.X = X
|
|
99
|
+
adata.layers["X_imputed"] = adata.X
|
|
99
100
|
return adata
|
|
100
101
|
|
|
101
102
|
|
|
@@ -208,6 +209,7 @@ def create_anndata_object(
|
|
|
208
209
|
X=X,
|
|
209
210
|
obs=obs,
|
|
210
211
|
var=var,
|
|
212
|
+
layers={'X_original': X}
|
|
211
213
|
)
|
|
212
214
|
|
|
213
215
|
return adata
|
|
@@ -314,7 +316,7 @@ def add_unstructured_data(
|
|
|
314
316
|
-------
|
|
315
317
|
>>> from anndata import AnnData
|
|
316
318
|
>>> adata = AnnData(np.random.rand(5, 3))
|
|
317
|
-
>>> add_unstructured_data(adata, 'mean', logger)
|
|
319
|
+
>>> adata = add_unstructured_data(adata, 'mean', logger)
|
|
318
320
|
# This will add the imputer strategy 'mean' and the data type 'dna_methylation' to the AnnData object.
|
|
319
321
|
|
|
320
322
|
"""
|
|
@@ -56,7 +56,7 @@ def progress(message: str) -> None:
|
|
|
56
56
|
@wraps(func)
|
|
57
57
|
def wrapper(*args, **kwargs):
|
|
58
58
|
# Extract indent_level from kwargs, default to 1 if not provided
|
|
59
|
-
indent_level = kwargs
|
|
59
|
+
indent_level = kwargs["indent_level"] if "indent_level" in kwargs.keys() else 1
|
|
60
60
|
|
|
61
61
|
logger = args[-1] # Assumes logger is the last positional argument
|
|
62
62
|
logger.start_progress(f"{message} started", indent_level=indent_level)
|
|
@@ -70,7 +70,7 @@ def progress(message: str) -> None:
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
@progress("Load all clock metadata")
|
|
73
|
-
def load_clock_metadata(dir: str, logger, indent_level: int =
|
|
73
|
+
def load_clock_metadata(dir: str, logger, indent_level: int = 2) -> dict:
|
|
74
74
|
"""
|
|
75
75
|
Loads the clock metadata from a specified source.
|
|
76
76
|
|
|
@@ -114,12 +114,11 @@ def load_clock_metadata(dir: str, logger, indent_level: int = 1) -> dict:
|
|
|
114
114
|
|
|
115
115
|
"""
|
|
116
116
|
url = f"https://pyaging.s3.amazonaws.com/clocks/metadata/all_clock_metadata.pt"
|
|
117
|
-
download(url, dir, logger, indent_level=
|
|
117
|
+
download(url, dir, logger, indent_level=indent_level)
|
|
118
118
|
all_clock_metadata = torch.load(f"{dir}/all_clock_metadata.pt")
|
|
119
119
|
return all_clock_metadata
|
|
120
120
|
|
|
121
121
|
|
|
122
|
-
@progress("Download data")
|
|
123
122
|
def download(url: str, dir: str, logger, indent_level: int = 1):
|
|
124
123
|
"""
|
|
125
124
|
Downloads a file from a specified URL to a local directory.
|
|
@@ -165,11 +164,11 @@ def download(url: str, dir: str, logger, indent_level: int = 1):
|
|
|
165
164
|
file_path = os.path.join(dir, file_path)
|
|
166
165
|
|
|
167
166
|
if os.path.exists(file_path):
|
|
168
|
-
logger.info(f"Data found in {file_path}", indent_level=indent_level
|
|
167
|
+
logger.info(f"Data found in {file_path}", indent_level=indent_level+1)
|
|
169
168
|
else:
|
|
170
169
|
if not os.path.exists(dir):
|
|
171
170
|
os.mkdir(dir)
|
|
172
|
-
logger.info(f"Downloading data to {file_path}", indent_level=indent_level
|
|
171
|
+
logger.info(f"Downloading data to {file_path}", indent_level=indent_level+1)
|
|
173
172
|
logger.indent_level = indent_level + 1
|
|
174
173
|
urlretrieve(url, file_path, reporthook=logger.request_report_hook)
|
|
175
174
|
|
|
@@ -220,7 +219,7 @@ def find_clock_by_doi(search_doi: str, dir: str = "pyaging_data") -> None:
|
|
|
220
219
|
logger.first_info("Starting find_clock_by_doi function")
|
|
221
220
|
|
|
222
221
|
# Load all metadata
|
|
223
|
-
all_clock_metadata = load_clock_metadata(dir, logger)
|
|
222
|
+
all_clock_metadata = load_clock_metadata(dir, logger, indent_level=1)
|
|
224
223
|
|
|
225
224
|
# Message to indicate the start of the search process
|
|
226
225
|
message = "Searching for clock based on DOI"
|
|
@@ -300,7 +299,7 @@ def cite_clock(clock_name: str, dir: str = "pyaging_data") -> None:
|
|
|
300
299
|
clock_name = clock_name.lower()
|
|
301
300
|
|
|
302
301
|
# Load all metadata
|
|
303
|
-
all_clock_metadata = load_clock_metadata(dir, logger)
|
|
302
|
+
all_clock_metadata = load_clock_metadata(dir, logger, indent_level=1)
|
|
304
303
|
|
|
305
304
|
message = f"Searching for citation of clock {clock_name}"
|
|
306
305
|
logger.start_progress(f"{message} started")
|
|
@@ -365,7 +364,7 @@ def show_all_clocks(dir: str = "pyaging_data") -> None:
|
|
|
365
364
|
logger.first_info("Starting show_all_clocks function")
|
|
366
365
|
|
|
367
366
|
# Load all metadata
|
|
368
|
-
all_clock_metadata = load_clock_metadata(dir, logger)
|
|
367
|
+
all_clock_metadata = load_clock_metadata(dir, logger, indent_level=1)
|
|
369
368
|
|
|
370
369
|
# Message to indicate the start of the search process
|
|
371
370
|
message = "Showing all available clock names"
|
|
@@ -423,7 +422,7 @@ def get_clock_metadata(clock_name: str, dir: str = "pyaging_data") -> None:
|
|
|
423
422
|
logger.first_info("Starting get_clock_metadata function")
|
|
424
423
|
|
|
425
424
|
# Load all metadata
|
|
426
|
-
all_clock_metadata = load_clock_metadata(dir, logger)
|
|
425
|
+
all_clock_metadata = load_clock_metadata(dir, logger, indent_level=1)
|
|
427
426
|
|
|
428
427
|
# Lowercase clock name
|
|
429
428
|
clock_name = clock_name.lower()
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def scale(data, scaler):
|
|
5
|
-
"""
|
|
6
|
-
Scales the input data using the provided scaler.
|
|
7
|
-
"""
|
|
8
|
-
# Apply the scaling transformation to the NumPy array
|
|
9
|
-
data_scaled = scaler.transform(data)
|
|
10
|
-
|
|
11
|
-
return data_scaled
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def binarize(data):
|
|
15
|
-
"""
|
|
16
|
-
Binarizes an array based on the median of each row, excluding zeros.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
# Create a mask for non-zero elements
|
|
20
|
-
non_zero_mask = data != 0
|
|
21
|
-
|
|
22
|
-
# Apply mask, calculate median for each row, and change data
|
|
23
|
-
medians = np.zeros(data.shape[0])
|
|
24
|
-
for i, row in enumerate(data):
|
|
25
|
-
non_zero_elements = row[non_zero_mask[i]]
|
|
26
|
-
data[i] = data[i] > np.median(non_zero_elements)
|
|
27
|
-
|
|
28
|
-
return data
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|