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.
- aspect/__init__.py +5 -0
- aspect/checkpoint_utils.py +95 -0
- aspect/cli_module/__init__.py +0 -0
- aspect/cli_module/cli.py +129 -0
- aspect/cli_module/featurize.py +187 -0
- aspect/cli_module/io.py +52 -0
- aspect/data.py +577 -0
- aspect/io.py +189 -0
- aspect/package_data.py +35 -0
- aspect/transform/__init__.py +9 -0
- aspect/transform/base.py +182 -0
- aspect/transform/deep_functions.py +143 -0
- aspect/transform/functions.py +333 -0
- aspect/transform/registry.py +17 -0
- aspect/typing.py +31 -0
- aspect_data-0.0.1.dist-info/METADATA +917 -0
- aspect_data-0.0.1.dist-info/RECORD +21 -0
- aspect_data-0.0.1.dist-info/WHEEL +5 -0
- aspect_data-0.0.1.dist-info/entry_points.txt +2 -0
- aspect_data-0.0.1.dist-info/licenses/LICENSE +21 -0
- aspect_data-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -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]]
|