bed-reader 1.0.6__cp313-none-win_amd64.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.
- bed_reader/__init__.py +32 -0
- bed_reader/_open_bed.py +1842 -0
- bed_reader/_sample_data.py +159 -0
- bed_reader/_to_bed.py +605 -0
- bed_reader/bed_reader.cp313-win_amd64.pyd +0 -0
- bed_reader/tests/__init__.py +1 -0
- bed_reader/tests/benchmark/bench2.ipynb +251 -0
- bed_reader/tests/benchmark/benchmark.py +135 -0
- bed_reader/tests/data/500x400_o640_array.memmap +0 -0
- bed_reader/tests/data/badfile.bed +1 -0
- bed_reader/tests/data/empty.bed +0 -0
- bed_reader/tests/data/plink_sim_10s_100v_10pmiss.bed +0 -0
- bed_reader/tests/data/plink_sim_10s_100v_10pmiss.bim +100 -0
- bed_reader/tests/data/plink_sim_10s_100v_10pmiss.fam +10 -0
- bed_reader/tests/data/small.bad_bim +4 -0
- bed_reader/tests/data/small.bed +1 -0
- bed_reader/tests/data/small.bim +4 -0
- bed_reader/tests/data/small.bim_bad_positions.bim +4 -0
- bed_reader/tests/data/small.deb +1 -0
- bed_reader/tests/data/small.fam +3 -0
- bed_reader/tests/data/small.fam_bad +4 -0
- bed_reader/tests/data/small.maf +3 -0
- bed_reader/tests/data/small.mib +4 -0
- bed_reader/tests/data/small2_array.memmap +0 -0
- bed_reader/tests/data/small_array.memmap +0 -0
- bed_reader/tests/data/small_no_bim.bed +1 -0
- bed_reader/tests/data/small_no_bim.fam +3 -0
- bed_reader/tests/data/small_no_fam.bed +1 -0
- bed_reader/tests/data/small_no_fam.bim +4 -0
- bed_reader/tests/data/small_too_short.bed +1 -0
- bed_reader/tests/data/small_too_short.bim +4 -0
- bed_reader/tests/data/small_too_short.fam +3 -0
- bed_reader/tests/data/smallmode0.bed +0 -0
- bed_reader/tests/data/smallmode0.bim +3 -0
- bed_reader/tests/data/smallmode0.fam +4 -0
- bed_reader/tests/data/smallmodebad.bed +1 -0
- bed_reader/tests/data/some_missing.bed +0 -0
- bed_reader/tests/data/some_missing.bim +100 -0
- bed_reader/tests/data/some_missing.fam +100 -0
- bed_reader/tests/data/some_missing.properties.npz +0 -0
- bed_reader/tests/data/some_missing.val.npy +0 -0
- bed_reader/tests/data/sparse.bed +1 -0
- bed_reader/tests/data/sparse.bim +20 -0
- bed_reader/tests/data/sparse.fam +10 -0
- bed_reader/tests/data/toydata.5chrom.bed +0 -0
- bed_reader/tests/data/toydata.5chrom.bim +10000 -0
- bed_reader/tests/data/toydata.5chrom.fam +500 -0
- bed_reader/tests/registry.txt +41 -0
- bed_reader/tests/test_file_dot.py +359 -0
- bed_reader/tests/test_open_bed.py +1448 -0
- bed_reader/tests/test_open_bed_cloud.py +1165 -0
- bed_reader/tests/test_opt_dep.py +29 -0
- bed_reader-1.0.6.dist-info/METADATA +155 -0
- bed_reader-1.0.6.dist-info/RECORD +57 -0
- bed_reader-1.0.6.dist-info/WHEEL +4 -0
- bed_reader-1.0.6.dist-info/licenses/AUTHORS.txt +2 -0
- bed_reader-1.0.6.dist-info/licenses/LICENSE.md +203 -0
bed_reader/_open_bed.py
ADDED
|
@@ -0,0 +1,1842 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import multiprocessing
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from itertools import repeat, takewhile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any, List, Mapping, Optional, Union
|
|
10
|
+
from urllib.parse import ParseResult as UrlParseResult
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from scipy import sparse
|
|
17
|
+
else:
|
|
18
|
+
try:
|
|
19
|
+
from scipy import sparse
|
|
20
|
+
except ImportError:
|
|
21
|
+
sparse = None
|
|
22
|
+
|
|
23
|
+
from .bed_reader import (
|
|
24
|
+
check_file_cloud,
|
|
25
|
+
read_cloud_f32,
|
|
26
|
+
read_cloud_f64,
|
|
27
|
+
read_cloud_i8,
|
|
28
|
+
read_f32,
|
|
29
|
+
read_f64,
|
|
30
|
+
read_i8,
|
|
31
|
+
url_to_bytes,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# https://stackoverflow.com/questions/845058/how-to-get-line-count-of-a-large-file-cheaply-in-python
|
|
36
|
+
def _rawincount(f):
|
|
37
|
+
f.seek(0)
|
|
38
|
+
bufgen = takewhile(lambda x: x, (f.read(1024 * 1024) for _ in repeat(None)))
|
|
39
|
+
return sum(buf.count(b"\n") for buf in bufgen)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class _MetaMeta:
|
|
44
|
+
suffix: str
|
|
45
|
+
column: int
|
|
46
|
+
dtype: type
|
|
47
|
+
missing_value: object
|
|
48
|
+
fill_sequence: object
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _all_same(key, length, missing, dtype):
|
|
52
|
+
if np.issubdtype(dtype, np.str_):
|
|
53
|
+
dtype = f"<U{len(missing)}"
|
|
54
|
+
return np.full(length, missing, dtype=dtype)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _sequence(key, length, missing, dtype):
|
|
58
|
+
if np.issubdtype(dtype, np.str_):
|
|
59
|
+
longest = len(f"{key}{length}")
|
|
60
|
+
dtype = f"<U{longest}"
|
|
61
|
+
return np.fromiter(
|
|
62
|
+
(f"{key}{i + 1}" for i in range(length)), dtype=dtype, count=length,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_delimiters = {"fam": r"\s+", "bim": "\t"}
|
|
67
|
+
_count_name = {"fam": "iid_count", "bim": "sid_count"}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_meta_meta = {
|
|
71
|
+
# https://stackoverflow.com/questions/41921255/staticmethod-object-is-not-callable
|
|
72
|
+
"fid": _MetaMeta("fam", 0, np.str_, "0", _all_same),
|
|
73
|
+
"iid": _MetaMeta("fam", 1, np.str_, None, _sequence),
|
|
74
|
+
"father": _MetaMeta("fam", 2, np.str_, "0", _all_same),
|
|
75
|
+
"mother": _MetaMeta("fam", 3, np.str_, "0", _all_same),
|
|
76
|
+
"sex": _MetaMeta("fam", 4, np.int32, 0, _all_same),
|
|
77
|
+
"pheno": _MetaMeta("fam", 5, np.str_, "0", _all_same),
|
|
78
|
+
"chromosome": _MetaMeta("bim", 0, np.str_, "0", _all_same),
|
|
79
|
+
"sid": _MetaMeta("bim", 1, np.str_, None, _sequence),
|
|
80
|
+
"cm_position": _MetaMeta("bim", 2, np.float32, 0, _all_same),
|
|
81
|
+
"bp_position": _MetaMeta("bim", 3, np.int32, 0, _all_same),
|
|
82
|
+
"allele_1": _MetaMeta("bim", 4, np.str_, "A1", _all_same),
|
|
83
|
+
"allele_2": _MetaMeta("bim", 5, np.str_, "A2", _all_same),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_num_threads(num_threads=None):
|
|
88
|
+
if num_threads is not None:
|
|
89
|
+
return num_threads
|
|
90
|
+
if "PST_NUM_THREADS" in os.environ:
|
|
91
|
+
return int(os.environ["PST_NUM_THREADS"])
|
|
92
|
+
if "NUM_THREADS" in os.environ:
|
|
93
|
+
return int(os.environ["NUM_THREADS"])
|
|
94
|
+
if "MKL_NUM_THREADS" in os.environ:
|
|
95
|
+
return int(os.environ["MKL_NUM_THREADS"])
|
|
96
|
+
return multiprocessing.cpu_count()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_max_concurrent_requests(max_concurrent_requests=None):
|
|
100
|
+
if max_concurrent_requests is not None:
|
|
101
|
+
return max_concurrent_requests
|
|
102
|
+
return 10
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_max_chunk_bytes(max_chunk_bytes=None):
|
|
106
|
+
if max_chunk_bytes is not None:
|
|
107
|
+
return max_chunk_bytes
|
|
108
|
+
return 8_000_000
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class open_bed:
|
|
112
|
+
"""Open a PLINK .bed file, local or cloud, for reading.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
location: pathlib.Path or str
|
|
117
|
+
File path or URL to the .bed file. See :doc:`cloud_urls` for details on cloud URLs.
|
|
118
|
+
iid_count: None or int, optional
|
|
119
|
+
Number of individuals (samples) in the .bed file.
|
|
120
|
+
The default (``iid_count=None``) finds the number
|
|
121
|
+
automatically by quickly scanning the .fam file.
|
|
122
|
+
sid_count: None or int, optional
|
|
123
|
+
Number of SNPs (variants) in the .bed file.
|
|
124
|
+
The default (``sid_count=None``) finds the number
|
|
125
|
+
automatically by quickly scanning the .bim file.
|
|
126
|
+
properties: dict, optional
|
|
127
|
+
A dictionary of any replacement properties. The default is an empty dictionary.
|
|
128
|
+
The keys of the dictionary are the names of the properties to replace.
|
|
129
|
+
The possible keys are:
|
|
130
|
+
|
|
131
|
+
"fid" (family id), "iid" (individual or sample id), "father" (father id),
|
|
132
|
+
"mother" (mother id), "sex", "pheno" (phenotype), "chromosome", "sid"
|
|
133
|
+
(SNP or variant id), "cm_position" (centimorgan position), "bp_position"
|
|
134
|
+
(base-pair position), "allele_1", "allele_2".
|
|
135
|
+
|
|
136
|
+
The values are replacement lists or arrays. A value can also be `None`,
|
|
137
|
+
meaning do not read or offer this property. See examples, below.
|
|
138
|
+
|
|
139
|
+
The list or array will be converted to a :class:`numpy.ndarray`
|
|
140
|
+
of the appropriate dtype, if necessary. Any :data:`numpy.nan` values
|
|
141
|
+
will converted to the appropriate missing value. The PLINK `.fam specification
|
|
142
|
+
<https://www.cog-genomics.org/plink2/formats#fam>`_
|
|
143
|
+
and `.bim specification <https://www.cog-genomics.org/plink2/formats#bim>`_
|
|
144
|
+
lists the dtypes and missing values for each property.
|
|
145
|
+
|
|
146
|
+
count_A1: bool, optional
|
|
147
|
+
True (default) to count the number of A1 alleles (the PLINK standard).
|
|
148
|
+
False to count the number of A2 alleles.
|
|
149
|
+
num_threads: None or int, optional
|
|
150
|
+
The number of threads with which to read data. Defaults to all available
|
|
151
|
+
processors.
|
|
152
|
+
Can also be set with these environment variables (listed in priority order):
|
|
153
|
+
'PST_NUM_THREADS', 'NUM_THREADS', 'MKL_NUM_THREADS'.
|
|
154
|
+
skip_format_check: bool, optional
|
|
155
|
+
False (default) to immediately check for expected starting bytes in
|
|
156
|
+
the .bed file. True to delay the check until (and if) data is read.
|
|
157
|
+
fam_location: pathlib.Path or str or URL, optional
|
|
158
|
+
Path to the file containing information about each individual (sample).
|
|
159
|
+
Defaults to replacing the .bed file’s suffix with .fam.
|
|
160
|
+
bim_location: pathlib.Path or str URL, optional
|
|
161
|
+
Path to the file containing information about each SNP (variant).
|
|
162
|
+
Defaults to replacing the .bed file’s suffix with .bim.
|
|
163
|
+
cloud_options: dict, optional
|
|
164
|
+
A dictionary of options for reading from cloud storage. The default is an empty.
|
|
165
|
+
max_concurrent_requests: None or int, optional
|
|
166
|
+
The maximum number of concurrent requests to make to the cloud storage service.
|
|
167
|
+
Defaults to 10.
|
|
168
|
+
max_chunk_bytes: None or int, optional
|
|
169
|
+
The maximum number of bytes to read in a single request to the cloud storage
|
|
170
|
+
service. Defaults to 8MB.
|
|
171
|
+
filepath: same as location
|
|
172
|
+
Deprecated. Use location instead.
|
|
173
|
+
fam_filepath: same as fam_location
|
|
174
|
+
Deprecated. Use fam_location instead.
|
|
175
|
+
bim_filepath: same as bim_location
|
|
176
|
+
Deprecated. Use bim_location instead.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
open_bed
|
|
181
|
+
an open_bed object
|
|
182
|
+
|
|
183
|
+
Examples
|
|
184
|
+
--------
|
|
185
|
+
Open a local file and list individual (sample) :attr:`iid` and SNP (variant) :attr:`sid`.
|
|
186
|
+
Then, :meth:`read` the whole file.
|
|
187
|
+
|
|
188
|
+
.. doctest::
|
|
189
|
+
|
|
190
|
+
>>> from bed_reader import open_bed, sample_file
|
|
191
|
+
>>>
|
|
192
|
+
>>> file_name = sample_file("small.bed")
|
|
193
|
+
>>> bed = open_bed(file_name)
|
|
194
|
+
>>> print(bed.iid)
|
|
195
|
+
['iid1' 'iid2' 'iid3']
|
|
196
|
+
>>> print(bed.sid)
|
|
197
|
+
['sid1' 'sid2' 'sid3' 'sid4']
|
|
198
|
+
>>> print(bed.read())
|
|
199
|
+
[[ 1. 0. nan 0.]
|
|
200
|
+
[ 2. 0. nan 2.]
|
|
201
|
+
[ 0. 1. 2. 0.]]
|
|
202
|
+
>>> del bed # optional: delete bed object
|
|
203
|
+
|
|
204
|
+
Open a cloud file with a non-default timeout.
|
|
205
|
+
Then, read the data for one SNP (variant)
|
|
206
|
+
at index position 2.
|
|
207
|
+
|
|
208
|
+
See :doc:`cloud_urls` for details on reading files from cloud storage.
|
|
209
|
+
|
|
210
|
+
.. doctest::
|
|
211
|
+
|
|
212
|
+
>>> import numpy as np
|
|
213
|
+
>>> with open_bed("https://raw.githubusercontent.com/fastlmm/bed-sample-files/main/small.bed",
|
|
214
|
+
... cloud_options={"timeout": "10s"}) as bed:
|
|
215
|
+
... print(bed.read(np.s_[:,2]))
|
|
216
|
+
[[nan]
|
|
217
|
+
[nan]
|
|
218
|
+
[ 2.]]
|
|
219
|
+
|
|
220
|
+
With the local file, replace :attr:`iid`.
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
>>> bed = open_bed(file_name, properties={"iid":["sample1","sample2","sample3"]})
|
|
224
|
+
>>> print(bed.iid) # replaced
|
|
225
|
+
['sample1' 'sample2' 'sample3']
|
|
226
|
+
>>> print(bed.sid) # same as before
|
|
227
|
+
['sid1' 'sid2' 'sid3' 'sid4']
|
|
228
|
+
|
|
229
|
+
Give the number of individuals (samples) and SNPs (variants) so that the .fam and
|
|
230
|
+
.bim files need never be opened.
|
|
231
|
+
|
|
232
|
+
>>> with open_bed(file_name, iid_count=3, sid_count=4) as bed:
|
|
233
|
+
... print(bed.read())
|
|
234
|
+
[[ 1. 0. nan 0.]
|
|
235
|
+
[ 2. 0. nan 2.]
|
|
236
|
+
[ 0. 1. 2. 0.]]
|
|
237
|
+
|
|
238
|
+
Mark some properties as "don’t read or offer".
|
|
239
|
+
|
|
240
|
+
>>> bed = open_bed(file_name, properties={
|
|
241
|
+
... "father" : None, "mother" : None, "sex" : None, "pheno" : None,
|
|
242
|
+
... "allele_1" : None, "allele_2":None })
|
|
243
|
+
>>> print(bed.iid) # read from file
|
|
244
|
+
['iid1' 'iid2' 'iid3']
|
|
245
|
+
>>> print(bed.allele_2) # not read and not offered
|
|
246
|
+
None
|
|
247
|
+
|
|
248
|
+
See the :meth:`read` for details of reading batches via slicing and fancy indexing.
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(
|
|
253
|
+
self,
|
|
254
|
+
location: Union[str, Path, UrlParseResult],
|
|
255
|
+
iid_count: Optional[int] = None,
|
|
256
|
+
sid_count: Optional[int] = None,
|
|
257
|
+
properties: Mapping[str, List[Any]] = {},
|
|
258
|
+
count_A1: bool = True,
|
|
259
|
+
num_threads: Optional[int] = None,
|
|
260
|
+
skip_format_check: bool = False,
|
|
261
|
+
fam_location: Optional[Union[str, Path, UrlParseResult]] = None,
|
|
262
|
+
bim_location: Optional[Union[str, Path, UrlParseResult]] = None,
|
|
263
|
+
cloud_options: Mapping[str, str] = {},
|
|
264
|
+
max_concurrent_requests: Optional[int] = None,
|
|
265
|
+
max_chunk_bytes: Optional[int] = None,
|
|
266
|
+
# accept old keywords
|
|
267
|
+
filepath: Optional[Union[str, Path]] = None,
|
|
268
|
+
fam_filepath: Optional[Union[str, Path]] = None,
|
|
269
|
+
bim_filepath: Optional[Union[str, Path]] = None,
|
|
270
|
+
) -> None:
|
|
271
|
+
location = self._combined(location, filepath, "location", "filepath")
|
|
272
|
+
fam_location = self._combined(
|
|
273
|
+
fam_location, fam_filepath, "fam_location", "fam_filepath",
|
|
274
|
+
)
|
|
275
|
+
bim_location = self._combined(
|
|
276
|
+
bim_location, bim_filepath, "bim_location", "bim_filepath",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
self.location = self._path_or_url(location)
|
|
280
|
+
self.cloud_options = cloud_options
|
|
281
|
+
self.count_A1 = count_A1
|
|
282
|
+
self._num_threads = num_threads
|
|
283
|
+
self._max_concurrent_requests = max_concurrent_requests
|
|
284
|
+
self._max_chunk_bytes = max_chunk_bytes
|
|
285
|
+
self.skip_format_check = skip_format_check
|
|
286
|
+
self._fam_location = (
|
|
287
|
+
self._path_or_url(fam_location)
|
|
288
|
+
if fam_location is not None
|
|
289
|
+
else self._replace_extension(self.location, "fam")
|
|
290
|
+
)
|
|
291
|
+
self._bim_location = (
|
|
292
|
+
self._path_or_url(bim_location)
|
|
293
|
+
if bim_location is not None
|
|
294
|
+
else self._replace_extension(self.location, "bim")
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
self.properties_dict, self._counts = open_bed._fix_up_properties(
|
|
298
|
+
properties, iid_count, sid_count, use_fill_sequence=False,
|
|
299
|
+
)
|
|
300
|
+
self._iid_range = None
|
|
301
|
+
self._sid_range = None
|
|
302
|
+
if not self.skip_format_check:
|
|
303
|
+
if self._is_url(self.location):
|
|
304
|
+
check_file_cloud(self.location.geturl(), self.cloud_options)
|
|
305
|
+
else:
|
|
306
|
+
with open(self.location, "rb") as filepointer:
|
|
307
|
+
self._mode = self._check_file(filepointer)
|
|
308
|
+
|
|
309
|
+
# # its an error to set both location and filepath
|
|
310
|
+
# location = self._combined(location, filepath, "location", "filepath")
|
|
311
|
+
# fam_location = self._combined(fam_location, fam_filepath, "fam_location", "fam_filepath")
|
|
312
|
+
# bim_location = self._combined(bim_location, bim_filepath, "bim_location", "bim_filepath")
|
|
313
|
+
@staticmethod
|
|
314
|
+
def _combined(location, filepath, location_name, filepath_name):
|
|
315
|
+
if location is not None and filepath is not None:
|
|
316
|
+
msg = f"Cannot set both {location_name} and {filepath_name}"
|
|
317
|
+
raise ValueError(msg)
|
|
318
|
+
# None, None is ok for now
|
|
319
|
+
return location if location is not None else filepath
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
def _replace_extension(location, extension):
|
|
323
|
+
if open_bed._is_url(location):
|
|
324
|
+
# Split the path and change the extension
|
|
325
|
+
path, _ = os.path.splitext(location.path)
|
|
326
|
+
new_path = f"{path}.{extension}"
|
|
327
|
+
|
|
328
|
+
# Create a new ParseResult with the updated path
|
|
329
|
+
return UrlParseResult(
|
|
330
|
+
scheme=location.scheme,
|
|
331
|
+
netloc=location.netloc,
|
|
332
|
+
path=new_path,
|
|
333
|
+
params=location.params,
|
|
334
|
+
query=location.query,
|
|
335
|
+
fragment=location.fragment,
|
|
336
|
+
)
|
|
337
|
+
assert isinstance(location, Path) # real assert
|
|
338
|
+
return location.parent / (location.stem + "." + extension)
|
|
339
|
+
|
|
340
|
+
@staticmethod
|
|
341
|
+
def _is_url(location):
|
|
342
|
+
return isinstance(location, UrlParseResult)
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def _path_or_url(input):
|
|
346
|
+
if isinstance(input, Path):
|
|
347
|
+
return input
|
|
348
|
+
if isinstance(input, UrlParseResult):
|
|
349
|
+
return input
|
|
350
|
+
assert isinstance(
|
|
351
|
+
input, str,
|
|
352
|
+
), "Expected a string or Path object or UrlParseResult"
|
|
353
|
+
parsed = urlparse(input)
|
|
354
|
+
if parsed.scheme and "://" in input:
|
|
355
|
+
return parsed
|
|
356
|
+
return Path(input)
|
|
357
|
+
|
|
358
|
+
def read(
|
|
359
|
+
self,
|
|
360
|
+
index: Optional[Any] = None,
|
|
361
|
+
dtype: Optional[Union[type, str]] = "float32",
|
|
362
|
+
order: Optional[str] = "F",
|
|
363
|
+
force_python_only: Optional[bool] = False,
|
|
364
|
+
num_threads=None,
|
|
365
|
+
max_concurrent_requests=None,
|
|
366
|
+
max_chunk_bytes=None,
|
|
367
|
+
) -> np.ndarray:
|
|
368
|
+
"""Read genotype information.
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
index:
|
|
373
|
+
An optional expression specifying the individuals (samples) and SNPs
|
|
374
|
+
(variants) to read. (See examples, below).
|
|
375
|
+
Defaults to ``None``, meaning read all.
|
|
376
|
+
|
|
377
|
+
(If index is a tuple, the first component indexes the individuals and the
|
|
378
|
+
second indexes
|
|
379
|
+
the SNPs. If it is not a tuple and not None, it indexes SNPs.)
|
|
380
|
+
|
|
381
|
+
dtype: {'float32' (default), 'float64', 'int8'}, optional
|
|
382
|
+
The desired data-type for the returned array.
|
|
383
|
+
order : {'F','C'}, optional
|
|
384
|
+
The desired memory layout for the returned array.
|
|
385
|
+
Defaults to ``F`` (Fortran order, which is SNP-major).
|
|
386
|
+
force_python_only: bool, optional
|
|
387
|
+
If False (default), uses the faster Rust code; otherwise it uses the slower
|
|
388
|
+
pure Python code.
|
|
389
|
+
|
|
390
|
+
num_threads: None or int, optional
|
|
391
|
+
The number of threads with which to read data. Defaults to all available
|
|
392
|
+
processors.
|
|
393
|
+
Can also be set with :class:`open_bed` or these
|
|
394
|
+
environment variables (listed in priority order):
|
|
395
|
+
'PST_NUM_THREADS', 'NUM_THREADS', 'MKL_NUM_THREADS'.
|
|
396
|
+
|
|
397
|
+
max_concurrent_requests: None or int, optional
|
|
398
|
+
The maximum number of concurrent requests to make to the cloud storage
|
|
399
|
+
service. Defaults to 10.
|
|
400
|
+
|
|
401
|
+
max_chunk_bytes: None or int, optional
|
|
402
|
+
The maximum number of bytes to read in a single request to the cloud
|
|
403
|
+
storage service. Defaults to 8MB.
|
|
404
|
+
|
|
405
|
+
Returns
|
|
406
|
+
-------
|
|
407
|
+
numpy.ndarray
|
|
408
|
+
2-D array containing values of 0, 1, 2, or missing
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
Rows represent individuals (samples). Columns represent SNPs (variants).
|
|
412
|
+
|
|
413
|
+
For ``dtype`` 'float32' and 'float64', NaN indicates missing values.
|
|
414
|
+
For 'int8', -127 indicates missing values.
|
|
415
|
+
|
|
416
|
+
Examples
|
|
417
|
+
--------
|
|
418
|
+
To read all data in a .bed file, set ``index`` to ``None``. This is the default.
|
|
419
|
+
|
|
420
|
+
.. doctest::
|
|
421
|
+
|
|
422
|
+
>>> from bed_reader import open_bed, sample_file
|
|
423
|
+
>>>
|
|
424
|
+
>>> file_name = sample_file("small.bed")
|
|
425
|
+
>>> with open_bed(file_name) as bed:
|
|
426
|
+
... print(bed.read())
|
|
427
|
+
[[ 1. 0. nan 0.]
|
|
428
|
+
[ 2. 0. nan 2.]
|
|
429
|
+
[ 0. 1. 2. 0.]]
|
|
430
|
+
|
|
431
|
+
To read selected individuals (samples) and/or SNPs (variants), set each part of
|
|
432
|
+
a :data:`numpy.s_` to an `int`, a list of `int`, a slice expression, or
|
|
433
|
+
a list of `bool`.
|
|
434
|
+
Negative integers count from the end of the list.
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
.. doctest::
|
|
438
|
+
|
|
439
|
+
>>> import numpy as np
|
|
440
|
+
>>> bed = open_bed(file_name)
|
|
441
|
+
>>> print(bed.read(np.s_[:,2])) # read the SNPs indexed by 2.
|
|
442
|
+
[[nan]
|
|
443
|
+
[nan]
|
|
444
|
+
[ 2.]]
|
|
445
|
+
>>> print(bed.read(np.s_[:,[2,3,0]])) # read the SNPs indexed by 2, 3, and 0
|
|
446
|
+
[[nan 0. 1.]
|
|
447
|
+
[nan 2. 2.]
|
|
448
|
+
[ 2. 0. 0.]]
|
|
449
|
+
>>> # read SNPs from 1 (inclusive) to 4 (exclusive)
|
|
450
|
+
>>> print(bed.read(np.s_[:,1:4]))
|
|
451
|
+
[[ 0. nan 0.]
|
|
452
|
+
[ 0. nan 2.]
|
|
453
|
+
[ 1. 2. 0.]]
|
|
454
|
+
>>> print(np.unique(bed.chromosome)) # print unique chrom values
|
|
455
|
+
['1' '5' 'Y']
|
|
456
|
+
>>> print(bed.read(np.s_[:,bed.chromosome=='5'])) # read all SNPs in chrom 5
|
|
457
|
+
[[nan]
|
|
458
|
+
[nan]
|
|
459
|
+
[ 2.]]
|
|
460
|
+
>>> print(bed.read(np.s_[0,:])) # Read 1st individual (across all SNPs)
|
|
461
|
+
[[ 1. 0. nan 0.]]
|
|
462
|
+
>>> print(bed.read(np.s_[::2,:])) # Read every 2nd individual
|
|
463
|
+
[[ 1. 0. nan 0.]
|
|
464
|
+
[ 0. 1. 2. 0.]]
|
|
465
|
+
>>> #read last and 2nd-to-last individuals and the last SNPs
|
|
466
|
+
>>> print(bed.read(np.s_[[-1,-2],-1]))
|
|
467
|
+
[[0.]
|
|
468
|
+
[2.]]
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
You can give a dtype for the output.
|
|
472
|
+
|
|
473
|
+
.. doctest::
|
|
474
|
+
|
|
475
|
+
>>> print(bed.read(dtype='int8'))
|
|
476
|
+
[[ 1 0 -127 0]
|
|
477
|
+
[ 2 0 -127 2]
|
|
478
|
+
[ 0 1 2 0]]
|
|
479
|
+
>>> del bed # optional: delete bed object
|
|
480
|
+
|
|
481
|
+
"""
|
|
482
|
+
iid_index_or_slice_etc, sid_index_or_slice_etc = self._split_index(index)
|
|
483
|
+
|
|
484
|
+
dtype = np.dtype(dtype)
|
|
485
|
+
if order not in {"F", "C"}:
|
|
486
|
+
msg = f"order '{order}' not known, only 'F', 'C'"
|
|
487
|
+
raise ValueError(msg)
|
|
488
|
+
|
|
489
|
+
# Later happy with _iid_range and _sid_range or could it be done with
|
|
490
|
+
# allocation them?
|
|
491
|
+
if self._iid_range is None:
|
|
492
|
+
self._iid_range = np.arange(self.iid_count, dtype="intp")
|
|
493
|
+
if self._sid_range is None:
|
|
494
|
+
self._sid_range = np.arange(self.sid_count, dtype="intp")
|
|
495
|
+
|
|
496
|
+
iid_index = np.ascontiguousarray(
|
|
497
|
+
self._iid_range[iid_index_or_slice_etc],
|
|
498
|
+
dtype="intp",
|
|
499
|
+
)
|
|
500
|
+
sid_index = np.ascontiguousarray(
|
|
501
|
+
self._sid_range[sid_index_or_slice_etc], dtype="intp",
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
if not force_python_only or open_bed._is_url(self.location):
|
|
505
|
+
num_threads = get_num_threads(
|
|
506
|
+
self._num_threads if num_threads is None else num_threads,
|
|
507
|
+
)
|
|
508
|
+
max_concurrent_requests = get_max_concurrent_requests(
|
|
509
|
+
self._max_concurrent_requests
|
|
510
|
+
if max_concurrent_requests is None
|
|
511
|
+
else max_concurrent_requests,
|
|
512
|
+
)
|
|
513
|
+
max_chunk_bytes = get_max_chunk_bytes(
|
|
514
|
+
self._max_chunk_bytes if max_chunk_bytes is None else max_chunk_bytes,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
val = np.zeros((len(iid_index), len(sid_index)), order=order, dtype=dtype)
|
|
518
|
+
|
|
519
|
+
if self.iid_count > 0 and self.sid_count > 0:
|
|
520
|
+
reader, location_str, is_cloud = self._pick_reader(dtype)
|
|
521
|
+
|
|
522
|
+
if not is_cloud:
|
|
523
|
+
reader(
|
|
524
|
+
location_str,
|
|
525
|
+
self.cloud_options,
|
|
526
|
+
iid_count=self.iid_count,
|
|
527
|
+
sid_count=self.sid_count,
|
|
528
|
+
is_a1_counted=self.count_A1,
|
|
529
|
+
iid_index=iid_index,
|
|
530
|
+
sid_index=sid_index,
|
|
531
|
+
val=val,
|
|
532
|
+
num_threads=num_threads,
|
|
533
|
+
)
|
|
534
|
+
else:
|
|
535
|
+
reader(
|
|
536
|
+
location_str,
|
|
537
|
+
self.cloud_options,
|
|
538
|
+
iid_count=self.iid_count,
|
|
539
|
+
sid_count=self.sid_count,
|
|
540
|
+
is_a1_counted=self.count_A1,
|
|
541
|
+
iid_index=iid_index,
|
|
542
|
+
sid_index=sid_index,
|
|
543
|
+
val=val,
|
|
544
|
+
num_threads=num_threads,
|
|
545
|
+
max_concurrent_requests=max_concurrent_requests,
|
|
546
|
+
max_chunk_bytes=max_chunk_bytes,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
else:
|
|
550
|
+
|
|
551
|
+
if not self.count_A1:
|
|
552
|
+
byteZero = 0
|
|
553
|
+
byteThree = 2
|
|
554
|
+
else:
|
|
555
|
+
byteZero = 2
|
|
556
|
+
byteThree = 0
|
|
557
|
+
missing = -127 if dtype == np.int8 else np.nan
|
|
558
|
+
# An earlier version of this code had a way to read consecutive SNPs of code
|
|
559
|
+
# in one read. May want
|
|
560
|
+
# to add that ability back to the code.
|
|
561
|
+
# Also, note that reading with python will often result in
|
|
562
|
+
# non-contiguous memory
|
|
563
|
+
# logging.warn("using pure python plink parser (might be much slower!!)")
|
|
564
|
+
|
|
565
|
+
if self.major == "SNP":
|
|
566
|
+
minor_count = self.iid_count
|
|
567
|
+
minor_index = iid_index
|
|
568
|
+
major_index = sid_index
|
|
569
|
+
else:
|
|
570
|
+
minor_count = self.sid_count
|
|
571
|
+
minor_index = sid_index
|
|
572
|
+
major_index = iid_index
|
|
573
|
+
|
|
574
|
+
val = np.zeros(
|
|
575
|
+
((int(np.ceil(0.25 * minor_count)) * 4), len(major_index)),
|
|
576
|
+
order=order,
|
|
577
|
+
dtype=dtype,
|
|
578
|
+
) # allocate it a little big
|
|
579
|
+
|
|
580
|
+
nbyte = int(np.ceil(0.25 * minor_count))
|
|
581
|
+
with open(self.location, "rb") as filepointer:
|
|
582
|
+
for major_index_value, major_index_index in enumerate(major_index):
|
|
583
|
+
startbit = int(np.ceil(0.25 * minor_count) * major_index_index + 3)
|
|
584
|
+
filepointer.seek(startbit)
|
|
585
|
+
bytes = np.array(bytearray(filepointer.read(nbyte))).reshape(
|
|
586
|
+
(int(np.ceil(0.25 * minor_count)), 1), order="F",
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
val[3::4, major_index_value : major_index_value + 1] = byteZero
|
|
590
|
+
val[3::4, major_index_value : major_index_value + 1][
|
|
591
|
+
bytes >= 64
|
|
592
|
+
] = missing
|
|
593
|
+
val[3::4, major_index_value : major_index_value + 1][
|
|
594
|
+
bytes >= 128
|
|
595
|
+
] = 1
|
|
596
|
+
val[3::4, major_index_value : major_index_value + 1][
|
|
597
|
+
bytes >= 192
|
|
598
|
+
] = byteThree
|
|
599
|
+
bytes = np.mod(bytes, 64)
|
|
600
|
+
val[2::4, major_index_value : major_index_value + 1] = byteZero
|
|
601
|
+
val[2::4, major_index_value : major_index_value + 1][
|
|
602
|
+
bytes >= 16
|
|
603
|
+
] = missing
|
|
604
|
+
val[2::4, major_index_value : major_index_value + 1][
|
|
605
|
+
bytes >= 32
|
|
606
|
+
] = 1
|
|
607
|
+
val[2::4, major_index_value : major_index_value + 1][
|
|
608
|
+
bytes >= 48
|
|
609
|
+
] = byteThree
|
|
610
|
+
bytes = np.mod(bytes, 16)
|
|
611
|
+
val[1::4, major_index_value : major_index_value + 1] = byteZero
|
|
612
|
+
val[1::4, major_index_value : major_index_value + 1][
|
|
613
|
+
bytes >= 4
|
|
614
|
+
] = missing
|
|
615
|
+
val[1::4, major_index_value : major_index_value + 1][bytes >= 8] = 1
|
|
616
|
+
val[1::4, major_index_value : major_index_value + 1][
|
|
617
|
+
bytes >= 12
|
|
618
|
+
] = byteThree
|
|
619
|
+
bytes = np.mod(bytes, 4)
|
|
620
|
+
val[0::4, major_index_value : major_index_value + 1] = byteZero
|
|
621
|
+
val[0::4, major_index_value : major_index_value + 1][
|
|
622
|
+
bytes >= 1
|
|
623
|
+
] = missing
|
|
624
|
+
val[0::4, major_index_value : major_index_value + 1][bytes >= 2] = 1
|
|
625
|
+
val[0::4, major_index_value : major_index_value + 1][
|
|
626
|
+
bytes >= 3
|
|
627
|
+
] = byteThree
|
|
628
|
+
val = val[minor_index, :] # reorder or trim any extra allocation
|
|
629
|
+
assert val.dtype == np.dtype(dtype) # real assert
|
|
630
|
+
if not open_bed._array_properties_are_ok(val, order):
|
|
631
|
+
val = val.copy(order=order)
|
|
632
|
+
# if in force python mode, and individual-major mode, then we need to transpose
|
|
633
|
+
if self.major == "individual":
|
|
634
|
+
val = val.T
|
|
635
|
+
return val
|
|
636
|
+
|
|
637
|
+
def _pick_reader(self, dtype):
|
|
638
|
+
if dtype == np.int8:
|
|
639
|
+
file_reader = read_i8
|
|
640
|
+
cloud_reader = read_cloud_i8
|
|
641
|
+
elif dtype == np.float64:
|
|
642
|
+
file_reader = read_f64
|
|
643
|
+
cloud_reader = read_cloud_f64
|
|
644
|
+
elif dtype == np.float32:
|
|
645
|
+
file_reader = read_f32
|
|
646
|
+
cloud_reader = read_cloud_f32
|
|
647
|
+
else:
|
|
648
|
+
raise ValueError(
|
|
649
|
+
f"dtype '{dtype}' not known, only "
|
|
650
|
+
+ "'int8', 'float32', and 'float64' are allowed.",
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
if open_bed._is_url(self.location):
|
|
654
|
+
reader = cloud_reader
|
|
655
|
+
location_str = self.location.geturl()
|
|
656
|
+
is_cloud = True
|
|
657
|
+
else:
|
|
658
|
+
reader = file_reader
|
|
659
|
+
location_str = str(self.location.as_posix())
|
|
660
|
+
is_cloud = False
|
|
661
|
+
return reader, location_str, is_cloud
|
|
662
|
+
|
|
663
|
+
def __str__(self) -> str:
|
|
664
|
+
return f"{self.__class__.__name__}('{self.location}',...)"
|
|
665
|
+
|
|
666
|
+
@property
|
|
667
|
+
def major(self) -> str:
|
|
668
|
+
"""Major mode of a local .bed file.
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
-------
|
|
672
|
+
str
|
|
673
|
+
'SNP' or 'individual'
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
Almost all PLINK 1.9 .bed files are 'SNP' major. This makes
|
|
677
|
+
reading the data by SNP(s) fast.
|
|
678
|
+
|
|
679
|
+
Errors
|
|
680
|
+
------
|
|
681
|
+
ValueError
|
|
682
|
+
If the file is a cloud file.
|
|
683
|
+
|
|
684
|
+
Example:
|
|
685
|
+
-------
|
|
686
|
+
|
|
687
|
+
.. doctest::
|
|
688
|
+
|
|
689
|
+
>>> from bed_reader import open_bed, sample_file
|
|
690
|
+
>>>
|
|
691
|
+
>>> file_name = sample_file("small.bed")
|
|
692
|
+
>>> with open_bed(file_name) as bed:
|
|
693
|
+
... print(bed.major)
|
|
694
|
+
SNP
|
|
695
|
+
|
|
696
|
+
"""
|
|
697
|
+
if self._is_url(self.location):
|
|
698
|
+
msg = "Cannot determine major mode for cloud files"
|
|
699
|
+
raise ValueError(msg)
|
|
700
|
+
# if self._mode is not set, set it
|
|
701
|
+
if not hasattr(self, "mode"):
|
|
702
|
+
with open(self.location, "rb") as filepointer:
|
|
703
|
+
self._mode = self._check_file(filepointer)
|
|
704
|
+
|
|
705
|
+
return "individual" if self._mode == b"\x00" else "SNP"
|
|
706
|
+
|
|
707
|
+
@property
|
|
708
|
+
def fid(self) -> np.ndarray:
|
|
709
|
+
"""Family id of each individual (sample).
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
-------
|
|
713
|
+
numpy.ndarray
|
|
714
|
+
array of str
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
'0' represents a missing value.
|
|
718
|
+
|
|
719
|
+
If needed, will cause a one-time read of the .fam file.
|
|
720
|
+
|
|
721
|
+
Example:
|
|
722
|
+
-------
|
|
723
|
+
|
|
724
|
+
.. doctest::
|
|
725
|
+
|
|
726
|
+
>>> from bed_reader import open_bed, sample_file
|
|
727
|
+
>>>
|
|
728
|
+
>>> file_name = sample_file("small.bed")
|
|
729
|
+
>>> with open_bed(file_name) as bed:
|
|
730
|
+
... print(bed.fid)
|
|
731
|
+
['fid1' 'fid1' 'fid2']
|
|
732
|
+
|
|
733
|
+
"""
|
|
734
|
+
return self.property_item("fid")
|
|
735
|
+
|
|
736
|
+
@property
|
|
737
|
+
def iid(self) -> np.ndarray:
|
|
738
|
+
"""Individual id of each individual (sample).
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
-------
|
|
742
|
+
numpy.ndarray
|
|
743
|
+
array of str
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
If needed, will cause a one-time read of the .fam file.
|
|
747
|
+
|
|
748
|
+
Example:
|
|
749
|
+
-------
|
|
750
|
+
|
|
751
|
+
.. doctest::
|
|
752
|
+
|
|
753
|
+
>>> from bed_reader import open_bed, sample_file
|
|
754
|
+
>>>
|
|
755
|
+
>>> file_name = sample_file("small.bed")
|
|
756
|
+
>>> with open_bed(file_name) as bed:
|
|
757
|
+
... print(bed.iid)
|
|
758
|
+
['iid1' 'iid2' 'iid3']
|
|
759
|
+
|
|
760
|
+
"""
|
|
761
|
+
return self.property_item("iid")
|
|
762
|
+
|
|
763
|
+
@property
|
|
764
|
+
def father(self) -> np.ndarray:
|
|
765
|
+
"""Father id of each individual (sample).
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
-------
|
|
769
|
+
numpy.ndarray
|
|
770
|
+
array of str
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
'0' represents a missing value.
|
|
774
|
+
|
|
775
|
+
If needed, will cause a one-time read of the .fam file.
|
|
776
|
+
|
|
777
|
+
Example:
|
|
778
|
+
-------
|
|
779
|
+
|
|
780
|
+
.. doctest::
|
|
781
|
+
|
|
782
|
+
>>> from bed_reader import open_bed, sample_file
|
|
783
|
+
>>>
|
|
784
|
+
>>> file_name = sample_file("small.bed")
|
|
785
|
+
>>> with open_bed(file_name) as bed:
|
|
786
|
+
... print(bed.father)
|
|
787
|
+
['iid23' 'iid23' 'iid22']
|
|
788
|
+
|
|
789
|
+
"""
|
|
790
|
+
return self.property_item("father")
|
|
791
|
+
|
|
792
|
+
@property
|
|
793
|
+
def mother(self) -> np.ndarray:
|
|
794
|
+
"""Mother id of each individual (sample).
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
-------
|
|
798
|
+
numpy.ndarray
|
|
799
|
+
array of str
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
'0' represents a missing value.
|
|
803
|
+
|
|
804
|
+
If needed, will cause a one-time read of the .fam file.
|
|
805
|
+
|
|
806
|
+
Example:
|
|
807
|
+
-------
|
|
808
|
+
|
|
809
|
+
.. doctest::
|
|
810
|
+
|
|
811
|
+
>>> from bed_reader import open_bed, sample_file
|
|
812
|
+
>>>
|
|
813
|
+
>>> file_name = sample_file("small.bed")
|
|
814
|
+
>>> with open_bed(file_name) as bed:
|
|
815
|
+
... print(bed.mother)
|
|
816
|
+
['iid34' 'iid34' 'iid33']
|
|
817
|
+
|
|
818
|
+
"""
|
|
819
|
+
return self.property_item("mother")
|
|
820
|
+
|
|
821
|
+
@property
|
|
822
|
+
def sex(self) -> np.ndarray:
|
|
823
|
+
"""Sex of each individual (sample).
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
-------
|
|
827
|
+
numpy.ndarray
|
|
828
|
+
array of 0, 1, or 2
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
0 is unknown, 1 is male, 2 is female
|
|
832
|
+
|
|
833
|
+
If needed, will cause a one-time read of the .fam file.
|
|
834
|
+
|
|
835
|
+
Example:
|
|
836
|
+
-------
|
|
837
|
+
.. doctest::
|
|
838
|
+
|
|
839
|
+
>>> from bed_reader import open_bed, sample_file
|
|
840
|
+
>>>
|
|
841
|
+
>>> file_name = sample_file("small.bed")
|
|
842
|
+
>>> with open_bed(file_name) as bed:
|
|
843
|
+
... print(bed.sex)
|
|
844
|
+
[1 2 0]
|
|
845
|
+
|
|
846
|
+
"""
|
|
847
|
+
return self.property_item("sex")
|
|
848
|
+
|
|
849
|
+
@property
|
|
850
|
+
def pheno(self) -> np.ndarray:
|
|
851
|
+
"""A phenotype for each individual (sample)
|
|
852
|
+
(seldom used).
|
|
853
|
+
|
|
854
|
+
Returns:
|
|
855
|
+
-------
|
|
856
|
+
numpy.ndarray
|
|
857
|
+
array of str
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
'0' may represent a missing value.
|
|
861
|
+
|
|
862
|
+
If needed, will cause a one-time read of the .fam file.
|
|
863
|
+
|
|
864
|
+
Example:
|
|
865
|
+
-------
|
|
866
|
+
|
|
867
|
+
.. doctest::
|
|
868
|
+
|
|
869
|
+
>>> from bed_reader import open_bed, sample_file
|
|
870
|
+
>>>
|
|
871
|
+
>>> file_name = sample_file("small.bed")
|
|
872
|
+
>>> with open_bed(file_name) as bed:
|
|
873
|
+
... print(bed.pheno)
|
|
874
|
+
['red' 'red' 'blue']
|
|
875
|
+
|
|
876
|
+
"""
|
|
877
|
+
return self.property_item("pheno")
|
|
878
|
+
|
|
879
|
+
@property
|
|
880
|
+
def properties(self) -> Mapping[str, np.array]:
|
|
881
|
+
"""All the properties returned as a dictionary.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
-------
|
|
885
|
+
dict
|
|
886
|
+
all the properties
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
The keys of the dictionary are the names of the properties, namely:
|
|
890
|
+
|
|
891
|
+
"fid" (family id), "iid" (individual or sample id), "father" (father id),
|
|
892
|
+
"mother" (mother id), "sex", "pheno" (phenotype), "chromosome", "sid"
|
|
893
|
+
(SNP or variant id), "cm_position" (centimorgan position), "bp_position"
|
|
894
|
+
(base-pair position), "allele_1", "allele_2".
|
|
895
|
+
|
|
896
|
+
The values are :class:`numpy.ndarray`.
|
|
897
|
+
|
|
898
|
+
If needed, will cause a one-time read of the .fam and .bim file.
|
|
899
|
+
|
|
900
|
+
Example:
|
|
901
|
+
-------
|
|
902
|
+
|
|
903
|
+
.. doctest::
|
|
904
|
+
|
|
905
|
+
>>> from bed_reader import open_bed, sample_file
|
|
906
|
+
>>>
|
|
907
|
+
>>> file_name = sample_file("small.bed")
|
|
908
|
+
>>> with open_bed(file_name) as bed:
|
|
909
|
+
... print(len(bed.properties)) #length of dict
|
|
910
|
+
12
|
|
911
|
+
|
|
912
|
+
"""
|
|
913
|
+
for key in _meta_meta:
|
|
914
|
+
self.property_item(key)
|
|
915
|
+
return self.properties_dict
|
|
916
|
+
|
|
917
|
+
def property_item(self, name: str) -> np.ndarray:
|
|
918
|
+
"""Retrieve one property by name.
|
|
919
|
+
|
|
920
|
+
Returns:
|
|
921
|
+
-------
|
|
922
|
+
numpy.ndarray
|
|
923
|
+
a property value
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
The name is one of these:
|
|
927
|
+
|
|
928
|
+
"fid" (family id), "iid" (individual or sample id), "father" (father id),
|
|
929
|
+
"mother" (mother id), "sex", "pheno" (phenotype), "chromosome", "sid"
|
|
930
|
+
(SNP or variant id), "cm_position" (centimorgan position), "bp_position"
|
|
931
|
+
(base-pair position), "allele_1", "allele_2".
|
|
932
|
+
|
|
933
|
+
If needed, will cause a one-time read of the .fam or .bim file.
|
|
934
|
+
|
|
935
|
+
Example:
|
|
936
|
+
-------
|
|
937
|
+
|
|
938
|
+
.. doctest::
|
|
939
|
+
|
|
940
|
+
>>> from bed_reader import open_bed, sample_file
|
|
941
|
+
>>>
|
|
942
|
+
>>> file_name = sample_file("small.bed")
|
|
943
|
+
>>> with open_bed(file_name) as bed:
|
|
944
|
+
... print(bed.property_item('chromosome'))
|
|
945
|
+
['1' '1' '5' 'Y']
|
|
946
|
+
|
|
947
|
+
"""
|
|
948
|
+
if name not in self.properties_dict:
|
|
949
|
+
mm = _meta_meta[name]
|
|
950
|
+
self._read_fam_or_bim(suffix=mm.suffix)
|
|
951
|
+
return self.properties_dict[name]
|
|
952
|
+
|
|
953
|
+
@property
|
|
954
|
+
def chromosome(self) -> np.ndarray:
|
|
955
|
+
"""Chromosome of each SNP (variant).
|
|
956
|
+
|
|
957
|
+
Returns:
|
|
958
|
+
-------
|
|
959
|
+
numpy.ndarray
|
|
960
|
+
array of str
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
'0' represents a missing value.
|
|
964
|
+
|
|
965
|
+
If needed, will cause a one-time read of the .bim file.
|
|
966
|
+
|
|
967
|
+
Example:
|
|
968
|
+
-------
|
|
969
|
+
|
|
970
|
+
.. doctest::
|
|
971
|
+
|
|
972
|
+
>>> from bed_reader import open_bed, sample_file
|
|
973
|
+
>>>
|
|
974
|
+
>>> file_name = sample_file("small.bed")
|
|
975
|
+
>>> with open_bed(file_name) as bed:
|
|
976
|
+
... print(bed.chromosome)
|
|
977
|
+
['1' '1' '5' 'Y']
|
|
978
|
+
|
|
979
|
+
"""
|
|
980
|
+
return self.property_item("chromosome")
|
|
981
|
+
|
|
982
|
+
@property
|
|
983
|
+
def sid(self) -> np.ndarray:
|
|
984
|
+
"""SNP id of each SNP (variant).
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
-------
|
|
988
|
+
numpy.ndarray
|
|
989
|
+
array of str
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
If needed, will cause a one-time read of the .bim file.
|
|
993
|
+
|
|
994
|
+
Example:
|
|
995
|
+
-------
|
|
996
|
+
|
|
997
|
+
.. doctest::
|
|
998
|
+
|
|
999
|
+
>>> from bed_reader import open_bed, sample_file
|
|
1000
|
+
>>>
|
|
1001
|
+
>>> file_name = sample_file("small.bed")
|
|
1002
|
+
>>> with open_bed(file_name) as bed:
|
|
1003
|
+
... print(bed.sid)
|
|
1004
|
+
['sid1' 'sid2' 'sid3' 'sid4']
|
|
1005
|
+
|
|
1006
|
+
"""
|
|
1007
|
+
return self.property_item("sid")
|
|
1008
|
+
|
|
1009
|
+
@property
|
|
1010
|
+
def cm_position(self) -> np.ndarray:
|
|
1011
|
+
"""Centimorgan position of each SNP (variant).
|
|
1012
|
+
|
|
1013
|
+
Returns:
|
|
1014
|
+
-------
|
|
1015
|
+
numpy.ndarray
|
|
1016
|
+
array of float
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
0.0 represents a missing value.
|
|
1020
|
+
|
|
1021
|
+
If needed, will cause a one-time read of the .bim file.
|
|
1022
|
+
|
|
1023
|
+
Example:
|
|
1024
|
+
-------
|
|
1025
|
+
|
|
1026
|
+
.. doctest::
|
|
1027
|
+
|
|
1028
|
+
>>> from bed_reader import open_bed, sample_file
|
|
1029
|
+
>>>
|
|
1030
|
+
>>> file_name = sample_file("small.bed")
|
|
1031
|
+
>>> with open_bed(file_name) as bed:
|
|
1032
|
+
... print(bed.cm_position)
|
|
1033
|
+
[ 100.4 2000.5 4000.7 7000.9]
|
|
1034
|
+
|
|
1035
|
+
"""
|
|
1036
|
+
return self.property_item("cm_position")
|
|
1037
|
+
|
|
1038
|
+
@property
|
|
1039
|
+
def bp_position(self) -> np.ndarray:
|
|
1040
|
+
"""Base-pair position of each SNP (variant).
|
|
1041
|
+
|
|
1042
|
+
Returns:
|
|
1043
|
+
-------
|
|
1044
|
+
numpy.ndarray
|
|
1045
|
+
array of int
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
0 represents a missing value.
|
|
1049
|
+
|
|
1050
|
+
If needed, will cause a one-time read of the .bim file.
|
|
1051
|
+
|
|
1052
|
+
Example:
|
|
1053
|
+
-------
|
|
1054
|
+
|
|
1055
|
+
.. doctest::
|
|
1056
|
+
|
|
1057
|
+
>>> from bed_reader import open_bed, sample_file
|
|
1058
|
+
>>>
|
|
1059
|
+
>>> file_name = sample_file("small.bed")
|
|
1060
|
+
>>> with open_bed(file_name) as bed:
|
|
1061
|
+
... print(bed.bp_position)
|
|
1062
|
+
[ 1 100 1000 1004]
|
|
1063
|
+
|
|
1064
|
+
"""
|
|
1065
|
+
return self.property_item("bp_position")
|
|
1066
|
+
|
|
1067
|
+
@property
|
|
1068
|
+
def allele_1(self) -> np.ndarray:
|
|
1069
|
+
"""First allele of each SNP (variant).
|
|
1070
|
+
|
|
1071
|
+
Returns:
|
|
1072
|
+
-------
|
|
1073
|
+
numpy.ndarray
|
|
1074
|
+
array of str
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
If needed, will cause a one-time read of the .bim file.
|
|
1078
|
+
|
|
1079
|
+
Example:
|
|
1080
|
+
-------
|
|
1081
|
+
|
|
1082
|
+
.. doctest::
|
|
1083
|
+
|
|
1084
|
+
>>> from bed_reader import open_bed, sample_file
|
|
1085
|
+
>>>
|
|
1086
|
+
>>> file_name = sample_file("small.bed")
|
|
1087
|
+
>>> with open_bed(file_name) as bed:
|
|
1088
|
+
... print(bed.allele_1)
|
|
1089
|
+
['A' 'T' 'A' 'T']
|
|
1090
|
+
|
|
1091
|
+
"""
|
|
1092
|
+
return self.property_item("allele_1")
|
|
1093
|
+
|
|
1094
|
+
@property
|
|
1095
|
+
def allele_2(self) -> np.ndarray:
|
|
1096
|
+
"""Second allele of each SNP (variant),.
|
|
1097
|
+
|
|
1098
|
+
Returns:
|
|
1099
|
+
-------
|
|
1100
|
+
numpy.ndarray
|
|
1101
|
+
array of str
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
If needed, will cause a one-time read of the .bim file.
|
|
1105
|
+
|
|
1106
|
+
Example:
|
|
1107
|
+
-------
|
|
1108
|
+
|
|
1109
|
+
.. doctest::
|
|
1110
|
+
|
|
1111
|
+
>>> from bed_reader import open_bed, sample_file
|
|
1112
|
+
>>>
|
|
1113
|
+
>>> file_name = sample_file("small.bed")
|
|
1114
|
+
>>> with open_bed(file_name) as bed:
|
|
1115
|
+
... print(bed.allele_2)
|
|
1116
|
+
['A' 'C' 'C' 'G']
|
|
1117
|
+
|
|
1118
|
+
"""
|
|
1119
|
+
return self.property_item("allele_2")
|
|
1120
|
+
|
|
1121
|
+
@property
|
|
1122
|
+
def iid_count(self) -> np.ndarray:
|
|
1123
|
+
"""Number of individuals (samples).
|
|
1124
|
+
|
|
1125
|
+
Returns:
|
|
1126
|
+
-------
|
|
1127
|
+
int
|
|
1128
|
+
number of individuals
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
If needed, will cause a fast line-count of the .fam file.
|
|
1132
|
+
|
|
1133
|
+
Example:
|
|
1134
|
+
-------
|
|
1135
|
+
|
|
1136
|
+
.. doctest::
|
|
1137
|
+
|
|
1138
|
+
>>> from bed_reader import open_bed, sample_file
|
|
1139
|
+
>>>
|
|
1140
|
+
>>> file_name = sample_file("small.bed")
|
|
1141
|
+
>>> with open_bed(file_name) as bed:
|
|
1142
|
+
... print(bed.iid_count)
|
|
1143
|
+
3
|
|
1144
|
+
|
|
1145
|
+
"""
|
|
1146
|
+
return self._count("fam")
|
|
1147
|
+
|
|
1148
|
+
@property
|
|
1149
|
+
def sid_count(self) -> np.ndarray:
|
|
1150
|
+
"""Number of SNPs (variants).
|
|
1151
|
+
|
|
1152
|
+
Returns:
|
|
1153
|
+
-------
|
|
1154
|
+
int
|
|
1155
|
+
number of SNPs
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
If needed, will cause a fast line-count of the .bim file.
|
|
1159
|
+
|
|
1160
|
+
Example:
|
|
1161
|
+
-------
|
|
1162
|
+
|
|
1163
|
+
.. doctest::
|
|
1164
|
+
|
|
1165
|
+
>>> from bed_reader import open_bed, sample_file
|
|
1166
|
+
>>>
|
|
1167
|
+
>>> file_name = sample_file("small.bed")
|
|
1168
|
+
>>> with open_bed(file_name) as bed:
|
|
1169
|
+
... print(bed.sid_count)
|
|
1170
|
+
4
|
|
1171
|
+
|
|
1172
|
+
"""
|
|
1173
|
+
return self._count("bim")
|
|
1174
|
+
|
|
1175
|
+
def _property_location(self, suffix):
|
|
1176
|
+
if suffix == "fam":
|
|
1177
|
+
return self._fam_location
|
|
1178
|
+
assert suffix == "bim" # real assert
|
|
1179
|
+
return self._bim_location
|
|
1180
|
+
|
|
1181
|
+
def _count(self, suffix):
|
|
1182
|
+
count = self._counts[suffix]
|
|
1183
|
+
if count is None:
|
|
1184
|
+
location = self._property_location(suffix)
|
|
1185
|
+
if open_bed._is_url(location):
|
|
1186
|
+
# should not download twice from cloud
|
|
1187
|
+
if suffix == "fam":
|
|
1188
|
+
if self.property_item("iid") is None:
|
|
1189
|
+
# ... unless user doesn't want iid
|
|
1190
|
+
file_bytes = bytes(
|
|
1191
|
+
url_to_bytes(location.geturl(), self.cloud_options),
|
|
1192
|
+
)
|
|
1193
|
+
count = _rawincount(BytesIO(file_bytes))
|
|
1194
|
+
else:
|
|
1195
|
+
count = len(self.iid)
|
|
1196
|
+
elif suffix == "bim":
|
|
1197
|
+
if self.property_item("sid") is None:
|
|
1198
|
+
# ... unless user doesn't want sid
|
|
1199
|
+
file_bytes = bytes(
|
|
1200
|
+
url_to_bytes(location.geturl(), self.cloud_options),
|
|
1201
|
+
)
|
|
1202
|
+
count = _rawincount(BytesIO(file_bytes))
|
|
1203
|
+
else:
|
|
1204
|
+
count = len(self.sid)
|
|
1205
|
+
else:
|
|
1206
|
+
msg = "real assert"
|
|
1207
|
+
raise ValueError(msg)
|
|
1208
|
+
else:
|
|
1209
|
+
with open(location, "rb") as f:
|
|
1210
|
+
count = _rawincount(f)
|
|
1211
|
+
self._counts[suffix] = count
|
|
1212
|
+
return count
|
|
1213
|
+
|
|
1214
|
+
@staticmethod
|
|
1215
|
+
def _check_file(filepointer):
|
|
1216
|
+
magic_number = filepointer.read(2)
|
|
1217
|
+
if magic_number != b"l\x1b":
|
|
1218
|
+
msg = "Not a valid .bed file"
|
|
1219
|
+
raise ValueError(msg)
|
|
1220
|
+
mode = filepointer.read(1)
|
|
1221
|
+
# Check if mode is either individual-major or SNP-major
|
|
1222
|
+
if mode not in (b"\x00", b"\x01"):
|
|
1223
|
+
msg = "Not a valid .bed file"
|
|
1224
|
+
raise ValueError(msg)
|
|
1225
|
+
return mode
|
|
1226
|
+
|
|
1227
|
+
def __del__(self) -> None:
|
|
1228
|
+
self.__exit__()
|
|
1229
|
+
|
|
1230
|
+
def __enter__(self):
|
|
1231
|
+
return self
|
|
1232
|
+
|
|
1233
|
+
def __exit__(self, *_):
|
|
1234
|
+
pass
|
|
1235
|
+
|
|
1236
|
+
@staticmethod
|
|
1237
|
+
def _array_properties_are_ok(val, order):
|
|
1238
|
+
if order == "F":
|
|
1239
|
+
return val.flags["F_CONTIGUOUS"]
|
|
1240
|
+
assert order == "C" # real assert
|
|
1241
|
+
return val.flags["C_CONTIGUOUS"]
|
|
1242
|
+
|
|
1243
|
+
@property
|
|
1244
|
+
def shape(self):
|
|
1245
|
+
"""Number of individuals (samples) and SNPs (variants).
|
|
1246
|
+
|
|
1247
|
+
Returns:
|
|
1248
|
+
-------
|
|
1249
|
+
(int, int)
|
|
1250
|
+
number of individuals, number of SNPs
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
If needed, will cause a fast line-count of the .fam and .bim files.
|
|
1254
|
+
|
|
1255
|
+
Example:
|
|
1256
|
+
-------
|
|
1257
|
+
|
|
1258
|
+
.. doctest::
|
|
1259
|
+
|
|
1260
|
+
>>> from bed_reader import open_bed, sample_file
|
|
1261
|
+
>>>
|
|
1262
|
+
>>> file_name = sample_file("small.bed")
|
|
1263
|
+
>>> with open_bed(file_name) as bed:
|
|
1264
|
+
... print(bed.shape)
|
|
1265
|
+
(3, 4)
|
|
1266
|
+
|
|
1267
|
+
"""
|
|
1268
|
+
return (self.iid_count, self.sid_count)
|
|
1269
|
+
|
|
1270
|
+
@staticmethod
|
|
1271
|
+
def _split_index(index):
|
|
1272
|
+
if not isinstance(index, tuple):
|
|
1273
|
+
index = (None, index)
|
|
1274
|
+
iid_index = open_bed._fix_up_index(index[0])
|
|
1275
|
+
sid_index = open_bed._fix_up_index(index[1])
|
|
1276
|
+
return iid_index, sid_index
|
|
1277
|
+
|
|
1278
|
+
@staticmethod
|
|
1279
|
+
def _fix_up_index(index):
|
|
1280
|
+
if index is None: # make a shortcut for None
|
|
1281
|
+
return slice(None)
|
|
1282
|
+
try: # If index is an int, return it in an array
|
|
1283
|
+
index = index.__index__() # (see
|
|
1284
|
+
# https://stackoverflow.com/questions/3501382/checking-whether-a-variable-is-an-integer-or-not)
|
|
1285
|
+
return [index]
|
|
1286
|
+
except Exception:
|
|
1287
|
+
pass
|
|
1288
|
+
return index
|
|
1289
|
+
|
|
1290
|
+
@staticmethod
|
|
1291
|
+
def _write_fam_or_bim(base_filepath, properties, suffix, property_filepath) -> None:
|
|
1292
|
+
assert suffix in {"fam", "bim"}, "real assert"
|
|
1293
|
+
|
|
1294
|
+
filepath = (
|
|
1295
|
+
Path(property_filepath)
|
|
1296
|
+
if property_filepath is not None
|
|
1297
|
+
else base_filepath.parent / (base_filepath.stem + "." + suffix)
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
fam_bim_list = []
|
|
1301
|
+
for key, mm in _meta_meta.items():
|
|
1302
|
+
if mm.suffix == suffix:
|
|
1303
|
+
assert len(fam_bim_list) == mm.column, "real assert"
|
|
1304
|
+
fam_bim_list.append(properties[key])
|
|
1305
|
+
|
|
1306
|
+
sep = " " if suffix == "fam" else "\t"
|
|
1307
|
+
|
|
1308
|
+
with open(filepath, "w") as filepointer:
|
|
1309
|
+
for index in range(len(fam_bim_list[0])):
|
|
1310
|
+
filepointer.write(
|
|
1311
|
+
sep.join(str(seq[index]) for seq in fam_bim_list) + "\n",
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
@staticmethod
|
|
1315
|
+
def _fix_up_properties_array(input, dtype, missing_value, key):
|
|
1316
|
+
if input is None:
|
|
1317
|
+
return None
|
|
1318
|
+
if len(input) == 0:
|
|
1319
|
+
return np.zeros([0], dtype=dtype)
|
|
1320
|
+
|
|
1321
|
+
if not isinstance(input, np.ndarray):
|
|
1322
|
+
return open_bed._fix_up_properties_array(
|
|
1323
|
+
np.array(input), dtype, missing_value, key,
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
if len(input.shape) != 1:
|
|
1327
|
+
msg = f"{key} should be one dimensional"
|
|
1328
|
+
raise ValueError(msg)
|
|
1329
|
+
|
|
1330
|
+
do_missing_values = True
|
|
1331
|
+
if np.issubdtype(input.dtype, np.floating) and np.issubdtype(dtype, int):
|
|
1332
|
+
input[input != input] = missing_value
|
|
1333
|
+
old_settings = np.seterr(invalid="warn")
|
|
1334
|
+
try:
|
|
1335
|
+
output = np.array(input, dtype=dtype)
|
|
1336
|
+
finally:
|
|
1337
|
+
np.seterr(**old_settings)
|
|
1338
|
+
elif not np.issubdtype(input.dtype, dtype):
|
|
1339
|
+
# This will convert, for example, numerical sids to string sids or
|
|
1340
|
+
# floats that happen to be integers into ints,
|
|
1341
|
+
# but there will be a warning generated.
|
|
1342
|
+
old_settings = np.seterr(invalid="warn")
|
|
1343
|
+
try:
|
|
1344
|
+
output = np.array(input, dtype=dtype)
|
|
1345
|
+
finally:
|
|
1346
|
+
np.seterr(**old_settings)
|
|
1347
|
+
else:
|
|
1348
|
+
output = input
|
|
1349
|
+
|
|
1350
|
+
# Change NaN in input to correct missing value
|
|
1351
|
+
if do_missing_values and np.issubdtype(input.dtype, np.floating):
|
|
1352
|
+
output[input != input] = missing_value
|
|
1353
|
+
|
|
1354
|
+
return output
|
|
1355
|
+
|
|
1356
|
+
@staticmethod
|
|
1357
|
+
def _fix_up_properties(properties, iid_count, sid_count, use_fill_sequence):
|
|
1358
|
+
for key in properties:
|
|
1359
|
+
if key not in _meta_meta:
|
|
1360
|
+
msg = f"properties key '{key}' not known"
|
|
1361
|
+
raise KeyError(msg)
|
|
1362
|
+
|
|
1363
|
+
count_dict = {"fam": iid_count, "bim": sid_count}
|
|
1364
|
+
properties_dict = {}
|
|
1365
|
+
for key, mm in _meta_meta.items():
|
|
1366
|
+
count = count_dict[mm.suffix]
|
|
1367
|
+
|
|
1368
|
+
if key not in properties or (use_fill_sequence and properties[key] is None):
|
|
1369
|
+
if use_fill_sequence:
|
|
1370
|
+
output = mm.fill_sequence(key, count, mm.missing_value, mm.dtype)
|
|
1371
|
+
else:
|
|
1372
|
+
continue # Test coverage reaches this, but doesn't report it.
|
|
1373
|
+
else:
|
|
1374
|
+
output = open_bed._fix_up_properties_array(
|
|
1375
|
+
properties[key], mm.dtype, mm.missing_value, key,
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
if output is not None:
|
|
1379
|
+
if count is None:
|
|
1380
|
+
count_dict[mm.suffix] = len(output)
|
|
1381
|
+
elif count != len(output):
|
|
1382
|
+
raise ValueError(
|
|
1383
|
+
f"The length of override {key}, {len(output)}, should not "
|
|
1384
|
+
+ "be different from the current "
|
|
1385
|
+
+ f"{_count_name[mm.suffix]}, {count}",
|
|
1386
|
+
)
|
|
1387
|
+
properties_dict[key] = output
|
|
1388
|
+
return properties_dict, count_dict
|
|
1389
|
+
|
|
1390
|
+
def _read_fam_or_bim(self, suffix) -> None:
|
|
1391
|
+
property_location = self._property_location(suffix)
|
|
1392
|
+
|
|
1393
|
+
logging.info(f"Loading {suffix} file {property_location}")
|
|
1394
|
+
|
|
1395
|
+
count = self._counts[suffix]
|
|
1396
|
+
|
|
1397
|
+
delimiter = _delimiters[suffix]
|
|
1398
|
+
if delimiter in {r"\s+"}:
|
|
1399
|
+
delimiter = None
|
|
1400
|
+
|
|
1401
|
+
usecolsdict = {}
|
|
1402
|
+
dtype_dict = {}
|
|
1403
|
+
for key, mm in _meta_meta.items():
|
|
1404
|
+
if mm.suffix is suffix and key not in self.properties_dict:
|
|
1405
|
+
usecolsdict[key] = mm.column
|
|
1406
|
+
dtype_dict[mm.column] = mm.dtype
|
|
1407
|
+
assert list(usecolsdict.values()) == sorted(usecolsdict.values()) # real assert
|
|
1408
|
+
assert len(usecolsdict) > 0 # real assert
|
|
1409
|
+
|
|
1410
|
+
if self._is_url(property_location):
|
|
1411
|
+
file_bytes = bytes(
|
|
1412
|
+
url_to_bytes(property_location.geturl(), self.cloud_options),
|
|
1413
|
+
)
|
|
1414
|
+
if len(file_bytes) == 0:
|
|
1415
|
+
columns, row_count = [], 0
|
|
1416
|
+
else: # note similar code below
|
|
1417
|
+
columns, row_count = _read_csv(
|
|
1418
|
+
BytesIO(file_bytes),
|
|
1419
|
+
delimiter=delimiter,
|
|
1420
|
+
dtype=dtype_dict,
|
|
1421
|
+
usecols=usecolsdict.values(),
|
|
1422
|
+
)
|
|
1423
|
+
elif os.path.getsize(property_location) == 0:
|
|
1424
|
+
columns, row_count = [], 0
|
|
1425
|
+
else:
|
|
1426
|
+
columns, row_count = _read_csv(
|
|
1427
|
+
property_location,
|
|
1428
|
+
delimiter=delimiter,
|
|
1429
|
+
dtype=dtype_dict,
|
|
1430
|
+
usecols=usecolsdict.values(),
|
|
1431
|
+
)
|
|
1432
|
+
|
|
1433
|
+
if count is None:
|
|
1434
|
+
self._counts[suffix] = row_count
|
|
1435
|
+
elif count != row_count:
|
|
1436
|
+
raise ValueError(
|
|
1437
|
+
f"The number of lines in the *.{suffix} file, {row_count}, "
|
|
1438
|
+
+ "should not be different from the current "
|
|
1439
|
+
+ "f{_count_name[suffix]}, {count}",
|
|
1440
|
+
)
|
|
1441
|
+
for i, key in enumerate(usecolsdict.keys()):
|
|
1442
|
+
mm = _meta_meta[key]
|
|
1443
|
+
if row_count == 0:
|
|
1444
|
+
output = np.array([], dtype=mm.dtype)
|
|
1445
|
+
else:
|
|
1446
|
+
output = columns[i]
|
|
1447
|
+
if not np.issubdtype(output.dtype, mm.dtype):
|
|
1448
|
+
output = np.array(output, dtype=mm.dtype)
|
|
1449
|
+
self.properties_dict[key] = output
|
|
1450
|
+
|
|
1451
|
+
def read_sparse(
|
|
1452
|
+
self,
|
|
1453
|
+
index: Optional[Any] = None,
|
|
1454
|
+
dtype: Optional[Union[type, str]] = "float32",
|
|
1455
|
+
batch_size: Optional[int] = None,
|
|
1456
|
+
format: Optional[str] = "csc",
|
|
1457
|
+
num_threads=None,
|
|
1458
|
+
max_concurrent_requests=None,
|
|
1459
|
+
max_chunk_bytes=None,
|
|
1460
|
+
) -> (Union[sparse.csc_matrix, sparse.csr_matrix]) if sparse is not None else None: # type: ignore
|
|
1461
|
+
"""Read genotype information into a :mod:`scipy.sparse` matrix. Sparse matrices
|
|
1462
|
+
may be useful when the data is mostly zeros.
|
|
1463
|
+
|
|
1464
|
+
.. note::
|
|
1465
|
+
This method requires :mod:`scipy`. Install `scipy` with:
|
|
1466
|
+
|
|
1467
|
+
.. code-block:: bash
|
|
1468
|
+
|
|
1469
|
+
pip install --upgrade bed-reader[sparse]
|
|
1470
|
+
|
|
1471
|
+
Parameters
|
|
1472
|
+
----------
|
|
1473
|
+
index:
|
|
1474
|
+
An optional expression specifying the individuals (samples) and SNPs
|
|
1475
|
+
(variants) to read. (See examples, below).
|
|
1476
|
+
Defaults to ``None``, meaning read all.
|
|
1477
|
+
|
|
1478
|
+
(If index is a tuple, the first component indexes the individuals and the
|
|
1479
|
+
second indexes
|
|
1480
|
+
the SNPs. If it is not a tuple and not None, it indexes SNPs.)
|
|
1481
|
+
|
|
1482
|
+
dtype: {'float32' (default), 'float64', 'int8'}, optional
|
|
1483
|
+
The desired data-type for the returned array.
|
|
1484
|
+
batch_size: None or int, optional
|
|
1485
|
+
Number of dense columns or rows to read at a time, internally.
|
|
1486
|
+
Defaults to round(sqrt(total-number-of-columns-or-rows-to-read)).
|
|
1487
|
+
format : {'csc','csr'}, optional
|
|
1488
|
+
The desired format of the sparse matrix.
|
|
1489
|
+
Defaults to ``csc`` (Compressed Sparse Column, which is SNP-major).
|
|
1490
|
+
num_threads: None or int, optional
|
|
1491
|
+
The number of threads with which to read data. Defaults to all available
|
|
1492
|
+
processors.
|
|
1493
|
+
Can also be set with :class:`open_bed` or these
|
|
1494
|
+
environment variables (listed in priority order):
|
|
1495
|
+
'PST_NUM_THREADS', 'NUM_THREADS', 'MKL_NUM_THREADS'.
|
|
1496
|
+
max_concurrent_requests: None or int, optional
|
|
1497
|
+
The maximum number of concurrent requests to make to the cloud storage
|
|
1498
|
+
service. Defaults to 10.
|
|
1499
|
+
max_chunk_bytes: None or int, optional
|
|
1500
|
+
The maximum number of bytes to read in a single request to the cloud
|
|
1501
|
+
storage service. Defaults to 8MB.
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
Returns
|
|
1505
|
+
-------
|
|
1506
|
+
a :class:`scipy.sparse.csc_matrix` (default) or :class:`scipy.sparse.csr_matrix`
|
|
1507
|
+
|
|
1508
|
+
Rows represent individuals (samples). Columns represent SNPs (variants).
|
|
1509
|
+
|
|
1510
|
+
For ``dtype`` 'float32' and 'float64', NaN indicates missing values.
|
|
1511
|
+
For 'int8', -127 indicates missing values.
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
The memory used by the final sparse matrix is approximately:
|
|
1515
|
+
|
|
1516
|
+
# of non-zero values * (4 bytes + 1 byte (for int8))
|
|
1517
|
+
|
|
1518
|
+
For example, consider reading 1000 individuals (samples) x 50,000 SNPs (variants)
|
|
1519
|
+
into csc format where the data is 97% sparse.
|
|
1520
|
+
The memory used will be about 7.5 MB (1000 x 50,000 x 3% x 5 bytes).
|
|
1521
|
+
This is 15% of the 50 MB needed by a dense matrix.
|
|
1522
|
+
|
|
1523
|
+
Internally, the function reads the data via small dense matrices.
|
|
1524
|
+
For this example, by default, the function will read 1000 individuals x 224 SNPs
|
|
1525
|
+
(because 224 * 224 is about 50,000).
|
|
1526
|
+
The memory used by the small dense matrix is 1000 x 244 x 1 byte (for int8) = 0.224 MB.
|
|
1527
|
+
|
|
1528
|
+
You can set `batch_size`. Larger values will be faster.
|
|
1529
|
+
Smaller values will use less memory.
|
|
1530
|
+
|
|
1531
|
+
For this example, we might want to set the `batch_size` to 5000. Then,
|
|
1532
|
+
the memory used by the small dense matrix
|
|
1533
|
+
would be 1000 x 5000 x 1 byte (for int8) = 5 MB,
|
|
1534
|
+
similar to the 7.5 MB needed for the final sparse matrix.
|
|
1535
|
+
|
|
1536
|
+
Examples
|
|
1537
|
+
--------
|
|
1538
|
+
Read all data in a .bed file into a :class:`scipy.sparse.csc_matrix`.
|
|
1539
|
+
The file has 10 individuals (samples) by 20 SNPs (variants).
|
|
1540
|
+
All but eight values are 0.
|
|
1541
|
+
|
|
1542
|
+
.. doctest::
|
|
1543
|
+
|
|
1544
|
+
>>> # pip install bed-reader[samples,sparse] # if needed
|
|
1545
|
+
>>> from bed_reader import open_bed, sample_file
|
|
1546
|
+
>>>
|
|
1547
|
+
>>> file_name = sample_file("sparse.bed")
|
|
1548
|
+
>>> with open_bed(file_name) as bed:
|
|
1549
|
+
... print(bed.shape) # doctest:+NORMALIZE_WHITESPACE +ELLIPSIS
|
|
1550
|
+
... val_sparse = bed.read_sparse(dtype="int8")
|
|
1551
|
+
(10, 20)
|
|
1552
|
+
>>> print("Nonzero Values", val_sparse.data) # doctest:+NORMALIZE_WHITESPACE +ELLIPSIS
|
|
1553
|
+
Nonzero Values [1 2 2 1 1 1 1 1]
|
|
1554
|
+
|
|
1555
|
+
To read selected individuals (samples) and/or SNPs (variants), set each part of
|
|
1556
|
+
a :data:`numpy.s_` to an `int`, a list of `int`, a slice expression, or
|
|
1557
|
+
a list of `bool`.
|
|
1558
|
+
Negative integers count from the end of the list.
|
|
1559
|
+
|
|
1560
|
+
.. doctest::
|
|
1561
|
+
|
|
1562
|
+
>>> import numpy as np
|
|
1563
|
+
>>> bed = open_bed(file_name)
|
|
1564
|
+
>>> print("Nonzero Values", bed.read_sparse(np.s_[:,5], dtype="int8").data) # read the SNPs indexed by 5.
|
|
1565
|
+
Nonzero Values [2]
|
|
1566
|
+
>>> # read the SNPs indexed by 5, 4, and 0
|
|
1567
|
+
>>> print("Nonzero Values", bed.read_sparse(np.s_[:,[5,4,0]], dtype="int8").data)
|
|
1568
|
+
Nonzero Values [2 1]
|
|
1569
|
+
>>> # read SNPs from 1 (inclusive) to 11 (exclusive)
|
|
1570
|
+
>>> print("Nonzero Values", bed.read_sparse(np.s_[:,1:11], dtype="int8").data)
|
|
1571
|
+
Nonzero Values [1 2 2 1 1]
|
|
1572
|
+
>>> print(np.unique(bed.chromosome)) # print unique chrom values
|
|
1573
|
+
['1' '5' 'Y']
|
|
1574
|
+
>>> # read all SNPs in chrom 5
|
|
1575
|
+
>>> print("Nonzero Values", bed.read_sparse(np.s_[:,bed.chromosome=='5'], dtype="int8").data)
|
|
1576
|
+
Nonzero Values [1 2 2 1 1 1 1 1]
|
|
1577
|
+
>>> # Read 1st individual (across all SNPs)
|
|
1578
|
+
>>> print("Nonzero Values", bed.read_sparse(np.s_[0,:], dtype="int8").data)
|
|
1579
|
+
Nonzero Values [2]
|
|
1580
|
+
>>> print("Nonzero Values", bed.read_sparse(np.s_[::2,:], dtype="int8").data) # Read every 2nd individual
|
|
1581
|
+
Nonzero Values [1 2 2 1 1]
|
|
1582
|
+
>>> # read last and 2nd-to-last individuals and the 15th-from-the-last SNP
|
|
1583
|
+
>>> print("Nonzero Values", bed.read_sparse(np.s_[[-1,-2],-15], dtype="int8").data)
|
|
1584
|
+
Nonzero Values [2]
|
|
1585
|
+
|
|
1586
|
+
"""
|
|
1587
|
+
if sparse is None:
|
|
1588
|
+
raise ImportError(
|
|
1589
|
+
"The function read_sparse() requires scipy. "
|
|
1590
|
+
+ "Install it with 'pip install --upgrade bed-reader[sparse]'.",
|
|
1591
|
+
)
|
|
1592
|
+
iid_index_or_slice_etc, sid_index_or_slice_etc = self._split_index(index)
|
|
1593
|
+
|
|
1594
|
+
dtype = np.dtype(dtype)
|
|
1595
|
+
|
|
1596
|
+
# Similar code in read().
|
|
1597
|
+
# Later happy with _iid_range and _sid_range or could it be done with
|
|
1598
|
+
# allocation them?
|
|
1599
|
+
if self._iid_range is None:
|
|
1600
|
+
self._iid_range = np.arange(self.iid_count, dtype="intp")
|
|
1601
|
+
if self._sid_range is None:
|
|
1602
|
+
self._sid_range = np.arange(self.sid_count, dtype="intp")
|
|
1603
|
+
|
|
1604
|
+
iid_index = np.ascontiguousarray(
|
|
1605
|
+
self._iid_range[iid_index_or_slice_etc],
|
|
1606
|
+
dtype="intp",
|
|
1607
|
+
)
|
|
1608
|
+
sid_index = np.ascontiguousarray(
|
|
1609
|
+
self._sid_range[sid_index_or_slice_etc], dtype="intp",
|
|
1610
|
+
)
|
|
1611
|
+
|
|
1612
|
+
if (
|
|
1613
|
+
len(iid_index) > np.iinfo(np.int32).max
|
|
1614
|
+
or len(sid_index) > np.iinfo(np.int32).max
|
|
1615
|
+
):
|
|
1616
|
+
msg = (
|
|
1617
|
+
"Too many Individuals or SNPs (variants) requested. "
|
|
1618
|
+
"Maximum is {np.iinfo(np.int32).max}."
|
|
1619
|
+
)
|
|
1620
|
+
raise ValueError(
|
|
1621
|
+
msg,
|
|
1622
|
+
)
|
|
1623
|
+
|
|
1624
|
+
|
|
1625
|
+
if batch_size is None:
|
|
1626
|
+
batch_size = round(np.sqrt(len(sid_index)))
|
|
1627
|
+
|
|
1628
|
+
num_threads = get_num_threads(
|
|
1629
|
+
self._num_threads if num_threads is None else num_threads,
|
|
1630
|
+
)
|
|
1631
|
+
max_concurrent_requests = get_max_concurrent_requests(
|
|
1632
|
+
self._max_concurrent_requests
|
|
1633
|
+
if max_concurrent_requests is None
|
|
1634
|
+
else max_concurrent_requests,
|
|
1635
|
+
)
|
|
1636
|
+
max_chunk_bytes = get_max_chunk_bytes(
|
|
1637
|
+
self._max_chunk_bytes if max_chunk_bytes is None else max_chunk_bytes,
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
if format == "csc":
|
|
1641
|
+
order = "F"
|
|
1642
|
+
indptr = np.zeros(len(sid_index) + 1, dtype=np.int32)
|
|
1643
|
+
elif format == "csr":
|
|
1644
|
+
order = "C"
|
|
1645
|
+
indptr = np.zeros(len(iid_index) + 1, dtype=np.int32)
|
|
1646
|
+
else:
|
|
1647
|
+
msg = f"format '{format}' not known. Expected 'csc' or 'csr'."
|
|
1648
|
+
raise ValueError(msg)
|
|
1649
|
+
|
|
1650
|
+
# We init data and indices with zero element arrays to set their dtype.
|
|
1651
|
+
data = [np.empty(0, dtype=dtype)]
|
|
1652
|
+
indices = [np.empty(0, dtype=np.int32)]
|
|
1653
|
+
|
|
1654
|
+
if self.iid_count > 0 and self.sid_count > 0:
|
|
1655
|
+
reader, location_str, is_cloud = self._pick_reader(dtype)
|
|
1656
|
+
|
|
1657
|
+
if format == "csc":
|
|
1658
|
+
val = np.zeros((len(iid_index), batch_size), order=order, dtype=dtype)
|
|
1659
|
+
for batch_start in range(0, len(sid_index), batch_size):
|
|
1660
|
+
batch_end = batch_start + batch_size
|
|
1661
|
+
if batch_end > len(sid_index):
|
|
1662
|
+
batch_end = len(sid_index)
|
|
1663
|
+
del val
|
|
1664
|
+
val = np.zeros(
|
|
1665
|
+
(len(iid_index), batch_end - batch_start),
|
|
1666
|
+
order=order,
|
|
1667
|
+
dtype=dtype,
|
|
1668
|
+
)
|
|
1669
|
+
batch_slice = np.s_[batch_start:batch_end]
|
|
1670
|
+
batch_index = sid_index[batch_slice]
|
|
1671
|
+
|
|
1672
|
+
if not is_cloud:
|
|
1673
|
+
reader(
|
|
1674
|
+
location_str,
|
|
1675
|
+
self.cloud_options,
|
|
1676
|
+
iid_count=self.iid_count,
|
|
1677
|
+
sid_count=self.sid_count,
|
|
1678
|
+
is_a1_counted=self.count_A1,
|
|
1679
|
+
iid_index=iid_index,
|
|
1680
|
+
sid_index=batch_index,
|
|
1681
|
+
val=val,
|
|
1682
|
+
num_threads=num_threads,
|
|
1683
|
+
)
|
|
1684
|
+
else:
|
|
1685
|
+
reader(
|
|
1686
|
+
location_str,
|
|
1687
|
+
self.cloud_options,
|
|
1688
|
+
iid_count=self.iid_count,
|
|
1689
|
+
sid_count=self.sid_count,
|
|
1690
|
+
is_a1_counted=self.count_A1,
|
|
1691
|
+
iid_index=iid_index,
|
|
1692
|
+
sid_index=batch_index,
|
|
1693
|
+
val=val,
|
|
1694
|
+
num_threads=num_threads,
|
|
1695
|
+
max_concurrent_requests=max_concurrent_requests,
|
|
1696
|
+
max_chunk_bytes=max_chunk_bytes,
|
|
1697
|
+
)
|
|
1698
|
+
|
|
1699
|
+
self.sparsify(
|
|
1700
|
+
val, order, iid_index, batch_slice, data, indices, indptr,
|
|
1701
|
+
)
|
|
1702
|
+
else:
|
|
1703
|
+
assert format == "csr" # real assert
|
|
1704
|
+
val = np.zeros((batch_size, len(sid_index)), order=order, dtype=dtype)
|
|
1705
|
+
for batch_start in range(0, len(iid_index), batch_size):
|
|
1706
|
+
batch_end = batch_start + batch_size
|
|
1707
|
+
if batch_end > len(iid_index):
|
|
1708
|
+
batch_end = len(iid_index)
|
|
1709
|
+
del val
|
|
1710
|
+
val = np.zeros(
|
|
1711
|
+
(batch_end - batch_start, len(sid_index)),
|
|
1712
|
+
order=order,
|
|
1713
|
+
dtype=dtype,
|
|
1714
|
+
)
|
|
1715
|
+
|
|
1716
|
+
batch_slice = np.s_[batch_start:batch_end]
|
|
1717
|
+
batch_index = iid_index[batch_slice]
|
|
1718
|
+
|
|
1719
|
+
if not is_cloud:
|
|
1720
|
+
reader(
|
|
1721
|
+
location_str,
|
|
1722
|
+
self.cloud_options,
|
|
1723
|
+
iid_count=self.iid_count,
|
|
1724
|
+
sid_count=self.sid_count,
|
|
1725
|
+
is_a1_counted=self.count_A1,
|
|
1726
|
+
iid_index=batch_index,
|
|
1727
|
+
sid_index=sid_index,
|
|
1728
|
+
val=val,
|
|
1729
|
+
num_threads=num_threads,
|
|
1730
|
+
)
|
|
1731
|
+
else:
|
|
1732
|
+
reader(
|
|
1733
|
+
location_str,
|
|
1734
|
+
self.cloud_options,
|
|
1735
|
+
iid_count=self.iid_count,
|
|
1736
|
+
sid_count=self.sid_count,
|
|
1737
|
+
is_a1_counted=self.count_A1,
|
|
1738
|
+
iid_index=batch_index,
|
|
1739
|
+
sid_index=sid_index,
|
|
1740
|
+
val=val,
|
|
1741
|
+
num_threads=num_threads,
|
|
1742
|
+
max_concurrent_requests=max_concurrent_requests,
|
|
1743
|
+
max_chunk_bytes=max_chunk_bytes,
|
|
1744
|
+
)
|
|
1745
|
+
|
|
1746
|
+
self.sparsify(
|
|
1747
|
+
val, order, sid_index, batch_slice, data, indices, indptr,
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
data = np.concatenate(data)
|
|
1751
|
+
indices = np.concatenate(indices)
|
|
1752
|
+
|
|
1753
|
+
if format == "csc":
|
|
1754
|
+
return sparse.csc_matrix(
|
|
1755
|
+
(data, indices, indptr), (len(iid_index), len(sid_index)),
|
|
1756
|
+
)
|
|
1757
|
+
assert format == "csr" # real assert
|
|
1758
|
+
return sparse.csr_matrix(
|
|
1759
|
+
(data, indices, indptr), (len(iid_index), len(sid_index)),
|
|
1760
|
+
)
|
|
1761
|
+
|
|
1762
|
+
def sparsify(self, val, order, minor_index, batch_slice, data, indices, indptr) -> None:
|
|
1763
|
+
flatten = np.ravel(val, order=order)
|
|
1764
|
+
nz_indices = np.flatnonzero(flatten).astype(np.int32)
|
|
1765
|
+
column_indexes = nz_indices // len(minor_index)
|
|
1766
|
+
counts = np.bincount(
|
|
1767
|
+
column_indexes, minlength=batch_slice.stop - batch_slice.start,
|
|
1768
|
+
).astype(np.int32)
|
|
1769
|
+
counts_with_initial = np.r_[
|
|
1770
|
+
indptr[batch_slice.start : batch_slice.start + 1], counts,
|
|
1771
|
+
]
|
|
1772
|
+
|
|
1773
|
+
data.append(flatten[nz_indices])
|
|
1774
|
+
indices.append(np.mod(nz_indices, len(minor_index)))
|
|
1775
|
+
indptr[1:][batch_slice] = np.cumsum(counts_with_initial)[1:]
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
def _read_csv(filepath, delimiter=None, dtype=None, usecols=None):
|
|
1779
|
+
pattern = re.compile(r"^np\.\w+\((.+?)\)$")
|
|
1780
|
+
|
|
1781
|
+
# Prepare the usecols by ensuring it is a list of indices
|
|
1782
|
+
usecols_indices = list(usecols)
|
|
1783
|
+
transposed = np.loadtxt(
|
|
1784
|
+
filepath,
|
|
1785
|
+
dtype=np.str_,
|
|
1786
|
+
delimiter=delimiter,
|
|
1787
|
+
usecols=usecols_indices,
|
|
1788
|
+
unpack=True,
|
|
1789
|
+
)
|
|
1790
|
+
if transposed.ndim == 1:
|
|
1791
|
+
transposed = transposed.reshape(-1, 1)
|
|
1792
|
+
row_count = transposed.shape[1] # because unpack=True
|
|
1793
|
+
|
|
1794
|
+
# Convert column lists to numpy arrays with the specified dtype
|
|
1795
|
+
columns = []
|
|
1796
|
+
for output_index, input_index in enumerate(usecols_indices):
|
|
1797
|
+
col = transposed[output_index]
|
|
1798
|
+
|
|
1799
|
+
# work around numpy/python bug
|
|
1800
|
+
if len(col) > 0 and pattern.fullmatch(col[0]):
|
|
1801
|
+
col = np.array([pattern.fullmatch(x).group(1) for x in col])
|
|
1802
|
+
|
|
1803
|
+
# Find the dtype for this column
|
|
1804
|
+
col_dtype = dtype.get(input_index, np.str_)
|
|
1805
|
+
# Convert the column list to a numpy array with the specified dtype
|
|
1806
|
+
columns.append(_convert_to_dtype(col, col_dtype))
|
|
1807
|
+
|
|
1808
|
+
return columns, row_count
|
|
1809
|
+
|
|
1810
|
+
|
|
1811
|
+
def _convert_to_dtype(str_arr, dtype):
|
|
1812
|
+
assert dtype in [np.str_, np.float32, np.int32] # real assert
|
|
1813
|
+
|
|
1814
|
+
if dtype == np.str_:
|
|
1815
|
+
return str_arr
|
|
1816
|
+
|
|
1817
|
+
try:
|
|
1818
|
+
new_arr = str_arr.astype(dtype)
|
|
1819
|
+
except ValueError as e:
|
|
1820
|
+
if dtype == np.float32:
|
|
1821
|
+
raise
|
|
1822
|
+
# for backwards compatibility, see if intermediate float helps int conversion
|
|
1823
|
+
try:
|
|
1824
|
+
assert dtype == np.int32 # real assert
|
|
1825
|
+
float_arr = str_arr.astype(np.float32)
|
|
1826
|
+
except ValueError:
|
|
1827
|
+
raise e
|
|
1828
|
+
new_arr = float_arr.astype(np.int32)
|
|
1829
|
+
if not np.array_equal(new_arr, float_arr):
|
|
1830
|
+
msg = f"invalid literal for int: '{str_arr[np.where(new_arr != float_arr)][:1]}')"
|
|
1831
|
+
raise ValueError(
|
|
1832
|
+
msg,
|
|
1833
|
+
)
|
|
1834
|
+
return new_arr
|
|
1835
|
+
|
|
1836
|
+
|
|
1837
|
+
if __name__ == "__main__":
|
|
1838
|
+
import pytest
|
|
1839
|
+
|
|
1840
|
+
logging.basicConfig(level=logging.INFO)
|
|
1841
|
+
|
|
1842
|
+
pytest.main(["--doctest-modules", __file__])
|