grnimmuneclock 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ """
2
+ GRNimmuneClock: Cell-Type Specific Immune Aging Clocks
3
+
4
+ This package provides pre-trained aging clocks for immune cell types,
5
+ built using gene regulatory network (GRN) analysis.
6
+
7
+ Example
8
+ -------
9
+ >>> from grnimmuneclock import AgingClock, load_example_data
10
+ >>>
11
+ >>> # Load pre-trained clock
12
+ >>> clock = AgingClock(cell_type='CD4T')
13
+ >>>
14
+ >>> # Load example data
15
+ >>> adata = load_example_data()
16
+ >>>
17
+ >>> # Predict ages
18
+ >>> adata_predicted = clock.predict(adata)
19
+ >>> print(adata_predicted.obs['predicted_age'])
20
+ """
21
+
22
+ from .core import AgingClock, load_example_data
23
+ from .__version__ import __version__, __author__, __license__
24
+ from . import plotting
25
+ from .training import (
26
+ train_aging_clock,
27
+ build_model,
28
+ merge_training_data,
29
+ evaluate_cv_performance,
30
+ tune_ridge_params
31
+ )
32
+ from .helpers import (
33
+ predict_age,
34
+ format_data,
35
+ retrieve_function,
36
+ save_function,
37
+ merge_adata,
38
+ evaluate_groupwise_median,
39
+ load_consensus_grn,
40
+ prepare_user_data
41
+ )
42
+
43
+ __all__ = [
44
+ 'AgingClock',
45
+ 'load_example_data',
46
+ 'plotting',
47
+ 'train_aging_clock',
48
+ 'build_model',
49
+ 'merge_training_data',
50
+ 'evaluate_cv_performance',
51
+ 'tune_ridge_params',
52
+ 'predict_age',
53
+ 'format_data',
54
+ 'retrieve_function',
55
+ 'save_function',
56
+ 'merge_adata',
57
+ 'evaluate_groupwise_median',
58
+ 'load_consensus_grn',
59
+ 'prepare_user_data',
60
+ '__version__',
61
+ '__author__',
62
+ '__license__'
63
+ ]
@@ -0,0 +1,5 @@
1
+ """Version information for GRNimmuneClock."""
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "Jalil Nourisa"
5
+ __license__ = "MIT"
grnimmuneclock/core.py ADDED
@@ -0,0 +1,349 @@
1
+ """
2
+ Core module for GRNimmuneClock - Cell-type specific immune aging clocks.
3
+ """
4
+
5
+ import os
6
+ import warnings
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Optional, Union, List
10
+ import numpy as np
11
+ import pandas as pd
12
+ import joblib
13
+ from scipy import sparse
14
+ from scipy.sparse import issparse
15
+ import anndata as ad
16
+ from anndata import AnnData
17
+
18
+
19
+ class AgingClock:
20
+ """
21
+ Cell-type specific aging clock for immune cells.
22
+
23
+ This class provides an interface to load pre-trained aging clock models
24
+ and predict biological age from gene expression data.
25
+
26
+ Parameters
27
+ ----------
28
+ cell_type : str
29
+ Cell type for the aging clock. Options: 'CD4T', 'CD8T', 'MONO', 'B', 'NK'
30
+ version : str, optional
31
+ Version of the model to load (default: 'all_data')
32
+ model_dir : str or Path, optional
33
+ Custom directory containing model files. If None, uses bundled models.
34
+
35
+ Attributes
36
+ ----------
37
+ cell_type : str
38
+ The cell type this clock is trained for
39
+ model : sklearn.Pipeline
40
+ The trained model (StandardScaler + Ridge regression)
41
+ feature_names : np.ndarray
42
+ Names of genes used as features
43
+ metadata : dict
44
+ Model metadata including training info and performance metrics
45
+
46
+ Examples
47
+ --------
48
+ >>> from grnimmuneclock import AgingClock
49
+ >>> clock = AgingClock(cell_type='CD4T')
50
+ >>> adata_predicted = clock.predict(adata)
51
+ >>> print(adata_predicted.obs['predicted_age'])
52
+ """
53
+
54
+ SUPPORTED_CELL_TYPES = ['CD4T', 'CD8T', 'MONO', 'B', 'NK']
55
+
56
+ def __init__(
57
+ self,
58
+ cell_type: str,
59
+ version: str = 'all_data',
60
+ model_dir: Optional[Union[str, Path]] = None
61
+ ):
62
+ if cell_type not in self.SUPPORTED_CELL_TYPES:
63
+ raise ValueError(
64
+ f"Unsupported cell type: {cell_type}. "
65
+ f"Choose from {self.SUPPORTED_CELL_TYPES}"
66
+ )
67
+
68
+ self.cell_type = cell_type
69
+ self.version = version
70
+ self.feature_type = 'gene_expression'
71
+ self.data_type = 'bulk'
72
+ self.reg_type = 'ridge'
73
+
74
+ # Determine model directory
75
+ if model_dir is None:
76
+ # Use bundled models
77
+ package_dir = Path(__file__).parent
78
+ model_dir = package_dir / 'models' / cell_type
79
+ else:
80
+ model_dir = Path(model_dir)
81
+
82
+ if not model_dir.exists():
83
+ raise FileNotFoundError(f"Model directory not found: {model_dir}")
84
+
85
+ self.model_dir = model_dir
86
+
87
+ # Load model and metadata
88
+ self._load_model()
89
+ self._load_metadata()
90
+
91
+ def _load_model(self):
92
+ """Load the trained model and feature names."""
93
+ model_path = self.model_dir / "model.pkl"
94
+ features_path = self.model_dir / "features.txt"
95
+
96
+ if not model_path.exists():
97
+ raise FileNotFoundError(f"Model file not found: {model_path}")
98
+ if not features_path.exists():
99
+ raise FileNotFoundError(f"Features file not found: {features_path}")
100
+
101
+ # Load model
102
+ self.model = joblib.load(model_path)
103
+
104
+ # Load feature names
105
+ self.feature_names = np.loadtxt(features_path, dtype=str)
106
+
107
+ def _load_metadata(self):
108
+ """Load model metadata if available."""
109
+ metadata_path = self.model_dir / 'metadata.json'
110
+
111
+ if metadata_path.exists():
112
+ with open(metadata_path, 'r') as f:
113
+ self.metadata = json.load(f)
114
+ else:
115
+ # Create basic metadata
116
+ self.metadata = {
117
+ 'cell_type': self.cell_type,
118
+ 'version': self.version,
119
+ 'feature_type': self.feature_type,
120
+ 'data_type': self.data_type,
121
+ 'reg_type': self.reg_type,
122
+ 'n_features': len(self.feature_names)
123
+ }
124
+
125
+ def _align_feature_space(self, adata: AnnData) -> AnnData:
126
+ """
127
+ Align input data features to match the model's feature space.
128
+
129
+ Missing features will be filled with zeros.
130
+
131
+ Parameters
132
+ ----------
133
+ adata : AnnData
134
+ Input data with gene expression
135
+
136
+ Returns
137
+ -------
138
+ AnnData
139
+ Data with aligned features
140
+ """
141
+ var_names = np.array(adata.var.index.tolist())
142
+ var_index = {gene: i for i, gene in enumerate(var_names)}
143
+
144
+ # Collect indices or mark as -1 for missing
145
+ idxs = np.array([var_index.get(gene, -1) for gene in self.feature_names])
146
+
147
+ # Create a matrix with correct shape
148
+ rows = adata.obs.shape[0]
149
+ cols = len(self.feature_names)
150
+ X_aligned = sparse.lil_matrix((rows, cols))
151
+
152
+ # Fill in available gene columns
153
+ present = idxs != -1
154
+ if present.sum() > 0:
155
+ X_aligned[:, present] = adata[:].X[:, idxs[present]]
156
+
157
+ # Convert to CSR for efficiency
158
+ X_aligned = X_aligned.tocsr()
159
+
160
+ # Create new AnnData object
161
+ new_adata = AnnData(
162
+ X=X_aligned,
163
+ obs=adata.obs.copy(),
164
+ var={"gene_symbols": self.feature_names},
165
+ )
166
+ new_adata.var_names = self.feature_names
167
+
168
+ # Warn about missing features
169
+ n_missing = (~present).sum()
170
+ if n_missing > 0:
171
+ coverage = present.sum() / len(self.feature_names)
172
+ warnings.warn(
173
+ f"{n_missing} features ({(1-coverage)*100:.1f}%) missing from input data. "
174
+ f"They will be set to zero."
175
+ )
176
+
177
+ return new_adata
178
+
179
+ def _validate_input(self, adata: AnnData):
180
+ """Validate input data format."""
181
+ if not isinstance(adata, AnnData):
182
+ raise TypeError("Input must be an AnnData object")
183
+
184
+ if adata.X is None:
185
+ raise ValueError("Input AnnData has no expression matrix (X)")
186
+
187
+ if adata.n_obs == 0:
188
+ raise ValueError("Input AnnData has no observations (cells/samples)")
189
+
190
+ if adata.n_vars == 0:
191
+ raise ValueError("Input AnnData has no variables (genes)")
192
+
193
+ def predict(self, adata: AnnData, return_adata: bool = True) -> Union[AnnData, np.ndarray]:
194
+ """
195
+ Predict biological age from gene expression data.
196
+
197
+ Parameters
198
+ ----------
199
+ adata : AnnData
200
+ Input data with gene expression in .X
201
+ Rows are samples/cells, columns are genes
202
+ return_adata : bool, optional
203
+ If True, return AnnData with 'predicted_age' in .obs
204
+ If False, return numpy array of predictions (default: True)
205
+
206
+ Returns
207
+ -------
208
+ AnnData or np.ndarray
209
+ If return_adata=True: Input AnnData with 'predicted_age' column added to .obs
210
+ If return_adata=False: Array of predicted ages
211
+
212
+ Examples
213
+ --------
214
+ >>> clock = AgingClock(cell_type='CD4T')
215
+ >>> adata_with_predictions = clock.predict(adata)
216
+ >>> ages = clock.predict(adata, return_adata=False)
217
+ """
218
+ # Validate input
219
+ self._validate_input(adata)
220
+
221
+ # Align features to model's feature space
222
+ adata_aligned = self._align_feature_space(adata)
223
+
224
+ # Get expression matrix
225
+ X = adata_aligned.X.copy()
226
+ if issparse(X):
227
+ X = X.toarray()
228
+
229
+ # Predict
230
+ predicted_age = self.model.predict(X)
231
+
232
+ if return_adata:
233
+ # Add predictions to original adata
234
+ adata.obs['predicted_age'] = predicted_age.copy()
235
+
236
+ # Calculate age acceleration if actual age is available
237
+ if 'age' in adata.obs.columns:
238
+ adata.obs['age_acceleration'] = adata.obs['predicted_age'] - adata.obs['age']
239
+
240
+ return adata
241
+ else:
242
+ return predicted_age
243
+
244
+ def predict_batch(
245
+ self,
246
+ adata_list: List[AnnData],
247
+ return_dataframe: bool = True
248
+ ) -> Union[pd.DataFrame, List[np.ndarray]]:
249
+ """
250
+ Predict ages for multiple AnnData objects.
251
+
252
+ Parameters
253
+ ----------
254
+ adata_list : list of AnnData
255
+ List of AnnData objects to predict on
256
+ return_dataframe : bool, optional
257
+ If True, return consolidated DataFrame (default: True)
258
+ If False, return list of prediction arrays
259
+
260
+ Returns
261
+ -------
262
+ pd.DataFrame or list of np.ndarray
263
+ Predictions for all inputs
264
+ """
265
+ predictions = []
266
+
267
+ for i, adata in enumerate(adata_list):
268
+ pred = self.predict(adata, return_adata=False)
269
+ predictions.append(pred)
270
+
271
+ if return_dataframe:
272
+ # Combine into DataFrame
273
+ df_list = []
274
+ for i, (adata, pred) in enumerate(zip(adata_list, predictions)):
275
+ df = pd.DataFrame({
276
+ 'predicted_age': pred,
277
+ 'batch': i
278
+ })
279
+ # Add other obs columns if available
280
+ for col in adata.obs.columns:
281
+ df[col] = adata.obs[col].values
282
+ df_list.append(df)
283
+
284
+ return pd.concat(df_list, ignore_index=True)
285
+ else:
286
+ return predictions
287
+
288
+ def __repr__(self):
289
+ return (
290
+ f"AgingClock(cell_type='{self.cell_type}', "
291
+ f"version='{self.version}', "
292
+ f"n_features={len(self.feature_names)})"
293
+ )
294
+
295
+ def get_feature_importance(self, top_n: Optional[int] = None) -> pd.DataFrame:
296
+ """
297
+ Get feature importances (model coefficients).
298
+
299
+ Parameters
300
+ ----------
301
+ top_n : int, optional
302
+ Return only top N features by absolute importance
303
+ If None, return all features
304
+
305
+ Returns
306
+ -------
307
+ pd.DataFrame
308
+ DataFrame with columns: 'feature', 'coefficient'
309
+ Sorted by absolute coefficient value (descending)
310
+ """
311
+ # Get coefficients from Ridge model
312
+ coefs = self.model.named_steps['ridge'].coef_
313
+
314
+ # Create DataFrame
315
+ df = pd.DataFrame({
316
+ 'feature': self.feature_names,
317
+ 'coefficient': coefs
318
+ })
319
+
320
+ # Sort by absolute value
321
+ df['abs_coefficient'] = np.abs(df['coefficient'])
322
+ df = df.sort_values('abs_coefficient', ascending=False)
323
+ df = df.drop('abs_coefficient', axis=1)
324
+
325
+ if top_n is not None:
326
+ df = df.head(top_n)
327
+
328
+ return df.reset_index(drop=True)
329
+
330
+
331
+ def load_example_data() -> AnnData:
332
+ """
333
+ Load example data for testing the aging clock.
334
+
335
+ Returns
336
+ -------
337
+ AnnData
338
+ Small example dataset from a single donor
339
+ """
340
+ package_dir = Path(__file__).parent
341
+ data_path = package_dir / 'data' / 'example_data.h5ad'
342
+
343
+ if not data_path.exists():
344
+ raise FileNotFoundError(
345
+ f"Example data not found at {data_path}. "
346
+ "Please ensure the package is properly installed."
347
+ )
348
+
349
+ return ad.read_h5ad(data_path)