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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyaging
3
- Version: 0.0.2
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
- self.log_time()
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
- # self.log_time()
285
- # self.report_progress(percent=100, progress_name=progress_name)#
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 : AnnData
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 : str or list of str, optional
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 : str
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
- suitable for age prediction.
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, clock_dict = load_clock(
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
- adata_expanded = check_features_in_adata(
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
- adata_expanded, features, logger, indent_level=2
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
- postprocessing, predicted_ages, clock_dict, logger, indent_level=2
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
- - clock_dict: The complete clock dictionary as loaded from the file.
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, preprocessing, postprocessing, clock_dict = load_clock("clock1", "pyaging_data", logger)
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=2)
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, clock_dict
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=3,
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
- np.zeros((adata.n_obs, num_missing_features)),
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=3,
199
+ indent_level=indent_level+1,
194
200
  )
195
201
  else:
196
- logger.info("All features are present in adata.var_names.", indent_level=2)
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
- data: torch.Tensor,
333
- clock_dict: dict,
343
+ preprocessing_helper,
344
+ features: List[str],
334
345
  logger,
335
346
  indent_level: int = 2,
336
- ) -> torch.Tensor:
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, and
344
- any additional parameters required for preprocessing are obtained from `clock_dict`.
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', etc.
364
+ 'binarize', and 'quantile_normalization_with_gold_standard'.
351
365
 
352
- data : torch.Tensor or similar
353
- The input data to be preprocessed. It should be in a format compatible with the specified
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
- clock_dict : dict
357
- A dictionary containing clock-specific information, which may include helper functions or
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
- processed_data : torch.Tensor or similar
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
- >>> processed_data = preprocess_data("scale", data, clock_dict, logger)
384
- >>> print(processed_data.shape)
385
- torch.Size([...])
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
- data = scale(data, clock_dict["preprocessing_helper"])
408
+ X = adata[:, features].X
409
+ X = scale(X, preprocessing_helper)
410
+ adata[:, features].X = X
392
411
  elif preprocessing == "log1p":
393
- data = np.log1p(data)
412
+ X = adata.X
413
+ X = np.log1p(X)
414
+ adata.X = X
394
415
  elif preprocessing == "binarize":
395
- data = binarize(data)
396
- return data
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
- clock_dict : dict
425
- A dictionary containing any additional information or parameters required for postprocessing.
426
- For example, parameters for scaling factors or offsets used in anti-log transformations.
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", data, clock_dict, logger)
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 and returns the .X data matrix as a Numpy array.
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
- return np.array(adata[:, features].X, dtype=np.float32)
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.pop("indent_level", 1)
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 = 1) -> dict:
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=2)
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 + 1)
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 + 1)
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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyaging"
3
- version = "0.0.2"
3
+ version = "0.0.4"
4
4
  description = "A Python-based compendium of GPU-optimized aging clocks."
5
5
  authors = ["Lucas Paulo de Lima Camillo <lucas_camillo@alumni.brown.edu>"]
6
6
  license = "BSD"
@@ -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