aspect-data 0.0.1__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,333 @@
1
+ """Data preprocessing functions."""
2
+
3
+ from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Union
4
+ from functools import cache, partial, wraps
5
+ import hashlib
6
+
7
+ import numpy as np
8
+
9
+ from .registry import register_function
10
+
11
+
12
+ def transform(fn) -> Callable:
13
+ def factory(*args, **kwargs) -> Callable:
14
+ @wraps(fn)
15
+ def _fn(
16
+ data: Mapping[str, Iterable],
17
+ input_column: str
18
+ ):
19
+ return fn(data[input_column], *args, **kwargs)
20
+ return _fn
21
+ return factory
22
+
23
+
24
+ @register_function("identity")
25
+ @transform
26
+ def identity(x) -> np.ndarray:
27
+ """Simple pass-through.
28
+
29
+ """
30
+ return np.asarray(x)
31
+
32
+
33
+ @register_function("log")
34
+ @transform
35
+ def natural_log(x) -> np.ndarray:
36
+ """Log10 of float.
37
+
38
+ """
39
+ return np.log(x)
40
+
41
+
42
+ @register_function("one-hot")
43
+ def OneHot(
44
+ categories: Iterable[Union[str, int]],
45
+ intercept: bool = False
46
+ ) -> Callable:
47
+ """Convert string labels into one-hot encodings.
48
+
49
+ """
50
+ prepend = [1] if intercept else []
51
+
52
+ def _one_hot(
53
+ data: Mapping[str, Iterable],
54
+ input_column: str
55
+ ) -> np.ndarray:
56
+ return np.asarray([
57
+ prepend + [1 if x == cat else 0 for cat in categories]
58
+ for x in data[input_column]
59
+ ])
60
+
61
+ return _one_hot
62
+
63
+
64
+ @register_function("hash")
65
+ def Hash(
66
+ ndim: int = 256,
67
+ hash_name: str = "sha1",
68
+ dense: Optional[int] = None,
69
+ normalize: bool = False,
70
+ seed: int = 42,
71
+ ) -> Callable:
72
+ """Deterministic string hashing featurizer.
73
+
74
+ Each input string is hashed to a fixed-length numeric vector in [0,1] or [-1,1].
75
+
76
+ Parameters
77
+ ==========
78
+ ndim : int
79
+ Output feature dimension (number of hash buckets).
80
+ hash_name : str
81
+ Hash function to use ('sha1', 'md5', 'blake2b', ...).
82
+ dense : int, optional
83
+ Dense projection to this number of dimensions.
84
+ normalize : bool
85
+ Whether to normalize vector to unit length (L2 norm = 1).
86
+ seed : int
87
+ Seed to vary hash folding (adds reproducible offset).
88
+
89
+ Returns
90
+ =======
91
+ Callable
92
+ Function mapping (data, input_column) → np.ndarray [N, n_features]
93
+
94
+ Examples
95
+ ========
96
+ >>> import numpy as np
97
+ >>> f = Hash(ndim=8, seed=0)
98
+ >>> X = f({"assay": ["MIC", "MIC", "binding"]}, "assay")
99
+ >>> X.shape
100
+ (3, 8)
101
+ >>> np.array_equal(X[0], X[1])
102
+ True
103
+ >>> np.array_equal(X[0], X[2])
104
+ False
105
+
106
+ Seed changes output:
107
+ >>> f2 = Hash(ndim=8, seed=1)
108
+ >>> X2 = f2({"assay": ["MIC"]}, "assay")
109
+ >>> np.array_equal(X2[0], X[0])
110
+ False
111
+
112
+ Different hash backend changes output:
113
+ >>> f_md5 = Hash(ndim=8, hash_name="md5", seed=0)
114
+ >>> np.array_equal(f_md5({"assay": ["MIC"]}, "assay")[0], X[0])
115
+ False
116
+
117
+ Dense projection has the right shape and dtype:
118
+ >>> g = Hash(ndim=8, dense=4, seed=0)
119
+ >>> Y = g({"assay": ["MIC", "binding"]}, "assay")
120
+ >>> Y.shape, Y.dtype == np.float32
121
+ ((2, 4), True)
122
+
123
+ Values are in [-1, 1] before normalization:
124
+ >>> v = f({"assay": ["MIC"]}, "assay")[0]
125
+ >>> (v.min() >= -1 - 1e-6 and v.max() <= 1 + 1e-6).item()
126
+ True
127
+
128
+ Non-string values are stringified:
129
+ >>> f({"assay": [123, None]}, "assay").shape
130
+ (2, 8)
131
+
132
+ """
133
+ from numpy.random import default_rng
134
+ if ndim <= 0:
135
+ raise ValueError("ndim must be > 0")
136
+ if dense is not None and dense <= 0:
137
+ raise ValueError("dense must be > 0 when provided")
138
+
139
+ if dense is not None:
140
+ generator = default_rng(seed=seed)
141
+ projector = generator.normal(
142
+ scale=1. / np.sqrt(dense),
143
+ size=(int(ndim), int(dense)),
144
+ ).astype(np.float32)
145
+ else:
146
+ projector = None
147
+
148
+ def _hash_single(s: str) -> np.ndarray:
149
+ # Stable deterministic hashing per string
150
+ if not isinstance(s, str):
151
+ s = str(s)
152
+ h = hashlib.new(hash_name)
153
+ h.update((s + str(seed)).encode("utf-8"))
154
+ digest = np.frombuffer(h.digest(), dtype=np.uint8)
155
+ # Repeat or truncate digest to reach n_features bytes
156
+ vec = np.resize(digest, ndim)
157
+ vec = (2. * vec / 255. - 1.) # map to [-1, 1]
158
+ return vec
159
+
160
+
161
+ def _hash(
162
+ data: Mapping[str, Iterable],
163
+ input_column: str
164
+ ) -> np.ndarray:
165
+ vectors = np.stack([
166
+ _hash_single(v) for v in data[input_column]
167
+ ], axis=0).astype(np.float32)
168
+
169
+ if projector is not None:
170
+ vectors = vectors @ projector
171
+ if normalize:
172
+ n = np.linalg.norm(vectors, axis=-1, keepdims=True)
173
+ nz = n > 0
174
+ vectors[nz[:, 0]] = vectors[nz[:, 0]] / n[nz]
175
+ return vectors
176
+
177
+ return _hash
178
+
179
+
180
+ def chemical_feature(feature_type, **kwargs) -> Callable:
181
+ try:
182
+ from schemist.features import calculate_feature
183
+ except ImportError:
184
+ raise ImportError("schemist not installed! Try `pip install aspect[chem]`.")
185
+ feature_calculator = partial(
186
+ calculate_feature,
187
+ feature_type=feature_type,
188
+ **kwargs,
189
+ )
190
+
191
+ def _fn(
192
+ data: Mapping[str, Iterable],
193
+ input_column: str
194
+ ) -> np.ndarray:
195
+ fingerprints, _ = feature_calculator(strings=data[input_column])
196
+ return fingerprints
197
+
198
+ return _fn
199
+
200
+
201
+ MorganFingerprint = register_function("morgan-fingerprint")(
202
+ partial(
203
+ chemical_feature,
204
+ "fp",
205
+ return_dataframe=False,
206
+ on_bits=False,
207
+ ),
208
+ )
209
+
210
+ Descriptors2D = register_function("descriptors-2d")(
211
+ partial(
212
+ chemical_feature,
213
+ "2d",
214
+ return_dataframe=False,
215
+ normalized=True,
216
+ histogram_normalized=True,
217
+ ),
218
+ )
219
+
220
+ Descriptors3D = register_function("descriptors-3d")(
221
+ partial(
222
+ chemical_feature,
223
+ "3d",
224
+ return_dataframe=False,
225
+ ),
226
+ )
227
+
228
+ @register_function("vectome-fingerprint")
229
+ def VectomeFingerprint(
230
+ method: str = "countsketch",
231
+ ndim: int = 2048,
232
+ check_spelling: bool = True,
233
+ **kwargs
234
+ ) -> Callable:
235
+ """Get MinHash fingerprint from species name or taxon ID.
236
+
237
+ """
238
+ try:
239
+ from vectome.vectorize import vectorize
240
+ except ImportError:
241
+ raise ImportError(f"Vectome not installed! Try `pip install aspect[bio]`.")
242
+
243
+ feature_calculator = cache(partial(
244
+ vectorize,
245
+ method=method,
246
+ dim=ndim,
247
+ check_spelling=check_spelling,
248
+ quiet=True,
249
+ **kwargs,
250
+ ))
251
+
252
+ def _vectome_fingerprint(
253
+ data: Mapping[str, Iterable],
254
+ input_column: str
255
+ ) -> np.ndarray:
256
+ return feature_calculator(query=tuple(data[input_column]))
257
+
258
+ return _vectome_fingerprint
259
+
260
+
261
+ @register_function("chemprop-mol")
262
+ def ChempropData(
263
+ label_column: Optional[Union[str, Iterable[str]]] = None,
264
+ extra_featurizers: Optional[Union[Mapping[str, Any], Iterable[Mapping[str, Any]]]] = None
265
+ ) -> Callable:
266
+ """Convert SMILES to iterable of Chemprop datum.
267
+
268
+ """
269
+ try:
270
+ from chemprop.data import (
271
+ MoleculeDatapoint,
272
+ MoleculeDataset,
273
+ MolGraph
274
+ )
275
+ except ImportError:
276
+ raise ImportError("Chemprop not installed. Try `pip install aspect[chemprop]`.")
277
+
278
+ if isinstance(extra_featurizers, str):
279
+ extra_featurizers = [extra_featurizers]
280
+ if isinstance(label_column, str):
281
+ label_column = [label_column]
282
+
283
+ def _stack_columns(
284
+ data: Mapping[str, Iterable],
285
+ nrows: int,
286
+ columns: Optional[Iterable[str]] = None
287
+ ) -> np.ndarray:
288
+ if columns is None:
289
+ array = [None] * nrows
290
+ else:
291
+ array = [np.asarray(data[col]) for col in columns]
292
+ array = [a if a.ndim > 1 else a[..., np.newaxis] for a in array]
293
+ if len(array) > 0:
294
+ array = np.concatenate(array, axis=-1).astype(np.float32)
295
+ else:
296
+ array = [None] * nrows
297
+ return array
298
+
299
+ def _chemprop_data(
300
+ data: Mapping[str, Iterable],
301
+ input_column: str
302
+ ) -> List[Dict[str, np.ndarray]]:
303
+ nrows = len(data[input_column])
304
+ y_vals = _stack_columns(data, nrows, label_column)
305
+ extra_features = _stack_columns(data, nrows, extra_featurizers)
306
+
307
+ mol_datapoints = [
308
+ MoleculeDatapoint.from_smi(smi=x, y=y, x_d=xd)
309
+ for x, y, xd in zip(data[input_column], y_vals, extra_features)
310
+ ]
311
+
312
+ datums = []
313
+ for datum in MoleculeDataset(mol_datapoints):
314
+ new_datum = {}
315
+ for key, val in datum._asdict().items():
316
+ if isinstance(val, MolGraph):
317
+ new_val = {
318
+ key2: val2.astype(np.float32) if isinstance(val2, np.ndarray) else np.float32(val2)
319
+ for key2, val2 in val._asdict().items()
320
+ }
321
+ elif isinstance(val, float):
322
+ new_val = np.float32(val)
323
+ elif isinstance(val, np.ndarray):
324
+ new_val = val.astype(np.float32)
325
+ elif val is not None:
326
+ new_val = val
327
+ else:
328
+ new_val = None
329
+ new_datum[key] = new_val
330
+ datums.append(new_datum)
331
+ return datums
332
+
333
+ return _chemprop_data
@@ -0,0 +1,17 @@
1
+ """Tools to make allowed preprocessing functions."""
2
+
3
+ from typing import Callable, Dict
4
+
5
+ # Registry to map function names to actual implementations
6
+ FUNCTION_REGISTRY: Dict[str, Callable] = {}
7
+
8
+
9
+ def register_function(name: str):
10
+ """Decorator to register a function in the function registry.
11
+
12
+ """
13
+ def decorator(f: Callable):
14
+ FUNCTION_REGISTRY[name] = f
15
+ return f
16
+
17
+ return decorator
aspect/typing.py ADDED
@@ -0,0 +1,31 @@
1
+ """Custom types."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Iterable, Mapping, Union
4
+
5
+ from datasets import Dataset, IterableDataset
6
+ from pandas import DataFrame
7
+ from numpy.typing import ArrayLike
8
+
9
+ if TYPE_CHECKING:
10
+ from .transform.base import ColumnTransform
11
+ else:
12
+ ColumnTransform = Any
13
+
14
+ DataLike = Union[
15
+ str,
16
+ DataFrame,
17
+ Mapping[str, ArrayLike],
18
+ Dataset,
19
+ IterableDataset
20
+ ]
21
+ FeatureLike = Union[
22
+ str,
23
+ Mapping[str, ColumnTransform],
24
+ Mapping[str, Mapping[str, Any]],
25
+ Iterable[Union[
26
+ str,
27
+ Mapping[str, ColumnTransform],
28
+ Mapping[str, Mapping[str, Any]]
29
+ ]]
30
+ ]
31
+ StrOrIterableOfStr = Union[str, Iterable[str]]