From a2d7008bae3ad1264da20f03f455864c950c6628 Mon Sep 17 00:00:00 2001 From: jie Date: Thu, 11 Mar 2021 16:17:04 +0000 Subject: [PATCH 001/181] fixing issue #86 from upstream: add get_spans() in Field class, similar to get_spans() in Session class --- exetera/core/fields.py | 46 +++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 5f757d79..3762d49f 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -6,6 +6,7 @@ from exetera.core.data_writer import DataWriter from exetera.core import utils +from exetera.core import persistence as per # def test_field_iterator(data): @@ -26,6 +27,7 @@ class Field: + #the argument 'group' is a h5py group object def __init__(self, session, group, name=None, write_enabled=False): # if name is None, the group is an existing field # if name is set but group[name] doesn't exist, then create the field @@ -35,10 +37,10 @@ def __init__(self, session, group, name=None, write_enabled=False): else: field = group[name] self._session = session - self._field = field + self._field = field #name of the upper group of h5py 'dataset' self._fieldtype = self._field.attrs['fieldtype'] self._write_enabled = write_enabled - self._value_wrapper = None + self._value_wrapper = None #wrap the h5py 'dataset' which under the 'values' section @property def name(self): @@ -59,12 +61,16 @@ def __bool__(self): # if f is not None: return True + def get_spans(self): + return per._get_spans(self._value_wrapper[:], None) + + class ReadOnlyFieldArray: def __init__(self, field, dataset_name): - self._field = field - self._name = dataset_name - self._dataset = field[dataset_name] + self._field = field #upper h5py group object + self._name = dataset_name #dataset name, always be 'values' + self._dataset = field[dataset_name] #h5py dataset object def __len__(self): return len(self._dataset) @@ -73,7 +79,7 @@ def __len__(self): def dtype(self): return self._dataset.dtype - def __getitem__(self, item): + def __getitem__(self, item): #rewrite the [] operator return self._dataset[item] def __setitem__(self, key, value): @@ -99,9 +105,9 @@ def complete(self): class WriteableFieldArray: def __init__(self, field, dataset_name): - self._field = field - self._name = dataset_name - self._dataset = field[dataset_name] + self._field = field # upper h5py group object + self._name = dataset_name # dataset name, always be 'values' + self._dataset = field[dataset_name] #h5py dataset object def __len__(self): return len(self._dataset) @@ -110,13 +116,16 @@ def __len__(self): def dtype(self): return self._dataset.dtype - def __getitem__(self, item): + def __getitem__(self, item): #rewrite the [] operator return self._dataset[item] def __setitem__(self, key, value): self._dataset[key] = value def clear(self): + """ + TODO: the name for clear is not corrent, + """ DataWriter._clear_dataset(self._field, self._name) def write_part(self, part): @@ -132,18 +141,18 @@ def complete(self): class ReadOnlyIndexedFieldArray: def __init__(self, field, index_name, values_name): - self._field = field - self._index_name = index_name - self._index_dataset = field[index_name] - self._values_name = values_name - self._values_dataset = field[values_name] + self._field = field #h5py group object + self._index_name = index_name #'index' + self._index_dataset = field[index_name] #h5py dataset object + self._values_name = values_name #'value + self._values_dataset = field[values_name] #h5py dataset object def __len__(self): # TODO: this occurs because of the initialized state of an indexed string. It would be better for the # index to be initialised as [0] return max(len(self._index_dataset)-1, 0) - def __getitem__(self, item): + def __getitem__(self, item): #rewrite the [] operator try: if isinstance(item, slice): start = item.start if item.start is not None else 0 @@ -209,7 +218,7 @@ def __init__(self, field, index_name, values_name): def __len__(self): return len(self._index_dataset) - 1 - def __getitem__(self, item): + def __getitem__(self, item): #rewrite the [] operator try: if isinstance(item, slice): start = item.start if item.start is not None else 0 @@ -411,6 +420,7 @@ def __len__(self): class NumericField(Field): + # Argument group is the hdf5 group name def __init__(self, session, group, name=None, write_enabled=False): super().__init__(session, group, name=name, write_enabled=write_enabled) @@ -424,7 +434,7 @@ def create_like(self, group, name, timestamp=None): return NumericField(self._session, group, name, write_enabled=True) @property - def data(self): + def data(self): #return the dataset as FieldArray if self._value_wrapper is None: if self._write_enabled: self._value_wrapper = WriteableFieldArray(self._field, 'values') From 62925bbde974a4a9766d803d84130aba6b6ada50 Mon Sep 17 00:00:00 2001 From: jie Date: Fri, 12 Mar 2021 11:10:33 +0000 Subject: [PATCH 002/181] add unit test for Field get_spans() function --- tests/test_fields.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_fields.py b/tests/test_fields.py index 8ed15bc3..f666ecea 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -24,7 +24,17 @@ def test_field_truthness(self): self.assertTrue(bool(f)) f = s.create_categorical(src, "d", "int8", {"no": 0, "yes": 1}) self.assertTrue(bool(f)) + + def test_get_spans(self): + vals = np.asarray([0, 1, 1, 3, 3, 6, 5, 5, 5], dtype=np.int32) + bio = BytesIO() + with session.Session() as s: + self.assertListEqual([0, 1, 3, 5, 6, 9], s.get_spans(vals).tolist()) + ds = s.open_dataset(bio, "w", "ds") + vals_f = s.create_numeric(ds, "vals", "int32") + vals_f.data.write(vals) + self.assertListEqual([0, 1, 3, 5, 6, 9], vals_f.get_spans().tolist()) class TestIndexedStringFields(unittest.TestCase): From 0e313dc7b8fc9cea84026d8590ce23711e14d5de Mon Sep 17 00:00:00 2001 From: jie Date: Fri, 12 Mar 2021 16:04:30 +0000 Subject: [PATCH 003/181] remove unuseful line comments --- exetera/core/fields.py | 48 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 3762d49f..6f57f862 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -27,20 +27,16 @@ class Field: - #the argument 'group' is a h5py group object def __init__(self, session, group, name=None, write_enabled=False): - # if name is None, the group is an existing field - # if name is set but group[name] doesn't exist, then create the field if name is None: - # the group is an existing field field = group else: field = group[name] self._session = session - self._field = field #name of the upper group of h5py 'dataset' + self._field = field self._fieldtype = self._field.attrs['fieldtype'] self._write_enabled = write_enabled - self._value_wrapper = None #wrap the h5py 'dataset' which under the 'values' section + self._value_wrapper = None @property def name(self): @@ -68,9 +64,9 @@ def get_spans(self): class ReadOnlyFieldArray: def __init__(self, field, dataset_name): - self._field = field #upper h5py group object - self._name = dataset_name #dataset name, always be 'values' - self._dataset = field[dataset_name] #h5py dataset object + self._field = field + self._name = dataset_name + self._dataset = field[dataset_name] def __len__(self): return len(self._dataset) @@ -79,7 +75,7 @@ def __len__(self): def dtype(self): return self._dataset.dtype - def __getitem__(self, item): #rewrite the [] operator + def __getitem__(self, item): return self._dataset[item] def __setitem__(self, key, value): @@ -105,9 +101,9 @@ def complete(self): class WriteableFieldArray: def __init__(self, field, dataset_name): - self._field = field # upper h5py group object - self._name = dataset_name # dataset name, always be 'values' - self._dataset = field[dataset_name] #h5py dataset object + self._field = field + self._name = dataset_name + self._dataset = field[dataset_name] def __len__(self): return len(self._dataset) @@ -116,7 +112,7 @@ def __len__(self): def dtype(self): return self._dataset.dtype - def __getitem__(self, item): #rewrite the [] operator + def __getitem__(self, item): return self._dataset[item] def __setitem__(self, key, value): @@ -124,7 +120,7 @@ def __setitem__(self, key, value): def clear(self): """ - TODO: the name for clear is not corrent, + TODO: unlink the dataset """ DataWriter._clear_dataset(self._field, self._name) @@ -141,18 +137,18 @@ def complete(self): class ReadOnlyIndexedFieldArray: def __init__(self, field, index_name, values_name): - self._field = field #h5py group object - self._index_name = index_name #'index' - self._index_dataset = field[index_name] #h5py dataset object - self._values_name = values_name #'value - self._values_dataset = field[values_name] #h5py dataset object + self._field = field + self._index_name = index_name + self._index_dataset = field[index_name] + self._values_name = values_name + self._values_dataset = field[values_name] def __len__(self): # TODO: this occurs because of the initialized state of an indexed string. It would be better for the # index to be initialised as [0] return max(len(self._index_dataset)-1, 0) - def __getitem__(self, item): #rewrite the [] operator + def __getitem__(self, item): try: if isinstance(item, slice): start = item.start if item.start is not None else 0 @@ -218,7 +214,7 @@ def __init__(self, field, index_name, values_name): def __len__(self): return len(self._index_dataset) - 1 - def __getitem__(self, item): #rewrite the [] operator + def __getitem__(self, item): try: if isinstance(item, slice): start = item.start if item.start is not None else 0 @@ -301,7 +297,12 @@ def complete(self): self._index_index = 0 + def base_field_contructor(session, group, name, timestamp=None, chunksize=None): + """ + Constructor are for 1)create the field (hdf5 group), 2)add basic attributes like chunksize, + timestamp, field type, and 3)add the dataset to the field (hdf5 group) under the name 'values' + """ if name in group: msg = "Field '{}' already exists in group '{}'" raise ValueError(msg.format(name, group)) @@ -420,7 +421,6 @@ def __len__(self): class NumericField(Field): - # Argument group is the hdf5 group name def __init__(self, session, group, name=None, write_enabled=False): super().__init__(session, group, name=name, write_enabled=write_enabled) @@ -434,7 +434,7 @@ def create_like(self, group, name, timestamp=None): return NumericField(self._session, group, name, write_enabled=True) @property - def data(self): #return the dataset as FieldArray + def data(self): if self._value_wrapper is None: if self._write_enabled: self._value_wrapper = WriteableFieldArray(self._field, 'values') From e211371f34fe371b8bf3a0a1ae154d37ce53680a Mon Sep 17 00:00:00 2001 From: jie Date: Mon, 15 Mar 2021 08:54:17 +0000 Subject: [PATCH 004/181] add dataset, datafreame class --- exetera/core/CSVDataset.py | 206 +++++++++++++++++++++++++++++++++++++ exetera/core/dataframe.py | 0 exetera/core/dataset.py | 206 ------------------------------------- 3 files changed, 206 insertions(+), 206 deletions(-) create mode 100644 exetera/core/CSVDataset.py create mode 100644 exetera/core/dataframe.py diff --git a/exetera/core/CSVDataset.py b/exetera/core/CSVDataset.py new file mode 100644 index 00000000..ae41db59 --- /dev/null +++ b/exetera/core/CSVDataset.py @@ -0,0 +1,206 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import csv +import time +import numpy as np + +from exetera.processing import numpy_buffer + + +class Dataset: + """ + field_descriptors: a dictionary of field names to field descriptors that describe how the field + should be transformed when loading + keys: a list of field names that represent the fields you wish to load and in what order they + should be put. Leaving this blankloads all of the keys in csv column order + """ + def __init__(self, source, field_descriptors=None, keys=None, filter_fn=None, + show_progress_every=False, start_from=None, stop_after=None, early_filter=None, + verbose=True): + + def print_if_verbose(*args): + if verbose: + print(*args) + + self.names_ = list() + self.fields_ = list() + self.names_ = list() + self.index_ = None + + csvf = csv.DictReader(source, delimiter=',', quotechar='"') + available_keys = csvf.fieldnames + + if not keys: + fields_to_use = available_keys + index_map = [i for i in range(len(fields_to_use))] + else: + fields_to_use = keys + index_map = [available_keys.index(k) for k in keys] + + early_key_index = None + if early_filter is not None: + if early_filter[0] not in available_keys: + raise ValueError( + f"'early_filter': tuple element zero must be a key that is in the dataset") + early_key_index = available_keys.index(early_filter[0]) + + tstart = time.time() + transforms_by_index = list() + new_fields = list() + + # build a full list of transforms by index whether they are are being filtered by 'keys' or not + for i_n, n in enumerate(available_keys): + if field_descriptors and n in field_descriptors and\ + field_descriptors[n].strings_to_values and\ + field_descriptors[n].out_of_range_label is None: + # transforms by csv field index + transforms_by_index.append(field_descriptors[n]) + else: + transforms_by_index.append(None) + + # build a new list of collections for every field that is to be loaded + for i_n in index_map: + if transforms_by_index[i_n] is not None: + to_datatype = transforms_by_index[i_n].to_datatype + if to_datatype == str: + new_fields.append(list()) + else: + new_fields.append(numpy_buffer.NumpyBuffer2(dtype=to_datatype)) + else: + new_fields.append(list()) + + # read the cvs rows into the fields + csvf = csv.reader(source, delimiter=',', quotechar='"') + ecsvf = iter(csvf) + filtered_count = 0 + for i_r, row in enumerate(ecsvf): + if show_progress_every: + if i_r % show_progress_every == 0: + if filtered_count == i_r: + print_if_verbose(i_r) + else: + print_if_verbose(f"{i_r} ({filtered_count})") + + if start_from is not None and i_r < start_from: + del row + continue + + # TODO: decide whether True means filter or not filter consistently + if early_filter is not None: + if not early_filter[1](row[early_key_index]): + continue + + # TODO: decide whether True means filter or not filter consistently + if not filter_fn or filter_fn(i_r): + # for i_f, f in enumerate(fields): + for i_df, i_f in enumerate(index_map): + f = row[i_f] + t = transforms_by_index[i_f] + try: + new_fields[i_df].append(f if not t else t.strings_to_values[f]) + except Exception as e: + msg = "{}: key error for value {} (permitted values are {}" + print_if_verbose(msg.format(fields_to_use[i_f], f, t.strings_to_values)) + del row + filtered_count += 1 + if stop_after and i_r >= stop_after: + break + + if show_progress_every: + print_if_verbose(f"{i_r} ({filtered_count})") + + # assign the built sequences to fields_ + for i_f, f in enumerate(new_fields): + if isinstance(f, list): + self.fields_.append(f) + else: + self.fields_.append(f.finalise()) + self.index_ = np.asarray([i for i in range(len(self.fields_[0]))], dtype=np.uint32) + self.names_ = fields_to_use + print_if_verbose('loading took', time.time() - tstart, "seconds") + + # if i > 0 and i % lines_per_dot == 0: + # if i % (lines_per_dot * newline_at) == 0: + # print(f'. {i}') + # else: + # print('.', end='') + # if i % (lines_per_dot * newline_at) != 0: + # print(f' {i}') + + def sort(self, keys): + #map names to indices + if isinstance(keys, str): + + def single_index_sort(index): + field = self.fields_[index] + + def inner_(r): + return field[r] + + return inner_ + self.index_ = sorted(self.index_, + key=single_index_sort(self.field_to_index(keys))) + else: + + kindices = [self.field_to_index(k) for k in keys] + + def index_sort(indices): + def inner_(r): + t = tuple(self.fields_[i][r] for i in indices) + return t + return inner_ + + self.index_ = sorted(self.index_, key=index_sort(kindices)) + + for i_f in range(len(self.fields_)): + unsorted_field = self.fields_[i_f] + self.fields_[i_f] = Dataset._apply_permutation(self.index_, unsorted_field) + del unsorted_field + + @staticmethod + def _apply_permutation(permutation, field): + # n = len(permutation) + # for i in range(0, n): + # print(i) + # pi = permutation[i] + # while pi < i: + # pi = permutation[pi] + # fields[i], fields[pi] = fields[pi], fields[i] + # return fields + if isinstance(field, list): + sorted_field = [None] * len(field) + for ip, p in enumerate(permutation): + sorted_field[ip] = field[p] + else: + sorted_field = np.empty_like(field) + for ip, p in enumerate(permutation): + sorted_field[ip] = field[p] + return sorted_field + + def field_by_name(self, field_name): + return self.fields_[self.field_to_index(field_name)] + + def field_to_index(self, field_name): + return self.names_.index(field_name) + + def value(self, row_index, field_index): + return self.fields_[field_index][row_index] + + def value_from_fieldname(self, index, field_name): + return self.fields_[self.field_to_index(field_name)][index] + + def row_count(self): + return len(self.index_) + + def show(self): + for ir, r in enumerate(self.names_): + print(f'{ir}-{r}') \ No newline at end of file diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py new file mode 100644 index 00000000..e69de29b diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 1348d3ff..e69de29b 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -1,206 +0,0 @@ -# Copyright 2020 KCL-BMEIS - King's College London -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import csv -import time -import numpy as np - -from exetera.processing import numpy_buffer - - -class Dataset: - """ - field_descriptors: a dictionary of field names to field descriptors that describe how the field - should be transformed when loading - keys: a list of field names that represent the fields you wish to load and in what order they - should be put. Leaving this blankloads all of the keys in csv column order - """ - def __init__(self, source, field_descriptors=None, keys=None, filter_fn=None, - show_progress_every=False, start_from=None, stop_after=None, early_filter=None, - verbose=True): - - def print_if_verbose(*args): - if verbose: - print(*args) - - self.names_ = list() - self.fields_ = list() - self.names_ = list() - self.index_ = None - - csvf = csv.DictReader(source, delimiter=',', quotechar='"') - available_keys = csvf.fieldnames - - if not keys: - fields_to_use = available_keys - index_map = [i for i in range(len(fields_to_use))] - else: - fields_to_use = keys - index_map = [available_keys.index(k) for k in keys] - - early_key_index = None - if early_filter is not None: - if early_filter[0] not in available_keys: - raise ValueError( - f"'early_filter': tuple element zero must be a key that is in the dataset") - early_key_index = available_keys.index(early_filter[0]) - - tstart = time.time() - transforms_by_index = list() - new_fields = list() - - # build a full list of transforms by index whether they are are being filtered by 'keys' or not - for i_n, n in enumerate(available_keys): - if field_descriptors and n in field_descriptors and\ - field_descriptors[n].strings_to_values and\ - field_descriptors[n].out_of_range_label is None: - # transforms by csv field index - transforms_by_index.append(field_descriptors[n]) - else: - transforms_by_index.append(None) - - # build a new list of collections for every field that is to be loaded - for i_n in index_map: - if transforms_by_index[i_n] is not None: - to_datatype = transforms_by_index[i_n].to_datatype - if to_datatype == str: - new_fields.append(list()) - else: - new_fields.append(numpy_buffer.NumpyBuffer2(dtype=to_datatype)) - else: - new_fields.append(list()) - - # read the cvs rows into the fields - csvf = csv.reader(source, delimiter=',', quotechar='"') - ecsvf = iter(csvf) - filtered_count = 0 - for i_r, row in enumerate(ecsvf): - if show_progress_every: - if i_r % show_progress_every == 0: - if filtered_count == i_r: - print_if_verbose(i_r) - else: - print_if_verbose(f"{i_r} ({filtered_count})") - - if start_from is not None and i_r < start_from: - del row - continue - - # TODO: decide whether True means filter or not filter consistently - if early_filter is not None: - if not early_filter[1](row[early_key_index]): - continue - - # TODO: decide whether True means filter or not filter consistently - if not filter_fn or filter_fn(i_r): - # for i_f, f in enumerate(fields): - for i_df, i_f in enumerate(index_map): - f = row[i_f] - t = transforms_by_index[i_f] - try: - new_fields[i_df].append(f if not t else t.strings_to_values[f]) - except Exception as e: - msg = "{}: key error for value {} (permitted values are {}" - print_if_verbose(msg.format(fields_to_use[i_f], f, t.strings_to_values)) - del row - filtered_count += 1 - if stop_after and i_r >= stop_after: - break - - if show_progress_every: - print_if_verbose(f"{i_r} ({filtered_count})") - - # assign the built sequences to fields_ - for i_f, f in enumerate(new_fields): - if isinstance(f, list): - self.fields_.append(f) - else: - self.fields_.append(f.finalise()) - self.index_ = np.asarray([i for i in range(len(self.fields_[0]))], dtype=np.uint32) - self.names_ = fields_to_use - print_if_verbose('loading took', time.time() - tstart, "seconds") - - # if i > 0 and i % lines_per_dot == 0: - # if i % (lines_per_dot * newline_at) == 0: - # print(f'. {i}') - # else: - # print('.', end='') - # if i % (lines_per_dot * newline_at) != 0: - # print(f' {i}') - - def sort(self, keys): - #map names to indices - if isinstance(keys, str): - - def single_index_sort(index): - field = self.fields_[index] - - def inner_(r): - return field[r] - - return inner_ - self.index_ = sorted(self.index_, - key=single_index_sort(self.field_to_index(keys))) - else: - - kindices = [self.field_to_index(k) for k in keys] - - def index_sort(indices): - def inner_(r): - t = tuple(self.fields_[i][r] for i in indices) - return t - return inner_ - - self.index_ = sorted(self.index_, key=index_sort(kindices)) - - for i_f in range(len(self.fields_)): - unsorted_field = self.fields_[i_f] - self.fields_[i_f] = Dataset._apply_permutation(self.index_, unsorted_field) - del unsorted_field - - @staticmethod - def _apply_permutation(permutation, field): - # n = len(permutation) - # for i in range(0, n): - # print(i) - # pi = permutation[i] - # while pi < i: - # pi = permutation[pi] - # fields[i], fields[pi] = fields[pi], fields[i] - # return fields - if isinstance(field, list): - sorted_field = [None] * len(field) - for ip, p in enumerate(permutation): - sorted_field[ip] = field[p] - else: - sorted_field = np.empty_like(field) - for ip, p in enumerate(permutation): - sorted_field[ip] = field[p] - return sorted_field - - def field_by_name(self, field_name): - return self.fields_[self.field_to_index(field_name)] - - def field_to_index(self, field_name): - return self.names_.index(field_name) - - def value(self, row_index, field_index): - return self.fields_[field_index][row_index] - - def value_from_fieldname(self, index, field_name): - return self.fields_[self.field_to_index(field_name)][index] - - def row_count(self): - return len(self.index_) - - def show(self): - for ir, r in enumerate(self.names_): - print(f'{ir}-{r}') From 329a7ccf3fa368423f6db3d7286dab04c135727c Mon Sep 17 00:00:00 2001 From: jie Date: Mon, 15 Mar 2021 10:52:18 +0000 Subject: [PATCH 005/181] closing issue 92, reset the dataset when call field.data.clear --- exetera/core/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 6f57f862..601b3392 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -119,10 +119,10 @@ def __setitem__(self, key, value): self._dataset[key] = value def clear(self): - """ - TODO: unlink the dataset - """ + nformat = self._dataset.dtype DataWriter._clear_dataset(self._field, self._name) + DataWriter.write(self._field, 'values', [], 0, nformat) + self._dataset = self._field[self._name] def write_part(self, part): DataWriter.write(self._field, self._name, part, len(part), dtype=self._dataset.dtype) From d9d8b02fcb905972ee2d3a161f68ab6ea663309d Mon Sep 17 00:00:00 2001 From: jie Date: Mon, 15 Mar 2021 10:52:18 +0000 Subject: [PATCH 006/181] closing issue 92, reset the dataset when call field.data.clear --- exetera/core/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 6f57f862..601b3392 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -119,10 +119,10 @@ def __setitem__(self, key, value): self._dataset[key] = value def clear(self): - """ - TODO: unlink the dataset - """ + nformat = self._dataset.dtype DataWriter._clear_dataset(self._field, self._name) + DataWriter.write(self._field, 'values', [], 0, nformat) + self._dataset = self._field[self._name] def write_part(self, part): DataWriter.write(self._field, self._name, part, len(part), dtype=self._dataset.dtype) From 21f0fa945826671be4da94c5532f75ef87237b0e Mon Sep 17 00:00:00 2001 From: jie Date: Mon, 15 Mar 2021 11:23:25 +0000 Subject: [PATCH 007/181] add unittest for field.data.clear function --- tests/test_fields.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index f666ecea..6759a2c3 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -24,7 +24,7 @@ def test_field_truthness(self): self.assertTrue(bool(f)) f = s.create_categorical(src, "d", "int8", {"no": 0, "yes": 1}) self.assertTrue(bool(f)) - + def test_get_spans(self): vals = np.asarray([0, 1, 1, 3, 3, 6, 5, 5, 5], dtype=np.int32) bio = BytesIO() @@ -36,6 +36,8 @@ def test_get_spans(self): vals_f.data.write(vals) self.assertListEqual([0, 1, 3, 5, 6, 9], vals_f.get_spans().tolist()) + + class TestIndexedStringFields(unittest.TestCase): def test_create_indexed_string(self): @@ -74,3 +76,23 @@ def test_update_legacy_indexed_string_that_has_uint_values(self): self.assertListEqual([97, 98, 98, 99, 99, 99, 100, 100, 100, 100], values.tolist()) + +class TestFieldArray(unittest.TestCase): + def test_write_part(self): + bio = BytesIO() + s = session.Session() + dst = s.open_dataset(bio, 'w', 'dst') + num = s.create_numeric(dst, 'num', 'int32') + num.data.write_part(np.arange(10)) + self.assertListEqual([0,1,2,3,4,5,6,7,8,9],list(num.data[:])) + + def test_clear(self): + bio = BytesIO() + s = session.Session() + dst = s.open_dataset(bio, 'w', 'dst') + num = s.create_numeric(dst, 'num', 'int32') + num.data.write_part(np.arange(10)) + num.data.clear() + self.assertListEqual([], list(num.data[:])) + + From c9363ef61075754883928740a2ffd9cb914200f0 Mon Sep 17 00:00:00 2001 From: jie Date: Mon, 15 Mar 2021 11:26:12 +0000 Subject: [PATCH 008/181] recover the dataset file to avoid merge error when fixing issue 92 --- exetera/core/CSVDataset.py | 206 ------------------------------------- exetera/core/dataset.py | 206 +++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+), 206 deletions(-) delete mode 100644 exetera/core/CSVDataset.py diff --git a/exetera/core/CSVDataset.py b/exetera/core/CSVDataset.py deleted file mode 100644 index ae41db59..00000000 --- a/exetera/core/CSVDataset.py +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright 2020 KCL-BMEIS - King's College London -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import csv -import time -import numpy as np - -from exetera.processing import numpy_buffer - - -class Dataset: - """ - field_descriptors: a dictionary of field names to field descriptors that describe how the field - should be transformed when loading - keys: a list of field names that represent the fields you wish to load and in what order they - should be put. Leaving this blankloads all of the keys in csv column order - """ - def __init__(self, source, field_descriptors=None, keys=None, filter_fn=None, - show_progress_every=False, start_from=None, stop_after=None, early_filter=None, - verbose=True): - - def print_if_verbose(*args): - if verbose: - print(*args) - - self.names_ = list() - self.fields_ = list() - self.names_ = list() - self.index_ = None - - csvf = csv.DictReader(source, delimiter=',', quotechar='"') - available_keys = csvf.fieldnames - - if not keys: - fields_to_use = available_keys - index_map = [i for i in range(len(fields_to_use))] - else: - fields_to_use = keys - index_map = [available_keys.index(k) for k in keys] - - early_key_index = None - if early_filter is not None: - if early_filter[0] not in available_keys: - raise ValueError( - f"'early_filter': tuple element zero must be a key that is in the dataset") - early_key_index = available_keys.index(early_filter[0]) - - tstart = time.time() - transforms_by_index = list() - new_fields = list() - - # build a full list of transforms by index whether they are are being filtered by 'keys' or not - for i_n, n in enumerate(available_keys): - if field_descriptors and n in field_descriptors and\ - field_descriptors[n].strings_to_values and\ - field_descriptors[n].out_of_range_label is None: - # transforms by csv field index - transforms_by_index.append(field_descriptors[n]) - else: - transforms_by_index.append(None) - - # build a new list of collections for every field that is to be loaded - for i_n in index_map: - if transforms_by_index[i_n] is not None: - to_datatype = transforms_by_index[i_n].to_datatype - if to_datatype == str: - new_fields.append(list()) - else: - new_fields.append(numpy_buffer.NumpyBuffer2(dtype=to_datatype)) - else: - new_fields.append(list()) - - # read the cvs rows into the fields - csvf = csv.reader(source, delimiter=',', quotechar='"') - ecsvf = iter(csvf) - filtered_count = 0 - for i_r, row in enumerate(ecsvf): - if show_progress_every: - if i_r % show_progress_every == 0: - if filtered_count == i_r: - print_if_verbose(i_r) - else: - print_if_verbose(f"{i_r} ({filtered_count})") - - if start_from is not None and i_r < start_from: - del row - continue - - # TODO: decide whether True means filter or not filter consistently - if early_filter is not None: - if not early_filter[1](row[early_key_index]): - continue - - # TODO: decide whether True means filter or not filter consistently - if not filter_fn or filter_fn(i_r): - # for i_f, f in enumerate(fields): - for i_df, i_f in enumerate(index_map): - f = row[i_f] - t = transforms_by_index[i_f] - try: - new_fields[i_df].append(f if not t else t.strings_to_values[f]) - except Exception as e: - msg = "{}: key error for value {} (permitted values are {}" - print_if_verbose(msg.format(fields_to_use[i_f], f, t.strings_to_values)) - del row - filtered_count += 1 - if stop_after and i_r >= stop_after: - break - - if show_progress_every: - print_if_verbose(f"{i_r} ({filtered_count})") - - # assign the built sequences to fields_ - for i_f, f in enumerate(new_fields): - if isinstance(f, list): - self.fields_.append(f) - else: - self.fields_.append(f.finalise()) - self.index_ = np.asarray([i for i in range(len(self.fields_[0]))], dtype=np.uint32) - self.names_ = fields_to_use - print_if_verbose('loading took', time.time() - tstart, "seconds") - - # if i > 0 and i % lines_per_dot == 0: - # if i % (lines_per_dot * newline_at) == 0: - # print(f'. {i}') - # else: - # print('.', end='') - # if i % (lines_per_dot * newline_at) != 0: - # print(f' {i}') - - def sort(self, keys): - #map names to indices - if isinstance(keys, str): - - def single_index_sort(index): - field = self.fields_[index] - - def inner_(r): - return field[r] - - return inner_ - self.index_ = sorted(self.index_, - key=single_index_sort(self.field_to_index(keys))) - else: - - kindices = [self.field_to_index(k) for k in keys] - - def index_sort(indices): - def inner_(r): - t = tuple(self.fields_[i][r] for i in indices) - return t - return inner_ - - self.index_ = sorted(self.index_, key=index_sort(kindices)) - - for i_f in range(len(self.fields_)): - unsorted_field = self.fields_[i_f] - self.fields_[i_f] = Dataset._apply_permutation(self.index_, unsorted_field) - del unsorted_field - - @staticmethod - def _apply_permutation(permutation, field): - # n = len(permutation) - # for i in range(0, n): - # print(i) - # pi = permutation[i] - # while pi < i: - # pi = permutation[pi] - # fields[i], fields[pi] = fields[pi], fields[i] - # return fields - if isinstance(field, list): - sorted_field = [None] * len(field) - for ip, p in enumerate(permutation): - sorted_field[ip] = field[p] - else: - sorted_field = np.empty_like(field) - for ip, p in enumerate(permutation): - sorted_field[ip] = field[p] - return sorted_field - - def field_by_name(self, field_name): - return self.fields_[self.field_to_index(field_name)] - - def field_to_index(self, field_name): - return self.names_.index(field_name) - - def value(self, row_index, field_index): - return self.fields_[field_index][row_index] - - def value_from_fieldname(self, index, field_name): - return self.fields_[self.field_to_index(field_name)][index] - - def row_count(self): - return len(self.index_) - - def show(self): - for ir, r in enumerate(self.names_): - print(f'{ir}-{r}') \ No newline at end of file diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index e69de29b..ae41db59 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -0,0 +1,206 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import csv +import time +import numpy as np + +from exetera.processing import numpy_buffer + + +class Dataset: + """ + field_descriptors: a dictionary of field names to field descriptors that describe how the field + should be transformed when loading + keys: a list of field names that represent the fields you wish to load and in what order they + should be put. Leaving this blankloads all of the keys in csv column order + """ + def __init__(self, source, field_descriptors=None, keys=None, filter_fn=None, + show_progress_every=False, start_from=None, stop_after=None, early_filter=None, + verbose=True): + + def print_if_verbose(*args): + if verbose: + print(*args) + + self.names_ = list() + self.fields_ = list() + self.names_ = list() + self.index_ = None + + csvf = csv.DictReader(source, delimiter=',', quotechar='"') + available_keys = csvf.fieldnames + + if not keys: + fields_to_use = available_keys + index_map = [i for i in range(len(fields_to_use))] + else: + fields_to_use = keys + index_map = [available_keys.index(k) for k in keys] + + early_key_index = None + if early_filter is not None: + if early_filter[0] not in available_keys: + raise ValueError( + f"'early_filter': tuple element zero must be a key that is in the dataset") + early_key_index = available_keys.index(early_filter[0]) + + tstart = time.time() + transforms_by_index = list() + new_fields = list() + + # build a full list of transforms by index whether they are are being filtered by 'keys' or not + for i_n, n in enumerate(available_keys): + if field_descriptors and n in field_descriptors and\ + field_descriptors[n].strings_to_values and\ + field_descriptors[n].out_of_range_label is None: + # transforms by csv field index + transforms_by_index.append(field_descriptors[n]) + else: + transforms_by_index.append(None) + + # build a new list of collections for every field that is to be loaded + for i_n in index_map: + if transforms_by_index[i_n] is not None: + to_datatype = transforms_by_index[i_n].to_datatype + if to_datatype == str: + new_fields.append(list()) + else: + new_fields.append(numpy_buffer.NumpyBuffer2(dtype=to_datatype)) + else: + new_fields.append(list()) + + # read the cvs rows into the fields + csvf = csv.reader(source, delimiter=',', quotechar='"') + ecsvf = iter(csvf) + filtered_count = 0 + for i_r, row in enumerate(ecsvf): + if show_progress_every: + if i_r % show_progress_every == 0: + if filtered_count == i_r: + print_if_verbose(i_r) + else: + print_if_verbose(f"{i_r} ({filtered_count})") + + if start_from is not None and i_r < start_from: + del row + continue + + # TODO: decide whether True means filter or not filter consistently + if early_filter is not None: + if not early_filter[1](row[early_key_index]): + continue + + # TODO: decide whether True means filter or not filter consistently + if not filter_fn or filter_fn(i_r): + # for i_f, f in enumerate(fields): + for i_df, i_f in enumerate(index_map): + f = row[i_f] + t = transforms_by_index[i_f] + try: + new_fields[i_df].append(f if not t else t.strings_to_values[f]) + except Exception as e: + msg = "{}: key error for value {} (permitted values are {}" + print_if_verbose(msg.format(fields_to_use[i_f], f, t.strings_to_values)) + del row + filtered_count += 1 + if stop_after and i_r >= stop_after: + break + + if show_progress_every: + print_if_verbose(f"{i_r} ({filtered_count})") + + # assign the built sequences to fields_ + for i_f, f in enumerate(new_fields): + if isinstance(f, list): + self.fields_.append(f) + else: + self.fields_.append(f.finalise()) + self.index_ = np.asarray([i for i in range(len(self.fields_[0]))], dtype=np.uint32) + self.names_ = fields_to_use + print_if_verbose('loading took', time.time() - tstart, "seconds") + + # if i > 0 and i % lines_per_dot == 0: + # if i % (lines_per_dot * newline_at) == 0: + # print(f'. {i}') + # else: + # print('.', end='') + # if i % (lines_per_dot * newline_at) != 0: + # print(f' {i}') + + def sort(self, keys): + #map names to indices + if isinstance(keys, str): + + def single_index_sort(index): + field = self.fields_[index] + + def inner_(r): + return field[r] + + return inner_ + self.index_ = sorted(self.index_, + key=single_index_sort(self.field_to_index(keys))) + else: + + kindices = [self.field_to_index(k) for k in keys] + + def index_sort(indices): + def inner_(r): + t = tuple(self.fields_[i][r] for i in indices) + return t + return inner_ + + self.index_ = sorted(self.index_, key=index_sort(kindices)) + + for i_f in range(len(self.fields_)): + unsorted_field = self.fields_[i_f] + self.fields_[i_f] = Dataset._apply_permutation(self.index_, unsorted_field) + del unsorted_field + + @staticmethod + def _apply_permutation(permutation, field): + # n = len(permutation) + # for i in range(0, n): + # print(i) + # pi = permutation[i] + # while pi < i: + # pi = permutation[pi] + # fields[i], fields[pi] = fields[pi], fields[i] + # return fields + if isinstance(field, list): + sorted_field = [None] * len(field) + for ip, p in enumerate(permutation): + sorted_field[ip] = field[p] + else: + sorted_field = np.empty_like(field) + for ip, p in enumerate(permutation): + sorted_field[ip] = field[p] + return sorted_field + + def field_by_name(self, field_name): + return self.fields_[self.field_to_index(field_name)] + + def field_to_index(self, field_name): + return self.names_.index(field_name) + + def value(self, row_index, field_index): + return self.fields_[field_index][row_index] + + def value_from_fieldname(self, index, field_name): + return self.fields_[self.field_to_index(field_name)][index] + + def row_count(self): + return len(self.index_) + + def show(self): + for ir, r in enumerate(self.names_): + print(f'{ir}-{r}') \ No newline at end of file From 14fc1f341dc7084516a81b5d028978fdcfcc279e Mon Sep 17 00:00:00 2001 From: jie Date: Mon, 15 Mar 2021 11:29:51 +0000 Subject: [PATCH 009/181] fix end_of_file char in dataset.py --- exetera/core/dataset.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index ae41db59..c23836cb 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -203,4 +203,5 @@ def row_count(self): def show(self): for ir, r in enumerate(self.names_): - print(f'{ir}-{r}') \ No newline at end of file + print(f'{ir}-{r}') + \ No newline at end of file From 2d133429c130167871a11923b23fbc1e48a5bfe3 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 16 Mar 2021 19:12:11 +0000 Subject: [PATCH 010/181] add get_span for index string field --- exetera/core/fields.py | 7 +++++++ exetera/core/persistence.py | 21 ++++++++++++++++++++- exetera/core/session.py | 30 +++++++++++++++++------------- exetera/core/validation.py | 2 ++ 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 601b3392..8552c1e8 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -2,6 +2,7 @@ import numpy as np import numba +from numba import jit, njit import h5py from exetera.core.data_writer import DataWriter @@ -394,6 +395,12 @@ def __len__(self): return len(self.data) + def get_spans(self): + return per._get_spans_for_index_string_field(self.indices[:], self.values[:]) + + + + class FixedStringField(Field): def __init__(self, session, group, name=None, write_enabled=False): super().__init__(session, group, name=name, write_enabled=write_enabled) diff --git a/exetera/core/persistence.py b/exetera/core/persistence.py index f847defe..7e1c85ca 100644 --- a/exetera/core/persistence.py +++ b/exetera/core/persistence.py @@ -265,6 +265,7 @@ def temp_dataset(): def _get_spans(field, fields): + if field is not None: return _get_spans_for_field(field) elif len(fields) == 1: @@ -296,6 +297,22 @@ def _get_spans_for_field(field0): results[-1] = True return np.nonzero(results)[0] +def _get_spans_for_index_string_field(indices,values): + result = [] + result.append(0) + for i in range(1, len(indices) - 1): + last = indices[i - 1] + current = indices[i] + next = indices[i + 1] + if next - current != current - last: + result.append(i) + continue # compare size first + if np.array_equal(values[last:current], values[current:next]): + pass + else: + result.append(i) + result.append(len(indices) - 1) # total number of elements + return result @njit def _get_spans_for_2_fields(field0, field1): @@ -716,7 +733,9 @@ def _aggregate_impl(predicate, fkey_indices=None, fkey_index_spans=None, class DataStore: - + ''' + DataStore is replaced by Session + ''' def __init__(self, chunksize=DEFAULT_CHUNKSIZE, timestamp=str(datetime.now(timezone.utc))): if not isinstance(timestamp, str): diff --git a/exetera/core/session.py b/exetera/core/session.py index ce378eb8..86237461 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -256,7 +256,7 @@ def apply_filter(self, filter_to_apply, src, dest=None): and returned from the function call. If the field is an IndexedStringField, the indices and values are returned separately. - :param filter_to_apply: the filter to be applied to the source field + :param filter_to_apply: the filter to be applied to the source field, an array of boolean :param src: the field to be filtered :param dest: optional - a field to write the filtered data to :return: the filtered values @@ -352,18 +352,21 @@ def get_spans(self, field=None, fields=None): """ if field is None and fields is None: raise ValueError("One of 'field' and 'fields' must be set") - if field is not None and fields is not None: + elif field is not None and fields is not None: raise ValueError("Only one of 'field' and 'fields' may be set") - raw_field = None - raw_fields = None - if field is not None: - raw_field = val.array_from_parameter(self, 'field', field) - - raw_fields = [] - if fields is not None: + elif field is not None: + if isinstance(field,fld.IndexedStringField): + indices,values = val.array_from_parameter(self, 'field',field) + return per._get_spans_for_index_string_field(indices,values) + else: + raw_field = None + raw_field = val.array_from_parameter(self, 'field', field) + return per._get_spans(raw_field,None) + elif fields is not None: + raw_fields = [] for i_f, f in enumerate(fields): raw_fields.append(val.array_from_parameter(self, "'fields[{}]'".format(i_f), f)) - return per._get_spans(raw_field, raw_fields) + return per._get_spans(None, raw_fields) def _apply_spans_no_src(self, predicate, spans, dest=None): @@ -607,7 +610,8 @@ def predicate_and_join(self, writer.write(destination_space_values) - + #the field is a hdf5 group that contains attribute 'fieldtype' + #return a exetera Field according to the filetype def get(self, field): if isinstance(field, fld.Field): return field @@ -643,8 +647,8 @@ def create_like(self, field, dest_group, dest_name, timestamp=None, chunksize=No def create_indexed_string(self, group, name, timestamp=None, chunksize=None): - fld.indexed_string_field_constructor(self, group, name, timestamp, chunksize) - return fld.IndexedStringField(self, group[name], write_enabled=True) + fld.indexed_string_field_constructor(self, group, name, timestamp, chunksize) #create the hdf5 object + return fld.IndexedStringField(self, group[name], write_enabled=True) #return the field wrapper def create_fixed_string(self, group, name, length, timestamp=None, chunksize=None): diff --git a/exetera/core/validation.py b/exetera/core/validation.py index 3fdd4e60..497a1761 100644 --- a/exetera/core/validation.py +++ b/exetera/core/validation.py @@ -122,6 +122,8 @@ def raw_array_from_parameter(datastore, name, field): def array_from_parameter(session, name, field): if isinstance(field, h5py.Group): return session.get(field).data[:] + elif isinstance(field, fld.IndexedStringField): + return field.indices[:],field.values[:] elif isinstance(field, fld.Field): return field.data[:] elif isinstance(field, np.ndarray): From 666073ef7d607419fa735ed8fff3dd7567810738 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 17 Mar 2021 09:33:03 +0000 Subject: [PATCH 011/181] unittest for get_span functions on different types of field, eg. fixed string, indexed string, etc. --- exetera/core/fields.py | 9 +++++++++ exetera/core/persistence.py | 8 +++----- tests/test_fields.py | 19 ++++++++++++++++++- tests/test_session.py | 9 +++++++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 8552c1e8..9e6e736c 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -426,6 +426,9 @@ def data(self): def __len__(self): return len(self.data) + def get_spans(self): + return per._get_spans(self.data[:], None) + class NumericField(Field): def __init__(self, session, group, name=None, write_enabled=False): @@ -452,6 +455,9 @@ def data(self): def __len__(self): return len(self.data) + def get_spans(self): + return per._get_spans(self.data[:], None) + class CategoricalField(Field): def __init__(self, session, group, @@ -481,6 +487,9 @@ def data(self): def __len__(self): return len(self.data) + def get_spans(self): + return per._get_spans(self.data[:], None) + # Note: key is presented as value: str, even though the dictionary must be presented # as str: value @property diff --git a/exetera/core/persistence.py b/exetera/core/persistence.py index 7e1c85ca..45dcb255 100644 --- a/exetera/core/persistence.py +++ b/exetera/core/persistence.py @@ -304,12 +304,10 @@ def _get_spans_for_index_string_field(indices,values): last = indices[i - 1] current = indices[i] next = indices[i + 1] - if next - current != current - last: + if next - current != current - last: # compare size first result.append(i) - continue # compare size first - if np.array_equal(values[last:current], values[current:next]): - pass - else: + continue + if not np.array_equal(values[last:current], values[current:next]): result.append(i) result.append(len(indices) - 1) # total number of elements return result diff --git a/tests/test_fields.py b/tests/test_fields.py index 6759a2c3..94161e28 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -26,6 +26,10 @@ def test_field_truthness(self): self.assertTrue(bool(f)) def test_get_spans(self): + ''' + Here test only the numeric field, categorical field and fixed string field. + Indexed string see TestIndexedStringFields below + ''' vals = np.asarray([0, 1, 1, 3, 3, 6, 5, 5, 5], dtype=np.int32) bio = BytesIO() with session.Session() as s: @@ -36,6 +40,13 @@ def test_get_spans(self): vals_f.data.write(vals) self.assertListEqual([0, 1, 3, 5, 6, 9], vals_f.get_spans().tolist()) + fxdstr = s.create_fixed_string(ds, 'fxdstr', 2) + fxdstr.data.write(np.asarray(['aa', 'bb', 'bb', 'cc', 'cc', 'dd', 'dd', 'dd', 'ee'], dtype='S2')) + self.assertListEqual([0,1,3,5,8,9],list(fxdstr.get_spans())) + + cat = s.create_categorical(ds, 'cat', 'int8', {'a': 1, 'b': 2}) + cat.data.write([1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 1, 2, 1, 2]) + self.assertListEqual([0,2,4,7,10,11,12,13,14],list(cat.get_spans())) class TestIndexedStringFields(unittest.TestCase): @@ -74,7 +85,13 @@ def test_update_legacy_indexed_string_that_has_uint_values(self): f.write(strings) values = hf['foo']['values'][:] self.assertListEqual([97, 98, 98, 99, 99, 99, 100, 100, 100, 100], values.tolist()) - + def test_index_string_field_get_span(self): + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + idx = s.create_indexed_string(ds, 'idx') + idx.data.write(['aa', 'bb', 'bb', 'c', 'c', 'c', 'ddd', 'ddd', 'e', 'f', 'f', 'f']) + self.assertListEqual([0, 1, 3, 6, 8, 9, 12], s.get_spans(idx)) class TestFieldArray(unittest.TestCase): diff --git a/tests/test_session.py b/tests/test_session.py index 4400288c..903acc41 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -445,6 +445,15 @@ def test_get_spans_two_fields(self): vals_2_f.data.write(vals_2) self.assertListEqual([0, 2, 3, 5, 6, 8, 12], s.get_spans(fields=(vals_1, vals_2)).tolist()) + def test_get_spans_index_string_field(self): + bio=BytesIO() + with session.Session() as s: + ds=s.open_dataset(bio,'w','ds') + idx= s.create_indexed_string(ds,'idx') + idx.data.write(['aa','bb','bb','c','c','c','d','d','e','f','f','f']) + self.assertListEqual([0,1,3,6,8,9,12],s.get_spans(idx)) + + class TestSessionAggregate(unittest.TestCase): From 8ba818f79163dc44cd034e07bff2713d57807659 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 19 Mar 2021 11:11:24 +0000 Subject: [PATCH 012/181] dataframe basic methods and unittest --- exetera/core/dataframe.py | 118 ++++++++++++++++++++++++++++++++++++++ tests/test_dataframe.py | 47 +++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 tests/test_dataframe.py diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index e69de29b..7c935373 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -0,0 +1,118 @@ +from exetera.core import fields as fld +from datetime import datetime,timezone + +class DataFrame(): + """ + DataFrame is a table of data that contains a list of Fields (columns) + """ + def __init__(self, data=None): + if data is not None: + if isinstance(data,dict) and isinstance(list(a.items())[0][0],str) and isinstance(list(a.items())[0][1], fld.Field) : + self.data=data + self.data = dict() + + def add(self,field,name=None): + if name is not None: + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + else: + self.data[name]=field + self.data[field.name]=field #note the name has '/' for hdf5 object + + def __contains__(self, name): + """ + check if dataframe contains a field, by the field name + name: the name of the field to check,return a bool + """ + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + else: + return self.data.__contains__(name) + + def contains_field(self,field): + """ + check if dataframe contains a field by the field object + field: the filed object to check, return a tuple(bool,str). The str is the name stored in dataframe. + """ + if not isinstance(field, fld.Field): + raise TypeError("The field must be a Field object") + else: + for v in self.data.values(): + if id(field) == id(v): + return True + break + return False + + def __getitem__(self, name): + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + elif not self.__contains__(name): + raise ValueError("Can not find the name from this dataframe.") + else: + return self.data[name] + + def get_field(self,name): + return self.__getitem__(name) + + def get_name(self,field): + """ + Get the name of the field in dataframe + """ + if not isinstance(field,fld.Field): + raise TypeError("The field argument must be a Field object.") + for name,v in self.data.items(): + if id(field) == id(v): + return name + break + return None + + def __setitem__(self, name, field): + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + elif not isinstance(field,fld.Field): + raise TypeError("The field must be a Field object.") + else: + self.data[name]=field + return True + + def __delitem__(self, name): + if not self.__contains__(name=name): + raise ValueError("This dataframe does not contain the name to delete.") + else: + del self.data[name] + return True + + def delete_field(self,field): + """ + Remove field from dataframe by field + """ + name = self.get_name(field) + if name is None: + raise ValueError("This dataframe does not contain the field to delete.") + else: + self.__delitem__(name) + + def list(self): + return tuple(n for n in self.data.keys()) + + def __iter__(self): + return iter(self.data) + + def __next__(self): + return next(self.data) + """ + def search(self): #is search similar to get & get_name? + pass + """ + def __len__(self): + return len(self.data) + + def apply_filter(self,filter_to_apply,dst): + pass + + def apply_index(self, index_to_apply, dest): + pass + + def sort_on(self,dest_group, keys, + timestamp=datetime.now(timezone.utc), write_mode='write', verbose=True): + pass \ No newline at end of file diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py new file mode 100644 index 00000000..0a966ccf --- /dev/null +++ b/tests/test_dataframe.py @@ -0,0 +1,47 @@ +import unittest +from io import BytesIO + +from exetera.core import session +from exetera.core import fields +from exetera.core import persistence as per +from exetera.core import dataframe + + +class TestDataFrame(unittest.TestCase): + + def test_dataframe_init(self): + bio=BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio,'w','dst') + numf = s.create_numeric(dst,'numf','int32') + #init + df = dataframe.DataFrame() + self.assertTrue(isinstance(df, dataframe.DataFrame)) + fdf = {'/numf',numf} + df2 = dataframe.DataFrame(fdf) + self.assertTrue(isinstance(df2,dataframe.DataFrame)) + #add & set & contains + df.add(numf) + self.assertTrue('/numf' in df) + self.assertTrue(df.contains_field(numf)) + cat=s.create_categorical(dst,'cat','int8',{'a':1,'b':2}) + self.assertFalse('/cat' in df) + self.assertFalse(df.contains_field(cat)) + df['/cat']=cat + self.assertTrue('/cat' in df) + #list & get + self.assertEqual(id(numf),id(df.get_field('/numf'))) + self.assertEqual(id(numf), id(df['/numf'])) + self.assertEqual('/numf',df.get_name(numf)) + #list & iter + dfit = iter(df) + self.assertEqual('/numf',next(dfit)) + self.assertEqual('/cat', next(dfit)) + #del & del by field + del df['/numf'] + self.assertFalse('/numf' in df) + df.delete_field(cat) + self.assertFalse(df.contains_field(cat)) + self.assertIsNone(df.get_name(cat)) + + From abb333730eaa73073c903ccbaa6dcf81b09a62bc Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 22 Mar 2021 11:02:00 +0000 Subject: [PATCH 013/181] more dataframe operations --- exetera/core/dataframe.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 7c935373..1eab84d9 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -115,4 +115,6 @@ def apply_index(self, index_to_apply, dest): def sort_on(self,dest_group, keys, timestamp=datetime.now(timezone.utc), write_mode='write', verbose=True): - pass \ No newline at end of file + pass + + '''other span operations???''' \ No newline at end of file From 9b9c4207638eb78917a5baf5a40f575290516473 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 24 Mar 2021 10:58:51 +0000 Subject: [PATCH 014/181] minor fixing --- tests/test_fields.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index de0f2441..9fae3a7c 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -129,6 +129,4 @@ def test_clear(self): num = s.create_numeric(dst, 'num', 'int32') num.data.write_part(np.arange(10)) num.data.clear() - self.assertListEqual([], list(num.data[:])) - - + self.assertListEqual([], list(num.data[:])) \ No newline at end of file From 55989d603be623a2343fe904d7aa4e20a347e101 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 24 Mar 2021 11:00:14 +0000 Subject: [PATCH 015/181] update get_span to field subclass --- exetera/core/fields.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 9e6e736c..fd2eda75 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -59,7 +59,7 @@ def __bool__(self): return True def get_spans(self): - return per._get_spans(self._value_wrapper[:], None) + pass @@ -394,6 +394,9 @@ def values(self): def __len__(self): return len(self.data) + def get_spans(self): + + def get_spans(self): return per._get_spans_for_index_string_field(self.indices[:], self.values[:]) From f2136d560275373fca4dece4b0687f0bc9410797 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 24 Mar 2021 11:02:38 +0000 Subject: [PATCH 016/181] intermedia commit due to test pr 118 --- exetera/core/session.py | 2 +- tests/test_session.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/exetera/core/session.py b/exetera/core/session.py index 86237461..ca59fc2f 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -288,7 +288,7 @@ def apply_index(self, index_to_apply, src, dest=None): and returned from the function call. If the field is an IndexedStringField, the indices and values are returned separately. - :param index_to_apply: the index to be applied to the source field + :param index_to_apply: the index to be applied to the source field, must be one of Group, Field, or ndarray :param src: the field to be index :param dest: optional - a field to write the indexed data to :return: the indexed values diff --git a/tests/test_session.py b/tests/test_session.py index 903acc41..2df43e4f 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -880,3 +880,7 @@ def test_date_importer(self): [datetime(year=int(v[0:4]), month=int(v[5:7]), day=int(v[8:10]) ).timestamp() for v in values], f.data[:].tolist()) + + +if __name__ == '__main__': + a=TestSessionSort().test_dataset_sort_index_ndarray() \ No newline at end of file From 000463dd1fbca12a2e5bd8a27eb356abc78dbf6c Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 24 Mar 2021 16:28:45 +0000 Subject: [PATCH 017/181] Implementate get_spans(ndarray) and get_spans(ndarray1, ndarray2) function in core.operations. Provide get_spans methods in fields using data attribute. --- exetera/core/fields.py | 14 +++++---- exetera/core/operations.py | 58 +++++++++++++++++++++++++++++++++++++ exetera/core/persistence.py | 18 ++---------- exetera/core/session.py | 11 +++---- 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 9e6e736c..1d2c27cb 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -8,6 +8,7 @@ from exetera.core.data_writer import DataWriter from exetera.core import utils from exetera.core import persistence as per +from exetera.core import operations as ops # def test_field_iterator(data): @@ -59,7 +60,7 @@ def __bool__(self): return True def get_spans(self): - return per._get_spans(self._value_wrapper[:], None) + pass @@ -396,7 +397,7 @@ def __len__(self): def get_spans(self): - return per._get_spans_for_index_string_field(self.indices[:], self.values[:]) + return ops._get_spans_for_index_string_field(self.indices[:], self.values[:]) @@ -427,7 +428,7 @@ def __len__(self): return len(self.data) def get_spans(self): - return per._get_spans(self.data[:], None) + return ops._get_spans_for_field(self.data[:]) class NumericField(Field): @@ -456,7 +457,7 @@ def __len__(self): return len(self.data) def get_spans(self): - return per._get_spans(self.data[:], None) + return ops._get_spans_for_field(self.data[:]) class CategoricalField(Field): @@ -488,7 +489,7 @@ def __len__(self): return len(self.data) def get_spans(self): - return per._get_spans(self.data[:], None) + return ops._get_spans_for_field(self.data[:]) # Note: key is presented as value: str, even though the dictionary must be presented # as str: value @@ -524,6 +525,9 @@ def data(self): def __len__(self): return len(self.data) + def get_span(self): + return ops._get_spans_for_field(self.data[:]) + class IndexedStringImporter: def __init__(self, session, group, name, timestamp=None, chunksize=None): diff --git a/exetera/core/operations.py b/exetera/core/operations.py index e7953594..0634355c 100644 --- a/exetera/core/operations.py +++ b/exetera/core/operations.py @@ -205,6 +205,64 @@ def apply_indices_to_index_values(indices_to_apply, indices, values): return dest_indices, dest_values +def _get_spans_for_field(ndarray): + results = np.zeros(len(ndarray) + 1, dtype=np.bool) + if np.issubdtype(ndarray.dtype, np.number): + fn = np.not_equal + else: + fn = np.char.not_equal + results[1:-1] = fn(ndarray[:-1], ndarray[1:]) + + results[0] = True + results[-1] = True + return np.nonzero(results)[0] + +def _get_spans_for_fields1(ndarray0, ndarray1): + count = 0 + spans = [] + span0 = _get_spans_for_field(ndarray0) + span1 = _get_spans_for_field(ndarray1) + j=0 + for i in range(len(span0)): + while j Date: Thu, 25 Mar 2021 09:59:11 +0000 Subject: [PATCH 018/181] Move the get_spans functions from persistence to operations. Modify the get_spans functions in Session to call field method and operation method. --- exetera/core/operations.py | 4 +-- exetera/core/persistence.py | 68 +++++++++++++++++-------------------- exetera/core/session.py | 24 ++++++------- 3 files changed, 44 insertions(+), 52 deletions(-) diff --git a/exetera/core/operations.py b/exetera/core/operations.py index 0634355c..562f68e5 100644 --- a/exetera/core/operations.py +++ b/exetera/core/operations.py @@ -217,7 +217,7 @@ def _get_spans_for_field(ndarray): results[-1] = True return np.nonzero(results)[0] -def _get_spans_for_fields1(ndarray0, ndarray1): +def _get_spans_for_2_fields_by_spans(ndarray0, ndarray1): count = 0 spans = [] span0 = _get_spans_for_field(ndarray0) @@ -234,7 +234,7 @@ def _get_spans_for_fields1(ndarray0, ndarray1): spans.extend(span1[j:]) return spans -def _get_spans_for_fields2(ndarray0, ndarray1): +def _get_spans_for_2_fields(ndarray0, ndarray1): count = 0 spans = np.zeros(len(ndarray0)+1, dtype=np.uint32) spans[0] = 0 diff --git a/exetera/core/persistence.py b/exetera/core/persistence.py index de74c1ac..9b17f39b 100644 --- a/exetera/core/persistence.py +++ b/exetera/core/persistence.py @@ -264,16 +264,16 @@ def temp_dataset(): hd.close() -def _get_spans(field, fields): - - if field is not None: - return _get_spans_for_field(field) - elif len(fields) == 1: - return _get_spans_for_field(fields[0]) - elif len(fields) == 2: - return _get_spans_for_2_fields(*fields) - else: - raise NotImplementedError("This operation does not support more than two fields at present") +# def _get_spans(field, fields): +# +# if field is not None: +# return _get_spans_for_field(field) +# elif len(fields) == 1: +# return _get_spans_for_field(fields[0]) +# elif len(fields) == 2: +# return _get_spans_for_2_fields(*fields) +# else: +# raise NotImplementedError("This operation does not support more than two fields at present") @njit @@ -285,32 +285,28 @@ def _index_spans(spans, results): return results -def _get_spans_for_field(field0): - results = np.zeros(len(field0) + 1, dtype=np.bool) - if np.issubdtype(field0.dtype, np.number): - fn = np.not_equal - else: - fn = np.char.not_equal - results[1:-1] = fn(field0[:-1], field0[1:]) - - results[0] = True - results[-1] = True - return np.nonzero(results)[0] - - - - - -def _get_spans_for_2_fields(field0, field1): - count = 0 - spans = np.zeros(len(field0)+1, dtype=np.uint32) - spans[0] = 0 - for i in np.arange(1, len(field0)): - if field0[i] != field0[i-1] or field1[i] != field1[i-1]: - count += 1 - spans[count] = i - spans[count+1] = len(field0) - return spans[:count+2] +# def _get_spans_for_field(field0): +# results = np.zeros(len(field0) + 1, dtype=np.bool) +# if np.issubdtype(field0.dtype, np.number): +# fn = np.not_equal +# else: +# fn = np.char.not_equal +# results[1:-1] = fn(field0[:-1], field0[1:]) +# +# results[0] = True +# results[-1] = True +# return np.nonzero(results)[0] + +# def _get_spans_for_2_fields(field0, field1): +# count = 0 +# spans = np.zeros(len(field0)+1, dtype=np.uint32) +# spans[0] = 0 +# for i in np.arange(1, len(field0)): +# if field0[i] != field0[i-1] or field1[i] != field1[i-1]: +# count += 1 +# spans[count] = i +# spans[count+1] = len(field0) +# return spans[:count+2] @njit diff --git a/exetera/core/session.py b/exetera/core/session.py index cc633350..d3849d42 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -350,20 +350,16 @@ def get_spans(self, field=None, fields=None): field: [1, 2, 2, 1, 1, 1, 3, 4, 4, 4, 2, 2, 2, 2, 2] result: [0, 1, 3, 6, 7, 10, 15] """ - if field is None and fields is None: - raise ValueError("One of 'field' and 'fields' must be set") - elif field is not None and fields is not None: - raise ValueError("Only one of 'field' and 'fields' may be set") - elif field is not None and isinstance(field,fld.Field): - return field.get_spans() - elif field is not None: - raw_field = val.array_from_parameter(self, 'field', field) - return per._get_spans(raw_field,None) - elif fields is not None: - raw_fields = [] - for i_f, f in enumerate(fields): - raw_fields.append(val.array_from_parameter(self, "'fields[{}]'".format(i_f), f)) - return per._get_spans(None, raw_fields) + if fields is not None: + if isinstance(fields[0],fld.Field): + return ops._get_spans_for_2_fields_by_spans(fields[0].get_spans(),fields[1].get_spans()) + if isinstance(fields[0],np.ndarray): + return ops._get_spans_for_2_fields(fields[0],fields[1]) + else: + if isinstance(field,fld.Field): + return field.get_spans() + if isinstance(field,np.ndarray): + return ops._get_spans_for_field(field) def _apply_spans_no_src(self, predicate, spans, dest=None): From 95c164559d76d8e49b4d5ddef193593c2f47257c Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 25 Mar 2021 10:06:08 +0000 Subject: [PATCH 019/181] minor edits for pull request --- exetera/core/dataframe.py | 7 ++++++- exetera/core/fields.py | 5 ----- exetera/core/persistence.py | 4 +--- exetera/core/session.py | 3 +-- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 1eab84d9..2eae4f1c 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -107,6 +107,12 @@ def search(self): #is search similar to get & get_name? def __len__(self): return len(self.data) + def get_spans(self): + spans=[] + for field in self.data.values(): + spans.append(field.get_spans()) + return spans + def apply_filter(self,filter_to_apply,dst): pass @@ -117,4 +123,3 @@ def sort_on(self,dest_group, keys, timestamp=datetime.now(timezone.utc), write_mode='write', verbose=True): pass - '''other span operations???''' \ No newline at end of file diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 30dd37d8..31d30b9a 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -2,7 +2,6 @@ import numpy as np import numba -from numba import jit, njit import h5py from exetera.core.data_writer import DataWriter @@ -395,10 +394,6 @@ def values(self): def __len__(self): return len(self.data) - def get_spans(self): - - - def get_spans(self): return ops._get_spans_for_index_string_field(self.indices[:], self.values[:]) diff --git a/exetera/core/persistence.py b/exetera/core/persistence.py index 9b17f39b..92e1f93b 100644 --- a/exetera/core/persistence.py +++ b/exetera/core/persistence.py @@ -715,9 +715,7 @@ def _aggregate_impl(predicate, fkey_indices=None, fkey_index_spans=None, class DataStore: - ''' - DataStore is replaced by Session - ''' + def __init__(self, chunksize=DEFAULT_CHUNKSIZE, timestamp=str(datetime.now(timezone.utc))): if not isinstance(timestamp, str): diff --git a/exetera/core/session.py b/exetera/core/session.py index ffb603ea..d5601e3e 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -603,8 +603,7 @@ def predicate_and_join(self, writer.write(destination_space_values) - #the field is a hdf5 group that contains attribute 'fieldtype' - #return a exetera Field according to the filetype + def get(self, field): if isinstance(field, fld.Field): return field From 664e25516d6fb5bab1c24f318cb5c631ed7ad5da Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 25 Mar 2021 10:13:06 +0000 Subject: [PATCH 020/181] remove dataframe for pull request --- exetera/core/dataframe.py | 125 -------------------------------------- 1 file changed, 125 deletions(-) delete mode 100644 exetera/core/dataframe.py diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py deleted file mode 100644 index 2eae4f1c..00000000 --- a/exetera/core/dataframe.py +++ /dev/null @@ -1,125 +0,0 @@ -from exetera.core import fields as fld -from datetime import datetime,timezone - -class DataFrame(): - """ - DataFrame is a table of data that contains a list of Fields (columns) - """ - def __init__(self, data=None): - if data is not None: - if isinstance(data,dict) and isinstance(list(a.items())[0][0],str) and isinstance(list(a.items())[0][1], fld.Field) : - self.data=data - self.data = dict() - - def add(self,field,name=None): - if name is not None: - if not isinstance(name,str): - raise TypeError("The name must be a str object.") - else: - self.data[name]=field - self.data[field.name]=field #note the name has '/' for hdf5 object - - def __contains__(self, name): - """ - check if dataframe contains a field, by the field name - name: the name of the field to check,return a bool - """ - if not isinstance(name,str): - raise TypeError("The name must be a str object.") - else: - return self.data.__contains__(name) - - def contains_field(self,field): - """ - check if dataframe contains a field by the field object - field: the filed object to check, return a tuple(bool,str). The str is the name stored in dataframe. - """ - if not isinstance(field, fld.Field): - raise TypeError("The field must be a Field object") - else: - for v in self.data.values(): - if id(field) == id(v): - return True - break - return False - - def __getitem__(self, name): - if not isinstance(name,str): - raise TypeError("The name must be a str object.") - elif not self.__contains__(name): - raise ValueError("Can not find the name from this dataframe.") - else: - return self.data[name] - - def get_field(self,name): - return self.__getitem__(name) - - def get_name(self,field): - """ - Get the name of the field in dataframe - """ - if not isinstance(field,fld.Field): - raise TypeError("The field argument must be a Field object.") - for name,v in self.data.items(): - if id(field) == id(v): - return name - break - return None - - def __setitem__(self, name, field): - if not isinstance(name,str): - raise TypeError("The name must be a str object.") - elif not isinstance(field,fld.Field): - raise TypeError("The field must be a Field object.") - else: - self.data[name]=field - return True - - def __delitem__(self, name): - if not self.__contains__(name=name): - raise ValueError("This dataframe does not contain the name to delete.") - else: - del self.data[name] - return True - - def delete_field(self,field): - """ - Remove field from dataframe by field - """ - name = self.get_name(field) - if name is None: - raise ValueError("This dataframe does not contain the field to delete.") - else: - self.__delitem__(name) - - def list(self): - return tuple(n for n in self.data.keys()) - - def __iter__(self): - return iter(self.data) - - def __next__(self): - return next(self.data) - """ - def search(self): #is search similar to get & get_name? - pass - """ - def __len__(self): - return len(self.data) - - def get_spans(self): - spans=[] - for field in self.data.values(): - spans.append(field.get_spans()) - return spans - - def apply_filter(self,filter_to_apply,dst): - pass - - def apply_index(self, index_to_apply, dest): - pass - - def sort_on(self,dest_group, keys, - timestamp=datetime.now(timezone.utc), write_mode='write', verbose=True): - pass - From 02265fe0df32ee51090a65334988cc5dbbd6e9b8 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 25 Mar 2021 10:14:28 +0000 Subject: [PATCH 021/181] remove dataframe test for pr --- tests/test_dataframe.py | 47 ----------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 tests/test_dataframe.py diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py deleted file mode 100644 index 0a966ccf..00000000 --- a/tests/test_dataframe.py +++ /dev/null @@ -1,47 +0,0 @@ -import unittest -from io import BytesIO - -from exetera.core import session -from exetera.core import fields -from exetera.core import persistence as per -from exetera.core import dataframe - - -class TestDataFrame(unittest.TestCase): - - def test_dataframe_init(self): - bio=BytesIO() - with session.Session() as s: - dst = s.open_dataset(bio,'w','dst') - numf = s.create_numeric(dst,'numf','int32') - #init - df = dataframe.DataFrame() - self.assertTrue(isinstance(df, dataframe.DataFrame)) - fdf = {'/numf',numf} - df2 = dataframe.DataFrame(fdf) - self.assertTrue(isinstance(df2,dataframe.DataFrame)) - #add & set & contains - df.add(numf) - self.assertTrue('/numf' in df) - self.assertTrue(df.contains_field(numf)) - cat=s.create_categorical(dst,'cat','int8',{'a':1,'b':2}) - self.assertFalse('/cat' in df) - self.assertFalse(df.contains_field(cat)) - df['/cat']=cat - self.assertTrue('/cat' in df) - #list & get - self.assertEqual(id(numf),id(df.get_field('/numf'))) - self.assertEqual(id(numf), id(df['/numf'])) - self.assertEqual('/numf',df.get_name(numf)) - #list & iter - dfit = iter(df) - self.assertEqual('/numf',next(dfit)) - self.assertEqual('/cat', next(dfit)) - #del & del by field - del df['/numf'] - self.assertFalse('/numf' in df) - df.delete_field(cat) - self.assertFalse(df.contains_field(cat)) - self.assertIsNone(df.get_name(cat)) - - From f536652fa02278a44d051ee1e0025544503d6295 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 25 Mar 2021 10:19:22 +0000 Subject: [PATCH 022/181] add dataframe --- exetera/core/dataframe.py | 125 ++++++++++++++++++++++++++++++++++++++ tests/test_dataframe.py | 47 ++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 exetera/core/dataframe.py create mode 100644 tests/test_dataframe.py diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py new file mode 100644 index 00000000..2eae4f1c --- /dev/null +++ b/exetera/core/dataframe.py @@ -0,0 +1,125 @@ +from exetera.core import fields as fld +from datetime import datetime,timezone + +class DataFrame(): + """ + DataFrame is a table of data that contains a list of Fields (columns) + """ + def __init__(self, data=None): + if data is not None: + if isinstance(data,dict) and isinstance(list(a.items())[0][0],str) and isinstance(list(a.items())[0][1], fld.Field) : + self.data=data + self.data = dict() + + def add(self,field,name=None): + if name is not None: + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + else: + self.data[name]=field + self.data[field.name]=field #note the name has '/' for hdf5 object + + def __contains__(self, name): + """ + check if dataframe contains a field, by the field name + name: the name of the field to check,return a bool + """ + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + else: + return self.data.__contains__(name) + + def contains_field(self,field): + """ + check if dataframe contains a field by the field object + field: the filed object to check, return a tuple(bool,str). The str is the name stored in dataframe. + """ + if not isinstance(field, fld.Field): + raise TypeError("The field must be a Field object") + else: + for v in self.data.values(): + if id(field) == id(v): + return True + break + return False + + def __getitem__(self, name): + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + elif not self.__contains__(name): + raise ValueError("Can not find the name from this dataframe.") + else: + return self.data[name] + + def get_field(self,name): + return self.__getitem__(name) + + def get_name(self,field): + """ + Get the name of the field in dataframe + """ + if not isinstance(field,fld.Field): + raise TypeError("The field argument must be a Field object.") + for name,v in self.data.items(): + if id(field) == id(v): + return name + break + return None + + def __setitem__(self, name, field): + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + elif not isinstance(field,fld.Field): + raise TypeError("The field must be a Field object.") + else: + self.data[name]=field + return True + + def __delitem__(self, name): + if not self.__contains__(name=name): + raise ValueError("This dataframe does not contain the name to delete.") + else: + del self.data[name] + return True + + def delete_field(self,field): + """ + Remove field from dataframe by field + """ + name = self.get_name(field) + if name is None: + raise ValueError("This dataframe does not contain the field to delete.") + else: + self.__delitem__(name) + + def list(self): + return tuple(n for n in self.data.keys()) + + def __iter__(self): + return iter(self.data) + + def __next__(self): + return next(self.data) + """ + def search(self): #is search similar to get & get_name? + pass + """ + def __len__(self): + return len(self.data) + + def get_spans(self): + spans=[] + for field in self.data.values(): + spans.append(field.get_spans()) + return spans + + def apply_filter(self,filter_to_apply,dst): + pass + + def apply_index(self, index_to_apply, dest): + pass + + def sort_on(self,dest_group, keys, + timestamp=datetime.now(timezone.utc), write_mode='write', verbose=True): + pass + diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py new file mode 100644 index 00000000..0a966ccf --- /dev/null +++ b/tests/test_dataframe.py @@ -0,0 +1,47 @@ +import unittest +from io import BytesIO + +from exetera.core import session +from exetera.core import fields +from exetera.core import persistence as per +from exetera.core import dataframe + + +class TestDataFrame(unittest.TestCase): + + def test_dataframe_init(self): + bio=BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio,'w','dst') + numf = s.create_numeric(dst,'numf','int32') + #init + df = dataframe.DataFrame() + self.assertTrue(isinstance(df, dataframe.DataFrame)) + fdf = {'/numf',numf} + df2 = dataframe.DataFrame(fdf) + self.assertTrue(isinstance(df2,dataframe.DataFrame)) + #add & set & contains + df.add(numf) + self.assertTrue('/numf' in df) + self.assertTrue(df.contains_field(numf)) + cat=s.create_categorical(dst,'cat','int8',{'a':1,'b':2}) + self.assertFalse('/cat' in df) + self.assertFalse(df.contains_field(cat)) + df['/cat']=cat + self.assertTrue('/cat' in df) + #list & get + self.assertEqual(id(numf),id(df.get_field('/numf'))) + self.assertEqual(id(numf), id(df['/numf'])) + self.assertEqual('/numf',df.get_name(numf)) + #list & iter + dfit = iter(df) + self.assertEqual('/numf',next(dfit)) + self.assertEqual('/cat', next(dfit)) + #del & del by field + del df['/numf'] + self.assertFalse('/numf' in df) + df.delete_field(cat) + self.assertFalse(df.contains_field(cat)) + self.assertIsNone(df.get_name(cat)) + + From 223dbe9f82605dc7a811031ef4fce132a4156dda Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 25 Mar 2021 11:26:14 +0000 Subject: [PATCH 023/181] fix get_spans_for_2_fields_by_spans, fix the unittest --- exetera/core/dataframe.py | 125 ------------------------------------ exetera/core/operations.py | 16 ++--- exetera/core/persistence.py | 25 ++++---- tests/test_dataframe.py | 47 -------------- tests/test_operations.py | 14 ++++ tests/test_persistence.py | 2 +- 6 files changed, 33 insertions(+), 196 deletions(-) delete mode 100644 exetera/core/dataframe.py delete mode 100644 tests/test_dataframe.py diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py deleted file mode 100644 index 2eae4f1c..00000000 --- a/exetera/core/dataframe.py +++ /dev/null @@ -1,125 +0,0 @@ -from exetera.core import fields as fld -from datetime import datetime,timezone - -class DataFrame(): - """ - DataFrame is a table of data that contains a list of Fields (columns) - """ - def __init__(self, data=None): - if data is not None: - if isinstance(data,dict) and isinstance(list(a.items())[0][0],str) and isinstance(list(a.items())[0][1], fld.Field) : - self.data=data - self.data = dict() - - def add(self,field,name=None): - if name is not None: - if not isinstance(name,str): - raise TypeError("The name must be a str object.") - else: - self.data[name]=field - self.data[field.name]=field #note the name has '/' for hdf5 object - - def __contains__(self, name): - """ - check if dataframe contains a field, by the field name - name: the name of the field to check,return a bool - """ - if not isinstance(name,str): - raise TypeError("The name must be a str object.") - else: - return self.data.__contains__(name) - - def contains_field(self,field): - """ - check if dataframe contains a field by the field object - field: the filed object to check, return a tuple(bool,str). The str is the name stored in dataframe. - """ - if not isinstance(field, fld.Field): - raise TypeError("The field must be a Field object") - else: - for v in self.data.values(): - if id(field) == id(v): - return True - break - return False - - def __getitem__(self, name): - if not isinstance(name,str): - raise TypeError("The name must be a str object.") - elif not self.__contains__(name): - raise ValueError("Can not find the name from this dataframe.") - else: - return self.data[name] - - def get_field(self,name): - return self.__getitem__(name) - - def get_name(self,field): - """ - Get the name of the field in dataframe - """ - if not isinstance(field,fld.Field): - raise TypeError("The field argument must be a Field object.") - for name,v in self.data.items(): - if id(field) == id(v): - return name - break - return None - - def __setitem__(self, name, field): - if not isinstance(name,str): - raise TypeError("The name must be a str object.") - elif not isinstance(field,fld.Field): - raise TypeError("The field must be a Field object.") - else: - self.data[name]=field - return True - - def __delitem__(self, name): - if not self.__contains__(name=name): - raise ValueError("This dataframe does not contain the name to delete.") - else: - del self.data[name] - return True - - def delete_field(self,field): - """ - Remove field from dataframe by field - """ - name = self.get_name(field) - if name is None: - raise ValueError("This dataframe does not contain the field to delete.") - else: - self.__delitem__(name) - - def list(self): - return tuple(n for n in self.data.keys()) - - def __iter__(self): - return iter(self.data) - - def __next__(self): - return next(self.data) - """ - def search(self): #is search similar to get & get_name? - pass - """ - def __len__(self): - return len(self.data) - - def get_spans(self): - spans=[] - for field in self.data.values(): - spans.append(field.get_spans()) - return spans - - def apply_filter(self,filter_to_apply,dst): - pass - - def apply_index(self, index_to_apply, dest): - pass - - def sort_on(self,dest_group, keys, - timestamp=datetime.now(timezone.utc), write_mode='write', verbose=True): - pass - diff --git a/exetera/core/operations.py b/exetera/core/operations.py index 562f68e5..9b3caf13 100644 --- a/exetera/core/operations.py +++ b/exetera/core/operations.py @@ -217,18 +217,16 @@ def _get_spans_for_field(ndarray): results[-1] = True return np.nonzero(results)[0] -def _get_spans_for_2_fields_by_spans(ndarray0, ndarray1): - count = 0 +def _get_spans_for_2_fields_by_spans(span0, span1): spans = [] - span0 = _get_spans_for_field(ndarray0) - span1 = _get_spans_for_field(ndarray1) j=0 for i in range(len(span0)): - while j Date: Thu, 25 Mar 2021 23:16:42 +0000 Subject: [PATCH 024/181] Initial commit for is_sorted method on Field --- exetera/core/fields.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 601b3392..e726daa7 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -50,6 +50,9 @@ def timestamp(self): def chunksize(self): return self._field.attrs['chunksize'] + def is_sorted(self): + raise NotImplementedError() + def __bool__(self): # this method is required to prevent __len__ being called on derived methods when fields are queried as # if f: @@ -376,6 +379,17 @@ def data(self): self._data_wrapper = wrapper(self._field, 'index', 'values') return self._data_wrapper + def is_sorted(self): + if len(self) < 2: + return True + + indices = self.indices[:] + values = self.values[:] + for i in range(len(indices)-2): + if values[indices[i]:indices[i+1]] > values[indices[i+1]:indices[i+2]]: + return False + return True + @property def indices(self): if self._index_wrapper is None: @@ -416,6 +430,12 @@ def data(self): self._value_wrapper = ReadOnlyFieldArray(self._field, 'values') return self._value_wrapper + def is_sorted(self): + if len(self) < 2: + return True + data = self.data[:] + return np.any(np.char.compare_chararrays(data[:-1], data[1:], ">")) + def __len__(self): return len(self.data) @@ -442,6 +462,12 @@ def data(self): self._value_wrapper = ReadOnlyFieldArray(self._field, 'values') return self._value_wrapper + def is_sorted(self): + if len(self) < 2: + return True + data = self.data[:] + return data[:-1] > data[1:] + def __len__(self): return len(self.data) @@ -471,6 +497,12 @@ def data(self): self._value_wrapper = ReadOnlyFieldArray(self._field, 'values') return self._value_wrapper + def is_sorted(self): + if len(self) < 2: + return True + data = self.data[:] + return data[:-1] > data[1:] + def __len__(self): return len(self.data) @@ -505,6 +537,12 @@ def data(self): self._value_wrapper = ReadOnlyFieldArray(self._field, 'values') return self._value_wrapper + def is_sorted(self): + if len(self) < 2: + return True + data = self.data[:] + return data[:-1] > data[1:] + def __len__(self): return len(self.data) From 37b8ac2e6a3fb5538397dbcda3ea594a6a16c94f Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 26 Mar 2021 10:50:29 +0000 Subject: [PATCH 025/181] minor edits for the pr --- exetera/core/fields.py | 15 ++++++++++++--- exetera/core/session.py | 18 +++++++++--------- tests/test_session.py | 6 +----- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 31d30b9a..290d15c2 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -5,9 +5,8 @@ import h5py from exetera.core.data_writer import DataWriter -from exetera.core import utils -from exetera.core import persistence as per from exetera.core import operations as ops +from exetera.core import validation as val # def test_field_iterator(data): @@ -59,7 +58,9 @@ def __bool__(self): return True def get_spans(self): - pass + raise NotImplementedError("Please use get_spans() on specific fields, not the field base class.") + + @@ -397,6 +398,14 @@ def __len__(self): def get_spans(self): return ops._get_spans_for_index_string_field(self.indices[:], self.values[:]) + def apply_filter(self,filter_to_apply): + pass + + def apply_index(self,index_to_apply): + dest_indices, dest_values = \ + ops.apply_indices_to_index_values(index_to_apply, + self.indices[:], self.values[:]) + return dest_indices, dest_values diff --git a/exetera/core/session.py b/exetera/core/session.py index d5601e3e..fd1aef43 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -298,17 +298,17 @@ def apply_index(self, index_to_apply, src, dest=None): if dest is not None: writer_ = val.field_from_parameter(self, 'writer', dest) if isinstance(src, fld.IndexedStringField): - src_ = val.field_from_parameter(self, 'reader', src) - dest_indices, dest_values =\ - ops.apply_indices_to_index_values(index_to_apply_, - src_.indices[:], src_.values[:]) + dest_indices, dest_values = src.apply_index(index_to_apply_) if writer_ is not None: writer_.indices.write(dest_indices) writer_.values.write(dest_values) return dest_indices, dest_values - else: - reader_ = val.array_from_parameter(self, 'reader', src) - result = reader_[index_to_apply] + + elif isinstance(src,fld.Field): + src.app + elif isinstance(src,np.ndarray): + #reader_ = val.array_from_parameter(self, 'reader', src) + result = src[index_to_apply] if writer_: writer_.data.write(result) return result @@ -639,8 +639,8 @@ def create_like(self, field, dest_group, dest_name, timestamp=None, chunksize=No def create_indexed_string(self, group, name, timestamp=None, chunksize=None): - fld.indexed_string_field_constructor(self, group, name, timestamp, chunksize) #create the hdf5 object - return fld.IndexedStringField(self, group[name], write_enabled=True) #return the field wrapper + fld.indexed_string_field_constructor(self, group, name, timestamp, chunksize) + return fld.IndexedStringField(self, group[name], write_enabled=True) def create_fixed_string(self, group, name, length, timestamp=None, chunksize=None): diff --git a/tests/test_session.py b/tests/test_session.py index 2df43e4f..490d0f75 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -879,8 +879,4 @@ def test_date_importer(self): self.assertListEqual( [datetime(year=int(v[0:4]), month=int(v[5:7]), day=int(v[8:10]) ).timestamp() for v in values], - f.data[:].tolist()) - - -if __name__ == '__main__': - a=TestSessionSort().test_dataset_sort_index_ndarray() \ No newline at end of file + f.data[:].tolist()) \ No newline at end of file From 0369c92263dca270add99f343dec5ff41c5851e5 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 26 Mar 2021 10:58:04 +0000 Subject: [PATCH 026/181] fix minor edit error for pr --- exetera/core/session.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/exetera/core/session.py b/exetera/core/session.py index fd1aef43..6fc76d9b 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -298,17 +298,17 @@ def apply_index(self, index_to_apply, src, dest=None): if dest is not None: writer_ = val.field_from_parameter(self, 'writer', dest) if isinstance(src, fld.IndexedStringField): - dest_indices, dest_values = src.apply_index(index_to_apply_) + src_ = val.field_from_parameter(self, 'reader', src) + dest_indices, dest_values = \ + ops.apply_indices_to_index_values(index_to_apply_, + src_.indices[:], src_.values[:]) if writer_ is not None: writer_.indices.write(dest_indices) writer_.values.write(dest_values) return dest_indices, dest_values - - elif isinstance(src,fld.Field): - src.app - elif isinstance(src,np.ndarray): - #reader_ = val.array_from_parameter(self, 'reader', src) - result = src[index_to_apply] + else: + reader_ = val.array_from_parameter(self, 'reader', src) + result = reader_[index_to_apply] if writer_: writer_.data.write(result) return result From f21324043d9c49d914a5914276d02f074635daca Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 26 Mar 2021 12:05:18 +0000 Subject: [PATCH 027/181] add apply_index and apply_filter methods on fields --- exetera/core/fields.py | 80 +++++++++++++++++++++++++++++++++++++---- exetera/core/session.py | 21 +++++------ 2 files changed, 83 insertions(+), 18 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 290d15c2..1313eb37 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -60,6 +60,11 @@ def __bool__(self): def get_spans(self): raise NotImplementedError("Please use get_spans() on specific fields, not the field base class.") + def apply_filter(self, filter_to_apply, dstfld=None): + raise NotImplementedError("Please use apply_filter() on specific fields, not the field base class.") + + def apply_index(self, index_to_apply, dstfld=None): + raise NotImplementedError("Please use apply_index() on specific fields, not the field base class.") @@ -398,13 +403,22 @@ def __len__(self): def get_spans(self): return ops._get_spans_for_index_string_field(self.indices[:], self.values[:]) - def apply_filter(self,filter_to_apply): - pass + def apply_filter(self,filter_to_apply,detfld=None): + dest_indices, dest_values = \ + ops.apply_filter_to_index_values(filter_to_apply, + self.indices[:], self.values[:]) + if detfld is not None: + detfld.indices.write(dest_indices) + detfld.values.write(dest_values) + return dest_indices, dest_values - def apply_index(self,index_to_apply): + def apply_index(self,index_to_apply,detfld=None): dest_indices, dest_values = \ ops.apply_indices_to_index_values(index_to_apply, self.indices[:], self.values[:]) + if detfld is not None: + detfld.indices.write(dest_indices) + detfld.values.write(dest_values) return dest_indices, dest_values @@ -437,6 +451,20 @@ def __len__(self): def get_spans(self): return ops._get_spans_for_field(self.data[:]) + def apply_filter(self, filter_to_apply, dstfld=None): + array = self.data[:] + result = array[filter_to_apply] + if dstfld is not None: + dstfld.data.write(result) + return result + + def apply_index(self, index_to_apply, dstfld=None): + array = self.data[:] + result = array[index_to_apply] + if dstfld is not None: + dstfld.data.write(result) + return result + class NumericField(Field): def __init__(self, session, group, name=None, write_enabled=False): @@ -466,6 +494,19 @@ def __len__(self): def get_spans(self): return ops._get_spans_for_field(self.data[:]) + def apply_filter(self,filter_to_apply,dstfld=None): + array = self.data[:] + result = array[filter_to_apply] + if dstfld is not None: + dstfld.data.write(result) + return result + + def apply_index(self,index_to_apply,dstfld=None): + array = self.data[:] + result = array[index_to_apply] + if dstfld is not None: + dstfld.data.write(result) + return result class CategoricalField(Field): def __init__(self, session, group, @@ -495,9 +536,6 @@ def data(self): def __len__(self): return len(self.data) - def get_spans(self): - return ops._get_spans_for_field(self.data[:]) - # Note: key is presented as value: str, even though the dictionary must be presented # as str: value @property @@ -507,6 +545,22 @@ def keys(self): keys = dict(zip(kv, kn)) return keys + def get_spans(self): + return ops._get_spans_for_field(self.data[:]) + + def apply_filter(self, filter_to_apply, dstfld=None): + array = self.data[:] + result = array[filter_to_apply] + if dstfld is not None: + dstfld.data.write(result) + return result + + def apply_index(self, index_to_apply, dstfld=None): + array = self.data[:] + result = array[index_to_apply] + if dstfld is not None: + dstfld.data.write(result) + return result class TimestampField(Field): def __init__(self, session, group, name=None, write_enabled=False): @@ -535,6 +589,20 @@ def __len__(self): def get_span(self): return ops._get_spans_for_field(self.data[:]) + def apply_filter(self, filter_to_apply, dstfld=None): + array = self.data[:] + result = array[filter_to_apply] + if dstfld is not None: + dstfld.data.write(result) + return result + + def apply_index(self, index_to_apply, dstfld=None): + array = self.data[:] + result = array[index_to_apply] + if dstfld is not None: + dstfld.data.write(result) + return result + class IndexedStringImporter: def __init__(self, session, group, name, timestamp=None, chunksize=None): diff --git a/exetera/core/session.py b/exetera/core/session.py index 6fc76d9b..0e739917 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -266,14 +266,12 @@ def apply_filter(self, filter_to_apply, src, dest=None): if dest is not None: writer_ = val.field_from_parameter(self, 'writer', dest) if isinstance(src, fld.IndexedStringField): - src_ = val.field_from_parameter(self, 'reader', src) - dest_indices, dest_values =\ - ops.apply_filter_to_index_values(filter_to_apply_, - src_.indices[:], src_.values[:]) - if writer_ is not None: - writer_.indices.write(dest_indices) - writer_.values.write(dest_values) + dest_indices, dest_values = src.apply_filter(filter_to_apply_,writer_) return dest_indices, dest_values + elif isinstance(src,fld.Field): + result = src.apply_filter(filter_to_apply_,writer_) + return result + #elif isinstance(src, df.datafrme): else: reader_ = val.array_from_parameter(self, 'reader', src) result = reader_[filter_to_apply] @@ -298,14 +296,13 @@ def apply_index(self, index_to_apply, src, dest=None): if dest is not None: writer_ = val.field_from_parameter(self, 'writer', dest) if isinstance(src, fld.IndexedStringField): - src_ = val.field_from_parameter(self, 'reader', src) dest_indices, dest_values = \ ops.apply_indices_to_index_values(index_to_apply_, - src_.indices[:], src_.values[:]) - if writer_ is not None: - writer_.indices.write(dest_indices) - writer_.values.write(dest_values) + src.indices[:], src.values[:]) return dest_indices, dest_values + elif isinstance(src,fld.Field): + result = src.apply_index(index_to_apply_,writer_) + return result else: reader_ = val.array_from_parameter(self, 'reader', src) result = reader_[index_to_apply] From fe36b94d73d5454203d33dd9eee89b398013eff5 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Fri, 26 Mar 2021 17:48:01 +0000 Subject: [PATCH 028/181] Adding in missing tests for all field types for is_sorted --- exetera/core/fields.py | 8 ++--- tests/test_fields.py | 68 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index fd09a386..bf5a5a35 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -443,7 +443,7 @@ def is_sorted(self): if len(self) < 2: return True data = self.data[:] - return np.any(np.char.compare_chararrays(data[:-1], data[1:], ">")) + return np.all(np.char.compare_chararrays(data[:-1], data[1:], "<=", False)) def __len__(self): return len(self.data) @@ -478,7 +478,7 @@ def is_sorted(self): if len(self) < 2: return True data = self.data[:] - return data[:-1] > data[1:] + return np.all(data[:-1] <= data[1:]) def __len__(self): return len(self.data) @@ -516,7 +516,7 @@ def is_sorted(self): if len(self) < 2: return True data = self.data[:] - return data[:-1] > data[1:] + return np.all(data[:-1] <= data[1:]) def __len__(self): return len(self.data) @@ -559,7 +559,7 @@ def is_sorted(self): if len(self) < 2: return True data = self.data[:] - return data[:-1] > data[1:] + return np.all(data[:-1] <= data[1:]) def __len__(self): return len(self.data) diff --git a/tests/test_fields.py b/tests/test_fields.py index 60e13da4..8503a02e 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -25,6 +25,9 @@ def test_field_truthness(self): f = s.create_categorical(src, "d", "int8", {"no": 0, "yes": 1}) self.assertTrue(bool(f)) + +class TestFieldGetSpans(unittest.TestCase): + def test_get_spans(self): ''' Here test only the numeric field, categorical field and fixed string field. @@ -66,6 +69,70 @@ def test_indexed_string_is_sorted(self): f2.data.write(svals) self.assertTrue(f2.is_sorted()) + def test_fixed_string_is_sorted(self): + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + + f = s.create_fixed_string(ds, 'f', 5) + vals = ['a', 'ba', 'bb', 'bac', 'de', 'ddddd', 'deff', 'aaaa', 'ccd'] + f.data.write([v.encode() for v in vals]) + self.assertFalse(f.is_sorted()) + + f2 = s.create_fixed_string(ds, 'f2', 5) + svals = sorted(vals) + f2.data.write([v.encode() for v in svals]) + self.assertTrue(f2.is_sorted()) + + def test_numeric_is_sorted(self): + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + + f = s.create_numeric(ds, 'f', 'int32') + vals = [74, 1897, 298, 0, -100098, 380982340, 8, 6587, 28421, 293878] + f.data.write(vals) + self.assertFalse(f.is_sorted()) + + f2 = s.create_numeric(ds, 'f2', 'int32') + svals = sorted(vals) + f2.data.write(svals) + self.assertTrue(f2.is_sorted()) + + def test_categorical_is_sorted(self): + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + + f = s.create_categorical(ds, 'f', 'int8', {'a': 0, 'c': 1, 'd': 2, 'b': 3}) + vals = [0, 1, 3, 2, 3, 2, 2, 0, 0, 1, 2] + f.data.write(vals) + self.assertFalse(f.is_sorted()) + + f2 = s.create_categorical(ds, 'f2', 'int8', {'a': 0, 'c': 1, 'd': 2, 'b': 3}) + svals = sorted(vals) + f2.data.write(svals) + self.assertTrue(f2.is_sorted()) + + def test_timestamp_is_sorted(self): + from datetime import datetime as D + from datetime import timedelta as T + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + + f = s.create_timestamp(ds, 'f') + d = D(2020, 5, 10) + vals = [d + T(seconds=50000), d - T(days=280), d + T(weeks=2), d + T(weeks=250), + d - T(weeks=378), d + T(hours=2897), d - T(days=23), d + T(minutes=39873)] + vals = [v.timestamp() for v in vals] + f.data.write(vals) + self.assertFalse(f.is_sorted()) + + f2 = s.create_timestamp(ds, 'f2') + svals = sorted(vals) + f2.data.write(svals) + self.assertTrue(f2.is_sorted()) class TestIndexedStringFields(unittest.TestCase): @@ -94,7 +161,6 @@ def test_create_indexed_string(self): # print(f2.data[1]) self.assertEqual('ccc', f2.data[1]) - def test_update_legacy_indexed_string_that_has_uint_values(self): bio = BytesIO() From daa6012f5e2b6d05e67ed6308505bc4ab2d2e1be Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 26 Mar 2021 19:43:58 +0000 Subject: [PATCH 029/181] update the apply filter and apply index on Fields --- exetera/core/fields.py | 100 +++++++++++++++++++++++++--------------- exetera/core/session.py | 12 ++--- 2 files changed, 69 insertions(+), 43 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 24972d84..092c6214 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -144,7 +144,7 @@ def __setitem__(self, key, value): def clear(self): nformat = self._dataset.dtype DataWriter._clear_dataset(self._field, self._name) - DataWriter.write(self._field, 'values', [], 0, nformat) + DataWriter.write(self._field, self._name, [], 0, nformat) self._dataset = self._field[self._name] def write_part(self, part): @@ -423,23 +423,33 @@ def __len__(self): def get_spans(self): return ops._get_spans_for_index_string_field(self.indices[:], self.values[:]) - def apply_filter(self,filter_to_apply,detfld=None): + def apply_filter(self,filter_to_apply,dstfld=None): + """ + Apply a filter (array of boolean) to the field, return itself if destination field (detfld) is not set. + """ dest_indices, dest_values = \ ops.apply_filter_to_index_values(filter_to_apply, self.indices[:], self.values[:]) - if detfld is not None: - detfld.indices.write(dest_indices) - detfld.values.write(dest_values) - return dest_indices, dest_values + newfld = dstfld if dstfld is not None else self + newfld.indices.clear() + newfld.indices.write(dest_indices) + newfld.values.clear() + newfld.values.write(dest_values) + return newfld - def apply_index(self,index_to_apply,detfld=None): + def apply_index(self,index_to_apply,dstfld=None): + """ + Reindex the current field, return itself if destination field is not set. + """ dest_indices, dest_values = \ ops.apply_indices_to_index_values(index_to_apply, self.indices[:], self.values[:]) - if detfld is not None: - detfld.indices.write(dest_indices) - detfld.values.write(dest_values) - return dest_indices, dest_values + newfld = dstfld if dstfld is not None else self + newfld.indices.clear() + newfld.indices.write(dest_indices) + newfld.values.clear() + newfld.values.write(dest_values) + return newfld class FixedStringField(HDF5Field): @@ -473,16 +483,20 @@ def get_spans(self): def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - if dstfld is not None: - dstfld.data.write(result) - return result + newfld = dstfld if dstfld is not None else self + if newfld._write_enabled == False: + newfld = newfld.writeable() + newfld.data[:] = result + return newfld def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - if dstfld is not None: - dstfld.data.write(result) - return result + newfld = dstfld if dstfld is not None else self + if newfld._write_enabled == False: + newfld = newfld.writeable() + newfld.data[:] = result + return newfld class NumericField(HDF5Field): @@ -513,19 +527,23 @@ def __len__(self): def get_spans(self): return ops.get_spans_for_field(self.data[:]) - def apply_filter(self,filter_to_apply,dstfld=None): + def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - if dstfld is not None: - dstfld.data.write(result) - return result + newfld = dstfld if dstfld is not None else self + if newfld._write_enabled == False: + newfld = newfld.writeable() + newfld.data[:] = result + return newfld - def apply_index(self,index_to_apply,dstfld=None): + def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - if dstfld is not None: - dstfld.data.write(result) - return result + newfld = dstfld if dstfld is not None else self + if newfld._write_enabled == False: + newfld = newfld.writeable() + newfld.data[:] = result + return newfld class CategoricalField(HDF5Field): @@ -574,16 +592,20 @@ def get_spans(self): def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - if dstfld is not None: - dstfld.data.write(result) - return result + newfld = dstfld if dstfld is not None else self + if newfld._write_enabled == False: + newfld = newfld.writeable() + newfld.data[:] = result + return newfld def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - if dstfld is not None: - dstfld.data.write(result) - return result + newfld = dstfld if dstfld is not None else self + if newfld._write_enabled == False: + newfld = newfld.writeable() + newfld.data[:] = result + return newfld class TimestampField(HDF5Field): def __init__(self, session, group, name=None, write_enabled=False): @@ -612,16 +634,20 @@ def __len__(self): def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - if dstfld is not None: - dstfld.data.write(result) - return result + newfld = dstfld if dstfld is not None else self + if newfld._write_enabled == False: + newfld = newfld.writeable() + newfld.data[:] = result + return newfld def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - if dstfld is not None: - dstfld.data.write(result) - return result + newfld = dstfld if dstfld is not None else self + if newfld._write_enabled == False: + newfld = newfld.writeable() + newfld.data[:] = result + return newfld diff --git a/exetera/core/session.py b/exetera/core/session.py index 3a0e08dd..a8fd7f87 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -267,11 +267,11 @@ def apply_filter(self, filter_to_apply, src, dest=None): if dest is not None: writer_ = val.field_from_parameter(self, 'writer', dest) if isinstance(src, fld.IndexedStringField): - dest_indices, dest_values = src.apply_filter(filter_to_apply_,writer_) - return dest_indices, dest_values + newfld = src.apply_filter(filter_to_apply_,writer_) + return newfld.indices, newfld.values elif isinstance(src,fld.Field): - result = src.apply_filter(filter_to_apply_,writer_) - return result + newfld = src.apply_filter(filter_to_apply_,writer_) + return newfld.data[:] #elif isinstance(src, df.datafrme): else: reader_ = val.array_from_parameter(self, 'reader', src) @@ -302,8 +302,8 @@ def apply_index(self, index_to_apply, src, dest=None): src.indices[:], src.values[:]) return dest_indices, dest_values elif isinstance(src,fld.Field): - result = src.apply_index(index_to_apply_,writer_) - return result + newfld = src.apply_index(index_to_apply_,writer_) + return newfld.data[:] else: reader_ = val.array_from_parameter(self, 'reader', src) result = reader_[index_to_apply] From 5c43f383f8c55326837e88c84ae8f8e27dcc856a Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 26 Mar 2021 20:08:19 +0000 Subject: [PATCH 030/181] minor updates to line up w/ upstream --- exetera/core/fields.py | 21 +++------------------ exetera/core/operations.py | 1 - 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 092c6214..b3503ba1 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -11,24 +11,6 @@ from exetera.core import operations as ops from exetera.core import validation as val - -# def test_field_iterator(data): -# @numba.njit -# def _inner(): -# for d in data: -# yield d -# return _inner() -# -# iterator_type = numba.from_dtype(test_field_iterator) -# -# @numba.jit -# def sum_iterator(iter_): -# total = np.int64(0) -# for i in iter_: -# total += i -# return total - - class HDF5Field(Field): def __init__(self, session, group, name=None, write_enabled=False): super().__init__() @@ -631,6 +613,9 @@ def data(self): def __len__(self): return len(self.data) + def get_spans(self): + return ops.get_spans_for_field(self.data[:]) + def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] diff --git a/exetera/core/operations.py b/exetera/core/operations.py index 7f495694..6d71f8e1 100644 --- a/exetera/core/operations.py +++ b/exetera/core/operations.py @@ -206,7 +206,6 @@ def apply_indices_to_index_values(indices_to_apply, indices, values): return dest_indices, dest_values - def get_spans_for_field(ndarray): results = np.zeros(len(ndarray) + 1, dtype=np.bool) if np.issubdtype(ndarray.dtype, np.number): From 459b91c4bdbe57c4d8276426aed14e0aca551310 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 26 Mar 2021 20:36:54 +0000 Subject: [PATCH 031/181] update apply filter & apply index methods in fields that differ if destination field is set: if set, use dstfld.write because new field usually empty; if not set, write to self using fld.data[:] --- exetera/core/fields.py | 168 ++++++++++++++++++++++++++++------------- 1 file changed, 115 insertions(+), 53 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index b3503ba1..1e2b560b 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -412,12 +412,20 @@ def apply_filter(self,filter_to_apply,dstfld=None): dest_indices, dest_values = \ ops.apply_filter_to_index_values(filter_to_apply, self.indices[:], self.values[:]) - newfld = dstfld if dstfld is not None else self - newfld.indices.clear() - newfld.indices.write(dest_indices) - newfld.values.clear() - newfld.values.write(dest_values) - return newfld + + if dstfld is not None: + if not dstfld._write_enabled: + dstfld=dstfld.writeable() + dstfld.indices.write(dest_indices) + dstfld.values.write(dest_values) + else: + if not self._write_enabled: + dstfld=self.writeable() + else: + dstfld=self + dstfld.indices[:] = dest_indices + dstfld.values[:] = dest_values + return dstfld def apply_index(self,index_to_apply,dstfld=None): """ @@ -426,12 +434,19 @@ def apply_index(self,index_to_apply,dstfld=None): dest_indices, dest_values = \ ops.apply_indices_to_index_values(index_to_apply, self.indices[:], self.values[:]) - newfld = dstfld if dstfld is not None else self - newfld.indices.clear() - newfld.indices.write(dest_indices) - newfld.values.clear() - newfld.values.write(dest_values) - return newfld + if dstfld is not None: + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + dstfld.indices.write(dest_indices) + dstfld.values.write(dest_values) + else: + if not self._write_enabled: + dstfld = self.writeable() + else: + dstfld = self + dstfld.indices[:] = dest_indices + dstfld.values[:] = dest_values + return dstfld class FixedStringField(HDF5Field): @@ -465,20 +480,32 @@ def get_spans(self): def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - newfld = dstfld if dstfld is not None else self - if newfld._write_enabled == False: - newfld = newfld.writeable() - newfld.data[:] = result - return newfld + if dstfld is not None: + if not dstfld._write_enabled: + dstfld=dstfld.writeable() + dstfld.data.write(result) + else: + if not self._write_enabled: + dstfld=self.writeable() + else: + dstfld=self + dstfld.data[:] = result + return dstfld def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - newfld = dstfld if dstfld is not None else self - if newfld._write_enabled == False: - newfld = newfld.writeable() - newfld.data[:] = result - return newfld + if dstfld is not None: + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + dstfld.data.write(result) + else: + if not self._write_enabled: + dstfld = self.writeable() + else: + dstfld = self + dstfld.data[:] = result + return dstfld class NumericField(HDF5Field): @@ -512,21 +539,32 @@ def get_spans(self): def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - newfld = dstfld if dstfld is not None else self - if newfld._write_enabled == False: - newfld = newfld.writeable() - newfld.data[:] = result - return newfld + if dstfld is not None: + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + dstfld.data.write(result) + else: + if not self._write_enabled: + dstfld = self.writeable() + else: + dstfld = self + dstfld.data[:] = result + return dstfld def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - newfld = dstfld if dstfld is not None else self - if newfld._write_enabled == False: - newfld = newfld.writeable() - newfld.data[:] = result - return newfld - + if dstfld is not None: + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + dstfld.data.write(result) + else: + if not self._write_enabled: + dstfld = self.writeable() + else: + dstfld = self + dstfld.data[:] = result + return dstfld class CategoricalField(HDF5Field): def __init__(self, session, group, @@ -574,20 +612,32 @@ def get_spans(self): def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - newfld = dstfld if dstfld is not None else self - if newfld._write_enabled == False: - newfld = newfld.writeable() - newfld.data[:] = result - return newfld + if dstfld is not None: + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + dstfld.data.write(result) + else: + if not self._write_enabled: + dstfld = self.writeable() + else: + dstfld = self + dstfld.data[:] = result + return dstfld def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - newfld = dstfld if dstfld is not None else self - if newfld._write_enabled == False: - newfld = newfld.writeable() - newfld.data[:] = result - return newfld + if dstfld is not None: + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + dstfld.data.write(result) + else: + if not self._write_enabled: + dstfld = self.writeable() + else: + dstfld = self + dstfld.data[:] = result + return dstfld class TimestampField(HDF5Field): def __init__(self, session, group, name=None, write_enabled=False): @@ -619,20 +669,32 @@ def get_spans(self): def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - newfld = dstfld if dstfld is not None else self - if newfld._write_enabled == False: - newfld = newfld.writeable() - newfld.data[:] = result - return newfld + if dstfld is not None: + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + dstfld.data.write(result) + else: + if not self._write_enabled: + dstfld = self.writeable() + else: + dstfld = self + dstfld.data[:] = result + return dstfld def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - newfld = dstfld if dstfld is not None else self - if newfld._write_enabled == False: - newfld = newfld.writeable() - newfld.data[:] = result - return newfld + if dstfld is not None: + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + dstfld.data.write(result) + else: + if not self._write_enabled: + dstfld = self.writeable() + else: + dstfld = self + dstfld.data[:] = result + return dstfld From c0ac9606a0f3a9d2d11dbb67b8b3954b64bb8134 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 29 Mar 2021 10:53:17 +0100 Subject: [PATCH 032/181] updated the apply_index and apply_filter methods in fields. Use olddata[:]=newdata if length of old dataset is equals to new dataset; clear() and write() data if not. --- exetera/core/fields.py | 171 +++++++++++++++++++---------------------- 1 file changed, 79 insertions(+), 92 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 1e2b560b..8cd7c340 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -413,18 +413,19 @@ def apply_filter(self,filter_to_apply,dstfld=None): ops.apply_filter_to_index_values(filter_to_apply, self.indices[:], self.values[:]) - if dstfld is not None: - if not dstfld._write_enabled: - dstfld=dstfld.writeable() + dstfld = self if dstfld is None else dstfld + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + if len(dstfld.indices) == len(dest_indices): + dstfld.indices[:] = dest_indices + else: + dstfld.indices.clear() dstfld.indices.write(dest_indices) - dstfld.values.write(dest_values) + if len(dstfld.values) == len(dest_values): + dstfld.values[:]=dest_values else: - if not self._write_enabled: - dstfld=self.writeable() - else: - dstfld=self - dstfld.indices[:] = dest_indices - dstfld.values[:] = dest_values + dstfld.values.clear() + dstfld.values.write(dest_values) return dstfld def apply_index(self,index_to_apply,dstfld=None): @@ -434,18 +435,19 @@ def apply_index(self,index_to_apply,dstfld=None): dest_indices, dest_values = \ ops.apply_indices_to_index_values(index_to_apply, self.indices[:], self.values[:]) - if dstfld is not None: - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - dstfld.indices.write(dest_indices) - dstfld.values.write(dest_values) - else: - if not self._write_enabled: - dstfld = self.writeable() - else: - dstfld = self + dstfld = self if dstfld is None else dstfld + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + if len(dstfld.indices) == len(dest_indices): dstfld.indices[:] = dest_indices + else: + dstfld.indices.clear() + dstfld.indices.write(dest_indices) + if len(dstfld.values) == len(dest_values): dstfld.values[:] = dest_values + else: + dstfld.values.clear() + dstfld.values.write(dest_values) return dstfld @@ -480,31 +482,28 @@ def get_spans(self): def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - if dstfld is not None: - if not dstfld._write_enabled: - dstfld=dstfld.writeable() - dstfld.data.write(result) - else: - if not self._write_enabled: - dstfld=self.writeable() - else: - dstfld=self + dstfld = self if dstfld is None else dstfld + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + if len(dstfld.data) == len(result): dstfld.data[:] = result + else: + dstfld.data.clear() + dstfld.data.write(result) return dstfld + def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - if dstfld is not None: - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - dstfld.data.write(result) - else: - if not self._write_enabled: - dstfld = self.writeable() - else: - dstfld = self + dstfld = self if dstfld is None else dstfld + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + if len(dstfld.data) == len(result): dstfld.data[:] = result + else: + dstfld.data.clear() + dstfld.data.write(result) return dstfld @@ -539,31 +538,27 @@ def get_spans(self): def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - if dstfld is not None: - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - dstfld.data.write(result) - else: - if not self._write_enabled: - dstfld = self.writeable() - else: - dstfld = self + dstfld = self if dstfld is None else dstfld + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + if len(dstfld.data) == len(result): dstfld.data[:] = result + else: + dstfld.data.clear() + dstfld.data.write(result) return dstfld def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - if dstfld is not None: - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - dstfld.data.write(result) - else: - if not self._write_enabled: - dstfld = self.writeable() - else: - dstfld = self + dstfld = self if dstfld is None else dstfld + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + if len(dstfld.data) == len(result): dstfld.data[:] = result + else: + dstfld.data.clear() + dstfld.data.write(result) return dstfld class CategoricalField(HDF5Field): @@ -612,31 +607,27 @@ def get_spans(self): def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - if dstfld is not None: - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - dstfld.data.write(result) - else: - if not self._write_enabled: - dstfld = self.writeable() - else: - dstfld = self + dstfld = self if dstfld is None else dstfld + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + if len(dstfld.data) == len(result): dstfld.data[:] = result + else: + dstfld.data.clear() + dstfld.data.write(result) return dstfld def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - if dstfld is not None: - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - dstfld.data.write(result) - else: - if not self._write_enabled: - dstfld = self.writeable() - else: - dstfld = self + dstfld = self if dstfld is None else dstfld + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + if len(dstfld.data) == len(result): dstfld.data[:] = result + else: + dstfld.data.clear() + dstfld.data.write(result) return dstfld class TimestampField(HDF5Field): @@ -669,31 +660,27 @@ def get_spans(self): def apply_filter(self, filter_to_apply, dstfld=None): array = self.data[:] result = array[filter_to_apply] - if dstfld is not None: - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - dstfld.data.write(result) - else: - if not self._write_enabled: - dstfld = self.writeable() - else: - dstfld = self + dstfld = self if dstfld is None else dstfld + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + if len(dstfld.data) == len(result): dstfld.data[:] = result + else: + dstfld.data.clear() + dstfld.data.write(result) return dstfld def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] result = array[index_to_apply] - if dstfld is not None: - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - dstfld.data.write(result) - else: - if not self._write_enabled: - dstfld = self.writeable() - else: - dstfld = self + dstfld = self if dstfld is None else dstfld + if not dstfld._write_enabled: + dstfld = dstfld.writeable() + if len(dstfld.data) == len(result): dstfld.data[:] = result + else: + dstfld.data.clear() + dstfld.data.write(result) return dstfld From dd0867db1c23c782bcc0d312d38e0a4922d850aa Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 30 Mar 2021 14:08:30 +0100 Subject: [PATCH 033/181] add dataframe basic functions and operations; working on dataset to enable dataframe to create fields. --- exetera/core/csvdataset.py | 207 +++++++++++++++++++++++++++++++++++++ exetera/core/dataframe.py | 162 +++++++++++++++++++++++++++++ exetera/core/dataset.py | 207 ------------------------------------- tests/test_dataframe.py | 70 +++++++++++++ 4 files changed, 439 insertions(+), 207 deletions(-) create mode 100644 exetera/core/csvdataset.py create mode 100644 exetera/core/dataframe.py create mode 100644 tests/test_dataframe.py diff --git a/exetera/core/csvdataset.py b/exetera/core/csvdataset.py new file mode 100644 index 00000000..c23836cb --- /dev/null +++ b/exetera/core/csvdataset.py @@ -0,0 +1,207 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import csv +import time +import numpy as np + +from exetera.processing import numpy_buffer + + +class Dataset: + """ + field_descriptors: a dictionary of field names to field descriptors that describe how the field + should be transformed when loading + keys: a list of field names that represent the fields you wish to load and in what order they + should be put. Leaving this blankloads all of the keys in csv column order + """ + def __init__(self, source, field_descriptors=None, keys=None, filter_fn=None, + show_progress_every=False, start_from=None, stop_after=None, early_filter=None, + verbose=True): + + def print_if_verbose(*args): + if verbose: + print(*args) + + self.names_ = list() + self.fields_ = list() + self.names_ = list() + self.index_ = None + + csvf = csv.DictReader(source, delimiter=',', quotechar='"') + available_keys = csvf.fieldnames + + if not keys: + fields_to_use = available_keys + index_map = [i for i in range(len(fields_to_use))] + else: + fields_to_use = keys + index_map = [available_keys.index(k) for k in keys] + + early_key_index = None + if early_filter is not None: + if early_filter[0] not in available_keys: + raise ValueError( + f"'early_filter': tuple element zero must be a key that is in the dataset") + early_key_index = available_keys.index(early_filter[0]) + + tstart = time.time() + transforms_by_index = list() + new_fields = list() + + # build a full list of transforms by index whether they are are being filtered by 'keys' or not + for i_n, n in enumerate(available_keys): + if field_descriptors and n in field_descriptors and\ + field_descriptors[n].strings_to_values and\ + field_descriptors[n].out_of_range_label is None: + # transforms by csv field index + transforms_by_index.append(field_descriptors[n]) + else: + transforms_by_index.append(None) + + # build a new list of collections for every field that is to be loaded + for i_n in index_map: + if transforms_by_index[i_n] is not None: + to_datatype = transforms_by_index[i_n].to_datatype + if to_datatype == str: + new_fields.append(list()) + else: + new_fields.append(numpy_buffer.NumpyBuffer2(dtype=to_datatype)) + else: + new_fields.append(list()) + + # read the cvs rows into the fields + csvf = csv.reader(source, delimiter=',', quotechar='"') + ecsvf = iter(csvf) + filtered_count = 0 + for i_r, row in enumerate(ecsvf): + if show_progress_every: + if i_r % show_progress_every == 0: + if filtered_count == i_r: + print_if_verbose(i_r) + else: + print_if_verbose(f"{i_r} ({filtered_count})") + + if start_from is not None and i_r < start_from: + del row + continue + + # TODO: decide whether True means filter or not filter consistently + if early_filter is not None: + if not early_filter[1](row[early_key_index]): + continue + + # TODO: decide whether True means filter or not filter consistently + if not filter_fn or filter_fn(i_r): + # for i_f, f in enumerate(fields): + for i_df, i_f in enumerate(index_map): + f = row[i_f] + t = transforms_by_index[i_f] + try: + new_fields[i_df].append(f if not t else t.strings_to_values[f]) + except Exception as e: + msg = "{}: key error for value {} (permitted values are {}" + print_if_verbose(msg.format(fields_to_use[i_f], f, t.strings_to_values)) + del row + filtered_count += 1 + if stop_after and i_r >= stop_after: + break + + if show_progress_every: + print_if_verbose(f"{i_r} ({filtered_count})") + + # assign the built sequences to fields_ + for i_f, f in enumerate(new_fields): + if isinstance(f, list): + self.fields_.append(f) + else: + self.fields_.append(f.finalise()) + self.index_ = np.asarray([i for i in range(len(self.fields_[0]))], dtype=np.uint32) + self.names_ = fields_to_use + print_if_verbose('loading took', time.time() - tstart, "seconds") + + # if i > 0 and i % lines_per_dot == 0: + # if i % (lines_per_dot * newline_at) == 0: + # print(f'. {i}') + # else: + # print('.', end='') + # if i % (lines_per_dot * newline_at) != 0: + # print(f' {i}') + + def sort(self, keys): + #map names to indices + if isinstance(keys, str): + + def single_index_sort(index): + field = self.fields_[index] + + def inner_(r): + return field[r] + + return inner_ + self.index_ = sorted(self.index_, + key=single_index_sort(self.field_to_index(keys))) + else: + + kindices = [self.field_to_index(k) for k in keys] + + def index_sort(indices): + def inner_(r): + t = tuple(self.fields_[i][r] for i in indices) + return t + return inner_ + + self.index_ = sorted(self.index_, key=index_sort(kindices)) + + for i_f in range(len(self.fields_)): + unsorted_field = self.fields_[i_f] + self.fields_[i_f] = Dataset._apply_permutation(self.index_, unsorted_field) + del unsorted_field + + @staticmethod + def _apply_permutation(permutation, field): + # n = len(permutation) + # for i in range(0, n): + # print(i) + # pi = permutation[i] + # while pi < i: + # pi = permutation[pi] + # fields[i], fields[pi] = fields[pi], fields[i] + # return fields + if isinstance(field, list): + sorted_field = [None] * len(field) + for ip, p in enumerate(permutation): + sorted_field[ip] = field[p] + else: + sorted_field = np.empty_like(field) + for ip, p in enumerate(permutation): + sorted_field[ip] = field[p] + return sorted_field + + def field_by_name(self, field_name): + return self.fields_[self.field_to_index(field_name)] + + def field_to_index(self, field_name): + return self.names_.index(field_name) + + def value(self, row_index, field_index): + return self.fields_[field_index][row_index] + + def value_from_fieldname(self, index, field_name): + return self.fields_[self.field_to_index(field_name)][index] + + def row_count(self): + return len(self.index_) + + def show(self): + for ir, r in enumerate(self.names_): + print(f'{ir}-{r}') + \ No newline at end of file diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py new file mode 100644 index 00000000..6d9b4165 --- /dev/null +++ b/exetera/core/dataframe.py @@ -0,0 +1,162 @@ +from exetera.core import fields as fld +from datetime import datetime,timezone + +class DataFrame(): + """ + DataFrame is a table of data that contains a list of Fields (columns) + """ + def __init__(self, group ,data=None): + if data is not None: + if isinstance(data,dict) and isinstance(list(data.items())[0][0],str) and isinstance(list(data.items())[0][1], fld.Field) : + self.data=data + self.data = dict() + self.name=group + + def add(self,field,name=None): + if name is not None: + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + else: + self.data[name]=field + self.data[field.name]=field #note the name has '/' for hdf5 object + + def __contains__(self, name): + """ + check if dataframe contains a field, by the field name + name: the name of the field to check,return a bool + """ + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + else: + return self.data.__contains__(name) + + def contains_field(self,field): + """ + check if dataframe contains a field by the field object + field: the filed object to check, return a tuple(bool,str). The str is the name stored in dataframe. + """ + if not isinstance(field, fld.Field): + raise TypeError("The field must be a Field object") + else: + for v in self.data.values(): + if id(field) == id(v): + return True + break + return False + + def __getitem__(self, name): + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + elif not self.__contains__(name): + raise ValueError("Can not find the name from this dataframe.") + else: + return self.data[name] + + def get_field(self,name): + return self.__getitem__(name) + + def get_name(self,field): + """ + Get the name of the field in dataframe + """ + if not isinstance(field,fld.Field): + raise TypeError("The field argument must be a Field object.") + for name,v in self.data.items(): + if id(field) == id(v): + return name + break + return None + + def __setitem__(self, name, field): + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + elif not isinstance(field,fld.Field): + raise TypeError("The field must be a Field object.") + else: + self.data[name]=field + return True + + def __delitem__(self, name): + if not self.__contains__(name=name): + raise ValueError("This dataframe does not contain the name to delete.") + else: + del self.data[name] + return True + + def delete_field(self,field): + """ + Remove field from dataframe by field + """ + name = self.get_name(field) + if name is None: + raise ValueError("This dataframe does not contain the field to delete.") + else: + self.__delitem__(name) + + def list(self): + return tuple(n for n in self.data.keys()) + + def __iter__(self): + return iter(self.data) + + def __next__(self): + return next(self.data) + """ + def search(self): #is search similar to get & get_name? + pass + """ + def __len__(self): + return len(self.data) + + def get_spans(self): + """ + Return the name and spans of each field as a dictionary. + """ + spans={} + for name,field in self.data.items(): + spans[name]=field.get_spans() + return spans + + def apply_filter(self,filter_to_apply,ddf=None): + """ + Apply the filter to all the fields in this dataframe, return a dataframe with filtered fields. + + :param filter_to_apply: the filter to be applied to the source field, an array of boolean + :param ddf: optional- the destination data frame + :returns: a dataframe contains all the fields filterd, self if ddf is not set + """ + if ddf is not None: + if not isinstance(ddf,DataFrame): + raise TypeError("The destination object must be an instance of DataFrame.") + for name, field in self.data.items(): + # TODO integration w/ session, dataset + newfld = field.create_like(ddf.name,field.name) + ddf.add(field.apply_filter(filter_to_apply,dstfld=newfld),name=name) + return ddf + else: + for field in self.data.values(): + field.apply_filter(filter_to_apply) + return self + + + def apply_index(self, index_to_apply, ddf=None): + """ + Apply the index to all the fields in this dataframe, return a dataframe with indexed fields. + + :param index_to_apply: the index to be applied to the fields, an ndarray of integers + :param ddf: optional- the destination data frame + :returns: a dataframe contains all the fields re-indexed, self if ddf is not set + """ + if ddf is not None: + if not isinstance(ddf, DataFrame): + raise TypeError("The destination object must be an instance of DataFrame.") + for name, field in self.data.items(): + #TODO integration w/ session, dataset + newfld = field.create_like(ddf.name, field.name) + ddf.add(field.apply_index(index_to_apply,dstfld=newfld), name=name) + return ddf + else: + for field in self.data.values(): + field.apply_index(index_to_apply) + return self + diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index c23836cb..e69de29b 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -1,207 +0,0 @@ -# Copyright 2020 KCL-BMEIS - King's College London -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import csv -import time -import numpy as np - -from exetera.processing import numpy_buffer - - -class Dataset: - """ - field_descriptors: a dictionary of field names to field descriptors that describe how the field - should be transformed when loading - keys: a list of field names that represent the fields you wish to load and in what order they - should be put. Leaving this blankloads all of the keys in csv column order - """ - def __init__(self, source, field_descriptors=None, keys=None, filter_fn=None, - show_progress_every=False, start_from=None, stop_after=None, early_filter=None, - verbose=True): - - def print_if_verbose(*args): - if verbose: - print(*args) - - self.names_ = list() - self.fields_ = list() - self.names_ = list() - self.index_ = None - - csvf = csv.DictReader(source, delimiter=',', quotechar='"') - available_keys = csvf.fieldnames - - if not keys: - fields_to_use = available_keys - index_map = [i for i in range(len(fields_to_use))] - else: - fields_to_use = keys - index_map = [available_keys.index(k) for k in keys] - - early_key_index = None - if early_filter is not None: - if early_filter[0] not in available_keys: - raise ValueError( - f"'early_filter': tuple element zero must be a key that is in the dataset") - early_key_index = available_keys.index(early_filter[0]) - - tstart = time.time() - transforms_by_index = list() - new_fields = list() - - # build a full list of transforms by index whether they are are being filtered by 'keys' or not - for i_n, n in enumerate(available_keys): - if field_descriptors and n in field_descriptors and\ - field_descriptors[n].strings_to_values and\ - field_descriptors[n].out_of_range_label is None: - # transforms by csv field index - transforms_by_index.append(field_descriptors[n]) - else: - transforms_by_index.append(None) - - # build a new list of collections for every field that is to be loaded - for i_n in index_map: - if transforms_by_index[i_n] is not None: - to_datatype = transforms_by_index[i_n].to_datatype - if to_datatype == str: - new_fields.append(list()) - else: - new_fields.append(numpy_buffer.NumpyBuffer2(dtype=to_datatype)) - else: - new_fields.append(list()) - - # read the cvs rows into the fields - csvf = csv.reader(source, delimiter=',', quotechar='"') - ecsvf = iter(csvf) - filtered_count = 0 - for i_r, row in enumerate(ecsvf): - if show_progress_every: - if i_r % show_progress_every == 0: - if filtered_count == i_r: - print_if_verbose(i_r) - else: - print_if_verbose(f"{i_r} ({filtered_count})") - - if start_from is not None and i_r < start_from: - del row - continue - - # TODO: decide whether True means filter or not filter consistently - if early_filter is not None: - if not early_filter[1](row[early_key_index]): - continue - - # TODO: decide whether True means filter or not filter consistently - if not filter_fn or filter_fn(i_r): - # for i_f, f in enumerate(fields): - for i_df, i_f in enumerate(index_map): - f = row[i_f] - t = transforms_by_index[i_f] - try: - new_fields[i_df].append(f if not t else t.strings_to_values[f]) - except Exception as e: - msg = "{}: key error for value {} (permitted values are {}" - print_if_verbose(msg.format(fields_to_use[i_f], f, t.strings_to_values)) - del row - filtered_count += 1 - if stop_after and i_r >= stop_after: - break - - if show_progress_every: - print_if_verbose(f"{i_r} ({filtered_count})") - - # assign the built sequences to fields_ - for i_f, f in enumerate(new_fields): - if isinstance(f, list): - self.fields_.append(f) - else: - self.fields_.append(f.finalise()) - self.index_ = np.asarray([i for i in range(len(self.fields_[0]))], dtype=np.uint32) - self.names_ = fields_to_use - print_if_verbose('loading took', time.time() - tstart, "seconds") - - # if i > 0 and i % lines_per_dot == 0: - # if i % (lines_per_dot * newline_at) == 0: - # print(f'. {i}') - # else: - # print('.', end='') - # if i % (lines_per_dot * newline_at) != 0: - # print(f' {i}') - - def sort(self, keys): - #map names to indices - if isinstance(keys, str): - - def single_index_sort(index): - field = self.fields_[index] - - def inner_(r): - return field[r] - - return inner_ - self.index_ = sorted(self.index_, - key=single_index_sort(self.field_to_index(keys))) - else: - - kindices = [self.field_to_index(k) for k in keys] - - def index_sort(indices): - def inner_(r): - t = tuple(self.fields_[i][r] for i in indices) - return t - return inner_ - - self.index_ = sorted(self.index_, key=index_sort(kindices)) - - for i_f in range(len(self.fields_)): - unsorted_field = self.fields_[i_f] - self.fields_[i_f] = Dataset._apply_permutation(self.index_, unsorted_field) - del unsorted_field - - @staticmethod - def _apply_permutation(permutation, field): - # n = len(permutation) - # for i in range(0, n): - # print(i) - # pi = permutation[i] - # while pi < i: - # pi = permutation[pi] - # fields[i], fields[pi] = fields[pi], fields[i] - # return fields - if isinstance(field, list): - sorted_field = [None] * len(field) - for ip, p in enumerate(permutation): - sorted_field[ip] = field[p] - else: - sorted_field = np.empty_like(field) - for ip, p in enumerate(permutation): - sorted_field[ip] = field[p] - return sorted_field - - def field_by_name(self, field_name): - return self.fields_[self.field_to_index(field_name)] - - def field_to_index(self, field_name): - return self.names_.index(field_name) - - def value(self, row_index, field_index): - return self.fields_[field_index][row_index] - - def value_from_fieldname(self, index, field_name): - return self.fields_[self.field_to_index(field_name)][index] - - def row_count(self): - return len(self.index_) - - def show(self): - for ir, r in enumerate(self.names_): - print(f'{ir}-{r}') - \ No newline at end of file diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py new file mode 100644 index 00000000..310c7c19 --- /dev/null +++ b/tests/test_dataframe.py @@ -0,0 +1,70 @@ +import unittest +from io import BytesIO +import numpy as np + +from exetera.core import session +from exetera.core import fields +from exetera.core import persistence as per +from exetera.core import dataframe + + +class TestDataFrame(unittest.TestCase): + + def test_dataframe_init(self): + bio=BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio,'w','dst') + numf = s.create_numeric(dst,'numf','int32') + #init + df = dataframe.DataFrame('dst') + self.assertTrue(isinstance(df, dataframe.DataFrame)) + fdf = {'/numf',numf} + df2 = dataframe.DataFrame('dst2',fdf) + self.assertTrue(isinstance(df2,dataframe.DataFrame)) + #add & set & contains + df.add(numf) + self.assertTrue('/numf' in df) + self.assertTrue(df.contains_field(numf)) + cat=s.create_categorical(dst,'cat','int8',{'a':1,'b':2}) + self.assertFalse('/cat' in df) + self.assertFalse(df.contains_field(cat)) + df['/cat']=cat + self.assertTrue('/cat' in df) + #list & get + self.assertEqual(id(numf),id(df.get_field('/numf'))) + self.assertEqual(id(numf), id(df['/numf'])) + self.assertEqual('/numf',df.get_name(numf)) + #list & iter + dfit = iter(df) + self.assertEqual('/numf',next(dfit)) + self.assertEqual('/cat', next(dfit)) + #del & del by field + del df['/numf'] + self.assertFalse('/numf' in df) + df.delete_field(cat) + self.assertFalse(df.contains_field(cat)) + self.assertIsNone(df.get_name(cat)) + + def test_dataframe_ops(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + df = dataframe.DataFrame('dst') + numf = s.create_numeric(dst, 'numf', 'int32') + numf.data.write([5,4,3,2,1]) + df.add(numf) + fst = s.create_fixed_string(dst,'fst',3) + fst.data.write([b'e',b'd',b'c',b'b',b'a']) + df.add(fst) + index=np.array([4,3,2,1,0]) + ddf = dataframe.DataFrame('dst2') + df.apply_index(index,ddf) + self.assertEqual([1,2,3,4,5],ddf.get_field('/numf').data[:].tolist()) + self.assertEqual([b'a',b'b',b'c',b'd',b'e'],ddf.get_field('/fst').data[:].tolist()) + + filter= np.array([True,True,False,False,True]) + df.apply_filter(filter) + self.assertEqual([1, 2, 5], df.get_field('/numf').data[:].tolist()) + self.assertEqual([b'a', b'b', b'e'], df.get_field('/fst').data[:].tolist()) + + From e52d8256d778025270e5b8ad8a24866cc8d98364 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 1 Apr 2021 11:15:34 +0100 Subject: [PATCH 034/181] add functions in dataframe add dataset class add functions in dataset move dataset module to csvdataset --- exetera/bin/add_imd.py | 2 +- exetera/bin/journaling_prototype.py | 2 +- exetera/core/__init__.py | 2 +- exetera/core/dataframe.py | 72 ++++++++---- exetera/core/dataset.py | 167 ++++++++++++++++++++++++++++ exetera/core/importer.py | 2 +- exetera/core/split.py | 8 +- tests/test_csvdataset.py | 161 +++++++++++++++++++++++++++ tests/test_dataframe.py | 50 +++++---- tests/test_dataset.py | 164 ++------------------------- tests/test_persistence.py | 10 +- 11 files changed, 431 insertions(+), 209 deletions(-) create mode 100644 tests/test_csvdataset.py diff --git a/exetera/bin/add_imd.py b/exetera/bin/add_imd.py index 6242e0e2..3c789250 100644 --- a/exetera/bin/add_imd.py +++ b/exetera/bin/add_imd.py @@ -11,7 +11,7 @@ from exetera.core import exporter, persistence, utils from exetera.core.persistence import DataStore from exetera.processing.nat_medicine_model import nature_medicine_model_1 -from exetera.core.dataset import Dataset +from exetera.core.csvdataset import Dataset # England # http://geoportal.statistics.gov.uk/datasets/index-of-multiple-deprivation-december-2019-lookup-in-england/data diff --git a/exetera/bin/journaling_prototype.py b/exetera/bin/journaling_prototype.py index 113aa450..1e437e98 100644 --- a/exetera/bin/journaling_prototype.py +++ b/exetera/bin/journaling_prototype.py @@ -6,7 +6,7 @@ import numpy as np -from exetera.core import dataset +from exetera.core import csvdataset from exetera.core import utils diff --git a/exetera/core/__init__.py b/exetera/core/__init__.py index 99cdcd83..5c4cc9a2 100644 --- a/exetera/core/__init__.py +++ b/exetera/core/__init__.py @@ -1,3 +1,3 @@ -from . import data_schema, data_writer, dataset, exporter, fields, filtered_field, importer, load_schema,\ +from . import data_schema, data_writer, csvdataset, exporter, fields, filtered_field, importer, load_schema,\ operations, persistence, readerwriter, regression, session, split, utils, validation diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 6d9b4165..a4221f58 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -1,24 +1,52 @@ from exetera.core import fields as fld from datetime import datetime,timezone +import h5py class DataFrame(): """ DataFrame is a table of data that contains a list of Fields (columns) """ - def __init__(self, group ,data=None): + def __init__(self, name, dataset,data=None,h5group:h5py.Group=None): + """ + Create a Dataframe object. + + :param name: name of the dataframe, or the group name in HDF5 + :param dataset: a dataset object, where this dataframe belongs to + :param dataframe: optional - replicate data from another dictionary + :param h5group: optional - acquire data from h5group object directly, the h5group needs to have a + h5group<-group-dataset structure, the group has a 'fieldtype' attribute + and the dataset is named 'values'. + """ + self.fields = dict() + self.name = name + self.dataset = dataset + if data is not None: if isinstance(data,dict) and isinstance(list(data.items())[0][0],str) and isinstance(list(data.items())[0][1], fld.Field) : - self.data=data - self.data = dict() - self.name=group + self.fields=data + elif h5group is not None and isinstance(h5group,h5py.Group): + fieldtype_map = { + 'indexedstring': fld.IndexedStringField, + 'fixedstring': fld.FixedStringField, + 'categorical': fld.CategoricalField, + 'boolean': fld.NumericField, + 'numeric': fld.NumericField, + 'datetime': fld.TimestampField, + 'date': fld.TimestampField, + 'timestamp': fld.TimestampField + } + for subg in h5group.keys(): + fieldtype = h5group[subg].attrs['fieldtype'].split(',')[0] + self.fields[subg] = fieldtype_map[fieldtype](self, h5group[subg]) + print(" ") def add(self,field,name=None): if name is not None: if not isinstance(name,str): raise TypeError("The name must be a str object.") else: - self.data[name]=field - self.data[field.name]=field #note the name has '/' for hdf5 object + self.fields[name]=field + self.fields[field.name]=field #note the name has '/' for hdf5 object def __contains__(self, name): """ @@ -28,7 +56,7 @@ def __contains__(self, name): if not isinstance(name,str): raise TypeError("The name must be a str object.") else: - return self.data.__contains__(name) + return self.fields.__contains__(name) def contains_field(self,field): """ @@ -38,7 +66,7 @@ def contains_field(self,field): if not isinstance(field, fld.Field): raise TypeError("The field must be a Field object") else: - for v in self.data.values(): + for v in self.fields.values(): if id(field) == id(v): return True break @@ -50,18 +78,18 @@ def __getitem__(self, name): elif not self.__contains__(name): raise ValueError("Can not find the name from this dataframe.") else: - return self.data[name] + return self.fields[name] def get_field(self,name): return self.__getitem__(name) def get_name(self,field): """ - Get the name of the field in dataframe + Get the name of the field in dataframe. """ if not isinstance(field,fld.Field): raise TypeError("The field argument must be a Field object.") - for name,v in self.data.items(): + for name,v in self.fields.items(): if id(field) == id(v): return name break @@ -73,14 +101,14 @@ def __setitem__(self, name, field): elif not isinstance(field,fld.Field): raise TypeError("The field must be a Field object.") else: - self.data[name]=field + self.fields[name]=field return True def __delitem__(self, name): if not self.__contains__(name=name): raise ValueError("This dataframe does not contain the name to delete.") else: - del self.data[name] + del self.fields[name] return True def delete_field(self,field): @@ -94,26 +122,26 @@ def delete_field(self,field): self.__delitem__(name) def list(self): - return tuple(n for n in self.data.keys()) + return tuple(n for n in self.fields.keys()) def __iter__(self): - return iter(self.data) + return iter(self.fields) def __next__(self): - return next(self.data) + return next(self.fields) """ def search(self): #is search similar to get & get_name? pass """ def __len__(self): - return len(self.data) + return len(self.fields) def get_spans(self): """ Return the name and spans of each field as a dictionary. """ spans={} - for name,field in self.data.items(): + for name,field in self.fields.items(): spans[name]=field.get_spans() return spans @@ -128,13 +156,13 @@ def apply_filter(self,filter_to_apply,ddf=None): if ddf is not None: if not isinstance(ddf,DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") - for name, field in self.data.items(): + for name, field in self.fields.items(): # TODO integration w/ session, dataset newfld = field.create_like(ddf.name,field.name) ddf.add(field.apply_filter(filter_to_apply,dstfld=newfld),name=name) return ddf else: - for field in self.data.values(): + for field in self.fields.values(): field.apply_filter(filter_to_apply) return self @@ -150,13 +178,13 @@ def apply_index(self, index_to_apply, ddf=None): if ddf is not None: if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") - for name, field in self.data.items(): + for name, field in self.fields.items(): #TODO integration w/ session, dataset newfld = field.create_like(ddf.name, field.name) ddf.add(field.apply_index(index_to_apply,dstfld=newfld), name=name) return ddf else: - for field in self.data.values(): + for field in self.fields.values(): field.apply_index(index_to_apply) return self diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index e69de29b..fbbbf67d 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -0,0 +1,167 @@ + +class Dataset(): + """ + DataSet is a container of dataframes + """ + def __init__(self,file_path,name): + pass + + def close(self): + pass + + def add(self, field, name=None): + pass + + def __contains__(self, name): + pass + + def contains_dataframe(self, dataframe): + pass + + def __getitem__(self, name): + pass + + def get_dataframe(self, name): + pass + + def get_name(self, dataframe): + pass + + def __setitem__(self, name, dataframe): + pass + + def __delitem__(self, name): + pass + + def delete_dataframe(self, dataframe): + pass + + def list(self): + pass + + def __iter__(self): + pass + + def __next__(self): + pass + + def __len__(self): + pass + +import h5py +from exetera.core import dataframe as edf +class HDF5Dataset(Dataset): + + def __init__(self, dataset_path, mode, name): + self.file = h5py.File(dataset_path, mode) + self.dataframes = dict() + + def close(self): + self.file.close() + + def create_group(self,name): + """ + Create a group object in HDF5 file and a Exetera dataframe in memory. + + :param name: the name of the group and dataframe + :return: a dataframe object + """ + self.file.create_group(name) + dataframe = edf.DataFrame(name,self) + self.dataframes[name]=dataframe + return dataframe + + + def add(self, dataframe, name=None): + """ + Add an existing dataframe to this dataset, write the existing group + attributes and HDF5 datasets to this dataset. + + :param dataframe: the dataframe to copy to this dataset + :param name: optional- change the dataframe name + """ + dname = dataframe if name is None else name + self.file.copy(dataframe.dataset[dataframe.name],self.file,name=dname) + df = edf.DataFrame(dname,self,h5group=self.file[dname]) + self.dataframes[dname]=df + + + def __contains__(self, name): + return self.dataframes.__contains__(name) + + def contains_dataframe(self, dataframe): + """ + Check if a dataframe is contained in this dataset by the dataframe object itself. + + :param dataframe: the dataframe object to check + :return: Ture or False if the dataframe is contained + """ + if not isinstance(dataframe, edf.DataFrame): + raise TypeError("The field must be a DataFrame object") + else: + for v in self.dataframes.values(): + if id(dataframe) == id(v): + return True + break + return False + + def __getitem__(self, name): + if not isinstance(name,str): + raise TypeError("The name must be a str object.") + elif not self.__contains__(name): + raise ValueError("Can not find the name from this dataset.") + else: + return self.dataframes[name] + + def get_dataframe(self, name): + self.__getitem__(name) + + def get_name(self, dataframe): + """ + Get the name of the dataframe in this dataset. + """ + if not isinstance(dataframe, edf.DataFrame): + raise TypeError("The field argument must be a DataFrame object.") + for name, v in self.fields.items(): + if id(dataframe) == id(v): + return name + break + return None + + def __setitem__(self, name, dataframe): + if not isinstance(name, str): + raise TypeError("The name must be a str object.") + elif not isinstance(dataframe, edf.DataFrame): + raise TypeError("The field must be a DataFrame object.") + else: + self.dataframes[name] = dataframe + return True + + def __delitem__(self, name): + if not self.__contains__(name): + raise ValueError("This dataframe does not contain the name to delete.") + else: + del self.dataframes[name] + return True + + def delete_dataframe(self, dataframe): + """ + Remove dataframe from this dataset by dataframe object. + """ + name = self.get_name(dataframe) + if name is None: + raise ValueError("This dataframe does not contain the field to delete.") + else: + self.__delitem__(name) + + def list(self): + return tuple(n for n in self.dataframes.keys()) + + def __iter__(self): + return iter(self.dataframes) + + def __next__(self): + return next(self.dataframes) + + def __len__(self): + return len(self.dataframes) diff --git a/exetera/core/importer.py b/exetera/core/importer.py index 0b005b2c..655881b4 100644 --- a/exetera/core/importer.py +++ b/exetera/core/importer.py @@ -16,7 +16,7 @@ import numpy as np import h5py -from exetera.core import dataset as dataset +from exetera.core import csvdataset as dataset from exetera.core import persistence as per from exetera.core import utils from exetera.core import operations as ops diff --git a/exetera/core/split.py b/exetera/core/split.py index 4f289add..9ea63985 100644 --- a/exetera/core/split.py +++ b/exetera/core/split.py @@ -11,7 +11,7 @@ import csv -from exetera.core import dataset, utils +from exetera.core import csvdataset, utils # read patients in batches of n @@ -76,8 +76,8 @@ def assessment_splitter(input_filename, output_filename, assessment_buckets, buc def split_data(patient_data, assessment_data, bucket_size=500000, territories=None): with open(patient_data) as f: - p_ds = dataset.Dataset(f, keys=('id', 'created_at'), - show_progress_every=500000) + p_ds = csvdataset.Dataset(f, keys=('id', 'created_at'), + show_progress_every=500000) # show_progress_every=500000, stop_after=500000) p_ds.sort(('created_at', 'id')) p_ids = p_ds.field_by_name('id') @@ -106,7 +106,7 @@ def split_data(patient_data, assessment_data, bucket_size=500000, territories=No print('buckets:', bucket_index) with open(assessment_data) as f: - a_ds = dataset.Dataset(f, keys=('patient_id', 'other_symptoms'), show_progress_every=500000) + a_ds = csvdataset.Dataset(f, keys=('patient_id', 'other_symptoms'), show_progress_every=500000) print(utils.build_histogram(buckets.values())) diff --git a/tests/test_csvdataset.py b/tests/test_csvdataset.py new file mode 100644 index 00000000..36680422 --- /dev/null +++ b/tests/test_csvdataset.py @@ -0,0 +1,161 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import io + +from exetera.core import csvdataset, utils + +small_dataset = ('id,patient_id,foo,bar\n' + '0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,11111111111111111111111111111111,,a\n' + '07777777777777777777777777777777,33333333333333333333333333333333,True,b\n' + '02222222222222222222222222222222,11111111111111111111111111111111,False,a\n') + +sorting_dataset = ('id,patient_id,created_at,updated_at\n' + 'a_1,p_1,100,100\n' + 'a_2,p_1,101,102\n' + 'a_3,p_2,101,101\n' + 'a_4,p_1,101,101\n' + 'a_5,p_2,102,102\n' + 'a_6,p_1,102,102\n' + 'a_7,p_2,102,103\n' + 'a_8,p_1,102,104\n' + 'a_9,p_2,103,105\n' + 'a_10,p_2,104,105\n' + 'a_11,p_1,104,104\n') + + +class TestDataset(unittest.TestCase): + + def test_construction(self): + s = io.StringIO(small_dataset) + ds = csvdataset.Dataset(s, verbose=False) + + # field names and fields must match in length + self.assertEqual(len(ds.names_), len(ds.fields_)) + + self.assertEqual(ds.row_count(), 3) + + self.assertEqual(ds.names_, ['id', 'patient_id', 'foo', 'bar']) + + expected_values = [(0, ['0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', '11111111111111111111111111111111', '', 'a']), + (1, ['07777777777777777777777777777777', '33333333333333333333333333333333', 'True', 'b']), + (2, ['02222222222222222222222222222222', '11111111111111111111111111111111', 'False', 'a'])] + + # value works as expected + for row in range(len(expected_values)): + for col in range(len(expected_values[0][1])): + self.assertEqual(ds.value(row, col), expected_values[row][1][col]) + + # value_from_fieldname works as expected + sorted_names = sorted(ds.names_) + for n in sorted_names: + index = ds.names_.index(n) + for row in range(len(expected_values)): + self.assertEqual(ds.value_from_fieldname(row, n), expected_values[row][1][index]) + + def test_construction_with_early_filter(self): + s = io.StringIO(small_dataset) + ds = csvdataset.Dataset(s, early_filter=('bar', lambda x: x in ('a',)), verbose=False) + + # field names and fields must match in length + self.assertEqual(len(ds.names_), len(ds.fields_)) + + self.assertEqual(ds.row_count(), 2) + + self.assertEqual(ds.names_, ['id', 'patient_id', 'foo', 'bar']) + + expected_values = [(0, ['0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', '11111111111111111111111111111111', '', 'a']), + (2, ['02222222222222222222222222222222', '11111111111111111111111111111111', 'False', 'a'])] + + # value works as expected + for row in range(len(expected_values)): + for col in range(len(expected_values[0][1])): + self.assertEqual(ds.value(row, col), expected_values[row][1][col]) + + # value_from_fieldname works as expected + sorted_names = sorted(ds.names_) + for n in sorted_names: + index = ds.names_.index(n) + for row in range(len(expected_values)): + self.assertEqual(ds.value_from_fieldname(row, n), expected_values[row][1][index]) + + def test_sort(self): + s = io.StringIO(small_dataset) + ds = csvdataset.Dataset(s, verbose=False) + + ds.sort(('patient_id', 'id')) + row_permutations = [2, 0, 1] + + def test_apply_permutation(self): + permutation = [2, 0, 1] + values = ['a', 'b', 'c'] + # temp_index = -1 + # temp_value = None + # empty_index = -1 + # + # for ip in range(len(permutation)): + # p = permutation[ip] + # if p != ip: + # if temp_index != -1: + # # move the temp index back into the empty space, it will be moved later + # if ip == empty_index: + # # can move the current element (index p) to the destination + # if temp_index != p: + # # move the item from its current location + + n = len(permutation) + for i in range(0, n): + pi = permutation[i] + while pi < i: + pi = permutation[pi] + values[i], values[pi] = values[pi], values[i] + self.assertListEqual(['c', 'a', 'b'], values) + + def test_single_key_sorts(self): + ds1 = csvdataset.Dataset(io.StringIO(sorting_dataset), verbose=False) + ds1.sort('patient_id') + self.assertListEqual([0, 1, 3, 5, 7, 10, 2, 4, 6, 8, 9], ds1.index_) + + ds2 = csvdataset.Dataset(io.StringIO(sorting_dataset), verbose=False) + ds2.sort(('patient_id',)) + self.assertListEqual([0, 1, 3, 5, 7, 10, 2, 4, 6, 8, 9], ds2.index_) + + def test_multi_key_sorts(self): + expected_ids =\ + ['a_1', 'a_2', 'a_4', 'a_6', 'a_8', 'a_11', 'a_3', 'a_5', 'a_7', 'a_9', 'a_10'] + expected_pids =\ + ['p_1', 'p_1', 'p_1', 'p_1', 'p_1', 'p_1', 'p_2', 'p_2', 'p_2', 'p_2', 'p_2'] + expected_vals1 =\ + ['100', '101', '101', '102', '102', '104', '101', '102', '102', '103', '104'] + expected_vals2 =\ + ['100', '102', '101', '102', '104', '104', '101', '102', '103', '105', '105'] + + ds1 = csvdataset.Dataset(io.StringIO(sorting_dataset), verbose=False) + ds1.sort('created_at') + ds1.sort('patient_id') + self.assertListEqual([0, 1, 3, 5, 7, 10, 2, 4, 6, 8, 9], ds1.index_) + self.assertListEqual(expected_ids, ds1.field_by_name('id')) + self.assertListEqual(expected_pids, ds1.field_by_name('patient_id')) + self.assertListEqual(expected_vals1, ds1.field_by_name('created_at')) + self.assertListEqual(expected_vals2, ds1.field_by_name('updated_at')) + # for i in range(ds1.row_count()): + # utils.print_diagnostic_row("{}".format(i), ds1, i, ds1.names_) + + ds2 = csvdataset.Dataset(io.StringIO(sorting_dataset), verbose=False) + ds2.sort(('patient_id', 'created_at')) + self.assertListEqual([0, 1, 3, 5, 7, 10, 2, 4, 6, 8, 9], ds2.index_) + self.assertListEqual(expected_ids, ds1.field_by_name('id')) + self.assertListEqual(expected_pids, ds1.field_by_name('patient_id')) + self.assertListEqual(expected_vals1, ds1.field_by_name('created_at')) + self.assertListEqual(expected_vals2, ds1.field_by_name('updated_at')) + # for i in range(ds2.row_count()): + # utils.print_diagnostic_row("{}".format(i), ds2, i, ds2.names_) diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 310c7c19..bb640bc4 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -16,10 +16,10 @@ def test_dataframe_init(self): dst = s.open_dataset(bio,'w','dst') numf = s.create_numeric(dst,'numf','int32') #init - df = dataframe.DataFrame('dst') + df = dataframe.DataFrame('dst',dst) self.assertTrue(isinstance(df, dataframe.DataFrame)) fdf = {'/numf',numf} - df2 = dataframe.DataFrame('dst2',fdf) + df2 = dataframe.DataFrame('dst2',dst,data=fdf) self.assertTrue(isinstance(df2,dataframe.DataFrame)) #add & set & contains df.add(numf) @@ -45,26 +45,36 @@ def test_dataframe_init(self): self.assertFalse(df.contains_field(cat)) self.assertIsNone(df.get_name(cat)) - def test_dataframe_ops(self): + def test_dataframe_init_fromh5(self): bio = BytesIO() with session.Session() as s: - dst = s.open_dataset(bio, 'w', 'dst') - df = dataframe.DataFrame('dst') - numf = s.create_numeric(dst, 'numf', 'int32') - numf.data.write([5,4,3,2,1]) - df.add(numf) - fst = s.create_fixed_string(dst,'fst',3) - fst.data.write([b'e',b'd',b'c',b'b',b'a']) - df.add(fst) - index=np.array([4,3,2,1,0]) - ddf = dataframe.DataFrame('dst2') - df.apply_index(index,ddf) - self.assertEqual([1,2,3,4,5],ddf.get_field('/numf').data[:].tolist()) - self.assertEqual([b'a',b'b',b'c',b'd',b'e'],ddf.get_field('/fst').data[:].tolist()) + dst=s.open_dataset(bio,'r+','dst') + num=s.create_numeric(dst,'num','uint8') + num.data.write([1,2,3,4,5,6,7]) + df = dataframe.DataFrame('dst',dst,h5group=dst) + + - filter= np.array([True,True,False,False,True]) - df.apply_filter(filter) - self.assertEqual([1, 2, 5], df.get_field('/numf').data[:].tolist()) - self.assertEqual([b'a', b'b', b'e'], df.get_field('/fst').data[:].tolist()) + # def test_dataframe_ops(self): + # bio = BytesIO() + # with session.Session() as s: + # dst = s.open_dataset(bio, 'w', 'dst') + # df = dataframe.DataFrame('dst',dst) + # numf = s.create_numeric(dst, 'numf', 'int32') + # numf.data.write([5,4,3,2,1]) + # df.add(numf) + # fst = s.create_fixed_string(dst,'fst',3) + # fst.data.write([b'e',b'd',b'c',b'b',b'a']) + # df.add(fst) + # index=np.array([4,3,2,1,0]) + # ddf = dataframe.DataFrame('dst2',dst) + # df.apply_index(index,ddf) + # self.assertEqual([1,2,3,4,5],ddf.get_field('/numf').data[:].tolist()) + # self.assertEqual([b'a',b'b',b'c',b'd',b'e'],ddf.get_field('/fst').data[:].tolist()) + # + # filter= np.array([True,True,False,False,True]) + # df.apply_filter(filter) + # self.assertEqual([1, 2, 5], df.get_field('/numf').data[:].tolist()) + # self.assertEqual([b'a', b'b', b'e'], df.get_field('/fst').data[:].tolist()) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 0c862a65..1776e553 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -1,161 +1,15 @@ -# Copyright 2020 KCL-BMEIS - King's College London -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import unittest -import io - -from exetera.core import dataset, utils - -small_dataset = ('id,patient_id,foo,bar\n' - '0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,11111111111111111111111111111111,,a\n' - '07777777777777777777777777777777,33333333333333333333333333333333,True,b\n' - '02222222222222222222222222222222,11111111111111111111111111111111,False,a\n') - -sorting_dataset = ('id,patient_id,created_at,updated_at\n' - 'a_1,p_1,100,100\n' - 'a_2,p_1,101,102\n' - 'a_3,p_2,101,101\n' - 'a_4,p_1,101,101\n' - 'a_5,p_2,102,102\n' - 'a_6,p_1,102,102\n' - 'a_7,p_2,102,103\n' - 'a_8,p_1,102,104\n' - 'a_9,p_2,103,105\n' - 'a_10,p_2,104,105\n' - 'a_11,p_1,104,104\n') - - -class TestDataset(unittest.TestCase): - - def test_construction(self): - s = io.StringIO(small_dataset) - ds = dataset.Dataset(s, verbose=False) - - # field names and fields must match in length - self.assertEqual(len(ds.names_), len(ds.fields_)) - - self.assertEqual(ds.row_count(), 3) - - self.assertEqual(ds.names_, ['id', 'patient_id', 'foo', 'bar']) - - expected_values = [(0, ['0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', '11111111111111111111111111111111', '', 'a']), - (1, ['07777777777777777777777777777777', '33333333333333333333333333333333', 'True', 'b']), - (2, ['02222222222222222222222222222222', '11111111111111111111111111111111', 'False', 'a'])] - - # value works as expected - for row in range(len(expected_values)): - for col in range(len(expected_values[0][1])): - self.assertEqual(ds.value(row, col), expected_values[row][1][col]) - - # value_from_fieldname works as expected - sorted_names = sorted(ds.names_) - for n in sorted_names: - index = ds.names_.index(n) - for row in range(len(expected_values)): - self.assertEqual(ds.value_from_fieldname(row, n), expected_values[row][1][index]) - - def test_construction_with_early_filter(self): - s = io.StringIO(small_dataset) - ds = dataset.Dataset(s, early_filter=('bar', lambda x: x in ('a',)), verbose=False) - - # field names and fields must match in length - self.assertEqual(len(ds.names_), len(ds.fields_)) - - self.assertEqual(ds.row_count(), 2) - - self.assertEqual(ds.names_, ['id', 'patient_id', 'foo', 'bar']) - - expected_values = [(0, ['0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', '11111111111111111111111111111111', '', 'a']), - (2, ['02222222222222222222222222222222', '11111111111111111111111111111111', 'False', 'a'])] - - # value works as expected - for row in range(len(expected_values)): - for col in range(len(expected_values[0][1])): - self.assertEqual(ds.value(row, col), expected_values[row][1][col]) - - # value_from_fieldname works as expected - sorted_names = sorted(ds.names_) - for n in sorted_names: - index = ds.names_.index(n) - for row in range(len(expected_values)): - self.assertEqual(ds.value_from_fieldname(row, n), expected_values[row][1][index]) - - def test_sort(self): - s = io.StringIO(small_dataset) - ds = dataset.Dataset(s, verbose=False) - - ds.sort(('patient_id', 'id')) - row_permutations = [2, 0, 1] - - def test_apply_permutation(self): - permutation = [2, 0, 1] - values = ['a', 'b', 'c'] - # temp_index = -1 - # temp_value = None - # empty_index = -1 - # - # for ip in range(len(permutation)): - # p = permutation[ip] - # if p != ip: - # if temp_index != -1: - # # move the temp index back into the empty space, it will be moved later - # if ip == empty_index: - # # can move the current element (index p) to the destination - # if temp_index != p: - # # move the item from its current location - - n = len(permutation) - for i in range(0, n): - pi = permutation[i] - while pi < i: - pi = permutation[pi] - values[i], values[pi] = values[pi], values[i] - self.assertListEqual(['c', 'a', 'b'], values) +from exetera.core import dataset +from exetera.core import session +from io import BytesIO - def test_single_key_sorts(self): - ds1 = dataset.Dataset(io.StringIO(sorting_dataset), verbose=False) - ds1.sort('patient_id') - self.assertListEqual([0, 1, 3, 5, 7, 10, 2, 4, 6, 8, 9], ds1.index_) +class TestDataSet(unittest.TestCase): + def TestDataSet_init(self): + bio=BytesIO() + with session.Session() as s: + dst=s.open_dataset(bio,'r+','dst') + - ds2 = dataset.Dataset(io.StringIO(sorting_dataset), verbose=False) - ds2.sort(('patient_id',)) - self.assertListEqual([0, 1, 3, 5, 7, 10, 2, 4, 6, 8, 9], ds2.index_) - def test_multi_key_sorts(self): - expected_ids =\ - ['a_1', 'a_2', 'a_4', 'a_6', 'a_8', 'a_11', 'a_3', 'a_5', 'a_7', 'a_9', 'a_10'] - expected_pids =\ - ['p_1', 'p_1', 'p_1', 'p_1', 'p_1', 'p_1', 'p_2', 'p_2', 'p_2', 'p_2', 'p_2'] - expected_vals1 =\ - ['100', '101', '101', '102', '102', '104', '101', '102', '102', '103', '104'] - expected_vals2 =\ - ['100', '102', '101', '102', '104', '104', '101', '102', '103', '105', '105'] - ds1 = dataset.Dataset(io.StringIO(sorting_dataset), verbose=False) - ds1.sort('created_at') - ds1.sort('patient_id') - self.assertListEqual([0, 1, 3, 5, 7, 10, 2, 4, 6, 8, 9], ds1.index_) - self.assertListEqual(expected_ids, ds1.field_by_name('id')) - self.assertListEqual(expected_pids, ds1.field_by_name('patient_id')) - self.assertListEqual(expected_vals1, ds1.field_by_name('created_at')) - self.assertListEqual(expected_vals2, ds1.field_by_name('updated_at')) - # for i in range(ds1.row_count()): - # utils.print_diagnostic_row("{}".format(i), ds1, i, ds1.names_) - ds2 = dataset.Dataset(io.StringIO(sorting_dataset), verbose=False) - ds2.sort(('patient_id', 'created_at')) - self.assertListEqual([0, 1, 3, 5, 7, 10, 2, 4, 6, 8, 9], ds2.index_) - self.assertListEqual(expected_ids, ds1.field_by_name('id')) - self.assertListEqual(expected_pids, ds1.field_by_name('patient_id')) - self.assertListEqual(expected_vals1, ds1.field_by_name('created_at')) - self.assertListEqual(expected_vals2, ds1.field_by_name('updated_at')) - # for i in range(ds2.row_count()): - # utils.print_diagnostic_row("{}".format(i), ds2, i, ds2.names_) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index e9a9370f..8800a516 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -506,11 +506,12 @@ def test_categorical_field_writer_from_reader(self): reader2 = datastore.get_reader(hf['foo2']) self.assertTrue(np.array_equal(reader[:], reader2[:])) - + from dateutil import tz as tzd def test_timestamp_reader(self): datastore = persistence.DataStore(10) - dt = datetime.now(timezone.utc) + from dateutil import tz + dt = datetime.now(tz=tz.tzlocal()) ts = str(dt) bio = BytesIO() random.seed(12345678) @@ -533,7 +534,8 @@ def test_timestamp_reader(self): def test_new_timestamp_reader(self): datastore = persistence.DataStore(10) - dt = datetime.now(timezone.utc) + from dateutil import tz + dt = datetime.now(tz=tz.tzlocal()) ts = str(dt) bio = BytesIO() random.seed(12345678) @@ -560,7 +562,7 @@ def test_new_timestamp_reader(self): def test_new_timestamp_writer_from_reader(self): datastore = persistence.DataStore(10) - dt = datetime.now(timezone.utc) + dt = datetime.now(timezone.utc)+timedelta(hours=1) ts = str(dt) bio = BytesIO() random.seed(12345678) From 463ea70aee5981fed7a0624eeecacae8e1669353 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 6 Apr 2021 16:20:20 +0100 Subject: [PATCH 035/181] integrates the dataset, dataframe into the session --- exetera/core/dataframe.py | 46 ++++++++++++++++++++++++++++++++++++++- exetera/core/dataset.py | 2 +- exetera/core/session.py | 37 +++++++++++++++++++++---------- tests/test_dataframe.py | 40 ++++++++++++++++++++-------------- tests/test_dataset.py | 14 ++++++++++-- 5 files changed, 107 insertions(+), 32 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index a4221f58..e53599ef 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -46,7 +46,51 @@ def add(self,field,name=None): raise TypeError("The name must be a str object.") else: self.fields[name]=field - self.fields[field.name]=field #note the name has '/' for hdf5 object + self.fields[field.name[field.name.index('/',1)+1:]]=field #note the name has '/' for hdf5 object + + def create_group(self,name): + """ + Create a group object in HDF5 file for field to use. + + :param name: the name of the group and field + :return: a hdf5 group object + """ + self.dataset.file.create_group("/"+self.name+"/"+name) + return self.dataset.file["/"+self.name+"/"+name] + + + def create_numeric(self, session, name, nformat, timestamp=None, chunksize=None): + fld.numeric_field_constructor(session, self, name, nformat, timestamp, chunksize) + field=fld.NumericField(session, self.dataset.file["/"+self.name+"/"+name], write_enabled=True) + self.fields[name]=field + return self.fields[name] + + def create_indexed_string(self, session, name, timestamp=None, chunksize=None): + fld.indexed_string_field_constructor(session, self, name, timestamp, chunksize) + field= fld.IndexedStringField(session, self.dataset.file["/"+self.name+"/"+name], write_enabled=True) + self.fields[name] = field + return self.fields[name] + + def create_fixed_string(self, session, name, length, timestamp=None, chunksize=None): + fld.fixed_string_field_constructor(session, self, name, length, timestamp, chunksize) + field= fld.FixedStringField(session, self.dataset.file["/"+self.name+"/"+name], write_enabled=True) + self.fields[name] = field + return self.fields[name] + + def create_categorical(self, session, name, nformat, key, + timestamp=None, chunksize=None): + fld.categorical_field_constructor(session, self, name, nformat, key, + timestamp, chunksize) + field= fld.CategoricalField(session, self.dataset.file["/"+self.name+"/"+name], write_enabled=True) + self.fields[name] = field + return self.fields[name] + + def create_timestamp(self, session, name, timestamp=None, chunksize=None): + fld.timestamp_field_constructor(session, self, name, timestamp, chunksize) + field= fld.TimestampField(session, self.dataset.file["/"+self.name+"/"+name], write_enabled=True) + self.fields[name] = field + return self.fields[name] + def __contains__(self, name): """ diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index fbbbf67d..1dc0dabf 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -59,7 +59,7 @@ def __init__(self, dataset_path, mode, name): def close(self): self.file.close() - def create_group(self,name): + def create_dataframe(self,name): """ Create a group object in HDF5 file and a Exetera dataframe in memory. diff --git a/exetera/core/session.py b/exetera/core/session.py index a8fd7f87..377eb736 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -15,6 +15,8 @@ from exetera.core import readerwriter as rw from exetera.core import validation as val from exetera.core import operations as ops +from exetera.core import dataset as ds +from exetera.core import dataframe as df from exetera.core import utils @@ -86,7 +88,8 @@ def open_dataset(self, dataset_path, mode, name): if name in self.datasets: raise ValueError("A dataset with name '{}' is already open, and must be closed first.".format(name)) - self.datasets[name] = h5py.File(dataset_path, h5py_modes[mode]) + #self.datasets[name] = h5py.File(dataset_path, h5py_modes[mode]) + self.datasets[name] = ds.HDF5Dataset(dataset_path,mode,name) return self.datasets[name] @@ -638,30 +641,40 @@ def create_like(self, field, dest_group, dest_name, timestamp=None, chunksize=No def create_indexed_string(self, group, name, timestamp=None, chunksize=None): - fld.indexed_string_field_constructor(self, group, name, timestamp, chunksize) - return fld.IndexedStringField(self, group[name], write_enabled=True) + if isinstance(group,ds.Dataset): + pass + elif isinstance(group,df.DataFrame): + return group.create_indexed_string(self,name, timestamp,chunksize) def create_fixed_string(self, group, name, length, timestamp=None, chunksize=None): - fld.fixed_string_field_constructor(self, group, name, length, timestamp, chunksize) - return fld.FixedStringField(self, group[name], write_enabled=True) + if isinstance(group,ds.Dataset): + pass + elif isinstance(group,df.DataFrame): + return group.create_fixed_string(self,name, length,timestamp,chunksize) def create_categorical(self, group, name, nformat, key, timestamp=None, chunksize=None): - fld.categorical_field_constructor(self, group, name, nformat, key, - timestamp, chunksize) - return fld.CategoricalField(self, group[name], write_enabled=True) + if isinstance(group, ds.Dataset): + pass + elif isinstance(group, df.DataFrame): + return group.create_categorical(self, name, nformat,key,timestamp,chunksize) def create_numeric(self, group, name, nformat, timestamp=None, chunksize=None): - fld.numeric_field_constructor(self, group, name, nformat, timestamp, chunksize) - return fld.NumericField(self, group[name], write_enabled=True) + if isinstance(group,ds.Dataset): + pass + elif isinstance(group,df.DataFrame): + return group.create_numeric(self,name, nformat, timestamp, chunksize) + def create_timestamp(self, group, name, timestamp=None, chunksize=None): - fld.timestamp_field_constructor(self, group, name, timestamp, chunksize) - return fld.TimestampField(self, group[name], write_enabled=True) + if isinstance(group,ds.Dataset): + pass + elif isinstance(group,df.DataFrame): + return group.create_timestamp(self,name, timestamp, chunksize) def get_or_create_group(self, group, name): diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index bb640bc4..b7284669 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -14,33 +14,33 @@ def test_dataframe_init(self): bio=BytesIO() with session.Session() as s: dst = s.open_dataset(bio,'w','dst') - numf = s.create_numeric(dst,'numf','int32') #init df = dataframe.DataFrame('dst',dst) self.assertTrue(isinstance(df, dataframe.DataFrame)) - fdf = {'/numf',numf} + numf = df.create_numeric(s,'numf','uint32') + fdf = {'numf',numf} df2 = dataframe.DataFrame('dst2',dst,data=fdf) self.assertTrue(isinstance(df2,dataframe.DataFrame)) #add & set & contains df.add(numf) - self.assertTrue('/numf' in df) + self.assertTrue('numf' in df) self.assertTrue(df.contains_field(numf)) - cat=s.create_categorical(dst,'cat','int8',{'a':1,'b':2}) - self.assertFalse('/cat' in df) + cat=s.create_categorical(df2,'cat','int8',{'a':1,'b':2}) + self.assertFalse('cat' in df) self.assertFalse(df.contains_field(cat)) - df['/cat']=cat - self.assertTrue('/cat' in df) + df['cat']=cat + self.assertTrue('cat' in df) #list & get - self.assertEqual(id(numf),id(df.get_field('/numf'))) - self.assertEqual(id(numf), id(df['/numf'])) - self.assertEqual('/numf',df.get_name(numf)) + self.assertEqual(id(numf),id(df.get_field('numf'))) + self.assertEqual(id(numf), id(df['numf'])) + self.assertEqual('numf',df.get_name(numf)) #list & iter dfit = iter(df) - self.assertEqual('/numf',next(dfit)) - self.assertEqual('/cat', next(dfit)) + self.assertEqual('numf',next(dfit)) + self.assertEqual('cat', next(dfit)) #del & del by field - del df['/numf'] - self.assertFalse('/numf' in df) + del df['numf'] + self.assertFalse('numf' in df) df.delete_field(cat) self.assertFalse(df.contains_field(cat)) self.assertIsNone(df.get_name(cat)) @@ -48,12 +48,20 @@ def test_dataframe_init(self): def test_dataframe_init_fromh5(self): bio = BytesIO() with session.Session() as s: - dst=s.open_dataset(bio,'r+','dst') + ds=s.open_dataset(bio,'w','ds') + dst = ds.create_dataframe('dst') num=s.create_numeric(dst,'num','uint8') num.data.write([1,2,3,4,5,6,7]) df = dataframe.DataFrame('dst',dst,h5group=dst) - + def test_dataframe_create_field(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'r+', 'dst') + df = dataframe.DataFrame('dst',dst) + num = df.create_numeric(s,'num','uint32') + num.data.write([1,2,3,4]) + self.assertEqual([1,2,3,4],num.data[:].tolist()) # def test_dataframe_ops(self): # bio = BytesIO() diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 1776e553..f1ccb7a0 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -4,11 +4,21 @@ from io import BytesIO class TestDataSet(unittest.TestCase): - def TestDataSet_init(self): + + def test_dataset_init(self): bio=BytesIO() with session.Session() as s: dst=s.open_dataset(bio,'r+','dst') - + df=dst.create_dataframe('df') + num=s.create_numeric(df,'num','int32') + num.data.write([1,2,3,4]) + self.assertEqual([1,2,3,4],num.data[:].tolist()) + + num2=s.create_numeric(df,'num2','int32') + num2 = s.get(df['num2']) + + def test_dataset_ops(self): + pass From 76d1952e2b93ab97ebb9d563bad57f95e5383544 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 7 Apr 2021 11:49:59 +0100 Subject: [PATCH 036/181] update the fieldsimporter and field.create_like methods to call dataframe.create update the unittests to follow s.open_dataset and dataset.create_dataframe flow --- exetera/core/dataframe.py | 19 ++++-- exetera/core/dataset.py | 9 +++ exetera/core/fields.py | 54 +++++------------ tests/test_dataframe.py | 42 ++++++------- tests/test_fields.py | 33 ++++++---- tests/test_journalling.py | 36 ++++++----- tests/test_operations.py | 20 ++++--- tests/test_session.py | 123 ++++++++++++++++++++++++-------------- 8 files changed, 191 insertions(+), 145 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index e53599ef..4ec6312b 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -1,5 +1,6 @@ from exetera.core import fields as fld from datetime import datetime,timezone +from exetera.core import dataset as dst import h5py class DataFrame(): @@ -20,7 +21,8 @@ def __init__(self, name, dataset,data=None,h5group:h5py.Group=None): self.fields = dict() self.name = name self.dataset = dataset - + if isinstance(dataset,dst.HDF5Dataset): + dataset[name]=self if data is not None: if isinstance(data,dict) and isinstance(list(data.items())[0][0],str) and isinstance(list(data.items())[0][1], fld.Field) : self.fields=data @@ -56,6 +58,7 @@ def create_group(self,name): :return: a hdf5 group object """ self.dataset.file.create_group("/"+self.name+"/"+name) + return self.dataset.file["/"+self.name+"/"+name] @@ -168,6 +171,15 @@ def delete_field(self,field): def list(self): return tuple(n for n in self.fields.keys()) + def keys(self): + return self.fields.keys() + + def values(self): + return self.fields.values() + + def items(self): + return self.fields.items() + def __iter__(self): return iter(self.fields) @@ -202,7 +214,7 @@ def apply_filter(self,filter_to_apply,ddf=None): raise TypeError("The destination object must be an instance of DataFrame.") for name, field in self.fields.items(): # TODO integration w/ session, dataset - newfld = field.create_like(ddf.name,field.name) + newfld = field.create_like(ddf,field.name) ddf.add(field.apply_filter(filter_to_apply,dstfld=newfld),name=name) return ddf else: @@ -223,8 +235,7 @@ def apply_index(self, index_to_apply, ddf=None): if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") for name, field in self.fields.items(): - #TODO integration w/ session, dataset - newfld = field.create_like(ddf.name, field.name) + newfld = field.create_like(ddf, field.name) ddf.add(field.apply_index(index_to_apply,dstfld=newfld), name=name) return ddf else: diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 1dc0dabf..7835a44c 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -157,6 +157,15 @@ def delete_dataframe(self, dataframe): def list(self): return tuple(n for n in self.dataframes.keys()) + def keys(self): + return self.file.keys() + + def values(self): + return self.file.values() + + def items(self): + return self.file.items() + def __iter__(self): return iter(self.dataframes) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 8cd7c340..a2e8d3f0 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -15,8 +15,6 @@ class HDF5Field(Field): def __init__(self, session, group, name=None, write_enabled=False): super().__init__() - # if name is None, the group is an existing field - # if name is set but group[name] doesn't exist, then create the field if name is None: field = group else: @@ -370,8 +368,8 @@ def writeable(self): def create_like(self, group, name, timestamp=None): ts = self.timestamp if timestamp is None else timestamp - indexed_string_field_constructor(self._session, group, name, ts, self.chunksize) - return IndexedStringField(self._session, group, name, write_enabled=True) + return group.create_indexed_string(self._session, name, ts, self.chunksize) + @property def indexed(self): @@ -461,8 +459,7 @@ def writeable(self): def create_like(self, group, name, timestamp=None): ts = self.timestamp if timestamp is None else timestamp length = self._field.attrs['strlen'] - fixed_string_field_constructor(self._session, group, name, length, ts, self.chunksize) - return FixedStringField(self._session, group, name, write_enabled=True) + return group.create_fixed_string(self._session,name,length,ts,self.chunksize) @property def data(self): @@ -517,8 +514,7 @@ def writeable(self): def create_like(self, group, name, timestamp=None): ts = self.timestamp if timestamp is None else timestamp nformat = self._field.attrs['nformat'] - numeric_field_constructor(self._session, group, name, nformat, ts, self.chunksize) - return NumericField(self._session, group, name, write_enabled=True) + return group.create_numeric(self._session,name,nformat,ts,self.chunksize) @property def data(self): @@ -573,9 +569,7 @@ def create_like(self, group, name, timestamp=None): ts = self.timestamp if timestamp is None else timestamp nformat = self._field.attrs['nformat'] if 'nformat' in self._field.attrs else 'int8' keys = {v: k for k, v in self.keys.items()} - categorical_field_constructor(self._session, group, name, nformat, keys, - ts, self.chunksize) - return CategoricalField(self._session, group, name, write_enabled=True) + return group.create_categorical(self._session,name,nformat,keys,ts,self.chunksize) @property def data(self): @@ -639,8 +633,7 @@ def writeable(self): def create_like(self, group, name, timestamp=None): ts = self.timestamp if timestamp is None else timestamp - timestamp_field_constructor(self._session, group, name, ts, self.chunksize) - return TimestampField(self._session, group, name, write_enabled=True) + return group.create_timestamp(self._session, name, ts, self.chunksize) @property def data(self): @@ -687,8 +680,7 @@ def apply_index(self, index_to_apply, dstfld=None): class IndexedStringImporter: def __init__(self, session, group, name, timestamp=None, chunksize=None): - indexed_string_field_constructor(session, group, name, timestamp, chunksize) - self._field = IndexedStringField(session, group, name, write_enabled=True) + self._field=group.create_indexed_string(session,name,timestamp,chunksize) def chunk_factory(self, length): return [None] * length @@ -706,8 +698,7 @@ def write(self, values): class FixedStringImporter: def __init__(self, session, group, name, length, timestamp=None, chunksize=None): - fixed_string_field_constructor(session, group, name, length, timestamp, chunksize) - self._field = FixedStringField(session, group, name, write_enabled=True) + self._field=group.create_fixed_string(session,name,length,timestamp,chunksize) def chunk_factory(self, length): return np.zeros(length, dtype=self._field.data.dtype) @@ -726,17 +717,11 @@ def write(self, values): class NumericImporter: def __init__(self, session, group, name, dtype, parser, timestamp=None, chunksize=None): filter_name = '{}_valid'.format(name) - numeric_field_constructor(session, group, name, dtype, timestamp, chunksize) - numeric_field_constructor(session, group, filter_name, 'bool', - timestamp, chunksize) - + self._field=group.create_numeric(session,name,dtype, timestamp, chunksize) + self._filter_field=group.create_numeric(session,filter_name, 'bool',timestamp, chunksize) chunksize = session.chunksize if chunksize is None else chunksize - self._field = NumericField(session, group, name, write_enabled=True) - self._filter_field = NumericField(session, group, filter_name, write_enabled=True) - self._parser = parser self._values = np.zeros(chunksize, dtype=self._field.data.dtype) - self._filter_values = np.zeros(chunksize, dtype='bool') def chunk_factory(self, length): @@ -763,8 +748,7 @@ def write(self, values): class CategoricalImporter: def __init__(self, session, group, name, value_type, keys, timestamp=None, chunksize=None): chunksize = session.chunksize if chunksize is None else chunksize - categorical_field_constructor(session, group, name, value_type, keys, timestamp, chunksize) - self._field = CategoricalField(session, group, name, write_enabled=True) + self._field=group.create_categorical(session,name,value_type,keys,timestamp,chunksize) self._keys = keys self._dtype = value_type self._key_type = 'U{}'.format(max(len(k.encode()) for k in keys)) @@ -789,15 +773,9 @@ class LeakyCategoricalImporter: def __init__(self, session, group, name, value_type, keys, out_of_range, timestamp=None, chunksize=None): chunksize = session.chunksize if chunksize is None else chunksize - categorical_field_constructor(session, group, name, value_type, keys, - timestamp, chunksize) out_of_range_name = '{}_{}'.format(name, out_of_range) - indexed_string_field_constructor(session, group, out_of_range_name, - timestamp, chunksize) - - self._field = CategoricalField(session, group, name, write_enabled=True) - self._str_field = IndexedStringField(session, group, out_of_range_name, write_enabled=True) - + self._field=group.create_categorical(session,name, value_type, keys,timestamp, chunksize) + self._str_field =group.create_indexed_string(session,out_of_range_name,timestamp, chunksize) self._keys = keys self._dtype = value_type self._key_type = 'S{}'.format(max(len(k.encode()) for k in keys)) @@ -840,8 +818,7 @@ class DateTimeImporter: def __init__(self, session, group, name, optional=False, write_days=False, timestamp=None, chunksize=None): chunksize = session.chunksize if chunksize is None else chunksize - timestamp_field_constructor(session, group, name, timestamp, chunksize) - self._field = TimestampField(session, group, name, write_enabled=True) + self._field =group.create_timestamp(session,name, timestamp, chunksize) self._results = np.zeros(chunksize , dtype='float64') self._optional = optional @@ -884,8 +861,7 @@ def write(self, values): class DateImporter: def __init__(self, session, group, name, optional=False, timestamp=None, chunksize=None): - timestamp_field_constructor(session, group, name, timestamp, chunksize) - self._field = TimestampField(session, group, name, write_enabled=True) + self._field=group.create_timestamp(session,name, timestamp, chunksize) self._results = np.zeros(chunksize, dtype='float64') if optional is True: diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index b7284669..cef3c556 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -63,26 +63,26 @@ def test_dataframe_create_field(self): num.data.write([1,2,3,4]) self.assertEqual([1,2,3,4],num.data[:].tolist()) - # def test_dataframe_ops(self): - # bio = BytesIO() - # with session.Session() as s: - # dst = s.open_dataset(bio, 'w', 'dst') - # df = dataframe.DataFrame('dst',dst) - # numf = s.create_numeric(dst, 'numf', 'int32') - # numf.data.write([5,4,3,2,1]) - # df.add(numf) - # fst = s.create_fixed_string(dst,'fst',3) - # fst.data.write([b'e',b'd',b'c',b'b',b'a']) - # df.add(fst) - # index=np.array([4,3,2,1,0]) - # ddf = dataframe.DataFrame('dst2',dst) - # df.apply_index(index,ddf) - # self.assertEqual([1,2,3,4,5],ddf.get_field('/numf').data[:].tolist()) - # self.assertEqual([b'a',b'b',b'c',b'd',b'e'],ddf.get_field('/fst').data[:].tolist()) - # - # filter= np.array([True,True,False,False,True]) - # df.apply_filter(filter) - # self.assertEqual([1, 2, 5], df.get_field('/numf').data[:].tolist()) - # self.assertEqual([b'a', b'b', b'e'], df.get_field('/fst').data[:].tolist()) + def test_dataframe_ops(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + df = dataframe.DataFrame('dst',dst) + numf = s.create_numeric(df, 'numf', 'int32') + numf.data.write([5,4,3,2,1]) + df.add(numf) + fst = s.create_fixed_string(df,'fst',3) + fst.data.write([b'e',b'd',b'c',b'b',b'a']) + df.add(fst) + index=np.array([4,3,2,1,0]) + ddf = dataframe.DataFrame('dst2',dst) + df.apply_index(index,ddf) + self.assertEqual([1,2,3,4,5],ddf.get_field('numf').data[:].tolist()) + self.assertEqual([b'a',b'b',b'c',b'd',b'e'],ddf.get_field('fst').data[:].tolist()) + + filter= np.array([True,True,False,False,True]) + df.apply_filter(filter) + self.assertEqual([5, 4, 1], df.get_field('numf').data[:].tolist()) + self.assertEqual([b'e', b'd', b'a'], df.get_field('fst').data[:].tolist()) diff --git a/tests/test_fields.py b/tests/test_fields.py index e5385b1a..4c708b2f 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -15,7 +15,8 @@ class TestFieldExistence(unittest.TestCase): def test_field_truthness(self): bio = BytesIO() with session.Session() as s: - src = s.open_dataset(bio, "w", "src") + dst = s.open_dataset(bio, "w", "src") + src=dst.create_dataframe('src') f = s.create_indexed_string(src, "a") self.assertTrue(bool(f)) f = s.create_fixed_string(src, "b", 5) @@ -35,7 +36,8 @@ def test_get_spans(self): with session.Session() as s: self.assertListEqual([0, 1, 3, 5, 6, 9], s.get_spans(vals).tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "src") + ds = dst.create_dataframe('src') vals_f = s.create_numeric(ds, "vals", "int32") vals_f.data.write(vals) self.assertListEqual([0, 1, 3, 5, 6, 9], vals_f.get_spans().tolist()) @@ -52,8 +54,9 @@ class TestIndexedStringFields(unittest.TestCase): def test_create_indexed_string(self): bio = BytesIO() - with h5py.File(bio, 'r+') as hf: - s = session.Session() + with session.Session() as s: + dst = s.open_dataset(bio, "w", "src") + hf = dst.create_dataframe('src') strings = ['a', 'bb', 'ccc', 'dddd'] f = fields.IndexedStringImporter(s, hf, 'foo') f.write(strings) @@ -78,18 +81,20 @@ def test_create_indexed_string(self): def test_update_legacy_indexed_string_that_has_uint_values(self): bio = BytesIO() - with h5py.File(bio, 'r+') as hf: - s = session.Session() + with session.Session() as s: + dst = s.open_dataset(bio, "w", "src") + hf = dst.create_dataframe('src') strings = ['a', 'bb', 'ccc', 'dddd'] f = fields.IndexedStringImporter(s, hf, 'foo') f.write(strings) - values = hf['foo']['values'][:] + values = hf['foo'].values[:] self.assertListEqual([97, 98, 98, 99, 99, 99, 100, 100, 100, 100], values.tolist()) def test_index_string_field_get_span(self): bio = BytesIO() with session.Session() as s: - ds = s.open_dataset(bio, 'w', 'ds') + dst = s.open_dataset(bio, "w", "src") + ds = dst.create_dataframe('src') idx = s.create_indexed_string(ds, 'idx') idx.data.write(['aa', 'bb', 'bb', 'c', 'c', 'c', 'ddd', 'ddd', 'e', 'f', 'f', 'f']) self.assertListEqual([0, 1, 3, 6, 8, 9, 12], s.get_spans(idx)) @@ -100,7 +105,8 @@ class TestFieldArray(unittest.TestCase): def test_write_part(self): bio = BytesIO() s = session.Session() - dst = s.open_dataset(bio, 'w', 'dst') + ds = s.open_dataset(bio, "w", "src") + dst = ds.create_dataframe('src') num = s.create_numeric(dst, 'num', 'int32') num.data.write_part(np.arange(10)) self.assertListEqual([0,1,2,3,4,5,6,7,8,9],list(num.data[:])) @@ -108,7 +114,8 @@ def test_write_part(self): def test_clear(self): bio = BytesIO() s = session.Session() - dst = s.open_dataset(bio, 'w', 'dst') + ds = s.open_dataset(bio, "w", "src") + dst = ds.create_dataframe('src') num = s.create_numeric(dst, 'num', 'int32') num.data.write_part(np.arange(10)) num.data.clear() @@ -121,7 +128,8 @@ class TestFieldArray(unittest.TestCase): def test_write_part(self): bio = BytesIO() s = session.Session() - dst = s.open_dataset(bio, 'w', 'dst') + ds = s.open_dataset(bio, "w", "src") + dst = ds.create_dataframe('src') num = s.create_numeric(dst, 'num', 'int32') num.data.write_part(np.arange(10)) self.assertListEqual([0,1,2,3,4,5,6,7,8,9],list(num.data[:])) @@ -129,7 +137,8 @@ def test_write_part(self): def test_clear(self): bio = BytesIO() s = session.Session() - dst = s.open_dataset(bio, 'w', 'dst') + ds = s.open_dataset(bio, "w", "src") + dst = ds.create_dataframe('src') num = s.create_numeric(dst, 'num', 'int32') num.data.write_part(np.arange(10)) num.data.clear() diff --git a/tests/test_journalling.py b/tests/test_journalling.py index 22322779..577e2bef 100644 --- a/tests/test_journalling.py +++ b/tests/test_journalling.py @@ -45,19 +45,23 @@ def test_journal_full(self): d1_bytes = BytesIO() d2_bytes = BytesIO() dr_bytes = BytesIO() - with h5py.File(d1_bytes, 'w') as d1_hf: - with h5py.File(d2_bytes, 'w') as d2_hf: - with h5py.File(dr_bytes, 'w') as dr_hf: - s = session.Session() - - s.create_fixed_string(d1_hf, 'id', 1).data.write(d1_id) - s.create_numeric(d1_hf, 'val', 'int32').data.write(d1_v1) - s.create_timestamp(d1_hf, 'j_valid_from').data.write(d1_jvf) - s.create_timestamp(d1_hf, 'j_valid_to').data.write(d1_jvt) - - s.create_fixed_string(d2_hf, 'id', 1).data.write(d2_id) - s.create_numeric(d2_hf, 'val', 'int32').data.write(d2_v1) - s.create_timestamp(d2_hf, 'j_valid_from').data.write(d2_jvf) - s.create_timestamp(d2_hf, 'j_valid_to').data.write(d2_jvt) - - journal.journal_table(s, Schema(), d1_hf, d2_hf, 'id', dr_hf) + s = session.Session() + with session.Session() as s: + dst1=s.open_dataset(d1_bytes,'r+','d1') + d1_hf=dst1.create_dataframe('d1') + d2_hf=dst1.create_dataframe('d2') + dr_hf=dst1.create_dataframe('df') + + s.create_fixed_string(d1_hf, 'id', 1).data.write(d1_id) + s.create_numeric(d1_hf, 'val', 'int32').data.write(d1_v1) + s.create_timestamp(d1_hf, 'j_valid_from').data.write(d1_jvf) + s.create_timestamp(d1_hf, 'j_valid_to').data.write(d1_jvt) + + s.create_fixed_string(d2_hf, 'id', 1).data.write(d2_id) + s.create_numeric(d2_hf, 'val', 'int32').data.write(d2_v1) + s.create_timestamp(d2_hf, 'j_valid_from').data.write(d2_jvf) + s.create_timestamp(d2_hf, 'j_valid_to').data.write(d2_jvt) + + journal.journal_table(s, Schema(), d1_hf, d2_hf, 'id', dr_hf) + + diff --git a/tests/test_operations.py b/tests/test_operations.py index 7e7ab827..12dccf71 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -109,9 +109,10 @@ def test_non_indexed_apply_spans_filter(self): def test_ordered_map_valid_stream(self): - s = session.Session() bio = BytesIO() - with h5py.File(bio, 'w') as hf: + with session.Session() as s: + dst = s.open_dataset(bio, 'r+', 'dst') + hf = dst.create_dataframe('hf') map_field = np.asarray([0, 0, 0, 1, 1, 3, 3, 3, 3, 5, 5, 5, 5, ops.INVALID_INDEX, ops.INVALID_INDEX, 7, 7, 7], dtype=np.int64) @@ -150,9 +151,10 @@ def test_ordered_map_to_right_right_unique(self): def test_ordered_map_to_right_left_unique_streamed(self): - s = session.Session() bio = BytesIO() - with h5py.File(bio, 'w') as hf: + with session.Session() as s: + dst = s.open_dataset(bio, 'r+', 'dst') + hf = dst.create_dataframe('hf') a_ids = np.asarray([0, 1, 2, 3, 5, 6, 7, 8, 10, 11, 12, 13, 15, 16, 17, 18], dtype=np.int64) b_ids = np.asarray([0, 1, 1, 2, 4, 5, 5, 6, 8, 9, 9, 10, 12, 13, 13, 14, @@ -252,9 +254,10 @@ def test_ordered_inner_map(self): self.assertTrue(np.array_equal(b_map, expected_b)) def test_ordered_inner_map_left_unique_streamed(self): - s = session.Session() bio = BytesIO() - with h5py.File(bio, 'w') as hf: + with session.Session() as s: + dst = s.open_dataset(bio,'r+','dst') + hf=dst.create_dataframe('hf') a_ids = np.asarray([0, 1, 2, 3, 5, 6, 7, 8, 10, 11, 12, 13, 15, 16, 17, 18], dtype=np.int64) b_ids = np.asarray([0, 1, 1, 2, 4, 5, 5, 6, 8, 9, 9, 10, 12, 13, 13, 14, @@ -427,9 +430,10 @@ def test_merge_indexed_journalled_entries(self): def test_streaming_sort_merge(self): - s = session.Session() bio = BytesIO() - with h5py.File(bio, 'w') as hf: + with session.Session() as s: + dst = s.open_dataset(bio, 'r+', 'dst') + hf = dst.create_dataframe('hf') rs = np.random.RandomState(12345678) length = 105 segment_length = 25 diff --git a/tests/test_session.py b/tests/test_session.py index 490d0f75..56f07c27 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -72,36 +72,39 @@ def test_merge_left_3(self): def test_merge_left_dataset(self): bio1 = BytesIO() - with h5py.File(bio1, 'w') as src: - s = session.Session() + with session.Session() as s: + src = s.open_dataset(bio1,'w','src') + p_id = np.array([100, 200, 300, 400, 500, 600, 800, 900]) p_val = np.array([-1, -2, -3, -4, -5, -6, -8, -9]) a_pid = np.array([100, 100, 100, 200, 200, 400, 400, 400, 400, 600, 600, 600, 700, 700, 900, 900, 900]) a_val = np.array([10, 11, 12, 23, 22, 40, 43, 42, 41, 60, 61, 63, 71, 71, 94, 93, 92]) - src.create_group('p') + src.create_dataframe('p') s.create_numeric(src['p'], 'id', 'int32').data.write(p_id) s.create_numeric(src['p'], 'val', 'int32').data.write(p_val) - src.create_group('a') + src.create_dataframe('a') s.create_numeric(src['a'], 'pid', 'int32').data.write(a_pid) - bio2 = BytesIO() - with h5py.File(bio1, 'r') as src: - with h5py.File(bio2, 'w') as snk: - s.merge_left(s.get(src['a']['pid']), s.get(src['p']['id']), + bio2 = BytesIO() + dst = s.open_dataset(bio2,'w','dst') + snk=dst.create_dataframe('snk') + s.merge_left(s.get(src['a']['pid']), s.get(src['p']['id']), right_fields=(s.get(src['p']['val']),), right_writers=(s.create_numeric(snk, 'val', 'int32'),) ) - expected = [-1, -1, -1, -2, -2, -4, -4, -4, -4, -6, -6, -6, 0, 0, -9, -9, -9] - actual = s.get(snk['val']).data[:] - self.assertListEqual(expected, actual.data[:].tolist()) + expected = [-1, -1, -1, -2, -2, -4, -4, -4, -4, -6, -6, -6, 0, 0, -9, -9, -9] + actual = s.get(snk['val']).data[:] + self.assertListEqual(expected, actual.data[:].tolist()) def test_ordered_merge_left_2(self): bio = BytesIO() - with h5py.File(bio, 'w') as hf: - s = session.Session() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + hf = dst.create_dataframe('dst') + p_id = np.array([100, 200, 300, 400, 500, 600, 800, 900]) p_val = np.array([-1, -2, -3, -4, -5, -6, -8, -9]) a_pid = np.array([100, 100, 100, 200, 200, 400, 400, 400, 400, 600, @@ -126,8 +129,9 @@ def test_ordered_merge_left_2(self): def test_ordered_merge_right_2(self): bio = BytesIO() - with h5py.File(bio, 'w') as hf: - s = session.Session() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + hf = dst.create_dataframe('dst') p_id = np.array([100, 200, 300, 400, 500, 600, 800, 900]) p_val = np.array([-1, -2, -3, -4, -5, -6, -8, -9]) a_pid = np.array([100, 100, 100, 200, 200, 400, 400, 400, 400, 600, @@ -248,8 +252,9 @@ def test_ordered_merge_inner_fields(self): dtype=np.int32) bio = BytesIO() - with h5py.File(bio, 'w') as hf: - s = session.Session() + with session.Session() as s: + dst = s.open_dataset(bio,'w','dst') + hf=dst.create_dataframe('dst') l_id_f = s.create_fixed_string(hf, 'l_id', 1); l_id_f.data.write(l_id) l_vals_f = s.create_numeric(hf, 'l_vals_f', 'int32'); l_vals_f.data.write(l_vals) l_vals_2_f = s.create_numeric(hf, 'l_vals_2_f', 'int32'); l_vals_2_f.data.write(l_vals_2) @@ -339,7 +344,10 @@ def test_dataset_sort_readers_writers(self): vb = np.asarray([5, 4, 3, 2, 1]) bio = BytesIO() - with h5py.File(bio, 'w') as hf: + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + hf = dst.create_dataframe('dst') + s.create_fixed_string(hf, 'x', 1).data.write(vx) s.create_numeric(hf, 'a', 'int32').data.write(va) s.create_numeric(hf, 'b', 'int32').data.write(vb) @@ -366,7 +374,10 @@ def test_dataset_sort_index_groups(self): vb = np.asarray([5, 4, 3, 2, 1]) bio = BytesIO() - with h5py.File(bio, 'w') as hf: + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + hf = dst.create_dataframe('dst') + s.create_fixed_string(hf, 'x', 1).data.write(vx) s.create_numeric(hf, 'a', 'int32').data.write(va) s.create_numeric(hf, 'b', 'int32').data.write(vb) @@ -390,7 +401,8 @@ def test_sort_on(self): bio = BytesIO() with session.Session(10) as s: - src = s.open_dataset(bio, "w", "src") + dst = s.open_dataset(bio, "w", "src") + src = dst.create_dataframe('ds') idx_f = s.create_fixed_string(src, "idx", 1) val_f = s.create_numeric(src, "val", "int32") val2_f = s.create_indexed_string(src, "val2") @@ -425,7 +437,8 @@ def test_get_spans_one_field(self): with session.Session() as s: self.assertListEqual([0, 1, 3, 5, 6, 9], s.get_spans(vals).tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "src") + ds = dst.create_dataframe('ds') vals_f = s.create_numeric(ds, "vals", "int32") vals_f.data.write(vals) self.assertListEqual([0, 1, 3, 5, 6, 9], s.get_spans(s.get(ds['vals'])).tolist()) @@ -438,7 +451,8 @@ def test_get_spans_two_fields(self): with session.Session() as s: self.assertListEqual([0, 2, 3, 5, 6, 8, 12], s.get_spans(fields=(vals_1, vals_2)).tolist()) - ds = s.open_dataset(bio, 'w', 'ds') + dst = s.open_dataset(bio, "w", "src") + ds = dst.create_dataframe('ds') vals_1_f = s.create_fixed_string(ds, 'vals_1', 1) vals_1_f.data.write(vals_1) vals_2_f = s.create_numeric(ds, 'vals_2', 'int32') @@ -448,7 +462,8 @@ def test_get_spans_two_fields(self): def test_get_spans_index_string_field(self): bio=BytesIO() with session.Session() as s: - ds=s.open_dataset(bio,'w','ds') + dst = s.open_dataset(bio, "w", "src") + ds = dst.create_dataframe('ds') idx= s.create_indexed_string(ds,'idx') idx.data.write(['aa','bb','bb','c','c','c','d','d','e','f','f','f']) self.assertListEqual([0,1,3,6,8,9,12],s.get_spans(idx)) @@ -466,7 +481,8 @@ def test_apply_spans_count(self): results = s.apply_spans_count(spans) self.assertListEqual([1, 2, 3, 4], results.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.apply_spans_count(spans, dest=s.create_numeric(ds, 'result', 'int32')) self.assertListEqual([1, 2, 3, 4], s.get(ds['result']).data[:].tolist()) @@ -480,7 +496,8 @@ def test_apply_spans_first(self): results = s.apply_spans_first(spans, vals) self.assertListEqual([0, 8, 6, 3], results.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.apply_spans_first(spans, vals, dest=s.create_numeric(ds, 'result', 'int64')) self.assertListEqual([0, 8, 6, 3], s.get(ds['result']).data[:].tolist()) @@ -497,7 +514,8 @@ def test_apply_spans_last(self): results = s.apply_spans_last(spans, vals) self.assertListEqual([0, 2, 5, 9], results.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.apply_spans_last(spans, vals, dest=s.create_numeric(ds, 'result', 'int64')) self.assertListEqual([0, 2, 5, 9], s.get(ds['result']).data[:].tolist()) @@ -514,7 +532,8 @@ def test_apply_spans_min(self): results = s.apply_spans_min(spans, vals) self.assertListEqual([0, 2, 4, 1], results.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.apply_spans_min(spans, vals, dest=s.create_numeric(ds, 'result', 'int64')) self.assertListEqual([0, 2, 4, 1], s.get(ds['result']).data[:].tolist()) @@ -531,7 +550,8 @@ def test_apply_spans_max(self): results = s.apply_spans_max(spans, vals) self.assertListEqual([0, 8, 6, 9], results.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.apply_spans_max(spans, vals, dest=s.create_numeric(ds, 'result', 'int64')) self.assertListEqual([0, 8, 6, 9], s.get(ds['result']).data[:].tolist()) @@ -547,7 +567,8 @@ def test_apply_spans_concat(self): spans = s.get_spans(idx) self.assertListEqual([0, 1, 3, 6, 10], spans.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.create_indexed_string(ds, 'vals').data.write(vals) s.apply_spans_concat(spans, s.get(ds['vals']), dest=s.create_indexed_string(ds, 'result')) self.assertListEqual([0, 1, 4, 9, 16], s.get(ds['result']).indices[:].tolist()) @@ -561,7 +582,8 @@ def test_apply_spans_concat_2(self): spans = s.get_spans(idx) self.assertListEqual([0, 2, 3, 5, 6, 10], spans.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.create_indexed_string(ds, 'vals').data.write(vals) s.apply_spans_concat(spans, s.get(ds['vals']), dest=s.create_indexed_string(ds, 'result')) self.assertListEqual([0, 7, 8, 15, 20, 35], s.get(ds['result']).indices[:].tolist()) @@ -581,7 +603,8 @@ def test_apply_spans_concat_field(self): # results = s.apply_spans_concat(spans, vals) # self.assertListEqual([0, 8, 6, 9], results.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') # s.apply_spans_concat(spans, vals, dest=s.create_indexed_string(ds, 'result')) # self.assertListEqual([0, 8, 6, 9], s.get(ds['result']).data[:].tolist()) @@ -609,7 +632,8 @@ def test_apply_spans_concat_small_chunk_size(self): self.assertListEqual([0, 3, 5, 8, 10, 13, 15, 18, 20, 23, 25, 28, 30, 33, 35, 38, 40, 43, 45, 48, 50], spans.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.create_indexed_string(ds, 'vals').data.write(vals) expected_indices = [0, @@ -637,7 +661,8 @@ def test_aggregate_count(self): results = s.aggregate_count(idx) self.assertListEqual([1, 2, 3, 4], results.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.aggregate_count(idx, dest=s.create_numeric(ds, 'result', 'int32')) self.assertListEqual([1, 2, 3, 4], s.get(ds['result']).data[:].tolist()) @@ -649,7 +674,8 @@ def test_aggregate_first(self): results = s.aggregate_first(idx, vals) self.assertListEqual([0, 8, 6, 3], results.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.aggregate_first(idx, vals, dest=s.create_numeric(ds, 'result', 'int64')) self.assertListEqual([0, 8, 6, 3], s.get(ds['result']).data[:].tolist()) @@ -665,7 +691,8 @@ def test_aggregate_last(self): results = s.aggregate_last(idx, vals) self.assertListEqual([0, 2, 5, 9], results.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.aggregate_last(idx, vals, dest=s.create_numeric(ds, 'result', 'int64')) self.assertListEqual([0, 2, 5, 9], s.get(ds['result']).data[:].tolist()) @@ -681,7 +708,8 @@ def test_aggregate_min(self): results = s.aggregate_min(idx, vals) self.assertListEqual([0, 2, 4, 1], results.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.aggregate_min(idx, vals, dest=s.create_numeric(ds, 'result', 'int64')) self.assertListEqual([0, 2, 4, 1], s.get(ds['result']).data[:].tolist()) @@ -697,7 +725,8 @@ def test_aggregate_max(self): results = s.aggregate_max(idx, vals) self.assertListEqual([0, 8, 6, 9], results.tolist()) - ds = s.open_dataset(bio, "w", "ds") + dst = s.open_dataset(bio, "w", "ds") + ds = dst.create_dataframe('ds') s.aggregate_max(idx, vals, dest=s.create_numeric(ds, 'result', 'int64')) self.assertListEqual([0, 8, 6, 9], s.get(ds['result']).data[:].tolist()) @@ -811,9 +840,10 @@ def test_write_then_read_indexed_string(self): class TestSessionImporters(unittest.TestCase): def test_indexed_string_importer(self): - s = session.Session() bio = BytesIO() - with h5py.File(bio, 'w') as hf: + with session.Session() as s: + dst = s.open_dataset(bio, 'r+', 'dst') + hf = dst.create_dataframe('hf') values = ['', '', '1.0.0', '', '1.0.ä', '1.0.0', '1.0.0', '1.0.0', '', '', '1.0.0', '1.0.0', '', '1.0.0', '1.0.ä', '1.0.0', ''] im = fields.IndexedStringImporter(s, hf, 'x') @@ -836,9 +866,10 @@ def test_indexed_string_importer(self): self.assertListEqual(expected, f.values[:].tolist()) def test_fixed_string_importer(self): - s = session.Session() bio = BytesIO() - with h5py.File(bio, 'w') as hf: + with session.Session() as s: + dst=s.open_dataset(bio,'r+','dst') + hf=dst.create_dataframe('hf') values = ['', '', '1.0.0', '', '1.0.ä', '1.0.0', '1.0.0', '1.0.0', '', '', '1.0.0', '1.0.0', '', '1.0.0', '1.0.ä', '1.0.0', ''] bvalues = [v.encode() for v in values] @@ -852,9 +883,10 @@ def test_fixed_string_importer(self): self.assertListEqual(expected, f.data[:].tolist()) def test_numeric_importer(self): - s = session.Session() bio = BytesIO() - with h5py.File(bio, 'w') as hf: + with session.Session() as s: + dst = s.open_dataset(bio, 'r+', 'dst') + hf = dst.create_dataframe('hf') values = ['', 'one', '2', '3.0', '4e1', '5.21e-2', 'foo', '-6', '-7.0', '-8e1', '-9.21e-2', '', 'one', '2', '3.0', '4e1', '5.21e-2', 'foo', '-6', '-7.0', '-8e1', '-9.21e-2'] im = fields.NumericImporter(s, hf, 'x', 'float32', per.try_str_to_float) @@ -869,9 +901,10 @@ def test_numeric_importer(self): def test_date_importer(self): from datetime import datetime - s = session.Session() bio = BytesIO() - with h5py.File(bio, 'w') as hf: + with session.Session() as s: + dst = s.open_dataset(bio,'r+','dst') + hf = dst.create_dataframe('hf') values = ['2020-05-10', '2020-05-12', '2020-05-12', '2020-05-15'] im = fields.DateImporter(s, hf, 'x') im.write(values) From 7cfecebf109fb9bfc82f823b76da007b8cda2600 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 7 Apr 2021 15:33:55 +0100 Subject: [PATCH 037/181] add license info to a few files --- exetera/core/data_schema.py | 11 +++++++++++ exetera/core/data_writer.py | 11 +++++++++++ exetera/core/dataframe.py | 11 +++++++++++ exetera/core/dataset.py | 10 ++++++++++ exetera/core/fields.py | 10 ++++++++++ exetera/core/session.py | 11 +++++++++++ exetera/core/validation.py | 12 ++++++++++++ 7 files changed, 76 insertions(+) diff --git a/exetera/core/data_schema.py b/exetera/core/data_schema.py index a17f427e..bfdc3feb 100644 --- a/exetera/core/data_schema.py +++ b/exetera/core/data_schema.py @@ -1,3 +1,14 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import exetera from exetera.core import readerwriter as rw diff --git a/exetera/core/data_writer.py b/exetera/core/data_writer.py index 62392340..aee9c146 100644 --- a/exetera/core/data_writer.py +++ b/exetera/core/data_writer.py @@ -1,3 +1,14 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from threading import Thread class DataWriter: diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 4ec6312b..6692ef95 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -1,3 +1,14 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from exetera.core import fields as fld from datetime import datetime,timezone from exetera.core import dataset as dst diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 7835a44c..34965fe3 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -1,3 +1,13 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. class Dataset(): """ diff --git a/exetera/core/fields.py b/exetera/core/fields.py index a2e8d3f0..92a33c51 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -1,3 +1,13 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from typing import Union from datetime import datetime, timezone diff --git a/exetera/core/session.py b/exetera/core/session.py index 377eb736..fbc2dc47 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -1,3 +1,14 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import os import uuid from datetime import datetime, timezone diff --git a/exetera/core/validation.py b/exetera/core/validation.py index 52d82107..5faf5de2 100644 --- a/exetera/core/validation.py +++ b/exetera/core/validation.py @@ -1,3 +1,15 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + import numpy as np import h5py From eaac2b68840b5fb690c99005bbd5b674a11fd35b Mon Sep 17 00:00:00 2001 From: clyyuanzi-london <59363720+clyyuanzi-london@users.noreply.github.com> Date: Thu, 8 Apr 2021 12:08:57 +0100 Subject: [PATCH 038/181] csv_reader_with_njit --- exetera/core/csv_reader_speedup.py | 176 ++++++++++++++++++++++ resources/assessment_input_small_data.csv | 10 ++ 2 files changed, 186 insertions(+) create mode 100644 exetera/core/csv_reader_speedup.py create mode 100644 resources/assessment_input_small_data.csv diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py new file mode 100644 index 00000000..9817574d --- /dev/null +++ b/exetera/core/csv_reader_speedup.py @@ -0,0 +1,176 @@ +import csv +import time +from numba import njit +import numpy as np + +def main(): + source = 'resources/assessment_input_small_data.csv' + original_csv_read(source) + file_read_line_fast_csv(source) + + +# original csv reader +def original_csv_read(source): + time0 = time.time() + with open(source) as f: + csvf = csv.reader(f, delimiter=',', quotechar='"') + + for i_r, row in enumerate(csvf): + pass + + print('Original csv reader took {} s'.format(time.time() - time0)) + + +# FAST open file read line +def file_read_line_fast_csv(source): + time0 = time.time() + #input_lines = [] + with open(source) as f: + header = csv.DictReader(f) + res = f.read() + + excel = my_fast_csv_reader_string(res) + # print(excel) + print('FAST Open file read lines took {} s'.format(time.time() - time0)) + + + + + +@njit +def my_fast_csv_reader_string(source, column_inds = None, column_vals = None): + ESCAPE_VALUE = '"' + SEPARATOR_VALUE = ',' + NEWLINE_VALUE = '\n' + + #colcount = len(column_inds) + #max_rowcount = len(column_inds[0])-1 + + index = np.int64(0) + line_start = np.int64(0) + cell_start = np.int64(0) + cell_end = np.int64(0) + col_index = np.int32(0) + row_index = np.int32(0) + + fieldnames = None + colcount = 0 + cell_value = [''] + cell_value.pop() + row = [''] + row.pop() + excel = [] + + # how to parse csv + # . " is the escape character + # . fields that need to contain '"', ',' or '\n' must be quoted + # . while escaped + # . ',' and '\n' are considered part of the field + # . i.e. a,"b,c","d\ne","f""g""" + # . while not escaped + # . ',' ends the cell and starts a new cell + # . '\n' ends the cell and starts a new row + # . after the first row, we should check that subsequent rows have the same cell count + escaped = False + end_cell = False + end_line = False + escaped_literal_candidate = False + while True: + c = source[index] + + if c == SEPARATOR_VALUE: + if not escaped: #or escaped_literal_candidate: + # don't write this char + end_cell = True + while index + 1 < len(source) and source[index + 1] == ' ': + index += 1 + + cell_start = index + 1 + + else: + # write literal ',' + cell_value.append(c) + + elif c == NEWLINE_VALUE: + if not escaped: #or escaped_literal_candidate: + # don't write this char + end_cell = True + end_line = True + + else: + # write literal '\n' + cell_value.append(c) + + elif c == ESCAPE_VALUE: + # ,"... - start of an escaped cell + # ...", - end of an escaped cell + # ...""... - literal quote character + # otherwise error + if not escaped: + # this must be the first character of a cell + if index != cell_start: + # raise error! + pass + # don't write this char + else: + escaped = True + else: + + escaped = False + # if escaped_literal_candidate: + # escaped_literal_candidate = False + # # literal quote character confirmed, write it + # cell_value.append(c) + # else: + # escaped_literal_candidate = True + # # don't write this char + + else: + cell_value.append(c) + # if escaped_literal_candidate: + # # error! + # pass + # # raise error return -2 + + # parse c + index += 1 + + + if end_cell: + end_cell = False + #column_inds[col_index][row_index+1] =\ + # column_inds[col_index][row_index] + cell_end - cell_start + row.append(''.join(cell_value)) + cell_value = [''] + cell_value.pop() + + col_index += 1 + + if end_line and fieldnames is None and row is not None: + fieldnames = row + colcount = len(row) + + + if col_index == colcount: + if not end_line: + raise Exception('.....') + else: + end_line = False + + row_index += 1 + col_index = 0 + excel.append(row) + row = [''] + row.pop() + #if row_index == max_rowcount: + #return index + + if index == len(source): + # "erase the partially written line" + return excel + #return line_start + + + +if __name__ == "__main__": + main() diff --git a/resources/assessment_input_small_data.csv b/resources/assessment_input_small_data.csv new file mode 100644 index 00000000..bc040d1c --- /dev/null +++ b/resources/assessment_input_small_data.csv @@ -0,0 +1,10 @@ +id,patient_id,created_at,updated_at,version,country_code,health_status,date_test_occurred,date_test_occurred_guess,fever,temperature,temperature_unit,persistent_cough,fatigue,shortness_of_breath,diarrhoea,diarrhoea_frequency,delirium,skipped_meals,location,treatment,had_covid_test,tested_covid_positive,abdominal_pain,chest_pain,hoarse_voice,loss_of_smell,headache,headache_frequency,other_symptoms,chills_or_shivers,eye_soreness,nausea,dizzy_light_headed,red_welts_on_face_or_lips,blisters_on_feet,typical_hayfever,sore_throat,unusual_muscle_pains,level_of_isolation,isolation_little_interaction,isolation_lots_of_people,isolation_healthcare_provider,always_used_shortage,have_used_PPE,never_used_shortage,sometimes_used_shortage,interacted_any_patients,treated_patients_with_covid,worn_face_mask,mask_cloth_or_scarf,mask_surgical,mask_n95_ffp,mask_not_sure_pfnts,mask_other,rash,skin_burning,hair_loss,feeling_down,brain_fog,altered_smell,runny_nose,sneezing,earache,ear_ringing,swollen_glands,irregular_heartbeat +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, \ No newline at end of file From a9ce1fb7c4404b15edcb948004368981cf5f859d Mon Sep 17 00:00:00 2001 From: clyyuanzi-london <59363720+clyyuanzi-london@users.noreply.github.com> Date: Fri, 9 Apr 2021 12:06:23 +0100 Subject: [PATCH 039/181] change output_excel from string to int --- exetera/core/csv_reader_speedup.py | 332 ++++++ resources/assessment_input_small_data.csv | 1297 +++++++++++++++++++++ 2 files changed, 1629 insertions(+) create mode 100644 exetera/core/csv_reader_speedup.py create mode 100644 resources/assessment_input_small_data.csv diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py new file mode 100644 index 00000000..44d6e8ac --- /dev/null +++ b/exetera/core/csv_reader_speedup.py @@ -0,0 +1,332 @@ +import csv +import time +from numba import njit +import numpy as np + + +class Timer: + def __init__(self, start_msg, new_line=False, end_msg='completed in'): + print(start_msg, end=': ' if new_line is False else '\n') + self.end_msg = end_msg + + def __enter__(self): + self.t0 = time.time() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + print(self.end_msg + f' {time.time() - self.t0} seconds') + + +def main(): + source = 'resources/assessment_input_large_data.csv' + + original_csv_read(source) + file_read_line_fast_csv(source) + + with Timer('Original csv reader took:'): + original_csv_read(source) + + with Timer('FAST Open file read lines took:'): + file_read_line_fast_csv(source) + + +# original csv reader +def original_csv_read(source): + time0 = time.time() + with open(source) as f: + csvf = csv.reader(f, delimiter=',', quotechar='"') + + for i_r, row in enumerate(csvf): + pass + + #print('Original csv reader took {} s'.format(time.time() - time0)) + + +# FAST open file read line +def file_read_line_fast_csv(source): + time0 = time.time() + #input_lines = [] + with open(source) as f: + header = csv.DictReader(f) + content = f.read() + + index_excel = my_fast_csv_reader_int(content) + + for row in index_excel: + for (s,e) in row: + r = content[s:e] + + # print(excel) + # print('FAST Open file read lines took {} s'.format(time.time() - time0)) + + +@njit +def my_fast_csv_reader_int(source): + ESCAPE_VALUE = '"' + SEPARATOR_VALUE = ',' + NEWLINE_VALUE = '\n' + + index = np.int64(0) + line_start = np.int64(0) + cell_start_idx = np.int64(0) + cell_end_idx = np.int64(0) + col_index = np.int64(0) + row_index = np.int64(0) + + fieldnames = None + colcount = np.int64(0) + row = [(0,0)] + row.pop() + excel = [] + + # how to parse csv + # . " is the escape character + # . fields that need to contain '"', ',' or '\n' must be quoted + # . while escaped + # . ',' and '\n' are considered part of the field + # . i.e. a,"b,c","d\ne","f""g""" + # . while not escaped + # . ',' ends the cell and starts a new cell + # . '\n' ends the cell and starts a new row + # . after the first row, we should check that subsequent rows have the same cell count + escaped = False + end_cell = False + end_line = False + escaped_literal_candidate = False + while True: + c = source[index] + + if c == SEPARATOR_VALUE: + if not escaped: #or escaped_literal_candidate: + # don't write this char + end_cell = True + cell_end_idx = index + # while index + 1 < len(source) and source[index + 1] == ' ': + # index += 1 + + + else: + # write literal ',' + # cell_value.append(c) + pass + + elif c == NEWLINE_VALUE: + if not escaped: #or escaped_literal_candidate: + # don't write this char + end_cell = True + end_line = True + cell_end_idx = index + else: + # write literal '\n' + pass + #cell_value.append(c) + + elif c == ESCAPE_VALUE: + # ,"... - start of an escaped cell + # ...", - end of an escaped cell + # ...""... - literal quote character + # otherwise error + if not escaped: + # this must be the first character of a cell + if index != cell_start_idx: + # raise error! + pass + # don't write this char + else: + escaped = True + else: + + escaped = False + # if escaped_literal_candidate: + # escaped_literal_candidate = False + # # literal quote character confirmed, write it + # cell_value.append(c) + # else: + # escaped_literal_candidate = True + # # don't write this char + + else: + # cell_value.append(c) + pass + # if escaped_literal_candidate: + # # error! + # pass + # # raise error return -2 + + # parse c + index += 1 + + if end_cell: + end_cell = False + #column_inds[col_index][row_index+1] =\ + # column_inds[col_index][row_index] + cell_end - cell_start + row.append((cell_start_idx, cell_end_idx)) + + cell_start_idx = cell_end_idx + 1 + + col_index += 1 + + if end_line and fieldnames is None and row is not None: + fieldnames = row + colcount = len(row) + + if col_index == colcount: + if not end_line: + raise Exception('.....') + else: + end_line = False + + row_index += np.int64(1) + col_index = np.int64(0) + excel.append(row) + row = [(0,0)] + row.pop() + #print(row) + #print(excel) + + + if index == len(source): + # "erase the partially written line" + return excel + #return line_start + + + + +@njit +def my_fast_csv_reader_string(source, column_inds = None, column_vals = None): + ESCAPE_VALUE = '"' + SEPARATOR_VALUE = ',' + NEWLINE_VALUE = '\n' + + #colcount = len(column_inds) + #max_rowcount = len(column_inds[0])-1 + + index = np.int64(0) + line_start = np.int64(0) + cell_start = np.int64(0) + cell_end = np.int64(0) + col_index = np.int32(0) + row_index = np.int32(0) + + fieldnames = None + colcount = 0 + cell_value = [''] + cell_value.pop() + row = [''] + row.pop() + excel = [] + + # how to parse csv + # . " is the escape character + # . fields that need to contain '"', ',' or '\n' must be quoted + # . while escaped + # . ',' and '\n' are considered part of the field + # . i.e. a,"b,c","d\ne","f""g""" + # . while not escaped + # . ',' ends the cell and starts a new cell + # . '\n' ends the cell and starts a new row + # . after the first row, we should check that subsequent rows have the same cell count + escaped = False + end_cell = False + end_line = False + escaped_literal_candidate = False + while True: + c = source[index] + + if c == SEPARATOR_VALUE: + if not escaped: #or escaped_literal_candidate: + # don't write this char + end_cell = True + while index + 1 < len(source) and source[index + 1] == ' ': + index += 1 + + cell_start = index + 1 + + else: + # write literal ',' + cell_value.append(c) + + elif c == NEWLINE_VALUE: + if not escaped: #or escaped_literal_candidate: + # don't write this char + end_cell = True + end_line = True + + else: + # write literal '\n' + cell_value.append(c) + + elif c == ESCAPE_VALUE: + # ,"... - start of an escaped cell + # ...", - end of an escaped cell + # ...""... - literal quote character + # otherwise error + if not escaped: + # this must be the first character of a cell + if index != cell_start: + # raise error! + pass + # don't write this char + else: + escaped = True + else: + + escaped = False + # if escaped_literal_candidate: + # escaped_literal_candidate = False + # # literal quote character confirmed, write it + # cell_value.append(c) + # else: + # escaped_literal_candidate = True + # # don't write this char + + else: + cell_value.append(c) + # if escaped_literal_candidate: + # # error! + # pass + # # raise error return -2 + + # parse c + index += 1 + + + if end_cell: + end_cell = False + #column_inds[col_index][row_index+1] =\ + # column_inds[col_index][row_index] + cell_end - cell_start + row.append(''.join(cell_value)) + cell_value = [''] + cell_value.pop() + + col_index += 1 + + if end_line and fieldnames is None and row is not None: + fieldnames = row + colcount = len(row) + + + if col_index == colcount: + if not end_line: + raise Exception('.....') + else: + end_line = False + + row_index += 1 + col_index = 0 + excel.append(row) + row = [''] + row.pop() + #if row_index == max_rowcount: + #return index + + if index == len(source): + # "erase the partially written line" + return excel + #return line_start + + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/resources/assessment_input_small_data.csv b/resources/assessment_input_small_data.csv new file mode 100644 index 00000000..72167bb5 --- /dev/null +++ b/resources/assessment_input_small_data.csv @@ -0,0 +1,1297 @@ +id,patient_id,created_at,updated_at,version,country_code,health_status,date_test_occurred,date_test_occurred_guess,fever,temperature,temperature_unit,persistent_cough,fatigue,shortness_of_breath,diarrhoea,diarrhoea_frequency,delirium,skipped_meals,location,treatment,had_covid_test,tested_covid_positive,abdominal_pain,chest_pain,hoarse_voice,loss_of_smell,headache,headache_frequency,other_symptoms,chills_or_shivers,eye_soreness,nausea,dizzy_light_headed,red_welts_on_face_or_lips,blisters_on_feet,typical_hayfever,sore_throat,unusual_muscle_pains,level_of_isolation,isolation_little_interaction,isolation_lots_of_people,isolation_healthcare_provider,always_used_shortage,have_used_PPE,never_used_shortage,sometimes_used_shortage,interacted_any_patients,treated_patients_with_covid,worn_face_mask,mask_cloth_or_scarf,mask_surgical,mask_n95_ffp,mask_not_sure_pfnts,mask_other,rash,skin_burning,hair_loss,feeling_down,brain_fog,altered_smell,runny_nose,sneezing,earache,ear_ringing,swollen_glands,irregular_heartbeat +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, \ No newline at end of file From e9d1053fc5b034651338eb771d3352e45d863466 Mon Sep 17 00:00:00 2001 From: clyyuanzi-london <59363720+clyyuanzi-london@users.noreply.github.com> Date: Fri, 9 Apr 2021 14:09:57 +0100 Subject: [PATCH 040/181] initialize column_idx matrix outside of the njit function --- exetera/core/csv_reader_speedup.py | 200 +++++------------------------ 1 file changed, 30 insertions(+), 170 deletions(-) diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py index c6488fe3..4b1ed5d7 100644 --- a/exetera/core/csv_reader_speedup.py +++ b/exetera/core/csv_reader_speedup.py @@ -1,12 +1,12 @@ import csv import time -from numba import njit +from numba import njit,jit import numpy as np class Timer: - def __init__(self, start_msg, new_line=False, end_msg='completed in'): - print(start_msg, end=': ' if new_line is False else '\n') + def __init__(self, start_msg, new_line=False, end_msg=''): + print(start_msg + ': ' if new_line is False else '\n') self.end_msg = end_msg def __enter__(self): @@ -20,14 +20,17 @@ def __exit__(self, exc_type, exc_val, exc_tb): def main(): source = 'resources/assessment_input_small_data.csv' + print(source) + # run once first original_csv_read(source) - file_read_line_fast_csv(source) - with Timer('Original csv reader took:'): + with Timer("Original csv reader took:"): original_csv_read(source) - with Timer('FAST Open file read lines took:'): + file_read_line_fast_csv(source) + with Timer("FAST Open file read lines took"): file_read_line_fast_csv(source) + # original csv reader @@ -39,33 +42,40 @@ def original_csv_read(source): for i_r, row in enumerate(csvf): pass - #print('Original csv reader took {} s'.format(time.time() - time0)) + # print('Original csv reader took {} s'.format(time.time() - time0)) # FAST open file read line def file_read_line_fast_csv(source): - time0 = time.time() - #input_lines = [] + with open(source) as f: header = csv.DictReader(f) + count_columns = len(header.fieldnames) content = f.read() + count_rows = content.count('\n') + + #print(count_rows, count_columns) + #print(content) + column_inds = np.zeros(count_rows * count_columns, dtype = np.int64).reshape(count_rows, count_columns) + #content_nparray = np.array(list(content)) - index_excel = my_fast_csv_reader_int(content) + my_fast_csv_reader_int(content, column_inds) - for row in index_excel: - for (s,e) in row: - r = content[s:e] - - # print(excel) - # print('FAST Open file read lines took {} s'.format(time.time() - time0)) + for row in column_inds: + #print(row) + for i, e in enumerate(row): + pass @njit -def my_fast_csv_reader_int(source): +def my_fast_csv_reader_int(source, column_inds): ESCAPE_VALUE = '"' SEPARATOR_VALUE = ',' NEWLINE_VALUE = '\n' + #max_rowcount = len(column_inds) - 1 + colcount = len(column_inds[0]) + index = np.int64(0) line_start = np.int64(0) cell_start_idx = np.int64(0) @@ -73,12 +83,6 @@ def my_fast_csv_reader_int(source): col_index = np.int64(0) row_index = np.int64(0) - fieldnames = None - colcount = np.int64(0) - row = [(0,0)] - row.pop() - excel = [] - # how to parse csv # . " is the escape character # . fields that need to contain '"', ',' or '\n' must be quoted @@ -160,153 +164,13 @@ def my_fast_csv_reader_int(source): end_cell = False #column_inds[col_index][row_index+1] =\ # column_inds[col_index][row_index] + cell_end - cell_start - row.append((cell_start_idx, cell_end_idx)) + column_inds[row_index][col_index] = cell_end_idx cell_start_idx = cell_end_idx + 1 col_index += 1 - if end_line and fieldnames is None and row is not None: - fieldnames = row - colcount = len(row) - if col_index == colcount: - if not end_line: - raise Exception('.....') - else: - end_line = False - - row_index += np.int64(1) - col_index = np.int64(0) - excel.append(row) - row = [(0,0)] - row.pop() - #print(row) - #print(excel) - - - if index == len(source): - # "erase the partially written line" - return excel - #return line_start - - - - -@njit -def my_fast_csv_reader_string(source, column_inds = None, column_vals = None): - ESCAPE_VALUE = '"' - SEPARATOR_VALUE = ',' - NEWLINE_VALUE = '\n' - - #colcount = len(column_inds) - #max_rowcount = len(column_inds[0])-1 - - index = np.int64(0) - line_start = np.int64(0) - cell_start = np.int64(0) - cell_end = np.int64(0) - col_index = np.int32(0) - row_index = np.int32(0) - - fieldnames = None - colcount = 0 - cell_value = [''] - cell_value.pop() - row = [''] - row.pop() - excel = [] - - # how to parse csv - # . " is the escape character - # . fields that need to contain '"', ',' or '\n' must be quoted - # . while escaped - # . ',' and '\n' are considered part of the field - # . i.e. a,"b,c","d\ne","f""g""" - # . while not escaped - # . ',' ends the cell and starts a new cell - # . '\n' ends the cell and starts a new row - # . after the first row, we should check that subsequent rows have the same cell count - escaped = False - end_cell = False - end_line = False - escaped_literal_candidate = False - while True: - c = source[index] - - if c == SEPARATOR_VALUE: - if not escaped: #or escaped_literal_candidate: - # don't write this char - end_cell = True - while index + 1 < len(source) and source[index + 1] == ' ': - index += 1 - - cell_start = index + 1 - - else: - # write literal ',' - cell_value.append(c) - - elif c == NEWLINE_VALUE: - if not escaped: #or escaped_literal_candidate: - # don't write this char - end_cell = True - end_line = True - - else: - # write literal '\n' - cell_value.append(c) - - elif c == ESCAPE_VALUE: - # ,"... - start of an escaped cell - # ...", - end of an escaped cell - # ...""... - literal quote character - # otherwise error - if not escaped: - # this must be the first character of a cell - if index != cell_start: - # raise error! - pass - # don't write this char - else: - escaped = True - else: - - escaped = False - # if escaped_literal_candidate: - # escaped_literal_candidate = False - # # literal quote character confirmed, write it - # cell_value.append(c) - # else: - # escaped_literal_candidate = True - # # don't write this char - - else: - cell_value.append(c) - # if escaped_literal_candidate: - # # error! - # pass - # # raise error return -2 - - # parse c - index += 1 - - - if end_cell: - end_cell = False - #column_inds[col_index][row_index+1] =\ - # column_inds[col_index][row_index] + cell_end - cell_start - row.append(''.join(cell_value)) - cell_value = [''] - cell_value.pop() - - col_index += 1 - - if end_line and fieldnames is None and row is not None: - fieldnames = row - colcount = len(row) - - if col_index == colcount: if not end_line: raise Exception('.....') @@ -315,15 +179,11 @@ def my_fast_csv_reader_string(source, column_inds = None, column_vals = None): row_index += 1 col_index = 0 - excel.append(row) - row = [''] - row.pop() - #if row_index == max_rowcount: - #return index + if index == len(source): # "erase the partially written line" - return excel + return column_inds #return line_start From e1ed80d804970fb81c5d7b1a3ec84f0217358183 Mon Sep 17 00:00:00 2001 From: clyyuanzi-london <59363720+clyyuanzi-london@users.noreply.github.com> Date: Fri, 9 Apr 2021 15:51:21 +0100 Subject: [PATCH 041/181] use np.fromfile to load the file into byte array --- exetera/core/csv_reader_speedup.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py index 4b1ed5d7..03250a92 100644 --- a/exetera/core/csv_reader_speedup.py +++ b/exetera/core/csv_reader_speedup.py @@ -19,7 +19,6 @@ def __exit__(self, exc_type, exc_val, exc_tb): def main(): source = 'resources/assessment_input_small_data.csv' - print(source) # run once first original_csv_read(source) @@ -52,12 +51,11 @@ def file_read_line_fast_csv(source): header = csv.DictReader(f) count_columns = len(header.fieldnames) content = f.read() - count_rows = content.count('\n') + count_rows = content.count('\n') + 1 + + content = np.fromfile(source, dtype='|S1') - #print(count_rows, count_columns) - #print(content) column_inds = np.zeros(count_rows * count_columns, dtype = np.int64).reshape(count_rows, count_columns) - #content_nparray = np.array(list(content)) my_fast_csv_reader_int(content, column_inds) @@ -69,9 +67,9 @@ def file_read_line_fast_csv(source): @njit def my_fast_csv_reader_int(source, column_inds): - ESCAPE_VALUE = '"' - SEPARATOR_VALUE = ',' - NEWLINE_VALUE = '\n' + ESCAPE_VALUE = b'"' + SEPARATOR_VALUE = b',' + NEWLINE_VALUE = b'\n' #max_rowcount = len(column_inds) - 1 colcount = len(column_inds[0]) @@ -99,7 +97,6 @@ def my_fast_csv_reader_int(source, column_inds): escaped_literal_candidate = False while True: c = source[index] - if c == SEPARATOR_VALUE: if not escaped: #or escaped_literal_candidate: # don't write this char From a057677b8032a19086ac115ae9f920e9e5b9fc3b Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Mon, 12 Apr 2021 00:33:31 +0100 Subject: [PATCH 042/181] Refactoring and reformatting of some of the dataset / dataframe code; moving Session and Dataset to abstract types; fixing of is_sorted tests that were broken with the merge of the new functionality --- exetera/core/abstract_types.py | 281 +++++++++++++++++++++++++++++++++ exetera/core/dataframe.py | 124 ++++++++------- exetera/core/dataset.py | 138 +++++++++------- exetera/core/fields.py | 39 +++-- exetera/core/session.py | 86 +++++++--- tests/test_dataframe.py | 79 ++++----- tests/test_dataset.py | 15 +- tests/test_fields.py | 25 +-- tests/test_session.py | 5 +- 9 files changed, 577 insertions(+), 215 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index 507781c8..bc136844 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -10,6 +10,7 @@ # limitations under the License. from abc import ABC, abstractmethod +from datetime import datetime, timezone class Field(ABC): @@ -29,6 +30,14 @@ def timestamp(self): def chunksize(self): raise NotImplementedError() + @abstractmethod + def writeable(self): + raise NotImplementedError() + + @abstractmethod + def create_like(self, group, name, timestamp=None): + raise NotImplementedError() + @property @abstractmethod def is_sorted(self): @@ -55,3 +64,275 @@ def __len__(self): @abstractmethod def get_spans(self): raise NotImplementedError() + + +class Dataset(ABC): + """ + DataSet is a container of dataframes + """ + + @property + @abstractmethod + def session(self): + raise NotImplementedError() + + @abstractmethod + def close(self): + raise NotImplementedError() + + @abstractmethod + def add(self, field, name=None): + raise NotImplementedError() + + @abstractmethod + def __contains__(self, name): + raise NotImplementedError() + + @abstractmethod + def contains_dataframe(self, dataframe): + raise NotImplementedError() + + @abstractmethod + def __getitem__(self, name): + raise NotImplementedError() + + @abstractmethod + def get_dataframe(self, name): + raise NotImplementedError() + + @abstractmethod + def get_name(self, dataframe): + raise NotImplementedError() + + @abstractmethod + def __setitem__(self, name, dataframe): + raise NotImplementedError() + + @abstractmethod + def __delitem__(self, name): + raise NotImplementedError() + + @abstractmethod + def delete_dataframe(self, dataframe): + raise NotImplementedError() + + @abstractmethod + def list(self): + raise NotImplementedError() + + @abstractmethod + def __iter__(self): + raise NotImplementedError() + + @abstractmethod + def __next__(self): + raise NotImplementedError() + + @abstractmethod + def __len__(self): + raise NotImplementedError() + + +class AbstractSession(ABC): + + @abstractmethod + def __enter__(self): + raise NotImplementedError() + + @abstractmethod + def __exit__(self, etype, evalue, etraceback): + raise NotImplementedError() + + @abstractmethod + def open_dataset(self, dataset_path, mode, name): + raise NotImplementedError() + + @abstractmethod + def close_dataset(self, name): + raise NotImplementedError() + + @abstractmethod + def list_datasets(self): + raise NotImplementedError() + + @abstractmethod + def get_dataset(self, name): + raise NotImplementedError() + + @abstractmethod + def close(self): + raise NotImplementedError() + + @abstractmethod + def get_shared_index(self, keys): + raise NotImplementedError() + + @abstractmethod + def set_timestamp(self, timestamp=str(datetime.now(timezone.utc))): + raise NotImplementedError() + + @abstractmethod + def sort_on(self, src_group, dest_group, keys, timestamp, + write_mode='write', verbose=True): + raise NotImplementedError() + + @abstractmethod + def dataset_sort_index(self, sort_indices, index=None): + raise NotImplementedError() + + @abstractmethod + def apply_filter(self, filter_to_apply, src, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_index(self, index_to_apply, src, dest=None): + raise NotImplementedError() + + @abstractmethod + def distinct(self, field=None, fields=None, filter=None): + raise NotImplementedError() + + @abstractmethod + def get_spans(self, field=None, fields=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_index_of_min(self, spans, src, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_index_of_max(self, spans, src, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_index_of_first(self, spans, src, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_count(self, spans, src=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_min(self, spans, src, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_max(self, spans, src, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_first(self, spans, src, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_last(self, spans, src, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_concat(self, spans, src, dest, + src_chunksize=None, dest_chunksize=None, chunksize_mult=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_count(self, index=None, src=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_first(self, index, src=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_last(self, index, src=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_min(self, index, src=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_max(self, index, src=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_custom(self, index, src=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def join(self, destination_pkey, fkey_indices, values_to_join, + writer=None, fkey_index_spans=None): + raise NotImplementedError() + + @abstractmethod + def predicate_and_join(self, predicate, destination_pkey, fkey_indices, + reader=None, writer=None, fkey_index_spans=None): + raise NotImplementedError() + + @abstractmethod + def get(self, field): + raise NotImplementedError() + + @abstractmethod + def create_like(self, field, dest_group, dest_name, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_indexed_string(self, group, name, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_fixed_string(self, group, name, length, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_categorical(self, group, name, nformat, key, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_numeric(self, group, name, nformat, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_timestamp(self, group, name, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def get_or_create_group(self, group, name): + raise NotImplementedError() + + @abstractmethod + def chunks(self, length, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def process(self, inputs, outputs, predicate): + raise NotImplementedError() + + @abstractmethod + def get_index(self, target, foreign_key, destination=None): + raise NotImplementedError() + + @abstractmethod + def merge_left(self, left_on, right_on, right_fields=tuple(), right_writers=None): + raise NotImplementedError() + + @abstractmethod + def merge_right(self, left_on, right_on, left_fields=tuple(), left_writers=None): + raise NotImplementedError() + + @abstractmethod + def merge_inner(self, left_on, right_on, + left_fields=None, left_writers=None, + right_fields=None, right_writers=None): + raise NotImplementedError() + + @abstractmethod + def ordered_merge_left(self, left_on, right_on, + right_field_sources=tuple(), left_field_sinks=None, + left_to_right_map=None, left_unique=False, right_unique=False): + raise NotImplementedError() + + @abstractmethod + def ordered_merge_right(self, right_on, left_on, + left_field_sources=tuple(), right_field_sinks=None, + right_to_left_map=None, right_unique=False, left_unique=False): + raise NotImplementedError() diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 6692ef95..057f1d44 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -9,16 +9,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from exetera.core.abstract_types import AbstractSession, Dataset from exetera.core import fields as fld -from datetime import datetime,timezone -from exetera.core import dataset as dst +from exetera.core import dataset as dst import h5py -class DataFrame(): + +class DataFrame: """ DataFrame is a table of data that contains a list of Fields (columns) """ - def __init__(self, name, dataset,data=None,h5group:h5py.Group=None): + def __init__(self, + dataset: Dataset, + name: str, + dataframe: dict = None, + h5group: h5py.Group = None): """ Create a Dataframe object. @@ -29,15 +34,20 @@ def __init__(self, name, dataset,data=None,h5group:h5py.Group=None): h5group<-group-dataset structure, the group has a 'fieldtype' attribute and the dataset is named 'values'. """ + # TODO: consider columns as a name rather than fields self.fields = dict() self.name = name self.dataset = dataset - if isinstance(dataset,dst.HDF5Dataset): - dataset[name]=self - if data is not None: - if isinstance(data,dict) and isinstance(list(data.items())[0][0],str) and isinstance(list(data.items())[0][1], fld.Field) : - self.fields=data - elif h5group is not None and isinstance(h5group,h5py.Group): + if isinstance(dataset, dst.HDF5Dataset): + dataset[name] = self + if dataframe is not None: + if isinstance(dataframe, dict) and isinstance(list(dataframe.items())[0][0], str) and\ + isinstance(list(dataframe.items())[0][1], fld.Field): + self.fields = dataframe + else: + raise ValueError("if set, 'dataframe' must be a dictionary mapping strings to fields") + + elif h5group is not None and isinstance(h5group, h5py.Group): fieldtype_map = { 'indexedstring': fld.IndexedStringField, 'fixedstring': fld.FixedStringField, @@ -53,15 +63,16 @@ def __init__(self, name, dataset,data=None,h5group:h5py.Group=None): self.fields[subg] = fieldtype_map[fieldtype](self, h5group[subg]) print(" ") - def add(self,field,name=None): + def add(self, field, name=None): if name is not None: - if not isinstance(name,str): + if not isinstance(name, str): raise TypeError("The name must be a str object.") else: - self.fields[name]=field - self.fields[field.name[field.name.index('/',1)+1:]]=field #note the name has '/' for hdf5 object + self.fields[name] = field + # note the name has '/' for hdf5 object + self.fields[field.name[field.name.index('/', 1)+1:]] = field - def create_group(self,name): + def create_group(self, name): """ Create a group object in HDF5 file for field to use. @@ -72,51 +83,53 @@ def create_group(self,name): return self.dataset.file["/"+self.name+"/"+name] - - def create_numeric(self, session, name, nformat, timestamp=None, chunksize=None): - fld.numeric_field_constructor(session, self, name, nformat, timestamp, chunksize) - field=fld.NumericField(session, self.dataset.file["/"+self.name+"/"+name], write_enabled=True) - self.fields[name]=field + def create_numeric(self, name, nformat, timestamp=None, chunksize=None): + fld.numeric_field_constructor(self.dataset.session, self, name, nformat, timestamp, chunksize) + field = fld.NumericField(self.dataset.session, self.dataset.file["/"+self.name+"/"+name], + write_enabled=True) + self.fields[name] = field return self.fields[name] - def create_indexed_string(self, session, name, timestamp=None, chunksize=None): - fld.indexed_string_field_constructor(session, self, name, timestamp, chunksize) - field= fld.IndexedStringField(session, self.dataset.file["/"+self.name+"/"+name], write_enabled=True) + def create_indexed_string(self, name, timestamp=None, chunksize=None): + fld.indexed_string_field_constructor(self.dataset.session, self, name, timestamp, chunksize) + field = fld.IndexedStringField(self.dataset.session, self.dataset.file["/"+self.name+"/"+name], + write_enabled=True) self.fields[name] = field return self.fields[name] - def create_fixed_string(self, session, name, length, timestamp=None, chunksize=None): - fld.fixed_string_field_constructor(session, self, name, length, timestamp, chunksize) - field= fld.FixedStringField(session, self.dataset.file["/"+self.name+"/"+name], write_enabled=True) + def create_fixed_string(self, name, length, timestamp=None, chunksize=None): + fld.fixed_string_field_constructor(self.dataset.session, self, name, length, timestamp, chunksize) + field = fld.FixedStringField(self.dataset.session, self.dataset.file["/"+self.name+"/"+name], + write_enabled=True) self.fields[name] = field return self.fields[name] - def create_categorical(self, session, name, nformat, key, - timestamp=None, chunksize=None): - fld.categorical_field_constructor(session, self, name, nformat, key, + def create_categorical(self, name, nformat, key, timestamp=None, chunksize=None): + fld.categorical_field_constructor(self.dataset.session, self, name, nformat, key, timestamp, chunksize) - field= fld.CategoricalField(session, self.dataset.file["/"+self.name+"/"+name], write_enabled=True) + field = fld.CategoricalField(self.dataset.session, self.dataset.file["/"+self.name+"/"+name], + write_enabled=True) self.fields[name] = field return self.fields[name] - def create_timestamp(self, session, name, timestamp=None, chunksize=None): - fld.timestamp_field_constructor(session, self, name, timestamp, chunksize) - field= fld.TimestampField(session, self.dataset.file["/"+self.name+"/"+name], write_enabled=True) + def create_timestamp(self, name, timestamp=None, chunksize=None): + fld.timestamp_field_constructor(self.dataset.session, self, name, timestamp, chunksize) + field = fld.TimestampField(self.dataset.session, self.dataset.file["/"+self.name+"/"+name], + write_enabled=True) self.fields[name] = field return self.fields[name] - def __contains__(self, name): """ check if dataframe contains a field, by the field name name: the name of the field to check,return a bool """ - if not isinstance(name,str): + if not isinstance(name, str): raise TypeError("The name must be a str object.") else: return self.fields.__contains__(name) - def contains_field(self,field): + def contains_field(self, field): """ check if dataframe contains a field by the field object field: the filed object to check, return a tuple(bool,str). The str is the name stored in dataframe. @@ -127,39 +140,37 @@ def contains_field(self,field): for v in self.fields.values(): if id(field) == id(v): return True - break return False def __getitem__(self, name): - if not isinstance(name,str): + if not isinstance(name, str): raise TypeError("The name must be a str object.") elif not self.__contains__(name): raise ValueError("Can not find the name from this dataframe.") else: return self.fields[name] - def get_field(self,name): + def get_field(self, name): return self.__getitem__(name) - def get_name(self,field): + def get_name(self, field): """ Get the name of the field in dataframe. """ - if not isinstance(field,fld.Field): + if not isinstance(field, fld.Field): raise TypeError("The field argument must be a Field object.") - for name,v in self.fields.items(): + for name, v in self.fields.items(): if id(field) == id(v): return name - break return None def __setitem__(self, name, field): - if not isinstance(name,str): + if not isinstance(name, str): raise TypeError("The name must be a str object.") - elif not isinstance(field,fld.Field): + elif not isinstance(field, fld.Field): raise TypeError("The field must be a Field object.") else: - self.fields[name]=field + self.fields[name] = field return True def __delitem__(self, name): @@ -169,7 +180,7 @@ def __delitem__(self, name): del self.fields[name] return True - def delete_field(self,field): + def delete_field(self, field): """ Remove field from dataframe by field """ @@ -196,6 +207,7 @@ def __iter__(self): def __next__(self): return next(self.fields) + """ def search(self): #is search similar to get & get_name? pass @@ -207,12 +219,12 @@ def get_spans(self): """ Return the name and spans of each field as a dictionary. """ - spans={} - for name,field in self.fields.items(): - spans[name]=field.get_spans() + spans = {} + for name, field in self.fields.items(): + spans[name] = field.get_spans() return spans - def apply_filter(self,filter_to_apply,ddf=None): + def apply_filter(self, filter_to_apply, ddf=None): """ Apply the filter to all the fields in this dataframe, return a dataframe with filtered fields. @@ -221,19 +233,18 @@ def apply_filter(self,filter_to_apply,ddf=None): :returns: a dataframe contains all the fields filterd, self if ddf is not set """ if ddf is not None: - if not isinstance(ddf,DataFrame): + if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") for name, field in self.fields.items(): # TODO integration w/ session, dataset - newfld = field.create_like(ddf,field.name) - ddf.add(field.apply_filter(filter_to_apply,dstfld=newfld),name=name) + newfld = field.create_like(ddf, field.name) + ddf.add(field.apply_filter(filter_to_apply, dstfld=newfld), name=name) return ddf else: for field in self.fields.values(): field.apply_filter(filter_to_apply) return self - def apply_index(self, index_to_apply, ddf=None): """ Apply the index to all the fields in this dataframe, return a dataframe with indexed fields. @@ -247,10 +258,9 @@ def apply_index(self, index_to_apply, ddf=None): raise TypeError("The destination object must be an instance of DataFrame.") for name, field in self.fields.items(): newfld = field.create_like(ddf, field.name) - ddf.add(field.apply_index(index_to_apply,dstfld=newfld), name=name) + ddf.add(field.apply_index(index_to_apply, dstfld=newfld), name=name) return ddf else: for field in self.fields.values(): field.apply_index(index_to_apply) return self - diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 34965fe3..b3c076d0 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -9,67 +9,78 @@ # See the License for the specific language governing permissions and # limitations under the License. -class Dataset(): - """ - DataSet is a container of dataframes - """ - def __init__(self,file_path,name): - pass - - def close(self): - pass - - def add(self, field, name=None): - pass - - def __contains__(self, name): - pass - - def contains_dataframe(self, dataframe): - pass - - def __getitem__(self, name): - pass - - def get_dataframe(self, name): - pass - - def get_name(self, dataframe): - pass - - def __setitem__(self, name, dataframe): - pass - - def __delitem__(self, name): - pass - - def delete_dataframe(self, dataframe): - pass - - def list(self): - pass - - def __iter__(self): - pass - - def __next__(self): - pass - - def __len__(self): - pass +# class Dataset(): +# """ +# DataSet is a container of dataframes +# """ +# def __init__(self,file_path,name): +# pass +# +# def close(self): +# pass +# +# def add(self, field, name=None): +# pass +# +# def __contains__(self, name): +# pass +# +# def contains_dataframe(self, dataframe): +# pass +# +# def __getitem__(self, name): +# pass +# +# def get_dataframe(self, name): +# pass +# +# def get_name(self, dataframe): +# pass +# +# def __setitem__(self, name, dataframe): +# pass +# +# def __delitem__(self, name): +# pass +# +# def delete_dataframe(self, dataframe): +# pass +# +# def list(self): +# pass +# +# def __iter__(self): +# pass +# +# def __next__(self): +# pass +# +# def __len__(self): +# pass import h5py +from exetera.core.abstract_types import Dataset from exetera.core import dataframe as edf + + class HDF5Dataset(Dataset): - def __init__(self, dataset_path, mode, name): + def __init__(self, session, dataset_path, mode, name): + self._session = session self.file = h5py.File(dataset_path, mode) self.dataframes = dict() + + @property + def session(self): + return self._session + + def close(self): self.file.close() - def create_dataframe(self,name): + + def create_dataframe(self, name): """ Create a group object in HDF5 file and a Exetera dataframe in memory. @@ -77,8 +88,8 @@ def create_dataframe(self,name): :return: a dataframe object """ self.file.create_group(name) - dataframe = edf.DataFrame(name,self) - self.dataframes[name]=dataframe + dataframe = edf.DataFrame(self, name) + self.dataframes[name] = dataframe return dataframe @@ -91,14 +102,15 @@ def add(self, dataframe, name=None): :param name: optional- change the dataframe name """ dname = dataframe if name is None else name - self.file.copy(dataframe.dataset[dataframe.name],self.file,name=dname) - df = edf.DataFrame(dname,self,h5group=self.file[dname]) - self.dataframes[dname]=df + self.file.copy(dataframe.dataset[dataframe.name], self.file, name=dname) + df = edf.DataFrame(self, dname, h5group=self.file[dname]) + self.dataframes[dname] = df def __contains__(self, name): return self.dataframes.__contains__(name) + def contains_dataframe(self, dataframe): """ Check if a dataframe is contained in this dataset by the dataframe object itself. @@ -112,20 +124,22 @@ def contains_dataframe(self, dataframe): for v in self.dataframes.values(): if id(dataframe) == id(v): return True - break return False + def __getitem__(self, name): - if not isinstance(name,str): + if not isinstance(name, str): raise TypeError("The name must be a str object.") elif not self.__contains__(name): raise ValueError("Can not find the name from this dataset.") else: return self.dataframes[name] + def get_dataframe(self, name): self.__getitem__(name) + def get_name(self, dataframe): """ Get the name of the dataframe in this dataset. @@ -138,6 +152,7 @@ def get_name(self, dataframe): break return None + def __setitem__(self, name, dataframe): if not isinstance(name, str): raise TypeError("The name must be a str object.") @@ -147,6 +162,7 @@ def __setitem__(self, name, dataframe): self.dataframes[name] = dataframe return True + def __delitem__(self, name): if not self.__contains__(name): raise ValueError("This dataframe does not contain the name to delete.") @@ -154,6 +170,7 @@ def __delitem__(self, name): del self.dataframes[name] return True + def delete_dataframe(self, dataframe): """ Remove dataframe from this dataset by dataframe object. @@ -164,23 +181,30 @@ def delete_dataframe(self, dataframe): else: self.__delitem__(name) + def list(self): return tuple(n for n in self.dataframes.keys()) + def keys(self): return self.file.keys() + def values(self): return self.file.values() + def items(self): return self.file.items() + def __iter__(self): return iter(self.dataframes) + def __next__(self): return next(self.dataframes) + def __len__(self): return len(self.dataframes) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 5da9ac4e..32e76893 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -371,7 +371,7 @@ def writeable(self): def create_like(self, group, name, timestamp=None): ts = self.timestamp if timestamp is None else timestamp - return group.create_indexed_string(self._session, name, ts, self.chunksize) + return group.create_indexed_string(name, ts, self.chunksize) @property @@ -420,7 +420,7 @@ def __len__(self): def get_spans(self): return ops._get_spans_for_index_string_field(self.indices[:], self.values[:]) - def apply_filter(self,filter_to_apply,dstfld=None): + def apply_filter(self, filter_to_apply, dstfld=None): """ Apply a filter (array of boolean) to the field, return itself if destination field (detfld) is not set. """ @@ -437,7 +437,7 @@ def apply_filter(self,filter_to_apply,dstfld=None): dstfld.indices.clear() dstfld.indices.write(dest_indices) if len(dstfld.values) == len(dest_values): - dstfld.values[:]=dest_values + dstfld.values[:] = dest_values else: dstfld.values.clear() dstfld.values.write(dest_values) @@ -476,7 +476,7 @@ def writeable(self): def create_like(self, group, name, timestamp=None): ts = self.timestamp if timestamp is None else timestamp length = self._field.attrs['strlen'] - return group.create_fixed_string(self._session,name,length,ts,self.chunksize) + return group.create_fixed_string(name, length, ts, self.chunksize) @property def data(self): @@ -537,7 +537,7 @@ def writeable(self): def create_like(self, group, name, timestamp=None): ts = self.timestamp if timestamp is None else timestamp nformat = self._field.attrs['nformat'] - return group.create_numeric(self._session,name,nformat,ts,self.chunksize) + return group.create_numeric(name, nformat, ts, self.chunksize) @property def data(self): @@ -598,7 +598,7 @@ def create_like(self, group, name, timestamp=None): ts = self.timestamp if timestamp is None else timestamp nformat = self._field.attrs['nformat'] if 'nformat' in self._field.attrs else 'int8' keys = {v: k for k, v in self.keys.items()} - return group.create_categorical(self._session,name,nformat,keys,ts,self.chunksize) + return group.create_categorical(name, nformat, keys, ts, self.chunksize) @property def data(self): @@ -668,7 +668,7 @@ def writeable(self): def create_like(self, group, name, timestamp=None): ts = self.timestamp if timestamp is None else timestamp - return group.create_timestamp(self._session, name, ts, self.chunksize) + return group.create_timestamp(name, ts, self.chunksize) @property def data(self): @@ -721,7 +721,7 @@ def apply_index(self, index_to_apply, dstfld=None): class IndexedStringImporter: def __init__(self, session, group, name, timestamp=None, chunksize=None): - self._field=group.create_indexed_string(session,name,timestamp,chunksize) + self._field = group.create_indexed_string(name, timestamp, chunksize) def chunk_factory(self, length): return [None] * length @@ -739,7 +739,7 @@ def write(self, values): class FixedStringImporter: def __init__(self, session, group, name, length, timestamp=None, chunksize=None): - self._field=group.create_fixed_string(session,name,length,timestamp,chunksize) + self._field = group.create_fixed_string(name, length, timestamp, chunksize) def chunk_factory(self, length): return np.zeros(length, dtype=self._field.data.dtype) @@ -758,8 +758,8 @@ def write(self, values): class NumericImporter: def __init__(self, session, group, name, dtype, parser, timestamp=None, chunksize=None): filter_name = '{}_valid'.format(name) - self._field=group.create_numeric(session,name,dtype, timestamp, chunksize) - self._filter_field=group.create_numeric(session,filter_name, 'bool',timestamp, chunksize) + self._field = group.create_numeric(name, dtype, timestamp, chunksize) + self._filter_field = group.create_numeric(filter_name, 'bool', timestamp, chunksize) chunksize = session.chunksize if chunksize is None else chunksize self._parser = parser self._values = np.zeros(chunksize, dtype=self._field.data.dtype) @@ -789,7 +789,7 @@ def write(self, values): class CategoricalImporter: def __init__(self, session, group, name, value_type, keys, timestamp=None, chunksize=None): chunksize = session.chunksize if chunksize is None else chunksize - self._field=group.create_categorical(session,name,value_type,keys,timestamp,chunksize) + self._field = group.create_categorical(name, value_type, keys, timestamp, chunksize) self._keys = keys self._dtype = value_type self._key_type = 'U{}'.format(max(len(k.encode()) for k in keys)) @@ -815,8 +815,8 @@ def __init__(self, session, group, name, value_type, keys, out_of_range, timestamp=None, chunksize=None): chunksize = session.chunksize if chunksize is None else chunksize out_of_range_name = '{}_{}'.format(name, out_of_range) - self._field=group.create_categorical(session,name, value_type, keys,timestamp, chunksize) - self._str_field =group.create_indexed_string(session,out_of_range_name,timestamp, chunksize) + self._field = group.create_categorical(name, value_type, keys, timestamp, chunksize) + self._str_field = group.create_indexed_string(out_of_range_name, timestamp, chunksize) self._keys = keys self._dtype = value_type self._key_type = 'S{}'.format(max(len(k.encode()) for k in keys)) @@ -859,14 +859,13 @@ class DateTimeImporter: def __init__(self, session, group, name, optional=False, write_days=False, timestamp=None, chunksize=None): chunksize = session.chunksize if chunksize is None else chunksize - self._field =group.create_timestamp(session,name, timestamp, chunksize) - self._results = np.zeros(chunksize , dtype='float64') + self._field = group.create_timestamp(name, timestamp, chunksize) + self._results = np.zeros(chunksize, dtype=np.float64) self._optional = optional if optional is True: filter_name = '{}_set'.format(name) - numeric_field_constructor(session, group, filter_name, 'bool', - timestamp, chunksize) + numeric_field_constructor(group, filter_name, 'bool', timestamp, chunksize) self._filter_field = NumericField(session, group, filter_name, write_enabled=True) def chunk_factory(self, length): @@ -902,8 +901,8 @@ def write(self, values): class DateImporter: def __init__(self, session, group, name, optional=False, timestamp=None, chunksize=None): - self._field=group.create_timestamp(session,name, timestamp, chunksize) - self._results = np.zeros(chunksize, dtype='float64') + self._field = group.create_timestamp(name, timestamp, chunksize) + self._results = np.zeros(() if chunksize is None else chunksize, dtype='float64') if optional is True: filter_name = '{}_set'.format(name) diff --git a/exetera/core/session.py b/exetera/core/session.py index fbc2dc47..dbdac53c 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -19,7 +19,7 @@ import h5py -from exetera.core.abstract_types import Field +from exetera.core.abstract_types import Field, AbstractSession from exetera.core import operations from exetera.core import persistence as per from exetera.core import fields as fld @@ -68,7 +68,7 @@ * provide field objects with additional api """ -class Session: +class Session(AbstractSession): def __init__(self, chunksize=ops.DEFAULT_CHUNKSIZE, timestamp=str(datetime.now(timezone.utc))): @@ -100,7 +100,7 @@ def open_dataset(self, dataset_path, mode, name): raise ValueError("A dataset with name '{}' is already open, and must be closed first.".format(name)) #self.datasets[name] = h5py.File(dataset_path, h5py_modes[mode]) - self.datasets[name] = ds.HDF5Dataset(dataset_path,mode,name) + self.datasets[name] = ds.HDF5Dataset(self, dataset_path, mode, name) return self.datasets[name] @@ -652,42 +652,82 @@ def create_like(self, field, dest_group, dest_name, timestamp=None, chunksize=No def create_indexed_string(self, group, name, timestamp=None, chunksize=None): - if isinstance(group,ds.Dataset): - pass - elif isinstance(group,df.DataFrame): - return group.create_indexed_string(self,name, timestamp,chunksize) + + if not isinstance(group, (df.DataFrame, h5py.Group)): + if isinstance(group, ds.Dataset): + raise ValueError("'group' must be an ExeTera DataFrame rather than a" + " top-level Dataset") + else: + raise ValueError("'group' must be an Exetera DataFrame but a " + "{} was passed to it".format(type(group))) + + if isinstance(group, h5py.Group): + return fld.indexed_string_field_constructor(self, group, name, timestamp, chunksize) + else: + return group.create_indexed_string(name, timestamp, chunksize) def create_fixed_string(self, group, name, length, timestamp=None, chunksize=None): - if isinstance(group,ds.Dataset): - pass - elif isinstance(group,df.DataFrame): - return group.create_fixed_string(self,name, length,timestamp,chunksize) + if not isinstance(group, (df.DataFrame, h5py.Group)): + if isinstance(group, ds.Dataset): + raise ValueError("'group' must be an ExeTera DataFrame rather than a" + " top-level Dataset") + else: + raise ValueError("'group' must be an Exetera DataFrame but a " + "{} was passed to it".format(type(group))) + if isinstance(group, h5py.Group): + return fld.fixed_string_field_constructor(self, group, name, timestamp, chunksize) + else: + return group.create_fixed_string(name, length, timestamp, chunksize) def create_categorical(self, group, name, nformat, key, timestamp=None, chunksize=None): - if isinstance(group, ds.Dataset): - pass - elif isinstance(group, df.DataFrame): - return group.create_categorical(self, name, nformat,key,timestamp,chunksize) + if not isinstance(group, (df.DataFrame, h5py.Group)): + if isinstance(group, ds.Dataset): + raise ValueError("'group' must be an ExeTera DataFrame rather than a" + " top-level Dataset") + else: + raise ValueError("'group' must be an Exetera DataFrame but a " + "{} was passed to it".format(type(group))) + + if isinstance(group, h5py.Group): + return fld.categorical_field_constructor(self, group, name, timestamp, chunksize) + else: + return group.create_categorical(name, nformat, key, timestamp, chunksize) def create_numeric(self, group, name, nformat, timestamp=None, chunksize=None): - if isinstance(group,ds.Dataset): - pass - elif isinstance(group,df.DataFrame): - return group.create_numeric(self,name, nformat, timestamp, chunksize) + if not isinstance(group, (df.DataFrame, h5py.Group)): + if isinstance(group, ds.Dataset): + raise ValueError("'group' must be an ExeTera DataFrame rather than a" + " top-level Dataset") + else: + raise ValueError("'group' must be an Exetera DataFrame but a " + "{} was passed to it".format(type(group))) + if isinstance(group, h5py.Group): + return fld.numeric_field_constructor(self. group, name, timestamp, chunksize) + else: + return group.create_numeric(name, nformat, timestamp, chunksize) def create_timestamp(self, group, name, timestamp=None, chunksize=None): - if isinstance(group,ds.Dataset): - pass - elif isinstance(group,df.DataFrame): - return group.create_timestamp(self,name, timestamp, chunksize) + if not isinstance(group, (df.DataFrame, h5py.Group)): + if isinstance(group, ds.Dataset): + raise ValueError("'group' must be an ExeTera DataFrame rather than a" + " top-level Dataset") + else: + raise ValueError("'group' must be an Exetera DataFrame but a " + "{} was passed to it".format(type(group))) + + if isinstance(group, h5py.Group): + return fld.categorical_field_constructor(self, group, name, timestamp, chunksize) + else: + return group.create_timestamp(name, timestamp, chunksize) + #TODO: update this method for the existence of dataframes def get_or_create_group(self, group, name): if name in group: return group[name] diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index cef3c556..caf69a01 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -11,34 +11,39 @@ class TestDataFrame(unittest.TestCase): def test_dataframe_init(self): - bio=BytesIO() + bio = BytesIO() with session.Session() as s: - dst = s.open_dataset(bio,'w','dst') - #init - df = dataframe.DataFrame('dst',dst) + dst = s.open_dataset(bio, 'w', 'dst') + + # init + df = dataframe.DataFrame(dst, 'dst') self.assertTrue(isinstance(df, dataframe.DataFrame)) - numf = df.create_numeric(s,'numf','uint32') - fdf = {'numf',numf} - df2 = dataframe.DataFrame('dst2',dst,data=fdf) - self.assertTrue(isinstance(df2,dataframe.DataFrame)) - #add & set & contains + numf = df.create_numeric('numf', 'uint32') + fdf = {'numf': numf} + df2 = dataframe.DataFrame(dst, 'dst2', dataframe=fdf) + self.assertTrue(isinstance(df2, dataframe.DataFrame)) + + # add & set & contains df.add(numf) self.assertTrue('numf' in df) self.assertTrue(df.contains_field(numf)) - cat=s.create_categorical(df2,'cat','int8',{'a':1,'b':2}) + cat = s.create_categorical(df2, 'cat', 'int8', {'a': 1, 'b': 2}) self.assertFalse('cat' in df) self.assertFalse(df.contains_field(cat)) - df['cat']=cat + df['cat'] = cat self.assertTrue('cat' in df) - #list & get - self.assertEqual(id(numf),id(df.get_field('numf'))) + + # list & get + self.assertEqual(id(numf), id(df.get_field('numf'))) self.assertEqual(id(numf), id(df['numf'])) - self.assertEqual('numf',df.get_name(numf)) - #list & iter + self.assertEqual('numf', df.get_name(numf)) + + # list & iter dfit = iter(df) - self.assertEqual('numf',next(dfit)) + self.assertEqual('numf', next(dfit)) self.assertEqual('cat', next(dfit)) - #del & del by field + + # del & del by field del df['numf'] self.assertFalse('numf' in df) df.delete_field(cat) @@ -48,41 +53,39 @@ def test_dataframe_init(self): def test_dataframe_init_fromh5(self): bio = BytesIO() with session.Session() as s: - ds=s.open_dataset(bio,'w','ds') + ds=s.open_dataset(bio, 'w', 'ds') dst = ds.create_dataframe('dst') - num=s.create_numeric(dst,'num','uint8') - num.data.write([1,2,3,4,5,6,7]) - df = dataframe.DataFrame('dst',dst,h5group=dst) + num=s.create_numeric(dst,'num', 'uint8') + num.data.write([1, 2, 3, 4, 5, 6, 7]) + df = dataframe.DataFrame(dst, 'dst', h5group=dst) def test_dataframe_create_field(self): bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, 'r+', 'dst') - df = dataframe.DataFrame('dst',dst) - num = df.create_numeric(s,'num','uint32') - num.data.write([1,2,3,4]) - self.assertEqual([1,2,3,4],num.data[:].tolist()) + df = dataframe.DataFrame(dst, 'dst',) + num = df.create_numeric('num', 'uint32') + num.data.write([1, 2, 3, 4]) + self.assertEqual([1, 2, 3, 4], num.data[:].tolist()) def test_dataframe_ops(self): bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, 'w', 'dst') - df = dataframe.DataFrame('dst',dst) + df = dataframe.DataFrame(dst, 'dst') numf = s.create_numeric(df, 'numf', 'int32') - numf.data.write([5,4,3,2,1]) + numf.data.write([5, 4, 3, 2, 1]) df.add(numf) - fst = s.create_fixed_string(df,'fst',3) - fst.data.write([b'e',b'd',b'c',b'b',b'a']) + fst = s.create_fixed_string(df, 'fst', 3) + fst.data.write([b'e', b'd', b'c', b'b', b'a']) df.add(fst) - index=np.array([4,3,2,1,0]) - ddf = dataframe.DataFrame('dst2',dst) - df.apply_index(index,ddf) - self.assertEqual([1,2,3,4,5],ddf.get_field('numf').data[:].tolist()) - self.assertEqual([b'a',b'b',b'c',b'd',b'e'],ddf.get_field('fst').data[:].tolist()) + index = np.array([4, 3, 2, 1, 0]) + ddf = dataframe.DataFrame(dst, 'dst2') + df.apply_index(index, ddf) + self.assertEqual([1, 2, 3, 4, 5], ddf.get_field('numf').data[:].tolist()) + self.assertEqual([b'a', b'b', b'c', b'd', b'e'], ddf.get_field('fst').data[:].tolist()) - filter= np.array([True,True,False,False,True]) - df.apply_filter(filter) + filter_to_apply = np.array([True, True, False, False, True]) + df.apply_filter(filter_to_apply) self.assertEqual([5, 4, 1], df.get_field('numf').data[:].tolist()) self.assertEqual([b'e', b'd', b'a'], df.get_field('fst').data[:].tolist()) - - diff --git a/tests/test_dataset.py b/tests/test_dataset.py index f1ccb7a0..d9a8dd80 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -3,18 +3,19 @@ from exetera.core import session from io import BytesIO + class TestDataSet(unittest.TestCase): def test_dataset_init(self): - bio=BytesIO() + bio = BytesIO() with session.Session() as s: - dst=s.open_dataset(bio,'r+','dst') - df=dst.create_dataframe('df') - num=s.create_numeric(df,'num','int32') - num.data.write([1,2,3,4]) - self.assertEqual([1,2,3,4],num.data[:].tolist()) + dst = s.open_dataset(bio, 'r+', 'dst') + df = dst.create_dataframe('df') + num = s.create_numeric(df,'num', 'int32') + num.data.write([1, 2, 3, 4]) + self.assertEqual([1, 2, 3, 4], num.data[:].tolist()) - num2=s.create_numeric(df,'num2','int32') + num2 = s.create_numeric(df, 'num2', 'int32') num2 = s.get(df['num2']) def test_dataset_ops(self): diff --git a/tests/test_fields.py b/tests/test_fields.py index 57197c9b..d4153e4c 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -60,13 +60,14 @@ def test_indexed_string_is_sorted(self): bio = BytesIO() with session.Session() as s: ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('foo') - f = s.create_indexed_string(ds, 'f') + f = df.create_indexed_string('f') vals = ['the', 'quick', '', 'brown', 'fox', 'jumps', '', 'over', 'the', 'lazy', '', 'dog'] f.data.write(vals) self.assertFalse(f.is_sorted()) - f2 = s.create_indexed_string(ds, 'f2') + f2 = df.create_indexed_string('f2') svals = sorted(vals) f2.data.write(svals) self.assertTrue(f2.is_sorted()) @@ -75,13 +76,14 @@ def test_fixed_string_is_sorted(self): bio = BytesIO() with session.Session() as s: ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('foo') - f = s.create_fixed_string(ds, 'f', 5) + f = df.create_fixed_string('f', 5) vals = ['a', 'ba', 'bb', 'bac', 'de', 'ddddd', 'deff', 'aaaa', 'ccd'] f.data.write([v.encode() for v in vals]) self.assertFalse(f.is_sorted()) - f2 = s.create_fixed_string(ds, 'f2', 5) + f2 = df.create_fixed_string('f2', 5) svals = sorted(vals) f2.data.write([v.encode() for v in svals]) self.assertTrue(f2.is_sorted()) @@ -90,13 +92,14 @@ def test_numeric_is_sorted(self): bio = BytesIO() with session.Session() as s: ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('foo') - f = s.create_numeric(ds, 'f', 'int32') + f = df.create_numeric('f', 'int32') vals = [74, 1897, 298, 0, -100098, 380982340, 8, 6587, 28421, 293878] f.data.write(vals) self.assertFalse(f.is_sorted()) - f2 = s.create_numeric(ds, 'f2', 'int32') + f2 = df.create_numeric('f2', 'int32') svals = sorted(vals) f2.data.write(svals) self.assertTrue(f2.is_sorted()) @@ -105,13 +108,14 @@ def test_categorical_is_sorted(self): bio = BytesIO() with session.Session() as s: ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('foo') - f = s.create_categorical(ds, 'f', 'int8', {'a': 0, 'c': 1, 'd': 2, 'b': 3}) + f = df.create_categorical('f', 'int8', {'a': 0, 'c': 1, 'd': 2, 'b': 3}) vals = [0, 1, 3, 2, 3, 2, 2, 0, 0, 1, 2] f.data.write(vals) self.assertFalse(f.is_sorted()) - f2 = s.create_categorical(ds, 'f2', 'int8', {'a': 0, 'c': 1, 'd': 2, 'b': 3}) + f2 = df.create_categorical('f2', 'int8', {'a': 0, 'c': 1, 'd': 2, 'b': 3}) svals = sorted(vals) f2.data.write(svals) self.assertTrue(f2.is_sorted()) @@ -122,8 +126,9 @@ def test_timestamp_is_sorted(self): bio = BytesIO() with session.Session() as s: ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('foo') - f = s.create_timestamp(ds, 'f') + f = df.create_timestamp('f') d = D(2020, 5, 10) vals = [d + T(seconds=50000), d - T(days=280), d + T(weeks=2), d + T(weeks=250), d - T(weeks=378), d + T(hours=2897), d - T(days=23), d + T(minutes=39873)] @@ -131,7 +136,7 @@ def test_timestamp_is_sorted(self): f.data.write(vals) self.assertFalse(f.is_sorted()) - f2 = s.create_timestamp(ds, 'f2') + f2 = df.create_timestamp('f2') svals = sorted(vals) f2.data.write(svals) self.assertTrue(f2.is_sorted()) diff --git a/tests/test_session.py b/tests/test_session.py index 56f07c27..37030ed9 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -73,7 +73,7 @@ def test_merge_left_3(self): def test_merge_left_dataset(self): bio1 = BytesIO() with session.Session() as s: - src = s.open_dataset(bio1,'w','src') + src = s.open_dataset(bio1, 'w', 'src') p_id = np.array([100, 200, 300, 400, 500, 600, 800, 900]) p_val = np.array([-1, -2, -3, -4, -5, -6, -8, -9]) @@ -92,8 +92,7 @@ def test_merge_left_dataset(self): snk=dst.create_dataframe('snk') s.merge_left(s.get(src['a']['pid']), s.get(src['p']['id']), right_fields=(s.get(src['p']['val']),), - right_writers=(s.create_numeric(snk, 'val', 'int32'),) - ) + right_writers=(s.create_numeric(snk, 'val', 'int32'),)) expected = [-1, -1, -1, -2, -2, -4, -4, -4, -4, -6, -6, -6, 0, 0, -9, -9, -9] actual = s.get(snk['val']).data[:] self.assertListEqual(expected, actual.data[:].tolist()) From db3ec9f88f33774a8d9e7f9a185ac19196d1d95c Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Mon, 12 Apr 2021 15:08:06 +0100 Subject: [PATCH 043/181] Work on fast csv reading --- exetera/core/csv_reader_speedup.py | 297 +++++++++++++++------- resources/assessment_input_small_data.csv | 3 - tests/test_dataset.py | 4 - 3 files changed, 205 insertions(+), 99 deletions(-) diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py index 03250a92..3382625b 100644 --- a/exetera/core/csv_reader_speedup.py +++ b/exetera/core/csv_reader_speedup.py @@ -17,29 +17,75 @@ def __exit__(self, exc_type, exc_val, exc_tb): print(self.end_msg + f' {time.time() - self.t0} seconds') +# def generate_test_arrays(count): +# strings = [b'one', b'two', b'three', b'four', b'five', b'six', b'seven'] +# raw_values = np.random.RandomState(12345678).randint(low=1, high=7, size=count) +# total_len = 0 +# for r in raw_values: +# total_len += len(strings[r]) +# indices = np.zeros(count+1, dtype=np.int64) +# values = np.zeros(total_len, dtype=np.int8) +# for i_r, r in enumerate(raw_values): +# indices[i_r+1] = indices[i_r] + len(strings[r]) +# for i_c in range(len(strings[r])): +# values[indices[i_r]+i_c] = strings[r][i_c] +# +# for i_r in range(20): +# start, end = indices[i_r], indices[i_r+1] +# print(values[start:end].tobytes()) + + def main(): - source = 'resources/assessment_input_small_data.csv' + # generate_test_arrays(1000) + col_dicts = [{'name': 'a', 'type': 'cat', 'vals': ('a', 'bb', 'ccc', 'dddd', 'eeeee')}, + {'name': 'b', 'type': 'float'}, + {'name': 'c', 'type': 'cat', 'vals': ('', '', '', '', '', 'True', 'False')}, + {'name': 'd', 'type': 'float'}, + {'name': 'e', 'type': 'float'}, + {'name': 'f', 'type': 'cat', 'vals': ('', '', '', '', '', 'True', 'False')}, + {'name': 'g', 'type': 'cat', 'vals': ('', '', '', '', 'True', 'False')}, + {'name': 'h', 'type': 'cat', 'vals': ('', '', '', 'No', 'Yes')}] + # make_test_data(100000, col_dicts) + source = '/home/ben/covid/benchmark_csv.csv' print(source) # run once first - original_csv_read(source) - + orig_inds = [] + orig_vals = [] + for i in range(len(col_dicts)+1): + orig_inds.append(np.zeros(1000000, dtype=np.int64)) + orig_vals.append(np.zeros(10000000, dtype='|S1')) + original_csv_read(source, orig_inds, orig_vals) + del orig_inds + del orig_vals + + orig_inds = [] + orig_vals = [] + for i in range(len(col_dicts)+1): + orig_inds.append(np.zeros(1000000, dtype=np.int64)) + orig_vals.append(np.zeros(10000000, dtype='|S1')) with Timer("Original csv reader took:"): - original_csv_read(source) + original_csv_read(source, orig_inds, orig_vals) + del orig_inds + del orig_vals + + file_read_line_fast_csv(source) file_read_line_fast_csv(source) - with Timer("FAST Open file read lines took"): - file_read_line_fast_csv(source) # original csv reader -def original_csv_read(source): +def original_csv_read(source, column_inds=None, column_vals=None): time0 = time.time() with open(source) as f: csvf = csv.reader(f, delimiter=',', quotechar='"') - for i_r, row in enumerate(csvf): - pass + if i_r == 0: + print(len(row)) + for i_c in range(len(row)): + entry = row[i_c].encode() + column_inds[i_c][i_r+1] = column_inds[i_c][i_r] + len(entry) + column_vals[column_inds[i_c][i_r]:column_inds[i_c][i_r+1]] = entry # print('Original csv reader took {} s'.format(time.time() - time0)) @@ -53,11 +99,21 @@ def file_read_line_fast_csv(source): content = f.read() count_rows = content.count('\n') + 1 - content = np.fromfile(source, dtype='|S1') - - column_inds = np.zeros(count_rows * count_columns, dtype = np.int64).reshape(count_rows, count_columns) - - my_fast_csv_reader_int(content, column_inds) + content = np.fromfile(source, dtype='|S1')#np.uint8) + column_inds = np.zeros((count_columns, count_rows), dtype=np.int64) + column_vals = np.zeros((count_columns, count_rows * 20), dtype=np.uint8) + + # separator = np.frombuffer(b',', dtype='S1')[0][0] + # delimiter = np.frombuffer(b'"', dtype='S1')[0][0] + ESCAPE_VALUE = np.frombuffer(b'"', dtype='S1')[0][0] + SEPARATOR_VALUE = np.frombuffer(b',', dtype='S1')[0][0] + NEWLINE_VALUE = np.frombuffer(b'\n', dtype='S1')[0][0] + # ESCAPE_VALUE = b'"' + # SEPARATOR_VALUE = b',' + # NEWLINE_VALUE = b'\n' + with Timer("my_fast_csv_reader_int"): + content = np.fromfile('/home/ben/covid/benchmark_csv.csv', dtype=np.uint8) + my_fast_csv_reader_int(content, column_inds, column_vals, ESCAPE_VALUE, SEPARATOR_VALUE, NEWLINE_VALUE) for row in column_inds: #print(row) @@ -65,11 +121,35 @@ def file_read_line_fast_csv(source): pass +def make_test_data(count, schema): + """ + [ {'name':name, 'type':'cat'|'float'|'fixed', 'values':(vals)} ] + """ + import pandas as pd + rng = np.random.RandomState(12345678) + columns = {} + for s in schema: + if s['type'] == 'cat': + vals = s['vals'] + arr = rng.randint(low=0, high=len(vals), size=count) + larr = [None] * count + for i in range(len(arr)): + larr[i] = vals[arr[i]] + columns[s['name']] = larr + elif s['type'] == 'float': + arr = rng.uniform(size=count) + columns[s['name']] = arr + + df = pd.DataFrame(columns) + df.to_csv('/home/ben/covid/benchmark_csv.csv', index_label='index') + + + @njit -def my_fast_csv_reader_int(source, column_inds): - ESCAPE_VALUE = b'"' - SEPARATOR_VALUE = b',' - NEWLINE_VALUE = b'\n' +def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separator_value, newline_value): + # ESCAPE_VALUE = b'"' + # SEPARATOR_VALUE = b',' + # NEWLINE_VALUE = b'\n' #max_rowcount = len(column_inds) - 1 colcount = len(column_inds[0]) @@ -79,7 +159,8 @@ def my_fast_csv_reader_int(source, column_inds): cell_start_idx = np.int64(0) cell_end_idx = np.int64(0) col_index = np.int64(0) - row_index = np.int64(0) + row_index = np.int64(-1) + current_char_count = np.int32(0) # how to parse csv # . " is the escape character @@ -95,88 +176,120 @@ def my_fast_csv_reader_int(source, column_inds): end_cell = False end_line = False escaped_literal_candidate = False + cur_ind_array = column_inds[0] + cur_val_array = column_vals[0] + cur_cell_start = cur_ind_array[row_index] + cur_cell_char_count = 0 while True: - c = source[index] - if c == SEPARATOR_VALUE: - if not escaped: #or escaped_literal_candidate: - # don't write this char - end_cell = True - cell_end_idx = index - # while index + 1 < len(source) and source[index + 1] == ' ': - # index += 1 - - - else: - # write literal ',' - # cell_value.append(c) - pass - - elif c == NEWLINE_VALUE: - if not escaped: #or escaped_literal_candidate: - # don't write this char - end_cell = True - end_line = True - cell_end_idx = index - else: - # write literal '\n' - pass - #cell_value.append(c) - - elif c == ESCAPE_VALUE: - # ,"... - start of an escaped cell - # ...", - end of an escaped cell - # ...""... - literal quote character - # otherwise error - if not escaped: - # this must be the first character of a cell - if index != cell_start_idx: - # raise error! - pass - # don't write this char - else: - escaped = True - else: - - escaped = False - # if escaped_literal_candidate: - # escaped_literal_candidate = False - # # literal quote character confirmed, write it - # cell_value.append(c) - # else: - # escaped_literal_candidate = True - # # don't write this char + write_char = False + end_cell = False + end_line = False + c = source[index] + if c == separator_value: + end_cell = True + elif c == newline_value: + end_cell = True + end_line = True else: - # cell_value.append(c) - pass - # if escaped_literal_candidate: - # # error! - # pass - # # raise error return -2 + write_char = True + + if write_char and row_index >= 0: + cur_val_array[cur_cell_start + cur_cell_char_count] = c + cur_cell_char_count += 1 - # parse c - index += 1 - if end_cell: - end_cell = False - #column_inds[col_index][row_index+1] =\ - # column_inds[col_index][row_index] + cell_end - cell_start - column_inds[row_index][col_index] = cell_end_idx + if row_index >= 0: + cur_ind_array[row_index+1] = cur_cell_start + cur_cell_char_count + if end_line: + row_index += 1 + col_index = 0 + else: + col_index += 1 - cell_start_idx = cell_end_idx + 1 + cur_ind_array = column_inds[col_index] + cur_val_array = column_vals[col_index] + cur_cell_start = cur_ind_array[row_index] + cur_cell_char_count = 0 - col_index += 1 + index += 1 - - if col_index == colcount: - if not end_line: - raise Exception('.....') - else: - end_line = False - - row_index += 1 - col_index = 0 + # c = source[index] + # if c == separator_value: + # if not escaped or escaped_literal_candidate: + # # don't write this char + # end_cell = True + # cell_end_idx = index + # # while index + 1 < len(source) and source[index + 1] == ' ': + # # index += 1 + # else: + # column_vals[column_inds[col_index][row_index]+current_char_count] = c + # + # + # elif c == newline_value: + # if not escaped: #or escaped_literal_candidate: + # # don't write this char + # end_cell = True + # end_line = True + # cell_end_idx = index + # else: + # # write literal '\n' + # pass + # #cell_value.append(c) + # + # elif c == escape_value: + # # ,"... - start of an escaped cell + # # ...", - end of an escaped cell + # # ...""... - literal quote character + # # otherwise error + # if not escaped: + # # this must be the first character of a cell + # if index != cell_start_idx: + # # raise error! + # pass + # # don't write this char + # else: + # escaped = True + # else: + # + # escaped = False + # # if escaped_literal_candidate: + # # escaped_literal_candidate = False + # # # literal quote character confirmed, write it + # # cell_value.append(c) + # # else: + # # escaped_literal_candidate = True + # # # don't write this char + # + # else: + # cell_value.append(c) + # # if escaped_literal_candidate: + # # # error! + # # pass + # # # raise error return -2 + # + # # parse c + # index += 1 + + # if end_cell: + # end_cell = False + # #column_inds[col_index][row_index+1] =\ + # # column_inds[col_index][row_index] + cell_end - cell_start + # column_inds[col_index][row_index] = cell_end_idx + # + # col_index += 1 + # cell_start_idx = column_inds[col_index][row_index-1] + # current_char_count = 0 + # + # if col_index == colcount: + # if not end_line: + # raise Exception('.....') + # else: + # end_line = False + # + # row_index += 1 + # col_index = 0 if index == len(source): # "erase the partially written line" diff --git a/resources/assessment_input_small_data.csv b/resources/assessment_input_small_data.csv index 7bc799b8..72167bb5 100644 --- a/resources/assessment_input_small_data.csv +++ b/resources/assessment_input_small_data.csv @@ -7,7 +7,6 @@ f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06: 0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -<<<<<<< HEAD 6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, 7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, @@ -1295,6 +1294,4 @@ f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06: 0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -======= ->>>>>>> eaac2b68840b5fb690c99005bbd5b674a11fd35b 6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, \ No newline at end of file diff --git a/tests/test_dataset.py b/tests/test_dataset.py index f1ccb7a0..b81c0731 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -19,7 +19,3 @@ def test_dataset_init(self): def test_dataset_ops(self): pass - - - - From f2efedcb0572b139c1aaf3ffb799cbf5e9aee7f6 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 12 Apr 2021 16:38:14 +0100 Subject: [PATCH 044/181] Address issue #138 on minor tweaks Fix bug: create dataframe in dataset construction method to mapping existing datasets Full syn between dataset with h5file when add dataframe (group), remove dataframe, set dataframe. --- exetera/core/abstract_types.py | 106 +++++++++++++++++++++++++++++++++ exetera/core/dataframe.py | 53 ++++++++--------- exetera/core/dataset.py | 74 +++++------------------ exetera/core/group.py | 30 ++++++---- exetera/core/session.py | 3 +- tests/test_dataframe.py | 12 ++-- tests/test_dataset.py | 57 ++++++++++++++++-- 7 files changed, 225 insertions(+), 110 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index bc136844..b29093c3 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -133,6 +133,112 @@ def __len__(self): raise NotImplementedError() +class DataFrame(ABC): + """ + DataFrame is a table of data that contains a list of Fields (columns) + """ + + @abstractmethod + def add(self): + raise NotImplementedError() + + @abstractmethod + def create_group(self, name): + raise NotImplementedError() + + @abstractmethod + def create_numeric(self, name, nformat, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_indexed_string(self, name, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_fixed_string(self, name, length, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_categorical(self, name, nformat, key, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_timestamp(self, name, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def __contains__(self, name): + raise NotImplementedError() + + @abstractmethod + def contains_field(self, field): + raise NotImplementedError() + + @abstractmethod + def __getitem__(self, name): + raise NotImplementedError() + + @abstractmethod + def get_field(self, name): + raise NotImplementedError() + + @abstractmethod + def get_name(self, field): + raise NotImplementedError() + + @abstractmethod + def __setitem__(self, name, field): + raise NotImplementedError() + + @abstractmethod + def __delitem__(self, name): + raise NotImplementedError() + + @abstractmethod + def delete_field(self, field): + raise NotImplementedError() + + @abstractmethod + def list(self): + raise NotImplementedError() + + @abstractmethod + def keys(self): + raise NotImplementedError() + + @abstractmethod + def values(self): + raise NotImplementedError() + + @abstractmethod + def items(self): + raise NotImplementedError() + + @abstractmethod + def __iter__(self): + raise NotImplementedError() + + @abstractmethod + def __next__(self): + raise NotImplementedError() + + @abstractmethod + def __len__(self): + raise NotImplementedError() + + @abstractmethod + def get_spans(self): + raise NotImplementedError() + + @abstractmethod + def apply_filter(self, filter_to_apply, ddf=None): + raise NotImplementedError() + + @abstractmethod + def apply_index(self, index_to_apply, ddf=None): + raise NotImplementedError() + + class AbstractSession(ABC): @abstractmethod diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 057f1d44..c6d3f69d 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -9,15 +9,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from exetera.core.abstract_types import AbstractSession, Dataset +from exetera.core.abstract_types import AbstractSession, Dataset, DataFrame from exetera.core import fields as fld from exetera.core import dataset as dst import h5py -class DataFrame: +class HDF5DataFrame(DataFrame): """ - DataFrame is a table of data that contains a list of Fields (columns) + DataFrame that utilising HDF5 file as storage. """ def __init__(self, dataset: Dataset, @@ -38,30 +38,31 @@ def __init__(self, self.fields = dict() self.name = name self.dataset = dataset - if isinstance(dataset, dst.HDF5Dataset): - dataset[name] = self + # if isinstance(dataset, dst.HDF5Dataset): + # dataset[name] = self if dataframe is not None: - if isinstance(dataframe, dict) and isinstance(list(dataframe.items())[0][0], str) and\ - isinstance(list(dataframe.items())[0][1], fld.Field): + if isinstance(dataframe, dict): + for k,v in dataframe.items(): + if not isinstance(k, str) or not isinstance(v, fld.Field): + raise ValueError("If dataframe parameter is set, must be a dictionary mapping strings to fields") self.fields = dataframe - else: - raise ValueError("if set, 'dataframe' must be a dictionary mapping strings to fields") - elif h5group is not None and isinstance(h5group, h5py.Group): - fieldtype_map = { - 'indexedstring': fld.IndexedStringField, - 'fixedstring': fld.FixedStringField, - 'categorical': fld.CategoricalField, - 'boolean': fld.NumericField, - 'numeric': fld.NumericField, - 'datetime': fld.TimestampField, - 'date': fld.TimestampField, - 'timestamp': fld.TimestampField - } for subg in h5group.keys(): - fieldtype = h5group[subg].attrs['fieldtype'].split(',')[0] - self.fields[subg] = fieldtype_map[fieldtype](self, h5group[subg]) - print(" ") + self.fields[subg]=dataset.session.get(h5group[subg]) + # fieldtype_map = { + # 'indexedstring': fld.IndexedStringField, + # 'fixedstring': fld.FixedStringField, + # 'categorical': fld.CategoricalField, + # 'boolean': fld.NumericField, + # 'numeric': fld.NumericField, + # 'datetime': fld.TimestampField, + # 'date': fld.TimestampField, + # 'timestamp': fld.TimestampField + # } + # for subg in h5group.keys(): + # fieldtype = h5group[subg].attrs['fieldtype'].split(',')[0] + # self.fields[subg] = fieldtype_map[fieldtype](self, h5group[subg]) + # print(" ") def add(self, field, name=None): if name is not None: @@ -208,10 +209,6 @@ def __iter__(self): def __next__(self): return next(self.fields) - """ - def search(self): #is search similar to get & get_name? - pass - """ def __len__(self): return len(self.fields) @@ -263,4 +260,4 @@ def apply_index(self, index_to_apply, ddf=None): else: for field in self.fields.values(): field.apply_index(index_to_apply) - return self + return self \ No newline at end of file diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index b3c076d0..f2043bf3 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -9,55 +9,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# class Dataset(): -# """ -# DataSet is a container of dataframes -# """ -# def __init__(self,file_path,name): -# pass -# -# def close(self): -# pass -# -# def add(self, field, name=None): -# pass -# -# def __contains__(self, name): -# pass -# -# def contains_dataframe(self, dataframe): -# pass -# -# def __getitem__(self, name): -# pass -# -# def get_dataframe(self, name): -# pass -# -# def get_name(self, dataframe): -# pass -# -# def __setitem__(self, name, dataframe): -# pass -# -# def __delitem__(self, name): -# pass -# -# def delete_dataframe(self, dataframe): -# pass -# -# def list(self): -# pass -# -# def __iter__(self): -# pass -# -# def __next__(self): -# pass -# -# def __len__(self): -# pass - import h5py from exetera.core.abstract_types import Dataset from exetera.core import dataframe as edf @@ -69,7 +20,9 @@ def __init__(self, session, dataset_path, mode, name): self._session = session self.file = h5py.File(dataset_path, mode) self.dataframes = dict() - + for subgrp in self.file.keys(): + hdf = edf.HDF5DataFrame(self,subgrp,h5group=self.file[subgrp]) + self.dataframes[subgrp]=hdf @property def session(self): @@ -88,7 +41,7 @@ def create_dataframe(self, name): :return: a dataframe object """ self.file.create_group(name) - dataframe = edf.DataFrame(self, name) + dataframe = edf.HDF5DataFrame(self, name) self.dataframes[name] = dataframe return dataframe @@ -101,9 +54,9 @@ def add(self, dataframe, name=None): :param dataframe: the dataframe to copy to this dataset :param name: optional- change the dataframe name """ - dname = dataframe if name is None else name - self.file.copy(dataframe.dataset[dataframe.name], self.file, name=dname) - df = edf.DataFrame(self, dname, h5group=self.file[dname]) + dname = dataframe.name if name is None else name + self.file.copy(dataframe.dataset.file[dataframe.name], self.file, name=dname) + df = edf.HDF5DataFrame(self, dname, h5group=self.file[dname]) self.dataframes[dname] = df @@ -152,15 +105,15 @@ def get_name(self, dataframe): break return None - def __setitem__(self, name, dataframe): if not isinstance(name, str): raise TypeError("The name must be a str object.") elif not isinstance(dataframe, edf.DataFrame): raise TypeError("The field must be a DataFrame object.") else: - self.dataframes[name] = dataframe - return True + if self.dataframes.__contains__(name): + self.__delitem__(name) + return self.add(dataframe,name) def __delitem__(self, name): @@ -168,6 +121,7 @@ def __delitem__(self, name): raise ValueError("This dataframe does not contain the name to delete.") else: del self.dataframes[name] + del self.file[name] return True @@ -187,15 +141,15 @@ def list(self): def keys(self): - return self.file.keys() + return self.dataframes.keys() def values(self): - return self.file.values() + return self.dataframes.values() def items(self): - return self.file.items() + return self.dataframes.items() def __iter__(self): diff --git a/exetera/core/group.py b/exetera/core/group.py index 2cf2e2df..9f28d147 100644 --- a/exetera/core/group.py +++ b/exetera/core/group.py @@ -1,19 +1,29 @@ +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -""" -Group / Field semantics ------------------------ -Location semantics - * Fields can be created without a logical location. Such fields are written to a 'temp' location when required - * Fields can be assigned a logical location or created with a logical location - * Fields have a physical location at the point they are written to the dataset. Fields that are assigned to a logical -location are also guaranteed to be written to a physical location -""" +class Group: + """ + Group / Field semantics + ----------------------- + Location semantics + * Fields can be created without a logical location. Such fields are written to a 'temp' location when required + * Fields can be assigned a logical location or created with a logical location + * Fields have a physical location at the point they are written to the dataset. Fields that are assigned to a logical + location are also guaranteed to be written to a physical location + """ -class Group: def __init__(self, parent): self.parent = parent diff --git a/exetera/core/session.py b/exetera/core/session.py index dbdac53c..ff00d2f8 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -361,7 +361,9 @@ def get_spans(self, field=None, fields=None): Example: field: [1, 2, 2, 1, 1, 1, 3, 4, 4, 4, 2, 2, 2, 2, 2] result: [0, 1, 3, 6, 7, 10, 15] + """ + if fields is not None: if isinstance(fields[0],fld.Field): return ops._get_spans_for_2_fields_by_spans(fields[0].get_spans(),fields[1].get_spans()) @@ -727,7 +729,6 @@ def create_timestamp(self, group, name, timestamp=None, chunksize=None): return group.create_timestamp(name, timestamp, chunksize) - #TODO: update this method for the existence of dataframes def get_or_create_group(self, group, name): if name in group: return group[name] diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index caf69a01..2d024186 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -16,11 +16,11 @@ def test_dataframe_init(self): dst = s.open_dataset(bio, 'w', 'dst') # init - df = dataframe.DataFrame(dst, 'dst') + df = dataframe.HDF5DataFrame(dst, 'dst') self.assertTrue(isinstance(df, dataframe.DataFrame)) numf = df.create_numeric('numf', 'uint32') fdf = {'numf': numf} - df2 = dataframe.DataFrame(dst, 'dst2', dataframe=fdf) + df2 = dataframe.HDF5DataFrame(dst, 'dst2', dataframe=fdf) self.assertTrue(isinstance(df2, dataframe.DataFrame)) # add & set & contains @@ -57,13 +57,13 @@ def test_dataframe_init_fromh5(self): dst = ds.create_dataframe('dst') num=s.create_numeric(dst,'num', 'uint8') num.data.write([1, 2, 3, 4, 5, 6, 7]) - df = dataframe.DataFrame(dst, 'dst', h5group=dst) + df = dataframe.HDF5DataFrame(dst, 'dst', h5group=dst) def test_dataframe_create_field(self): bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, 'r+', 'dst') - df = dataframe.DataFrame(dst, 'dst',) + df = dataframe.HDF5DataFrame(dst, 'dst',) num = df.create_numeric('num', 'uint32') num.data.write([1, 2, 3, 4]) self.assertEqual([1, 2, 3, 4], num.data[:].tolist()) @@ -72,7 +72,7 @@ def test_dataframe_ops(self): bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, 'w', 'dst') - df = dataframe.DataFrame(dst, 'dst') + df = dataframe.HDF5DataFrame(dst, 'dst') numf = s.create_numeric(df, 'numf', 'int32') numf.data.write([5, 4, 3, 2, 1]) df.add(numf) @@ -80,7 +80,7 @@ def test_dataframe_ops(self): fst.data.write([b'e', b'd', b'c', b'b', b'a']) df.add(fst) index = np.array([4, 3, 2, 1, 0]) - ddf = dataframe.DataFrame(dst, 'dst2') + ddf = dataframe.HDF5DataFrame(dst, 'dst2') df.apply_index(index, ddf) self.assertEqual([1, 2, 3, 4, 5], ddf.get_field('numf').data[:].tolist()) self.assertEqual([b'a', b'b', b'c', b'd', b'e'], ddf.get_field('fst').data[:].tolist()) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index d9a8dd80..644d7d9b 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -1,7 +1,12 @@ import unittest -from exetera.core import dataset + +import h5py +import numpy as np + from exetera.core import session +from exetera.core.abstract_types import DataFrame from io import BytesIO +from exetera.core import data_writer class TestDataSet(unittest.TestCase): @@ -11,16 +16,58 @@ def test_dataset_init(self): with session.Session() as s: dst = s.open_dataset(bio, 'r+', 'dst') df = dst.create_dataframe('df') + + #create field using session api num = s.create_numeric(df,'num', 'int32') num.data.write([1, 2, 3, 4]) self.assertEqual([1, 2, 3, 4], num.data[:].tolist()) - num2 = s.create_numeric(df, 'num2', 'int32') - num2 = s.get(df['num2']) + cat = s.create_categorical(df, 'cat', 'int8', {'a': 1, 'b': 2}) + cat.data.write([1,1,2,2]) + self.assertEqual([1,1,2,2],s.get(df['cat']).data[:].tolist()) + + #create field using dataframe api + idsf = df.create_indexed_string('idsf') + idsf.data.write(['a','bb','ccc','dddd']) + self.assertEqual(['a','bb','ccc','dddd'],df['idsf'].data[:]) - def test_dataset_ops(self): - pass + fsf = df.create_fixed_string('fsf',3) + fsf.data.write([b'aaa',b'bbb',b'ccc',b'ddd']) + self.assertEqual([b'aaa',b'bbb',b'ccc',b'ddd'],df['fsf'].data[:].tolist()) + + def test_dataset_init_with_data(self): + bio = BytesIO() + with session.Session() as s: + h5file = h5py.File(bio,'w') + h5file.create_group("grp1") #dataframe + num1 = h5file["grp1"].create_group('num1') #field + num1.attrs['fieldtype'] = 'numeric,{}'.format('uint32') + num1.attrs['nformat'] = 'uint32' + ds=num1.create_dataset('values',(5,),dtype='uint32') + ds[:]=np.array([0,1,2,3,4]) + h5file.close() + #read existing datafile + dst=s.open_dataset(bio,'r+','dst') + self.assertTrue(isinstance(dst['grp1'],DataFrame)) + self.assertEqual(s.get(dst['grp1']['num1']).data[:].tolist(),[0,1,2,3,4]) + #add dataframe + bio2 = BytesIO() + ds2 = s.open_dataset(bio2,'w','ds2') + df2=ds2.create_dataframe('df2') + fs=df2.create_fixed_string('fs',1) + fs.data.write([b'a',b'b',b'c',b'd']) + dst.add(df2) + self.assertTrue(isinstance(dst['df2'],DataFrame)) + self.assertEqual([b'a',b'b',b'c',b'd'],dst['df2']['fs'].data[:].tolist()) + #del dataframe + del dst['df2'] #only 'grp1' left + self.assertTrue(len(dst.list())==1) + self.assertTrue(len(dst.file.keys())==1) + #set dataframe + dst['grp1']=df2 + self.assertTrue(isinstance(dst['grp1'], DataFrame)) + self.assertEqual([b'a', b'b', b'c', b'd'], dst['grp1']['fs'].data[:].tolist()) \ No newline at end of file From 49263308a95d1e38f7d841c5281d325dbe5569b4 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 12 Apr 2021 16:41:38 +0100 Subject: [PATCH 045/181] remove draft group.py from repo --- exetera/core/group.py | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 exetera/core/group.py diff --git a/exetera/core/group.py b/exetera/core/group.py deleted file mode 100644 index 9f28d147..00000000 --- a/exetera/core/group.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2020 KCL-BMEIS - King's College London -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - - - -class Group: - """ - Group / Field semantics - ----------------------- - - Location semantics - * Fields can be created without a logical location. Such fields are written to a 'temp' location when required - * Fields can be assigned a logical location or created with a logical location - * Fields have a physical location at the point they are written to the dataset. Fields that are assigned to a logical - location are also guaranteed to be written to a physical location - """ - - def __init__(self, parent): - self.parent = parent - - def create_group(self, group_name): - self.parent \ No newline at end of file From 56bb19095de1dbb245487c0fd1ce6a175389ec95 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Mon, 12 Apr 2021 17:49:57 +0100 Subject: [PATCH 046/181] Improved performance from the fast csv reader through avoiding ndarray slicing --- exetera/core/csv_reader_speedup.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py index 3382625b..1b0e9e3b 100644 --- a/exetera/core/csv_reader_speedup.py +++ b/exetera/core/csv_reader_speedup.py @@ -102,6 +102,7 @@ def file_read_line_fast_csv(source): content = np.fromfile(source, dtype='|S1')#np.uint8) column_inds = np.zeros((count_columns, count_rows), dtype=np.int64) column_vals = np.zeros((count_columns, count_rows * 20), dtype=np.uint8) + print(column_inds.shape) # separator = np.frombuffer(b',', dtype='S1')[0][0] # delimiter = np.frombuffer(b'"', dtype='S1')[0][0] @@ -176,9 +177,9 @@ def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separ end_cell = False end_line = False escaped_literal_candidate = False - cur_ind_array = column_inds[0] - cur_val_array = column_vals[0] - cur_cell_start = cur_ind_array[row_index] + # cur_ind_array = column_inds[0] + # cur_val_array = column_vals[0] + cur_cell_start = column_inds[col_index, row_index] if row_index >= 0 else 0 cur_cell_char_count = 0 while True: write_char = False @@ -195,21 +196,21 @@ def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separ write_char = True if write_char and row_index >= 0: - cur_val_array[cur_cell_start + cur_cell_char_count] = c + column_vals[col_index, cur_cell_start + cur_cell_char_count] = c cur_cell_char_count += 1 if end_cell: if row_index >= 0: - cur_ind_array[row_index+1] = cur_cell_start + cur_cell_char_count + column_inds[col_index, row_index+1] = cur_cell_start + cur_cell_char_count if end_line: row_index += 1 col_index = 0 else: col_index += 1 - cur_ind_array = column_inds[col_index] - cur_val_array = column_vals[col_index] - cur_cell_start = cur_ind_array[row_index] + # cur_ind_array = column_inds[col_index] + # cur_val_array = column_vals[col_index] + cur_cell_start = column_inds[col_index, row_index] cur_cell_char_count = 0 index += 1 From f0b7e37c0edb430f35618a3607f83a9121759180 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 13 Apr 2021 14:06:44 +0100 Subject: [PATCH 047/181] fix dataframe api --- exetera/core/abstract_types.py | 10 +-- exetera/core/dataframe.py | 154 ++++++++++++++++----------------- exetera/core/dataset.py | 101 ++++++++++----------- tests/test_dataframe.py | 13 ++- tests/test_dataset.py | 2 +- 5 files changed, 133 insertions(+), 147 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index b29093c3..00b4b820 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -116,10 +116,6 @@ def __delitem__(self, name): def delete_dataframe(self, dataframe): raise NotImplementedError() - @abstractmethod - def list(self): - raise NotImplementedError() - @abstractmethod def __iter__(self): raise NotImplementedError() @@ -139,7 +135,7 @@ class DataFrame(ABC): """ @abstractmethod - def add(self): + def add(self, field, name=None): raise NotImplementedError() @abstractmethod @@ -198,10 +194,6 @@ def __delitem__(self, name): def delete_field(self, field): raise NotImplementedError() - @abstractmethod - def list(self): - raise NotImplementedError() - @abstractmethod def keys(self): raise NotImplementedError() diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index c6d3f69d..8ab0d5ea 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -22,56 +22,54 @@ class HDF5DataFrame(DataFrame): def __init__(self, dataset: Dataset, name: str, - dataframe: dict = None, - h5group: h5py.Group = None): + h5group: h5py.Group, + dataframe: dict = None): """ - Create a Dataframe object. + Create a Dataframe object, user should always call from dataset.create_dataframe. :param name: name of the dataframe, or the group name in HDF5 :param dataset: a dataset object, where this dataframe belongs to - :param dataframe: optional - replicate data from another dictionary - :param h5group: optional - acquire data from h5group object directly, the h5group needs to have a + :param h5group: acquire data from h5group object directly, the h5group needs to have a h5group<-group-dataset structure, the group has a 'fieldtype' attribute and the dataset is named 'values'. + :param dataframe: optional - replicate data from another dictionary """ - # TODO: consider columns as a name rather than fields - self.fields = dict() + self.name = name - self.dataset = dataset - # if isinstance(dataset, dst.HDF5Dataset): - # dataset[name] = self + self._columns = dict() + self._dataset = dataset + self._h5group = h5group + if dataframe is not None: if isinstance(dataframe, dict): - for k,v in dataframe.items(): + for k, v in dataframe.items(): if not isinstance(k, str) or not isinstance(v, fld.Field): - raise ValueError("If dataframe parameter is set, must be a dictionary mapping strings to fields") - self.fields = dataframe - elif h5group is not None and isinstance(h5group, h5py.Group): - for subg in h5group.keys(): - self.fields[subg]=dataset.session.get(h5group[subg]) - # fieldtype_map = { - # 'indexedstring': fld.IndexedStringField, - # 'fixedstring': fld.FixedStringField, - # 'categorical': fld.CategoricalField, - # 'boolean': fld.NumericField, - # 'numeric': fld.NumericField, - # 'datetime': fld.TimestampField, - # 'date': fld.TimestampField, - # 'timestamp': fld.TimestampField - # } - # for subg in h5group.keys(): - # fieldtype = h5group[subg].attrs['fieldtype'].split(',')[0] - # self.fields[subg] = fieldtype_map[fieldtype](self, h5group[subg]) - # print(" ") + raise ValueError("If dataframe parameter is set, " + "must be a dictionary mapping strings to fields") + self._columns = dataframe + for subg in h5group.keys(): + self._columns[subg] = dataset.session.get(h5group[subg]) + + @property + def columns(self): + return dict(self._columns) + + @property + def dataset(self): + return self._dataset + + @property + def h5group(self): + return self._h5group def add(self, field, name=None): if name is not None: if not isinstance(name, str): raise TypeError("The name must be a str object.") else: - self.fields[name] = field + self._columns[name] = field # note the name has '/' for hdf5 object - self.fields[field.name[field.name.index('/', 1)+1:]] = field + self._columns[field.name[field.name.index('/', 1)+1:]] = field def create_group(self, name): """ @@ -80,45 +78,44 @@ def create_group(self, name): :param name: the name of the group and field :return: a hdf5 group object """ - self.dataset.file.create_group("/"+self.name+"/"+name) - - return self.dataset.file["/"+self.name+"/"+name] + self._h5group.create_group(name) + return self._h5group[name] def create_numeric(self, name, nformat, timestamp=None, chunksize=None): - fld.numeric_field_constructor(self.dataset.session, self, name, nformat, timestamp, chunksize) - field = fld.NumericField(self.dataset.session, self.dataset.file["/"+self.name+"/"+name], + fld.numeric_field_constructor(self._dataset.session, self, name, nformat, timestamp, chunksize) + field = fld.NumericField(self._dataset.session, self._h5group[name], write_enabled=True) - self.fields[name] = field - return self.fields[name] + self._columns[name] = field + return self._columns[name] def create_indexed_string(self, name, timestamp=None, chunksize=None): - fld.indexed_string_field_constructor(self.dataset.session, self, name, timestamp, chunksize) - field = fld.IndexedStringField(self.dataset.session, self.dataset.file["/"+self.name+"/"+name], + fld.indexed_string_field_constructor(self._dataset.session, self, name, timestamp, chunksize) + field = fld.IndexedStringField(self._dataset.session, self._h5group[name], write_enabled=True) - self.fields[name] = field - return self.fields[name] + self._columns[name] = field + return self._columns[name] def create_fixed_string(self, name, length, timestamp=None, chunksize=None): - fld.fixed_string_field_constructor(self.dataset.session, self, name, length, timestamp, chunksize) - field = fld.FixedStringField(self.dataset.session, self.dataset.file["/"+self.name+"/"+name], + fld.fixed_string_field_constructor(self._dataset.session, self, name, length, timestamp, chunksize) + field = fld.FixedStringField(self._dataset.session, self._h5group[name], write_enabled=True) - self.fields[name] = field - return self.fields[name] + self._columns[name] = field + return self._columns[name] def create_categorical(self, name, nformat, key, timestamp=None, chunksize=None): - fld.categorical_field_constructor(self.dataset.session, self, name, nformat, key, + fld.categorical_field_constructor(self._dataset.session, self, name, nformat, key, timestamp, chunksize) - field = fld.CategoricalField(self.dataset.session, self.dataset.file["/"+self.name+"/"+name], + field = fld.CategoricalField(self._dataset.session, self._h5group[name], write_enabled=True) - self.fields[name] = field - return self.fields[name] + self._columns[name] = field + return self._columns[name] def create_timestamp(self, name, timestamp=None, chunksize=None): - fld.timestamp_field_constructor(self.dataset.session, self, name, timestamp, chunksize) - field = fld.TimestampField(self.dataset.session, self.dataset.file["/"+self.name+"/"+name], + fld.timestamp_field_constructor(self._dataset.session, self, name, timestamp, chunksize) + field = fld.TimestampField(self._dataset.session, self._h5group[name], write_enabled=True) - self.fields[name] = field - return self.fields[name] + self._columns[name] = field + return self._columns[name] def __contains__(self, name): """ @@ -128,7 +125,7 @@ def __contains__(self, name): if not isinstance(name, str): raise TypeError("The name must be a str object.") else: - return self.fields.__contains__(name) + return self._columns.__contains__(name) def contains_field(self, field): """ @@ -138,7 +135,7 @@ def contains_field(self, field): if not isinstance(field, fld.Field): raise TypeError("The field must be a Field object") else: - for v in self.fields.values(): + for v in self._columns.values(): if id(field) == id(v): return True return False @@ -149,7 +146,7 @@ def __getitem__(self, name): elif not self.__contains__(name): raise ValueError("Can not find the name from this dataframe.") else: - return self.fields[name] + return self._columns[name] def get_field(self, name): return self.__getitem__(name) @@ -160,7 +157,7 @@ def get_name(self, field): """ if not isinstance(field, fld.Field): raise TypeError("The field argument must be a Field object.") - for name, v in self.fields.items(): + for name, v in self._columns.items(): if id(field) == id(v): return name return None @@ -171,14 +168,14 @@ def __setitem__(self, name, field): elif not isinstance(field, fld.Field): raise TypeError("The field must be a Field object.") else: - self.fields[name] = field + self._columns[name] = field return True def __delitem__(self, name): if not self.__contains__(name=name): raise ValueError("This dataframe does not contain the name to delete.") else: - del self.fields[name] + del self._columns[name] return True def delete_field(self, field): @@ -191,33 +188,30 @@ def delete_field(self, field): else: self.__delitem__(name) - def list(self): - return tuple(n for n in self.fields.keys()) - def keys(self): - return self.fields.keys() + return self._columns.keys() def values(self): - return self.fields.values() + return self._columns.values() def items(self): - return self.fields.items() + return self._columns.items() def __iter__(self): - return iter(self.fields) + return iter(self._columns) def __next__(self): - return next(self.fields) + return next(self._columns) def __len__(self): - return len(self.fields) + return len(self._columns) def get_spans(self): """ Return the name and spans of each field as a dictionary. """ spans = {} - for name, field in self.fields.items(): + for name, field in self._columns.items(): spans[name] = field.get_spans() return spans @@ -232,13 +226,12 @@ def apply_filter(self, filter_to_apply, ddf=None): if ddf is not None: if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") - for name, field in self.fields.items(): - # TODO integration w/ session, dataset - newfld = field.create_like(ddf, field.name) + for name, field in self._columns.items(): + newfld = field.create_like(ddf, field.name[field.name.index('/', 1)+1:]) ddf.add(field.apply_filter(filter_to_apply, dstfld=newfld), name=name) return ddf else: - for field in self.fields.values(): + for field in self._columns.values(): field.apply_filter(filter_to_apply) return self @@ -253,11 +246,12 @@ def apply_index(self, index_to_apply, ddf=None): if ddf is not None: if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") - for name, field in self.fields.items(): - newfld = field.create_like(ddf, field.name) - ddf.add(field.apply_index(index_to_apply, dstfld=newfld), name=name) + for name, field in self._columns.items(): + newfld = field.create_like(ddf, field.name[field.name.index('/', 1)+1:]) + idx = field.apply_index(index_to_apply, dstfld=newfld) + ddf.add(idx, name=name) return ddf else: - for field in self.fields.values(): + for field in self._columns.values(): field.apply_index(index_to_apply) - return self \ No newline at end of file + return self diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index f2043bf3..ddab8b4a 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -17,52 +17,61 @@ class HDF5Dataset(Dataset): def __init__(self, session, dataset_path, mode, name): + self.name = name self._session = session - self.file = h5py.File(dataset_path, mode) - self.dataframes = dict() - for subgrp in self.file.keys(): - hdf = edf.HDF5DataFrame(self,subgrp,h5group=self.file[subgrp]) - self.dataframes[subgrp]=hdf + self._file = h5py.File(dataset_path, mode) + self._dataframes = dict() + for subgrp in self._file.keys(): + self.create_dataframe(subgrp, h5group=self._file[subgrp]) @property def session(self): return self._session + @property + def dataframes(self): + return self._dataframes - def close(self): - self.file.close() + @property + def file(self): + return self._file + def close(self): + self._file.close() - def create_dataframe(self, name): + def create_dataframe(self, name, dataframe: dict = None, h5group: h5py.Group = None): """ Create a group object in HDF5 file and a Exetera dataframe in memory. - :param name: the name of the group and dataframe + :param name: name of the dataframe, or the group name in HDF5 + :param dataframe: optional - replicate data from another dictionary + :param h5group: optional - acquire data from h5group object directly, the h5group needs to have a + h5group<-group-dataset structure, the group has a 'fieldtype' attribute + and the dataset is named 'values'. :return: a dataframe object """ - self.file.create_group(name) - dataframe = edf.HDF5DataFrame(self, name) - self.dataframes[name] = dataframe + if h5group is None: + self._file.create_group(name) + h5group = self._file[name] + dataframe = edf.HDF5DataFrame(self, name, h5group, dataframe) + self._dataframes[name] = dataframe return dataframe - def add(self, dataframe, name=None): """ - Add an existing dataframe to this dataset, write the existing group + Add an existing dataframe (from other dataset) to this dataset, write the existing group attributes and HDF5 datasets to this dataset. :param dataframe: the dataframe to copy to this dataset :param name: optional- change the dataframe name """ dname = dataframe.name if name is None else name - self.file.copy(dataframe.dataset.file[dataframe.name], self.file, name=dname) - df = edf.HDF5DataFrame(self, dname, h5group=self.file[dname]) - self.dataframes[dname] = df - + self._file.copy(dataframe.h5group, self._file, name=dname) + df = edf.HDF5DataFrame(self, dname, h5group=self._file[dname]) + self._dataframes[dname] = df def __contains__(self, name): - return self.dataframes.__contains__(name) - + return self._dataframes.__contains__(name) def contains_dataframe(self, dataframe): """ @@ -74,57 +83,59 @@ def contains_dataframe(self, dataframe): if not isinstance(dataframe, edf.DataFrame): raise TypeError("The field must be a DataFrame object") else: - for v in self.dataframes.values(): + for v in self._dataframes.values(): if id(dataframe) == id(v): return True return False - def __getitem__(self, name): if not isinstance(name, str): raise TypeError("The name must be a str object.") elif not self.__contains__(name): raise ValueError("Can not find the name from this dataset.") else: - return self.dataframes[name] - + return self._dataframes[name] def get_dataframe(self, name): self.__getitem__(name) - def get_name(self, dataframe): """ Get the name of the dataframe in this dataset. """ if not isinstance(dataframe, edf.DataFrame): raise TypeError("The field argument must be a DataFrame object.") - for name, v in self.fields.items(): + for name, v in self.dataframes.items(): if id(dataframe) == id(v): return name - break return None def __setitem__(self, name, dataframe): + """ + Add an existing dataframe (from other dataset) to this dataset, the existing dataframe can from: + 1) this dataset, so perform a 'rename' operation, or; + 2) another dataset, so perform an 'add' or 'replace' operation + """ if not isinstance(name, str): raise TypeError("The name must be a str object.") elif not isinstance(dataframe, edf.DataFrame): raise TypeError("The field must be a DataFrame object.") else: - if self.dataframes.__contains__(name): - self.__delitem__(name) - return self.add(dataframe,name) - + if dataframe.dataset == self: # rename a dataframe + return self._file.move(dataframe.name, name) + else: # new dataframe from another dataset + if self._dataframes.__contains__(name): + self.__delitem__(name) + return self.add(dataframe, name) def __delitem__(self, name): if not self.__contains__(name): raise ValueError("This dataframe does not contain the name to delete.") else: - del self.dataframes[name] - del self.file[name] + del self._dataframes[name] + del self._file[name] return True - def delete_dataframe(self, dataframe): """ Remove dataframe from this dataset by dataframe object. @@ -135,30 +146,20 @@ def delete_dataframe(self, dataframe): else: self.__delitem__(name) - - def list(self): - return tuple(n for n in self.dataframes.keys()) - - def keys(self): - return self.dataframes.keys() - + return self._dataframes.keys() def values(self): - return self.dataframes.values() - + return self._dataframes.values() def items(self): - return self.dataframes.items() - + return self._dataframes.items() def __iter__(self): - return iter(self.dataframes) - + return iter(self._dataframes) def __next__(self): - return next(self.dataframes) - + return next(self._dataframes) def __len__(self): - return len(self.dataframes) + return len(self._dataframes) diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 2d024186..ca7bf24f 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -14,13 +14,12 @@ def test_dataframe_init(self): bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, 'w', 'dst') - # init - df = dataframe.HDF5DataFrame(dst, 'dst') + df = dst.create_dataframe('dst') self.assertTrue(isinstance(df, dataframe.DataFrame)) numf = df.create_numeric('numf', 'uint32') fdf = {'numf': numf} - df2 = dataframe.HDF5DataFrame(dst, 'dst2', dataframe=fdf) + df2 = dst.create_dataframe('dst2', dataframe=fdf) self.assertTrue(isinstance(df2, dataframe.DataFrame)) # add & set & contains @@ -57,13 +56,13 @@ def test_dataframe_init_fromh5(self): dst = ds.create_dataframe('dst') num=s.create_numeric(dst,'num', 'uint8') num.data.write([1, 2, 3, 4, 5, 6, 7]) - df = dataframe.HDF5DataFrame(dst, 'dst', h5group=dst) + df = ds.create_dataframe('dst2', h5group=dst) def test_dataframe_create_field(self): bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, 'r+', 'dst') - df = dataframe.HDF5DataFrame(dst, 'dst',) + df = dst.create_dataframe('dst') num = df.create_numeric('num', 'uint32') num.data.write([1, 2, 3, 4]) self.assertEqual([1, 2, 3, 4], num.data[:].tolist()) @@ -72,7 +71,7 @@ def test_dataframe_ops(self): bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, 'w', 'dst') - df = dataframe.HDF5DataFrame(dst, 'dst') + df = dst.create_dataframe('dst') numf = s.create_numeric(df, 'numf', 'int32') numf.data.write([5, 4, 3, 2, 1]) df.add(numf) @@ -80,7 +79,7 @@ def test_dataframe_ops(self): fst.data.write([b'e', b'd', b'c', b'b', b'a']) df.add(fst) index = np.array([4, 3, 2, 1, 0]) - ddf = dataframe.HDF5DataFrame(dst, 'dst2') + ddf = dst.create_dataframe('dst2') df.apply_index(index, ddf) self.assertEqual([1, 2, 3, 4, 5], ddf.get_field('numf').data[:].tolist()) self.assertEqual([b'a', b'b', b'c', b'd', b'e'], ddf.get_field('fst').data[:].tolist()) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 644d7d9b..ca55eff5 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -64,7 +64,7 @@ def test_dataset_init_with_data(self): #del dataframe del dst['df2'] #only 'grp1' left - self.assertTrue(len(dst.list())==1) + self.assertTrue(len(dst.keys())==1) self.assertTrue(len(dst.file.keys())==1) #set dataframe From 737eeede39fc4ea10994617a26c4a25c0aeb85f7 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 13 Apr 2021 15:13:24 +0100 Subject: [PATCH 048/181] fixing #13 and #14, add dest parameter to get_spans(), tidy up the field/fields parameters --- exetera/core/dataframe.py | 3 +- exetera/core/operations.py | 4 ++ exetera/core/persistence.py | 37 ---------- exetera/core/session.py | 131 +++++++++++++----------------------- tests/test_session.py | 18 ++++- 5 files changed, 67 insertions(+), 126 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 8ab0d5ea..68971b34 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -9,9 +9,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from exetera.core.abstract_types import AbstractSession, Dataset, DataFrame +from exetera.core.abstract_types import Dataset, DataFrame from exetera.core import fields as fld -from exetera.core import dataset as dst import h5py diff --git a/exetera/core/operations.py b/exetera/core/operations.py index 01450339..266da42f 100644 --- a/exetera/core/operations.py +++ b/exetera/core/operations.py @@ -218,6 +218,8 @@ def get_spans_for_field(ndarray): results[-1] = True return np.nonzero(results)[0] + +@njit def _get_spans_for_2_fields_by_spans(span0, span1): spans = [] j=0 @@ -233,6 +235,8 @@ def _get_spans_for_2_fields_by_spans(span0, span1): spans.extend(span1[j:]) return spans + +@njit def _get_spans_for_2_fields(ndarray0, ndarray1): count = 0 spans = np.zeros(len(ndarray0)+1, dtype=np.uint32) diff --git a/exetera/core/persistence.py b/exetera/core/persistence.py index 2a6568c0..36c1359d 100644 --- a/exetera/core/persistence.py +++ b/exetera/core/persistence.py @@ -265,19 +265,6 @@ def temp_dataset(): hd.flush() hd.close() - -# def _get_spans(field, fields): -# -# if field is not None: -# return _get_spans_for_field(field) -# elif len(fields) == 1: -# return _get_spans_for_field(fields[0]) -# elif len(fields) == 2: -# return _get_spans_for_2_fields(*fields) -# else: -# raise NotImplementedError("This operation does not support more than two fields at present") - - @njit def _index_spans(spans, results): sp_sta = spans[:-1] @@ -287,30 +274,6 @@ def _index_spans(spans, results): return results -# def _get_spans_for_field(field0): -# results = np.zeros(len(field0) + 1, dtype=np.bool) -# if np.issubdtype(field0.dtype, np.number): -# fn = np.not_equal -# else: -# fn = np.char.not_equal -# results[1:-1] = fn(field0[:-1], field0[1:]) -# -# results[0] = True -# results[-1] = True -# return np.nonzero(results)[0] - -# def _get_spans_for_2_fields(field0, field1): -# count = 0 -# spans = np.zeros(len(field0)+1, dtype=np.uint32) -# spans[0] = 0 -# for i in np.arange(1, len(field0)): -# if field0[i] != field0[i-1] or field1[i] != field1[i-1]: -# count += 1 -# spans[count] = i -# spans[count+1] = len(field0) -# return spans[:count+2] - - @njit def _apply_spans_index_of_max(spans, src_array, dest_array): for i in range(len(spans)-1): diff --git a/exetera/core/session.py b/exetera/core/session.py index a233dd14..fe90a866 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -52,17 +52,14 @@ def __init__(self, self.timestamp = timestamp self.datasets = dict() - def __enter__(self): """Context manager enter.""" return self - def __exit__(self, etype, evalue, etraceback): """Context manager exit closes any open datasets.""" self.close() - def open_dataset(self, dataset_path: str, mode: str, @@ -82,7 +79,6 @@ def open_dataset(self, self.datasets[name] = ds.HDF5Dataset(self, dataset_path, mode, name) return self.datasets[name] - def close_dataset(self, name: str): """ @@ -94,7 +90,6 @@ def close_dataset(self, self.datasets[name].close() del self.datasets[name] - def list_datasets(self): """ List the open datasets for this Session object. This is returned as a tuple of strings @@ -108,7 +103,6 @@ def list_datasets(self): """ return tuple(n for n in self.datasets.keys()) - def get_dataset(self, name: str): """ @@ -120,7 +114,6 @@ def get_dataset(self, """ return self.datasets[name] - def close(self): """ Close all open datasets @@ -130,7 +123,6 @@ def close(self): v.close() self.datasets = dict() - def get_shared_index(self, keys: Tuple[np.array]): """ @@ -168,7 +160,6 @@ def get_shared_index(self, return tuple(np.searchsorted(concatted, k) for k in raw_keys) - def set_timestamp(self, timestamp: str = str(datetime.now(timezone.utc))): """ @@ -183,7 +174,6 @@ def set_timestamp(self, raise ValueError(error_str.format(type(timestamp))) self.timestamp = timestamp - def sort_on(self, src_group: h5py.Group, dest_group: h5py.Group, @@ -201,6 +191,7 @@ def sort_on(self, exist :return: None """ + # TODO: fields is being ignored at present def print_if_verbose(*args): if verbose: @@ -233,7 +224,6 @@ def print_if_verbose(*args): print_if_verbose(f" '{k}' reordered in {time.time() - t1}s") print_if_verbose(f"fields reordered in {time.time() - t0}s") - def dataset_sort_index(self, sort_indices, index=None): """ Generate a sorted index based on a set of fields upon which to sort and an optional @@ -265,7 +255,6 @@ def dataset_sort_index(self, sort_indices, index=None): return acc_index - def apply_filter(self, filter_to_apply, src, dest=None): """ Apply a filter to an a src field. The filtered field is written to dest if it set, @@ -287,7 +276,7 @@ def apply_filter(self, filter_to_apply, src, dest=None): elif isinstance(src, Field): newfld = src.apply_filter(filter_to_apply_, writer_) return newfld.data[:] - #elif isinstance(src, df.datafrme): + # elif isinstance(src, df.datafrme): else: reader_ = val.array_from_parameter(self, 'reader', src) result = reader_[filter_to_apply] @@ -295,7 +284,6 @@ def apply_filter(self, filter_to_apply, src, dest=None): writer_.data.write(result) return result - def apply_index(self, index_to_apply, src, dest=None): """ Apply a index to an a src field. The indexed field is written to dest if it set, @@ -312,7 +300,7 @@ def apply_index(self, index_to_apply, src, dest=None): if dest is not None: writer_ = val.field_from_parameter(self, 'writer', dest) if isinstance(src, fld.IndexedStringField): - dest_indices, dest_values =\ + dest_indices, dest_values = \ ops.apply_indices_to_index_values(index_to_apply_, src.indices[:], src.values[:]) return dest_indices, dest_values @@ -326,7 +314,6 @@ def apply_index(self, index_to_apply, src, dest=None): writer_.data.write(result) return result - def distinct(self, field=None, fields=None, filter=None): if field is None and fields is None: @@ -346,10 +333,8 @@ def distinct(self, field=None, fields=None, filter=None): results = [uniques[f'{i}'] for i in range(len(fields))] return results - - def get_spans(self, - field: Union[Field, np.array] = None, - fields: Union[Tuple[Field], Tuple[np.array]] = None): + def get_spans(self, field: Union[Field, np.array] = None, + dest: Field = None, **kwargs): """ Calculate a set of spans that indicate contiguous equal values. The entries in the result array correspond to the inclusive start and @@ -366,28 +351,47 @@ def get_spans(self, result: [0, 1, 3, 6, 7, 10, 15] :param field: A Field or numpy array to be evaluated for spans - :param fields: A tuple of Fields or tuple of numpy arrays to be evaluated for spans + :param dest: A destination Field to store the result + :param **kwargs: See below. For parameters set in both argument and kwargs, use kwargs + + :Keyword Arguments: + * field -- Similar to field parameter, in case user specify field as keyword + * fields -- A tuple of Fields or tuple of numpy arrays to be evaluated for spans + * dest -- Similar to dest parameter, in case user specify as keyword + :return: The resulting set of spans as a numpy array """ - if field is None and fields is None: - raise ValueError("One of 'field' and 'fields' must be set") - if field is not None and fields is not None: - raise ValueError("Only one of 'field' and 'fields' may be set") - raw_field = None - if field is not None: - raw_field = val.array_from_parameter(self, 'field', field) + fields = [] + result = None + if len(kwargs) > 0: + for k in kwargs.keys(): + if k == 'field': + field = kwargs[k] + elif k == 'fields': + fields = kwargs[k] + elif k == 'dest': + dest = kwargs[k] + if dest is not None and not isinstance(dest, Field): + raise TypeError(f"'dest' must be one of 'Field' but is {type(dest)}") - if fields is not None: + if field is not None: + if isinstance(field, Field): + result = field.get_spans() + elif isinstance(field, np.ndarray): + result = ops.get_spans_for_field(field) + elif len(fields) > 0: if isinstance(fields[0], Field): - return ops._get_spans_for_2_fields_by_spans(fields[0].get_spans(), fields[1].get_spans()) - if isinstance(fields[0], np.ndarray): - return ops._get_spans_for_2_fields(fields[0], fields[1]) + result = ops._get_spans_for_2_fields_by_spans(fields[0].get_spans(), fields[1].get_spans()) + elif isinstance(fields[0], np.ndarray): + result = ops._get_spans_for_2_fields(fields[0], fields[1]) else: - if isinstance(field, Field): - return field.get_spans() - if isinstance(field, np.ndarray): - return ops.get_spans_for_field(field) + raise ValueError("One of 'field' and 'fields' must be set") + if dest is not None: + dest.data.write(result) + return dest + else: + return result def _apply_spans_no_src(self, predicate: Callable[[np.array, np.array], None], @@ -401,7 +405,7 @@ def _apply_spans_no_src(self, :params dest: if set, the field to which the results are written :returns: A numpy array containing the resulting values """ - assert(dest is None or isinstance(dest, Field)) + assert (dest is None or isinstance(dest, Field)) if dest is not None: dest_f = val.field_from_parameter(self, 'dest', dest) @@ -414,7 +418,6 @@ def _apply_spans_no_src(self, predicate(spans, results) return results - def _apply_spans_src(self, predicate: Callable[[np.array, np.array, np.array], None], spans: np.array, @@ -429,7 +432,7 @@ def _apply_spans_src(self, :params dest: if set, the field to which the results are written :returns: A numpy array containing the resulting values """ - assert(dest is None or isinstance(dest, Field)) + assert (dest is None or isinstance(dest, Field)) target_ = val.array_from_parameter(self, 'target', target) if len(target) != spans[-1]: error_msg = ("'target' (length {}) must be one element shorter than 'spans' " @@ -447,7 +450,6 @@ def _apply_spans_src(self, predicate(spans, target_, results) return results - def apply_spans_index_of_min(self, spans: np.array, target: np.array, @@ -461,7 +463,6 @@ def apply_spans_index_of_min(self, """ return self._apply_spans_src(ops.apply_spans_index_of_min, spans, target, dest) - def apply_spans_index_of_max(self, spans: np.array, target: np.array, @@ -475,7 +476,6 @@ def apply_spans_index_of_max(self, """ return self._apply_spans_src(ops.apply_spans_index_of_max, spans, target, dest) - def apply_spans_index_of_first(self, spans: np.array, dest: Field = None): @@ -487,7 +487,6 @@ def apply_spans_index_of_first(self, """ return self._apply_spans_no_src(ops.apply_spans_index_of_first, spans, dest) - def apply_spans_index_of_last(self, spans: np.array, dest: Field = None): @@ -499,7 +498,6 @@ def apply_spans_index_of_last(self, """ return self._apply_spans_no_src(ops.apply_spans_index_of_last, spans, dest) - def apply_spans_count(self, spans: np.array, dest: Field = None): @@ -511,7 +509,6 @@ def apply_spans_count(self, """ return self._apply_spans_no_src(ops.apply_spans_count, spans, dest) - def apply_spans_min(self, spans: np.array, target: np.array, @@ -525,7 +522,6 @@ def apply_spans_min(self, """ return self._apply_spans_src(ops.apply_spans_min, spans, target, dest) - def apply_spans_max(self, spans: np.array, target: np.array, @@ -539,7 +535,6 @@ def apply_spans_max(self, """ return self._apply_spans_src(ops.apply_spans_max, spans, target, dest) - def apply_spans_first(self, spans: np.array, target: np.array, @@ -553,7 +548,6 @@ def apply_spans_first(self, """ return self._apply_spans_src(ops.apply_spans_first, spans, target, dest) - def apply_spans_last(self, spans: np.array, target: np.array, @@ -567,7 +561,6 @@ def apply_spans_last(self, """ return self._apply_spans_src(ops.apply_spans_last, spans, target, dest) - def apply_spans_concat(self, spans, target, @@ -584,7 +577,6 @@ def apply_spans_concat(self, dest_chunksize = dest.chunksize if dest_chunksize is None else dest_chunksize chunksize_mult = 16 if chunksize_mult is None else chunksize_mult - src_index = target.indices[:] src_values = target.values[:] dest_index = np.zeros(src_chunksize, src_index.dtype) @@ -620,7 +612,6 @@ def apply_spans_concat(self, # dest.write_raw(dest_index[:index_i], dest_values[:index_v]) # dest.complete() - def _aggregate_impl(self, predicate, index, target=None, dest=None): """ An implementation method for aggregation of fields via various predicates. This method takes a predicate that @@ -650,7 +641,6 @@ def _aggregate_impl(self, predicate, index, target=None, dest=None): return dest if dest is not None else results - def aggregate_count(self, index, dest=None): """ Finds the number of entries within each sub-group of index. @@ -665,7 +655,6 @@ def aggregate_count(self, index, dest=None): """ return self._aggregate_impl(self.apply_spans_count, index, None, dest) - def aggregate_first(self, index, target=None, dest=None): """ Finds the first entries within each sub-group of index. @@ -682,7 +671,6 @@ def aggregate_first(self, index, target=None, dest=None): """ return self.aggregate_custom(self.apply_spans_first, index, target, dest) - def aggregate_last(self, index, target=None, dest=None): """ Finds the first entries within each sub-group of index. @@ -699,7 +687,6 @@ def aggregate_last(self, index, target=None, dest=None): """ return self.aggregate_custom(self.apply_spans_last, index, target, dest) - def aggregate_min(self, index, target=None, dest=None): """ Finds the minimum value within each sub-group of index. @@ -716,7 +703,6 @@ def aggregate_min(self, index, target=None, dest=None): """ return self.aggregate_custom(self.apply_spans_min, index, target, dest) - def aggregate_max(self, index, target=None, dest=None): """ Finds the maximum value within each sub-group of index. @@ -733,7 +719,6 @@ def aggregate_max(self, index, target=None, dest=None): """ return self.aggregate_custom(self.apply_spans_max, index, target, dest) - def aggregate_custom(self, predicate, index, target=None, dest=None): if target is None: raise ValueError("'src' must not be None") @@ -743,7 +728,6 @@ def aggregate_custom(self, predicate, index, target=None, dest=None): return self._aggregate_impl(predicate, index, target, dest) - def join(self, destination_pkey, fkey_indices, values_to_join, writer=None, fkey_index_spans=None): @@ -782,10 +766,9 @@ def join(self, safe_values_to_join = raw_values_to_join[invalid_filter] # now get the memory that the results will be mapped to - #destination_space_values = writer.chunk_factory(len(destination_pkey)) + # destination_space_values = writer.chunk_factory(len(destination_pkey)) destination_space_values = np.zeros(len(destination_pkey), dtype=raw_values_to_join.dtype) - # finally, map the results from the source space to the destination space destination_space_values[safe_unique_fkey_indices] = safe_values_to_join @@ -794,7 +777,6 @@ def join(self, else: return destination_space_values - def predicate_and_join(self, predicate, destination_pkey, fkey_indices, reader=None, writer=None, fkey_index_spans=None): @@ -827,7 +809,7 @@ def predicate_and_join(self, dtype = reader.dtype() else: dtype = np.uint32 - results = np.zeros(len(fkey_index_spans)-1, dtype=dtype) + results = np.zeros(len(fkey_index_spans) - 1, dtype=dtype) predicate(fkey_index_spans, reader, results) # the predicate results are in the same space as the unique_fkey_indices, which @@ -841,7 +823,6 @@ def predicate_and_join(self, writer.write(destination_space_values) - def get(self, field: Union[Field, h5py.Group]): """ @@ -880,7 +861,6 @@ def get(self, fieldtype = field.attrs['fieldtype'].split(',')[0] return fieldtype_map[fieldtype](self, field) - def create_like(self, field, dest_group, dest_name, timestamp=None, chunksize=None): """ Create a field of the same type as an existing field, in the location and with the name provided. @@ -907,7 +887,6 @@ def create_like(self, field, dest_group, dest_name, timestamp=None, chunksize=No else: raise ValueError("'field' must be either a Field or a h5py.Group, but is {}".format(type(field))) - def create_indexed_string(self, group, name, timestamp=None, chunksize=None): """ Create an indexed string field in the given DataFrame with the given name. @@ -932,7 +911,6 @@ def create_indexed_string(self, group, name, timestamp=None, chunksize=None): else: return group.create_indexed_string(name, timestamp, chunksize) - def create_fixed_string(self, group, name, length, timestamp=None, chunksize=None): """ Create an fixed string field in the given DataFrame with the given name, with the given max string length per entry. @@ -957,7 +935,6 @@ def create_fixed_string(self, group, name, length, timestamp=None, chunksize=Non else: return group.create_fixed_string(name, length, timestamp, chunksize) - def create_categorical(self, group, name, nformat, key, timestamp=None, chunksize=None): """ Create a categorical field in the given DataFrame with the given name. This function also takes a numerical format @@ -987,7 +964,6 @@ def create_categorical(self, group, name, nformat, key, timestamp=None, chunksiz else: return group.create_categorical(name, nformat, key, timestamp, chunksize) - def create_numeric(self, group, name, nformat, timestamp=None, chunksize=None): """ Create a numeric field in the given DataFrame with the given name. @@ -1011,11 +987,10 @@ def create_numeric(self, group, name, nformat, timestamp=None, chunksize=None): "{} was passed to it".format(type(group))) if isinstance(group, h5py.Group): - return fld.numeric_field_constructor(self. group, name, timestamp, chunksize) + return fld.numeric_field_constructor(self.group, name, timestamp, chunksize) else: return group.create_numeric(name, nformat, timestamp, chunksize) - def create_timestamp(self, group, name, timestamp=None, chunksize=None): """ Create a timestamp field in the given group with the given name. @@ -1033,13 +1008,11 @@ def create_timestamp(self, group, name, timestamp=None, chunksize=None): else: return group.create_timestamp(name, timestamp, chunksize) - def get_or_create_group(self, group, name): if name in group: return group[name] return group.create_group(name) - def chunks(self, length, chunksize=None): if chunksize is None: chunksize = self.chunksize @@ -1049,7 +1022,6 @@ def chunks(self, length, chunksize=None): yield cur, next cur = next - def process(self, inputs, outputs, predicate): # TODO: modifying the dictionaries in place is not great @@ -1090,7 +1062,6 @@ def process(self, inputs, outputs, predicate): for k, v in output_writers.items(): output_writers[k].flush() - def get_index(self, target, foreign_key, destination=None): print(' building patient_id index') t0 = time.time() @@ -1124,7 +1095,6 @@ def get_index(self, target, foreign_key, destination=None): else: return foreign_key_index - def get_trash_group(self, group): group_names = group.name[1:].split('/') @@ -1137,14 +1107,12 @@ def get_trash_group(self, group): except KeyError: pass - def temp_filename(self): uid = str(uuid.uuid4()) while os.path.exists(uid + '.hdf5'): uid = str(uuid.uuid4()) return uid + '.hdf5' - def merge_left(self, left_on, right_on, right_fields=tuple(), right_writers=None): l_key_raw = val.raw_array_from_parameter(self, 'left_on', left_on) @@ -1171,7 +1139,6 @@ def merge_left(self, left_on, right_on, return right_results - def merge_right(self, left_on, right_on, left_fields=None, left_writers=None): l_key_raw = val.raw_array_from_parameter(self, 'left_on', left_on) @@ -1197,7 +1164,6 @@ def merge_right(self, left_on, right_on, return left_results - def merge_inner(self, left_on, right_on, left_fields=None, left_writers=None, right_fields=None, right_writers=None): l_key_raw = val.raw_array_from_parameter(self, 'left_on', left_on) @@ -1234,7 +1200,6 @@ def merge_inner(self, left_on, right_on, return left_results, right_results - def _map_fields(self, field_map, field_sources, field_sinks): rtn_sinks = None if field_sinks is None: @@ -1259,7 +1224,6 @@ def _map_fields(self, field_map, field_sources, field_sinks): ops.map_valid(src_, field_map, snk_) return None if rtn_sinks is None else tuple(rtn_sinks) - def _streaming_map_fields(self, field_map, field_sources, field_sinks): # field map must be a field # field sources must be fields @@ -1271,7 +1235,6 @@ def _streaming_map_fields(self, field_map, field_sources, field_sinks): snk_ = val.field_from_parameter(self, 'field_sinks', snk) ops.ordered_map_valid_stream(src_, map_, snk_) - def ordered_merge_left(self, left_on, right_on, right_field_sources=tuple(), left_field_sinks=None, left_to_right_map=None, left_unique=False, right_unique=False): """ @@ -1321,7 +1284,7 @@ def ordered_merge_left(self, left_on, right_on, right_field_sources=tuple(), lef if streamable: has_unmapped = \ ops.ordered_map_to_right_right_unique_streamed(left_on, right_on, - left_to_right_map) + left_to_right_map) result = left_to_right_map else: result = np.zeros(len(left_on), dtype=np.int64) @@ -1347,7 +1310,6 @@ def ordered_merge_left(self, left_on, right_on, right_field_sources=tuple(), lef rtn_left_sinks = self._map_fields(result, right_field_sources, left_field_sinks) return rtn_left_sinks - def ordered_merge_right(self, left_on, right_on, left_field_sources=tuple(), right_field_sinks=None, right_to_left_map=None, left_unique=False, right_unique=False): @@ -1376,7 +1338,6 @@ def ordered_merge_right(self, left_on, right_on, return self.ordered_merge_left(right_on, left_on, left_field_sources, right_field_sinks, right_to_left_map, right_unique, left_unique) - def ordered_merge_inner(self, left_on, right_on, left_field_sources=tuple(), left_field_sinks=None, right_field_sources=tuple(), right_field_sinks=None, diff --git a/tests/test_session.py b/tests/test_session.py index 570fc5e4..86191304 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -7,6 +7,7 @@ from exetera.core import session from exetera.core import fields +from exetera.core import dataframe from exetera.core import persistence as per @@ -449,7 +450,6 @@ def test_apply_filter(self): class TestSessionGetSpans(unittest.TestCase): def test_get_spans_one_field(self): - vals = np.asarray([0, 1, 1, 3, 3, 6, 5, 5, 5], dtype=np.int32) bio = BytesIO() with session.Session() as s: @@ -462,7 +462,6 @@ def test_get_spans_one_field(self): self.assertListEqual([0, 1, 3, 5, 6, 9], s.get_spans(s.get(ds['vals'])).tolist()) def test_get_spans_two_fields(self): - vals_1 = np.asarray(['a', 'a', 'a', 'b', 'b', 'b', 'b', 'b', 'c', 'c', 'c', 'c'], dtype='S1') vals_2 = np.asarray([5, 5, 6, 2, 2, 3, 4, 4, 7, 7, 7, 7], dtype=np.int32) bio = BytesIO() @@ -486,6 +485,21 @@ def test_get_spans_index_string_field(self): idx.data.write(['aa','bb','bb','c','c','c','d','d','e','f','f','f']) self.assertListEqual([0,1,3,6,8,9,12],s.get_spans(idx)) + def test_get_spans_with_dest(self): + vals = np.asarray([0, 1, 1, 3, 3, 6, 5, 5, 5], dtype=np.int32) + bio = BytesIO() + with session.Session() as s: + self.assertListEqual([0, 1, 3, 5, 6, 9], s.get_spans(vals).tolist()) + + dst = s.open_dataset(bio, "w", "src") + ds = dst.create_dataframe('ds') + vals_f = s.create_numeric(ds, "vals", "int32") + vals_f.data.write(vals) + self.assertListEqual([0, 1, 3, 5, 6, 9], s.get_spans(s.get(ds['vals'])).tolist()) + + span_dest = ds.create_numeric('span','int32') + s.get_spans(ds['vals'],dest=span_dest) + self.assertListEqual([0, 1, 3, 5, 6, 9],ds['span'].data[:].tolist()) class TestSessionAggregate(unittest.TestCase): From 732762d5cf35e31f2d47682913346ba79e1dca84 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 13 Apr 2021 15:37:01 +0100 Subject: [PATCH 049/181] minor fix remove dataframe and file property from dataset, as not used so far. --- exetera/core/dataset.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index ddab8b4a..b91bf390 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -28,14 +28,6 @@ def __init__(self, session, dataset_path, mode, name): def session(self): return self._session - @property - def dataframes(self): - return self._dataframes - - @property - def file(self): - return self._file - def close(self): self._file.close() @@ -105,7 +97,7 @@ def get_name(self, dataframe): """ if not isinstance(dataframe, edf.DataFrame): raise TypeError("The field argument must be a DataFrame object.") - for name, v in self.dataframes.items(): + for name, v in self._dataframes.items(): if id(dataframe) == id(v): return name return None From ab6508c494579d40dc639da5d0745afd33073d5c Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 13 Apr 2021 15:49:55 +0100 Subject: [PATCH 050/181] minor fix on unittest --- tests/test_dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index ca55eff5..a9d849f9 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -65,7 +65,7 @@ def test_dataset_init_with_data(self): #del dataframe del dst['df2'] #only 'grp1' left self.assertTrue(len(dst.keys())==1) - self.assertTrue(len(dst.file.keys())==1) + self.assertTrue(len(dst._file.keys())==1) #set dataframe dst['grp1']=df2 From 358d82b5dd82492a3213d3ad6d7f855dd3a8d7f4 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 14 Apr 2021 09:50:35 +0100 Subject: [PATCH 051/181] add docstring for dataset --- exetera/core/dataset.py | 84 ++++++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 14 deletions(-) diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index b91bf390..c5795e13 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -10,13 +10,25 @@ # limitations under the License. import h5py -from exetera.core.abstract_types import Dataset +from exetera.core.abstract_types import Dataset,DataFrame from exetera.core import dataframe as edf class HDF5Dataset(Dataset): def __init__(self, session, dataset_path, mode, name): + """ + Create a HDF5Dataset instance that contains dataframes. The dataframes are represented in a dict() with the + name(str) as a key. The construction should always be called by Session.open_dataset() otherwise the instance + is not included in Session.datasets. If the HDF5 datafile contains group, the content in loaded into dataframes. + + :param session: The session instance to include this dataset to. + :param dataset_path: The path of HDF5 file. + :param mode: the mode in which the dataset should be opened. This is one of "r", "r+" or "w". + :param name: the name that is associated with this dataset. This can be used to retrieve the dataset when + calling :py:meth:`~session.Session.get_dataset`. + :return: A HDF5Dataset instance. + """ self.name = name self._session = session self._file = h5py.File(dataset_path, mode) @@ -26,9 +38,15 @@ def __init__(self, session, dataset_path, mode, name): @property def session(self): + """ + The session property interface. + + :return: The _session instance. + """ return self._session def close(self): + """Close the HDF5 file operations.""" self._file.close() def create_dataframe(self, name, dataframe: dict = None, h5group: h5py.Group = None): @@ -54,25 +72,32 @@ def add(self, dataframe, name=None): Add an existing dataframe (from other dataset) to this dataset, write the existing group attributes and HDF5 datasets to this dataset. - :param dataframe: the dataframe to copy to this dataset - :param name: optional- change the dataframe name + :param dataframe: the dataframe to copy to this dataset. + :param name: optional- change the dataframe name. + :return: None if the operation is successful; otherwise throw Error. """ dname = dataframe.name if name is None else name self._file.copy(dataframe.h5group, self._file, name=dname) df = edf.HDF5DataFrame(self, dname, h5group=self._file[dname]) self._dataframes[dname] = df - def __contains__(self, name): + def __contains__(self, name: str): + """ + Check if the name exists in this dataset. + + :param name: Name of the dataframe to check. + :return: Boolean if the name exists. + """ return self._dataframes.__contains__(name) - def contains_dataframe(self, dataframe): + def contains_dataframe(self, dataframe: DataFrame): """ Check if a dataframe is contained in this dataset by the dataframe object itself. :param dataframe: the dataframe object to check :return: Ture or False if the dataframe is contained """ - if not isinstance(dataframe, edf.DataFrame): + if not isinstance(dataframe, DataFrame): raise TypeError("The field must be a DataFrame object") else: for v in self._dataframes.values(): @@ -80,7 +105,12 @@ def contains_dataframe(self, dataframe): return True return False - def __getitem__(self, name): + def __getitem__(self, name: str): + """ + Get the dataframe by dataset[dataframe_name]. + + :param name: The name of the dataframe to get. + """ if not isinstance(name, str): raise TypeError("The name must be a str object.") elif not self.__contains__(name): @@ -88,12 +118,21 @@ def __getitem__(self, name): else: return self._dataframes[name] - def get_dataframe(self, name): + def get_dataframe(self, name: str): + """ + Get the dataframe by dataset.get_dataframe(dataframe_name). + + :param name: The name of the dataframe. + :return: The dataframe or throw Error if the name is not existed in this dataset. + """ self.__getitem__(name) - def get_name(self, dataframe): + def get_name(self, dataframe: DataFrame): """ - Get the name of the dataframe in this dataset. + If the dataframe exist in this dataset, return the name; otherwise return None. + + :param dataframe: The dataframe instance to find the name. + :return: name (str) of the dataframe or None if dataframe not found in this dataset. """ if not isinstance(dataframe, edf.DataFrame): raise TypeError("The field argument must be a DataFrame object.") @@ -102,11 +141,15 @@ def get_name(self, dataframe): return name return None - def __setitem__(self, name, dataframe): + def __setitem__(self, name: str, dataframe: DataFrame): """ Add an existing dataframe (from other dataset) to this dataset, the existing dataframe can from: 1) this dataset, so perform a 'rename' operation, or; 2) another dataset, so perform an 'add' or 'replace' operation + + :param name: The name of the dataframe to store in this dataset. + :param dataframe: The dataframe instance to store in this dataset. + :return: None if the operation is successful; otherwise throw Error. """ if not isinstance(name, str): raise TypeError("The name must be a str object.") @@ -120,7 +163,12 @@ def __setitem__(self, name, dataframe): self.__delitem__(name) return self.add(dataframe, name) - def __delitem__(self, name): + def __delitem__(self, name: str): + """ + Delete a dataframe by del dataset[name]. + :param name: The name of dataframe to delete. + :return: Boolean if the dataframe is deleted. + """ if not self.__contains__(name): raise ValueError("This dataframe does not contain the name to delete.") else: @@ -128,9 +176,11 @@ def __delitem__(self, name): del self._file[name] return True - def delete_dataframe(self, dataframe): + def delete_dataframe(self, dataframe: DataFrame): """ - Remove dataframe from this dataset by dataframe object. + Remove dataframe from this dataset by the dataframe object. + :param dataframe: The dataframe instance to delete. + :return: Boolean if the dataframe is deleted. """ name = self.get_name(dataframe) if name is None: @@ -139,19 +189,25 @@ def delete_dataframe(self, dataframe): self.__delitem__(name) def keys(self): + """Return all dataframe names in this dataset.""" return self._dataframes.keys() def values(self): + """Return all dataframe instance in this dataset.""" return self._dataframes.values() def items(self): + """Return the (name, dataframe) tuple in this dataset.""" return self._dataframes.items() def __iter__(self): + """Iteration through the dataframes stored in this dataset.""" return iter(self._dataframes) def __next__(self): + """Next dataframe for iteration through dataframes stored.""" return next(self._dataframes) def __len__(self): + """Return the number of dataframes stored in this dataset.""" return len(self._dataframes) From 98a4d7f7f36bd336835ee493040f678e2bbad3d2 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 15 Apr 2021 10:03:39 +0100 Subject: [PATCH 052/181] copy/move for dataframe; docstrings --- exetera/core/dataframe.py | 31 +++++++++++++++++++++++-------- exetera/core/dataset.py | 13 +++++++++++++ exetera/core/session.py | 2 +- tests/test_dataset.py | 29 ++++++++++++++++++++++++++--- 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 68971b34..4a6da14f 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -24,14 +24,16 @@ def __init__(self, h5group: h5py.Group, dataframe: dict = None): """ - Create a Dataframe object, user should always call from dataset.create_dataframe. - - :param name: name of the dataframe, or the group name in HDF5 - :param dataset: a dataset object, where this dataframe belongs to - :param h5group: acquire data from h5group object directly, the h5group needs to have a - h5group<-group-dataset structure, the group has a 'fieldtype' attribute - and the dataset is named 'values'. - :param dataframe: optional - replicate data from another dictionary + Create a Dataframe object, that contains a dictionary of fields. User should always create dataframe by + dataset.create_dataframe, otherwise the dataframe is not stored in the dataset. + + :param name: name of the dataframe. + :param dataset: a dataset object, where this dataframe belongs to. + :param h5group: the h5group object to store the fields. If the h5group is not empty, acquire data from h5group + object directly. The h5group structure is h5group<-h5group-dataset structure, the later group has a + 'fieldtype' attribute and only one dataset named 'values'. So that the structure is mapped to + Dataframe<-Field-Field.data automatically. + :param dataframe: optional - replicate data from another dictionary of (name:str, field: Field). """ self.name = name @@ -51,17 +53,30 @@ def __init__(self, @property def columns(self): + """ + The columns property interface. + """ return dict(self._columns) @property def dataset(self): + """ + The dataset property interface. + """ return self._dataset @property def h5group(self): + """ + The h5group property interface. + """ return self._h5group def add(self, field, name=None): + """ + Add a field to this dataframe. + + """ if name is not None: if not isinstance(name, str): raise TypeError("The name must be a str object.") diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index c5795e13..ea96f74d 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -211,3 +211,16 @@ def __next__(self): def __len__(self): """Return the number of dataframes stored in this dataset.""" return len(self._dataframes) + + @staticmethod + def copy(dataframe: DataFrame, dataset: Dataset, name: str): + dataset.add(dataframe,name=name) + + @staticmethod + def move(dataframe: DataFrame, dataset: Dataset, name:str): + dataset.add(dataframe, name=name) + dataframe._dataset.delete_dataframe(dataframe) + + @staticmethod + def drop(dataframe: DataFrame): + dataframe._dataset.delete_dataframe(dataframe) diff --git a/exetera/core/session.py b/exetera/core/session.py index fe90a866..53b64c29 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -43,7 +43,7 @@ def __init__(self, is no longer required. In general, it should only be changed for testing. :param timestamp: Set the official timestamp for the Session's creation rather than taking the current date/time. - :return A newly created Session object + :return: A newly created Session object """ if not isinstance(timestamp, str): error_str = "'timestamp' must be a string but is of type {}" diff --git a/tests/test_dataset.py b/tests/test_dataset.py index a9d849f9..b9a0eaec 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -3,10 +3,10 @@ import h5py import numpy as np -from exetera.core import session +from exetera.core import session,fields from exetera.core.abstract_types import DataFrame from io import BytesIO -from exetera.core import data_writer +from exetera.core.dataset import HDF5Dataset class TestDataSet(unittest.TestCase): @@ -70,4 +70,27 @@ def test_dataset_init_with_data(self): #set dataframe dst['grp1']=df2 self.assertTrue(isinstance(dst['grp1'], DataFrame)) - self.assertEqual([b'a', b'b', b'c', b'd'], dst['grp1']['fs'].data[:].tolist()) \ No newline at end of file + self.assertEqual([b'a', b'b', b'c', b'd'], dst['grp1']['fs'].data[:].tolist()) + + def test_dataste_static_func(self): + bio = BytesIO() + bio2 = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'r+', 'dst') + df = dst.create_dataframe('df') + num1 = df.create_numeric('num','uint32') + num1.data.write([1,2,3,4]) + + ds2 = s.open_dataset(bio2,'r+','ds2') + HDF5Dataset.copy(df,ds2,'df2') + print(type(ds2['df2'])) + self.assertTrue(isinstance(ds2['df2'],DataFrame)) + self.assertTrue(isinstance(ds2['df2']['num'],fields.Field)) + + HDF5Dataset.drop(ds2['df2']) + self.assertTrue(len(ds2)==0) + + HDF5Dataset.move(df,ds2,'df2') + self.assertTrue(len(dst) == 0) + self.assertTrue(len(ds2) == 1) + From a0e016760c60d303e5a247510a3faaa1b55f1152 Mon Sep 17 00:00:00 2001 From: clyyuanzi-london <59363720+clyyuanzi-london@users.noreply.github.com> Date: Thu, 15 Apr 2021 11:31:13 +0100 Subject: [PATCH 053/181] categorical field: convert from byte int to value int within njit function --- exetera/core/csv_reader_speedup.py | 253 +++++++++++--------- exetera/core/importer.py | 361 ++++++++++++++++++++++++++++- exetera/core/readerwriter.py | 17 +- 3 files changed, 507 insertions(+), 124 deletions(-) diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py index 03250a92..390d7737 100644 --- a/exetera/core/csv_reader_speedup.py +++ b/exetera/core/csv_reader_speedup.py @@ -17,19 +17,64 @@ def __exit__(self, exc_type, exc_val, exc_tb): print(self.end_msg + f' {time.time() - self.t0} seconds') +# def generate_test_arrays(count): +# strings = [b'one', b'two', b'three', b'four', b'five', b'six', b'seven'] +# raw_values = np.random.RandomState(12345678).randint(low=1, high=7, size=count) +# total_len = 0 +# for r in raw_values: +# total_len += len(strings[r]) +# indices = np.zeros(count+1, dtype=np.int64) +# values = np.zeros(total_len, dtype=np.int8) +# for i_r, r in enumerate(raw_values): +# indices[i_r+1] = indices[i_r] + len(strings[r]) +# for i_c in range(len(strings[r])): +# values[indices[i_r]+i_c] = strings[r][i_c] +# +# for i_r in range(20): +# start, end = indices[i_r], indices[i_r+1] +# print(values[start:end].tobytes()) + + def main(): - source = 'resources/assessment_input_small_data.csv' - print(source) - # run once first - original_csv_read(source) - + # generate_test_arrays(1000) + col_dicts = [{'name': 'a', 'type': 'cat', 'vals': ('a', 'bb', 'ccc', 'dddd', 'eeeee')}, + {'name': 'b', 'type': 'float'}, + {'name': 'c', 'type': 'cat', 'vals': ('', '', '', '', '', 'True', 'False')}, + {'name': 'd', 'type': 'float'}, + {'name': 'e', 'type': 'float'}, + {'name': 'f', 'type': 'cat', 'vals': ('', '', '', '', '', 'True', 'False')}, + {'name': 'g', 'type': 'cat', 'vals': ('', '', '', '', 'True', 'False')}, + {'name': 'h', 'type': 'cat', 'vals': ('', '', '', 'No', 'Yes')}] + # make_test_data(100000, col_dicts) + #source = '/Users/lc21/Documents/KCL_BMEIS/ExeTera/resources/assessment_input_small_data.csv' + + source = '/Users/frecar/Desktop/ExeTera/resources/assessment_input_sample_data.csv' + + # print(source) + # # run once first + # orig_inds = [] + # orig_vals = [] + # for i in range(len(col_dicts)+1): + # orig_inds.append(np.zeros(1000000, dtype=np.int64)) + # orig_vals.append(np.zeros(10000000, dtype='|S1')) + # original_csv_read(source, orig_inds, orig_vals) + # del orig_inds + # del orig_vals + + # orig_inds = [] + # orig_vals = [] + # for i in range(len(col_dicts)+1): + # orig_inds.append(np.zeros(1000000, dtype=np.int64)) + # orig_vals.append(np.zeros(10000000, dtype='|S1')) with Timer("Original csv reader took:"): original_csv_read(source) + # del orig_inds + # del orig_vals + + file_read_line_fast_csv(source) file_read_line_fast_csv(source) - with Timer("FAST Open file read lines took"): - file_read_line_fast_csv(source) - + # original csv reader @@ -37,13 +82,12 @@ def original_csv_read(source): time0 = time.time() with open(source) as f: csvf = csv.reader(f, delimiter=',', quotechar='"') - for i_r, row in enumerate(csvf): pass # print('Original csv reader took {} s'.format(time.time() - time0)) - + # FAST open file read line def file_read_line_fast_csv(source): @@ -53,25 +97,60 @@ def file_read_line_fast_csv(source): content = f.read() count_rows = content.count('\n') + 1 - content = np.fromfile(source, dtype='|S1') - - column_inds = np.zeros(count_rows * count_columns, dtype = np.int64).reshape(count_rows, count_columns) - - my_fast_csv_reader_int(content, column_inds) - - for row in column_inds: - #print(row) - for i, e in enumerate(row): - pass + content = np.fromfile(source, dtype='|S1')#np.uint8) + column_inds = np.zeros((count_columns, count_rows), dtype=np.int64) + column_vals = np.zeros((count_columns, count_rows * 25), dtype=np.uint8) + + # separator = np.frombuffer(b',', dtype='S1')[0][0] + # delimiter = np.frombuffer(b'"', dtype='S1')[0][0] + ESCAPE_VALUE = np.frombuffer(b'"', dtype='S1')[0][0] + SEPARATOR_VALUE = np.frombuffer(b',', dtype='S1')[0][0] + NEWLINE_VALUE = np.frombuffer(b'\n', dtype='S1')[0][0] + # ESCAPE_VALUE = b'"' + # SEPARATOR_VALUE = b',' + # NEWLINE_VALUE = b'\n' + with Timer("my_fast_csv_reader_int"): + #source = '/Users/lc21/Documents/KCL_BMEIS/ExeTera/resources/assessment_input_small_data.csv' + #source = '/Users/frecar/Desktop/ExeTera/resources/assessment_input_small_data.csv' + content = np.fromfile(source, dtype=np.uint8) + my_fast_csv_reader_int(content, column_inds, column_vals, ESCAPE_VALUE, SEPARATOR_VALUE, NEWLINE_VALUE) + + #a = get_cell(0,1,column_inds, column_vals) + + return column_inds, column_vals + + +def get_cell(row,col, column_inds, column_vals): + start_row_index = column_inds[col][row] + end_row_index = column_inds[col][row+1] + return column_vals[col][start_row_index:end_row_index].tobytes() + + +def make_test_data(count, schema): + """ + [ {'name':name, 'type':'cat'|'float'|'fixed', 'values':(vals)} ] + """ + import pandas as pd + rng = np.random.RandomState(12345678) + columns = {} + for s in schema: + if s['type'] == 'cat': + vals = s['vals'] + arr = rng.randint(low=0, high=len(vals), size=count) + larr = [None] * count + for i in range(len(arr)): + larr[i] = vals[arr[i]] + columns[s['name']] = larr + elif s['type'] == 'float': + arr = rng.uniform(size=count) + columns[s['name']] = arr + + df = pd.DataFrame(columns) + df.to_csv('/home/ben/covid/benchmark_csv.csv', index_label='index') @njit -def my_fast_csv_reader_int(source, column_inds): - ESCAPE_VALUE = b'"' - SEPARATOR_VALUE = b',' - NEWLINE_VALUE = b'\n' - - #max_rowcount = len(column_inds) - 1 +def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separator_value, newline_value): colcount = len(column_inds[0]) index = np.int64(0) @@ -79,111 +158,59 @@ def my_fast_csv_reader_int(source, column_inds): cell_start_idx = np.int64(0) cell_end_idx = np.int64(0) col_index = np.int64(0) - row_index = np.int64(0) - - # how to parse csv - # . " is the escape character - # . fields that need to contain '"', ',' or '\n' must be quoted - # . while escaped - # . ',' and '\n' are considered part of the field - # . i.e. a,"b,c","d\ne","f""g""" - # . while not escaped - # . ',' ends the cell and starts a new cell - # . '\n' ends the cell and starts a new row - # . after the first row, we should check that subsequent rows have the same cell count + row_index = np.int64(-1) + current_char_count = np.int32(0) + escaped = False end_cell = False end_line = False escaped_literal_candidate = False - while True: - c = source[index] - if c == SEPARATOR_VALUE: - if not escaped: #or escaped_literal_candidate: - # don't write this char - end_cell = True - cell_end_idx = index - # while index + 1 < len(source) and source[index + 1] == ' ': - # index += 1 - - else: - # write literal ',' - # cell_value.append(c) - pass + cur_cell_start = column_inds[col_index, row_index] if row_index >= 0 else 0 + cur_cell_char_count = 0 + while True: + write_char = False + end_cell = False + end_line = False - elif c == NEWLINE_VALUE: - if not escaped: #or escaped_literal_candidate: - # don't write this char + c = source[index] + if c == separator_value: + if not escaped: end_cell = True - end_line = True - cell_end_idx = index else: - # write literal '\n' - pass - #cell_value.append(c) - - elif c == ESCAPE_VALUE: - # ,"... - start of an escaped cell - # ...", - end of an escaped cell - # ...""... - literal quote character - # otherwise error + write_char = True + + elif c == newline_value: if not escaped: - # this must be the first character of a cell - if index != cell_start_idx: - # raise error! - pass - # don't write this char - else: - escaped = True + end_cell = True + end_line = True else: - - escaped = False - # if escaped_literal_candidate: - # escaped_literal_candidate = False - # # literal quote character confirmed, write it - # cell_value.append(c) - # else: - # escaped_literal_candidate = True - # # don't write this char - + write_char = True + elif c == escape_value: + escaped = not escaped else: - # cell_value.append(c) - pass - # if escaped_literal_candidate: - # # error! - # pass - # # raise error return -2 + write_char = True + + if write_char and row_index >= 0: + column_vals[col_index, cur_cell_start + cur_cell_char_count] = c + cur_cell_char_count += 1 - # parse c - index += 1 - if end_cell: - end_cell = False - #column_inds[col_index][row_index+1] =\ - # column_inds[col_index][row_index] + cell_end - cell_start - column_inds[row_index][col_index] = cell_end_idx - - cell_start_idx = cell_end_idx + 1 - - col_index += 1 - - - if col_index == colcount: - if not end_line: - raise Exception('.....') - else: - end_line = False - + if row_index >= 0: + column_inds[col_index, row_index+1] = cur_cell_start + cur_cell_char_count + if end_line: row_index += 1 col_index = 0 + else: + col_index += 1 + cur_cell_start = column_inds[col_index, row_index] + cur_cell_char_count = 0 - if index == len(source): - # "erase the partially written line" - return column_inds - #return line_start - + index += 1 + if index == len(source): + break if __name__ == "__main__": main() diff --git a/exetera/core/importer.py b/exetera/core/importer.py index 655881b4..940ba11e 100644 --- a/exetera/core/importer.py +++ b/exetera/core/importer.py @@ -15,20 +15,23 @@ import numpy as np import h5py +from numba import njit,jit, prange, vectorize, float64 +from numba.typed import List +from collections import Counter from exetera.core import csvdataset as dataset from exetera.core import persistence as per from exetera.core import utils from exetera.core import operations as ops from exetera.core.load_schema import load_schema - +from exetera.core.csv_reader_speedup import file_read_line_fast_csv def import_with_schema(timestamp, dest_file_name, schema_file, files, overwrite, include, exclude): print(timestamp) print(schema_file) print(files) - + with open(schema_file) as sf: schema = load_schema(sf) @@ -79,7 +82,7 @@ def import_with_schema(timestamp, dest_file_name, schema_file, files, overwrite, print("Warning:", msg.format(files[sk], missing_names)) # raise ValueError(msg.format(files[sk], missing_names)) - # check if included/exclude fields are in the file + # check if included/exclude fields are in the file include_missing_names = set(include.get(sk, [])).difference(names) if len(include_missing_names) > 0: msg = "The following include fields are not part of the {}: {}" @@ -118,7 +121,341 @@ def import_with_schema(timestamp, dest_file_name, schema_file, files, overwrite, class DatasetImporter: - def __init__(self, datastore, source, hf, space, schema, timestamp, + def __init__(self, datastore, source, hf, space, schema, timestamp, + include=None, exclude=None, + keys=None, + stop_after=None, show_progress_every=None, filter_fn=None, + early_filter=None): + # self.names_ = list() + self.index_ = None + + #stop_after = 2000000 + + file_read_line_fast_csv(source) + + time0 = time.time() + + seen_ids = set() + + if space not in hf.keys(): + hf.create_group(space) + group = hf[space] + + with open(source) as sf: + csvf = csv.DictReader(sf, delimiter=',', quotechar='"') + + available_keys = [k.strip() for k in csvf.fieldnames if k.strip() in schema.fields] + if space in include and len(include[space]) > 0: + available_keys = include[space] + if space in exclude and len(exclude[space]) > 0: + available_keys = [k for k in available_keys if k not in exclude[space]] + + available_keys = ['ruc11cd','ruc11'] + #available_keys = ['ruc11'] + + if not keys: + fields_to_use = available_keys + # index_map = [csvf.fieldnames.index(k) for k in fields_to_use] + # index_map = [i for i in range(len(fields_to_use))] + else: + for k in keys: + if k not in available_keys: + raise ValueError(f"key '{k}' isn't in the available keys ({keys})") + fields_to_use = keys + # index_map = [csvf.fieldnames.index(k) for k in fields_to_use] + + csvf_fieldnames = [k.strip() for k in csvf.fieldnames] + index_map = [csvf_fieldnames.index(k) for k in fields_to_use] + + early_key_index = None + if early_filter is not None: + if early_filter[0] not in available_keys: + raise ValueError( + f"'early_filter': tuple element zero must be a key that is in the dataset") + early_key_index = available_keys.index(early_filter[0]) + + chunk_size = 1 << 20 + new_fields = dict() + new_field_list = list() + field_chunk_list = list() + categorical_map_list = list() + longest_keys = list() + + # TODO: categorical writers should use the datatype specified in the schema + for i_n in range(len(fields_to_use)): + field_name = fields_to_use[i_n] + sch = schema.fields[field_name] + writer = sch.importer(datastore, group, field_name, timestamp) + # TODO: this list is required because we convert the categorical values to + # numerical values ahead of adding them. We could use importers that handle + # that transform internally instead + + string_map = sch.strings_to_values + + byte_map = None + if sch.out_of_range_label is None and string_map: + #byte_map = { key : string_map[key] for key in string_map.keys() } + + t = [np.fromstring(x, dtype=np.uint8) for x in string_map.keys()] + longest_key = len(max(t, key=len)) + + byte_map = np.zeros(longest_key * len(t) , dtype=np.uint8) + print('string_map', string_map) + print("longest_key", longest_key) + + start_pos = 0 + for x_id, x in enumerate(t): + for c_id, c in enumerate(x): + byte_map[start_pos + c_id] = c + start_pos += longest_key + + print(byte_map) + + + #for key in sorted(string_map.keys()): + # byte_map.append(np.fromstring(key, dtype=np.uint8)) + + #byte_map = [np.fromstring(key, dtype=np.uint8) for key in sorted(string_map.keys())] + #byte_map.sort() + + longest_keys.append(longest_key) + categorical_map_list.append(byte_map) + + new_fields[field_name] = writer + new_field_list.append(writer) + field_chunk_list.append(writer.chunk_factory(chunk_size)) + + column_ids, column_vals = file_read_line_fast_csv(source) + + print(f"CSV read {time.time() - time0}s") + + chunk_index = 0 + + key_to_search = np.fromstring('Urban city and twn', dtype=np.uint8) + #print("key to search") + #print(key_to_search) + + print(index_map) + for ith, i_c in enumerate(index_map): + chunk_index = 0 + + if show_progress_every: + if i_c % 1 == 0: + print(f"{i_c} cols parsed in {time.time() - time0}s") + + if early_filter is not None: + if not early_filter[1](row[early_key_index]): + continue + + if i_c == stop_after: + break + + categorical_map = None + if len(categorical_map_list) > ith: + categorical_map = categorical_map_list[ith] + + indices = column_ids[i_c] + values = column_vals[i_c] + + @njit + def findFirst_basic(a, b, div): + for i in range(0, len(a), div): + #i = i*longest_key + result = True + for j in range(len(b)): + result = result and (a[i+j] == b[j]) + if not result: + break + if result: + return i + return 0 + + @njit + def map_values(chunk, indices, cat_map, div): + #print(indices) + size = 0 + for row_ix in range(len(indices) - 1): + temp_val = values[indices[row_ix] : indices[row_ix+1]] + internal_val = findFirst_basic(categorical_map, temp_val, div) // div + chunk[row_ix] = internal_val + size += 1 + return size + + #print("i_c", i_c, categorical_map) + chunk = np.zeros(chunk_size, dtype=np.uint8) + + total = [] + + # NEED TO NOT WRITE THE WHOLE CHUNK.. as the counter shows too many 0! + + chunk_index = 0 + while chunk_index < len(indices): + size = map_values(chunk, indices[chunk_index:chunk_index+chunk_size], categorical_map, longest_keys[ith]) + + data = chunk[:size] + + new_field_list[ith].write_part(data) + total.extend(data) + + chunk_index += chunk_size + + print("idx", chunk_index) + + print("i_c", i_c, Counter(total)) + + if chunk_index != 0: + new_field_list[ith].write_part(chunk[:chunk_index]) + #total.extend(chunk[:chunk_index]) + + + for i_df in range(len(index_map)): + new_field_list[i_df].flush() + + + print(f"Total time {time.time() - time0}s") + #exit() + + def __ainit__(self, datastore, source, hf, space, schema, timestamp, + include=None, exclude=None, + keys=None, + stop_after=None, show_progress_every=None, filter_fn=None, + early_filter=None): + # self.names_ = list() + self.index_ = None + + #stop_after = 2000000 + + file_read_line_fast_csv(source) + + time0 = time.time() + + seen_ids = set() + + if space not in hf.keys(): + hf.create_group(space) + group = hf[space] + + with open(source) as sf: + csvf = csv.DictReader(sf, delimiter=',', quotechar='"') + + available_keys = [k.strip() for k in csvf.fieldnames if k.strip() in schema.fields] + if space in include and len(include[space]) > 0: + available_keys = include[space] + if space in exclude and len(exclude[space]) > 0: + available_keys = [k for k in available_keys if k not in exclude[space]] + + available_keys = ['ruc11cd','ruc11'] + + if not keys: + fields_to_use = available_keys + # index_map = [csvf.fieldnames.index(k) for k in fields_to_use] + # index_map = [i for i in range(len(fields_to_use))] + else: + for k in keys: + if k not in available_keys: + raise ValueError(f"key '{k}' isn't in the available keys ({keys})") + fields_to_use = keys + # index_map = [csvf.fieldnames.index(k) for k in fields_to_use] + + + csvf_fieldnames = [k.strip() for k in csvf.fieldnames] + index_map = [csvf_fieldnames.index(k) for k in fields_to_use] + + early_key_index = None + if early_filter is not None: + if early_filter[0] not in available_keys: + raise ValueError( + f"'early_filter': tuple element zero must be a key that is in the dataset") + early_key_index = available_keys.index(early_filter[0]) + + chunk_size = 1 << 20 + new_fields = dict() + new_field_list = list() + field_chunk_list = list() + categorical_map_list = list() + + # TODO: categorical writers should use the datatype specified in the schema + for i_n in range(len(fields_to_use)): + field_name = fields_to_use[i_n] + sch = schema.fields[field_name] + writer = sch.importer(datastore, group, field_name, timestamp) + # TODO: this list is required because we convert the categorical values to + # numerical values ahead of adding them. We could use importers that handle + # that transform internally instead + + string_map = sch.strings_to_values + if sch.out_of_range_label is None and string_map: + byte_map = { str.encode(key) : string_map[key] for key in string_map.keys() } + else: + byte_map = None + + categorical_map_list.append(byte_map) + + new_fields[field_name] = writer + new_field_list.append(writer) + field_chunk_list.append(writer.chunk_factory(chunk_size)) + + column_ids, column_vals = file_read_line_fast_csv(source) + + print(f"CSV read {time.time() - time0}s") + + chunk_index = 0 + + for ith, i_c in enumerate(index_map): + chunk_index = 0 + + col = column_ids[i_c] + + if show_progress_every: + if i_c % 1 == 0: + print(f"{i_c} cols parsed in {time.time() - time0}s") + + if early_filter is not None: + if not early_filter[1](row[early_key_index]): + continue + + if i_c == stop_after: + break + + categorical_map = None + if len(categorical_map_list) > ith: + categorical_map = categorical_map_list[ith] + + a = column_vals[i_c].copy() + + for row_ix in range(len(col) - 1): + val = a[col[row_ix] : col[row_ix+1]].tobytes() + + if categorical_map is not None: + if val not in categorical_map: + #print(i_c, row_ix) + error = "'{}' not valid: must be one of {} for field '{}'" + raise KeyError( + error.format(val, categorical_map, available_keys[i_c])) + val = categorical_map[val] + + field_chunk_list[ith][chunk_index] = val + + chunk_index += 1 + + if chunk_index == chunk_size: + new_field_list[ith].write_part(field_chunk_list[ith]) + + chunk_index = 0 + + #print(f"Total time {time.time() - time0}s") + + if chunk_index != 0: + for ith in range(len(index_map)): + new_field_list[ith].write_part(field_chunk_list[ith][:chunk_index]) + + for ith in range(len(index_map)): + new_field_list[ith].flush() + + print(f"Total time {time.time() - time0}s") + + + def __ainit__(self, datastore, source, hf, space, schema, timestamp, include=None, exclude=None, keys=None, stop_after=None, show_progress_every=None, filter_fn=None, @@ -144,6 +481,9 @@ def __init__(self, datastore, source, hf, space, schema, timestamp, if space in exclude and len(exclude[space]) > 0: available_keys = [k for k in available_keys if k not in exclude[space]] + available_keys = ['ruc11'] + available_keys = ['ruc11cd','ruc11'] + # available_keys = csvf.fieldnames if not keys: @@ -172,6 +512,7 @@ def __init__(self, datastore, source, hf, space, schema, timestamp, new_field_list = list() field_chunk_list = list() categorical_map_list = list() + # TODO: categorical writers should use the datatype specified in the schema for i_n in range(len(fields_to_use)): field_name = fields_to_use[i_n] @@ -191,6 +532,7 @@ def __init__(self, datastore, source, hf, space, schema, timestamp, chunk_index = 0 try: + total = [] for i_r, row in enumerate(ecsvf): if show_progress_every: if i_r % show_progress_every == 0: @@ -219,6 +561,8 @@ def __init__(self, datastore, source, hf, space, schema, timestamp, for i_df in range(len(index_map)): # with utils.Timer("writing to {}".format(self.names_[i_df])): # new_field_list[i_df].write_part(field_chunk_list[i_df]) + total.extend(field_chunk_list[i_df]) + new_field_list[i_df].write_part(field_chunk_list[i_df]) chunk_index = 0 @@ -230,9 +574,18 @@ def __init__(self, datastore, source, hf, space, schema, timestamp, if chunk_index != 0: for i_df in range(len(index_map)): new_field_list[i_df].write_part(field_chunk_list[i_df][:chunk_index]) + total.extend(field_chunk_list[i_df][:chunk_index]) + print("====") + print("i_df", i_df, Counter(total)) for i_df in range(len(index_map)): new_field_list[i_df].flush() print(f"{i_r} rows parsed in {time.time() - time0}s") + print(f"Total time {time.time() - time0}s") + +def get_cell(row, col, column_inds, column_vals): + start_row_index = column_inds[col][row] + end_row_index = column_inds[col][row+1] + return column_vals[col][start_row_index:end_row_index].tobytes() diff --git a/exetera/core/readerwriter.py b/exetera/core/readerwriter.py index cc729315..696b926c 100644 --- a/exetera/core/readerwriter.py +++ b/exetera/core/readerwriter.py @@ -248,7 +248,11 @@ def write_part(self, values): self.ever_written = True for s in values: - evalue = s.encode() + if isinstance(s, str): + evalue = s.encode() + else: + evalue = s + for v in evalue: self.values[self.value_index] = v self.value_index += 1 @@ -440,25 +444,24 @@ def write_part(self, values): validity = np.zeros(len(values), dtype='bool') for i in range(len(values)): valid, value = self.parser(values[i], self.invalid_value) - elements[i] = value validity[i] = valid if self.validation_mode == 'strict' and not valid: - if self._is_blank_str(values[i]): + if self._is_blank(values[i]): raise ValueError(f"Numeric value in the field '{self.field_name}' can not be empty in strict mode") else: raise ValueError(f"The following numeric value in the field '{self.field_name}' can not be parsed:{values[i].strip()}") - if self.validation_mode == 'allow_empty' and not self._is_blank_str(values[i]) and not valid: - raise ValueError(f"The following numeric value in the field '{self.field_name}' can not be parsed:{values[i].strip()}") + if self.validation_mode == 'allow_empty' and not self._is_blank(values[i]) and not valid: + raise ValueError(f"The following numeric value in the field '{self.field_name}' can not be parsed:{values[i]}") self.data_writer.write_part(elements) if self.flag_writer is not None: self.flag_writer.write_part(validity) - def _is_blank_str(self, value): - return type(value) == str and value.strip() == '' + def _is_blank(self, value): + return (isinstance(value, str) and value.strip() == '') or value == b'' def flush(self): self.data_writer.flush() From c788b967ec7abe227e84d81e49af3e053789c472 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Thu, 15 Apr 2021 13:07:35 +0100 Subject: [PATCH 054/181] Adding in of pseudocode version of fast categorical lookup --- exetera/core/csv_reader_speedup.py | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py index 2815705d..7fc1c044 100644 --- a/exetera/core/csv_reader_speedup.py +++ b/exetera/core/csv_reader_speedup.py @@ -240,5 +240,57 @@ def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separ if index == len(source): break + +""" +original categories: +"one", "two", "three", "four", "five" +0 , 1 , 2 , 3 , 4 + +sorted categories +"five", "four", "one", "three", "two" + +sorted category map +4, 3, 0, 2, 1 + +lengths of sorted categories +4, 4, 3, 5, 3 + +sorted category indexed string + +scindex = [0, 4, 8, 11, 16, 19] +scvalues = [fivefouronethreetwo] + +col_inds = value_index[col_index,...] + +def my_fast_categorical_mapper(...): + for e in range(len(rows_read)-1): + key_start = value_inds[col_index, e] + key_end = value_inds[col_index, e+1] + key_len = key_end - key_start + + for i in range(1, len(scindex)): + skeylen = scindex[i] - scindex[i - 1] + if skeylen == len(key): + index = i + for j in range(keylen): + entry_start = scindex[i-1] + if value_inds[col_index, key_start + j] != scvalues[entry_start + j]: + index = -1 + break + + if index != -1: + destination_vals[e] = index + + + + + + + + + + +""" + if __name__ == "__main__": main() From 60f2ba92547be8dbe7803c4fbc92c1c32e510d1d Mon Sep 17 00:00:00 2001 From: clyyuanzi-london <59363720+clyyuanzi-london@users.noreply.github.com> Date: Thu, 15 Apr 2021 13:22:32 +0100 Subject: [PATCH 055/181] clean up the comments --- exetera/core/csv_reader_speedup.py | 37 +++++------------------------- exetera/core/importer.py | 1 - 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py index 2815705d..750a8e9f 100644 --- a/exetera/core/csv_reader_speedup.py +++ b/exetera/core/csv_reader_speedup.py @@ -46,30 +46,10 @@ def main(): {'name': 'g', 'type': 'cat', 'vals': ('', '', '', '', 'True', 'False')}, {'name': 'h', 'type': 'cat', 'vals': ('', '', '', 'No', 'Yes')}] # make_test_data(100000, col_dicts) - #source = '/Users/lc21/Documents/KCL_BMEIS/ExeTera/resources/assessment_input_small_data.csv' - - source = '/Users/frecar/Desktop/ExeTera/resources/assessment_input_sample_data.csv' - - # print(source) - # # run once first - # orig_inds = [] - # orig_vals = [] - # for i in range(len(col_dicts)+1): - # orig_inds.append(np.zeros(1000000, dtype=np.int64)) - # orig_vals.append(np.zeros(10000000, dtype='|S1')) - # original_csv_read(source, orig_inds, orig_vals) - # del orig_inds - # del orig_vals - - # orig_inds = [] - # orig_vals = [] - # for i in range(len(col_dicts)+1): - # orig_inds.append(np.zeros(1000000, dtype=np.int64)) - # orig_vals.append(np.zeros(10000000, dtype='|S1')) + source = 'resources/assessment_input_small_data.csv' + with Timer("Original csv reader took:"): original_csv_read(source) - # del orig_inds - # del orig_vals file_read_line_fast_csv(source) @@ -106,22 +86,16 @@ def file_read_line_fast_csv(source): column_inds = np.zeros((count_columns, count_rows), dtype=np.int64) column_vals = np.zeros((count_columns, count_rows * 25), dtype=np.uint8) - # separator = np.frombuffer(b',', dtype='S1')[0][0] - # delimiter = np.frombuffer(b'"', dtype='S1')[0][0] + ESCAPE_VALUE = np.frombuffer(b'"', dtype='S1')[0][0] SEPARATOR_VALUE = np.frombuffer(b',', dtype='S1')[0][0] NEWLINE_VALUE = np.frombuffer(b'\n', dtype='S1')[0][0] - # ESCAPE_VALUE = b'"' - # SEPARATOR_VALUE = b',' - # NEWLINE_VALUE = b'\n' + with Timer("my_fast_csv_reader_int"): - #source = '/Users/lc21/Documents/KCL_BMEIS/ExeTera/resources/assessment_input_small_data.csv' - #source = '/Users/frecar/Desktop/ExeTera/resources/assessment_input_small_data.csv' + content = np.fromfile(source, dtype=np.uint8) my_fast_csv_reader_int(content, column_inds, column_vals, ESCAPE_VALUE, SEPARATOR_VALUE, NEWLINE_VALUE) - #a = get_cell(0,1,column_inds, column_vals) - return column_inds, column_vals @@ -229,6 +203,7 @@ def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separ if end_line: row_index += 1 col_index = 0 + else: col_index += 1 diff --git a/exetera/core/importer.py b/exetera/core/importer.py index 940ba11e..bdc0de07 100644 --- a/exetera/core/importer.py +++ b/exetera/core/importer.py @@ -576,7 +576,6 @@ def __ainit__(self, datastore, source, hf, space, schema, timestamp, new_field_list[i_df].write_part(field_chunk_list[i_df][:chunk_index]) total.extend(field_chunk_list[i_df][:chunk_index]) - print("====") print("i_df", i_df, Counter(total)) for i_df in range(len(index_map)): new_field_list[i_df].flush() From c341eb2111c5144efba1d01861ce7417269cef13 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 16 Apr 2021 10:09:25 +0100 Subject: [PATCH 056/181] docstrings for dataframe --- exetera/core/dataframe.py | 144 ++++++++++++++++++++++++++++++-------- 1 file changed, 115 insertions(+), 29 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 8ba2a6f4..416687f3 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -46,7 +46,8 @@ def __init__(self, @property def columns(self): """ - The columns property interface. + The columns property interface. Columns is a dictionary to store the fields by (field_name, field_object). + The field_name is field.name without prefix '/' and HDF5 group name. """ return dict(self._columns) @@ -60,22 +61,24 @@ def dataset(self): @property def h5group(self): """ - The h5group property interface. + The h5group property interface, used to handle underlying storage. """ return self._h5group - def add(self, field, name=None): + def add(self, field): """ - Add a field to this dataframe. + Add a field to this dataframe as well as the HDF5 Group. + :param field: field to add to this dataframe, copy the underlying dataset """ - if name is not None: - if not isinstance(name, str): - raise TypeError("The name must be a str object.") - else: - self._columns[name] = field - # note the name has '/' for hdf5 object - self._columns[field.name[field.name.index('/', 1)+1:]] = field + dname = field.name[field.name.index('/', 1)+1:] + nfield = field.create_like(self, dname) + if field.indexed: + nfield.indices.write(field.indices[:]) + nfield.values.write(field.values[:]) + else: + nfield.data.write(field.data[:]) + self._columns[dname] = nfield def create_group(self, name): """ @@ -89,6 +92,9 @@ def create_group(self, name): return self._h5group[name] def create_numeric(self, name, nformat, timestamp=None, chunksize=None): + """ + Create a numeric type field. + """ fld.numeric_field_constructor(self._dataset.session, self, name, nformat, timestamp, chunksize) field = fld.NumericField(self._dataset.session, self._h5group[name], write_enabled=True) @@ -96,6 +102,9 @@ def create_numeric(self, name, nformat, timestamp=None, chunksize=None): return self._columns[name] def create_indexed_string(self, name, timestamp=None, chunksize=None): + """ + Create a indexed string type field. + """ fld.indexed_string_field_constructor(self._dataset.session, self, name, timestamp, chunksize) field = fld.IndexedStringField(self._dataset.session, self._h5group[name], write_enabled=True) @@ -103,6 +112,9 @@ def create_indexed_string(self, name, timestamp=None, chunksize=None): return self._columns[name] def create_fixed_string(self, name, length, timestamp=None, chunksize=None): + """ + Create a fixed string type field. + """ fld.fixed_string_field_constructor(self._dataset.session, self, name, length, timestamp, chunksize) field = fld.FixedStringField(self._dataset.session, self._h5group[name], write_enabled=True) @@ -110,6 +122,9 @@ def create_fixed_string(self, name, length, timestamp=None, chunksize=None): return self._columns[name] def create_categorical(self, name, nformat, key, timestamp=None, chunksize=None): + """ + Create a categorical type field. + """ fld.categorical_field_constructor(self._dataset.session, self, name, nformat, key, timestamp, chunksize) field = fld.CategoricalField(self._dataset.session, self._h5group[name], @@ -118,6 +133,9 @@ def create_categorical(self, name, nformat, key, timestamp=None, chunksize=None) return self._columns[name] def create_timestamp(self, name, timestamp=None, chunksize=None): + """ + Create a timestamp type field. + """ fld.timestamp_field_constructor(self._dataset.session, self, name, timestamp, chunksize) field = fld.TimestampField(self._dataset.session, self._h5group[name], write_enabled=True) @@ -127,7 +145,8 @@ def create_timestamp(self, name, timestamp=None, chunksize=None): def __contains__(self, name): """ check if dataframe contains a field, by the field name - name: the name of the field to check,return a bool + + :param name: the name of the field to check,return a bool """ if not isinstance(name, str): raise TypeError("The name must be a str object.") @@ -137,7 +156,8 @@ def __contains__(self, name): def contains_field(self, field): """ check if dataframe contains a field by the field object - field: the filed object to check, return a tuple(bool,str). The str is the name stored in dataframe. + + :param field: the filed object to check, return a tuple(bool,str). The str is the name stored in dataframe. """ if not isinstance(field, fld.Field): raise TypeError("The field must be a Field object") @@ -148,6 +168,11 @@ def contains_field(self, field): return False def __getitem__(self, name): + """ + Get a field stored by the field name. + + :param name: The name of field to get. + """ if not isinstance(name, str): raise TypeError("The name must be a str object.") elif not self.__contains__(name): @@ -156,40 +181,51 @@ def __getitem__(self, name): return self._columns[name] def get_field(self, name): - return self.__getitem__(name) - - def get_name(self, field): """ - Get the name of the field in dataframe. + Get a field stored by the field name. + + :param name: The name of field to get. """ - if not isinstance(field, fld.Field): - raise TypeError("The field argument must be a Field object.") - for name, v in self._columns.items(): - if id(field) == id(v): - return name - return None + return self.__getitem__(name) + + # def get_name(self, field): + # """ + # Get the name of the field in dataframe. + # """ + # if not isinstance(field, fld.Field): + # raise TypeError("The field argument must be a Field object.") + # for name, v in self._columns.items(): + # if id(field) == id(v): + # return name + # return None def __setitem__(self, name, field): if not isinstance(name, str): raise TypeError("The name must be a str object.") - elif not isinstance(field, fld.Field): + if not isinstance(field, fld.Field): raise TypeError("The field must be a Field object.") + nfield = field.create_like(self, name) + if field.indexed: + nfield.indices.write(field.indices[:]) + nfield.values.write(field.values[:]) else: - self._columns[name] = field - return True + nfield.data.write(field.data[:]) + self._columns[name] = nfield def __delitem__(self, name): if not self.__contains__(name=name): raise ValueError("This dataframe does not contain the name to delete.") else: + del self._h5group[name] del self._columns[name] - return True def delete_field(self, field): """ - Remove field from dataframe by field + Remove field from dataframe by field. + + :param field: The field to delete from this dataframe. """ - name = self.get_name(field) + name = field.name[field.name.index('/', 1)+1:] if name is None: raise ValueError("This dataframe does not contain the field to delete.") else: @@ -216,6 +252,8 @@ def __len__(self): def get_spans(self): """ Return the name and spans of each field as a dictionary. + + :returns: A dictionary of (field_name, field_spans). """ spans = {} for name, field in self._columns.items(): @@ -262,3 +300,51 @@ def apply_index(self, index_to_apply, ddf=None): for field in self._columns.values(): field.apply_index(index_to_apply) return self + + @staticmethod + def copy(field: fld.Field, dataframe: DataFrame, name: str): + """ + Copy a field to another dataframe as well as underlying dataset. + + :param field: The source field to copy. + :param dataframe: The destination dataframe to copy to. + :param name: The name of field under destination dataframe. + """ + dfield = field.create_like(dataframe, name) + if field.indexed: + dfield.indices.write(field.indices[:]) + dfield.values.write(field.values[:]) + else: + dfield.data.write(field.data[:]) + dataframe.columns[name] = dfield + + @staticmethod + def drop(dataframe: DataFrame, field: fld.Field): + """ + Drop a field from a dataframe. + + :param dataframe: The dataframe where field is located. + :param field: The field to delete. + """ + dataframe.delete_field(field) + + + + @staticmethod + def move(src_df: DataFrame, field: fld.Field, dest_df: DataFrame, name: str): + """ + Move a field to another dataframe as well as underlying dataset. + + :param src_df: The source dataframe where the field is located. + :param field: The field to move. + :param dest_df: The destination dataframe to move to. + :param name: The name of field under destination dataframe. + """ + dfield = field.create_like(dest_df, name) + if field.indexed: + dfield.indices.write(field.indices[:]) + dfield.values.write(field.values[:]) + else: + dfield.data.write(field.data[:]) + dest_df.columns[name] = dfield + src_df.delete_field(field) From b23f1d8891ada67efd014fa5ab50dd3b8abdbd5e Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Fri, 16 Apr 2021 12:13:35 +0100 Subject: [PATCH 057/181] Major reworking of apply_filter / apply_index for fields; they shouldn't destructively change self by default. Also addition of further mem versions of fields and factoring out of common functionality. Fix to field when indices / values are cleared but this leaves data pointing to the old field --- exetera/core/dataframe.py | 4 +- exetera/core/fields.py | 843 +++++++++++++++++++++++++------------- exetera/core/session.py | 30 +- tests/test_dataframe.py | 2 +- tests/test_fields.py | 238 ++++++++++- 5 files changed, 794 insertions(+), 323 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 0b13ce8e..6bdb3bcc 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -215,11 +215,11 @@ def apply_filter(self, filter_to_apply, ddf=None): raise TypeError("The destination object must be an instance of DataFrame.") for name, field in self._columns.items(): newfld = field.create_like(ddf, field.name[field.name.index('/', 1)+1:]) - ddf.add(field.apply_filter(filter_to_apply, dstfld=newfld), name=name) + ddf.add(field.apply_filter_to_indexed_field(filter_to_apply, dstfld=newfld), name=name) return ddf else: for field in self._columns.values(): - field.apply_filter(filter_to_apply) + field.apply_filter_to_indexed_field(filter_to_apply) return self def apply_index(self, index_to_apply, ddf=None): diff --git a/exetera/core/fields.py b/exetera/core/fields.py index f7ee08bf..69960b14 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -188,19 +188,19 @@ def complete(self): class MemoryFieldArray: - def __init__(self, field, dtype): - self._field = field + def __init__(self, dtype): self._dtype = dtype self._dataset = None def __len__(self): - return len(self._dataset) - + return 0 if self._dataset is None else len(self._dataset) @property def dtype(self): - return self._dataset.dtype + return self._dtype def __getitem__(self, item): + if self._dataset is None: + raise ValueError("Cannot get data from an empty Field") return self._dataset[item] def __setitem__(self, key, value): @@ -210,11 +210,18 @@ def clear(self): self._dataset = None def write_part(self, part, move_mem=False): + if not isinstance(part, np.ndarray): + raise ValueError("'part' must be a numpy array but is '{}'".format(type(part))) if self._dataset is None: - self._dataset = part[:] + if move_mem is True and dtype_to_str(part.dtype) == self._dtype: + self._dataset = part + else: + self._dataset = part.copy() else: - self._dataset.resize(len(self._dataset) + len(part)) - self._dataset[-len(part):] = part + new_dataset = np.zeros(len(self._dataset) + len(part), dtype=self._dataset.dtype) + new_dataset[:len(self._dataset)] = self._dataset + new_dataset[-len(part):] = part + self._dataset = new_dataset def write(self, part): self.write_part(part) @@ -225,27 +232,25 @@ def complete(self): class ReadOnlyIndexedFieldArray: - def __init__(self, field, index_name, values_name): + def __init__(self, field, indices, values): self._field = field - self._index_name = index_name - self._index_dataset = field[index_name] - self._values_name = values_name - self._values_dataset = field[values_name] + self._indices = indices + self._values = values def __len__(self): # TODO: this occurs because of the initialized state of an indexed string. It would be better for the # index to be initialised as [0] - return max(len(self._index_dataset)-1, 0) + return max(len(self._indices)-1, 0) def __getitem__(self, item): try: if isinstance(item, slice): start = item.start if item.start is not None else 0 - stop = item.stop if item.stop is not None else len(self._index_dataset) - 1 + stop = item.stop if item.stop is not None else len(self._indices) - 1 step = item.step #TODO: validate slice - index = self._index_dataset[start:stop+1] - bytestr = self._values_dataset[index[0]:index[-1]] + index = self._indices[start:stop+1] + bytestr = self._values[index[0]:index[-1]] results = [None] * (len(index)-1) startindex = start for ir in range(len(results)): @@ -254,12 +259,12 @@ def __getitem__(self, item): index[ir+1]-np.int64(startindex)].tobytes().decode() return results elif isinstance(item, int): - if item >= len(self._index_dataset) - 1: + if item >= len(self._indices) - 1: raise ValueError("index is out of range") - start, stop = self._index_dataset[item:item + 2] + start, stop = self._indices[item:item+2] if start == stop: return '' - value = self._values_dataset[start:stop].tobytes().decode() + value = self._values[start:stop].tobytes().decode() return value except Exception as e: print("{}: unexpected exception {}".format(self._field.name, e)) @@ -287,31 +292,31 @@ def complete(self): class WriteableIndexedFieldArray: - def __init__(self, field, index_name, values_name): - self._field = field - self._index_name = index_name - self._index_dataset = field[index_name] - self._values_name = values_name - self._values_dataset = field[values_name] - self._chunksize = self._field.attrs['chunksize'] + def __init__(self, chunksize, indices, values): + # self._field = field + self._indices = indices + self._values = values + # self._chunksize = self._field.attrs['chunksize'] + self._chunksize = chunksize self._raw_values = np.zeros(self._chunksize, dtype=np.uint8) self._raw_indices = np.zeros(self._chunksize, dtype=np.int64) - self._accumulated = self._index_dataset[-1] if len(self._index_dataset) else 0 + self._accumulated = self._indices[-1] if len(self._indices) > 0 else 0 self._index_index = 0 self._value_index = 0 def __len__(self): - return len(self._index_dataset) - 1 + return max(len(self._indices) - 1, 0) def __getitem__(self, item): try: if isinstance(item, slice): start = item.start if item.start is not None else 0 - stop = item.stop if item.stop is not None else len(self._index_dataset) - 1 + stop = item.stop if item.stop is not None else len(self._indices) - 1 step = item.step # TODO: validate slice - index = self._index_dataset[start:stop + 1] - bytestr = self._values_dataset[index[0]:index[-1]] + + index = self._indices[start:stop+1] + bytestr = self._values[index[0]:index[-1]] results = [None] * (len(index) - 1) startindex = start rmax = min(len(results), stop - start) @@ -322,15 +327,15 @@ def __getitem__(self, item): results[ir] = rstr return results elif isinstance(item, int): - if item >= len(self._index_dataset) - 1: + if item >= len(self._indices) - 1: raise ValueError("index is out of range") - start, stop = self._index_dataset[item:item + 2] + start, stop = self._indices[item:item+2] if start == stop: return '' - value = self._values_dataset[start:stop].tobytes().decode() + value = self._values[start:stop].tobytes().decode() return value except Exception as e: - print("{}: unexpected exception {}".format(self._field.name, e)) + print(e) raise def __setitem__(self, key, value): @@ -339,15 +344,10 @@ def __setitem__(self, key, value): def clear(self): self._accumulated = 0 - DataWriter.clear_dataset(self._field, self._index_name) - DataWriter.clear_dataset(self._field, self._values_name) - DataWriter.write(self._field, self._index_name, [], 0, 'int64') - DataWriter.write(self._field, self._values_name, [], 0, 'uint8') - self._index_dataset = self._field[self._index_name] - self._values_dataset = self._field[self._values_name] + self._indices.clear() + self._values.clear() self._accumulated = 0 - def write_part(self, part): for s in part: evalue = s.encode() @@ -355,34 +355,29 @@ def write_part(self, part): self._raw_values[self._value_index] = v self._value_index += 1 if self._value_index == self._chunksize: - DataWriter.write(self._field, self._values_name, - self._raw_values, self._value_index) + self._values.write_part(self._raw_values[:self._value_index]) self._value_index = 0 self._accumulated += 1 self._raw_indices[self._index_index] = self._accumulated self._index_index += 1 if self._index_index == self._chunksize: - if len(self._field['index']) == 0: - DataWriter.write(self._field, self._index_name, [0], 1) - DataWriter.write(self._field, self._index_name, - self._raw_indices, self._index_index) + if len(self._indices) == 0: + self._indices.write_part(np.array([0])) + self._indices.write_part(self._raw_indices[:self._index_index]) self._index_index = 0 - def write(self, part): self.write_part(part) self.complete() def complete(self): if self._value_index != 0: - DataWriter.write(self._field, self._values_name, - self._raw_values, self._value_index) + self._values.write(self._raw_values[:self._value_index]) self._value_index = 0 if self._index_index != 0: - if len(self._field['index']) == 0: - DataWriter.write(self._field, self._index_name, [0], 1) - DataWriter.write(self._field, self._index_name, - self._raw_indices, self._index_index) + if len(self._indices) == 0: + self._indices.write_part(np.array([0])) + self._indices.write(self._raw_indices[:self._index_index]) self._index_index = 0 @@ -390,28 +385,115 @@ def complete(self): # =================== +class IndexedStringMemField(MemoryField): + def __init__(self, session, chunksize=None): + super().__init__(session) + self._session = session + self._chunksize = session.chunksize if chunksize is None else chunksize + self._data_wrapper = None + self._index_wrapper = None + self._value_wrapper = None + self._write_enabled = True + + def writeable(self): + return self + + def create_like(self, group, name, timestamp=None): + return FieldDataOps.indexed_string_create_like(group, name, timestamp) + + @property + def indexed(self): + return True + + @property + def data(self): + if self._data_wrapper is None: + self._data_wrapper = WriteableIndexedFieldArray(self._chunksize, self.indices, self.values) + return self._data_wrapper + + def is_sorted(self): + if len(self) < 2: + return True + + indices = self.indices[:] + values = self.values[:] + last = values[indices[0]:indices[1]].tobytes() + for i in range(1, len(indices)-1): + cur = values[indices[i]:indices[i+1]].tobytes() + if last > cur: + return False + last = cur + return True + + @property + def indices(self): + if self._index_wrapper is None: + self._index_wrapper = MemoryFieldArray('int64') + return self._index_wrapper + + @property + def values(self): + if self._value_wrapper is None: + self._value_wrapper = MemoryFieldArray('int8') + return self._value_wrapper + + def __len__(self): + return len(self.data) + + def get_spans(self): + return ops._get_spans_for_index_string_field(self.indices[:], self.values[:]) + + def apply_filter(self, filter_to_apply, target=None, in_place=False): + """ + Apply a boolean filter to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the filtered data is written to. + + :param: filter_to_apply: a Field or numpy array that contains the boolean filter data + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The filtered field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_filter_to_indexed_field(self, filter_to_apply, target, in_place) + + def apply_index(self, index_to_apply, target=None, in_place=False): + """ + Apply an index to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the reindexed data is written to. + + :param: index_to_apply: a Field or numpy array that contains the indices + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The reindexed field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_index_to_indexed_field(self, index_to_apply, target, in_place) + + class NumericMemField(MemoryField): def __init__(self, session, nformat): super().__init__(session) self._nformat = nformat + # TODO: this is a hack, we should have a property on the interface of Field + self._write_enabled = True def writeable(self): return self def create_like(self, group, name, timestamp=None): - ts = timestamp - nformat = self._nformat - if isinstance(group, h5py.Group): - numeric_field_constructor(self._session, group, name, nformat, ts, self.chunksize) - return NumericField(self._session, group[name], write_enabled=True) - else: - return group.create_numeric(name, nformat, ts, self.chunksize) + return FieldDataOps.numeric_field_create_like(self, group, name, timestamp) @property def data(self): if self._value_wrapper is None: - self._value_wrapper = MemoryFieldArray(self, self._nformat) + self._value_wrapper = MemoryFieldArray(self._nformat) return self._value_wrapper def is_sorted(self): @@ -426,29 +508,37 @@ def __len__(self): def get_spans(self): return ops.get_spans_for_field(self.data[:]) - def apply_filter(self, filter_to_apply, dstfld=None): - result = self.data[filter_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld + def apply_filter(self, filter_to_apply, target=None, in_place=False): + """ + Apply a boolean filter to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the filtered data is written to. + + :param: filter_to_apply: a Field or numpy array that contains the boolean filter data + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The filtered field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_filter_to_field(self, filter_to_apply, target, in_place) - def apply_index(self, index_to_apply, dstfld=None): - result = self.data[index_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld + def apply_index(self, index_to_apply, target=None, in_place=False): + """ + Apply an index to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the reindexed data is written to. + + :param: index_to_apply: a Field or numpy array that contains the indices + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The reindexed field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) def __add__(self, second): return FieldDataOps.numeric_add(self._session, self, second) @@ -534,6 +624,7 @@ def __init__(self, session, nformat, keys): super().__init__(session) self._nformat = nformat self._keys = keys + self._write_enabled = True def writeable(self): return self @@ -552,7 +643,7 @@ def create_like(self, group, name, timestamp=None): @property def data(self): if self._value_wrapper is None: - self._value_wrapper = MemoryFieldArray(self, self._nformat) + self._value_wrapper = MemoryFieldArray(self._nformat) return self._value_wrapper def is_sorted(self): @@ -584,29 +675,37 @@ def remap(self, key_map, new_key): result.data.write(values) return result - def apply_filter(self, filter_to_apply, dstfld=None): - result = self.data[filter_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld + def apply_filter(self, filter_to_apply, target=None, in_place=False): + """ + Apply a boolean filter to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the filtered data is written to. + + :param: filter_to_apply: a Field or numpy array that contains the boolean filter data + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The filtered field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_filter_to_field(self, filter_to_apply, target, in_place) - def apply_index(self, index_to_apply, dstfld=None): - result = self.data[index_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld + def apply_index(self, index_to_apply, target=None, in_place=False): + """ + Apply an index to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the reindexed data is written to. + + :param: index_to_apply: a Field or numpy array that contains the indices + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The reindexed field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) def __lt__(self, value): return FieldDataOps.less_than(self._session, self, value) @@ -701,12 +800,7 @@ def writeable(self): return IndexedStringField(self._session, self._field, write_enabled=True) def create_like(self, group, name, timestamp=None): - ts = self.timestamp if timestamp is None else timestamp - if isinstance(group, h5py.Group): - indexed_string_field_constructor(self._session, group, name, ts, self.chunksize) - return IndexedStringField(self._session, group[name], write_enabled=True) - else: - return group.create_indexed_string(name, ts, self.chunksize) + return FieldDataOps.indexed_string_create_like(self, group, name, timestamp) @property def indexed(self): @@ -715,9 +809,10 @@ def indexed(self): @property def data(self): if self._data_wrapper is None: + wrapper =\ WriteableIndexedFieldArray if self._write_enabled else ReadOnlyIndexedFieldArray - self._data_wrapper = wrapper(self._field, 'index', 'values') + self._data_wrapper = wrapper(self.chunksize, self.indices, self.values) return self._data_wrapper def is_sorted(self): @@ -754,50 +849,38 @@ def __len__(self): def get_spans(self): return ops._get_spans_for_index_string_field(self.indices[:], self.values[:]) - def apply_filter(self, filter_to_apply, dstfld=None): + def apply_filter(self, filter_to_apply, target=None, in_place=False): """ - Apply a filter (array of boolean) to the field, return itself if destination field (detfld) is not set. + Apply a boolean filter to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the filtered data is written to. + + :param: filter_to_apply: a Field or numpy array that contains the boolean filter data + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The filtered field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. """ - dest_indices, dest_values = \ - ops.apply_filter_to_index_values(filter_to_apply, - self.indices[:], self.values[:]) + return FieldDataOps.apply_filter_to_indexed_field(self, filter_to_apply, target, in_place) - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.indices) == len(dest_indices): - dstfld.indices[:] = dest_indices - else: - dstfld.indices.clear() - dstfld.indices.write(dest_indices) - if len(dstfld.values) == len(dest_values): - dstfld.values[:] = dest_values - else: - dstfld.values.clear() - dstfld.values.write(dest_values) - return dstfld - def apply_index(self,index_to_apply,dstfld=None): + def apply_index(self, index_to_apply, target=None, in_place=False): """ - Reindex the current field, return itself if destination field is not set. + Apply an index to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the reindexed data is written to. + + :param: index_to_apply: a Field or numpy array that contains the indices + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The reindexed field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. """ - dest_indices, dest_values = \ - ops.apply_indices_to_index_values(index_to_apply, - self.indices[:], self.values[:]) - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.indices) == len(dest_indices): - dstfld.indices[:] = dest_indices - else: - dstfld.indices.clear() - dstfld.indices.write(dest_indices) - if len(dstfld.values) == len(dest_values): - dstfld.values[:] = dest_values - else: - dstfld.values.clear() - dstfld.values.write(dest_values) - return dstfld + return FieldDataOps.apply_index_to_indexed_field(self, index_to_apply, target, in_place) class FixedStringField(HDF5Field): @@ -837,49 +920,50 @@ def __len__(self): def get_spans(self): return ops.get_spans_for_field(self.data[:]) - def apply_filter(self, filter_to_apply, dstfld=None): - array = self.data[:] - result = array[filter_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld + def apply_filter(self, filter_to_apply, target=None, in_place=False): + """ + Apply a boolean filter to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the filtered data is written to. + + :param: filter_to_apply: a Field or numpy array that contains the boolean filter data + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The filtered field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_filter_to_field(self, filter_to_apply, target, in_place) + def apply_index(self, index_to_apply, target=None, in_place=False): + """ + Apply an index to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the reindexed data is written to. + + :param: index_to_apply: a Field or numpy array that contains the indices + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The reindexed field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) - def apply_index(self, index_to_apply, dstfld=None): - array = self.data[:] - result = array[index_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld class NumericField(HDF5Field): def __init__(self, session, group, name=None, mem_only=True, write_enabled=False): super().__init__(session, group, name=name, write_enabled=write_enabled) + self._nformat = self._field.attrs['nformat'] def writeable(self): return NumericField(self._session, self._field, write_enabled=True) def create_like(self, group, name, timestamp=None): - ts = self.timestamp if timestamp is None else timestamp - nformat = self._field.attrs['nformat'] - if isinstance(group, h5py.Group): - numeric_field_constructor(self._session, group, name, nformat, ts, self.chunksize) - return NumericField(self._session, group[name], write_enabled=True) - else: - return group.create_numeric(name, nformat, ts, self.chunksize) + return FieldDataOps.numeric_field_create_like(self, group, name, timestamp) @property def data(self): @@ -902,31 +986,37 @@ def __len__(self): def get_spans(self): return ops.get_spans_for_field(self.data[:]) - def apply_filter(self, filter_to_apply, dstfld=None): - array = self.data[:] - result = array[filter_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld + def apply_filter(self, filter_to_apply, target=None, in_place=False): + """ + Apply a boolean filter to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the filtered data is written to. + + :param: filter_to_apply: a Field or numpy array that contains the boolean filter data + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The filtered field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_filter_to_field(self, filter_to_apply, target, in_place) - def apply_index(self, index_to_apply, dstfld=None): - array = self.data[:] - result = array[index_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld + def apply_index(self, index_to_apply, target=None, in_place=False): + """ + Apply an index to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the reindexed data is written to. + + :param: index_to_apply: a Field or numpy array that contains the indices + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The reindexed field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) def __add__(self, second): return FieldDataOps.numeric_add(self._session, self, second) @@ -1069,18 +1159,21 @@ def remap(self, key_map, new_key): result.data.write(values) return result - def apply_filter(self, filter_to_apply, dstfld=None): - array = self.data[:] - result = array[filter_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld + def apply_filter(self, filter_to_apply, target=None, in_place=False): + """ + Apply a boolean filter to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the filtered data is written to. + + :param: filter_to_apply: a Field or numpy array that contains the boolean filter data + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The filtered field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_filter_to_field(self, filter_to_apply, target, in_place) def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] @@ -1150,18 +1243,21 @@ def __len__(self): def get_spans(self): return ops.get_spans_for_field(self.data[:]) - def apply_filter(self, filter_to_apply, dstfld=None): - array = self.data[:] - result = array[filter_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld + def apply_filter(self, filter_to_apply, target=None, in_place=False): + """ + Apply a boolean filter to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the filtered data is written to. + + :param: filter_to_apply: a Field or numpy array that contains the boolean filter data + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The filtered field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_filter_to_field(self, filter_to_apply, target, in_place) def apply_index(self, index_to_apply, dstfld=None): array = self.data[:] @@ -1455,6 +1551,7 @@ def write(self, values): # Operation implementations +# ========================= def as_field(data, key=None): @@ -1469,51 +1566,49 @@ def as_field(data, key=None): raise NotImplementedError() -class FieldDataOps: +def argsort(field: Field, + dtype: str=None): + supported_dtypes = ('int32', 'int64', 'uint32') + if dtype not in supported_dtypes: + raise ValueError("If set, 'dtype' must be one of {}".format(supported_dtypes)) + indices = np.argsort(field.data[:]) + + f = NumericMemField(None, dtype_to_str(indices.dtype) if dtype is None else dtype) + f.data.write(indices) + return f + + +def dtype_to_str(dtype): + if isinstance(dtype, str): + return dtype + + if dtype == bool: + return 'bool' + elif dtype == np.int8: + return 'int8' + elif dtype == np.int16: + return 'int16' + elif dtype == np.int32: + return 'int32' + elif dtype == np.int64: + return 'int64' + elif dtype == np.uint8: + return 'uint8' + elif dtype == np.uint16: + return 'uint16' + elif dtype == np.uint32: + return 'uint32' + elif dtype == np.uint64: + return 'uint64' + elif dtype == np.float32: + return 'float32' + elif dtype == np.float64: + return 'float64' + + raise ValueError("Unsupported dtype '{}'".format(dtype)) - type_converters = { - bool: 'bool', - np.int8: 'int8', - np.int16: 'int16', - np.int32: 'int32', - np.int64: 'int64', - np.uint8: 'uint8', - np.uint16: 'uint16', - np.uint32: 'uint32', - np.uint64: 'uint64', - np.float32: 'float32', - np.float64: 'float64' - } - @classmethod - def dtype_to_str(cls, dtype): - if isinstance(dtype, str): - return dtype - - if dtype == bool: - return 'bool' - elif dtype == np.int8: - return 'int8' - elif dtype == np.int16: - return 'int16' - elif dtype == np.int32: - return 'int32' - elif dtype == np.int64: - return 'int64' - elif dtype == np.uint8: - return 'uint8' - elif dtype == np.uint16: - return 'uint16' - elif dtype == np.uint32: - return 'uint32' - elif dtype == np.uint64: - return 'uint64' - elif dtype == np.float32: - return 'float32' - elif dtype == np.float64: - return 'float64' - - raise ValueError("Unsupported dtype '{}'".format(dtype)) +class FieldDataOps: @classmethod def numeric_add(cls, session, first, second): @@ -1528,7 +1623,7 @@ def numeric_add(cls, session, first, second): second_data = second r = first_data + second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1545,7 +1640,7 @@ def numeric_sub(cls, session, first, second): second_data = second r = first_data - second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1562,7 +1657,7 @@ def numeric_mul(cls, session, first, second): second_data = second r = first_data * second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1579,7 +1674,7 @@ def numeric_truediv(cls, session, first, second): second_data = second r = first_data / second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1596,7 +1691,7 @@ def numeric_floordiv(cls, session, first, second): second_data = second r = first_data // second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1613,7 +1708,7 @@ def numeric_mod(cls, session, first, second): second_data = second r = first_data % second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1630,9 +1725,9 @@ def numeric_divmod(cls, session, first, second): second_data = second r1, r2 = np.divmod(first_data, second_data) - f1 = NumericMemField(session, cls.dtype_to_str(r1.dtype)) + f1 = NumericMemField(session, dtype_to_str(r1.dtype)) f1.data.write(r1) - f2 = NumericMemField(session, cls.dtype_to_str(r2.dtype)) + f2 = NumericMemField(session, dtype_to_str(r2.dtype)) f2.data.write(r2) return f1, f2 @@ -1649,7 +1744,7 @@ def numeric_and(cls, session, first, second): second_data = second r = first_data & second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1666,7 +1761,7 @@ def numeric_xor(cls, session, first, second): second_data = second r = first_data ^ second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1683,7 +1778,7 @@ def numeric_or(cls, session, first, second): second_data = second r = first_data | second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1700,7 +1795,7 @@ def less_than(cls, session, first, second): second_data = second r = first_data < second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1717,7 +1812,7 @@ def less_than_equal(cls, session, first, second): second_data = second r = first_data <= second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1734,7 +1829,7 @@ def equal(cls, session, first, second): second_data = second r = first_data == second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1751,7 +1846,7 @@ def not_equal(cls, session, first, second): second_data = second r = first_data != second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1768,7 +1863,7 @@ def greater_than(cls, session, first, second): second_data = second r = first_data > second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @@ -1785,6 +1880,166 @@ def greater_than_equal(cls, session, first, second): second_data = second r = first_data >= second_data - f = NumericMemField(session, cls.dtype_to_str(r.dtype)) + f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) - return f \ No newline at end of file + return f + + @staticmethod + def apply_filter_to_indexed_field(source, filter_to_apply, target=None, in_place=False): + if in_place is True and target is not None: + raise ValueError("if 'in_place is True, 'target' must be None") + + dest_indices, dest_values = \ + ops.apply_filter_to_index_values(filter_to_apply, + source.indices[:], source.values[:]) + + if in_place: + if not source._write_enabled: + raise ValueError("This field is marked read-only. Call writeable() on it before " + "performing in-place filtering") + source.indices.clear() + source.indices.write(dest_indices) + source.values.clear() + source.values.write(dest_values) + return source + + if target is not None: + if len(target.indices) == len(dest_indices): + target.indices[:] = dest_indices + else: + target.indices.clear() + target.indices.write(dest_indices) + if len(target.values) == len(dest_values): + target.values[:] = dest_values + else: + target.values.clear() + target.values.write(dest_values) + return target + else: + mem_field = IndexedStringMemField(source._session, source.chunksize) + mem_field.indices.write(dest_indices) + mem_field.values.write(dest_values) + return mem_field + + @staticmethod + def apply_index_to_indexed_field(source, index_to_apply, target=None, in_place=False): + if in_place is True and target is not None: + raise ValueError("if 'in_place is True, 'target' must be None") + + dest_indices, dest_values = \ + ops.apply_indices_to_index_values(index_to_apply, + source.indices[:], source.values[:]) + + if in_place: + if not source._write_enabled: + raise ValueError("This field is marked read-only. Call writeable() on it before " + "performing in-place filtering") + source.indices.clear() + source.indices.write(dest_indices) + source.values.clear() + source.values.write(dest_values) + return source + + if target is not None: + if len(target.indices) == len(dest_indices): + target.indices[:] = dest_indices + else: + target.indices.clear() + target.indices.write(dest_indices) + if len(target.values) == len(dest_values): + target.values[:] = dest_values + else: + target.values.clear() + target.values.write(dest_values) + return target + else: + mem_field = IndexedStringMemField(source._session, source.chunksize) + mem_field.indices.write(dest_indices) + mem_field.values.write(dest_values) + return mem_field + + + @staticmethod + def apply_filter_to_field(source, filter_to_apply, target=None, in_place=None): + if in_place is True and target is not None: + raise ValueError("if 'in_place is True, 'target' must be None") + + dest_data = source.data[filter_to_apply] + + if in_place: + if not source._write_enabled: + raise ValueError("This field is marked read-only. Call writeable() on it before " + "performing in-place filtering") + source.data.clear() + source.data.write(dest_data) + return source + + if target is not None: + if len(target.data) == len(dest_data): + target.data[:] = dest_data + else: + target.data.clear() + target.data.write(dest_data) + else: + mem_field = source.create_like(None, None) + mem_field.data.write(dest_data) + return mem_field + + @staticmethod + def apply_index_to_field(source, index_to_apply, target=None, in_place=None): + if in_place is True and target is not None: + raise ValueError("if 'in_place is True, 'target' must be None") + + dest_data = source.data[:][index_to_apply] + + if in_place: + if not source._write_enabled: + raise ValueError("This field is marked read-only. Call writeable() on it before " + "performing in-place filtering") + source.data.clear() + source.data.write(dest_data) + return source + + if target is not None: + if len(target.data) == len(dest_data): + target.data[:] = dest_data + else: + target.data.clear() + target.data.write(dest_data) + else: + mem_field = source.create_like(None, None) + mem_field.data.write(dest_data) + return mem_field + + @staticmethod + def indexed_string_create_like(source, group, name, timestamp): + if group is None and name is not None: + raise ValueError("if 'group' is None, 'name' must also be 'None'") + + ts = source.timestamp if timestamp is None else timestamp + + if group is None: + return IndexedStringMemField(source._session, source.chunksize) + + if isinstance(group, h5py.Group): + indexed_string_field_constructor(source._session, group, name, ts, source._chunksize) + return IndexedStringField(source._session, group[name], write_enabled=True) + else: + return group.create_indexed_string(name, ts, source._chunksize) + + @staticmethod + def numeric_field_create_like(source, group, name, timestamp): + if group is None and name is not None: + raise ValueError("if 'group' is None, 'name' must also be 'None'") + + ts = source.timestamp if timestamp is None else timestamp + nformat = source._nformat + + if group is None: + return NumericMemField(source._session, nformat) + + if isinstance(group, h5py.Group): + numeric_field_constructor(source._session, group, name, nformat, ts, source.chunksize) + return NumericField(source._session, group[name], write_enabled=True) + else: + return group.create_numeric(name, nformat, ts, source.chunksize) diff --git a/exetera/core/session.py b/exetera/core/session.py index f67419b9..4a4b25e4 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -270,12 +270,12 @@ def apply_filter(self, filter_to_apply, src, dest=None): writer_ = None if dest is not None: writer_ = val.field_from_parameter(self, 'writer', dest) - if isinstance(src, fld.IndexedStringField): + if isinstance(src, Field): newfld = src.apply_filter(filter_to_apply_, writer_) - return newfld.indices, newfld.values - elif isinstance(src, Field): - newfld = src.apply_filter(filter_to_apply_, writer_) - return newfld.data[:] + if src.indexed: + return newfld.indices[:], newfld.values[:] + else: + return newfld.data[:] # elif isinstance(src, df.datafrme): else: reader_ = val.array_from_parameter(self, 'reader', src) @@ -299,14 +299,20 @@ def apply_index(self, index_to_apply, src, dest=None): writer_ = None if dest is not None: writer_ = val.field_from_parameter(self, 'writer', dest) - if isinstance(src, fld.IndexedStringField): - dest_indices, dest_values = \ - ops.apply_indices_to_index_values(index_to_apply_, - src.indices[:], src.values[:]) - return dest_indices, dest_values - elif isinstance(src, Field): + if isinstance(src, Field): newfld = src.apply_index(index_to_apply_, writer_) - return newfld.data[:] + if src.indexed: + return newfld.indices[:], newfld.values[:] + else: + return newfld.data[:] + # if src.indexed: + # dest_indices, dest_values = \ + # ops.apply_indices_to_index_values(index_to_apply_, + # src.indices[:], src.values[:]) + # return dest_indices, dest_values + # elif isinstance(src, Field): + # newfld = src.apply_index(index_to_apply_, writer_) + # return newfld.data[:] else: reader_ = val.array_from_parameter(self, 'reader', src) result = reader_[index_to_apply] diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index b5e1c97a..32cabad2 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -77,6 +77,6 @@ def test_dataframe_ops(self): self.assertEqual([b'a', b'b', b'c', b'd', b'e'], ddf.get_field('fst').data[:].tolist()) filter_to_apply = np.array([True, True, False, False, True]) - df.apply_filter(filter_to_apply) + df.apply_filter_to_indexed_field(filter_to_apply) self.assertEqual([5, 4, 1], df.get_field('numf').data[:].tolist()) self.assertEqual([b'e', b'd', b'a'], df.get_field('fst').data[:].tolist()) diff --git a/tests/test_fields.py b/tests/test_fields.py index 7eae72f1..960f5004 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -144,7 +144,7 @@ def test_timestamp_is_sorted(self): class TestIndexedStringFields(unittest.TestCase): - def test_create_indexed_string(self): + def test_filter_indexed_string(self): bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, "w", "src") @@ -153,22 +153,35 @@ def test_create_indexed_string(self): f = fields.IndexedStringImporter(s, hf, 'foo') f.write(strings) f = s.get(hf['foo']) - # f = s.create_indexed_string(hf, 'foo') self.assertListEqual([0, 1, 3, 6, 10], f.indices[:].tolist()) f2 = s.create_indexed_string(hf, 'bar') s.apply_filter(np.asarray([False, True, True, False]), f, f2) - # print(f2.indices[:]) self.assertListEqual([0, 2, 5], f2.indices[:].tolist()) - # print(f2.values[:]) self.assertListEqual([98, 98, 99, 99, 99], f2.values[:].tolist()) - # print(f2.data[:]) self.assertListEqual(['bb', 'ccc'], f2.data[:]) - # print(f2.data[0]) self.assertEqual('bb', f2.data[0]) - # print(f2.data[1]) self.assertEqual('ccc', f2.data[1]) + def test_reindex_indexed_string(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, "w", "src") + hf = dst.create_dataframe('src') + strings = ['a', 'bb', 'ccc', 'dddd'] + f = fields.IndexedStringImporter(s, hf, 'foo') + f.write(strings) + f = s.get(hf['foo']) + self.assertListEqual([0, 1, 3, 6, 10], f.indices[:].tolist()) + + f2 = s.create_indexed_string(hf, 'bar') + s.apply_index(np.asarray([3, 0, 2, 1], dtype=np.int64), f, f2) + self.assertListEqual([0, 4, 5, 8, 10], f2.indices[:].tolist()) + self.assertListEqual([100, 100, 100, 100, 97, 99, 99, 99, 98, 98], + f2.values[:].tolist()) + self.assertListEqual(['dddd', 'a', 'ccc', 'bb'], f2.data[:]) + + def test_update_legacy_indexed_string_that_has_uint_values(self): bio = BytesIO() @@ -300,13 +313,13 @@ def test_tuple(expected, actual): ds = s.open_dataset(bio, 'w', 'ds') df = ds.create_dataframe('df') - m1 = fields.NumericMemField(s, fields.FieldDataOps.dtype_to_str(a1.dtype)) - m2 = fields.NumericMemField(s, fields.FieldDataOps.dtype_to_str(a2.dtype)) + m1 = fields.NumericMemField(s, fields.dtype_to_str(a1.dtype)) + m2 = fields.NumericMemField(s, fields.dtype_to_str(a2.dtype)) m1.data.write(a1) m2.data.write(a2) - f1 = df.create_numeric('f1', fields.FieldDataOps.dtype_to_str(a1.dtype)) - f2 = df.create_numeric('f2', fields.FieldDataOps.dtype_to_str(a2.dtype)) + f1 = df.create_numeric('f1', fields.dtype_to_str(a1.dtype)) + f2 = df.create_numeric('f2', fields.dtype_to_str(a2.dtype)) f1.data.write(a1) f2.data.write(a2) @@ -321,14 +334,14 @@ def test_tuple(expected, actual): r = function(f1, f2) if isinstance(r, tuple): df.create_numeric( - 'f3a', fields.FieldDataOps.dtype_to_str(r[0].data.dtype)).data.write(r[0]) + 'f3a', fields.dtype_to_str(r[0].data.dtype)).data.write(r[0]) df.create_numeric( - 'f3b', fields.FieldDataOps.dtype_to_str(r[1].data.dtype)).data.write(r[1]) + 'f3b', fields.dtype_to_str(r[1].data.dtype)).data.write(r[1]) test_simple(expected[0], df['f3a']) test_simple(expected[1], df['f3b']) else: df.create_numeric( - 'f3', fields.FieldDataOps.dtype_to_str(r.data.dtype)).data.write(r) + 'f3', fields.dtype_to_str(r.data.dtype)).data.write(r) test_simple(expected, df['f3']) def test_mixed_field_add(self): @@ -458,3 +471,200 @@ def test_categorical_remap(self): bar.data.write(mbar) print(bar.data[:]) print(bar.keys) + + +class TestFieldApplyFilter(unittest.TestCase): + + def test_indexed_string_apply_filter(self): + + data = ['a', 'bb', 'ccc', 'dddd', '', 'eeee', 'fff', 'gg', 'h'] + filt = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0], dtype=bool) + + expected_indices = [0, 1, 3, 6, 10, 10, 14, 17, 19, 20] + expected_values = [97, 98, 98, 99, 99, 99, 100, 100, 100, 100, + 101, 101, 101, 101, 102, 102, 102, 103, 103, 104] + expected_filt_indices = [0, 2, 6, 10, 12] + expected_filt_values = [98, 98, 100, 100, 100, 100, 101, 101, 101, 101, 103, 103] + expected_filt_data = ['bb', 'dddd', 'eeee', 'gg'] + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_indexed_string('foo') + f.data.write(data) + self.assertListEqual(expected_indices, f.indices[:].tolist()) + self.assertListEqual(expected_values, f.values[:].tolist()) + self.assertListEqual(data, f.data[:]) + + g = f.apply_filter(filt, in_place=True) + self.assertListEqual(expected_filt_indices, f.indices[:].tolist()) + self.assertListEqual(expected_filt_values, f.values[:].tolist()) + self.assertListEqual(expected_filt_data, f.data[:]) + + mf = fields.IndexedStringMemField(s) + mf.data.write(data) + self.assertListEqual(expected_indices, mf.indices[:].tolist()) + self.assertListEqual(expected_values, mf.values[:].tolist()) + self.assertListEqual(data, mf.data[:]) + + mf.apply_filter(filt, in_place=True) + self.assertListEqual(expected_filt_indices, mf.indices[:].tolist()) + self.assertListEqual(expected_filt_values, mf.values[:].tolist()) + self.assertListEqual(expected_filt_data, mf.data[:]) + + b = df.create_indexed_string('bar') + b.data.write(data) + self.assertListEqual(expected_indices, b.indices[:].tolist()) + self.assertListEqual(expected_values, b.values[:].tolist()) + self.assertListEqual(data, b.data[:]) + + mb = b.apply_filter(filt) + self.assertListEqual(expected_filt_indices, mb.indices[:].tolist()) + self.assertListEqual(expected_filt_values, mb.values[:].tolist()) + self.assertListEqual(expected_filt_data, mb.data[:]) + + def test_numeric_apply_filter(self): + data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=np.int32) + filt = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0], dtype=bool) + expected = [2, 4, 6, 8] + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_numeric('foo', 'int32') + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.apply_filter(filt, in_place=True) + self.assertListEqual(expected, f.data[:].tolist()) + + mf = fields.NumericMemField(s, 'int32') + mf.data.write(data) + self.assertListEqual(data.tolist(), mf.data[:].tolist()) + + mf.apply_filter(filt, in_place=True) + self.assertListEqual(expected, mf.data[:].tolist()) + + b = df.create_numeric('bar', 'int32') + b.data.write(data) + self.assertListEqual(data.tolist(), b.data[:].tolist()) + + mb = b.apply_filter(filt) + self.assertListEqual(expected, mb.data[:].tolist()) + + +class TestFieldApplyIndex(unittest.TestCase): + + def test_indexed_string_apply_index(self): + + data = ['a', 'bb', 'ccc', 'dddd', '', 'eeee', 'fff', 'gg', 'h'] + inds = np.array([8, 0, 7, 1, 6, 2, 5, 3, 4], dtype=np.int32) + + expected_indices = [0, 1, 3, 6, 10, 10, 14, 17, 19, 20] + expected_values = [97, 98, 98, 99, 99, 99, 100, 100, 100, 100, + 101, 101, 101, 101, 102, 102, 102, 103, 103, 104] + expected_filt_indices = [0, 1, 2, 4, 6, 9, 12, 16, 20, 20] + expected_filt_values = [104, 97, 103, 103, 98, 98, 102, 102, 102, 99, 99, 99, + 101, 101, 101, 101, 100, 100, 100, 100] + expected_filt_data = ['h', 'a', 'gg', 'bb', 'fff', 'ccc', 'eeee', 'dddd', ''] + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_indexed_string('foo') + f.data.write(data) + self.assertListEqual(expected_indices, f.indices[:].tolist()) + self.assertListEqual(expected_values, f.values[:].tolist()) + self.assertListEqual(data, f.data[:]) + + g = f.apply_index(inds, in_place=True) + self.assertListEqual(expected_filt_indices, f.indices[:].tolist()) + self.assertListEqual(expected_filt_values, f.values[:].tolist()) + self.assertListEqual(expected_filt_data, f.data[:]) + + mf = fields.IndexedStringMemField(s) + mf.data.write(data) + self.assertListEqual(expected_indices, mf.indices[:].tolist()) + self.assertListEqual(expected_values, mf.values[:].tolist()) + self.assertListEqual(data, mf.data[:]) + + mf.apply_index(inds, in_place=True) + self.assertListEqual(expected_filt_indices, mf.indices[:].tolist()) + self.assertListEqual(expected_filt_values, mf.values[:].tolist()) + self.assertListEqual(expected_filt_data, mf.data[:]) + + b = df.create_indexed_string('bar') + b.data.write(data) + self.assertListEqual(expected_indices, b.indices[:].tolist()) + self.assertListEqual(expected_values, b.values[:].tolist()) + self.assertListEqual(data, b.data[:]) + + mb = b.apply_index(inds) + self.assertListEqual(expected_filt_indices, mb.indices[:].tolist()) + self.assertListEqual(expected_filt_values, mb.values[:].tolist()) + self.assertListEqual(expected_filt_data, mb.data[:]) + + def test_numeric_apply_index(self): + data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9], dtype='int32') + indices = np.array([8, 0, 7, 1, 6, 2, 5, 3, 4], dtype=np.int32) + expected = [9, 1, 8, 2, 7, 3, 6, 4, 5] + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_numeric('foo', 'int32') + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.apply_index(indices, in_place=True) + self.assertListEqual(expected, f.data[:].tolist()) + + mf = fields.NumericMemField(s, 'int32') + mf.data.write(data) + self.assertListEqual(data.tolist(), mf.data[:].tolist()) + + mf.apply_index(indices, in_place=True) + self.assertListEqual(expected, mf.data[:].tolist()) + + b = df.create_numeric('bar', 'int32') + b.data.write(data) + self.assertListEqual(data.tolist(), b.data[:].tolist()) + + mb = b.apply_index(indices) + self.assertListEqual(expected, mb.data[:].tolist()) + + +class TestFieldCreateLike(unittest.TestCase): + + def test_indexed_string_field_create_like(self): + data = ['a', 'bb', 'ccc', 'ddd'] + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_indexed_string('foo') + f.data.write(data) + self.assertListEqual(data, f.data[:]) + + g = f.create_like(None, None) + self.assertIsInstance(g, fields.IndexedStringMemField) + self.assertEqual(0, len(g.data)) + + def test_numeric_field_create_like(self): + data = np.asarray([1, 2, 3, 4], dtype=np.int32) + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_numeric('foo', 'int32') + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.create_like(None, None) + self.assertIsInstance(g, fields.NumericMemField) + self.assertEqual(0, len(g.data)) From 63bd5a0e852b5fcf40328f970d2f12db470feca6 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 16 Apr 2021 13:12:48 +0100 Subject: [PATCH 058/181] add unittest for various fields in dataframe add dataframe.add/drop/move add docstrings --- docs/exetera.core.rst | 8 ++ exetera/core/abstract_types.py | 12 +-- exetera/core/dataframe.py | 15 +--- exetera/core/dataset.py | 90 +++++++++++++------ exetera/core/fields.py | 4 +- tests/test_dataframe.py | 152 +++++++++++++++++++++++++++++++-- tests/test_dataset.py | 4 +- tests/test_fields.py | 19 ----- tests/test_session.py | 4 +- 9 files changed, 232 insertions(+), 76 deletions(-) diff --git a/docs/exetera.core.rst b/docs/exetera.core.rst index 3cb60e23..4ee9e9a1 100644 --- a/docs/exetera.core.rst +++ b/docs/exetera.core.rst @@ -28,6 +28,14 @@ exetera.core.dataset module :undoc-members: :show-inheritance: +exetera.core.dataframe module +--------------------------- + +.. automodule:: exetera.core.dataframe + :members: + :undoc-members: + :show-inheritance: + exetera.core.exporter module ---------------------------- diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index f3ab5b50..bd5db71d 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -100,9 +100,9 @@ def __getitem__(self, name): def get_dataframe(self, name): raise NotImplementedError() - @abstractmethod - def get_name(self, dataframe): - raise NotImplementedError() + # @abstractmethod + # def get_name(self, dataframe): + # raise NotImplementedError() @abstractmethod def __setitem__(self, name, dataframe): @@ -178,9 +178,9 @@ def __getitem__(self, name): def get_field(self, name): raise NotImplementedError() - @abstractmethod - def get_name(self, field): - raise NotImplementedError() + # @abstractmethod + # def get_name(self, field): + # raise NotImplementedError() @abstractmethod def __setitem__(self, name, field): diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 416687f3..69160a6e 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -273,7 +273,7 @@ def apply_filter(self, filter_to_apply, ddf=None): raise TypeError("The destination object must be an instance of DataFrame.") for name, field in self._columns.items(): newfld = field.create_like(ddf, field.name[field.name.index('/', 1)+1:]) - ddf.add(field.apply_filter(filter_to_apply, dstfld=newfld), name=name) + field.apply_filter(filter_to_apply, dstfld=newfld) return ddf else: for field in self._columns.values(): @@ -294,7 +294,6 @@ def apply_index(self, index_to_apply, ddf=None): for name, field in self._columns.items(): newfld = field.create_like(ddf, field.name[field.name.index('/', 1)+1:]) idx = field.apply_index(index_to_apply, dstfld=newfld) - ddf.add(idx, name=name) return ddf else: for field in self._columns.values(): @@ -328,8 +327,6 @@ def drop(dataframe: DataFrame, field: fld.Field): """ dataframe.delete_field(field) - - @staticmethod def move(src_df: DataFrame, field: fld.Field, dest_df: DataFrame, name: str): """ @@ -340,11 +337,5 @@ def move(src_df: DataFrame, field: fld.Field, dest_df: DataFrame, name: str): :param dest_df: The destination dataframe to move to. :param name: The name of field under destination dataframe. """ - dfield = field.create_like(dest_df, name) - if field.indexed: - dfield.indices.write(field.indices[:]) - dfield.values.write(field.values[:]) - else: - dfield.data.write(field.data[:]) - dest_df.columns[name] = dfield - src_df.delete_field(field) + HDF5DataFrame.copy(field, dest_df, name) + HDF5DataFrame.drop(src_df, field) diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 37bb7481..e8911455 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -91,9 +91,17 @@ def add(self, dataframe, name=None): :return: None if the operation is successful; otherwise throw Error. """ dname = dataframe.name if name is None else name - self._file.copy(dataframe._h5group, self._file, name=dname) - df = edf.HDF5DataFrame(self, dname, h5group=self._file[dname]) - self._dataframes[dname] = df + self._file.create_group(dname) + h5group = self._file[dname] + _dataframe = edf.HDF5DataFrame(self, dname, h5group) + for k, v in dataframe.items(): + f = v.create_like(_dataframe, k) + if f.indexed: + f.indices.write(v.indices[:]) + f.values.write(v.values[:]) + else: + f.data.write(v.data[:]) + self._dataframes[dname] = _dataframe def __contains__(self, name: str): """ @@ -141,19 +149,19 @@ def get_dataframe(self, name: str): """ self.__getitem__(name) - def get_name(self, dataframe: DataFrame): - """ - If the dataframe exist in this dataset, return the name; otherwise return None. - - :param dataframe: The dataframe instance to find the name. - :return: name (str) of the dataframe or None if dataframe not found in this dataset. - """ - if not isinstance(dataframe, edf.DataFrame): - raise TypeError("The field argument must be a DataFrame object.") - for name, v in self._dataframes.items(): - if id(dataframe) == id(v): - return name - return None + # def get_name(self, dataframe: DataFrame): + # """ + # If the dataframe exist in this dataset, return the name; otherwise return None. + # + # :param dataframe: The dataframe instance to find the name. + # :return: name (str) of the dataframe or None if dataframe not found in this dataset. + # """ + # if not isinstance(dataframe, edf.DataFrame): + # raise TypeError("The field argument must be a DataFrame object.") + # for name, v in self._dataframes.items(): + # if id(dataframe) == id(v): + # return name + # return None def __setitem__(self, name: str, dataframe: DataFrame): """ @@ -171,11 +179,12 @@ def __setitem__(self, name: str, dataframe: DataFrame): raise TypeError("The field must be a DataFrame object.") else: if dataframe.dataset == self: # rename a dataframe - return self._file.move(dataframe.name, name) + + self._file.move(dataframe.h5group.name, name) else: # new dataframe from another dataset if self._dataframes.__contains__(name): self.__delitem__(name) - return self.add(dataframe, name) + self.add(dataframe, name) def __delitem__(self, name: str): """ @@ -196,7 +205,8 @@ def delete_dataframe(self, dataframe: DataFrame): :param dataframe: The dataframe instance to delete. :return: Boolean if the dataframe is deleted. """ - name = self.get_name(dataframe) + #name = self.get_name(dataframe) + name = dataframe.name if name is None: raise ValueError("This dataframe does not contain the field to delete.") else: @@ -228,13 +238,45 @@ def __len__(self): @staticmethod def copy(dataframe: DataFrame, dataset: Dataset, name: str): - dataset.add(dataframe,name=name) + """ + Copy dataframe to another dataset via HDF5DataFrame.copy(ds1['df1'], ds2, 'df1']) - @staticmethod - def move(dataframe: DataFrame, dataset: Dataset, name:str): - dataset.add(dataframe, name=name) - dataframe._dataset.delete_dataframe(dataframe) + :param dataframe: The dataframe to copy. + :param dataset: The destination dataset. + :param name: The name of dataframe in destination dataset. + """ + dataset._file.create_group(name) + h5group = dataset._file[name] + _dataframe = edf.HDF5DataFrame(dataset, name, h5group) + for k, v in dataframe.items(): + f = v.create_like(_dataframe, k) + if f.indexed: + f.indices.write(v.indices[:]) + f.values.write(v.values[:]) + else: + f.data.write(v.data[:]) + dataset._dataframes[name] = _dataframe @staticmethod def drop(dataframe: DataFrame): + """ + Delete a dataframe by HDF5DataFrame.drop(ds['df1']). + + :param dataframe: The dataframe to delete. + """ dataframe._dataset.delete_dataframe(dataframe) + + @staticmethod + def move(dataframe: DataFrame, dataset: Dataset, name:str): + """ + Move a dataframe to another dataset via HDF5DataFrame.move(ds1['df1'], ds2, 'df1']). + If move within the same dataset, e.g. HDF5DataFrame.move(ds1['df1'], ds1, 'df2']), function as a rename for both + dataframe and HDF5Group. However, to + + :param dataframe: The dataframe to copy. + :param dataset: The destination dataset. + :param name: The name of dataframe in destination dataset. + """ + HDF5Dataset.copy(dataframe, dataset, name) + HDF5Dataset.drop(dataframe) + diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 85d0cadc..21890977 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -1104,7 +1104,7 @@ def __le__(self, value): def __eq__(self, value): return FieldDataOps.equal(self._session, self, value) - def __eq__(self, value): + def __ne__(self, value): return FieldDataOps.not_equal(self._session, self, value) def __gt__(self, value): @@ -1227,7 +1227,7 @@ def __le__(self, value): def __eq__(self, value): return FieldDataOps.equal(self._session, self, value) - def __eq__(self, value): + def __ne__(self, value): return FieldDataOps.not_equal(self._session, self, value) def __gt__(self, value): diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index b5e1c97a..ab554f7e 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -18,15 +18,12 @@ def test_dataframe_init(self): df = dst.create_dataframe('dst') self.assertTrue(isinstance(df, dataframe.DataFrame)) numf = df.create_numeric('numf', 'uint32') - # fdf = {'numf': numf} - # df2 = dst.create_dataframe('dst2', dataframe=fdf) df2 = dst.create_dataframe('dst2', dataframe=df) self.assertTrue(isinstance(df2, dataframe.DataFrame)) # add & set & contains - df.add(numf) self.assertTrue('numf' in df) - self.assertTrue(df.contains_field(numf)) + self.assertTrue('numf' in df2) cat = s.create_categorical(df2, 'cat', 'int8', {'a': 1, 'b': 2}) self.assertFalse('cat' in df) self.assertFalse(df.contains_field(cat)) @@ -36,7 +33,6 @@ def test_dataframe_init(self): # list & get self.assertEqual(id(numf), id(df.get_field('numf'))) self.assertEqual(id(numf), id(df['numf'])) - self.assertEqual('numf', df.get_name(numf)) # list & iter dfit = iter(df) @@ -48,9 +44,8 @@ def test_dataframe_init(self): self.assertFalse('numf' in df) df.delete_field(cat) self.assertFalse(df.contains_field(cat)) - self.assertIsNone(df.get_name(cat)) - def test_dataframe_create_field(self): + def test_dataframe_create_numeric(self): bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, 'r+', 'dst') @@ -58,6 +53,145 @@ def test_dataframe_create_field(self): num = df.create_numeric('num', 'uint32') num.data.write([1, 2, 3, 4]) self.assertEqual([1, 2, 3, 4], num.data[:].tolist()) + num2 = df.create_numeric('num2', 'uint32') + num2.data.write([1, 2, 3, 4]) + + def test_dataframe_create_numeric(self): + bio = BytesIO() + with session.Session() as s: + np.random.seed(12345678) + values = np.random.randint(low=0, high=1000000, size=100000000) + dst = s.open_dataset(bio, 'r+', 'dst') + df = dst.create_dataframe('dst') + a = df.create_numeric('a','int32') + a.data.write(values) + + total = np.sum(a.data[:]) + self.assertEqual(49997540637149, total) + + a.data[:] = a.data[:] * 2 + total = np.sum(a.data[:]) + self.assertEqual(99995081274298, total) + + def test_dataframe_create_categorical(self): + bio = BytesIO() + with session.Session() as s: + np.random.seed(12345678) + values = np.random.randint(low=0, high=3, size=100000000) + dst = s.open_dataset(bio, 'r+', 'dst') + hf = dst.create_dataframe('dst') + a = hf.create_categorical('a', 'int8', + {'foo': 0, 'bar': 1, 'boo': 2}) + a.data.write(values) + + total = np.sum(a.data[:]) + self.assertEqual(99987985, total) + + def test_dataframe_create_fixed_string(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'r+', 'dst') + hf = dst.create_dataframe('dst') + np.random.seed(12345678) + values = np.random.randint(low=0, high=4, size=1000000) + svalues = [b''.join([b'x'] * v) for v in values] + a = hf.create_fixed_string('a', 8) + a.data.write(svalues) + + total = np.unique(a.data[:]) + self.assertListEqual([b'', b'x', b'xx', b'xxx'], total.tolist()) + + a.data[:] = np.core.defchararray.add(a.data[:], b'y') + self.assertListEqual( + [b'xxxy', b'xxy', b'xxxy', b'y', b'xy', b'y', b'xxxy', b'xxxy', b'xy', b'y'], + a.data[:10].tolist()) + + + def test_dataframe_create_indexed_string(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'r+', 'dst') + hf = dst.create_dataframe('dst') + np.random.seed(12345678) + values = np.random.randint(low=0, high=4, size=200000) + svalues = [''.join(['x'] * v) for v in values] + a = hf.create_indexed_string('a', 8) + a.data.write(svalues) + + total = np.unique(a.data[:]) + self.assertListEqual(['', 'x', 'xx', 'xxx'], total.tolist()) + + strs = a.data[:] + strs = [s + 'y' for s in strs] + a.data.clear() + a.data.write(strs) + + # print(strs[:10]) + self.assertListEqual( + ['xxxy', 'xxy', 'xxxy', 'y', 'xy', 'y', 'xxxy', 'xxxy', 'xy', 'y'], strs[:10]) + # print(a.indices[:10]) + self.assertListEqual([0, 4, 7, 11, 12, 14, 15, 19, 23, 25], + a.indices[:10].tolist()) + # print(a.values[:10]) + self.assertListEqual( + [120, 120, 120, 121, 120, 120, 121, 120, 120, 120], a.values[:10].tolist()) + # print(a.data[:10]) + self.assertListEqual( + ['xxxy', 'xxy', 'xxxy', 'y', 'xy', 'y', 'xxxy', 'xxxy', 'xy', 'y'], a.data[:10]) + + + def test_dataframe_create_mem_numeric(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'r+', 'dst') + df = dst.create_dataframe('dst') + num = df.create_numeric('num', 'uint32') + num.data.write([1, 2, 3, 4]) + self.assertEqual([1, 2, 3, 4], num.data[:].tolist()) + num2 = df.create_numeric('num2', 'uint32') + num2.data.write([1, 2, 3, 4]) + + df['num3'] = num + num2 + self.assertEqual([2, 4, 6, 8], df['num3'].data[:].tolist()) + df['num4'] = num - np.array([1, 2, 3, 4]) + self.assertEqual([0, 0, 0, 0], df['num4'].data[:].tolist()) + df['num5'] = num * np.array([1, 2, 3, 4]) + self.assertEqual([1, 4, 9, 16], df['num5'].data[:].tolist()) + df['num6'] = df['num5'] / np.array([1, 2, 3, 4]) + self.assertEqual([1, 2, 3, 4], df['num6'].data[:].tolist()) + df['num7'] = df['num'] & df['num2'] + self.assertEqual([1, 2, 3, 4], df['num7'].data[:].tolist()) + df['num8'] = df['num'] | df['num2'] + self.assertEqual([1, 2, 3, 4], df['num8'].data[:].tolist()) + df['num9'] = df['num'] ^ df['num2'] + self.assertEqual([0, 0, 0, 0], df['num9'].data[:].tolist()) + df['num10'] = df['num'] % df['num2'] + self.assertEqual([0, 0, 0, 0], df['num10'].data[:].tolist()) + + + def test_dataframe_create_mem_categorical(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'r+', 'dst') + df = dst.create_dataframe('dst') + cat1 = df.create_categorical('cat1','uint8',{'foo': 0, 'bar': 1, 'boo': 2}) + cat1.data.write([0, 1, 2, 0, 1, 2]) + + cat2 = df.create_categorical('cat2','uint8',{'foo': 0, 'bar': 1, 'boo': 2}) + cat2.data.write([1, 2, 0, 1, 2, 0]) + + df['r1'] = cat1 < cat2 + self.assertEqual([True, True, False, True, True, False], df['r1'].data[:].tolist()) + df['r2'] = cat1 <= cat2 + self.assertEqual([True, True, False, True, True, False], df['r2'].data[:].tolist()) + df['r3'] = cat1 == cat2 + self.assertEqual([False, False, False, False, False, False], df['r3'].data[:].tolist()) + df['r4'] = cat1 != cat2 + self.assertEqual([True, True, True, True, True, True], df['r4'].data[:].tolist()) + df['r5'] = cat1 > cat2 + self.assertEqual([False, False, True, False, False, True], df['r5'].data[:].tolist()) + df['r6'] = cat1 >= cat2 + self.assertEqual([False, False, True, False, False, True], df['r6'].data[:].tolist()) def test_dataframe_ops(self): bio = BytesIO() @@ -66,10 +200,10 @@ def test_dataframe_ops(self): df = dst.create_dataframe('dst') numf = s.create_numeric(df, 'numf', 'int32') numf.data.write([5, 4, 3, 2, 1]) - df.add(numf) + fst = s.create_fixed_string(df, 'fst', 3) fst.data.write([b'e', b'd', b'c', b'b', b'a']) - df.add(fst) + index = np.array([4, 3, 2, 1, 0]) ddf = dst.create_dataframe('dst2') df.apply_index(index, ddf) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 9e37a7a8..380e78da 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -137,8 +137,8 @@ def test_dataframe_create_with_dataframe(self): df2['c_foo'].data[:] = ccontents2 self.assertListEqual(ccontents1.tolist(), df1['c_foo'].data[:].tolist()) self.assertListEqual(ccontents2.tolist(), df2['c_foo'].data[:].tolist()) - self.assertDictEqual({1: b'a', 2: b'b'}, df1['c_foo'].keys) - self.assertDictEqual({1: b'a', 2: b'b'}, df2['c_foo'].keys) + self.assertDictEqual({1: 'a', 2: 'b'}, df1['c_foo'].keys) + self.assertDictEqual({1: 'a', 2: 'b'}, df2['c_foo'].keys) self.assertListEqual(ncontents1.tolist(), df1['n_foo'].data[:].tolist()) self.assertListEqual(ncontents1.tolist(), df2['n_foo'].data[:].tolist()) diff --git a/tests/test_fields.py b/tests/test_fields.py index 7eae72f1..9205b10d 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -191,26 +191,7 @@ def test_index_string_field_get_span(self): self.assertListEqual([0, 1, 3, 6, 8, 9, 12], s.get_spans(idx)) -class TestFieldArray(unittest.TestCase): - - def test_write_part(self): - bio = BytesIO() - s = session.Session() - ds = s.open_dataset(bio, "w", "src") - dst = ds.create_dataframe('src') - num = s.create_numeric(dst, 'num', 'int32') - num.data.write_part(np.arange(10)) - self.assertListEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], list(num.data[:])) - def test_clear(self): - bio = BytesIO() - s = session.Session() - ds = s.open_dataset(bio, "w", "src") - dst = ds.create_dataframe('src') - num = s.create_numeric(dst, 'num', 'int32') - num.data.write_part(np.arange(10)) - num.data.clear() - self.assertListEqual([], list(num.data[:])) diff --git a/tests/test_session.py b/tests/test_session.py index 96b90777..21d0e530 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -54,7 +54,7 @@ def test_create_then_load_categorical(self): with h5py.File(bio, 'r') as src: f = s.get(src['df']['foo']) self.assertListEqual(contents, f.data[:].tolist()) - self.assertDictEqual({1: b'a', 2: b'b'}, f.keys) + self.assertDictEqual({1: 'a', 2: 'b'}, f.keys) def test_create_then_load_numeric(self): bio = BytesIO() @@ -100,7 +100,7 @@ def test_create_then_load_categorical(self): with session.Session() as s: src = s.open_dataset(bio, 'r', 'src') f = s.get(src['df']['foo']) - self.assertDictEqual({1: b'a', 2: b'b'}, f.keys) + self.assertDictEqual({1: 'a', 2: 'b'}, f.keys) def test_create_new_then_load(self): bio1 = BytesIO() From cb9f2a27c86648ba1da12f771b7beb02bb28ea8c Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 16 Apr 2021 13:19:36 +0100 Subject: [PATCH 059/181] add unittest for Dataframe.add/drop/move --- tests/test_dataframe.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index ab554f7e..73412132 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -193,6 +193,23 @@ def test_dataframe_create_mem_categorical(self): df['r6'] = cat1 >= cat2 self.assertEqual([False, False, True, False, False, True], df['r6'].data[:].tolist()) + def test_datafrmae_static_methods(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + df = dst.create_dataframe('dst') + numf = s.create_numeric(df, 'numf', 'int32') + numf.data.write([5, 4, 3, 2, 1]) + + df2 = dst.create_dataframe('df2') + dataframe.HDF5DataFrame.copy(numf, df2,'numf') + self.assertListEqual([5, 4, 3, 2, 1], df2['numf'].data[:].tolist()) + dataframe.HDF5DataFrame.drop(df, numf) + self.assertTrue('numf' not in df) + dataframe.HDF5DataFrame.move(df2,df2['numf'],df,'numf') + self.assertTrue('numf' not in df2) + self.assertListEqual([5, 4, 3, 2, 1], df['numf'].data[:].tolist()) + def test_dataframe_ops(self): bio = BytesIO() with session.Session() as s: From 013f401951bfaaec821633ac15614e4ef90fbca7 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 16 Apr 2021 13:40:58 +0100 Subject: [PATCH 060/181] minor change on name to make sure name in consistent over dataframe, dataset.key and h5group --- exetera/core/abstract_types.py | 4 ++-- exetera/core/dataset.py | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index bd5db71d..d981be6b 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -81,7 +81,7 @@ def close(self): raise NotImplementedError() @abstractmethod - def add(self, field, name=None): + def add(self, field): raise NotImplementedError() @abstractmethod @@ -135,7 +135,7 @@ class DataFrame(ABC): """ @abstractmethod - def add(self, field, name=None): + def add(self, field): raise NotImplementedError() @abstractmethod diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index e8911455..13627d8a 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -81,7 +81,7 @@ def create_dataframe(self, name, dataframe: DataFrame = None): self._dataframes[name] = _dataframe return _dataframe - def add(self, dataframe, name=None): + def add(self, dataframe): """ Add an existing dataframe (from other dataset) to this dataset, write the existing group attributes and HDF5 datasets to this dataset. @@ -90,7 +90,7 @@ def add(self, dataframe, name=None): :param name: optional- change the dataframe name. :return: None if the operation is successful; otherwise throw Error. """ - dname = dataframe.name if name is None else name + dname = dataframe.name self._file.create_group(dname) h5group = self._file[dname] _dataframe = edf.HDF5DataFrame(self, dname, h5group) @@ -175,16 +175,19 @@ def __setitem__(self, name: str, dataframe: DataFrame): """ if not isinstance(name, str): raise TypeError("The name must be a str object.") - elif not isinstance(dataframe, edf.DataFrame): + if not isinstance(dataframe, edf.DataFrame): raise TypeError("The field must be a DataFrame object.") - else: - if dataframe.dataset == self: # rename a dataframe - self._file.move(dataframe.h5group.name, name) - else: # new dataframe from another dataset - if self._dataframes.__contains__(name): - self.__delitem__(name) - self.add(dataframe, name) + if dataframe.dataset == self: # rename a dataframe + del self._dataframes[dataframe.name] + dataframe.name = name + self._file.move(dataframe.h5group.name, name) + else: # new dataframe from another dataset + if self._dataframes.__contains__(name): + self.__delitem__(name) + dataframe.name = name + self.add(dataframe) + def __delitem__(self, name: str): """ From 18ce7cee698db8711f221a90269b691e2f388046 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 16 Apr 2021 13:47:22 +0100 Subject: [PATCH 061/181] minor fixed of adding prefix b to string in test_session and test_dataset --- tests/test_dataset.py | 4 ++-- tests/test_session.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 380e78da..9e37a7a8 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -137,8 +137,8 @@ def test_dataframe_create_with_dataframe(self): df2['c_foo'].data[:] = ccontents2 self.assertListEqual(ccontents1.tolist(), df1['c_foo'].data[:].tolist()) self.assertListEqual(ccontents2.tolist(), df2['c_foo'].data[:].tolist()) - self.assertDictEqual({1: 'a', 2: 'b'}, df1['c_foo'].keys) - self.assertDictEqual({1: 'a', 2: 'b'}, df2['c_foo'].keys) + self.assertDictEqual({1: b'a', 2: b'b'}, df1['c_foo'].keys) + self.assertDictEqual({1: b'a', 2: b'b'}, df2['c_foo'].keys) self.assertListEqual(ncontents1.tolist(), df1['n_foo'].data[:].tolist()) self.assertListEqual(ncontents1.tolist(), df2['n_foo'].data[:].tolist()) diff --git a/tests/test_session.py b/tests/test_session.py index 21d0e530..96b90777 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -54,7 +54,7 @@ def test_create_then_load_categorical(self): with h5py.File(bio, 'r') as src: f = s.get(src['df']['foo']) self.assertListEqual(contents, f.data[:].tolist()) - self.assertDictEqual({1: 'a', 2: 'b'}, f.keys) + self.assertDictEqual({1: b'a', 2: b'b'}, f.keys) def test_create_then_load_numeric(self): bio = BytesIO() @@ -100,7 +100,7 @@ def test_create_then_load_categorical(self): with session.Session() as s: src = s.open_dataset(bio, 'r', 'src') f = s.get(src['df']['foo']) - self.assertDictEqual({1: 'a', 2: 'b'}, f.keys) + self.assertDictEqual({1: b'a', 2: b'b'}, f.keys) def test_create_new_then_load(self): bio1 = BytesIO() From 8657081a2bedb0870e926ef8441690f416a0d11d Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 16 Apr 2021 13:50:47 +0100 Subject: [PATCH 062/181] minor fixed of adding prefix b to string in test_session and test_dataset --- tests/test_dataset.py | 2 +- tests/test_session.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 9e37a7a8..6997da3a 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -137,7 +137,7 @@ def test_dataframe_create_with_dataframe(self): df2['c_foo'].data[:] = ccontents2 self.assertListEqual(ccontents1.tolist(), df1['c_foo'].data[:].tolist()) self.assertListEqual(ccontents2.tolist(), df2['c_foo'].data[:].tolist()) - self.assertDictEqual({1: b'a', 2: b'b'}, df1['c_foo'].keys) + self.assertDictEqual({1: 'a', 2: 'b'}, df1['c_foo'].keys) self.assertDictEqual({1: b'a', 2: b'b'}, df2['c_foo'].keys) self.assertListEqual(ncontents1.tolist(), df1['n_foo'].data[:].tolist()) diff --git a/tests/test_session.py b/tests/test_session.py index 96b90777..21d0e530 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -54,7 +54,7 @@ def test_create_then_load_categorical(self): with h5py.File(bio, 'r') as src: f = s.get(src['df']['foo']) self.assertListEqual(contents, f.data[:].tolist()) - self.assertDictEqual({1: b'a', 2: b'b'}, f.keys) + self.assertDictEqual({1: 'a', 2: 'b'}, f.keys) def test_create_then_load_numeric(self): bio = BytesIO() @@ -100,7 +100,7 @@ def test_create_then_load_categorical(self): with session.Session() as s: src = s.open_dataset(bio, 'r', 'src') f = s.get(src['df']['foo']) - self.assertDictEqual({1: b'a', 2: b'b'}, f.keys) + self.assertDictEqual({1: 'a', 2: 'b'}, f.keys) def test_create_new_then_load(self): bio1 = BytesIO() From 51e2fec63da00b1354ec0b9eb9a0241332ddc1b8 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Fri, 16 Apr 2021 14:03:45 +0100 Subject: [PATCH 063/181] Completed initial pass of memory fields for all types --- exetera/core/fields.py | 345 +++++++++++++++++++++++++++++++++-------- tests/test_fields.py | 224 +++++++++++++++++++++++++- 2 files changed, 503 insertions(+), 66 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 69960b14..9deeea1b 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -73,6 +73,7 @@ class MemoryField(Field): def __init__(self, session): super().__init__() self._session = session + self._write_enabled = True self._value_wrapper = None @property @@ -393,7 +394,6 @@ def __init__(self, session, chunksize=None): self._data_wrapper = None self._index_wrapper = None self._value_wrapper = None - self._write_enabled = True def writeable(self): return self @@ -476,12 +476,76 @@ def apply_index(self, index_to_apply, target=None, in_place=False): return FieldDataOps.apply_index_to_indexed_field(self, index_to_apply, target, in_place) +class FixedStringMemField(MemoryField): + def __init__(self, session, length): + super().__init__(session) + # TODO: caution; we may want to consider the issues with long-lived field instances getting + # out of sync with their stored counterparts. Maybe a revision number of the stored field + # is required that we can check to see if we are out of date. That or just make this a + # property and have it always look the value up + self._length = length + + def writeable(self): + return FixedStringField(self._session, self._field, write_enabled=True) + + def create_like(self, group, name, timestamp=None): + return FieldDataOps.fixed_string_field_create_like(self, group, name, timestamp) + + @property + def data(self): + if self._value_wrapper is None: + self._value_wrapper = MemoryFieldArray("S{}".format(self._length)) + return self._value_wrapper + + def is_sorted(self): + if len(self) < 2: + return True + data = self.data[:] + return np.all(np.char.compare_chararrays(data[:-1], data[1:], "<=", False)) + + def __len__(self): + return len(self.data) + + def get_spans(self): + return ops.get_spans_for_field(self.data[:]) + + def apply_filter(self, filter_to_apply, target=None, in_place=False): + """ + Apply a boolean filter to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the filtered data is written to. + + :param: filter_to_apply: a Field or numpy array that contains the boolean filter data + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The filtered field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_filter_to_field(self, filter_to_apply, target, in_place) + + def apply_index(self, index_to_apply, target=None, in_place=False): + """ + Apply an index to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the reindexed data is written to. + + :param: index_to_apply: a Field or numpy array that contains the indices + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The reindexed field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) + + class NumericMemField(MemoryField): def __init__(self, session, nformat): super().__init__(session) self._nformat = nformat - # TODO: this is a hack, we should have a property on the interface of Field - self._write_enabled = True def writeable(self): @@ -624,21 +688,12 @@ def __init__(self, session, nformat, keys): super().__init__(session) self._nformat = nformat self._keys = keys - self._write_enabled = True def writeable(self): return self def create_like(self, group, name, timestamp=None): - ts = timestamp - nformat = self._nformat - keys = self._keys - if isinstance(group, h5py.Group): - numeric_field_constructor(self._session, group, name, nformat, keys, - ts, self.chunksize) - return CategoricalField(self._session, group[name], write_enabled=True) - else: - return group.create_categorical(name, nformat, keys, ts, self.chunksize) + return FieldDataOps.categorical_field_create_like(self, group, name, timestamp) @property def data(self): @@ -726,6 +781,127 @@ def __ge__(self, value): return FieldDataOps.greater_than_equal(self._session, self, value) +class TimestampMemField(MemoryField): + def __init__(self, session): + super().__init__(session) + + def writeable(self): + return TimestampField(self._session, self._field, write_enabled=True) + + def create_like(self, group, name, timestamp=None): + return FieldDataOps.timestamp_field_create_like(self, group, name, timestamp) + + @property + def data(self): + if self._value_wrapper is None: + self._value_wrapper = MemoryFieldArray(np.float64) + return self._value_wrapper + + def is_sorted(self): + if len(self) < 2: + return True + data = self.data[:] + return np.all(data[:-1] <= data[1:]) + + def __len__(self): + return len(self.data) + + def get_spans(self): + return ops.get_spans_for_field(self.data[:]) + + def apply_filter(self, filter_to_apply, target=None, in_place=False): + """ + Apply a boolean filter to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the filtered data is written to. + + :param: filter_to_apply: a Field or numpy array that contains the boolean filter data + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The filtered field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_filter_to_field(self, filter_to_apply, target, in_place) + + def apply_index(self, index_to_apply, target=None, in_place=False): + """ + Apply an index to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the reindexed data is written to. + + :param: index_to_apply: a Field or numpy array that contains the indices + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The reindexed field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) + + def __add__(self, second): + return FieldDataOps.numeric_add(self._session, self, second) + + def __radd__(self, first): + return FieldDataOps.numeric_add(self._session, first, self) + + def __sub__(self, second): + return FieldDataOps.numeric_sub(self._session, self, second) + + def __rsub__(self, first): + return FieldDataOps.numeric_sub(self._session, first, self) + + def __mul__(self, second): + return FieldDataOps.numeric_mul(self._session, self, second) + + def __rmul__(self, first): + return FieldDataOps.numeric_mul(self._session, first, self) + + def __truediv__(self, second): + return FieldDataOps.numeric_truediv(self._session, self, second) + + def __rtruediv__(self, first): + return FieldDataOps.numeric_truediv(self._session, first, self) + + def __floordiv__(self, second): + return FieldDataOps.numeric_floordiv(self._session, self, second) + + def __rfloordiv__(self, first): + return FieldDataOps.numeric_floordiv(self._session, first, self) + + def __mod__(self, second): + return FieldDataOps.numeric_mod(self._session, self, second) + + def __rmod__(self, first): + return FieldDataOps.numeric_mod(self._session, first, self) + + def __divmod__(self, second): + return FieldDataOps.numeric_divmod(self._session, self, second) + + def __rdivmod__(self, first): + return FieldDataOps.numeric_divmod(self._session, first, self) + + def __lt__(self, value): + return FieldDataOps.less_than(self._session, self, value) + + def __le__(self, value): + return FieldDataOps.less_than_equal(self._session, self, value) + + def __eq__(self, value): + return FieldDataOps.equal(self._session, self, value) + + def __eq__(self, value): + return FieldDataOps.not_equal(self._session, self, value) + + def __gt__(self, value): + return FieldDataOps.greater_than(self._session, self, value) + + def __ge__(self, value): + return FieldDataOps.greater_than_equal(self._session, self, value) + + # HDF5 field constructors # ======================= @@ -886,18 +1062,17 @@ def apply_index(self, index_to_apply, target=None, in_place=False): class FixedStringField(HDF5Field): def __init__(self, session, group, name=None, write_enabled=False): super().__init__(session, group, name=name, write_enabled=write_enabled) + # TODO: caution; we may want to consider the issues with long-lived field instances getting + # out of sync with their stored counterparts. Maybe a revision number of the stored field + # is required that we can check to see if we are out of date. That or just make this a + # property and have it always look the value up + self._length = self._field.attrs['strlen'] def writeable(self): return FixedStringField(self._session, self._field, write_enabled=True) def create_like(self, group, name, timestamp=None): - ts = self.timestamp if timestamp is None else timestamp - length = self._field.attrs['strlen'] - if isinstance(group, h5py.Group): - fixed_string_field_constructor(self._session, group, name, length, ts, self.chunksize) - return FixedStringField(self._session, group[name], write_enabled=True) - else: - return group.create_fixed_string(name, length, ts, self.chunksize) + return FieldDataOps.fixed_string_field_create_like(self, group, name, timestamp) @property def data(self): @@ -953,7 +1128,6 @@ def apply_index(self, index_to_apply, target=None, in_place=False): return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) - class NumericField(HDF5Field): def __init__(self, session, group, name=None, mem_only=True, write_enabled=False): super().__init__(session, group, name=name, write_enabled=write_enabled) @@ -1107,15 +1281,7 @@ def writeable(self): return CategoricalField(self._session, self._field, write_enabled=True) def create_like(self, group, name, timestamp=None): - ts = self.timestamp if timestamp is None else timestamp - nformat = self._field.attrs['nformat'] if 'nformat' in self._field.attrs else 'int8' - keys = {v: k for k, v in self.keys.items()} - if isinstance(group, h5py.Group): - categorical_field_constructor(self._session, group, name, nformat, keys, ts, - self.chunksize) - return CategoricalField(self, group[name], write_enabled=True) - else: - return group.create_categorical(name, nformat, keys, timestamp, self.chunksize) + return FieldDataOps.categorical_field_create_like(self, group, name, timestamp) @property def data(self): @@ -1175,18 +1341,21 @@ def apply_filter(self, filter_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_filter_to_field(self, filter_to_apply, target, in_place) - def apply_index(self, index_to_apply, dstfld=None): - array = self.data[:] - result = array[index_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld + def apply_index(self, index_to_apply, target=None, in_place=False): + """ + Apply an index to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the reindexed data is written to. + + :param: index_to_apply: a Field or numpy array that contains the indices + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The reindexed field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) def __lt__(self, value): return FieldDataOps.less_than(self._session, self, value) @@ -1197,7 +1366,7 @@ def __le__(self, value): def __eq__(self, value): return FieldDataOps.equal(self._session, self, value) - def __eq__(self, value): + def __ne__(self, value): return FieldDataOps.not_equal(self._session, self, value) def __gt__(self, value): @@ -1215,12 +1384,7 @@ def writeable(self): return TimestampField(self._session, self._field, write_enabled=True) def create_like(self, group, name, timestamp=None): - ts = self.timestamp if timestamp is None else timestamp - if isinstance(group, h5py.Group): - timestamp_field_constructor(self._session, group, name, ts, self.chunksize) - return TimestampField(self._session, group[name], write_enabled=True) - else: - return group.create_timestamp(name, ts, self.chunksize) + return FieldDataOps.timestamp_field_create_like(self, group, name, timestamp) @property def data(self): @@ -1259,18 +1423,21 @@ def apply_filter(self, filter_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_filter_to_field(self, filter_to_apply, target, in_place) - def apply_index(self, index_to_apply, dstfld=None): - array = self.data[:] - result = array[index_to_apply] - dstfld = self if dstfld is None else dstfld - if not dstfld._write_enabled: - dstfld = dstfld.writeable() - if len(dstfld.data) == len(result): - dstfld.data[:] = result - else: - dstfld.data.clear() - dstfld.data.write(result) - return dstfld + def apply_index(self, index_to_apply, target=None, in_place=False): + """ + Apply an index to this field. This operation doesn't modify the field on which it + is called unless 'in_place is set to true'. The user can specify a 'target' field that + the reindexed data is written to. + + :param: index_to_apply: a Field or numpy array that contains the indices + :param: target: if set, this is the field that is written do. This field must be writable. + If 'target' is set, 'in_place' must be False. + :param: in_place: if True, perform the operation destructively on this field. This field + must be writable. If 'in_place' is True, 'target' must be None + :return: The reindexed field. This is a new field instance unless 'target' is set, in which + case it is the target field, or unless 'in_place' is True, in which case it is this field. + """ + return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) def __add__(self, second): return FieldDataOps.numeric_add(self._session, self, second) @@ -2027,6 +2194,23 @@ def indexed_string_create_like(source, group, name, timestamp): else: return group.create_indexed_string(name, ts, source._chunksize) + @staticmethod + def fixed_string_field_create_like(source, group, name, timestamp): + if group is None and name is not None: + raise ValueError("if 'group' is None, 'name' must also be 'None'") + + ts = source.timestamp if timestamp is None else timestamp + length = source._length + + if group is None: + return FixedStringMemField(source._session, length) + + if isinstance(group, h5py.Group): + numeric_field_constructor(source._session, group, name, length, ts, source.chunksize) + return FixedStringField(source._session, group[name], write_enabled=True) + else: + return group.create_fixed_string(name, length, ts) + @staticmethod def numeric_field_create_like(source, group, name, timestamp): if group is None and name is not None: @@ -2042,4 +2226,41 @@ def numeric_field_create_like(source, group, name, timestamp): numeric_field_constructor(source._session, group, name, nformat, ts, source.chunksize) return NumericField(source._session, group[name], write_enabled=True) else: - return group.create_numeric(name, nformat, ts, source.chunksize) + return group.create_numeric(name, nformat, ts) + + @staticmethod + def categorical_field_create_like(source, group, name, timestamp): + if group is None and name is not None: + raise ValueError("if 'group' is None, 'name' must also be 'None'") + + ts = source.timestamp if timestamp is None else timestamp + nformat = source._nformat + keys = source.keys + # TODO: we have to flip the keys until we fix https://github.com/KCL-BMEIS/ExeTera/issues/150 + keys = {v: k for k, v in keys.items()} + + if group is None: + return CategoricalMemField(source._session, nformat, keys) + + if isinstance(group, h5py.Group): + categorical_field_constructor(source._session, group, name, nformat, keys, + ts, source.chunksize) + return CategoricalField(source._session, group[name], write_enabled=True) + else: + return group.create_numeric(name, nformat, keys, ts) + + @staticmethod + def timestamp_field_create_like(source, group, name, timestamp): + if group is None and name is not None: + raise ValueError("if 'group' is None, 'name' must also be 'None'") + + ts = source.timestamp if timestamp is None else timestamp + + if group is None: + return TimestampMemField(source._session) + + if isinstance(group, h5py.Group): + timestamp_field_constructor(source._session, group, name, ts, source.chunksize) + return TimestampField(source._session, group[name], write_enabled=True) + else: + return group.create_numeric(name, ts) diff --git a/tests/test_fields.py b/tests/test_fields.py index 960f5004..8d49f123 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -251,6 +251,18 @@ def test_clear(self): class TestMemoryFieldCreateLike(unittest.TestCase): + + def test_categorical_create_like(self): + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + foo = df.create_categorical('foo', 'int8', {b'a': 0, b'b': 1}) + foo.data.write(np.array([0, 1, 1, 0])) + foo2 = foo.create_like(df, 'foo2') + foo2.data.write(foo) + self.assertListEqual([0, 1, 1, 0], foo2.data[:].tolist()) + def test_numeric_create_like(self): bio = BytesIO() with session.Session() as s: @@ -465,12 +477,12 @@ def test_categorical_remap(self): foo = df.create_categorical('foo', 'int8', {b'a': 1, b'b': 2}) foo.data.write(np.array([1, 2, 2, 1], dtype='int8')) mbar = foo.remap([(1, 0), (2, 1)], {b'a': 0, b'b': 1}) - print(mbar.data[:]) - print(mbar.keys) + self.assertListEqual([0, 1, 1, 0], mbar.data[:].tolist()) + self.assertDictEqual({0: b'a', 1: b'b'}, mbar.keys) bar = mbar.create_like(df, 'bar') bar.data.write(mbar) - print(bar.data[:]) - print(bar.keys) + self.assertListEqual([0, 1, 1, 0], mbar.data[:].tolist()) + self.assertDictEqual({0: b'a', 1: b'b'}, mbar.keys) class TestFieldApplyFilter(unittest.TestCase): @@ -554,6 +566,71 @@ def test_numeric_apply_filter(self): mb = b.apply_filter(filt) self.assertListEqual(expected, mb.data[:].tolist()) + def test_categorical_apply_filter(self): + data = np.array([0, 1, 2, 0, 1, 2, 2, 1, 0], dtype=np.int32) + keys = {b'a': 0, b'b': 1, b'c': 2} + filt = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0], dtype=bool) + expected = [1, 0, 2, 1] + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_categorical('foo', 'int8', keys) + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.apply_filter(filt, in_place=True) + self.assertListEqual(expected, f.data[:].tolist()) + + mf = fields.CategoricalMemField(s, 'int8', keys) + mf.data.write(data) + self.assertListEqual(data.tolist(), mf.data[:].tolist()) + + mf.apply_filter(filt, in_place=True) + self.assertListEqual(expected, mf.data[:].tolist()) + + b = df.create_categorical('bar', 'int8', keys) + b.data.write(data) + self.assertListEqual(data.tolist(), b.data[:].tolist()) + + mb = b.apply_filter(filt) + self.assertListEqual(expected, mb.data[:].tolist()) + + def test_timestamp_apply_filter(self): + from datetime import datetime as D + data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11), + D(2110, 11, 1), D(2002, 3, 3), D(2018, 2, 28), D(2400, 9, 1)] + data = np.asarray([d.timestamp() for d in data], dtype=np.float64) + filt = np.array([0, 1, 0, 1, 0, 1, 0, 1], dtype=bool) + expected = data[filt].tolist() + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_timestamp('foo') + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.apply_filter(filt, in_place=True) + self.assertListEqual(expected, f.data[:].tolist()) + + mf = fields.TimestampMemField(s) + mf.data.write(data) + self.assertListEqual(data.tolist(), mf.data[:].tolist()) + + mf.apply_filter(filt, in_place=True) + self.assertListEqual(expected, mf.data[:].tolist()) + + b = df.create_timestamp('bar') + b.data.write(data) + self.assertListEqual(data.tolist(), b.data[:].tolist()) + + mb = b.apply_filter(filt) + self.assertListEqual(expected, mb.data[:].tolist()) + + class TestFieldApplyIndex(unittest.TestCase): @@ -607,6 +684,35 @@ def test_indexed_string_apply_index(self): self.assertListEqual(expected_filt_values, mb.values[:].tolist()) self.assertListEqual(expected_filt_data, mb.data[:]) + def test_fixed_string_apply_index(self): + data = np.array([b'a', b'bb', b'ccc', b'dddd', b'eeee', b'fff', b'gg', b'h'], dtype='S4') + indices = np.array([7, 0, 6, 1, 5, 2, 4, 3], dtype=np.int32) + expected = [b'h', b'a', b'gg', b'bb', b'fff', b'ccc', b'eeee', b'dddd'] + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_fixed_string('foo', 4) + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.apply_index(indices, in_place=True) + self.assertListEqual(expected, f.data[:].tolist()) + + mf = fields.FixedStringMemField(s, 4) + mf.data.write(data) + self.assertListEqual(data.tolist(), mf.data[:].tolist()) + + mf.apply_index(indices, in_place=True) + self.assertListEqual(expected, mf.data[:].tolist()) + + b = df.create_fixed_string('bar', 4) + b.data.write(data) + self.assertListEqual(data.tolist(), b.data[:].tolist()) + + mb = b.apply_index(indices) + self.assertListEqual(expected, mb.data[:].tolist()) + def test_numeric_apply_index(self): data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9], dtype='int32') indices = np.array([8, 0, 7, 1, 6, 2, 5, 3, 4], dtype=np.int32) @@ -636,6 +742,68 @@ def test_numeric_apply_index(self): mb = b.apply_index(indices) self.assertListEqual(expected, mb.data[:].tolist()) + def test_categorical_apply_index(self): + data = np.array([0, 1, 2, 0, 1, 2, 2, 1, 0], dtype=np.int32) + keys = {b'a': 0, b'b': 1, b'c': 2} + indices = np.array([8, 0, 7, 1, 6, 2, 5, 3, 4], dtype=np.int32) + expected = [0, 0, 1, 1, 2, 2, 2, 0, 1] + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_categorical('foo', 'int8', keys) + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.apply_index(indices, in_place=True) + self.assertListEqual(expected, f.data[:].tolist()) + + mf = fields.CategoricalMemField(s, 'int8', keys) + mf.data.write(data) + self.assertListEqual(data.tolist(), mf.data[:].tolist()) + + mf.apply_index(indices, in_place=True) + self.assertListEqual(expected, mf.data[:].tolist()) + + b = df.create_categorical('bar', 'int8', keys) + b.data.write(data) + self.assertListEqual(data.tolist(), b.data[:].tolist()) + + mb = b.apply_index(indices) + self.assertListEqual(expected, mb.data[:].tolist()) + + def test_timestamp_apply_index(self): + from datetime import datetime as D + data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11), + D(2110, 11, 1), D(2002, 3, 3), D(2018, 2, 28), D(2400, 9, 1)] + data = np.asarray([d.timestamp() for d in data], dtype=np.float64) + indices = np.array([7, 0, 6, 1, 5, 2, 4, 3], dtype=np.int32) + expected = data[indices].tolist() + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_timestamp('foo', 'int32') + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.apply_index(indices, in_place=True) + self.assertListEqual(expected, f.data[:].tolist()) + + mf = fields.TimestampMemField(s) + mf.data.write(data) + self.assertListEqual(data.tolist(), mf.data[:].tolist()) + + mf.apply_index(indices, in_place=True) + self.assertListEqual(expected, mf.data[:].tolist()) + + b = df.create_timestamp('bar') + b.data.write(data) + self.assertListEqual(data.tolist(), b.data[:].tolist()) + + mb = b.apply_index(indices) + self.assertListEqual(expected, mb.data[:].tolist()) + class TestFieldCreateLike(unittest.TestCase): @@ -654,6 +822,21 @@ def test_indexed_string_field_create_like(self): self.assertIsInstance(g, fields.IndexedStringMemField) self.assertEqual(0, len(g.data)) + def test_fixed_string_field_create_like(self): + data = np.asarray([b'a', b'bb', b'ccc', b'dddd'], dtype='S4') + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_fixed_string('foo', 4) + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.create_like(None, None) + self.assertIsInstance(g, fields.FixedStringMemField) + self.assertEqual(0, len(g.data)) + def test_numeric_field_create_like(self): data = np.asarray([1, 2, 3, 4], dtype=np.int32) @@ -668,3 +851,36 @@ def test_numeric_field_create_like(self): g = f.create_like(None, None) self.assertIsInstance(g, fields.NumericMemField) self.assertEqual(0, len(g.data)) + + def test_categorical_field_create_like(self): + data = np.asarray([0, 1, 1, 0], dtype=np.int8) + key = {b'a': 0, b'b': 1} + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_categorical('foo', 'int8', key) + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.create_like(None, None) + self.assertIsInstance(g, fields.CategoricalMemField) + self.assertEqual(0, len(g.data)) + + def test_timestamp_field_create_like(self): + from datetime import datetime as D + data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11)] + data = np.asarray([d.timestamp() for d in data], dtype=np.float64) + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_timestamp('foo') + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.create_like(None, None) + self.assertIsInstance(g, fields.TimestampMemField) + self.assertEqual(0, len(g.data)) From 955aeded53e24f56f975a82c783070271ff550e0 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 16 Apr 2021 14:05:03 +0100 Subject: [PATCH 064/181] categloric field.keys will return byte key as string, thus minor change on the unittest --- tests/test_dataset.py | 2 +- tests/test_session.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 6997da3a..380e78da 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -138,7 +138,7 @@ def test_dataframe_create_with_dataframe(self): self.assertListEqual(ccontents1.tolist(), df1['c_foo'].data[:].tolist()) self.assertListEqual(ccontents2.tolist(), df2['c_foo'].data[:].tolist()) self.assertDictEqual({1: 'a', 2: 'b'}, df1['c_foo'].keys) - self.assertDictEqual({1: b'a', 2: b'b'}, df2['c_foo'].keys) + self.assertDictEqual({1: 'a', 2: 'b'}, df2['c_foo'].keys) self.assertListEqual(ncontents1.tolist(), df1['n_foo'].data[:].tolist()) self.assertListEqual(ncontents1.tolist(), df2['n_foo'].data[:].tolist()) diff --git a/tests/test_session.py b/tests/test_session.py index 21d0e530..72551dc2 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -47,7 +47,7 @@ def test_create_then_load_categorical(self): with session.Session() as s: with h5py.File(bio, 'w') as src: df = src.create_group('df') - f = s.create_categorical(df, 'foo', 'int8', {'a': 1, 'b': 2}) + f = s.create_categorical(df, 'foo', 'int8', {b'a': 1, b'b': 2}) f.data.write(np.array(contents)) with session.Session() as s: @@ -94,7 +94,7 @@ def test_create_then_load_categorical(self): with session.Session() as s: src = s.open_dataset(bio, 'w', 'src') df = src.create_dataframe('df') - f = s.create_categorical(df, 'foo', 'int8', {'a': 1, 'b': 2}) + f = s.create_categorical(df, 'foo', 'int8', {b'a': 1, b'b': 2}) f.data.write(np.array([1, 2, 1, 2])) with session.Session() as s: From 039d8ee20ea6c4bf8ed57743312780311a3b7aed Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 16 Apr 2021 15:47:31 +0100 Subject: [PATCH 065/181] solved the byte to string issue, problem is dof python 3.7 and 3.8 --- tests/test_dataset.py | 4 ++-- tests/test_session.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 380e78da..9e37a7a8 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -137,8 +137,8 @@ def test_dataframe_create_with_dataframe(self): df2['c_foo'].data[:] = ccontents2 self.assertListEqual(ccontents1.tolist(), df1['c_foo'].data[:].tolist()) self.assertListEqual(ccontents2.tolist(), df2['c_foo'].data[:].tolist()) - self.assertDictEqual({1: 'a', 2: 'b'}, df1['c_foo'].keys) - self.assertDictEqual({1: 'a', 2: 'b'}, df2['c_foo'].keys) + self.assertDictEqual({1: b'a', 2: b'b'}, df1['c_foo'].keys) + self.assertDictEqual({1: b'a', 2: b'b'}, df2['c_foo'].keys) self.assertListEqual(ncontents1.tolist(), df1['n_foo'].data[:].tolist()) self.assertListEqual(ncontents1.tolist(), df2['n_foo'].data[:].tolist()) diff --git a/tests/test_session.py b/tests/test_session.py index 72551dc2..02ddb6a3 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -54,7 +54,7 @@ def test_create_then_load_categorical(self): with h5py.File(bio, 'r') as src: f = s.get(src['df']['foo']) self.assertListEqual(contents, f.data[:].tolist()) - self.assertDictEqual({1: 'a', 2: 'b'}, f.keys) + self.assertDictEqual({1: b'a', 2: b'b'}, f.keys) def test_create_then_load_numeric(self): bio = BytesIO() @@ -100,7 +100,7 @@ def test_create_then_load_categorical(self): with session.Session() as s: src = s.open_dataset(bio, 'r', 'src') f = s.get(src['df']['foo']) - self.assertDictEqual({1: 'a', 2: 'b'}, f.keys) + self.assertDictEqual({1: b'a', 2: b'b'}, f.keys) def test_create_new_then_load(self): bio1 = BytesIO() From 547bb8814e65a6fe04e90dd6d32cfdc96c190d5d Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Fri, 16 Apr 2021 16:35:16 +0100 Subject: [PATCH 066/181] Miscellaneous field fixes; fixed issues with dataframe apply_filter / apply_index --- exetera/core/dataframe.py | 13 ++++++------- exetera/core/fields.py | 6 +++--- tests/test_dataframe.py | 11 ++++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 6bdb3bcc..73b3c713 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -214,12 +214,12 @@ def apply_filter(self, filter_to_apply, ddf=None): if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") for name, field in self._columns.items(): - newfld = field.create_like(ddf, field.name[field.name.index('/', 1)+1:]) - ddf.add(field.apply_filter_to_indexed_field(filter_to_apply, dstfld=newfld), name=name) + newfld = field.create_like(ddf, name) + field.apply_index(filter_to_apply, target=newfld) return ddf else: for field in self._columns.values(): - field.apply_filter_to_indexed_field(filter_to_apply) + field.apply_filter(filter_to_apply, in_place=True) return self def apply_index(self, index_to_apply, ddf=None): @@ -234,11 +234,10 @@ def apply_index(self, index_to_apply, ddf=None): if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") for name, field in self._columns.items(): - newfld = field.create_like(ddf, field.name[field.name.index('/', 1)+1:]) - idx = field.apply_index(index_to_apply, dstfld=newfld) - ddf.add(idx, name=name) + newfld = field.create_like(ddf, name) + field.apply_index(index_to_apply, target=newfld) return ddf else: for field in self._columns.values(): - field.apply_index(index_to_apply) + field.apply_index(index_to_apply, in_place=True) return self diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 9deeea1b..7ba7865b 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -2189,10 +2189,10 @@ def indexed_string_create_like(source, group, name, timestamp): return IndexedStringMemField(source._session, source.chunksize) if isinstance(group, h5py.Group): - indexed_string_field_constructor(source._session, group, name, ts, source._chunksize) + indexed_string_field_constructor(source._session, group, name, ts, source.chunksize) return IndexedStringField(source._session, group[name], write_enabled=True) else: - return group.create_indexed_string(name, ts, source._chunksize) + return group.create_indexed_string(name, ts, source.chunksize) @staticmethod def fixed_string_field_create_like(source, group, name, timestamp): @@ -2247,7 +2247,7 @@ def categorical_field_create_like(source, group, name, timestamp): ts, source.chunksize) return CategoricalField(source._session, group[name], write_enabled=True) else: - return group.create_numeric(name, nformat, keys, ts) + return group.create_categorical(name, nformat, keys, ts) @staticmethod def timestamp_field_create_like(source, group, name, timestamp): diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 32cabad2..569503c5 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -73,10 +73,11 @@ def test_dataframe_ops(self): index = np.array([4, 3, 2, 1, 0]) ddf = dst.create_dataframe('dst2') df.apply_index(index, ddf) - self.assertEqual([1, 2, 3, 4, 5], ddf.get_field('numf').data[:].tolist()) - self.assertEqual([b'a', b'b', b'c', b'd', b'e'], ddf.get_field('fst').data[:].tolist()) + self.assertEqual([1, 2, 3, 4, 5], ddf['numf'].data[:].tolist()) + self.assertEqual([b'a', b'b', b'c', b'd', b'e'], ddf['fst'].data[:].tolist()) filter_to_apply = np.array([True, True, False, False, True]) - df.apply_filter_to_indexed_field(filter_to_apply) - self.assertEqual([5, 4, 1], df.get_field('numf').data[:].tolist()) - self.assertEqual([b'e', b'd', b'a'], df.get_field('fst').data[:].tolist()) + ddf = dst.create_dataframe('dst3') + df.apply_filter(filter_to_apply, ddf) + self.assertEqual([5, 4, 1], ddf['numf'].data[:].tolist()) + self.assertEqual([b'e', b'd', b'a'], ddf['fst'].data[:].tolist()) From 700635f62e04399693f11469fef3607ce9a6e7bd Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Fri, 16 Apr 2021 17:55:07 +0100 Subject: [PATCH 067/181] Moving most binary op logic out into a static method in FieldDataOps --- exetera/core/fields.py | 241 +++++++++-------------------------------- 1 file changed, 54 insertions(+), 187 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 7ba7865b..579d5129 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -1777,8 +1777,8 @@ def dtype_to_str(dtype): class FieldDataOps: - @classmethod - def numeric_add(cls, session, first, second): + @staticmethod + def _binary_op(session, first, second, function): if isinstance(first, Field): first_data = first.data[:] else: @@ -1789,95 +1789,52 @@ def numeric_add(cls, session, first, second): else: second_data = second - r = first_data + second_data + r = function(first_data, second_data) f = NumericMemField(session, dtype_to_str(r.dtype)) f.data.write(r) return f @classmethod - def numeric_sub(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first + def numeric_add(cls, session, first, second): + def function_add(first, second): + return first + second - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second + return cls._binary_op(session, first, second, function_add) - r = first_data - second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + @classmethod + def numeric_sub(cls, session, first, second): + def function_sub(first, second): + return first - second + + return cls._binary_op(session, first, second, function_sub) @classmethod def numeric_mul(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first + def function_mul(first, second): + return first * second - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second - - r = first_data * second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_mul) @classmethod def numeric_truediv(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first + def function_truediv(first, second): + return first / second - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second - - r = first_data / second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_truediv) @classmethod def numeric_floordiv(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first + def function_floordiv(first, second): + return first // second - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second - - r = first_data // second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_floordiv) @classmethod def numeric_mod(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first + def function_mod(first, second): + return first % second - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second - - r = first_data % second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_mod) @classmethod def numeric_divmod(cls, session, first, second): @@ -1900,156 +1857,66 @@ def numeric_divmod(cls, session, first, second): @classmethod def numeric_and(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first + def function_and(first, second): + return first & second - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second - - r = first_data & second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_and) @classmethod def numeric_xor(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first + def function_xor(first, second): + return first ^ second - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second - - r = first_data ^ second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_xor) @classmethod def numeric_or(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first - - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second + def function_or(first, second): + return first | second - r = first_data | second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_or) @classmethod def less_than(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first - - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second + def function_less_than(first, second): + return first < second - r = first_data < second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_less_than) @classmethod def less_than_equal(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first - - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second + def function_less_than_equal(first, second): + return first <= second - r = first_data <= second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_less_than_equal) @classmethod def equal(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first + def function_equal(first, second): + return first == second - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second - - r = first_data == second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_equal) @classmethod def not_equal(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first + def function_not_equal(first, second): + return first != second - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second - - r = first_data != second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_not_equal) @classmethod def greater_than(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first + def function_greater_than(first, second): + return first > second - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second - - r = first_data > second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_greater_than) @classmethod def greater_than_equal(cls, session, first, second): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first - - if isinstance(second, Field): - second_data = second.data[:] - else: - second_data = second + def function_greater_than_equal(first, second): + return first >= second - r = first_data >= second_data - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f + return cls._binary_op(session, first, second, function_greater_than_equal) @staticmethod def apply_filter_to_indexed_field(source, filter_to_apply, target=None, in_place=False): @@ -2127,7 +1994,7 @@ def apply_index_to_indexed_field(source, index_to_apply, target=None, in_place=F @staticmethod - def apply_filter_to_field(source, filter_to_apply, target=None, in_place=None): + def apply_filter_to_field(source, filter_to_apply, target=None, in_place=False): if in_place is True and target is not None: raise ValueError("if 'in_place is True, 'target' must be None") @@ -2153,7 +2020,7 @@ def apply_filter_to_field(source, filter_to_apply, target=None, in_place=None): return mem_field @staticmethod - def apply_index_to_field(source, index_to_apply, target=None, in_place=None): + def apply_index_to_field(source, index_to_apply, target=None, in_place=False): if in_place is True and target is not None: raise ValueError("if 'in_place is True, 'target' must be None") From b631932d6faf46d73ad67a06ffc4e613b0050b8e Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sat, 17 Apr 2021 10:28:25 +0100 Subject: [PATCH 068/181] Dataframe copy, move and drop operations have been moved out of the DataFrame static methods as python doesn't support static and instance method name overloading (my bad) --- exetera/core/abstract_types.py | 2 +- exetera/core/dataset.py | 136 ++++++++++++++++----------------- tests/test_dataset.py | 34 ++++----- 3 files changed, 82 insertions(+), 90 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index d981be6b..75b15b9b 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -81,7 +81,7 @@ def close(self): raise NotImplementedError() @abstractmethod - def add(self, field): + def copy(self, field): raise NotImplementedError() @abstractmethod diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 13627d8a..90df624f 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -81,7 +81,7 @@ def create_dataframe(self, name, dataframe: DataFrame = None): self._dataframes[name] = _dataframe return _dataframe - def add(self, dataframe): + def copy(self, dataframe, name): """ Add an existing dataframe (from other dataset) to this dataset, write the existing group attributes and HDF5 datasets to this dataset. @@ -90,18 +90,19 @@ def add(self, dataframe): :param name: optional- change the dataframe name. :return: None if the operation is successful; otherwise throw Error. """ - dname = dataframe.name - self._file.create_group(dname) - h5group = self._file[dname] - _dataframe = edf.HDF5DataFrame(self, dname, h5group) - for k, v in dataframe.items(): - f = v.create_like(_dataframe, k) - if f.indexed: - f.indices.write(v.indices[:]) - f.values.write(v.values[:]) - else: - f.data.write(v.data[:]) - self._dataframes[dname] = _dataframe + copy(dataframe, self, name) + # dname = dataframe.name + # self._file.create_group(dname) + # h5group = self._file[dname] + # _dataframe = edf.HDF5DataFrame(self, dname, h5group) + # for k, v in dataframe.items(): + # f = v.create_like(_dataframe, k) + # if f.indexed: + # f.indices.write(v.indices[:]) + # f.values.write(v.values[:]) + # else: + # f.data.write(v.data[:]) + # self._dataframes[dname] = _dataframe def __contains__(self, name: str): """ @@ -149,25 +150,11 @@ def get_dataframe(self, name: str): """ self.__getitem__(name) - # def get_name(self, dataframe: DataFrame): - # """ - # If the dataframe exist in this dataset, return the name; otherwise return None. - # - # :param dataframe: The dataframe instance to find the name. - # :return: name (str) of the dataframe or None if dataframe not found in this dataset. - # """ - # if not isinstance(dataframe, edf.DataFrame): - # raise TypeError("The field argument must be a DataFrame object.") - # for name, v in self._dataframes.items(): - # if id(dataframe) == id(v): - # return name - # return None - def __setitem__(self, name: str, dataframe: DataFrame): """ Add an existing dataframe (from other dataset) to this dataset, the existing dataframe can from: 1) this dataset, so perform a 'rename' operation, or; - 2) another dataset, so perform an 'add' or 'replace' operation + 2) another dataset, so perform a copy operation :param name: The name of the dataframe to store in this dataset. :param dataframe: The dataframe instance to store in this dataset. @@ -183,11 +170,11 @@ def __setitem__(self, name: str, dataframe: DataFrame): dataframe.name = name self._file.move(dataframe.h5group.name, name) else: # new dataframe from another dataset - if self._dataframes.__contains__(name): - self.__delitem__(name) - dataframe.name = name - self.add(dataframe) - + # if self._dataframes.__contains__(name): + # self.__delitem__(name) + # dataframe.name = name + copy(dataframe, self, name) + # self.add(dataframe) def __delitem__(self, name: str): """ @@ -239,47 +226,52 @@ def __len__(self): """Return the number of dataframes stored in this dataset.""" return len(self._dataframes) - @staticmethod - def copy(dataframe: DataFrame, dataset: Dataset, name: str): - """ - Copy dataframe to another dataset via HDF5DataFrame.copy(ds1['df1'], ds2, 'df1']) - :param dataframe: The dataframe to copy. - :param dataset: The destination dataset. - :param name: The name of dataframe in destination dataset. - """ - dataset._file.create_group(name) - h5group = dataset._file[name] - _dataframe = edf.HDF5DataFrame(dataset, name, h5group) - for k, v in dataframe.items(): - f = v.create_like(_dataframe, k) - if f.indexed: - f.indices.write(v.indices[:]) - f.values.write(v.values[:]) - else: - f.data.write(v.data[:]) - dataset._dataframes[name] = _dataframe - - @staticmethod - def drop(dataframe: DataFrame): - """ - Delete a dataframe by HDF5DataFrame.drop(ds['df1']). +def copy(dataframe: DataFrame, dataset: Dataset, name: str): + """ + Copy dataframe to another dataset via HDF5DataFrame.copy(ds1['df1'], ds2, 'df1']) - :param dataframe: The dataframe to delete. - """ - dataframe._dataset.delete_dataframe(dataframe) + :param dataframe: The dataframe to copy. + :param dataset: The destination dataset. + :param name: The name of dataframe in destination dataset. + """ + if name in dataset: + raise ValueError("A dataframe with the the name {} already exists in the " + "destination dataset".format(name)) - @staticmethod - def move(dataframe: DataFrame, dataset: Dataset, name:str): - """ - Move a dataframe to another dataset via HDF5DataFrame.move(ds1['df1'], ds2, 'df1']). - If move within the same dataset, e.g. HDF5DataFrame.move(ds1['df1'], ds1, 'df2']), function as a rename for both - dataframe and HDF5Group. However, to + # TODO: + h5group = dataset._file.create_group(name) - :param dataframe: The dataframe to copy. - :param dataset: The destination dataset. - :param name: The name of dataframe in destination dataset. - """ - HDF5Dataset.copy(dataframe, dataset, name) - HDF5Dataset.drop(dataframe) + _dataframe = edf.HDF5DataFrame(dataset, name, h5group) + for k, v in dataframe.items(): + f = v.create_like(_dataframe, k) + if f.indexed: + f.indices.write(v.indices[:]) + f.values.write(v.values[:]) + else: + f.data.write(v.data[:]) + dataset._dataframes[name] = _dataframe + + +def drop(dataframe: DataFrame): + """ + Delete a dataframe by HDF5DataFrame.drop(ds['df1']). + + :param dataframe: The dataframe to delete. + """ + dataframe._dataset.delete_dataframe(dataframe) + + +def move(dataframe: DataFrame, dataset: Dataset, name:str): + """ + Move a dataframe to another dataset via HDF5DataFrame.move(ds1['df1'], ds2, 'df1']). + If move within the same dataset, e.g. HDF5DataFrame.move(ds1['df1'], ds1, 'df2']), function as a rename for both + dataframe and HDF5Group. However, to + + :param dataframe: The dataframe to copy. + :param dataset: The destination dataset. + :param name: The name of dataframe in destination dataset. + """ + copy(dataframe, dataset, name) + drop(dataframe) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 9e37a7a8..01b9728c 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -3,10 +3,10 @@ import h5py import numpy as np -from exetera.core import session,fields +from exetera.core import session, fields from exetera.core.abstract_types import DataFrame from io import BytesIO -from exetera.core.dataset import HDF5Dataset +from exetera.core.dataset import HDF5Dataset, copy, drop, move class TestDataSet(unittest.TestCase): @@ -56,7 +56,7 @@ def test_dataset_init_with_data(self): fs = df2.create_fixed_string('fs', 1) fs.data.write([b'a', b'b', b'c', b'd']) - dst.add(df2) + dst.copy(df2, 'df2') self.assertTrue(isinstance(dst['df2'], DataFrame)) self.assertEqual([b'a', b'b', b'c', b'd'], dst['df2']['fs'].data[:].tolist()) @@ -64,30 +64,30 @@ def test_dataset_init_with_data(self): self.assertTrue(len(dst.keys()) == 1) self.assertTrue(len(dst._file.keys()) == 1) - # set dataframe - dst['grp1'] = df2 - self.assertTrue(isinstance(dst['grp1'], DataFrame)) - self.assertEqual([b'a', b'b', b'c', b'd'], dst['grp1']['fs'].data[:].tolist()) + # set dataframe (this is a copy between datasets + dst['df3'] = df2 + self.assertTrue(isinstance(dst['df3'], DataFrame)) + self.assertEqual([b'a', b'b', b'c', b'd'], dst['df3']['fs'].data[:].tolist()) - def test_dataste_static_func(self): + def test_dataset_static_func(self): bio = BytesIO() bio2 = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, 'r+', 'dst') df = dst.create_dataframe('df') - num1 = df.create_numeric('num','uint32') - num1.data.write([1,2,3,4]) + num1 = df.create_numeric('num', 'uint32') + num1.data.write([1, 2, 3, 4]) - ds2 = s.open_dataset(bio2,'r+','ds2') - HDF5Dataset.copy(df,ds2,'df2') + ds2 = s.open_dataset(bio2, 'r+', 'ds2') + copy(df, ds2, 'df2') print(type(ds2['df2'])) - self.assertTrue(isinstance(ds2['df2'],DataFrame)) - self.assertTrue(isinstance(ds2['df2']['num'],fields.Field)) + self.assertTrue(isinstance(ds2['df2'], DataFrame)) + self.assertTrue(isinstance(ds2['df2']['num'], fields.Field)) - HDF5Dataset.drop(ds2['df2']) - self.assertTrue(len(ds2)==0) + drop(ds2['df2']) + self.assertTrue(len(ds2) == 0) - HDF5Dataset.move(df,ds2,'df2') + move(df, ds2, 'df2') self.assertTrue(len(dst) == 0) self.assertTrue(len(ds2) == 1) From 4804417c05224b13d09735541b0b5cfc3ff08726 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sat, 17 Apr 2021 11:49:17 +0100 Subject: [PATCH 069/181] Fixing accidental introduction of CRLF to abstract_types --- exetera/core/abstract_types.py | 891 +++++++++++++++++---------------- exetera/core/dataset.py | 23 +- 2 files changed, 461 insertions(+), 453 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index 75b15b9b..b4b73c47 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -1,440 +1,451 @@ -# Copyright 2020 KCL-BMEIS - King's College London -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod -from datetime import datetime, timezone - - -class Field(ABC): - - @property - @abstractmethod - def name(self): - raise NotImplementedError() - - @property - @abstractmethod - def timestamp(self): - raise NotImplementedError() - - @property - @abstractmethod - def chunksize(self): - raise NotImplementedError() - - @abstractmethod - def writeable(self): - raise NotImplementedError() - - @abstractmethod - def create_like(self, group, name, timestamp=None): - raise NotImplementedError() - - @property - @abstractmethod - def is_sorted(self): - raise NotImplementedError() - - @property - @abstractmethod - def indexed(self): - raise NotImplementedError() - - @property - @abstractmethod - def data(self): - raise NotImplementedError() - - @abstractmethod - def __bool__(self): - raise NotImplementedError() - - @abstractmethod - def __len__(self): - raise NotImplementedError() - - @abstractmethod - def get_spans(self): - raise NotImplementedError() - - -class Dataset(ABC): - """ - DataSet is a container of dataframes - """ - - @property - @abstractmethod - def session(self): - raise NotImplementedError() - - @abstractmethod - def close(self): - raise NotImplementedError() - - @abstractmethod - def copy(self, field): - raise NotImplementedError() - - @abstractmethod - def __contains__(self, name): - raise NotImplementedError() - - @abstractmethod - def contains_dataframe(self, dataframe): - raise NotImplementedError() - - @abstractmethod - def __getitem__(self, name): - raise NotImplementedError() - - @abstractmethod - def get_dataframe(self, name): - raise NotImplementedError() - - # @abstractmethod - # def get_name(self, dataframe): - # raise NotImplementedError() - - @abstractmethod - def __setitem__(self, name, dataframe): - raise NotImplementedError() - - @abstractmethod - def __delitem__(self, name): - raise NotImplementedError() - - @abstractmethod - def delete_dataframe(self, dataframe): - raise NotImplementedError() - - @abstractmethod - def __iter__(self): - raise NotImplementedError() - - @abstractmethod - def __next__(self): - raise NotImplementedError() - - @abstractmethod - def __len__(self): - raise NotImplementedError() - - -class DataFrame(ABC): - """ - DataFrame is a table of data that contains a list of Fields (columns) - """ - - @abstractmethod - def add(self, field): - raise NotImplementedError() - - @abstractmethod - def create_group(self, name): - raise NotImplementedError() - - @abstractmethod - def create_numeric(self, name, nformat, timestamp=None, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def create_indexed_string(self, name, timestamp=None, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def create_fixed_string(self, name, length, timestamp=None, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def create_categorical(self, name, nformat, key, timestamp=None, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def create_timestamp(self, name, timestamp=None, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def __contains__(self, name): - raise NotImplementedError() - - @abstractmethod - def contains_field(self, field): - raise NotImplementedError() - - @abstractmethod - def __getitem__(self, name): - raise NotImplementedError() - - @abstractmethod - def get_field(self, name): - raise NotImplementedError() - - # @abstractmethod - # def get_name(self, field): - # raise NotImplementedError() - - @abstractmethod - def __setitem__(self, name, field): - raise NotImplementedError() - - @abstractmethod - def __delitem__(self, name): - raise NotImplementedError() - - @abstractmethod - def delete_field(self, field): - raise NotImplementedError() - - @abstractmethod - def keys(self): - raise NotImplementedError() - - @abstractmethod - def values(self): - raise NotImplementedError() - - @abstractmethod - def items(self): - raise NotImplementedError() - - @abstractmethod - def __iter__(self): - raise NotImplementedError() - - @abstractmethod - def __next__(self): - raise NotImplementedError() - - @abstractmethod - def __len__(self): - raise NotImplementedError() - - @abstractmethod - def get_spans(self): - raise NotImplementedError() - - @abstractmethod - def apply_filter(self, filter_to_apply, ddf=None): - raise NotImplementedError() - - @abstractmethod - def apply_index(self, index_to_apply, ddf=None): - raise NotImplementedError() - - -class AbstractSession(ABC): - - @abstractmethod - def __enter__(self): - raise NotImplementedError() - - @abstractmethod - def __exit__(self, etype, evalue, etraceback): - raise NotImplementedError() - - @abstractmethod - def open_dataset(self, dataset_path, mode, name): - raise NotImplementedError() - - @abstractmethod - def close_dataset(self, name): - raise NotImplementedError() - - @abstractmethod - def list_datasets(self): - raise NotImplementedError() - - @abstractmethod - def get_dataset(self, name): - raise NotImplementedError() - - @abstractmethod - def close(self): - raise NotImplementedError() - - @abstractmethod - def get_shared_index(self, keys): - raise NotImplementedError() - - @abstractmethod - def set_timestamp(self, timestamp=str(datetime.now(timezone.utc))): - raise NotImplementedError() - - @abstractmethod - def sort_on(self, src_group, dest_group, keys, timestamp, - write_mode='write', verbose=True): - raise NotImplementedError() - - @abstractmethod - def dataset_sort_index(self, sort_indices, index=None): - raise NotImplementedError() - - @abstractmethod - def apply_filter(self, filter_to_apply, src, dest=None): - raise NotImplementedError() - - @abstractmethod - def apply_index(self, index_to_apply, src, dest=None): - raise NotImplementedError() - - @abstractmethod - def distinct(self, field=None, fields=None, filter=None): - raise NotImplementedError() - - @abstractmethod - def get_spans(self, field=None, fields=None): - raise NotImplementedError() - - @abstractmethod - def apply_spans_index_of_min(self, spans, target, dest=None): - raise NotImplementedError() - - @abstractmethod - def apply_spans_index_of_max(self, spans, target, dest=None): - raise NotImplementedError() - - @abstractmethod - def apply_spans_index_of_first(self, spans, dest=None): - raise NotImplementedError() - - @abstractmethod - def apply_spans_index_of_last(self, spans, dest=None): - raise NotImplementedError() - - @abstractmethod - def apply_spans_count(self, spans, dest=None): - raise NotImplementedError() - - @abstractmethod - def apply_spans_min(self, spans, target, dest=None): - raise NotImplementedError() - - @abstractmethod - def apply_spans_max(self, spans, target, dest=None): - raise NotImplementedError() - - @abstractmethod - def apply_spans_first(self, spans, target, dest=None): - raise NotImplementedError() - - @abstractmethod - def apply_spans_last(self, spans, target, dest=None): - raise NotImplementedError() - - @abstractmethod - def apply_spans_concat(self, spans, target, dest, - src_chunksize=None, dest_chunksize=None, chunksize_mult=None): - raise NotImplementedError() - - @abstractmethod - def aggregate_count(self, index, dest=None): - raise NotImplementedError() - - @abstractmethod - def aggregate_first(self, index, target=None, dest=None): - raise NotImplementedError() - - @abstractmethod - def aggregate_last(self, index, target=None, dest=None): - raise NotImplementedError() - - @abstractmethod - def aggregate_min(self, index, target=None, dest=None): - raise NotImplementedError() - - @abstractmethod - def aggregate_max(self, index, target=None, dest=None): - raise NotImplementedError() - - @abstractmethod - def aggregate_custom(self, predicate, index, target=None, dest=None): - raise NotImplementedError() - - @abstractmethod - def join(self, destination_pkey, fkey_indices, values_to_join, - writer=None, fkey_index_spans=None): - raise NotImplementedError() - - @abstractmethod - def predicate_and_join(self, predicate, destination_pkey, fkey_indices, - reader=None, writer=None, fkey_index_spans=None): - raise NotImplementedError() - - @abstractmethod - def get(self, field): - raise NotImplementedError() - - @abstractmethod - def create_like(self, field, dest_group, dest_name, timestamp=None, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def create_indexed_string(self, group, name, timestamp=None, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def create_fixed_string(self, group, name, length, timestamp=None, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def create_categorical(self, group, name, nformat, key, timestamp=None, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def create_numeric(self, group, name, nformat, timestamp=None, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def create_timestamp(self, group, name, timestamp=None, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def get_or_create_group(self, group, name): - raise NotImplementedError() - - @abstractmethod - def chunks(self, length, chunksize=None): - raise NotImplementedError() - - @abstractmethod - def process(self, inputs, outputs, predicate): - raise NotImplementedError() - - @abstractmethod - def get_index(self, target, foreign_key, destination=None): - raise NotImplementedError() - - @abstractmethod - def merge_left(self, left_on, right_on, right_fields=tuple(), right_writers=None): - raise NotImplementedError() - - @abstractmethod - def merge_right(self, left_on, right_on, left_fields=tuple(), left_writers=None): - raise NotImplementedError() - - @abstractmethod - def merge_inner(self, left_on, right_on, - left_fields=None, left_writers=None, - right_fields=None, right_writers=None): - raise NotImplementedError() - - @abstractmethod - def ordered_merge_left(self, left_on, right_on, - right_field_sources=tuple(), left_field_sinks=None, - left_to_right_map=None, left_unique=False, right_unique=False): - raise NotImplementedError() - - @abstractmethod - def ordered_merge_right(self, right_on, left_on, - left_field_sources=tuple(), right_field_sinks=None, - right_to_left_map=None, right_unique=False, left_unique=False): - raise NotImplementedError() +# Copyright 2020 KCL-BMEIS - King's College London +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +from datetime import datetime, timezone + + +class Field(ABC): + + @property + @abstractmethod + def name(self): + raise NotImplementedError() + + @property + @abstractmethod + def timestamp(self): + raise NotImplementedError() + + @property + @abstractmethod + def chunksize(self): + raise NotImplementedError() + + @abstractmethod + def writeable(self): + raise NotImplementedError() + + @abstractmethod + def create_like(self, group, name, timestamp=None): + raise NotImplementedError() + + @property + @abstractmethod + def is_sorted(self): + raise NotImplementedError() + + @property + @abstractmethod + def indexed(self): + raise NotImplementedError() + + @property + @abstractmethod + def data(self): + raise NotImplementedError() + + @abstractmethod + def __bool__(self): + raise NotImplementedError() + + @abstractmethod + def __len__(self): + raise NotImplementedError() + + @abstractmethod + def get_spans(self): + raise NotImplementedError() + + +class Dataset(ABC): + """ + DataSet is a container of dataframes + """ + + @property + @abstractmethod + def session(self): + raise NotImplementedError() + + @abstractmethod + def create_dataframe(self, + name: str, + dataframe: 'DataFrame'): + raise NotImplementedError() + + @abstractmethod + def close(self): + raise NotImplementedError() + + @abstractmethod + def copy(self, + field: 'Field'): + raise NotImplementedError() + + @abstractmethod + def __contains__(self, + name: str): + raise NotImplementedError() + + @abstractmethod + def contains_dataframe(self, + dataframe: 'DataFrame'): + raise NotImplementedError() + + @abstractmethod + def __getitem__(self, + name: str): + raise NotImplementedError() + + @abstractmethod + def get_dataframe(self, + name: str): + raise NotImplementedError() + + @abstractmethod + def __setitem__(self, + name: str, + dataframe: 'DataFrame'): + raise NotImplementedError() + + @abstractmethod + def __delitem__(self, + name: str): + raise NotImplementedError() + + @abstractmethod + def delete_dataframe(self, + dataframe: 'DataFrame'): + raise NotImplementedError() + + @abstractmethod + def __iter__(self): + raise NotImplementedError() + + @abstractmethod + def __next__(self): + raise NotImplementedError() + + @abstractmethod + def __len__(self): + raise NotImplementedError() + + +class DataFrame(ABC): + """ + DataFrame is a table of data that contains a list of Fields (columns) + """ + + @abstractmethod + def add(self, field): + raise NotImplementedError() + + @abstractmethod + def create_group(self, name): + raise NotImplementedError() + + @abstractmethod + def create_numeric(self, name, nformat, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_indexed_string(self, name, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_fixed_string(self, name, length, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_categorical(self, name, nformat, key, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_timestamp(self, name, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def __contains__(self, name): + raise NotImplementedError() + + @abstractmethod + def contains_field(self, field): + raise NotImplementedError() + + @abstractmethod + def __getitem__(self, name): + raise NotImplementedError() + + @abstractmethod + def get_field(self, name): + raise NotImplementedError() + + # @abstractmethod + # def get_name(self, field): + # raise NotImplementedError() + + @abstractmethod + def __setitem__(self, name, field): + raise NotImplementedError() + + @abstractmethod + def __delitem__(self, name): + raise NotImplementedError() + + @abstractmethod + def delete_field(self, field): + raise NotImplementedError() + + @abstractmethod + def keys(self): + raise NotImplementedError() + + @abstractmethod + def values(self): + raise NotImplementedError() + + @abstractmethod + def items(self): + raise NotImplementedError() + + @abstractmethod + def __iter__(self): + raise NotImplementedError() + + @abstractmethod + def __next__(self): + raise NotImplementedError() + + @abstractmethod + def __len__(self): + raise NotImplementedError() + + @abstractmethod + def get_spans(self): + raise NotImplementedError() + + @abstractmethod + def apply_filter(self, filter_to_apply, ddf=None): + raise NotImplementedError() + + @abstractmethod + def apply_index(self, index_to_apply, ddf=None): + raise NotImplementedError() + + +class AbstractSession(ABC): + + @abstractmethod + def __enter__(self): + raise NotImplementedError() + + @abstractmethod + def __exit__(self, etype, evalue, etraceback): + raise NotImplementedError() + + @abstractmethod + def open_dataset(self, dataset_path, mode, name): + raise NotImplementedError() + + @abstractmethod + def close_dataset(self, name): + raise NotImplementedError() + + @abstractmethod + def list_datasets(self): + raise NotImplementedError() + + @abstractmethod + def get_dataset(self, name): + raise NotImplementedError() + + @abstractmethod + def close(self): + raise NotImplementedError() + + @abstractmethod + def get_shared_index(self, keys): + raise NotImplementedError() + + @abstractmethod + def set_timestamp(self, timestamp=str(datetime.now(timezone.utc))): + raise NotImplementedError() + + @abstractmethod + def sort_on(self, src_group, dest_group, keys, timestamp, + write_mode='write', verbose=True): + raise NotImplementedError() + + @abstractmethod + def dataset_sort_index(self, sort_indices, index=None): + raise NotImplementedError() + + @abstractmethod + def apply_filter(self, filter_to_apply, src, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_index(self, index_to_apply, src, dest=None): + raise NotImplementedError() + + @abstractmethod + def distinct(self, field=None, fields=None, filter=None): + raise NotImplementedError() + + @abstractmethod + def get_spans(self, field=None, fields=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_index_of_min(self, spans, target, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_index_of_max(self, spans, target, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_index_of_first(self, spans, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_index_of_last(self, spans, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_count(self, spans, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_min(self, spans, target, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_max(self, spans, target, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_first(self, spans, target, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_last(self, spans, target, dest=None): + raise NotImplementedError() + + @abstractmethod + def apply_spans_concat(self, spans, target, dest, + src_chunksize=None, dest_chunksize=None, chunksize_mult=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_count(self, index, dest=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_first(self, index, target=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_last(self, index, target=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_min(self, index, target=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_max(self, index, target=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def aggregate_custom(self, predicate, index, target=None, dest=None): + raise NotImplementedError() + + @abstractmethod + def join(self, destination_pkey, fkey_indices, values_to_join, + writer=None, fkey_index_spans=None): + raise NotImplementedError() + + @abstractmethod + def predicate_and_join(self, predicate, destination_pkey, fkey_indices, + reader=None, writer=None, fkey_index_spans=None): + raise NotImplementedError() + + @abstractmethod + def get(self, field): + raise NotImplementedError() + + @abstractmethod + def create_like(self, field, dest_group, dest_name, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_indexed_string(self, group, name, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_fixed_string(self, group, name, length, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_categorical(self, group, name, nformat, key, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_numeric(self, group, name, nformat, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def create_timestamp(self, group, name, timestamp=None, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def get_or_create_group(self, group, name): + raise NotImplementedError() + + @abstractmethod + def chunks(self, length, chunksize=None): + raise NotImplementedError() + + @abstractmethod + def process(self, inputs, outputs, predicate): + raise NotImplementedError() + + @abstractmethod + def get_index(self, target, foreign_key, destination=None): + raise NotImplementedError() + + @abstractmethod + def merge_left(self, left_on, right_on, right_fields=tuple(), right_writers=None): + raise NotImplementedError() + + @abstractmethod + def merge_right(self, left_on, right_on, left_fields=tuple(), left_writers=None): + raise NotImplementedError() + + @abstractmethod + def merge_inner(self, left_on, right_on, + left_fields=None, left_writers=None, + right_fields=None, right_writers=None): + raise NotImplementedError() + + @abstractmethod + def ordered_merge_left(self, left_on, right_on, + right_field_sources=tuple(), left_field_sinks=None, + left_to_right_map=None, left_unique=False, right_unique=False): + raise NotImplementedError() + + @abstractmethod + def ordered_merge_right(self, right_on, left_on, + left_field_sources=tuple(), right_field_sinks=None, + right_to_left_map=None, right_unique=False, left_unique=False): + raise NotImplementedError() diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 90df624f..a15a82d3 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -49,10 +49,6 @@ def session(self): """ return self._session - def close(self): - """Close the HDF5 file operations.""" - self._file.close() - def create_dataframe(self, name, dataframe: DataFrame = None): """ Create a group object in HDF5 file and a Exetera dataframe in memory. @@ -81,6 +77,10 @@ def create_dataframe(self, name, dataframe: DataFrame = None): self._dataframes[name] = _dataframe return _dataframe + def close(self): + """Close the HDF5 file operations.""" + self._file.close() + def copy(self, dataframe, name): """ Add an existing dataframe (from other dataset) to this dataset, write the existing group @@ -165,16 +165,14 @@ def __setitem__(self, name: str, dataframe: DataFrame): if not isinstance(dataframe, edf.DataFrame): raise TypeError("The field must be a DataFrame object.") - if dataframe.dataset == self: # rename a dataframe + if dataframe.dataset == self: + # rename a dataframe del self._dataframes[dataframe.name] dataframe.name = name self._file.move(dataframe.h5group.name, name) - else: # new dataframe from another dataset - # if self._dataframes.__contains__(name): - # self.__delitem__(name) - # dataframe.name = name + else: + # new dataframe from another dataset copy(dataframe, self, name) - # self.add(dataframe) def __delitem__(self, name: str): """ @@ -239,10 +237,8 @@ def copy(dataframe: DataFrame, dataset: Dataset, name: str): raise ValueError("A dataframe with the the name {} already exists in the " "destination dataset".format(name)) - # TODO: - h5group = dataset._file.create_group(name) + _dataframe = dataset.create_dataframe(name) - _dataframe = edf.HDF5DataFrame(dataset, name, h5group) for k, v in dataframe.items(): f = v.create_like(_dataframe, k) if f.indexed: @@ -250,6 +246,7 @@ def copy(dataframe: DataFrame, dataset: Dataset, name: str): f.values.write(v.values[:]) else: f.data.write(v.data[:]) + dataset._dataframes[name] = _dataframe From f16cb09c2ce6e06af91bfd24e9cffa3841a17b6a Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sat, 17 Apr 2021 17:22:31 +0100 Subject: [PATCH 070/181] Fixed bug where apply_filter and apply_index weren't returning a field on all code paths; beefed up tests to cover this --- exetera/core/fields.py | 10 ++- exetera/core/operations.py | 1 - tests/test_fields.py | 175 ++++++++++++++++++++++++++++++++++--- 3 files changed, 173 insertions(+), 13 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index c809b57d..b3a697de 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -1923,8 +1923,10 @@ def apply_filter_to_indexed_field(source, filter_to_apply, target=None, in_place if in_place is True and target is not None: raise ValueError("if 'in_place is True, 'target' must be None") + filter_to_apply_ = val.array_from_field_or_lower('filter_to_apply', filter_to_apply) + dest_indices, dest_values = \ - ops.apply_filter_to_index_values(filter_to_apply, + ops.apply_filter_to_index_values(filter_to_apply_, source.indices[:], source.values[:]) if in_place: @@ -1960,8 +1962,10 @@ def apply_index_to_indexed_field(source, index_to_apply, target=None, in_place=F if in_place is True and target is not None: raise ValueError("if 'in_place is True, 'target' must be None") + index_to_apply_ = val.array_from_field_or_lower('index_to_apply', index_to_apply) + dest_indices, dest_values = \ - ops.apply_indices_to_index_values(index_to_apply, + ops.apply_indices_to_index_values(index_to_apply_, source.indices[:], source.values[:]) if in_place: @@ -2014,6 +2018,7 @@ def apply_filter_to_field(source, filter_to_apply, target=None, in_place=False): else: target.data.clear() target.data.write(dest_data) + return target else: mem_field = source.create_like(None, None) mem_field.data.write(dest_data) @@ -2040,6 +2045,7 @@ def apply_index_to_field(source, index_to_apply, target=None, in_place=False): else: target.data.clear() target.data.write(dest_data) + return target else: mem_field = source.create_like(None, None) mem_field.data.write(dest_data) diff --git a/exetera/core/operations.py b/exetera/core/operations.py index 266da42f..389b5ebc 100644 --- a/exetera/core/operations.py +++ b/exetera/core/operations.py @@ -151,7 +151,6 @@ def data_iterator(data_field, chunksize=1 << 20): for v in range(c[0], c[1]): yield data[v] - @njit def apply_filter_to_index_values(index_filter, indices, values): # pass 1 - determine the destination lengths diff --git a/tests/test_fields.py b/tests/test_fields.py index 435d4e4f..88bafcee 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -484,16 +484,34 @@ def test_indexed_string_apply_filter(self): with session.Session() as s: ds = s.open_dataset(bio, 'w', 'ds') df = ds.create_dataframe('df') - f = df.create_indexed_string('foo') + f = df.create_indexed_string('f') f.data.write(data) self.assertListEqual(expected_indices, f.indices[:].tolist()) self.assertListEqual(expected_values, f.values[:].tolist()) self.assertListEqual(data, f.data[:]) - g = f.apply_filter(filt, in_place=True) + ff = f.apply_filter(filt, in_place=True) self.assertListEqual(expected_filt_indices, f.indices[:].tolist()) self.assertListEqual(expected_filt_values, f.values[:].tolist()) self.assertListEqual(expected_filt_data, f.data[:]) + self.assertListEqual(expected_filt_indices, ff.indices[:].tolist()) + self.assertListEqual(expected_filt_values, ff.values[:].tolist()) + self.assertListEqual(expected_filt_data, ff.data[:]) + + g = f.create_like(df, 'g') + g.data.write(data) + fg = f.create_like(df, 'fg') + fgr = g.apply_filter(filt, fg) + self.assertListEqual(expected_filt_indices, fg.indices[:].tolist()) + self.assertListEqual(expected_filt_values, fg.values[:].tolist()) + self.assertListEqual(expected_filt_data, fg.data[:]) + self.assertListEqual(expected_filt_indices, fgr.indices[:].tolist()) + self.assertListEqual(expected_filt_values, fgr.values[:].tolist()) + self.assertListEqual(expected_filt_data, fgr.data[:]) + fh = g.apply_filter(filt) + self.assertListEqual(expected_filt_indices, fh.indices[:].tolist()) + self.assertListEqual(expected_filt_values, fh.values[:].tolist()) + self.assertListEqual(expected_filt_data, fh.data[:]) mf = fields.IndexedStringMemField(s) mf.data.write(data) @@ -517,6 +535,47 @@ def test_indexed_string_apply_filter(self): self.assertListEqual(expected_filt_values, mb.values[:].tolist()) self.assertListEqual(expected_filt_data, mb.data[:]) + def test_fixed_string_apply_filter(self): + data = np.array([b'a', b'bb', b'ccc', b'dddd', b'eeee', b'fff', b'gg', b'h'], dtype='S4') + filt = np.array([0, 1, 0, 1, 0, 1, 0, 1], dtype=bool) + expected = [b'bb', b'dddd', b'fff', b'h'] + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = df.create_fixed_string('foo', 4) + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + ff = f.apply_filter(filt, in_place=True) + self.assertListEqual(expected, f.data[:].tolist()) + self.assertListEqual(expected, ff.data[:].tolist()) + + g = f.create_like(df, 'g') + g.data.write(data) + fg = f.create_like(df, 'fg') + fgr = g.apply_filter(filt, fg) + self.assertListEqual(expected, fg.data[:].tolist()) + self.assertListEqual(expected, fgr.data[:].tolist()) + + fh = g.apply_filter(filt) + self.assertListEqual(expected, fh.data[:].tolist()) + + mf = fields.FixedStringMemField(s, 4) + mf.data.write(data) + self.assertListEqual(data.tolist(), mf.data[:].tolist()) + + mf.apply_filter(filt, in_place=True) + self.assertListEqual(expected, mf.data[:].tolist()) + + b = df.create_fixed_string('bar', 4) + b.data.write(data) + self.assertListEqual(data.tolist(), b.data[:].tolist()) + + mb = b.apply_filter(filt) + self.assertListEqual(expected, mb.data[:].tolist()) + def test_numeric_apply_filter(self): data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=np.int32) filt = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0], dtype=bool) @@ -530,8 +589,19 @@ def test_numeric_apply_filter(self): f.data.write(data) self.assertListEqual(data.tolist(), f.data[:].tolist()) - g = f.apply_filter(filt, in_place=True) + ff = f.apply_filter(filt, in_place=True) self.assertListEqual(expected, f.data[:].tolist()) + self.assertListEqual(expected, ff.data[:].tolist()) + + g = f.create_like(df, 'g') + g.data.write(data) + fg = f.create_like(df, 'fg') + fgr = g.apply_filter(filt, fg) + self.assertListEqual(expected, fg.data[:].tolist()) + self.assertListEqual(expected, fgr.data[:].tolist()) + + fh = g.apply_filter(filt) + self.assertListEqual(expected, fh.data[:].tolist()) mf = fields.NumericMemField(s, 'int32') mf.data.write(data) @@ -561,8 +631,19 @@ def test_categorical_apply_filter(self): f.data.write(data) self.assertListEqual(data.tolist(), f.data[:].tolist()) - g = f.apply_filter(filt, in_place=True) + ff = f.apply_filter(filt, in_place=True) self.assertListEqual(expected, f.data[:].tolist()) + self.assertListEqual(expected, ff.data[:].tolist()) + + g = f.create_like(df, 'g') + g.data.write(data) + fg = f.create_like(df, 'fg') + fgr = g.apply_filter(filt, fg) + self.assertListEqual(expected, fg.data[:].tolist()) + self.assertListEqual(expected, fgr.data[:].tolist()) + + fh = g.apply_filter(filt) + self.assertListEqual(expected, fh.data[:].tolist()) mf = fields.CategoricalMemField(s, 'int8', keys) mf.data.write(data) @@ -594,8 +675,19 @@ def test_timestamp_apply_filter(self): f.data.write(data) self.assertListEqual(data.tolist(), f.data[:].tolist()) - g = f.apply_filter(filt, in_place=True) + ff = f.apply_filter(filt, in_place=True) self.assertListEqual(expected, f.data[:].tolist()) + self.assertListEqual(expected, ff.data[:].tolist()) + + g = f.create_like(df, 'g') + g.data.write(data) + fg = f.create_like(df, 'fg') + fgr = g.apply_filter(filt, fg) + self.assertListEqual(expected, fg.data[:].tolist()) + self.assertListEqual(expected, fgr.data[:].tolist()) + + fh = g.apply_filter(filt) + self.assertListEqual(expected, fh.data[:].tolist()) mf = fields.TimestampMemField(s) mf.data.write(data) @@ -638,10 +730,29 @@ def test_indexed_string_apply_index(self): self.assertListEqual(expected_values, f.values[:].tolist()) self.assertListEqual(data, f.data[:]) - g = f.apply_index(inds, in_place=True) + ff = f.apply_index(inds, in_place=True) self.assertListEqual(expected_filt_indices, f.indices[:].tolist()) self.assertListEqual(expected_filt_values, f.values[:].tolist()) self.assertListEqual(expected_filt_data, f.data[:]) + self.assertListEqual(expected_filt_indices, ff.indices[:].tolist()) + self.assertListEqual(expected_filt_values, ff.values[:].tolist()) + self.assertListEqual(expected_filt_data, ff.data[:]) + + g = f.create_like(df, 'g') + g.data.write(data) + fg = f.create_like(df, 'fg') + fgr = g.apply_index(inds, fg) + self.assertListEqual(expected_filt_indices, fg.indices[:].tolist()) + self.assertListEqual(expected_filt_values, fg.values[:].tolist()) + self.assertListEqual(expected_filt_data, fg.data[:]) + self.assertListEqual(expected_filt_indices, fgr.indices[:].tolist()) + self.assertListEqual(expected_filt_values, fgr.values[:].tolist()) + self.assertListEqual(expected_filt_data, fgr.data[:]) + + fh = g.apply_index(inds) + self.assertListEqual(expected_filt_indices, fh.indices[:].tolist()) + self.assertListEqual(expected_filt_values, fh.values[:].tolist()) + self.assertListEqual(expected_filt_data, fh.data[:]) mf = fields.IndexedStringMemField(s) mf.data.write(data) @@ -677,8 +788,19 @@ def test_fixed_string_apply_index(self): f.data.write(data) self.assertListEqual(data.tolist(), f.data[:].tolist()) - g = f.apply_index(indices, in_place=True) + ff = f.apply_index(indices, in_place=True) self.assertListEqual(expected, f.data[:].tolist()) + self.assertListEqual(expected, ff.data[:].tolist()) + + g = f.create_like(df, 'g') + g.data.write(data) + fg = f.create_like(df, 'fg') + fgr = g.apply_index(indices, fg) + self.assertListEqual(expected, fg.data[:].tolist()) + self.assertListEqual(expected, fgr.data[:].tolist()) + + fh = g.apply_index(indices) + self.assertListEqual(expected, fh.data[:].tolist()) mf = fields.FixedStringMemField(s, 4) mf.data.write(data) @@ -706,8 +828,19 @@ def test_numeric_apply_index(self): f.data.write(data) self.assertListEqual(data.tolist(), f.data[:].tolist()) - g = f.apply_index(indices, in_place=True) + ff = f.apply_index(indices, in_place=True) self.assertListEqual(expected, f.data[:].tolist()) + self.assertListEqual(expected, ff.data[:].tolist()) + + g = f.create_like(df, 'g') + g.data.write(data) + fg = f.create_like(df, 'fg') + fgr = g.apply_index(indices, fg) + self.assertListEqual(expected, fg.data[:].tolist()) + self.assertListEqual(expected, fgr.data[:].tolist()) + + fh = g.apply_index(indices) + self.assertListEqual(expected, fh.data[:].tolist()) mf = fields.NumericMemField(s, 'int32') mf.data.write(data) @@ -736,8 +869,19 @@ def test_categorical_apply_index(self): f.data.write(data) self.assertListEqual(data.tolist(), f.data[:].tolist()) - g = f.apply_index(indices, in_place=True) + ff = f.apply_index(indices, in_place=True) self.assertListEqual(expected, f.data[:].tolist()) + self.assertListEqual(expected, ff.data[:].tolist()) + + g = f.create_like(df, 'g') + g.data.write(data) + fg = f.create_like(df, 'fg') + fgr = g.apply_index(indices, fg) + self.assertListEqual(expected, fg.data[:].tolist()) + self.assertListEqual(expected, fgr.data[:].tolist()) + + fh = g.apply_index(indices) + self.assertListEqual(expected, fh.data[:].tolist()) mf = fields.CategoricalMemField(s, 'int8', keys) mf.data.write(data) @@ -768,8 +912,19 @@ def test_timestamp_apply_index(self): f.data.write(data) self.assertListEqual(data.tolist(), f.data[:].tolist()) - g = f.apply_index(indices, in_place=True) + ff = f.apply_index(indices, in_place=True) self.assertListEqual(expected, f.data[:].tolist()) + self.assertListEqual(expected, ff.data[:].tolist()) + + g = f.create_like(df, 'g') + g.data.write(data) + fg = f.create_like(df, 'fg') + fgr = g.apply_index(indices, fg) + self.assertListEqual(expected, fg.data[:].tolist()) + self.assertListEqual(expected, fgr.data[:].tolist()) + + fh = g.apply_index(indices) + self.assertListEqual(expected, fh.data[:].tolist()) mf = fields.TimestampMemField(s) mf.data.write(data) From 37dac08e972774908b42e35f09480c4bbac37f1a Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sat, 17 Apr 2021 17:44:40 +0100 Subject: [PATCH 071/181] Fixed issue in timestamp_field_create_like when group is set and is a dataframe --- exetera/core/fields.py | 2 +- tests/test_fields.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index b3a697de..fe55521d 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -2136,4 +2136,4 @@ def timestamp_field_create_like(source, group, name, timestamp): timestamp_field_constructor(source._session, group, name, ts, source.chunksize) return TimestampField(source._session, group[name], write_enabled=True) else: - return group.create_numeric(name, ts) + return group.create_timestamp(name, ts) diff --git a/tests/test_fields.py b/tests/test_fields.py index 88bafcee..6ab9817d 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -958,6 +958,10 @@ def test_indexed_string_field_create_like(self): self.assertIsInstance(g, fields.IndexedStringMemField) self.assertEqual(0, len(g.data)) + h = f.create_like(df, "h") + self.assertIsInstance(h, fields.IndexedStringField) + self.assertEqual(0, len(h.data)) + def test_fixed_string_field_create_like(self): data = np.asarray([b'a', b'bb', b'ccc', b'dddd'], dtype='S4') @@ -973,6 +977,10 @@ def test_fixed_string_field_create_like(self): self.assertIsInstance(g, fields.FixedStringMemField) self.assertEqual(0, len(g.data)) + h = f.create_like(df, "h") + self.assertIsInstance(h, fields.FixedStringField) + self.assertEqual(0, len(h.data)) + def test_numeric_field_create_like(self): data = np.asarray([1, 2, 3, 4], dtype=np.int32) @@ -988,6 +996,10 @@ def test_numeric_field_create_like(self): self.assertIsInstance(g, fields.NumericMemField) self.assertEqual(0, len(g.data)) + h = f.create_like(df, "h") + self.assertIsInstance(h, fields.NumericField) + self.assertEqual(0, len(h.data)) + def test_categorical_field_create_like(self): data = np.asarray([0, 1, 1, 0], dtype=np.int8) key = {b'a': 0, b'b': 1} @@ -1004,6 +1016,10 @@ def test_categorical_field_create_like(self): self.assertIsInstance(g, fields.CategoricalMemField) self.assertEqual(0, len(g.data)) + h = f.create_like(df, "h") + self.assertIsInstance(h, fields.CategoricalField) + self.assertEqual(0, len(h.data)) + def test_timestamp_field_create_like(self): from datetime import datetime as D data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11)] @@ -1020,3 +1036,7 @@ def test_timestamp_field_create_like(self): g = f.create_like(None, None) self.assertIsInstance(g, fields.TimestampMemField) self.assertEqual(0, len(g.data)) + + h = f.create_like(df, "h") + self.assertIsInstance(h, fields.TimestampField) + self.assertEqual(0, len(h.data)) From 8c62e0aa05f992526a9a53573c7baf98233a915e Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sat, 17 Apr 2021 17:56:25 +0100 Subject: [PATCH 072/181] persistence.filter_duplicate_fields now supports fields as well as ndarrays --- exetera/core/persistence.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/exetera/core/persistence.py b/exetera/core/persistence.py index 36c1359d..4f69705f 100644 --- a/exetera/core/persistence.py +++ b/exetera/core/persistence.py @@ -596,11 +596,12 @@ def _values_from_reader_or_ndarray(name, field): # TODO: handle usage of reader def filter_duplicate_fields(field): - - filter_ = np.ones(len(field), dtype=bool) - _filter_duplicate_fields(field, filter_) + field_ = val.array_from_field_or_lower('field', field) + filter_ = np.ones(len(field_), dtype=bool) + _filter_duplicate_fields(field_, filter_) return filter_ + def _filter_duplicate_fields(field, filter): seen_ids = dict() for i in range(len(field)): From cfcb69b7a73b2bf710fc080a4c91ea5020e97c3f Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sat, 17 Apr 2021 18:01:05 +0100 Subject: [PATCH 073/181] sort_on message now shows in verbose mode under all circumstances --- exetera/core/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exetera/core/session.py b/exetera/core/session.py index 555cbe0f..0b09e626 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -221,7 +221,7 @@ def print_if_verbose(*args): else: r.data[:] = self.apply_index(sorted_index, r) del r - print_if_verbose(f" '{k}' reordered in {time.time() - t1}s") + print_if_verbose(f" '{k}' reordered in {time.time() - t1}s") print_if_verbose(f"fields reordered in {time.time() - t0}s") def dataset_sort_index(self, sort_indices, index=None): From 22504efcdaa3d9deff94f4bd788142fe9a96f731 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sat, 17 Apr 2021 19:06:53 +0100 Subject: [PATCH 074/181] Fixed bug in apply filter when a destination dataset is applied --- exetera/core/dataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 3dd4b07a..5bc98cb4 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -273,7 +273,7 @@ def apply_filter(self, filter_to_apply, ddf=None): raise TypeError("The destination object must be an instance of DataFrame.") for name, field in self._columns.items(): newfld = field.create_like(ddf, name) - field.apply_index(filter_to_apply, target=newfld) + field.apply_filter(filter_to_apply, target=newfld) return ddf else: for field in self._columns.values(): From 23c373df2a3ea18d4f8a4e8c2f1e86b64b14ede7 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sat, 17 Apr 2021 19:16:24 +0100 Subject: [PATCH 075/181] Added a test to catch dataframe.apply_filter bug --- tests/test_dataframe.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 2d7a0ecb..42cfd3a9 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -232,3 +232,27 @@ def test_dataframe_ops(self): df.apply_filter(filter_to_apply, ddf) self.assertEqual([5, 4, 1], ddf['numf'].data[:].tolist()) self.assertEqual([b'e', b'd', b'a'], ddf['fst'].data[:].tolist()) + + +class TestDataFrameApplyFilter(unittest.TestCase): + + def test_apply_filter(self): + + src = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype='int32') + filt = np.array([0, 1, 0, 1, 0, 1, 1, 0], dtype='bool') + expected = src[filt].tolist() + + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + df = dst.create_dataframe('df') + numf = s.create_numeric(df, 'numf', 'int32') + numf.data.write(src) + df2 = dst.create_dataframe('df2') + df2b = df.apply_filter(filt, df2) + self.assertListEqual(expected, df2['numf'].data[:].tolist()) + self.assertListEqual(expected, df2b['numf'].data[:].tolist()) + self.assertListEqual(src.tolist(), df['numf'].data[:].tolist()) + + df.apply_filter(filt) + self.assertListEqual(expected, df['numf'].data[:].tolist()) From 98624e663b1eeb8ac165a80e4322285436d58b22 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sat, 17 Apr 2021 19:58:09 +0100 Subject: [PATCH 076/181] Bug fix: categorical_field_constructor in fields.py was returning numeric field when pass a h5py group as a destination for the field --- exetera/core/fields.py | 2 +- tests/test_fields.py | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index fe55521d..71874514 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -2079,7 +2079,7 @@ def fixed_string_field_create_like(source, group, name, timestamp): return FixedStringMemField(source._session, length) if isinstance(group, h5py.Group): - numeric_field_constructor(source._session, group, name, length, ts, source.chunksize) + fixed_string_field_constructor(source._session, group, name, length, ts, source.chunksize) return FixedStringField(source._session, group[name], write_enabled=True) else: return group.create_fixed_string(name, length, ts) diff --git a/tests/test_fields.py b/tests/test_fields.py index 6ab9817d..d21b3a61 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1040,3 +1040,86 @@ def test_timestamp_field_create_like(self): h = f.create_like(df, "h") self.assertIsInstance(h, fields.TimestampField) self.assertEqual(0, len(h.data)) + + +class TestFieldCreateLikeWithGroups(unittest.TestCase): + + def test_indexed_string_field_create_like(self): + data = ['a', 'bb', 'ccc', 'ddd'] + + bio = BytesIO() + with h5py.File(bio, 'w') as ds: + with session.Session() as s: + df = ds.create_group('df') + f = s.create_indexed_string(df, 'foo') + f.data.write(data) + self.assertListEqual(data, f.data[:]) + + g = f.create_like(df, "g") + self.assertIsInstance(g, fields.IndexedStringField) + self.assertEqual(0, len(g.data)) + + def test_fixed_string_field_create_like(self): + data = np.asarray([b'a', b'bb', b'ccc', b'dddd'], dtype='S4') + + bio = BytesIO() + with h5py.File(bio, 'w') as ds: + with session.Session() as s: + df = ds.create_group('df') + f = s.create_fixed_string(df, 'foo', 4) + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.create_like(df, "g") + self.assertIsInstance(g, fields.FixedStringField) + self.assertEqual(0, len(g.data)) + + def test_numeric_field_create_like(self): + expected = [1, 2, 3, 4] + data = np.asarray(expected, dtype=np.int32) + + bio = BytesIO() + with h5py.File(bio, 'w') as ds: + with session.Session() as s: + df = ds.create_group('df') + f = s.create_numeric(df, 'foo', 'int32') + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.create_like(df, "g") + self.assertIsInstance(g, fields.NumericField) + self.assertEqual(0, len(g.data)) + + def test_categorical_field_create_like(self): + data = np.asarray([0, 1, 1, 0], dtype=np.int8) + key = {b'a': 0, b'b': 1} + + bio = BytesIO() + with h5py.File(bio, 'w') as ds: + with session.Session() as s: + df = ds.create_group('df') + f = s.create_categorical(df, 'foo', 'int8', key) + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.create_like(df, "g") + self.assertIsInstance(g, fields.CategoricalField) + self.assertEqual(0, len(g.data)) + self.assertDictEqual({0: b'a', 1: b'b'}, g.keys) + + def test_timestamp_field_create_like(self): + from datetime import datetime as D + data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11)] + data = np.asarray([d.timestamp() for d in data], dtype=np.float64) + + bio = BytesIO() + with h5py.File(bio, 'w') as ds: + with session.Session() as s: + df = ds.create_group('df') + f = s.create_timestamp(df, 'foo') + f.data.write(data) + self.assertListEqual(data.tolist(), f.data[:].tolist()) + + g = f.create_like(df, "g") + self.assertIsInstance(g, fields.TimestampField) + self.assertEqual(0, len(g.data)) From 76d871761349fe501feaa2e37cae500e5a2cf2aa Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sat, 17 Apr 2021 20:05:35 +0100 Subject: [PATCH 077/181] Copying data before filtering, as filtering in h5py is very slow --- exetera/core/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 71874514..09af55e0 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -2002,7 +2002,7 @@ def apply_filter_to_field(source, filter_to_apply, target=None, in_place=False): if in_place is True and target is not None: raise ValueError("if 'in_place is True, 'target' must be None") - dest_data = source.data[filter_to_apply] + dest_data = source.data[:][filter_to_apply] if in_place: if not source._write_enabled: From 44a9c3d8f81cdcc067f8362739e2ba39b1e52268 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sun, 18 Apr 2021 21:54:59 +0100 Subject: [PATCH 078/181] Adding apply_spans functions to fields --- exetera/core/fields.py | 275 ++++++++++++++++++++++++++++++++++++- exetera/core/operations.py | 78 +++++++++++ exetera/core/session.py | 2 +- tests/test_fields.py | 142 ++++++++++++++++++- tests/test_operations.py | 14 ++ 5 files changed, 507 insertions(+), 4 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 09af55e0..45416065 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -9,7 +9,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union +from typing import Callable, Optional, Union from datetime import datetime, timezone import numpy as np @@ -195,6 +195,7 @@ def __init__(self, dtype): def __len__(self): return 0 if self._dataset is None else len(self._dataset) + @property def dtype(self): return self._dtype @@ -243,6 +244,10 @@ def __len__(self): # index to be initialised as [0] return max(len(self._indices)-1, 0) + @property + def dtype(self): + return self._dtype + def __getitem__(self, item): try: if isinstance(item, slice): @@ -308,6 +313,10 @@ def __init__(self, chunksize, indices, values): def __len__(self): return max(len(self._indices) - 1, 0) + @property + def dtype(self): + return self._dtype + def __getitem__(self, item): try: if isinstance(item, slice): @@ -475,6 +484,17 @@ def apply_index(self, index_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_index_to_indexed_field(self, index_to_apply, target, in_place) + def apply_spans_first(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) + + def apply_spans_last(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_last(self, spans_to_apply, target, in_place) + + def apply_spans_min(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_min(self, spans_to_apply, target, in_place) + + def apply_spans_max(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_max(self, spans_to_apply, target, in_place) class FixedStringMemField(MemoryField): def __init__(self, session, length): @@ -541,6 +561,18 @@ def apply_index(self, index_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) + def apply_spans_first(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) + + def apply_spans_last(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_last(self, spans_to_apply, target, in_place) + + def apply_spans_min(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_min(self, spans_to_apply, target, in_place) + + def apply_spans_max(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_max(self, spans_to_apply, target, in_place) + class NumericMemField(MemoryField): def __init__(self, session, nformat): @@ -604,6 +636,18 @@ def apply_index(self, index_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) + def apply_spans_first(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) + + def apply_spans_last(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_last(self, spans_to_apply, target, in_place) + + def apply_spans_min(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_min(self, spans_to_apply, target, in_place) + + def apply_spans_max(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_max(self, spans_to_apply, target, in_place) + def __add__(self, second): return FieldDataOps.numeric_add(self._session, self, second) @@ -762,6 +806,18 @@ def apply_index(self, index_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) + def apply_spans_first(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) + + def apply_spans_last(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_last(self, spans_to_apply, target, in_place) + + def apply_spans_min(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_min(self, spans_to_apply, target, in_place) + + def apply_spans_max(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_max(self, spans_to_apply, target, in_place) + def __lt__(self, value): return FieldDataOps.less_than(self._session, self, value) @@ -841,6 +897,18 @@ def apply_index(self, index_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) + def apply_spans_first(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) + + def apply_spans_last(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_last(self, spans_to_apply, target, in_place) + + def apply_spans_min(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_min(self, spans_to_apply, target, in_place) + + def apply_spans_max(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_max(self, spans_to_apply, target, in_place) + def __add__(self, second): return FieldDataOps.numeric_add(self._session, self, second) @@ -1041,7 +1109,6 @@ def apply_filter(self, filter_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_filter_to_indexed_field(self, filter_to_apply, target, in_place) - def apply_index(self, index_to_apply, target=None, in_place=False): """ Apply an index to this field. This operation doesn't modify the field on which it @@ -1058,6 +1125,18 @@ def apply_index(self, index_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_index_to_indexed_field(self, index_to_apply, target, in_place) + def apply_spans_first(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) + + def apply_spans_last(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_last(self, spans_to_apply, target, in_place) + + def apply_spans_min(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_min(self, spans_to_apply, target, in_place) + + def apply_spans_max(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_max(self, spans_to_apply, target, in_place) + class FixedStringField(HDF5Field): def __init__(self, session, group, name=None, write_enabled=False): @@ -1127,6 +1206,18 @@ def apply_index(self, index_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) + def apply_spans_first(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) + + def apply_spans_last(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_last(self, spans_to_apply, target, in_place) + + def apply_spans_min(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_min(self, spans_to_apply, target, in_place) + + def apply_spans_max(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_max(self, spans_to_apply, target, in_place) + class NumericField(HDF5Field): def __init__(self, session, group, name=None, mem_only=True, write_enabled=False): @@ -1192,6 +1283,18 @@ def apply_index(self, index_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) + def apply_spans_first(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) + + def apply_spans_last(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_last(self, spans_to_apply, target, in_place) + + def apply_spans_min(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_min(self, spans_to_apply, target, in_place) + + def apply_spans_max(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_max(self, spans_to_apply, target, in_place) + def __add__(self, second): return FieldDataOps.numeric_add(self._session, self, second) @@ -1357,6 +1460,18 @@ def apply_index(self, index_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) + def apply_spans_first(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) + + def apply_spans_last(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_last(self, spans_to_apply, target, in_place) + + def apply_spans_min(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_min(self, spans_to_apply, target, in_place) + + def apply_spans_max(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_max(self, spans_to_apply, target, in_place) + def __lt__(self, value): return FieldDataOps.less_than(self._session, self, value) @@ -1439,6 +1554,18 @@ def apply_index(self, index_to_apply, target=None, in_place=False): """ return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) + def apply_spans_first(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) + + def apply_spans_last(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_last(self, spans_to_apply, target, in_place) + + def apply_spans_min(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_min(self, spans_to_apply, target, in_place) + + def apply_spans_max(self, spans_to_apply, target=None, in_place=False): + return FieldDataOps.apply_spans_max(self, spans_to_apply, target, in_place) + def __add__(self, second): return FieldDataOps.numeric_add(self._session, self, second) @@ -2051,6 +2178,150 @@ def apply_index_to_field(source, index_to_apply, target=None, in_place=False): mem_field.data.write(dest_data) return mem_field + @staticmethod + def _apply_spans_src(source: Field, + predicate: Callable[[np.ndarray, np.ndarray, np.ndarray], Field], + spans: Union[Field, np.ndarray], + target: Optional[Field] = None, + in_place: bool = False) -> Field: + + if in_place is True and target is not None: + raise ValueError("if 'in_place is True, 'target' must be None") + + spans_ = val.array_from_field_or_lower('spans', spans) + result_inds = np.zeros(len(spans)) + results = np.zeros(len(spans)-1, dtype=source.data.dtype) + predicate(spans_, source.data[:], results) + + if in_place is True: + if not source._write_enabled: + raise ValueError("This field is marked read-only. Call writeable() on it before " + "performing in-place apply_span methods") + source.data.clear() + source.data.write(results) + return source + + if target is None: + result_field = source.create_like(None, None) + result_field.data.write(results) + return result_field + else: + target.data.clear() + target.data.write(results) + return target + + @staticmethod + def _apply_spans_indexed_src(source: Field, + predicate: Callable[[np.ndarray, np.ndarray, + np.ndarray, np.ndarray], Field], + spans: Union[Field, np.ndarray], + target: Optional[Field] = None, + in_place: bool = False) -> Field: + + if in_place is True and target is not None: + raise ValueError("if 'in_place is True, 'target' must be None") + + spans_ = val.array_from_field_or_lower('spans', spans) + + # step 1: get the indices through the index predicate + results = np.zeros(len(spans)-1, dtype=np.int64) + predicate(spans_, source.indices[:], source.values[:], results) + + # step 2: run apply_index on the source + return FieldDataOps.apply_index_to_indexed_field(source, results, target, in_place) + + @staticmethod + def _apply_spans_indexed_no_src(source: Field, + predicate: Callable[[np.ndarray, np.ndarray], Field], + spans: Union[Field, np.ndarray], + target: Optional[Field] = None, + in_place: bool = False) -> Field: + + if in_place is True and target is not None: + raise ValueError("if 'in_place is True, 'target' must be None") + + spans_ = val.array_from_field_or_lower('spans', spans) + + # step 1: get the indices through the index predicate + results = np.zeros(len(spans)-1, dtype=np.int64) + predicate(spans_, results) + + # step 2: run apply_index on the source + return FieldDataOps.apply_index_to_indexed_field(source, results, target, in_place) + + @staticmethod + def apply_spans_first(source: Field, + spans: Union[Field, np.ndarray], + target: Optional[Field] = None, + in_place: bool = None) -> Field: + + spans_ = val.array_from_field_or_lower('spans', spans) + if np.any(spans_[:-1] == spans_[1:]): + raise ValueError("cannot perform 'first' on spans with empty entries") + + if source.indexed: + return FieldDataOps._apply_spans_indexed_no_src(source, + ops.apply_spans_index_of_first, + spans_, target, in_place) + else: + return FieldDataOps._apply_spans_src(source, ops.apply_spans_first, spans_, + target, in_place) + + @staticmethod + def apply_spans_last(source: Field, + spans: Union[Field, np.ndarray], + target: Optional[Field] = None, + in_place: bool = None) -> Field: + + spans_ = val.array_from_field_or_lower('spans', spans) + if np.any(spans_[:-1] == spans_[1:]): + raise ValueError("cannot perform 'first' on spans with empty entries") + + if source.indexed: + return FieldDataOps._apply_spans_indexed_no_src(source, + ops.apply_spans_index_of_last, + spans_, target, in_place) + else: + return FieldDataOps._apply_spans_src(source, ops.apply_spans_last, spans_, + target, in_place) + + @staticmethod + def apply_spans_min(source: Field, + spans: Union[Field, np.ndarray], + target: Optional[Field] = None, + in_place: bool = None) -> Field: + + spans_ = val.array_from_field_or_lower('spans', spans) + if np.any(spans_[:-1] == spans_[1:]): + raise ValueError("cannot perform 'first' on spans with empty entries") + + if source.indexed: + return FieldDataOps._apply_spans_indexed_src(source, + ops.apply_spans_index_of_min_indexed, + spans_, target, in_place) + else: + return FieldDataOps._apply_spans_src(source, ops.apply_spans_min, spans_, + target, in_place) + + + @staticmethod + def apply_spans_max(source: Field, + spans: Union[Field, np.ndarray], + target: Optional[Field] = None, + in_place: bool = None) -> Field: + + spans_ = val.array_from_field_or_lower('spans', spans) + if np.any(spans_[:-1] == spans_[1:]): + raise ValueError("cannot perform 'first' on spans with empty entries") + + if source.indexed: + return FieldDataOps._apply_spans_indexed_src(source, + ops.apply_spans_index_of_max_indexed, + spans_, target, in_place) + else: + return FieldDataOps._apply_spans_src(source, ops.apply_spans_max, spans_, + target, in_place) + @staticmethod def indexed_string_create_like(source, group, name, timestamp): if group is None and name is not None: diff --git a/exetera/core/operations.py b/exetera/core/operations.py index 389b5ebc..7a3229b8 100644 --- a/exetera/core/operations.py +++ b/exetera/core/operations.py @@ -279,6 +279,84 @@ def apply_spans_index_of_min(spans, src_array, dest_array): return dest_array +@njit +def apply_spans_index_of_min_indexed(spans, src_indices, src_values, dest_array): + for i in range(len(spans)-1): + cur = spans[i] + next = spans[i+1] + + if next - cur == 1: + dest_array[i] = cur + else: + minind = cur + minstart = src_indices[cur] + minend = src_indices[cur+1] + minlen = minend - minstart + for j in range(cur+1, next): + curstart = src_indices[j] + curend = src_indices[j+1] + curlen = curend - curstart + shortlen = min(curlen, minlen) + found = False + for k in range(shortlen): + if src_values[curstart+k] < src_values[minstart+k]: + minind = j + minstart = curstart + minend = curend + found = True + break + elif src_values[curstart+k] > src_values[minstart+k]: + found = True + break + if not found and curlen < minlen: + minind = j + minstart = curstart + minend = curend + + dest_array[i] = minind + + return dest_array + + +@njit +def apply_spans_index_of_max_indexed(spans, src_indices, src_values, dest_array): + for i in range(len(spans)-1): + cur = spans[i] + next = spans[i+1] + + if next - cur == 1: + dest_array[i] = cur + else: + minind = cur + minstart = src_indices[cur] + minend = src_indices[cur+1] + minlen = minend - minstart + for j in range(cur+1, next): + curstart = src_indices[j] + curend = src_indices[j+1] + curlen = curend - curstart + shortlen = min(curlen, minlen) + found = False + for k in range(shortlen): + if src_values[curstart+k] > src_values[minstart+k]: + minind = j + minstart = curstart + minlen = curend - curstart + found = True + break + elif src_values[curstart+k] < src_values[minstart+k]: + found = True + break + if not found and curlen > minlen: + minind = j + minstart = curstart + minlen = curend - curstart + + dest_array[i] = minind + + return dest_array + + @njit def apply_spans_index_of_max(spans, src_array, dest_array): for i in range(len(spans)-1): diff --git a/exetera/core/session.py b/exetera/core/session.py index 0b09e626..bcb505bb 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -425,7 +425,7 @@ def _apply_spans_no_src(self, return results def _apply_spans_src(self, - predicate: Callable[[np.array, np.array, np.array], None], + predicate: Callable[[np.ndarray, np.ndarray, np.ndarray], None], spans: np.array, target: np.array, dest: Field = None) -> np.array: diff --git a/tests/test_fields.py b/tests/test_fields.py index d21b3a61..c84a4566 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -704,7 +704,6 @@ def test_timestamp_apply_filter(self): self.assertListEqual(expected, mb.data[:].tolist()) - class TestFieldApplyIndex(unittest.TestCase): def test_indexed_string_apply_index(self): @@ -941,6 +940,147 @@ def test_timestamp_apply_index(self): self.assertListEqual(expected, mb.data[:].tolist()) +class TestFieldApplySpansCount(unittest.TestCase): + + def _test_apply_spans_src(self, spans, src_data, expected, create_fn, apply_fn): + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f = create_fn(df) + f.data.write(src_data) + + actual = apply_fn(f, spans, None) + if actual.indexed: + self.assertListEqual(expected, actual.data[:]) + else: + self.assertListEqual(expected, actual.data[:].tolist()) + + def test_indexed_string_apply_spans(self): + spans = np.array([0, 2, 3, 6, 8], dtype=np.int32) + src_data = ['a', 'bb', 'ccc', 'dddd', 'eeee', 'fff', 'gg', 'h'] + + expected = ['a', 'ccc', 'dddd', 'gg'] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_indexed_string('foo'), + lambda f, p, d: f.apply_spans_first(p, d)) + + expected = ['bb', 'ccc', 'fff', 'h'] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_indexed_string('foo'), + lambda f, p, d: f.apply_spans_last(p, d)) + + expected = ['a', 'ccc', 'dddd', 'gg'] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_indexed_string('foo'), + lambda f, p, d: f.apply_spans_min(p, d)) + + expected = ['bb', 'ccc', 'fff', 'h'] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_indexed_string('foo'), + lambda f, p, d: f.apply_spans_max(p, d)) + + def test_fixed_string_apply_spans(self): + spans = np.array([0, 2, 3, 6, 8], dtype=np.int32) + src_data = [b'a1', b'a2', b'b1', b'c1', b'c2', b'c3', b'd1', b'd2'] + + expected = [b'a1', b'b1', b'c1', b'd1'] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_fixed_string('foo', 2), + lambda f, p, d: f.apply_spans_first(p, d)) + + expected = [b'a2', b'b1', b'c3', b'd2'] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_fixed_string('foo', 2), + lambda f, p, d: f.apply_spans_last(p, d)) + + expected = [b'a1', b'b1', b'c1', b'd1'] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_fixed_string('foo', 2), + lambda f, p, d: f.apply_spans_min(p, d)) + + expected = [b'a2', b'b1', b'c3', b'd2'] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_fixed_string('foo', 2), + lambda f, p, d: f.apply_spans_max(p, d)) + + def test_numeric_apply_spans(self): + spans = np.array([0, 2, 3, 6, 8], dtype=np.int32) + src_data = [1, 2, 11, 21, 22, 23, 31, 32] + + expected = [1, 11, 21, 31] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_numeric('foo', 'int32'), + lambda f, p, d: f.apply_spans_first(p, d)) + + expected = [2, 11, 23, 32] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_numeric('foo', 'int32'), + lambda f, p, d: f.apply_spans_last(p, d)) + + expected = [1, 11, 21, 31] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_numeric('foo', 'int32'), + lambda f, p, d: f.apply_spans_min(p, d)) + + expected = [2, 11, 23, 32] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_numeric('foo', 'int32'), + lambda f, p, d: f.apply_spans_max(p, d)) + + def test_categorical_apply_spans(self): + spans = np.array([0, 2, 3, 6, 8], dtype=np.int32) + src_data = [0, 1, 2, 0, 1, 2, 0, 1] + keys = {b'a': 0, b'b': 1, b'c': 2} + + expected = [0, 2, 0, 0] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_categorical('foo', 'int8', keys), + lambda f, p, d: f.apply_spans_first(p, d)) + + expected = [1, 2, 2, 1] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_categorical('foo', 'int8', keys), + lambda f, p, d: f.apply_spans_last(p, d)) + + expected = [0, 2, 0, 0] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_categorical('foo', 'int8', keys), + lambda f, p, d: f.apply_spans_min(p, d)) + + expected = [1, 2, 2, 1] + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_categorical('foo', 'int8', keys), + lambda f, p, d: f.apply_spans_max(p, d)) + + def test_timestamp_apply_spans(self): + spans = np.array([0, 2, 3, 6, 8], dtype=np.int32) + from datetime import datetime as D + src_data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11), + D(2021, 1, 1), D(2022, 5, 18), D(2951, 8, 17), D(1841, 10, 11)] + src_data = np.asarray([d.timestamp() for d in src_data], dtype=np.float64) + + expected = src_data[[0, 2, 3, 6]].tolist() + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_timestamp('foo'), + lambda f, p, d: f.apply_spans_first(p, d)) + + expected = src_data[[1, 2, 5, 7]].tolist() + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_timestamp('foo'), + lambda f, p, d: f.apply_spans_last(p, d)) + + expected = src_data[[0, 2, 3, 6]].tolist() + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_timestamp('foo'), + lambda f, p, d: f.apply_spans_min(p, d)) + + expected = src_data[[1, 2, 5, 7]].tolist() + self._test_apply_spans_src(spans, src_data, expected, + lambda df: df.create_timestamp('foo'), + lambda f, p, d: f.apply_spans_max(p, d)) + + class TestFieldCreateLike(unittest.TestCase): def test_indexed_string_field_create_like(self): diff --git a/tests/test_operations.py b/tests/test_operations.py index 65dad212..030c076d 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -57,8 +57,21 @@ def test_safe_map_values(self): np.asarray([1, 8, 2, 7, ops.INVALID_INDEX, 0, 9, 1, 8]), np.asarray([3, 45, 6, 36, 0, 1, 55, 3, 45]), 0) + class TestAggregation(unittest.TestCase): + def test_apply_spans_indexed_field(self): + indices = np.asarray([0, 2, 4, 7, 10, 12, 14, 16, 18, 20, 22, 24], dtype=np.int32) + values = np.frombuffer(b'a1a2a2ab2ab2b1c1c2d2d1e1', dtype=np.int8) + spans = np.asarray([0, 3, 6, 8, 10, 11], dtype=np.int32) + dest = np.zeros(len(spans)-1, dtype=np.int32) + + ops.apply_spans_index_of_min_indexed(spans, indices, values, dest) + self.assertListEqual(dest.tolist(), [0, 5, 6, 9, 10]) + + ops.apply_spans_index_of_max_indexed(spans, indices, values, dest) + self.assertListEqual(dest.tolist(), [2, 3, 7, 8, 10]) + def test_non_indexed_apply_spans(self): values = np.asarray([1, 2, 3, 3, 2, 1, 1, 2, 2, 1, 1], dtype=np.int32) spans = np.asarray([0, 3, 6, 8, 10, 11], dtype=np.int32) @@ -139,6 +152,7 @@ def test_ordered_map_to_right_both_unique(self): dtype=np.int64) self.assertTrue(np.array_equal(results, expected)) + def test_ordered_map_to_right_right_unique(self): raw_ids = [0, 1, 2, 3, 5, 6, 7, 9] a_ids = np.asarray(raw_ids, dtype=np.int64) From 210f847891b2776598dead10255284a1c537bdc6 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Sun, 18 Apr 2021 22:36:23 +0100 Subject: [PATCH 079/181] Fixed TestFieldApplySpansCount.test_timestamp_apply_spans that had been written but not run --- tests/test_fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index c84a4566..9473e6ca 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1070,12 +1070,12 @@ def test_timestamp_apply_spans(self): lambda df: df.create_timestamp('foo'), lambda f, p, d: f.apply_spans_last(p, d)) - expected = src_data[[0, 2, 3, 6]].tolist() + expected = src_data[[0, 2, 3, 7]].tolist() self._test_apply_spans_src(spans, src_data, expected, lambda df: df.create_timestamp('foo'), lambda f, p, d: f.apply_spans_min(p, d)) - expected = src_data[[1, 2, 5, 7]].tolist() + expected = src_data[[1, 2, 5, 6]].tolist() self._test_apply_spans_src(spans, src_data, expected, lambda df: df.create_timestamp('foo'), lambda f, p, d: f.apply_spans_max(p, d)) From a7d667357fb0f1280cbd135c6b670095729a650c Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Mon, 19 Apr 2021 22:50:10 +0100 Subject: [PATCH 080/181] Issues found with indexed strings and merging; fixes found for apply_filter and apply_index when being passed a field rather than an ndarray; both with augmented testing --- exetera/core/fields.py | 9 +- exetera/core/operations.py | 5 +- exetera/core/session.py | 49 ++++++++--- tests/test_fields.py | 3 + tests/test_session.py | 163 +++++++++++++++++++++++++++++++++++++ 5 files changed, 213 insertions(+), 16 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 8062d387..5cc04d79 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -2126,10 +2126,13 @@ def apply_index_to_indexed_field(source, index_to_apply, target=None, in_place=F @staticmethod def apply_filter_to_field(source, filter_to_apply, target=None, in_place=False): + if in_place is True and target is not None: raise ValueError("if 'in_place is True, 'target' must be None") - dest_data = source.data[:][filter_to_apply] + filter_to_apply_ = val.array_from_field_or_lower('filter_to_apply', filter_to_apply) + + dest_data = source.data[:][filter_to_apply_] if in_place: if not source._write_enabled: @@ -2156,7 +2159,9 @@ def apply_index_to_field(source, index_to_apply, target=None, in_place=False): if in_place is True and target is not None: raise ValueError("if 'in_place is True, 'target' must be None") - dest_data = source.data[:][index_to_apply] + index_to_apply_ = val.array_from_field_or_lower('index_to_apply', index_to_apply) + + dest_data = source.data[:][index_to_apply_] if in_place: if not source._write_enabled: diff --git a/exetera/core/operations.py b/exetera/core/operations.py index 7a3229b8..a3768c14 100644 --- a/exetera/core/operations.py +++ b/exetera/core/operations.py @@ -23,7 +23,7 @@ def chunks(length, chunksize=1 << 20): def safe_map(field, map_field, map_filter, empty_value=None): if isinstance(field, Field): - if isinstance(field, fields.IndexedStringField): + if field.indexed: return safe_map_indexed_values( field.indices[:], field.values[:], map_field, map_filter, empty_value) else: @@ -61,7 +61,8 @@ def safe_map_indexed_values(data_indices, data_values, map_field, map_filter, em dst = offset dse = offset + empty_value_len i_result[i+1] = dse - v_result[dst:dse] = empty_value + if empty_value is not None: + v_result[dst:dse] = empty_value offset += dse - dst return i_result, v_result diff --git a/exetera/core/session.py b/exetera/core/session.py index bcb505bb..9c9994ad 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -1140,18 +1140,30 @@ def merge_left(self, left_on, right_on, right_results = list() for irf, rf in enumerate(right_fields): - rf_raw = val.raw_array_from_parameter(self, 'right_fields[{}]'.format(irf), rf) - joined_field = ops.safe_map(rf_raw, r_to_l_map, r_to_l_filt) - # joined_field = per._safe_map(rf_raw, r_to_l_map, r_to_l_filt) - if right_writers is None: - right_results.append(joined_field) + if isinstance(rf, Field) and rf.indexed: + indices, values = ops.safe_map_indexed_values(rf.indices[:], rf.values[:], + r_to_l_map, r_to_l_filt) + if right_writers is None: + result = fld.IndexedStringMemField(self) + result.indices.write(indices) + result.values.write(values) + right_results.append(result) + else: + right_writers[irf].indices.write(indices) + right_writers[irf].values.write(values) else: - right_writers[irf].data.write(joined_field) + rf_raw = val.array_from_field_or_lower('right_fields[{}]'.format(irf), rf) + values = ops.safe_map_values(rf_raw, r_to_l_map, r_to_l_filt) + + if right_writers is None: + right_results.append(values) + else: + right_writers[irf].data.write(values) return right_results def merge_right(self, left_on, right_on, - left_fields=None, left_writers=None): + left_fields=tuple(), left_writers=None): l_key_raw = val.raw_array_from_parameter(self, 'left_on', left_on) l_index = np.arange(len(l_key_raw), dtype=np.int64) l_df = pd.DataFrame({'l_k': l_key_raw, 'l_index': l_index}) @@ -1166,12 +1178,25 @@ def merge_right(self, left_on, right_on, left_results = list() for ilf, lf in enumerate(left_fields): - lf_raw = val.raw_array_from_parameter(self, 'left_fields[{}]'.format(ilf), lf) - joined_field = ops.safe_map(lf_raw, l_to_r_map, l_to_r_filt) - if left_writers is None: - left_results.append(joined_field) + if isinstance(lf, Field) and lf.indexed: + indices, values = ops.safe_map_indexed_values(lf.indices[:], lf.values[:], + l_to_r_map, l_to_r_filt) + if left_writers is None: + result = fld.IndexedStringMemField(self) + result.indices.write(indices) + result.values.write(values) + left_results.append(result) + else: + left_writers[ilf].indices.write(indices) + left_writers[ilf].values.write(values) else: - left_writers[ilf].data.write(joined_field) + lf_raw = val.raw_array_from_parameter(self, 'left_fields[{}]'.format(ilf), lf) + values = ops.safe_map_values(lf_raw, l_to_r_map, l_to_r_filt) + + if left_writers is None: + left_results.append(values) + else: + left_writers[ilf].data.write(values) return left_results diff --git a/tests/test_fields.py b/tests/test_fields.py index a0d9a469..aeb7d7eb 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -535,6 +535,9 @@ def test_indexed_string_apply_filter(self): self.assertListEqual(expected_filt_values, mb.values[:].tolist()) self.assertListEqual(expected_filt_data, mb.data[:]) + df2 = ds.create_dataframe("filter") + + def test_fixed_string_apply_filter(self): data = np.array([b'a', b'bb', b'ccc', b'dddd', b'eeee', b'fff', b'gg', b'h'], dtype='S4') filt = np.array([0, 1, 0, 1, 0, 1, 0, 1], dtype=bool) diff --git a/tests/test_session.py b/tests/test_session.py index 02ddb6a3..527bacc9 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -412,6 +412,169 @@ def test_ordered_merge_inner(self): self.assertTrue(np.array_equal(actual[1][1], r_vals_2_exp)) + def test_merge_indexed_fields_left(self): + l_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') + r_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') + r_vals = ['bb1', 'ccc1', '', 'dddd1', 'ggggggg1', 'ffffff1', 'bb2', '', 'ccc2'] + + bio1 = BytesIO() + bio2 = BytesIO() + with session.Session() as s: + l_ds = s.open_dataset(bio1, 'w', 'l_ds') + l_df = l_ds.create_dataframe('l_df') + l_df.create_numeric('id', 'int32').data.write(l_id) + r_ds = s.open_dataset(bio2, 'w', 'r_ds') + + r_df = r_ds.create_dataframe('r_df') + r_df.create_numeric('id', 'int32').data.write(r_id) + r_df.create_indexed_string('vals').data.write(r_vals) + + s.merge_left(left_on=l_df['id'], right_on=r_df['id'], + right_fields=(r_df['vals'],), + right_writers=(l_df.create_indexed_string('vals'),)) + + r_df2 = r_ds.create_dataframe('r_df2') + r_df2.create_numeric('id', 'int32').data.write(r_id) + r_df2.create_indexed_string('vals').data.write(r_vals) + + results = s.merge_left(left_on=l_df['id'], right_on=r_df2['id'], + right_fields=(r_df2['vals'],)) + + expected = ['', '', '', 'bb1', 'bb2', 'ccc1', 'ccc2', 'dddd1', '', 'ffffff1', 'ggggggg1'] + self.assertListEqual(expected, l_df['vals'].data[:]) + self.assertListEqual(expected, results[0].data[:]) + + l_id = l_id[::-1] + r_id = r_id[::-1] + r_vals = r_vals[::-1] + + bio1 = BytesIO() + bio2 = BytesIO() + with session.Session() as s: + l_ds = s.open_dataset(bio1, 'w', 'l_ds') + l_df = l_ds.create_dataframe('l_df') + l_df.create_numeric('id', 'int32').data.write(l_id) + r_ds = s.open_dataset(bio2, 'w', 'r_ds') + + r_df = r_ds.create_dataframe('r_df') + r_df.create_numeric('id', 'int32').data.write(r_id) + r_df.create_indexed_string('vals').data.write(r_vals) + + s.merge_left(left_on=l_df['id'], right_on=r_df['id'], + right_fields=(r_df['vals'],), + right_writers=(l_df.create_indexed_string('vals'),)) + + r_df2 = r_ds.create_dataframe('r_df2') + r_df2.create_numeric('id', 'int32').data.write(r_id) + r_df2.create_indexed_string('vals').data.write(r_vals) + + results = s.merge_left(left_on=l_df['id'], right_on=r_df2['id'], + right_fields=(r_df2['vals'],)) + + expected = ['ggggggg1', 'ffffff1', '', 'dddd1', 'ccc2', 'ccc1', 'bb2', 'bb1', '', '', ''] + self.assertListEqual(expected, l_df['vals'].data[:]) + self.assertListEqual(expected, results[0].data[:]) + + + def test_merge_indexed_fields_right(self): + r_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') + l_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') + l_vals = ['bb1', 'ccc1', '', 'dddd1', 'ggggggg1', 'ffffff1', 'bb2', '', 'ccc2'] + + bio1 = BytesIO() + bio2 = BytesIO() + with session.Session() as s: + r_ds = s.open_dataset(bio1, 'w', 'r_ds') + r_df = r_ds.create_dataframe('r_df') + r_df.create_numeric('id', 'int32').data.write(r_id) + l_ds = s.open_dataset(bio2, 'w', 'l_ds') + + l_df = l_ds.create_dataframe('l_df') + l_df.create_numeric('id', 'int32').data.write(l_id) + l_df.create_indexed_string('vals').data.write(l_vals) + + s.merge_right(left_on=l_df['id'], right_on=r_df['id'], + left_fields=(l_df['vals'],), + left_writers=(r_df.create_indexed_string('vals'),)) + + l_df2 = l_ds.create_dataframe('l_df2') + l_df2.create_numeric('id', 'int32').data.write(l_id) + l_df2.create_indexed_string('vals').data.write(l_vals) + + results = s.merge_right(left_on=l_df2['id'], right_on=r_df['id'], + left_fields=(l_df2['vals'],)) + + expected = ['', '', '', 'bb1', 'bb2', 'ccc1', 'ccc2', 'dddd1', '', 'ffffff1', 'ggggggg1'] + self.assertListEqual(expected, r_df['vals'].data[:]) + self.assertListEqual(expected, results[0].data[:]) + + r_id = r_id[::-1] + l_id = l_id[::-1] + l_vals = l_vals[::-1] + + bio1 = BytesIO() + bio2 = BytesIO() + with session.Session() as s: + r_ds = s.open_dataset(bio1, 'w', 'r_ds') + r_df = r_ds.create_dataframe('r_df') + r_df.create_numeric('id', 'int32').data.write(r_id) + l_ds = s.open_dataset(bio2, 'w', 'l_ds') + + l_df = l_ds.create_dataframe('l_df') + l_df.create_numeric('id', 'int32').data.write(l_id) + l_df.create_indexed_string('vals').data.write(l_vals) + + s.merge_right(left_on=l_df['id'], right_on=r_df['id'], + left_fields=(l_df['vals'],), + left_writers=(r_df.create_indexed_string('vals'),)) + + l_df2 = l_ds.create_dataframe('l_df2') + l_df2.create_numeric('id', 'int32').data.write(l_id) + l_df2.create_indexed_string('vals').data.write(l_vals) + + results = s.merge_right(left_on=l_df2['id'], right_on=r_df['id'], + left_fields=(l_df2['vals'],)) + + expected = ['ggggggg1', 'ffffff1', '', 'dddd1', 'ccc2', 'ccc1', 'bb2', 'bb1', '', '', ''] + self.assertListEqual(expected, r_df['vals'].data[:]) + self.assertListEqual(expected, results[0].data[:]) + + + # def test_ordered_merge_indexed_fields(self): + # l_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') + # r_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') + # r_vals = ['bb1', 'ccc1', '', 'dddd1', 'ggggggg1', 'ffffff1', 'bb2', '', 'ccc2'] + # bio1 = BytesIO() + # bio2 = BytesIO() + # with session.Session() as s: + # l_ds = s.open_dataset(bio1, 'w', 'l_ds') + # l_df = l_ds.create_dataframe('l_df') + # l_df.create_numeric('id', 'int32').data.write(l_id) + # r_ds = s.open_dataset(bio2, 'w', 'r_ds') + # + # r_df = r_ds.create_dataframe('r_df') + # r_df.create_numeric('id', 'int32').data.write(r_id) + # r_df.create_indexed_string('vals').data.write(r_vals) + # r_indices = s.dataset_sort_index((r_df['id'],)) + # r_df.apply_index(r_indices) + # print(r_df['id'].data[:]) + # print(r_df['vals'].data[:]) + # + # s.ordered_merge_left(left_on=l_df['id'], right_on=r_df['id'], + # right_field_sources=(r_df['vals'],), + # left_field_sinks=(l_df.create_indexed_string('vals'),)) + # + # r_df2 = r_ds.create_dataframe('r_df2') + # r_df2.create_numeric('id', 'int32').data.write(r_id) + # r_df2.create_indexed_string('vals').data.write(r_vals) + # + # results = s.ordered_merge_left(left_on=l_df['id'], right_on=r_df2['id'], + # right_field_sources=(r_df2['vals'],)) + # + # print(l_df['vals'].data[:]) + # print(results[0].data[:]) + + class TestSessionJoin(unittest.TestCase): def test_session_join(self): From 3d322c26a153e273d13a0b765060fabf6a47f1be Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Tue, 20 Apr 2021 00:30:05 +0100 Subject: [PATCH 081/181] Updated merge functions to consistently return memory fields if not provided with outputs but provided with fields --- exetera/core/dataframe.py | 8 +- exetera/core/session.py | 126 ++++++++++++++------ tests/test_session.py | 238 +++++++++++++++++--------------------- 3 files changed, 201 insertions(+), 171 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 5bc98cb4..e7f18385 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -174,9 +174,9 @@ def __getitem__(self, name): :param name: The name of field to get. """ if not isinstance(name, str): - raise TypeError("The name must be a str object.") + raise TypeError("The name must be of type str but is of type '{}'".format(str)) elif not self.__contains__(name): - raise ValueError("Can not find the name from this dataframe.") + raise ValueError("There is no field named '{}' in this dataframe".format(name)) else: return self._columns[name] @@ -201,7 +201,7 @@ def get_field(self, name): def __setitem__(self, name, field): if not isinstance(name, str): - raise TypeError("The name must be a str object.") + raise TypeError("The name must be of type str but is of type '{}'".format(str)) if not isinstance(field, fld.Field): raise TypeError("The field must be a Field object.") nfield = field.create_like(self, name) @@ -214,7 +214,7 @@ def __setitem__(self, name, field): def __delitem__(self, name): if not self.__contains__(name=name): - raise ValueError("This dataframe does not contain the name to delete.") + raise ValueError("There is no field named '{}' in this dataframe".format(name)) else: del self._h5group[name] del self._columns[name] diff --git a/exetera/core/session.py b/exetera/core/session.py index 9c9994ad..19d7a5c2 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -1140,20 +1140,28 @@ def merge_left(self, left_on, right_on, right_results = list() for irf, rf in enumerate(right_fields): - if isinstance(rf, Field) and rf.indexed: - indices, values = ops.safe_map_indexed_values(rf.indices[:], rf.values[:], - r_to_l_map, r_to_l_filt) - if right_writers is None: - result = fld.IndexedStringMemField(self) - result.indices.write(indices) - result.values.write(values) - right_results.append(result) + if isinstance(rf, Field): + if rf.indexed: + indices, values = ops.safe_map_indexed_values(rf.indices[:], rf.values[:], + r_to_l_map, r_to_l_filt) + if right_writers is None: + result = fld.IndexedStringMemField(self) + result.indices.write(indices) + result.values.write(values) + right_results.append(result) + else: + right_writers[irf].indices.write(indices) + right_writers[irf].values.write(values) else: - right_writers[irf].indices.write(indices) - right_writers[irf].values.write(values) + values = ops.safe_map_values(rf.data[:], r_to_l_map, r_to_l_filt) + if right_writers is None: + result = rf.create_like() + result.data.write(values) + right_results.append(result) + else: + right_writers[irf].data.write(values) else: - rf_raw = val.array_from_field_or_lower('right_fields[{}]'.format(irf), rf) - values = ops.safe_map_values(rf_raw, r_to_l_map, r_to_l_filt) + values = ops.safe_map_values(rf, r_to_l_map, r_to_l_filt) if right_writers is None: right_results.append(values) @@ -1178,20 +1186,28 @@ def merge_right(self, left_on, right_on, left_results = list() for ilf, lf in enumerate(left_fields): - if isinstance(lf, Field) and lf.indexed: - indices, values = ops.safe_map_indexed_values(lf.indices[:], lf.values[:], - l_to_r_map, l_to_r_filt) - if left_writers is None: - result = fld.IndexedStringMemField(self) - result.indices.write(indices) - result.values.write(values) - left_results.append(result) + if isinstance(lf, Field): + if lf.indexed: + indices, values = ops.safe_map_indexed_values(lf.indices[:], lf.values[:], + l_to_r_map, l_to_r_filt) + if left_writers is None: + result = fld.IndexedStringMemField(self) + result.indices.write(indices) + result.values.write(values) + left_results.append(result) + else: + left_writers[ilf].indices.write(indices) + left_writers[ilf].values.write(values) else: - left_writers[ilf].indices.write(indices) - left_writers[ilf].values.write(values) + values = ops.safe_map_values(lf.data[:], l_to_r_map, l_to_r_filt) + if left_writers is None: + result = lf.create_like() + result.data.write(values) + left_results.append(result) + else: + left_writers[ilf].data.write(values) else: - lf_raw = val.raw_array_from_parameter(self, 'left_fields[{}]'.format(ilf), lf) - values = ops.safe_map_values(lf_raw, l_to_r_map, l_to_r_filt) + values = ops.safe_map_values(lf, l_to_r_map, l_to_r_filt) if left_writers is None: left_results.append(values) @@ -1218,21 +1234,63 @@ def merge_inner(self, left_on, right_on, left_results = list() for ilf, lf in enumerate(left_fields): - lf_raw = val.raw_array_from_parameter(self, 'left_fields[{}]'.format(ilf), lf) - joined_field = ops.safe_map(lf_raw, l_to_i_map, l_to_i_filt) - if left_writers is None: - left_results.append(joined_field) + if isinstance(lf, Field): + if lf.indexed: + indices, values = ops.safe_map_indexed_values(lf.indices[:], lf.values[:], + l_to_i_map, l_to_i_filt) + if left_writers is None: + result = fld.IndexedStringMemField(self) + result.indices.write(indices) + result.values.write(values) + left_results.append(result) + else: + left_writers[ilf].indices.write(indices) + left_writers[ilf].values.write(values) + else: + values = ops.safe_map_values(lf.data[:], l_to_i_map, l_to_i_filt) + if left_writers is None: + result = lf.create_like() + result.data.write(values) + left_results.append(result) + else: + left_writers[ilf].data.write(values) else: - left_writers[ilf].data.write(joined_field) + values = ops.safe_map_values(lf, l_to_i_map, l_to_i_filt) + + if left_writers is None: + left_results.append(values) + else: + left_writers[ilf].data.write(values) right_results = list() for irf, rf in enumerate(right_fields): - rf_raw = val.raw_array_from_parameter(self, 'right_fields[{}]'.format(irf), rf) - joined_field = ops.safe_map(rf_raw, r_to_i_map, r_to_i_filt) - if right_writers is None: - right_results.append(joined_field) + if isinstance(rf, Field): + if rf.indexed: + indices, values = ops.safe_map_indexed_values(rf.indices[:], rf.values[:], + r_to_i_map, r_to_i_filt) + if right_writers is None: + result = fld.IndexedStringMemField(self) + result.indices.write(indices) + result.values.write(values) + right_results.append(result) + else: + right_writers[irf].indices.write(indices) + right_writers[irf].values.write(values) + else: + values = ops.safe_map_values(rf.data[:], r_to_i_map, r_to_i_filt) + if right_writers is None: + result = rf.create_like() + result.data.write(values) + right_results.append(result) + else: + right_writers[irf].data.write(values) else: - right_writers[irf].data.write(joined_field) + values = ops.safe_map_values(rf, r_to_i_map, r_to_i_filt) + + if right_writers is None: + right_results.append(values) + else: + right_writers[irf].data.write(values) return left_results, right_results diff --git a/tests/test_session.py b/tests/test_session.py index 527bacc9..5379e32e 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -413,166 +413,138 @@ def test_ordered_merge_inner(self): def test_merge_indexed_fields_left(self): - l_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') - r_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') - r_vals = ['bb1', 'ccc1', '', 'dddd1', 'ggggggg1', 'ffffff1', 'bb2', '', 'ccc2'] - bio1 = BytesIO() - bio2 = BytesIO() - with session.Session() as s: - l_ds = s.open_dataset(bio1, 'w', 'l_ds') - l_df = l_ds.create_dataframe('l_df') - l_df.create_numeric('id', 'int32').data.write(l_id) - r_ds = s.open_dataset(bio2, 'w', 'r_ds') + def _perform_inner(l_id, r_id, r_vals, expected): + bio1 = BytesIO() + bio2 = BytesIO() + with session.Session() as s: + l_ds = s.open_dataset(bio1, 'w', 'l_ds') + l_df = l_ds.create_dataframe('l_df') + l_df.create_numeric('id', 'int32').data.write(l_id) + r_ds = s.open_dataset(bio2, 'w', 'r_ds') - r_df = r_ds.create_dataframe('r_df') - r_df.create_numeric('id', 'int32').data.write(r_id) - r_df.create_indexed_string('vals').data.write(r_vals) + r_df = r_ds.create_dataframe('r_df') + r_df.create_numeric('id', 'int32').data.write(r_id) + r_df.create_indexed_string('vals').data.write(r_vals) - s.merge_left(left_on=l_df['id'], right_on=r_df['id'], - right_fields=(r_df['vals'],), - right_writers=(l_df.create_indexed_string('vals'),)) + s.merge_left(left_on=l_df['id'], right_on=r_df['id'], + right_fields=(r_df['vals'],), + right_writers=(l_df.create_indexed_string('vals'),)) - r_df2 = r_ds.create_dataframe('r_df2') - r_df2.create_numeric('id', 'int32').data.write(r_id) - r_df2.create_indexed_string('vals').data.write(r_vals) + r_df2 = r_ds.create_dataframe('r_df2') + r_df2.create_numeric('id', 'int32').data.write(r_id) + r_df2.create_indexed_string('vals').data.write(r_vals) - results = s.merge_left(left_on=l_df['id'], right_on=r_df2['id'], - right_fields=(r_df2['vals'],)) + results = s.merge_left(left_on=l_df['id'], right_on=r_df2['id'], + right_fields=(r_df2['vals'],)) - expected = ['', '', '', 'bb1', 'bb2', 'ccc1', 'ccc2', 'dddd1', '', 'ffffff1', 'ggggggg1'] - self.assertListEqual(expected, l_df['vals'].data[:]) - self.assertListEqual(expected, results[0].data[:]) + self.assertListEqual(expected, l_df['vals'].data[:]) + self.assertListEqual(expected, results[0].data[:]) + + l_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') + r_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') + r_vals = ['bb1', 'ccc1', '', 'dddd1', 'ggggggg1', 'ffffff1', 'bb2', '', 'ccc2'] + expected = ['', '', '', 'bb1', 'bb2', 'ccc1', 'ccc2', 'dddd1', '', 'ffffff1', 'ggggggg1'] + _perform_inner(l_id, r_id, r_vals, expected) l_id = l_id[::-1] r_id = r_id[::-1] r_vals = r_vals[::-1] + expected = ['ggggggg1', 'ffffff1', '', 'dddd1', 'ccc2', 'ccc1', 'bb2', 'bb1', '', '', ''] + _perform_inner(l_id, r_id, r_vals, expected) - bio1 = BytesIO() - bio2 = BytesIO() - with session.Session() as s: - l_ds = s.open_dataset(bio1, 'w', 'l_ds') - l_df = l_ds.create_dataframe('l_df') - l_df.create_numeric('id', 'int32').data.write(l_id) - r_ds = s.open_dataset(bio2, 'w', 'r_ds') - r_df = r_ds.create_dataframe('r_df') - r_df.create_numeric('id', 'int32').data.write(r_id) - r_df.create_indexed_string('vals').data.write(r_vals) + def test_merge_indexed_fields_right(self): + + def _perform_test(r_id, l_id, l_vals, expected): + bio1 = BytesIO() + bio2 = BytesIO() + with session.Session() as s: + r_ds = s.open_dataset(bio1, 'w', 'r_ds') + r_df = r_ds.create_dataframe('r_df') + r_df.create_numeric('id', 'int32').data.write(r_id) + l_ds = s.open_dataset(bio2, 'w', 'l_ds') - s.merge_left(left_on=l_df['id'], right_on=r_df['id'], - right_fields=(r_df['vals'],), - right_writers=(l_df.create_indexed_string('vals'),)) + l_df = l_ds.create_dataframe('l_df') + l_df.create_numeric('id', 'int32').data.write(l_id) + l_df.create_indexed_string('vals').data.write(l_vals) - r_df2 = r_ds.create_dataframe('r_df2') - r_df2.create_numeric('id', 'int32').data.write(r_id) - r_df2.create_indexed_string('vals').data.write(r_vals) + s.merge_right(left_on=l_df['id'], right_on=r_df['id'], + left_fields=(l_df['vals'],), + left_writers=(r_df.create_indexed_string('vals'),)) - results = s.merge_left(left_on=l_df['id'], right_on=r_df2['id'], - right_fields=(r_df2['vals'],)) + l_df2 = l_ds.create_dataframe('l_df2') + l_df2.create_numeric('id', 'int32').data.write(l_id) + l_df2.create_indexed_string('vals').data.write(l_vals) - expected = ['ggggggg1', 'ffffff1', '', 'dddd1', 'ccc2', 'ccc1', 'bb2', 'bb1', '', '', ''] - self.assertListEqual(expected, l_df['vals'].data[:]) - self.assertListEqual(expected, results[0].data[:]) + results = s.merge_right(left_on=l_df2['id'], right_on=r_df['id'], + left_fields=(l_df2['vals'],)) + self.assertListEqual(expected, r_df['vals'].data[:]) + self.assertListEqual(expected, results[0].data[:]) - def test_merge_indexed_fields_right(self): r_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') l_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') l_vals = ['bb1', 'ccc1', '', 'dddd1', 'ggggggg1', 'ffffff1', 'bb2', '', 'ccc2'] + expected = ['', '', '', 'bb1', 'bb2', 'ccc1', 'ccc2', 'dddd1', '', 'ffffff1', 'ggggggg1'] + _perform_test(r_id, l_id, l_vals, expected) - bio1 = BytesIO() - bio2 = BytesIO() - with session.Session() as s: - r_ds = s.open_dataset(bio1, 'w', 'r_ds') - r_df = r_ds.create_dataframe('r_df') - r_df.create_numeric('id', 'int32').data.write(r_id) - l_ds = s.open_dataset(bio2, 'w', 'l_ds') - - l_df = l_ds.create_dataframe('l_df') - l_df.create_numeric('id', 'int32').data.write(l_id) - l_df.create_indexed_string('vals').data.write(l_vals) - - s.merge_right(left_on=l_df['id'], right_on=r_df['id'], - left_fields=(l_df['vals'],), - left_writers=(r_df.create_indexed_string('vals'),)) - - l_df2 = l_ds.create_dataframe('l_df2') - l_df2.create_numeric('id', 'int32').data.write(l_id) - l_df2.create_indexed_string('vals').data.write(l_vals) - - results = s.merge_right(left_on=l_df2['id'], right_on=r_df['id'], - left_fields=(l_df2['vals'],)) + r_id = r_id[::-1] + l_id = l_id[::-1] + l_vals = l_vals[::-1] + expected = ['ggggggg1', 'ffffff1', '', 'dddd1', 'ccc2', 'ccc1', 'bb2', 'bb1', '', '', ''] + _perform_test(r_id, l_id, l_vals, expected) + + + def test_merge_indexed_fields_inner(self): + + def _perform_inner(r_id, r_vals, l_id, l_vals, l_expected, r_expected): + bio1 = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio1, 'w', 'r_ds') + r_df = ds.create_dataframe('r_df') + r_df.create_numeric('id', 'int32').data.write(r_id) + r_df.create_indexed_string('vals').data.write(r_vals) + + l_df = ds.create_dataframe('l_df') + l_df.create_numeric('id', 'int32').data.write(l_id) + l_df.create_indexed_string('vals').data.write(l_vals) + + i_df = ds.create_dataframe('i_df') + i_df.create_numeric('l_id', 'int32') + i_df.create_numeric('r_id', 'int32') + i_df.create_indexed_string('l_vals') + i_df.create_indexed_string('r_vals') + s.merge_inner(left_on=l_df['id'], right_on=r_df['id'], + left_fields=(l_df['id'], l_df['vals'],), + left_writers=(i_df['l_id'], i_df['l_vals']), + right_fields=(r_df['id'], r_df['vals']), + right_writers=(i_df['r_id'], i_df['r_vals'])) + print(i_df['l_id'].data[:]) + print(i_df['r_id'].data[:]) + print(i_df['l_vals'].data[:]) + print(i_df['r_vals'].data[:]) + + results = s.merge_inner(left_on=l_df['id'], right_on=r_df['id'], + left_fields=(l_df['id'], l_df['vals']), + right_fields=(r_df['id'], r_df['vals'])) + print(results) + + expected = ['', '', '', 'bb1', 'bb2', 'ccc1', 'ccc2', 'dddd1', '', 'ffffff1', 'ggggggg1'] + # self.assertListEqual(expected, r_df['vals'].data[:]) + # self.assertListEqual(expected, results[0].data[:]) - expected = ['', '', '', 'bb1', 'bb2', 'ccc1', 'ccc2', 'dddd1', '', 'ffffff1', 'ggggggg1'] - self.assertListEqual(expected, r_df['vals'].data[:]) - self.assertListEqual(expected, results[0].data[:]) + r_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') + r_vals = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven'] + l_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') + l_vals = ['bb1', 'ccc1', '', 'dddd1', 'ggggggg1', 'ffffff1', 'bb2', '', 'ccc2'] + _perform_inner(r_id, r_vals, l_id, l_vals, None, None) r_id = r_id[::-1] + r_vals = r_vals[::-1] l_id = l_id[::-1] l_vals = l_vals[::-1] - - bio1 = BytesIO() - bio2 = BytesIO() - with session.Session() as s: - r_ds = s.open_dataset(bio1, 'w', 'r_ds') - r_df = r_ds.create_dataframe('r_df') - r_df.create_numeric('id', 'int32').data.write(r_id) - l_ds = s.open_dataset(bio2, 'w', 'l_ds') - - l_df = l_ds.create_dataframe('l_df') - l_df.create_numeric('id', 'int32').data.write(l_id) - l_df.create_indexed_string('vals').data.write(l_vals) - - s.merge_right(left_on=l_df['id'], right_on=r_df['id'], - left_fields=(l_df['vals'],), - left_writers=(r_df.create_indexed_string('vals'),)) - - l_df2 = l_ds.create_dataframe('l_df2') - l_df2.create_numeric('id', 'int32').data.write(l_id) - l_df2.create_indexed_string('vals').data.write(l_vals) - - results = s.merge_right(left_on=l_df2['id'], right_on=r_df['id'], - left_fields=(l_df2['vals'],)) - - expected = ['ggggggg1', 'ffffff1', '', 'dddd1', 'ccc2', 'ccc1', 'bb2', 'bb1', '', '', ''] - self.assertListEqual(expected, r_df['vals'].data[:]) - self.assertListEqual(expected, results[0].data[:]) - - - # def test_ordered_merge_indexed_fields(self): - # l_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') - # r_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') - # r_vals = ['bb1', 'ccc1', '', 'dddd1', 'ggggggg1', 'ffffff1', 'bb2', '', 'ccc2'] - # bio1 = BytesIO() - # bio2 = BytesIO() - # with session.Session() as s: - # l_ds = s.open_dataset(bio1, 'w', 'l_ds') - # l_df = l_ds.create_dataframe('l_df') - # l_df.create_numeric('id', 'int32').data.write(l_id) - # r_ds = s.open_dataset(bio2, 'w', 'r_ds') - # - # r_df = r_ds.create_dataframe('r_df') - # r_df.create_numeric('id', 'int32').data.write(r_id) - # r_df.create_indexed_string('vals').data.write(r_vals) - # r_indices = s.dataset_sort_index((r_df['id'],)) - # r_df.apply_index(r_indices) - # print(r_df['id'].data[:]) - # print(r_df['vals'].data[:]) - # - # s.ordered_merge_left(left_on=l_df['id'], right_on=r_df['id'], - # right_field_sources=(r_df['vals'],), - # left_field_sinks=(l_df.create_indexed_string('vals'),)) - # - # r_df2 = r_ds.create_dataframe('r_df2') - # r_df2.create_numeric('id', 'int32').data.write(r_id) - # r_df2.create_indexed_string('vals').data.write(r_vals) - # - # results = s.ordered_merge_left(left_on=l_df['id'], right_on=r_df2['id'], - # right_field_sources=(r_df2['vals'],)) - # - # print(l_df['vals'].data[:]) - # print(results[0].data[:]) + _perform_inner(r_id, r_vals, l_id, l_vals, None, None) class TestSessionJoin(unittest.TestCase): From e8edd9d195389e5e3f54c45a53ca2546d4dba81f Mon Sep 17 00:00:00 2001 From: clyyuanzi-london <59363720+clyyuanzi-london@users.noreply.github.com> Date: Tue, 20 Apr 2021 10:15:17 +0100 Subject: [PATCH 082/181] concate cat keys instead of padding --- exetera/core/csv_reader_speedup.py | 101 ++++----- exetera/core/importer.py | 321 ++++++++++------------------- 2 files changed, 152 insertions(+), 270 deletions(-) diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py index a0c393de..6df21b4b 100644 --- a/exetera/core/csv_reader_speedup.py +++ b/exetera/core/csv_reader_speedup.py @@ -79,23 +79,29 @@ def file_read_line_fast_csv(source): with open(source) as f: header = csv.DictReader(f) count_columns = len(header.fieldnames) - content = f.read() - count_rows = content.count('\n') + 1 - - content = np.fromfile(source, dtype='|S1')#np.uint8) - column_inds = np.zeros((count_columns, count_rows), dtype=np.int64) - column_vals = np.zeros((count_columns, count_rows * 25), dtype=np.uint8) + + count_rows = sum(1 for _ in f) # w/o header row + #content = f.read() + # count_rows = content.count('\n') + 1 # +1: for the case that last line doesn't have \n + + column_inds = np.zeros((count_columns, count_rows + 1), dtype=np.int64) # add one more row for initial index 0 + # change it to longest key + column_vals = np.zeros((count_columns, count_rows * 100), dtype=np.uint8) ESCAPE_VALUE = np.frombuffer(b'"', dtype='S1')[0][0] SEPARATOR_VALUE = np.frombuffer(b',', dtype='S1')[0][0] NEWLINE_VALUE = np.frombuffer(b'\n', dtype='S1')[0][0] + #print(lineterminator.tobytes()) + #print("hello") + #CARRIAGE_RETURN_VALUE = np.frombuffer(b'\r', dtype='S1')[0][0] + print("test") with Timer("my_fast_csv_reader_int"): - content = np.fromfile(source, dtype=np.uint8) my_fast_csv_reader_int(content, column_inds, column_vals, ESCAPE_VALUE, SEPARATOR_VALUE, NEWLINE_VALUE) + return column_inds, column_vals @@ -154,7 +160,10 @@ def make_test_data(count, schema): @njit def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separator_value, newline_value): - colcount = len(column_inds[0]) + colcount = len(column_inds) + maxrowcount = len(column_inds[0]) - 1 # minus extra index 0 row that created for column_inds + print('colcount', colcount) + print('maxrowcount', maxrowcount) index = np.int64(0) line_start = np.int64(0) @@ -169,6 +178,7 @@ def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separ end_line = False escaped_literal_candidate = False cur_cell_start = column_inds[col_index, row_index] if row_index >= 0 else 0 + cur_cell_char_count = 0 while True: write_char = False @@ -176,13 +186,14 @@ def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separ end_line = False c = source[index] + if c == separator_value: if not escaped: end_cell = True else: write_char = True - elif c == newline_value: + elif c == newline_value : if not escaped: end_cell = True end_line = True @@ -197,13 +208,24 @@ def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separ column_vals[col_index, cur_cell_start + cur_cell_char_count] = c cur_cell_char_count += 1 + # if col_index == 5: + # print('%%%%%%') + # print(c) + if end_cell: if row_index >= 0: - column_inds[col_index, row_index+1] = cur_cell_start + cur_cell_char_count + column_inds[col_index, row_index + 1] = cur_cell_start + cur_cell_char_count + # print("========") + # print(col_index, row_index + 1, column_vals.shape) + # print(column_inds) + # print(column_vals) + # print("========") if end_line: row_index += 1 col_index = 0 - + # print('~~~~~~~~~~~') + # print(col_index, row_index) + # print('~~~~~~~~~~~') else: col_index += 1 @@ -213,59 +235,16 @@ def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separ index += 1 if index == len(source): - break - - -""" -original categories: -"one", "two", "three", "four", "five" -0 , 1 , 2 , 3 , 4 - -sorted categories -"five", "four", "one", "three", "two" - -sorted category map -4, 3, 0, 2, 1 - -lengths of sorted categories -4, 4, 3, 5, 3 - -sorted category indexed string - -scindex = [0, 4, 8, 11, 16, 19] -scvalues = [fivefouronethreetwo] - -col_inds = value_index[col_index,...] - -def my_fast_categorical_mapper(...): - for e in range(len(rows_read)-1): - key_start = value_inds[col_index, e] - key_end = value_inds[col_index, e+1] - key_len = key_end - key_start - - for i in range(1, len(scindex)): - skeylen = scindex[i] - scindex[i - 1] - if skeylen == len(key): - index = i - for j in range(keylen): - entry_start = scindex[i-1] - if value_inds[col_index, key_start + j] != scvalues[entry_start + j]: - index = -1 - break - - if index != -1: - destination_vals[e] = index - - - - - - - + if col_index == colcount - 1: #and row_index == maxrowcount - 1: + # print('excuese me') + column_inds[col_index, row_index + 1] = cur_cell_start + cur_cell_char_count + # print('source', source, 'len_source', len(source), len(source)) + # print('index', cur_cell_start + cur_cell_char_count) + # print('break',col_index, row_index) + break -""" if __name__ == "__main__": main() diff --git a/exetera/core/importer.py b/exetera/core/importer.py index bdc0de07..e0cf2764 100644 --- a/exetera/core/importer.py +++ b/exetera/core/importer.py @@ -126,12 +126,33 @@ def __init__(self, datastore, source, hf, space, schema, timestamp, keys=None, stop_after=None, show_progress_every=None, filter_fn=None, early_filter=None): + + old = False + if old: + self.old(datastore, source, hf, space, schema, timestamp, + include, exclude, + keys, + stop_after, show_progress_every, filter_fn, + early_filter) + else: + self.nnnn(datastore, source, hf, space, schema, timestamp, + include, exclude, + keys, + stop_after, show_progress_every, filter_fn, + early_filter) + + def nnnn(self, datastore, source, hf, space, schema, timestamp, + include=None, exclude=None, + keys=None, + stop_after=None, show_progress_every=None, filter_fn=None, + early_filter=None): # self.names_ = list() self.index_ = None #stop_after = 2000000 file_read_line_fast_csv(source) + #exit() time0 = time.time() @@ -150,9 +171,11 @@ def __init__(self, datastore, source, hf, space, schema, timestamp, if space in exclude and len(exclude[space]) > 0: available_keys = [k for k in available_keys if k not in exclude[space]] + available_keys = ['ruc11cd','ruc11'] #available_keys = ['ruc11'] + if not keys: fields_to_use = available_keys # index_map = [csvf.fieldnames.index(k) for k in fields_to_use] @@ -193,34 +216,39 @@ def __init__(self, datastore, source, hf, space, schema, timestamp, string_map = sch.strings_to_values byte_map = None - if sch.out_of_range_label is None and string_map: - #byte_map = { key : string_map[key] for key in string_map.keys() } - t = [np.fromstring(x, dtype=np.uint8) for x in string_map.keys()] - longest_key = len(max(t, key=len)) - - byte_map = np.zeros(longest_key * len(t) , dtype=np.uint8) - print('string_map', string_map) - print("longest_key", longest_key) + if sch.out_of_range_label is None and string_map: + # sort by length of key first, and then sort alphabetically + sorted_string_map = {k: v for k, v in sorted(string_map.items(), key=lambda item: (len(item[0]), item[0]))} + sorted_string_key = [(len(k), np.frombuffer(k.encode(), dtype=np.uint8), v) for k, v in sorted_string_map.items()] + sorted_string_values = list(sorted_string_map.values()) + + # assign byte_map_key_lengths, byte_map_value + byte_map_key_lengths = np.zeros(len(sorted_string_map), dtype=np.uint8) + byte_map_value = np.zeros(len(sorted_string_map), dtype=np.uint8) - start_pos = 0 - for x_id, x in enumerate(t): - for c_id, c in enumerate(x): - byte_map[start_pos + c_id] = c - start_pos += longest_key + for i, (length, _, v) in enumerate(sorted_string_key): + byte_map_key_lengths[i] = length + byte_map_value[i] = v - print(byte_map) + # assign byte_map_keys, byte_map_key_indices + byte_map_keys = np.zeros(sum(byte_map_key_lengths), dtype=np.uint8) + byte_map_key_indices = np.zeros(len(sorted_string_map)+1, dtype=np.uint8) + idx_pointer = 0 + for i, (_, b_key, _) in enumerate(sorted_string_key): + for b in b_key: + byte_map_keys[idx_pointer] = b + idx_pointer += 1 + + byte_map_key_indices[i + 1] = idx_pointer - #for key in sorted(string_map.keys()): - # byte_map.append(np.fromstring(key, dtype=np.uint8)) - #byte_map = [np.fromstring(key, dtype=np.uint8) for key in sorted(string_map.keys())] - #byte_map.sort() + byte_map = [byte_map_keys, byte_map_key_lengths, byte_map_key_indices, byte_map_value] - longest_keys.append(longest_key) categorical_map_list.append(byte_map) + new_fields[field_name] = writer new_field_list.append(writer) field_chunk_list.append(writer.chunk_factory(chunk_size)) @@ -231,11 +259,8 @@ def __init__(self, datastore, source, hf, space, schema, timestamp, chunk_index = 0 - key_to_search = np.fromstring('Urban city and twn', dtype=np.uint8) - #print("key to search") - #print(key_to_search) + total_col = [] - print(index_map) for ith, i_c in enumerate(index_map): chunk_index = 0 @@ -252,57 +277,69 @@ def __init__(self, datastore, source, hf, space, schema, timestamp, categorical_map = None if len(categorical_map_list) > ith: - categorical_map = categorical_map_list[ith] - - indices = column_ids[i_c] - values = column_vals[i_c] - - @njit - def findFirst_basic(a, b, div): - for i in range(0, len(a), div): - #i = i*longest_key - result = True - for j in range(len(b)): - result = result and (a[i+j] == b[j]) - if not result: - break - if result: - return i - return 0 - - @njit - def map_values(chunk, indices, cat_map, div): - #print(indices) - size = 0 - for row_ix in range(len(indices) - 1): - temp_val = values[indices[row_ix] : indices[row_ix+1]] - internal_val = findFirst_basic(categorical_map, temp_val, div) // div - chunk[row_ix] = internal_val - size += 1 - return size - - #print("i_c", i_c, categorical_map) - chunk = np.zeros(chunk_size, dtype=np.uint8) + cat_keys, cat_key_len, cat_index, cat_values = categorical_map_list[ith] + + @njit + def my_fast_categorical_mapper(chunk, chunk_index, chunk_size, cat_keys, cat_key_len, cat_index, cat_values): + error_row_idx = -1 + for row_idx in range(chunk_size): + # Finds length, which we use to lookup potential matches + key_start = column_ids[i_c, chunk_index + row_idx] + key_end = column_ids[i_c, chunk_index + row_idx + 1] + key_len = key_end - key_start + + # start_idx = np.searchsorted(cat_key_len, key_len, "left") + # stop_idx = np.searchsorted(cat_key_len, key_len, "right") + + # print('key_start', key_start, 'key_end', key_end) + # print('start_idx', start_idx, 'stop_idx', stop_idx) + + for i in range(len(cat_index) - 1): + sc_key_len = cat_index[i + 1] - cat_index[i] + + if key_len != sc_key_len: + continue - total = [] + index = i + for j in range(key_len): + entry_start = cat_index[i] + if column_vals[i_c, key_start + j] != cat_keys[entry_start + j]: + index = -1 + break + + if index != -1: + chunk[row_idx] = cat_values[index] - # NEED TO NOT WRITE THE WHOLE CHUNK.. as the counter shows too many 0! + return error_row_idx + + total = [] chunk_index = 0 - while chunk_index < len(indices): - size = map_values(chunk, indices[chunk_index:chunk_index+chunk_size], categorical_map, longest_keys[ith]) + indices_len = len(column_ids[i_c]) + + # print('@@@@@') + # print('column_ids', 'i_c', i_c, column_ids) + # print('column_vals', 'i_c', i_c, column_vals) + # print('@@@@@') + while chunk_index < indices_len: + if chunk_index + chunk_size > indices_len: + chunk_size = indices_len - chunk_index - data = chunk[:size] + #print('chunk_size', chunk_size) - new_field_list[ith].write_part(data) - total.extend(data) + chunk = np.zeros(chunk_size, dtype=np.uint8) + + my_fast_categorical_mapper(chunk, chunk_index, chunk_size, cat_keys, cat_key_len, cat_index, cat_values) + new_field_list[ith].write_part(chunk) + total.extend(chunk) chunk_index += chunk_size - print("idx", chunk_index) + total_col.append(total) print("i_c", i_c, Counter(total)) + if chunk_index != 0: new_field_list[ith].write_part(chunk[:chunk_index]) #total.extend(chunk[:chunk_index]) @@ -310,152 +347,13 @@ def map_values(chunk, indices, cat_map, div): for i_df in range(len(index_map)): new_field_list[i_df].flush() - + print(f"Total time {time.time() - time0}s") #exit() - def __ainit__(self, datastore, source, hf, space, schema, timestamp, - include=None, exclude=None, - keys=None, - stop_after=None, show_progress_every=None, filter_fn=None, - early_filter=None): - # self.names_ = list() - self.index_ = None - - #stop_after = 2000000 - - file_read_line_fast_csv(source) - - time0 = time.time() - - seen_ids = set() - - if space not in hf.keys(): - hf.create_group(space) - group = hf[space] - - with open(source) as sf: - csvf = csv.DictReader(sf, delimiter=',', quotechar='"') - - available_keys = [k.strip() for k in csvf.fieldnames if k.strip() in schema.fields] - if space in include and len(include[space]) > 0: - available_keys = include[space] - if space in exclude and len(exclude[space]) > 0: - available_keys = [k for k in available_keys if k not in exclude[space]] - - available_keys = ['ruc11cd','ruc11'] - - if not keys: - fields_to_use = available_keys - # index_map = [csvf.fieldnames.index(k) for k in fields_to_use] - # index_map = [i for i in range(len(fields_to_use))] - else: - for k in keys: - if k not in available_keys: - raise ValueError(f"key '{k}' isn't in the available keys ({keys})") - fields_to_use = keys - # index_map = [csvf.fieldnames.index(k) for k in fields_to_use] - - - csvf_fieldnames = [k.strip() for k in csvf.fieldnames] - index_map = [csvf_fieldnames.index(k) for k in fields_to_use] - - early_key_index = None - if early_filter is not None: - if early_filter[0] not in available_keys: - raise ValueError( - f"'early_filter': tuple element zero must be a key that is in the dataset") - early_key_index = available_keys.index(early_filter[0]) - - chunk_size = 1 << 20 - new_fields = dict() - new_field_list = list() - field_chunk_list = list() - categorical_map_list = list() - - # TODO: categorical writers should use the datatype specified in the schema - for i_n in range(len(fields_to_use)): - field_name = fields_to_use[i_n] - sch = schema.fields[field_name] - writer = sch.importer(datastore, group, field_name, timestamp) - # TODO: this list is required because we convert the categorical values to - # numerical values ahead of adding them. We could use importers that handle - # that transform internally instead - - string_map = sch.strings_to_values - if sch.out_of_range_label is None and string_map: - byte_map = { str.encode(key) : string_map[key] for key in string_map.keys() } - else: - byte_map = None - - categorical_map_list.append(byte_map) - - new_fields[field_name] = writer - new_field_list.append(writer) - field_chunk_list.append(writer.chunk_factory(chunk_size)) - - column_ids, column_vals = file_read_line_fast_csv(source) - - print(f"CSV read {time.time() - time0}s") - - chunk_index = 0 - - for ith, i_c in enumerate(index_map): - chunk_index = 0 - - col = column_ids[i_c] - - if show_progress_every: - if i_c % 1 == 0: - print(f"{i_c} cols parsed in {time.time() - time0}s") - - if early_filter is not None: - if not early_filter[1](row[early_key_index]): - continue - - if i_c == stop_after: - break - - categorical_map = None - if len(categorical_map_list) > ith: - categorical_map = categorical_map_list[ith] - - a = column_vals[i_c].copy() - - for row_ix in range(len(col) - 1): - val = a[col[row_ix] : col[row_ix+1]].tobytes() - - if categorical_map is not None: - if val not in categorical_map: - #print(i_c, row_ix) - error = "'{}' not valid: must be one of {} for field '{}'" - raise KeyError( - error.format(val, categorical_map, available_keys[i_c])) - val = categorical_map[val] - - field_chunk_list[ith][chunk_index] = val - - chunk_index += 1 - - if chunk_index == chunk_size: - new_field_list[ith].write_part(field_chunk_list[ith]) - - chunk_index = 0 - - #print(f"Total time {time.time() - time0}s") - - if chunk_index != 0: - for ith in range(len(index_map)): - new_field_list[ith].write_part(field_chunk_list[ith][:chunk_index]) - - for ith in range(len(index_map)): - new_field_list[ith].flush() - - print(f"Total time {time.time() - time0}s") - - - def __ainit__(self, datastore, source, hf, space, schema, timestamp, + + def old(self, datastore, source, hf, space, schema, timestamp, include=None, exclude=None, keys=None, stop_after=None, show_progress_every=None, filter_fn=None, @@ -481,7 +379,7 @@ def __ainit__(self, datastore, source, hf, space, schema, timestamp, if space in exclude and len(exclude[space]) > 0: available_keys = [k for k in available_keys if k not in exclude[space]] - available_keys = ['ruc11'] + available_keys = ['ruc11cd'] available_keys = ['ruc11cd','ruc11'] # available_keys = csvf.fieldnames @@ -532,7 +430,7 @@ def __ainit__(self, datastore, source, hf, space, schema, timestamp, chunk_index = 0 try: - total = [] + total = [[],[]] for i_r, row in enumerate(ecsvf): if show_progress_every: if i_r % show_progress_every == 0: @@ -561,7 +459,7 @@ def __ainit__(self, datastore, source, hf, space, schema, timestamp, for i_df in range(len(index_map)): # with utils.Timer("writing to {}".format(self.names_[i_df])): # new_field_list[i_df].write_part(field_chunk_list[i_df]) - total.extend(field_chunk_list[i_df]) + total[i_df].extend(field_chunk_list[i_df]) new_field_list[i_df].write_part(field_chunk_list[i_df]) chunk_index = 0 @@ -569,14 +467,19 @@ def __ainit__(self, datastore, source, hf, space, schema, timestamp, except Exception as e: msg = "row {}: caught exception {}\nprevious row {}" print(msg.format(i_r + 1, e, row)) + + raise if chunk_index != 0: for i_df in range(len(index_map)): new_field_list[i_df].write_part(field_chunk_list[i_df][:chunk_index]) - total.extend(field_chunk_list[i_df][:chunk_index]) + total[i_df].extend(field_chunk_list[i_df][:chunk_index]) - print("i_df", i_df, Counter(total)) + print("i_df", i_df, Counter(total[i_df])) + + print('ruc == ruc11cd', total[0] == total[1]) + for i_df in range(len(index_map)): new_field_list[i_df].flush() From c2ba9ff61c49e0c0614c4ebb5c8464258ddaefd7 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 20 Apr 2021 10:16:14 +0100 Subject: [PATCH 083/181] some docstring for fields --- exetera/core/fields.py | 49 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index e43d5fc2..a5f77c08 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -23,6 +23,15 @@ class HDF5Field(Field): def __init__(self, session, group, name=None, write_enabled=False): + """ + Construct a HDF5 file based Field. This construction is not used directly, rather, should be called from + specific field types, e.g. NumericField. + + :param session: The session instance. + :param group: The HDF5 Group object. + :param name: The name of this field if not specified in group. + :param write_enabled: A read-only/read-write switch. + """ super().__init__() if name is None: @@ -71,6 +80,12 @@ def apply_index(self, index_to_apply, dstfld=None): class MemoryField(Field): def __init__(self, session): + """ + Construct a field stored in memory only, often used when perform arithmetic/comparison operations from storage + based fields, e.g. field3 = field1 + field2 will create a memory field during add operation and assign to field3. + + :param session: The session instance. + """ super().__init__() self._session = session self._write_enabled = True @@ -109,8 +124,18 @@ def apply_index(self, index_to_apply, dstfld=None): raise NotImplementedError("Please use apply_index() on specific fields, not the field base class.") +# Field arrays +# ============ + + class ReadOnlyFieldArray: def __init__(self, field, dataset_name): + """ + Construct a readonly FieldArray which used as the wrapper of data in Fields (apart from IndexedStringFields). + + :param field: The HDF5 group object used as storage. + :param dataset_name: The name of the dataset object in HDF5, normally use 'values' + """ self._field = field self._name = dataset_name self._dataset = field[dataset_name] @@ -146,11 +171,14 @@ def complete(self): "for a writeable copy of the field") -# Field arrays -# ============ - class WriteableFieldArray: def __init__(self, field, dataset_name): + """ + Construct a read/write FieldArray which used as the wrapper of data in Field. + + :param field: The HDF5 group object used as storage. + :param dataset_name: The name of the dataset object in HDF5, normally use 'values' + """ self._field = field self._name = dataset_name self._dataset = field[dataset_name] @@ -188,8 +216,12 @@ def complete(self): class MemoryFieldArray: - def __init__(self, dtype): + """ + Construct a memory based FieldArray which used as the wrapper of data in Field. The data is stored in numpy array. + + :param dtype: The data type for construct the numpy array. + """ self._dtype = dtype self._dataset = None @@ -235,6 +267,13 @@ def complete(self): class ReadOnlyIndexedFieldArray: def __init__(self, field, indices, values): + """ + Construct a IndexFieldArray which used as the wrapper of data in IndexedStringField. + + :param field: The HDF5 group object for store the data. + :param indices: The indices of the IndexedStringField. + :param values: The values of the IndexedStringField. + """ self._field = field self._indices = indices self._values = values @@ -496,6 +535,7 @@ def apply_spans_min(self, spans_to_apply, target=None, in_place=False): def apply_spans_max(self, spans_to_apply, target=None, in_place=False): return FieldDataOps.apply_spans_max(self, spans_to_apply, target, in_place) + class FixedStringMemField(MemoryField): def __init__(self, session, length): super().__init__(session) @@ -579,7 +619,6 @@ def __init__(self, session, nformat): super().__init__(session) self._nformat = nformat - def writeable(self): return self From 1a198151fe1505c8b5ebae4f5a2abf85f1fc5cfb Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 20 Apr 2021 10:33:58 +0100 Subject: [PATCH 084/181] dataframe copy/move/drop and unittest --- exetera/core/dataframe.py | 4 ++-- tests/test_dataframe.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 411f607f..55e064d6 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -337,5 +337,5 @@ def move(src_df: DataFrame, field: fld.Field, dest_df: DataFrame, name: str): :param dest_df: The destination dataframe to move to. :param name: The name of field under destination dataframe. """ - HDF5DataFrame.copy(field, dest_df, name) - HDF5DataFrame.drop(src_df, field) + copy(field, dest_df, name) + drop(src_df, field) diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 42cfd3a9..99b0b475 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -202,11 +202,11 @@ def test_datafrmae_static_methods(self): numf.data.write([5, 4, 3, 2, 1]) df2 = dst.create_dataframe('df2') - dataframe.HDF5DataFrame.copy(numf, df2,'numf') + dataframe.copy(numf, df2,'numf') self.assertListEqual([5, 4, 3, 2, 1], df2['numf'].data[:].tolist()) - dataframe.HDF5DataFrame.drop(df, numf) + dataframe.drop(df, numf) self.assertTrue('numf' not in df) - dataframe.HDF5DataFrame.move(df2,df2['numf'],df,'numf') + dataframe.move(df2,df2['numf'],df,'numf') self.assertTrue('numf' not in df2) self.assertListEqual([5, 4, 3, 2, 1], df['numf'].data[:].tolist()) From 1fb036225bb7de0336fdb960010670510eb224fb Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Tue, 20 Apr 2021 11:05:04 +0100 Subject: [PATCH 085/181] Fixing issue with dataframe move/copy being static --- exetera/core/abstract_types.py | 5 ++ exetera/core/dataframe.py | 118 ++++++++++++++++++--------------- exetera/core/fields.py | 70 +++++++++++-------- exetera/core/session.py | 12 ++-- tests/test_dataframe.py | 11 +-- tests/test_session.py | 30 +++++---- 6 files changed, 140 insertions(+), 106 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index b4b73c47..554bb1c0 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -25,6 +25,11 @@ def name(self): def timestamp(self): raise NotImplementedError() + @property + @abstractmethod + def dataframe(self): + raise NotImplementedError() + @property @abstractmethod def chunksize(self): diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index e7f18385..0929517c 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -80,6 +80,10 @@ def add(self, field): nfield.data.write(field.data[:]) self._columns[dname] = nfield + def drop(self, name): + del self._columns[name] + del self._h5group[name] + def create_group(self, name): """ Create a group object in HDF5 file for field to use. Please note, this function is for @@ -91,22 +95,13 @@ def create_group(self, name): self._h5group.create_group(name) return self._h5group[name] - def create_numeric(self, name, nformat, timestamp=None, chunksize=None): - """ - Create a numeric type field. - """ - fld.numeric_field_constructor(self._dataset.session, self, name, nformat, timestamp, chunksize) - field = fld.NumericField(self._dataset.session, self._h5group[name], - write_enabled=True) - self._columns[name] = field - return self._columns[name] - def create_indexed_string(self, name, timestamp=None, chunksize=None): """ Create a indexed string type field. """ - fld.indexed_string_field_constructor(self._dataset.session, self, name, timestamp, chunksize) - field = fld.IndexedStringField(self._dataset.session, self._h5group[name], + fld.indexed_string_field_constructor(self._dataset.session, self, name, + timestamp, chunksize) + field = fld.IndexedStringField(self._dataset.session, self._h5group[name], self, name, write_enabled=True) self._columns[name] = field return self._columns[name] @@ -115,19 +110,31 @@ def create_fixed_string(self, name, length, timestamp=None, chunksize=None): """ Create a fixed string type field. """ - fld.fixed_string_field_constructor(self._dataset.session, self, name, length, timestamp, chunksize) - field = fld.FixedStringField(self._dataset.session, self._h5group[name], + fld.fixed_string_field_constructor(self._dataset.session, self, name, + length, timestamp, chunksize) + field = fld.FixedStringField(self._dataset.session, self._h5group[name], self, name, write_enabled=True) self._columns[name] = field return self._columns[name] + def create_numeric(self, name, nformat, timestamp=None, chunksize=None): + """ + Create a numeric type field. + """ + fld.numeric_field_constructor(self._dataset.session, self, name, + nformat, timestamp, chunksize) + field = fld.NumericField(self._dataset.session, self._h5group[name], self, name, + write_enabled=True) + self._columns[name] = field + return self._columns[name] + def create_categorical(self, name, nformat, key, timestamp=None, chunksize=None): """ Create a categorical type field. """ fld.categorical_field_constructor(self._dataset.session, self, name, nformat, key, timestamp, chunksize) - field = fld.CategoricalField(self._dataset.session, self._h5group[name], + field = fld.CategoricalField(self._dataset.session, self._h5group[name], self, name, write_enabled=True) self._columns[name] = field return self._columns[name] @@ -136,8 +143,9 @@ def create_timestamp(self, name, timestamp=None, chunksize=None): """ Create a timestamp type field. """ - fld.timestamp_field_constructor(self._dataset.session, self, name, timestamp, chunksize) - field = fld.TimestampField(self._dataset.session, self._h5group[name], + fld.timestamp_field_constructor(self._dataset.session, self, name, + timestamp, chunksize) + field = fld.TimestampField(self._dataset.session, self._h5group[name], self, name, write_enabled=True) self._columns[name] = field return self._columns[name] @@ -225,7 +233,9 @@ def delete_field(self, field): :param field: The field to delete from this dataframe. """ - name = field.name[field.name.index('/', 1)+1:] + if field.dataframe != self: + raise ValueError("This field is owned by a different dataframe") + name = field.name if name is None: raise ValueError("This dataframe does not contain the field to delete.") else: @@ -300,42 +310,42 @@ def apply_index(self, index_to_apply, ddf=None): field.apply_index(index_to_apply, in_place=True) return self - @staticmethod - def copy(field: fld.Field, dataframe: DataFrame, name: str): - """ - Copy a field to another dataframe as well as underlying dataset. - - :param field: The source field to copy. - :param dataframe: The destination dataframe to copy to. - :param name: The name of field under destination dataframe. - """ - dfield = field.create_like(dataframe, name) - if field.indexed: - dfield.indices.write(field.indices[:]) - dfield.values.write(field.values[:]) - else: - dfield.data.write(field.data[:]) - dataframe.columns[name] = dfield - - @staticmethod - def drop(dataframe: DataFrame, field: fld.Field): - """ - Drop a field from a dataframe. - :param dataframe: The dataframe where field is located. - :param field: The field to delete. - """ - dataframe.delete_field(field) +def copy(field: fld.Field, dataframe: DataFrame, name: str): + """ + Copy a field to another dataframe as well as underlying dataset. - @staticmethod - def move(src_df: DataFrame, field: fld.Field, dest_df: DataFrame, name: str): - """ - Move a field to another dataframe as well as underlying dataset. + :param field: The source field to copy. + :param dataframe: The destination dataframe to copy to. + :param name: The name of field under destination dataframe. + """ + dfield = field.create_like(dataframe, name) + if field.indexed: + dfield.indices.write(field.indices[:]) + dfield.values.write(field.values[:]) + else: + dfield.data.write(field.data[:]) + dataframe.columns[name] = dfield + + +# def drop(dataframe: DataFrame, field: fld.Field): +# """ +# Drop a field from a dataframe. +# +# :param dataframe: The dataframe where field is located. +# :param field: The field to delete. +# """ +# dataframe.delete_field(field) + + +def move(field: fld.Field, dest_df: DataFrame, name: str): + """ + Move a field to another dataframe as well as underlying dataset. - :param src_df: The source dataframe where the field is located. - :param field: The field to move. - :param dest_df: The destination dataframe to move to. - :param name: The name of field under destination dataframe. - """ - HDF5DataFrame.copy(field, dest_df, name) - HDF5DataFrame.drop(src_df, field) + :param src_df: The source dataframe where the field is located. + :param field: The field to move. + :param dest_df: The destination dataframe to move to. + :param name: The name of field under destination dataframe. + """ + copy(field, dest_df, name) + field.dataframe.drop(field.name) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 5cc04d79..7e700630 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -22,22 +22,28 @@ from exetera.core import validation as val class HDF5Field(Field): - def __init__(self, session, group, name=None, write_enabled=False): + def __init__(self, session, group, dataframe, name=None, write_enabled=False): super().__init__() - if name is None: - field = group - else: - field = group[name] + # if name is None: + # field = group + # else: + # field = group[name] self._session = session - self._field = field + self._field = group + self._name = name self._fieldtype = self._field.attrs['fieldtype'] + self._dataframe = dataframe self._write_enabled = write_enabled self._value_wrapper = None @property def name(self): - return self._field.name + return self._name + + @property + def dataframe(self): + return self._dataframe @property def timestamp(self): @@ -80,6 +86,10 @@ def __init__(self, session): def name(self): return None + @property + def dataframe(self): + return None + @property def timestamp(self): return None @@ -506,7 +516,7 @@ def __init__(self, session, length): self._length = length def writeable(self): - return FixedStringField(self._session, self._field, write_enabled=True) + return FixedStringField(self._session, self._field, None, write_enabled=True) def create_like(self, group=None, name=None, timestamp=None): return FieldDataOps.fixed_string_field_create_like(self, group, name, timestamp) @@ -1033,15 +1043,16 @@ def timestamp_field_constructor(session, group, name, timestamp=None, chunksize= class IndexedStringField(HDF5Field): - def __init__(self, session, group, name=None, write_enabled=False): - super().__init__(session, group, name=name, write_enabled=write_enabled) + def __init__(self, session, group, dataframe, name=None, write_enabled=False): + super().__init__(session, group, dataframe, name=name, write_enabled=write_enabled) self._session = session + self._dataframe = None self._data_wrapper = None self._index_wrapper = None self._value_wrapper = None def writeable(self): - return IndexedStringField(self._session, self._field, write_enabled=True) + return IndexedStringField(self._session, self._field, None, write_enabled=True) def create_like(self, group=None, name=None, timestamp=None): return FieldDataOps.indexed_string_create_like(self, group, name, timestamp) @@ -1139,8 +1150,8 @@ def apply_spans_max(self, spans_to_apply, target=None, in_place=False): class FixedStringField(HDF5Field): - def __init__(self, session, group, name=None, write_enabled=False): - super().__init__(session, group, name=name, write_enabled=write_enabled) + def __init__(self, session, group, dataframe, name=None, write_enabled=False): + super().__init__(session, group, dataframe, name=name, write_enabled=write_enabled) # TODO: caution; we may want to consider the issues with long-lived field instances getting # out of sync with their stored counterparts. Maybe a revision number of the stored field # is required that we can check to see if we are out of date. That or just make this a @@ -1148,7 +1159,7 @@ def __init__(self, session, group, name=None, write_enabled=False): self._length = self._field.attrs['strlen'] def writeable(self): - return FixedStringField(self._session, self._field, write_enabled=True) + return FixedStringField(self._session, self._field, None, write_enabled=True) def create_like(self, group=None, name=None, timestamp=None): return FieldDataOps.fixed_string_field_create_like(self, group, name, timestamp) @@ -1220,12 +1231,12 @@ def apply_spans_max(self, spans_to_apply, target=None, in_place=False): class NumericField(HDF5Field): - def __init__(self, session, group, name=None, mem_only=True, write_enabled=False): - super().__init__(session, group, name=name, write_enabled=write_enabled) + def __init__(self, session, group, dataframe, name, write_enabled=False): + super().__init__(session, group, dataframe, name=name, write_enabled=write_enabled) self._nformat = self._field.attrs['nformat'] def writeable(self): - return NumericField(self._session, self._field, write_enabled=True) + return NumericField(self._session, self._field, None, self._name, write_enabled=True) def create_like(self, group=None, name=None, timestamp=None): return FieldDataOps.numeric_field_create_like(self, group, name, timestamp) @@ -1375,9 +1386,8 @@ def __ge__(self, value): class CategoricalField(HDF5Field): - def __init__(self, session, group, - name=None, write_enabled=False): - super().__init__(session, group, name=name, write_enabled=write_enabled) + def __init__(self, session, group, dataframe, name=None, write_enabled=False): + super().__init__(session, group, dataframe, name=name, write_enabled=write_enabled) self._nformat = self._field.attrs['nformat'] if 'nformat' in self._field.attrs else 'int8' def writeable(self): @@ -1492,8 +1502,8 @@ def __ge__(self, value): class TimestampField(HDF5Field): - def __init__(self, session, group, name=None, write_enabled=False): - super().__init__(session, group, name=name, write_enabled=write_enabled) + def __init__(self, session, group, dataframe, name=None, write_enabled=False): + super().__init__(session, group, dataframe, name=name, write_enabled=write_enabled) def writeable(self): return TimestampField(self._session, self._field, write_enabled=True) @@ -1778,7 +1788,8 @@ def __init__(self, session, group, name, if optional is True: filter_name = '{}_set'.format(name) numeric_field_constructor(group, filter_name, 'bool', timestamp, chunksize) - self._filter_field = NumericField(session, group, filter_name, write_enabled=True) + self._filter_field = NumericField(session, group, None, filter_name, + write_enabled=True) def chunk_factory(self, length): return np.zeros(length, dtype='U32') @@ -1820,7 +1831,8 @@ def __init__(self, session, group, name, filter_name = '{}_set'.format(name) numeric_field_constructor(session, group, filter_name, 'bool', timestamp, chunksize) - self._filter_field = NumericField(session, group, filter_name, write_enabled=True) + self._filter_field = NumericField(session, group, None, filter_name, + write_enabled=True) def chunk_factory(self, length): return np.zeros(length, dtype='U10') @@ -2339,7 +2351,7 @@ def indexed_string_create_like(source, group, name, timestamp): if isinstance(group, h5py.Group): indexed_string_field_constructor(source._session, group, name, ts, source.chunksize) - return IndexedStringField(source._session, group[name], write_enabled=True) + return IndexedStringField(source._session, group[name], None, name, write_enabled=True) else: return group.create_indexed_string(name, ts, source.chunksize) @@ -2356,7 +2368,7 @@ def fixed_string_field_create_like(source, group, name, timestamp): if isinstance(group, h5py.Group): fixed_string_field_constructor(source._session, group, name, length, ts, source.chunksize) - return FixedStringField(source._session, group[name], write_enabled=True) + return FixedStringField(source._session, group[name], None, name, write_enabled=True) else: return group.create_fixed_string(name, length, ts) @@ -2373,7 +2385,7 @@ def numeric_field_create_like(source, group, name, timestamp): if isinstance(group, h5py.Group): numeric_field_constructor(source._session, group, name, nformat, ts, source.chunksize) - return NumericField(source._session, group[name], write_enabled=True) + return NumericField(source._session, group[name], None, name, write_enabled=True) else: return group.create_numeric(name, nformat, ts) @@ -2394,7 +2406,7 @@ def categorical_field_create_like(source, group, name, timestamp): if isinstance(group, h5py.Group): categorical_field_constructor(source._session, group, name, nformat, keys, ts, source.chunksize) - return CategoricalField(source._session, group[name], write_enabled=True) + return CategoricalField(source._session, group[name], None, name, write_enabled=True) else: return group.create_categorical(name, nformat, keys, ts) @@ -2410,6 +2422,6 @@ def timestamp_field_create_like(source, group, name, timestamp): if isinstance(group, h5py.Group): timestamp_field_constructor(source._session, group, name, ts, source.chunksize) - return TimestampField(source._session, group[name], write_enabled=True) + return TimestampField(source._session, group[name], None, name, write_enabled=True) else: return group.create_timestamp(name, ts) diff --git a/exetera/core/session.py b/exetera/core/session.py index 19d7a5c2..b292d5c6 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -865,7 +865,7 @@ def get(self, } fieldtype = field.attrs['fieldtype'].split(',')[0] - return fieldtype_map[fieldtype](self, field) + return fieldtype_map[fieldtype](self, field, None, field.name) def create_like(self, field, dest_group, dest_name, timestamp=None, chunksize=None): """ @@ -914,7 +914,7 @@ def create_indexed_string(self, group, name, timestamp=None, chunksize=None): if isinstance(group, h5py.Group): fld.indexed_string_field_constructor(self, group, name, timestamp, chunksize) - return fld.IndexedStringField(self, group[name], write_enabled=True) + return fld.IndexedStringField(self, group[name], None, name, write_enabled=True) else: return group.create_indexed_string(name, timestamp, chunksize) @@ -939,7 +939,7 @@ def create_fixed_string(self, group, name, length, timestamp=None, chunksize=Non "{} was passed to it".format(type(group))) if isinstance(group, h5py.Group): fld.fixed_string_field_constructor(self, group, name, length, timestamp, chunksize) - return fld.FixedStringField(self, group[name], write_enabled=True) + return fld.FixedStringField(self, group[name], None, name, write_enabled=True) else: return group.create_fixed_string(name, length, timestamp, chunksize) @@ -969,7 +969,7 @@ def create_categorical(self, group, name, nformat, key, timestamp=None, chunksiz if isinstance(group, h5py.Group): fld.categorical_field_constructor(self, group, name, nformat, key, timestamp, chunksize) - return fld.CategoricalField(self, group[name], write_enabled=True) + return fld.CategoricalField(self, group[name], None, name, write_enabled=True) else: return group.create_categorical(name, nformat, key, timestamp, chunksize) @@ -997,7 +997,7 @@ def create_numeric(self, group, name, nformat, timestamp=None, chunksize=None): if isinstance(group, h5py.Group): fld.numeric_field_constructor(self, group, name, nformat, timestamp, chunksize) - return fld.NumericField(self, group[name], write_enabled=True) + return fld.NumericField(self, group[name], None, name, write_enabled=True) else: return group.create_numeric(name, nformat, timestamp, chunksize) @@ -1015,7 +1015,7 @@ def create_timestamp(self, group, name, timestamp=None, chunksize=None): if isinstance(group, h5py.Group): fld.timestamp_field_constructor(self, group, name, timestamp, chunksize) - return fld.TimestampField(self, group[name], write_enabled=True) + return fld.TimestampField(self, group[name], None, name, write_enabled=True) else: return group.create_timestamp(name, timestamp, chunksize) diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 42cfd3a9..6ae8b3ae 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -42,7 +42,8 @@ def test_dataframe_init(self): # del & del by field del df['numf'] self.assertFalse('numf' in df) - df.delete_field(cat) + with self.assertRaises(ValueError, msg="This field is owned by a different dataframe"): + df.delete_field(cat) self.assertFalse(df.contains_field(cat)) def test_dataframe_create_numeric(self): @@ -193,7 +194,7 @@ def test_dataframe_create_mem_categorical(self): df['r6'] = cat1 >= cat2 self.assertEqual([False, False, True, False, False, True], df['r6'].data[:].tolist()) - def test_datafrmae_static_methods(self): + def test_dataframe_static_methods(self): bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, 'w', 'dst') @@ -202,11 +203,11 @@ def test_datafrmae_static_methods(self): numf.data.write([5, 4, 3, 2, 1]) df2 = dst.create_dataframe('df2') - dataframe.HDF5DataFrame.copy(numf, df2,'numf') + dataframe.copy(numf, df2,'numf') self.assertListEqual([5, 4, 3, 2, 1], df2['numf'].data[:].tolist()) - dataframe.HDF5DataFrame.drop(df, numf) + df.drop('numf') self.assertTrue('numf' not in df) - dataframe.HDF5DataFrame.move(df2,df2['numf'],df,'numf') + dataframe.move(df2['numf'], df, 'numf') self.assertTrue('numf' not in df2) self.assertListEqual([5, 4, 3, 2, 1], df['numf'].data[:].tolist()) diff --git a/tests/test_session.py b/tests/test_session.py index 5379e32e..edf58d68 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -361,14 +361,20 @@ def test_ordered_merge_inner_fields(self): bio = BytesIO() with session.Session() as s: - dst = s.open_dataset(bio,'w','dst') - hf=dst.create_dataframe('dst') - l_id_f = s.create_fixed_string(hf, 'l_id', 1); l_id_f.data.write(l_id) - l_vals_f = s.create_numeric(hf, 'l_vals_f', 'int32'); l_vals_f.data.write(l_vals) - l_vals_2_f = s.create_numeric(hf, 'l_vals_2_f', 'int32'); l_vals_2_f.data.write(l_vals_2) - r_id_f = s.create_fixed_string(hf, 'r_id', 1); r_id_f.data.write(r_id) - r_vals_f = s.create_numeric(hf, 'r_vals_f', 'int32'); r_vals_f.data.write(r_vals) - r_vals_2_f = s.create_numeric(hf, 'r_vals_2_f', 'int32'); r_vals_2_f.data.write(r_vals_2) + dst = s.open_dataset(bio, 'w', 'dst') + hf = dst.create_dataframe('dst') + l_id_f = s.create_fixed_string(hf, 'l_id', 1) + l_id_f.data.write(l_id) + l_vals_f = s.create_numeric(hf, 'l_vals_f', 'int32') + l_vals_f.data.write(l_vals) + l_vals_2_f = s.create_numeric(hf, 'l_vals_2_f', 'int32') + l_vals_2_f.data.write(l_vals_2) + r_id_f = s.create_fixed_string(hf, 'r_id', 1) + r_id_f.data.write(r_id) + r_vals_f = s.create_numeric(hf, 'r_vals_f', 'int32') + r_vals_f.data.write(r_vals) + r_vals_2_f = s.create_numeric(hf, 'r_vals_2_f', 'int32') + r_vals_2_f.data.write(r_vals_2) i_l_vals_f = s.create_numeric(hf, 'i_l_vals_f', 'int32') i_l_vals_2_f = s.create_numeric(hf, 'i_l_vals_2_f', 'int32') i_r_vals_f = s.create_numeric(hf, 'i_r_vals_f', 'int32') @@ -1004,7 +1010,7 @@ def test_write_then_read_numeric(self): np.random.seed(12345678) values = np.random.randint(low=0, high=1000000, size=100000000) fields.numeric_field_constructor(s, hf, 'a', 'int32') - a = fields.NumericField(s, hf['a'], write_enabled=True) + a = fields.NumericField(s, hf['a'], None, 'a', write_enabled=True) a.data.write(values) total = np.sum(a.data[:]) @@ -1026,7 +1032,7 @@ def test_write_then_read_categorical(self): values = np.random.randint(low=0, high=3, size=100000000) fields.categorical_field_constructor(s, hf, 'a', 'int8', {'foo': 0, 'bar': 1, 'boo': 2}) - a = fields.CategoricalField(s, hf['a'], write_enabled=True) + a = fields.CategoricalField(s, hf['a'], None, 'a', write_enabled=True) a.data.write(values) total = np.sum(a.data[:]) @@ -1044,7 +1050,7 @@ def test_write_then_read_fixed_string(self): values = np.random.randint(low=0, high=4, size=1000000) svalues = [b''.join([b'x'] * v) for v in values] fields.fixed_string_field_constructor(s, hf, 'a', 8) - a = fields.FixedStringField(s, hf['a'], write_enabled=True) + a = fields.FixedStringField(s, hf['a'], None, 'a', write_enabled=True) a.data.write(svalues) total = np.unique(a.data[:]) @@ -1068,7 +1074,7 @@ def test_write_then_read_indexed_string(self): values = np.random.randint(low=0, high=4, size=200000) svalues = [''.join(['x'] * v) for v in values] fields.indexed_string_field_constructor(s, hf, 'a', 8) - a = fields.IndexedStringField(s, hf['a'], write_enabled=True) + a = fields.IndexedStringField(s, hf['a'], None, 'a', write_enabled=True) a.data.write(svalues) total = np.unique(a.data[:]) From 937368eebb44fa11f5fa3bbbaa1b86fa3d42e582 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Tue, 20 Apr 2021 11:28:35 +0100 Subject: [PATCH 086/181] Updating HDF5Field writeable methods to account for prior changes --- exetera/core/fields.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 7e700630..33d63f83 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -516,7 +516,7 @@ def __init__(self, session, length): self._length = length def writeable(self): - return FixedStringField(self._session, self._field, None, write_enabled=True) + return self def create_like(self, group=None, name=None, timestamp=None): return FieldDataOps.fixed_string_field_create_like(self, group, name, timestamp) @@ -852,7 +852,7 @@ def __init__(self, session): super().__init__(session) def writeable(self): - return TimestampField(self._session, self._field, write_enabled=True) + return self def create_like(self, group=None, name=None, timestamp=None): return FieldDataOps.timestamp_field_create_like(self, group, name, timestamp) @@ -1052,7 +1052,8 @@ def __init__(self, session, group, dataframe, name=None, write_enabled=False): self._value_wrapper = None def writeable(self): - return IndexedStringField(self._session, self._field, None, write_enabled=True) + return IndexedStringField(self._session, self._field, self._dataframe, self._name, + write_enabled=True) def create_like(self, group=None, name=None, timestamp=None): return FieldDataOps.indexed_string_create_like(self, group, name, timestamp) @@ -1159,7 +1160,8 @@ def __init__(self, session, group, dataframe, name=None, write_enabled=False): self._length = self._field.attrs['strlen'] def writeable(self): - return FixedStringField(self._session, self._field, None, write_enabled=True) + return FixedStringField(self._session, self._field, self._dataframe, self._name, + write_enabled=True) def create_like(self, group=None, name=None, timestamp=None): return FieldDataOps.fixed_string_field_create_like(self, group, name, timestamp) @@ -1391,7 +1393,8 @@ def __init__(self, session, group, dataframe, name=None, write_enabled=False): self._nformat = self._field.attrs['nformat'] if 'nformat' in self._field.attrs else 'int8' def writeable(self): - return CategoricalField(self._session, self._field, write_enabled=True) + return CategoricalField(self._session, self._field, self._dataframe, self._name, + write_enabled=True) def create_like(self, group=None, name=None, timestamp=None): return FieldDataOps.categorical_field_create_like(self, group, name, timestamp) @@ -1506,7 +1509,8 @@ def __init__(self, session, group, dataframe, name=None, write_enabled=False): super().__init__(session, group, dataframe, name=name, write_enabled=write_enabled) def writeable(self): - return TimestampField(self._session, self._field, write_enabled=True) + return TimestampField(self._session, self._field, self._dataframe, self._name, + write_enabled=True) def create_like(self, group=None, name=None, timestamp=None): return FieldDataOps.timestamp_field_create_like(self, group, name, timestamp) From cddcf66443c6b21d726efdf5687940e22f99a21b Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Tue, 20 Apr 2021 14:29:22 +0100 Subject: [PATCH 087/181] Adding merge functionality for dataframes --- exetera/core/dataframe.py | 81 ++++++++++++++++++++++++++++++++++----- exetera/core/dataset.py | 9 ----- tests/test_dataframe.py | 25 ++++++++++++ 3 files changed, 96 insertions(+), 19 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 0929517c..900220df 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -8,9 +8,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional, Sequence, Tuple, Union +import numpy as np +import pandas as pd from exetera.core.abstract_types import Dataset, DataFrame from exetera.core import fields as fld +from exetera.core import operations as ops import h5py @@ -328,16 +332,6 @@ def copy(field: fld.Field, dataframe: DataFrame, name: str): dataframe.columns[name] = dfield -# def drop(dataframe: DataFrame, field: fld.Field): -# """ -# Drop a field from a dataframe. -# -# :param dataframe: The dataframe where field is located. -# :param field: The field to delete. -# """ -# dataframe.delete_field(field) - - def move(field: fld.Field, dest_df: DataFrame, name: str): """ Move a field to another dataframe as well as underlying dataset. @@ -349,3 +343,70 @@ def move(field: fld.Field, dest_df: DataFrame, name: str): """ copy(field, dest_df, name) field.dataframe.drop(field.name) + + +def merge(left: DataFrame, + right: DataFrame, + dest: DataFrame, + left_on: Union[str, fld.Field], + right_on: Union[str, fld.Field], + left_fields: Optional[Sequence[str]] = None, + right_fields: Optional[Sequence[str]] = None, + left_suffix: str = '_l', + right_suffix: str = '_r', + how='left'): + + left_on_ = left[left_on] if isinstance(left_on, str) else left_on + right_on_ = right[right_on] if isinstance(right_on, str) else right_on + if len(left_on_.data) < (2 << 30) and len(right_on_.data) < (2 << 30): + index_dtype = np.int32 + else: + index_dtype = np.int64 + + # create the merging dataframes, using only the fields involved in the merge + l_df = pd.DataFrame({'l_k': left_on_.data[:], + 'l_i': np.arange(len(left_on_.data), dtype=index_dtype)}) + r_df = pd.DataFrame({'r_k': right_on_.data[:], + 'r_i': np.arange(len(right_on_.data), dtype=index_dtype)}) + df = pd.merge(left=l_df, right=r_df, left_on='l_k', right_on='r_k', how=how) + l_to_d_map = df['l_i'].to_numpy(dtype=np.int32) + l_to_d_filt = np.logical_not(df['l_i'].isnull()).to_numpy() + r_to_d_map = df['r_i'].to_numpy(dtype=np.int32) + r_to_d_filt = np.logical_not(df['r_i'].isnull()).to_numpy() + + # perform the mapping + left_fields_ = left.keys() if left_fields is None else left_fields + right_fields_ = right.keys() if right_fields is None else right_fields + for f in right_fields_: + dest_f = f + if f in left_fields_: + dest_f += right_suffix + r = right[f] + d = r.create_like(dest, dest_f) + if r.indexed: + i, v = ops.safe_map_indexed_values(r.indices[:], r.values[:], r_to_d_map, r_to_d_filt) + d.indices.write(i) + d.values.write(v) + else: + v = ops.safe_map_values(r.data[:], r_to_d_map, r_to_d_filt) + d.data.write(v) + if np.all(r_to_d_filt) == False: + d = dest.create_numeric('valid'+right_suffix, 'bool') + d.data.write(r_to_d_filt) + + for f in left_fields_: + dest_f = f + if f in right_fields_: + dest_f += left_suffix + l = left[f] + d = l.create_like(dest, dest_f) + if l.indexed: + i, v = ops.safe_map_indexed_values(l.indices[:], l.values[:], l_to_d_map, l_to_d_filt) + d.indices.write(i) + d.values.write(v) + else: + v = ops.safe_map_values(l.data[:], l_to_d_map, l_to_d_filt) + d.data.write(v) + if np.all(l_to_d_filt) == False: + d = dest.create_numeric('valid'+left_suffix, 'bool') + d.data.write(l_to_d_filt) diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 8148167e..0c30978b 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -251,15 +251,6 @@ def copy(dataframe: DataFrame, dataset: Dataset, name: str): dataset._dataframes[name] = _dataframe -def drop(dataframe: DataFrame): - """ - Delete a dataframe by HDF5DataFrame.drop(ds['df1']). - - :param dataframe: The dataframe to delete. - """ - dataframe._dataset.delete_dataframe(dataframe) - - def move(dataframe: DataFrame, dataset: Dataset, name:str): """ Move a dataframe to another dataset via HDF5DataFrame.move(ds1['df1'], ds2, 'df1']). diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 6ae8b3ae..2347e5d2 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -257,3 +257,28 @@ def test_apply_filter(self): df.apply_filter(filt) self.assertListEqual(expected, df['numf'].data[:].tolist()) + + +class TestDataFrameMerge(unittest.TestCase): + + def tests_merge_left(self): + + l_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') + r_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') + r_vals = ['bb1', 'ccc1', '', 'dddd1', 'ggggggg1', 'ffffff1', 'bb2', '', 'ccc2'] + expected = ['', '', '', 'bb1', 'bb2', 'ccc1', 'ccc2', 'dddd1', '', 'ffffff1', 'ggggggg1'] + + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + ldf = dst.create_dataframe('ldf') + rdf = dst.create_dataframe('rdf') + ldf.create_numeric('l_id', 'int32').data.write(l_id) + rdf.create_numeric('r_id', 'int32').data.write(r_id) + rdf.create_indexed_string('r_vals').data.write(r_vals) + ddf = dst.create_dataframe('ddf') + dataframe.merge(ldf, rdf, ddf, 'l_id', 'r_id', how='left') + self.assertEqual(expected, ddf['r_vals'].data[:]) + valid_if_equal = (ddf['l_id'].data[:] == ddf['r_id'].data[:]) | \ + np.logical_not(ddf['valid_r'].data[:]) + self.assertTrue(np.all(valid_if_equal)) From 534cbd49702cce3841ccd09fadb1f56c9b130538 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Tue, 20 Apr 2021 14:42:43 +0100 Subject: [PATCH 088/181] dataset.drop is a member method of Dataset as it did not make sense for it to be static or outside of the class --- exetera/core/dataset.py | 7 ++++++- tests/test_dataset.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 0c30978b..cacf9300 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -201,6 +201,11 @@ def delete_dataframe(self, dataframe: DataFrame): else: self.__delitem__(name) + def drop(self, + name: str): + del self._dataframes[name] + del self._file[name] + def keys(self): """Return all dataframe names in this dataset.""" return self._dataframes.keys() @@ -262,5 +267,5 @@ def move(dataframe: DataFrame, dataset: Dataset, name:str): :param name: The name of dataframe in destination dataset. """ copy(dataframe, dataset, name) - drop(dataframe) + dataframe.dataset.drop(dataframe.name) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 01b9728c..3460be4f 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -6,7 +6,7 @@ from exetera.core import session, fields from exetera.core.abstract_types import DataFrame from io import BytesIO -from exetera.core.dataset import HDF5Dataset, copy, drop, move +from exetera.core.dataset import HDF5Dataset, copy, move class TestDataSet(unittest.TestCase): @@ -84,7 +84,7 @@ def test_dataset_static_func(self): self.assertTrue(isinstance(ds2['df2'], DataFrame)) self.assertTrue(isinstance(ds2['df2']['num'], fields.Field)) - drop(ds2['df2']) + ds2.drop('df2') self.assertTrue(len(ds2) == 0) move(df, ds2, 'df2') From e5dc536fd415dae3155de5ca22942708015ffe28 Mon Sep 17 00:00:00 2001 From: Ben Murray Date: Tue, 20 Apr 2021 14:49:18 +0100 Subject: [PATCH 089/181] Added missing methods / properties to DataFrame ABC --- exetera/core/abstract_types.py | 14 ++++++++++++++ exetera/core/dataframe.py | 3 ++- exetera/core/dataset.py | 1 - 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index 554bb1c0..fef4b54e 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -150,10 +150,24 @@ class DataFrame(ABC): DataFrame is a table of data that contains a list of Fields (columns) """ + @property + @abstractmethod + def columns(self): + raise NotImplementedError() + + @property + @abstractmethod + def dataset(self): + raise NotImplementedError() + @abstractmethod def add(self, field): raise NotImplementedError() + @abstractmethod + def drop(self, name: str): + raise NotImplementedError() + @abstractmethod def create_group(self, name): raise NotImplementedError() diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 900220df..5f7fdc82 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -84,7 +84,8 @@ def add(self, field): nfield.data.write(field.data[:]) self._columns[dname] = nfield - def drop(self, name): + def drop(self, + name: str): del self._columns[name] del self._h5group[name] diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index cacf9300..09a70176 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -268,4 +268,3 @@ def move(dataframe: DataFrame, dataset: Dataset, name:str): """ copy(dataframe, dataset, name) dataframe.dataset.drop(dataframe.name) - From 9b1a4a9dd7726f90ef59b0ef5e3776416b7c6b6a Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 20 Apr 2021 15:06:16 +0100 Subject: [PATCH 090/181] minor update on dataframe static function --- exetera/core/dataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 55e064d6..66f235d7 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -338,4 +338,4 @@ def move(src_df: DataFrame, field: fld.Field, dest_df: DataFrame, name: str): :param name: The name of field under destination dataframe. """ copy(field, dest_df, name) - drop(src_df, field) + drop(src_df, field) From 196768582fe853d793b4fa2c865dcef7b8fa6303 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 20 Apr 2021 15:06:37 +0100 Subject: [PATCH 091/181] minor update --- exetera/core/dataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 66f235d7..55e064d6 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -338,4 +338,4 @@ def move(src_df: DataFrame, field: fld.Field, dest_df: DataFrame, name: str): :param name: The name of field under destination dataframe. """ copy(field, dest_df, name) - drop(src_df, field) + drop(src_df, field) From 6bdb08ed31babc758ae961ea503d04ec5b9d68eb Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 21 Apr 2021 10:11:00 +0100 Subject: [PATCH 092/181] minor update session --- exetera/core/session.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exetera/core/session.py b/exetera/core/session.py index b292d5c6..9038a1b7 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -14,10 +14,9 @@ import uuid from datetime import datetime, timezone import time -import warnings + import numpy as np import pandas as pd - import h5py from exetera.core.abstract_types import Field, AbstractSession From cf5f5a60b713ba20dc8538749ddeaca01fd1f1d4 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 21 Apr 2021 10:17:58 +0100 Subject: [PATCH 093/181] minor comments update --- exetera/core/dataset.py | 1 + tests/test_dataframe.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index e3c0963c..16c571b0 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -56,6 +56,7 @@ def __init__(self, session, dataset_path, mode, name): def session(self): """ The session property interface. + :return: The _session instance. """ return self._session diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 06807c29..fdd6a057 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -201,7 +201,6 @@ def test_dataframe_static_methods(self): df2 = dst.create_dataframe('df2') dataframe.copy(numf, df2,'numf') self.assertListEqual([5, 4, 3, 2, 1], df2['numf'].data[:].tolist()) - df.drop('numf') self.assertTrue('numf' not in df) dataframe.move(df2['numf'], df, 'numf') From 23ad71a747eb6a3f39a780cc4820971a4c3050e7 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 21 Apr 2021 10:18:23 +0100 Subject: [PATCH 094/181] minor comments update --- exetera/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 16c571b0..251dcad1 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -56,7 +56,7 @@ def __init__(self, session, dataset_path, mode, name): def session(self): """ The session property interface. - + :return: The _session instance. """ return self._session From 75eefc0c284529644138a278c9c2253a1ad21885 Mon Sep 17 00:00:00 2001 From: clyyuanzi-london <59363720+clyyuanzi-london@users.noreply.github.com> Date: Wed, 21 Apr 2021 15:36:51 +0100 Subject: [PATCH 095/181] add unittest for csv_reader_speedup.py --- exetera/core/csv_reader_speedup.py | 152 ++++++++++++++++------------- tests/test_csv_reader_speedup.py | 118 ++++++++++++++++++++++ 2 files changed, 202 insertions(+), 68 deletions(-) create mode 100644 tests/test_csv_reader_speedup.py diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py index 6df21b4b..1a4121c6 100644 --- a/exetera/core/csv_reader_speedup.py +++ b/exetera/core/csv_reader_speedup.py @@ -79,16 +79,20 @@ def file_read_line_fast_csv(source): with open(source) as f: header = csv.DictReader(f) count_columns = len(header.fieldnames) - + count_rows = sum(1 for _ in f) # w/o header row - #content = f.read() + f.seek(0) + print(f.read()) # count_rows = content.count('\n') + 1 # +1: for the case that last line doesn't have \n column_inds = np.zeros((count_columns, count_rows + 1), dtype=np.int64) # add one more row for initial index 0 # change it to longest key column_vals = np.zeros((count_columns, count_rows * 100), dtype=np.uint8) + print('====initialize=====') + print(column_inds, column_vals) + ESCAPE_VALUE = np.frombuffer(b'"', dtype='S1')[0][0] SEPARATOR_VALUE = np.frombuffer(b',', dtype='S1')[0][0] NEWLINE_VALUE = np.frombuffer(b'\n', dtype='S1')[0][0] @@ -96,75 +100,25 @@ def file_read_line_fast_csv(source): #print(lineterminator.tobytes()) #print("hello") #CARRIAGE_RETURN_VALUE = np.frombuffer(b'\r', dtype='S1')[0][0] - print("test") - with Timer("my_fast_csv_reader_int"): + # print("test") + with Timer("my_fast_csv_reader"): content = np.fromfile(source, dtype=np.uint8) - my_fast_csv_reader_int(content, column_inds, column_vals, ESCAPE_VALUE, SEPARATOR_VALUE, NEWLINE_VALUE) - + print(content) + my_fast_csv_reader(content, column_inds, column_vals, ESCAPE_VALUE, SEPARATOR_VALUE, NEWLINE_VALUE) + print('======after csv reader====') + print(column_inds) + print(column_vals) return column_inds, column_vals -def get_cell(row,col, column_inds, column_vals): - start_row_index = column_inds[col][row] - end_row_index = column_inds[col][row+1] - return column_vals[col][start_row_index:end_row_index].tobytes() - - -def make_test_data(count, schema): - """ - [ {'name':name, 'type':'cat'|'float'|'fixed', 'values':(vals)} ] - """ - import pandas as pd - rng = np.random.RandomState(12345678) - columns = {} - for s in schema: - if s['type'] == 'cat': - vals = s['vals'] - arr = rng.randint(low=0, high=len(vals), size=count) - larr = [None] * count - for i in range(len(arr)): - larr[i] = vals[arr[i]] - columns[s['name']] = larr - elif s['type'] == 'float': - arr = rng.uniform(size=count) - columns[s['name']] = arr - - df = pd.DataFrame(columns) - df.to_csv('/home/ben/covid/benchmark_csv.csv', index_label='index') - - -def make_test_data(count, schema): - """ - [ {'name':name, 'type':'cat'|'float'|'fixed', 'values':(vals)} ] - """ - import pandas as pd - rng = np.random.RandomState(12345678) - columns = {} - for s in schema: - if s['type'] == 'cat': - vals = s['vals'] - arr = rng.randint(low=0, high=len(vals), size=count) - larr = [None] * count - for i in range(len(arr)): - larr[i] = vals[arr[i]] - columns[s['name']] = larr - elif s['type'] == 'float': - arr = rng.uniform(size=count) - columns[s['name']] = arr - - df = pd.DataFrame(columns) - df.to_csv('/home/ben/covid/benchmark_csv.csv', index_label='index') - - - @njit -def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separator_value, newline_value): +def my_fast_csv_reader(source, column_inds, column_vals, escape_value, separator_value, newline_value): colcount = len(column_inds) maxrowcount = len(column_inds[0]) - 1 # minus extra index 0 row that created for column_inds print('colcount', colcount) print('maxrowcount', maxrowcount) - + index = np.int64(0) line_start = np.int64(0) cell_start_idx = np.int64(0) @@ -208,10 +162,6 @@ def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separ column_vals[col_index, cur_cell_start + cur_cell_char_count] = c cur_cell_char_count += 1 - # if col_index == 5: - # print('%%%%%%') - # print(c) - if end_cell: if row_index >= 0: column_inds[col_index, row_index + 1] = cur_cell_start + cur_cell_char_count @@ -236,15 +186,81 @@ def my_fast_csv_reader_int(source, column_inds, column_vals, escape_value, separ if index == len(source): if col_index == colcount - 1: #and row_index == maxrowcount - 1: - # print('excuese me') column_inds[col_index, row_index + 1] = cur_cell_start + cur_cell_char_count - # print('source', source, 'len_source', len(source), len(source)) + # print('source', source, 'len_source', len(source)) # print('index', cur_cell_start + cur_cell_char_count) - # print('break',col_index, row_index) + # print('break', col_index, row_index) break +@njit +def my_fast_categorical_mapper(chunk, i_c, column_ids, column_vals, cat_keys, cat_index, cat_values): + pos = 0 + for row_idx in range(len(column_ids[i_c]) - 1): + # Finds length, which we use to lookup potential matches + key_start = column_ids[i_c, row_idx] + key_end = column_ids[i_c, row_idx + 1] + key_len = key_end - key_start + + print('key_start', key_start, 'key_end', key_end) + + for i in range(len(cat_index) - 1): + sc_key_len = cat_index[i + 1] - cat_index[i] + + if key_len != sc_key_len: + continue + + index = i + for j in range(key_len): + entry_start = cat_index[i] + if column_vals[i_c, key_start + j] != cat_keys[entry_start + j]: + index = -1 + break + + if index != -1: + chunk[row_idx] = cat_values[index] + + pos = row_idx + 1 + return pos + + +def get_byte_map(string_map): + # sort by length of key first, and then sort alphabetically + sorted_string_map = {k: v for k, v in sorted(string_map.items(), key=lambda item: (len(item[0]), item[0]))} + sorted_string_key = [(len(k), np.frombuffer(k.encode(), dtype=np.uint8), v) for k, v in sorted_string_map.items()] + sorted_string_values = list(sorted_string_map.values()) + + # assign byte_map_key_lengths, byte_map_value + byte_map_key_lengths = np.zeros(len(sorted_string_map), dtype=np.uint8) + byte_map_value = np.zeros(len(sorted_string_map), dtype=np.uint8) + + for i, (length, _, v) in enumerate(sorted_string_key): + byte_map_key_lengths[i] = length + byte_map_value[i] = v + + # assign byte_map_keys, byte_map_key_indices + byte_map_keys = np.zeros(sum(byte_map_key_lengths), dtype=np.uint8) + byte_map_key_indices = np.zeros(len(sorted_string_map)+1, dtype=np.uint8) + + idx_pointer = 0 + for i, (_, b_key, _) in enumerate(sorted_string_key): + for b in b_key: + byte_map_keys[idx_pointer] = b + idx_pointer += 1 + + byte_map_key_indices[i + 1] = idx_pointer + + byte_map = [byte_map_keys, byte_map_key_lengths, byte_map_key_indices, byte_map_value] + return byte_map + + + +def get_cell(row,col, column_inds, column_vals): + start_row_index = column_inds[col][row] + end_row_index = column_inds[col][row+1] + return column_vals[col][start_row_index:end_row_index].tobytes() + if __name__ == "__main__": main() diff --git a/tests/test_csv_reader_speedup.py b/tests/test_csv_reader_speedup.py new file mode 100644 index 00000000..791d0370 --- /dev/null +++ b/tests/test_csv_reader_speedup.py @@ -0,0 +1,118 @@ +from unittest import TestCase +from exetera.core.csv_reader_speedup import my_fast_csv_reader, file_read_line_fast_csv, get_byte_map, my_fast_categorical_mapper +import tempfile +import numpy as np +import os +import pandas as pd + + +TEST_SCHEMA = [{'name': 'a', 'type': 'cat', 'vals': ('','a', 'bb', 'ccc', 'dddd', 'eeeee'), + 'strings_to_values': {'':0,'a':1, 'bb':2, 'ccc':3, 'dddd':4, 'eeeee':5}}, + {'name': 'b', 'type': 'float'}, + {'name': 'c', 'type': 'cat', 'vals': ('', '', '', '', '', 'True', 'False'), + 'strings_to_values': {"": 0, "False": 1, "True": 2}}, + {'name': 'd', 'type': 'float'}, + {'name': 'e', 'type': 'float'}, + {'name': 'f', 'type': 'cat', 'vals': ('', '', '', '', '', 'True', 'False'), + 'strings_to_values': {"": 0, "False": 1, "True": 2}}, + {'name': 'g', 'type': 'cat', 'vals': ('', '', '', '', 'True', 'False'), + 'strings_to_values': {"": 0, "False": 1, "True": 2}}, + {'name': 'h', 'type': 'cat', 'vals': ('', '', '', 'No', 'Yes'), + 'strings_to_values': {"": 0, "No": 1, "Yes": 2}}] + + + +class TestFastCSVReader(TestCase): + # def setUp(self): + + # self.fd_csv, self.csv_file_name = tempfile.mkstemp(suffix='.csv') + # with open(self.csv_file_name, 'w') as fcsv: + # fcsv.write(TEST_CSV_CONTENTS) + + def _make_test_data(self, count, schema, csv_file_name): + """ + [ {'name':name, 'type':'cat'|'float'|'fixed', 'values':(vals)} ] + """ + import pandas as pd + rng = np.random.RandomState(12345678) + columns = {} + cat_columns_v = {} + cat_map_dict = {} + for s in schema: + if s['type'] == 'cat': + vals = s['vals'] + arr = rng.randint(low=0, high=len(vals), size=count) + larr = [None] * count + arr_str_to_val = [None] * count + for i in range(len(arr)): + larr[i] = vals[arr[i]] + arr_str_to_val[i] = s['strings_to_values'][vals[arr[i]]] + + columns[s['name']] = larr + cat_columns_v[s['name']] = arr_str_to_val + cat_map_dict[s['name']] = s['strings_to_values'] + + elif s['type'] == 'float': + arr = rng.uniform(size=count) + columns[s['name']] = arr + + # create csv file + df = pd.DataFrame(columns) + df.to_csv(csv_file_name, index = False) + + # create byte map for each categorical field + fieldnames = list(df) + categorical_map_list = [None] * len(fieldnames) + for i, fn in enumerate(fieldnames): + if fn in cat_map_dict: + string_map = cat_map_dict[fn] + categorical_map_list[i] = get_byte_map(string_map) + + return df, cat_columns_v, categorical_map_list + + + def test_my_fast_csv_reader(self): + self.fd_csv, self.csv_file_name = tempfile.mkstemp(suffix='.csv') + df, cat_columns_v, categorical_map_list = self._make_test_data(5, TEST_SCHEMA, self.csv_file_name) + print(df) + print(cat_columns_v) + print(categorical_map_list) + + column_inds, column_vals = file_read_line_fast_csv(self.csv_file_name) + + + field_to_use = list(df) + for i_c, field in enumerate(field_to_use): + if categorical_map_list[i_c] is None: + continue + + cat_keys, _, cat_index, cat_values = categorical_map_list[i_c] + print(cat_keys, cat_index, cat_values ) + + chunk_size = 10 + chunk = np.zeros(chunk_size, dtype=np.uint8) + + pos = my_fast_categorical_mapper(chunk, i_c, column_inds, column_vals, cat_keys, cat_index, cat_values) + + chunk = list(chunk[:pos]) + + self.assertListEqual(chunk, cat_columns_v[field]) + + + os.close(self.fd_csv) + + + # print('=====') + # with open(self.csv_file_name) as f: + # print(f.read()) + # print('=====') + + # df = pd.read_csv(self.csv_file_name) + # print(df) + # print(list(df)) + + + + + # def tearDown(self): + # os.close(self.fd_csv) From c02fe32b2cfd98577b465a052213cf3d14ad2d8e Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 26 Apr 2021 17:43:46 +0100 Subject: [PATCH 096/181] count operation; logical not for numeric fields --- exetera/core/fields.py | 22 ++++++++++++++++++++++ exetera/core/operations.py | 33 ++++++++++++++++++++++++++++++--- tests/test_operations.py | 27 +++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index aedabb30..3509fd2d 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -1409,6 +1409,9 @@ def __or__(self, second): def __ror__(self, first): return FieldDataOps.numeric_or(self._session, first, self) + def __invert__(self): + return FieldDataOps.logical_not(self._session, self) + def __lt__(self, value): return FieldDataOps.less_than(self._session, self, value) @@ -1978,6 +1981,18 @@ def _binary_op(session, first, second, function): f.data.write(r) return f + @staticmethod + def _unary_op(session, first, function): + if isinstance(first, Field): + first_data = first.data[:] + else: + first_data = first + + r = function(first_data) + f = NumericMemField(session, dtype_to_str(r.dtype)) + f.data.write(r) + return f + @classmethod def numeric_add(cls, session, first, second): def function_add(first, second): @@ -2060,6 +2075,13 @@ def function_or(first, second): return cls._binary_op(session, first, second, function_or) + @classmethod + def logical_not(cls, session, first): + def function_logical_not(first): + return np.logical_not(first) + + return cls._unary_op(session, first, function_logical_not) + @classmethod def less_than(cls, session, first, second): def function_less_than(first, second): diff --git a/exetera/core/operations.py b/exetera/core/operations.py index a3768c14..fdca2150 100644 --- a/exetera/core/operations.py +++ b/exetera/core/operations.py @@ -5,7 +5,7 @@ from numba.typed import List from exetera.core import validation as val -from exetera.core.abstract_types import Field +from exetera.core.abstract_types import Field, DataFrame from exetera.core import fields, utils DEFAULT_CHUNKSIZE = 1 << 20 @@ -68,7 +68,7 @@ def safe_map_indexed_values(data_indices, data_values, map_field, map_filter, em return i_result, v_result -@njit +#@njit def safe_map_values(data_field, map_field, map_filter, empty_value=None): result = np.zeros_like(map_field, dtype=data_field.dtype) empty_val = result[0] if empty_value is None else empty_value @@ -444,7 +444,7 @@ def apply_spans_index_of_last_filter(spans, dest_array, filter_array): return dest_array, filter_array -@njit +#@njit def apply_spans_count(spans, dest_array): for i in range(len(spans)-1): dest_array[i] = np.int64(spans[i+1] - spans[i]) @@ -1294,3 +1294,30 @@ def is_ordered(field): else: fn = np.char.greater return not np.any(fn(field[:-1], field[1:])) + + +def count(field: Field): + """ + Count the number of appearances of each item in the field content. + + :param field: The content field to count. + + :return: a dictionary contains the index and count result. + """ + if field.indexed: + sorted = np.sort(np.array(field.data[:])) + idx = np.unique(sorted) + span = get_spans_for_field(sorted) + result = np.zeros(len(span) - 1, dtype=np.uint32) + apply_spans_count(span, result) + else: + if not is_ordered(field.data[:]): + sorted = np.sort(field.data[:]) + else: + sorted = field.data[:] + idx = np.unique(sorted) + span = get_spans_for_field(sorted) + result = np.zeros(len(span) - 1, dtype=np.uint32) + apply_spans_count(span, result) + + return dict(zip(idx, result)) diff --git a/tests/test_operations.py b/tests/test_operations.py index 030c076d..ffe377f4 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -489,6 +489,33 @@ def test_is_ordered(self): arr = np.asarray([1, 1, 1, 1, 1]) self.assertTrue(ops.is_ordered(arr)) + def test_count(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + df = dst.create_dataframe('df') + fld = df.create_numeric('num', 'int32') + fld.data.write([1, 1, 2, 3, 4, 5, 6, 3, 4, 1, 1, 2, 4, 2, 3, 4]) + dict = ops.count(fld) + self.assertEqual([1, 2, 3, 4, 5, 6], list(dict.keys())) + self.assertEqual([4, 3, 3, 4, 1, 1], list(dict.values())) + fld = df.create_fixed_string('fst', 1) + fld.data.write([b'a', b'c', b'd', b'b', b'a', b'a', b'd', b'c', b'a']) + dict = ops.count(fld) + self.assertEqual([b'a', b'b', b'c', b'd'], list(dict.keys())) + self.assertEqual([4, 1, 2, 2], list(dict.values())) + fld = df.create_indexed_string('ids') + fld.data.write(['cc', 'aa', 'bb', 'cc', 'cc', 'ddd', 'dd', 'ddd']) + dict = ops.count(fld) + self.assertEqual(['aa', 'bb', 'cc', 'dd', 'ddd'], list(dict.keys())) + self.assertEqual([1, 1, 3, 1, 2], list(dict.values())) + fld = df.create_categorical('cat', 'int8', {'a': 1, 'b': 2}) + fld.data.write([1, 1, 2, 2, 1, 1, 2, 2, 1, 2, 1, 2, 1]) + dict = ops.count(fld) + self.assertEqual(list(fld.keys.keys()), list(dict.keys())) + self.assertEqual([7, 6], list(dict.values())) + + class TestGetSpans(unittest.TestCase): def test_get_spans_two_field(self): From 58159d0c101601e8cdc45970633bf113a3c22ab4 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 27 Apr 2021 08:50:34 +0100 Subject: [PATCH 097/181] remove csv speed up work from commit --- exetera/core/csv_reader_speedup.py | 266 ----- exetera/core/importer.py | 266 +---- exetera/core/readerwriter.py | 57 +- resources/assessment_input_small_data.csv | 1297 --------------------- tests/test_csv_reader_speedup.py | 118 -- 5 files changed, 33 insertions(+), 1971 deletions(-) delete mode 100644 exetera/core/csv_reader_speedup.py delete mode 100644 resources/assessment_input_small_data.csv delete mode 100644 tests/test_csv_reader_speedup.py diff --git a/exetera/core/csv_reader_speedup.py b/exetera/core/csv_reader_speedup.py deleted file mode 100644 index 1a4121c6..00000000 --- a/exetera/core/csv_reader_speedup.py +++ /dev/null @@ -1,266 +0,0 @@ -import csv -import time -from numba import njit,jit -import numpy as np - - -class Timer: - def __init__(self, start_msg, new_line=False, end_msg=''): - print(start_msg + ': ' if new_line is False else '\n') - self.end_msg = end_msg - - def __enter__(self): - self.t0 = time.time() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - print(self.end_msg + f' {time.time() - self.t0} seconds') - - -# def generate_test_arrays(count): -# strings = [b'one', b'two', b'three', b'four', b'five', b'six', b'seven'] -# raw_values = np.random.RandomState(12345678).randint(low=1, high=7, size=count) -# total_len = 0 -# for r in raw_values: -# total_len += len(strings[r]) -# indices = np.zeros(count+1, dtype=np.int64) -# values = np.zeros(total_len, dtype=np.int8) -# for i_r, r in enumerate(raw_values): -# indices[i_r+1] = indices[i_r] + len(strings[r]) -# for i_c in range(len(strings[r])): -# values[indices[i_r]+i_c] = strings[r][i_c] -# -# for i_r in range(20): -# start, end = indices[i_r], indices[i_r+1] -# print(values[start:end].tobytes()) - - -def main(): - # generate_test_arrays(1000) - col_dicts = [{'name': 'a', 'type': 'cat', 'vals': ('a', 'bb', 'ccc', 'dddd', 'eeeee')}, - {'name': 'b', 'type': 'float'}, - {'name': 'c', 'type': 'cat', 'vals': ('', '', '', '', '', 'True', 'False')}, - {'name': 'd', 'type': 'float'}, - {'name': 'e', 'type': 'float'}, - {'name': 'f', 'type': 'cat', 'vals': ('', '', '', '', '', 'True', 'False')}, - {'name': 'g', 'type': 'cat', 'vals': ('', '', '', '', 'True', 'False')}, - {'name': 'h', 'type': 'cat', 'vals': ('', '', '', 'No', 'Yes')}] - # make_test_data(100000, col_dicts) - source = 'resources/assessment_input_small_data.csv' - - with Timer("Original csv reader took:"): - original_csv_read(source) - - - file_read_line_fast_csv(source) - - file_read_line_fast_csv(source) - - -# original csv reader -def original_csv_read(source, column_inds=None, column_vals=None): - time0 = time.time() - with open(source) as f: - csvf = csv.reader(f, delimiter=',', quotechar='"') - for i_r, row in enumerate(csvf): - if i_r == 0: - print(len(row)) - for i_c in range(len(row)): - entry = row[i_c].encode() - column_inds[i_c][i_r+1] = column_inds[i_c][i_r] + len(entry) - column_vals[column_inds[i_c][i_r]:column_inds[i_c][i_r+1]] = entry - - # print('Original csv reader took {} s'.format(time.time() - time0)) - - -# FAST open file read line -def file_read_line_fast_csv(source): - - with open(source) as f: - header = csv.DictReader(f) - count_columns = len(header.fieldnames) - - count_rows = sum(1 for _ in f) # w/o header row - - f.seek(0) - print(f.read()) - # count_rows = content.count('\n') + 1 # +1: for the case that last line doesn't have \n - - column_inds = np.zeros((count_columns, count_rows + 1), dtype=np.int64) # add one more row for initial index 0 - # change it to longest key - column_vals = np.zeros((count_columns, count_rows * 100), dtype=np.uint8) - - print('====initialize=====') - print(column_inds, column_vals) - - ESCAPE_VALUE = np.frombuffer(b'"', dtype='S1')[0][0] - SEPARATOR_VALUE = np.frombuffer(b',', dtype='S1')[0][0] - NEWLINE_VALUE = np.frombuffer(b'\n', dtype='S1')[0][0] - - #print(lineterminator.tobytes()) - #print("hello") - #CARRIAGE_RETURN_VALUE = np.frombuffer(b'\r', dtype='S1')[0][0] - # print("test") - with Timer("my_fast_csv_reader"): - content = np.fromfile(source, dtype=np.uint8) - print(content) - my_fast_csv_reader(content, column_inds, column_vals, ESCAPE_VALUE, SEPARATOR_VALUE, NEWLINE_VALUE) - - print('======after csv reader====') - print(column_inds) - print(column_vals) - return column_inds, column_vals - - -@njit -def my_fast_csv_reader(source, column_inds, column_vals, escape_value, separator_value, newline_value): - colcount = len(column_inds) - maxrowcount = len(column_inds[0]) - 1 # minus extra index 0 row that created for column_inds - print('colcount', colcount) - print('maxrowcount', maxrowcount) - - index = np.int64(0) - line_start = np.int64(0) - cell_start_idx = np.int64(0) - cell_end_idx = np.int64(0) - col_index = np.int64(0) - row_index = np.int64(-1) - current_char_count = np.int32(0) - - escaped = False - end_cell = False - end_line = False - escaped_literal_candidate = False - cur_cell_start = column_inds[col_index, row_index] if row_index >= 0 else 0 - - cur_cell_char_count = 0 - while True: - write_char = False - end_cell = False - end_line = False - - c = source[index] - - if c == separator_value: - if not escaped: - end_cell = True - else: - write_char = True - - elif c == newline_value : - if not escaped: - end_cell = True - end_line = True - else: - write_char = True - elif c == escape_value: - escaped = not escaped - else: - write_char = True - - if write_char and row_index >= 0: - column_vals[col_index, cur_cell_start + cur_cell_char_count] = c - cur_cell_char_count += 1 - - if end_cell: - if row_index >= 0: - column_inds[col_index, row_index + 1] = cur_cell_start + cur_cell_char_count - # print("========") - # print(col_index, row_index + 1, column_vals.shape) - # print(column_inds) - # print(column_vals) - # print("========") - if end_line: - row_index += 1 - col_index = 0 - # print('~~~~~~~~~~~') - # print(col_index, row_index) - # print('~~~~~~~~~~~') - else: - col_index += 1 - - cur_cell_start = column_inds[col_index, row_index] - cur_cell_char_count = 0 - - index += 1 - - if index == len(source): - if col_index == colcount - 1: #and row_index == maxrowcount - 1: - column_inds[col_index, row_index + 1] = cur_cell_start + cur_cell_char_count - - # print('source', source, 'len_source', len(source)) - # print('index', cur_cell_start + cur_cell_char_count) - # print('break', col_index, row_index) - break - - -@njit -def my_fast_categorical_mapper(chunk, i_c, column_ids, column_vals, cat_keys, cat_index, cat_values): - pos = 0 - for row_idx in range(len(column_ids[i_c]) - 1): - # Finds length, which we use to lookup potential matches - key_start = column_ids[i_c, row_idx] - key_end = column_ids[i_c, row_idx + 1] - key_len = key_end - key_start - - print('key_start', key_start, 'key_end', key_end) - - for i in range(len(cat_index) - 1): - sc_key_len = cat_index[i + 1] - cat_index[i] - - if key_len != sc_key_len: - continue - - index = i - for j in range(key_len): - entry_start = cat_index[i] - if column_vals[i_c, key_start + j] != cat_keys[entry_start + j]: - index = -1 - break - - if index != -1: - chunk[row_idx] = cat_values[index] - - pos = row_idx + 1 - return pos - - -def get_byte_map(string_map): - # sort by length of key first, and then sort alphabetically - sorted_string_map = {k: v for k, v in sorted(string_map.items(), key=lambda item: (len(item[0]), item[0]))} - sorted_string_key = [(len(k), np.frombuffer(k.encode(), dtype=np.uint8), v) for k, v in sorted_string_map.items()] - sorted_string_values = list(sorted_string_map.values()) - - # assign byte_map_key_lengths, byte_map_value - byte_map_key_lengths = np.zeros(len(sorted_string_map), dtype=np.uint8) - byte_map_value = np.zeros(len(sorted_string_map), dtype=np.uint8) - - for i, (length, _, v) in enumerate(sorted_string_key): - byte_map_key_lengths[i] = length - byte_map_value[i] = v - - # assign byte_map_keys, byte_map_key_indices - byte_map_keys = np.zeros(sum(byte_map_key_lengths), dtype=np.uint8) - byte_map_key_indices = np.zeros(len(sorted_string_map)+1, dtype=np.uint8) - - idx_pointer = 0 - for i, (_, b_key, _) in enumerate(sorted_string_key): - for b in b_key: - byte_map_keys[idx_pointer] = b - idx_pointer += 1 - - byte_map_key_indices[i + 1] = idx_pointer - - byte_map = [byte_map_keys, byte_map_key_lengths, byte_map_key_indices, byte_map_value] - return byte_map - - - -def get_cell(row,col, column_inds, column_vals): - start_row_index = column_inds[col][row] - end_row_index = column_inds[col][row+1] - return column_vals[col][start_row_index:end_row_index].tobytes() - - -if __name__ == "__main__": - main() diff --git a/exetera/core/importer.py b/exetera/core/importer.py index 51f1f195..dfafe025 100644 --- a/exetera/core/importer.py +++ b/exetera/core/importer.py @@ -16,19 +16,15 @@ import numpy as np import h5py -from numba import njit,jit, prange, vectorize, float64 -from numba.typed import List -from collections import Counter from exetera.core import csvdataset as dataset from exetera.core import persistence as per from exetera.core import utils from exetera.core import operations as ops from exetera.core.load_schema import load_schema -from exetera.core.csv_reader_speedup import file_read_line_fast_csv -def import_with_schema(timestamp, dest_file_name, schema_file, files, overwrite, include, exclude): +def import_with_schema(timestamp, dest_file_name, schema_file, files, overwrite, include, exclude): print(timestamp) print(schema_file) print(files) @@ -48,11 +44,13 @@ def import_with_schema(timestamp, dest_file_name, schema_file, files, overwrite, include_tables, exclude_tables = set(include.keys()), set(exclude.keys()) if include_tables and not include_tables.issubset(input_file_tables): extra_tables = include_tables.difference(input_file_tables) - raise ValueError("-n/--include: the following include table(s) are not part of any input files: {}".format(extra_tables)) + raise ValueError( + "-n/--include: the following include table(s) are not part of any input files: {}".format(extra_tables)) if exclude_tables and not exclude_tables.issubset(input_file_tables): extra_tables = exclude_tables.difference(input_file_tables) - raise ValueError("-x/--exclude: the following exclude table(s) are not part of any input files: {}".format(extra_tables)) + raise ValueError( + "-x/--exclude: the following exclude table(s) are not part of any input files: {}".format(extra_tables)) stop_after = {} reserved_column_names = ('j_valid_from', 'j_valid_to') @@ -96,7 +94,6 @@ def import_with_schema(timestamp, dest_file_name, schema_file, files, overwrite, msg = "The following exclude fields are not part of the {}: {}" raise ValueError(msg.format(files[sk], exclude_missing_names)) - for sk in schema.keys(): if sk not in files: continue @@ -129,238 +126,6 @@ def __init__(self, datastore, source, hf, space, schema, timestamp, keys=None, stop_after=None, show_progress_every=None, filter_fn=None, early_filter=None): - - old = False - if old: - self.old(datastore, source, hf, space, schema, timestamp, - include, exclude, - keys, - stop_after, show_progress_every, filter_fn, - early_filter) - else: - self.nnnn(datastore, source, hf, space, schema, timestamp, - include, exclude, - keys, - stop_after, show_progress_every, filter_fn, - early_filter) - - def nnnn(self, datastore, source, hf, space, schema, timestamp, - include=None, exclude=None, - keys=None, - stop_after=None, show_progress_every=None, filter_fn=None, - early_filter=None): - # self.names_ = list() - self.index_ = None - - #stop_after = 2000000 - - file_read_line_fast_csv(source) - #exit() - - time0 = time.time() - - seen_ids = set() - - if space not in hf.keys(): - hf.create_group(space) - group = hf[space] - - with open(source) as sf: - csvf = csv.DictReader(sf, delimiter=',', quotechar='"') - - available_keys = [k.strip() for k in csvf.fieldnames if k.strip() in schema.fields] - if space in include and len(include[space]) > 0: - available_keys = include[space] - if space in exclude and len(exclude[space]) > 0: - available_keys = [k for k in available_keys if k not in exclude[space]] - - - available_keys = ['ruc11cd','ruc11'] - #available_keys = ['ruc11'] - - - if not keys: - fields_to_use = available_keys - # index_map = [csvf.fieldnames.index(k) for k in fields_to_use] - # index_map = [i for i in range(len(fields_to_use))] - else: - for k in keys: - if k not in available_keys: - raise ValueError(f"key '{k}' isn't in the available keys ({keys})") - fields_to_use = keys - # index_map = [csvf.fieldnames.index(k) for k in fields_to_use] - - csvf_fieldnames = [k.strip() for k in csvf.fieldnames] - index_map = [csvf_fieldnames.index(k) for k in fields_to_use] - - early_key_index = None - if early_filter is not None: - if early_filter[0] not in available_keys: - raise ValueError( - f"'early_filter': tuple element zero must be a key that is in the dataset") - early_key_index = available_keys.index(early_filter[0]) - - chunk_size = 1 << 20 - new_fields = dict() - new_field_list = list() - field_chunk_list = list() - categorical_map_list = list() - longest_keys = list() - - # TODO: categorical writers should use the datatype specified in the schema - for i_n in range(len(fields_to_use)): - field_name = fields_to_use[i_n] - sch = schema.fields[field_name] - writer = sch.importer(datastore, group, field_name, timestamp) - # TODO: this list is required because we convert the categorical values to - # numerical values ahead of adding them. We could use importers that handle - # that transform internally instead - - string_map = sch.strings_to_values - - byte_map = None - - if sch.out_of_range_label is None and string_map: - # sort by length of key first, and then sort alphabetically - sorted_string_map = {k: v for k, v in sorted(string_map.items(), key=lambda item: (len(item[0]), item[0]))} - sorted_string_key = [(len(k), np.frombuffer(k.encode(), dtype=np.uint8), v) for k, v in sorted_string_map.items()] - sorted_string_values = list(sorted_string_map.values()) - - # assign byte_map_key_lengths, byte_map_value - byte_map_key_lengths = np.zeros(len(sorted_string_map), dtype=np.uint8) - byte_map_value = np.zeros(len(sorted_string_map), dtype=np.uint8) - - for i, (length, _, v) in enumerate(sorted_string_key): - byte_map_key_lengths[i] = length - byte_map_value[i] = v - - # assign byte_map_keys, byte_map_key_indices - byte_map_keys = np.zeros(sum(byte_map_key_lengths), dtype=np.uint8) - byte_map_key_indices = np.zeros(len(sorted_string_map)+1, dtype=np.uint8) - - idx_pointer = 0 - for i, (_, b_key, _) in enumerate(sorted_string_key): - for b in b_key: - byte_map_keys[idx_pointer] = b - idx_pointer += 1 - - byte_map_key_indices[i + 1] = idx_pointer - - - byte_map = [byte_map_keys, byte_map_key_lengths, byte_map_key_indices, byte_map_value] - - categorical_map_list.append(byte_map) - - - new_fields[field_name] = writer - new_field_list.append(writer) - field_chunk_list.append(writer.chunk_factory(chunk_size)) - - column_ids, column_vals = file_read_line_fast_csv(source) - - print(f"CSV read {time.time() - time0}s") - - chunk_index = 0 - - total_col = [] - - for ith, i_c in enumerate(index_map): - chunk_index = 0 - - if show_progress_every: - if i_c % 1 == 0: - print(f"{i_c} cols parsed in {time.time() - time0}s") - - if early_filter is not None: - if not early_filter[1](row[early_key_index]): - continue - - if i_c == stop_after: - break - - categorical_map = None - if len(categorical_map_list) > ith: - cat_keys, cat_key_len, cat_index, cat_values = categorical_map_list[ith] - - @njit - def my_fast_categorical_mapper(chunk, chunk_index, chunk_size, cat_keys, cat_key_len, cat_index, cat_values): - error_row_idx = -1 - for row_idx in range(chunk_size): - # Finds length, which we use to lookup potential matches - key_start = column_ids[i_c, chunk_index + row_idx] - key_end = column_ids[i_c, chunk_index + row_idx + 1] - key_len = key_end - key_start - - # start_idx = np.searchsorted(cat_key_len, key_len, "left") - # stop_idx = np.searchsorted(cat_key_len, key_len, "right") - - # print('key_start', key_start, 'key_end', key_end) - # print('start_idx', start_idx, 'stop_idx', stop_idx) - - for i in range(len(cat_index) - 1): - sc_key_len = cat_index[i + 1] - cat_index[i] - - if key_len != sc_key_len: - continue - - index = i - for j in range(key_len): - entry_start = cat_index[i] - if column_vals[i_c, key_start + j] != cat_keys[entry_start + j]: - index = -1 - break - - if index != -1: - chunk[row_idx] = cat_values[index] - - return error_row_idx - - - total = [] - chunk_index = 0 - indices_len = len(column_ids[i_c]) - - # print('@@@@@') - # print('column_ids', 'i_c', i_c, column_ids) - # print('column_vals', 'i_c', i_c, column_vals) - # print('@@@@@') - while chunk_index < indices_len: - if chunk_index + chunk_size > indices_len: - chunk_size = indices_len - chunk_index - - #print('chunk_size', chunk_size) - - chunk = np.zeros(chunk_size, dtype=np.uint8) - - my_fast_categorical_mapper(chunk, chunk_index, chunk_size, cat_keys, cat_key_len, cat_index, cat_values) - - new_field_list[ith].write_part(chunk) - total.extend(chunk) - chunk_index += chunk_size - - total_col.append(total) - - print("i_c", i_c, Counter(total)) - - - if chunk_index != 0: - new_field_list[ith].write_part(chunk[:chunk_index]) - #total.extend(chunk[:chunk_index]) - - - for i_df in range(len(index_map)): - new_field_list[i_df].flush() - - - print(f"Total time {time.time() - time0}s") - #exit() - - - def old(self, datastore, source, hf, space, schema, timestamp, - include=None, exclude=None, - keys=None, - stop_after=None, show_progress_every=None, filter_fn=None, - early_filter=None): # self.names_ = list() self.index_ = None @@ -382,9 +147,6 @@ def old(self, datastore, source, hf, space, schema, timestamp, if space in exclude and len(exclude[space]) > 0: available_keys = [k for k in available_keys if k not in exclude[space]] - available_keys = ['ruc11cd'] - available_keys = ['ruc11cd','ruc11'] - # available_keys = csvf.fieldnames if not keys: @@ -413,7 +175,6 @@ def old(self, datastore, source, hf, space, schema, timestamp, new_field_list = list() field_chunk_list = list() categorical_map_list = list() - # TODO: categorical writers should use the datatype specified in the schema for i_n in range(len(fields_to_use)): field_name = fields_to_use[i_n] @@ -433,7 +194,6 @@ def old(self, datastore, source, hf, space, schema, timestamp, chunk_index = 0 try: - total = [[],[]] for i_r, row in enumerate(ecsvf): if show_progress_every: if i_r % show_progress_every == 0: @@ -462,35 +222,19 @@ def old(self, datastore, source, hf, space, schema, timestamp, for i_df in range(len(index_map)): # with utils.Timer("writing to {}".format(self.names_[i_df])): # new_field_list[i_df].write_part(field_chunk_list[i_df]) - total[i_df].extend(field_chunk_list[i_df]) - new_field_list[i_df].write_part(field_chunk_list[i_df]) chunk_index = 0 except Exception as e: msg = "row {}: caught exception {}\nprevious row {}" print(msg.format(i_r + 1, e, row)) - - raise if chunk_index != 0: for i_df in range(len(index_map)): new_field_list[i_df].write_part(field_chunk_list[i_df][:chunk_index]) - total[i_df].extend(field_chunk_list[i_df][:chunk_index]) - print("i_df", i_df, Counter(total[i_df])) - - print('ruc == ruc11cd', total[0] == total[1]) - for i_df in range(len(index_map)): new_field_list[i_df].flush() print(f"{i_r} rows parsed in {time.time() - time0}s") - - print(f"Total time {time.time() - time0}s") - -def get_cell(row, col, column_inds, column_vals): - start_row_index = column_inds[col][row] - end_row_index = column_inds[col][row+1] - return column_vals[col][start_row_index:end_row_index].tobytes() diff --git a/exetera/core/readerwriter.py b/exetera/core/readerwriter.py index 3417a7da..1008e395 100644 --- a/exetera/core/readerwriter.py +++ b/exetera/core/readerwriter.py @@ -32,15 +32,15 @@ def __getitem__(self, item): start = item.start if item.start is not None else 0 stop = item.stop if item.stop is not None else len(self.field['index']) - 1 step = item.step - #TODO: validate slice - index = self.field['index'][start:stop+1] + # TODO: validate slice + index = self.field['index'][start:stop + 1] bytestr = self.field['values'][index[0]:index[-1]] - results = [None] * (len(index)-1) + results = [None] * (len(index) - 1) startindex = start for ir in range(len(results)): - results[ir] =\ - bytestr[index[ir]-np.int64(startindex): - index[ir+1]-np.int64(startindex)].tobytes().decode() + results[ir] = \ + bytestr[index[ir] - np.int64(startindex): + index[ir + 1] - np.int64(startindex)].tobytes().decode() return results except Exception as e: print("{}: unexpected exception {}".format(self.field.name, e)) @@ -59,7 +59,7 @@ def dtype(self): def sort(self, index, writer): field_index = self.field['index'][:] field_values = self.field['values'][:] - r_field_index, r_field_values =\ + r_field_index, r_field_values = \ pers._apply_sort_to_index_values(index, field_index, field_values) writer.write_raw(r_field_index, r_field_values) @@ -248,11 +248,7 @@ def write_part(self, values): self.ever_written = True for s in values: - if isinstance(s, str): - evalue = s.encode() - else: - evalue = s - + evalue = s.encode() for v in evalue: self.values[self.value_index] = v self.value_index += 1 @@ -422,7 +418,7 @@ def __init__(self, datastore, group, name, nformat, parser, invalid_value=0, self.flag_writer = None if create_flag_field: self.flag_writer = NumericWriter(datastore, group, f"{name}{flag_field_suffix}", - 'bool', timestamp, write_mode) + 'bool', timestamp, write_mode) self.field_name = name self.parser = parser self.invalid_value = invalid_value @@ -443,24 +439,27 @@ def write_part(self, values): validity = np.zeros(len(values), dtype='bool') for i in range(len(values)): valid, value = self.parser(values[i], self.invalid_value) + elements[i] = value validity[i] = valid - - if self.validation_mode == 'strict' and not valid: - if self._is_blank(values[i]): - raise ValueError(f"Numeric value in the field '{self.field_name}' can not be empty in strict mode") - else: - raise ValueError(f"The following numeric value in the field '{self.field_name}' can not be parsed:{values[i].strip()}") - if self.validation_mode == 'allow_empty' and not self._is_blank(values[i]) and not valid: - raise ValueError(f"The following numeric value in the field '{self.field_name}' can not be parsed:{values[i]}") + if self.validation_mode == 'strict' and not valid: + if self._is_blank_str(values[i]): + raise ValueError(f"Numeric value in the field '{self.field_name}' can not be empty in strict mode") + else: + raise ValueError( + f"The following numeric value in the field '{self.field_name}' can not be parsed:{values[i].strip()}") + + if self.validation_mode == 'allow_empty' and not self._is_blank_str(values[i]) and not valid: + raise ValueError( + f"The following numeric value in the field '{self.field_name}' can not be parsed:{values[i].strip()}") self.data_writer.write_part(elements) if self.flag_writer is not None: self.flag_writer.write_part(validity) - def _is_blank(self, value): - return (isinstance(value, str) and value.strip() == '') or value == b'' + def _is_blank_str(self, value): + return type(value) == str and value.strip() == '' def flush(self): self.data_writer.flush() @@ -552,7 +551,7 @@ def __init__(self, datastore, group, name, create_day_field=False, self.create_day_field = create_day_field if create_day_field: self.datestr = FixedStringWriter(datastore, group, f"{name}_day", - '10', timestamp, write_mode) + '10', timestamp, write_mode) self.datetimeset = None if optional: self.datetimeset = NumericWriter(datastore, group, f"{name}_set", @@ -566,11 +565,11 @@ def write_part(self, values): self.datetime.write_part(values) if self.create_day_field: - days=self._get_days(values) + days = self._get_days(values) self.datestr.write_part(days) if self.datetimeset is not None: - flags=self._get_flags(values) + flags = self._get_flags(values) self.datetimeset.write_part(flags) def _get_days(self, values): @@ -731,10 +730,10 @@ def __init__(self, datastore, group, name, create_day_field=False, self.create_day_field = create_day_field if create_day_field: self.datestr = FixedStringWriter(datastore, group, f"{name}_day", - '10', timestamp, write_mode) + '10', timestamp, write_mode) self.dateset = None if optional: - self.dateset =\ + self.dateset = \ NumericWriter(datastore, group, f"{name}_set", 'bool', timestamp, write_mode) def chunk_factory(self, length): @@ -771,4 +770,4 @@ def flush(self): def write(self, values): self.write_part(values) - self.flush() + self.flush() \ No newline at end of file diff --git a/resources/assessment_input_small_data.csv b/resources/assessment_input_small_data.csv deleted file mode 100644 index 72167bb5..00000000 --- a/resources/assessment_input_small_data.csv +++ /dev/null @@ -1,1297 +0,0 @@ -id,patient_id,created_at,updated_at,version,country_code,health_status,date_test_occurred,date_test_occurred_guess,fever,temperature,temperature_unit,persistent_cough,fatigue,shortness_of_breath,diarrhoea,diarrhoea_frequency,delirium,skipped_meals,location,treatment,had_covid_test,tested_covid_positive,abdominal_pain,chest_pain,hoarse_voice,loss_of_smell,headache,headache_frequency,other_symptoms,chills_or_shivers,eye_soreness,nausea,dizzy_light_headed,red_welts_on_face_or_lips,blisters_on_feet,typical_hayfever,sore_throat,unusual_muscle_pains,level_of_isolation,isolation_little_interaction,isolation_lots_of_people,isolation_healthcare_provider,always_used_shortage,have_used_PPE,never_used_shortage,sometimes_used_shortage,interacted_any_patients,treated_patients_with_covid,worn_face_mask,mask_cloth_or_scarf,mask_surgical,mask_n95_ffp,mask_not_sure_pfnts,mask_other,rash,skin_burning,hair_loss,feeling_down,brain_fog,altered_smell,runny_nose,sneezing,earache,ear_ringing,swollen_glands,irregular_heartbeat -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -7f52b83d8bf6acd0859fba9a138ee9ad,6cdbb2cd6264dcb5460b8c40cb84c645,2020-03-21 16:05:49.933000+00:00,2020-03-21 16:09:42.640000+00:00,,GB,not_healthy,,,False,36.7,C,True,mild,severe,True,,True,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -fd34deffb6b5994da20ffaf575cd2e80,62f3995963ac837194c6a1fe905b8b4b,2020-03-21 20:28:50.036000+00:00,2020-03-21 20:29:44.912000+00:00,,GB,not_healthy,,,False,,,False,mild,no,True,,False,True,home,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -0d6e1ec90413f82e09004148051dbe89,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:56:41.072000+00:00,2020-03-22 06:56:42.723000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f882d60674fda7b57257d043fd76f9f6,a0759694a167ce1646a9285d2dd62c1d,2020-03-22 06:57:06.833000+00:00,2020-03-22 06:57:14.167000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -f93c6d09fb4deb46fcfd7ba424e7bee9,5810eae8dbf08a96be39126533114ecb,2020-03-21 09:38:08.548000+00:00,2020-03-21 09:38:16.901000+00:00,,GB,healthy,,,,,,,,,,,,,,,False,no,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -6217d4aba3607f1abba49506a8c18447,3c1058cf9f4584a388d27e5e746db915,2020-03-21 20:11:33.632000+00:00,2020-03-21 20:13:30.007000+00:00,,GB,not_healthy,,,True,38.0,C,True,mild,mild,False,,True,True,back_from_hospital,oxygen,False,,,,,,,,,,,,,,,,,,,,,,,,,,,, \ No newline at end of file diff --git a/tests/test_csv_reader_speedup.py b/tests/test_csv_reader_speedup.py deleted file mode 100644 index 791d0370..00000000 --- a/tests/test_csv_reader_speedup.py +++ /dev/null @@ -1,118 +0,0 @@ -from unittest import TestCase -from exetera.core.csv_reader_speedup import my_fast_csv_reader, file_read_line_fast_csv, get_byte_map, my_fast_categorical_mapper -import tempfile -import numpy as np -import os -import pandas as pd - - -TEST_SCHEMA = [{'name': 'a', 'type': 'cat', 'vals': ('','a', 'bb', 'ccc', 'dddd', 'eeeee'), - 'strings_to_values': {'':0,'a':1, 'bb':2, 'ccc':3, 'dddd':4, 'eeeee':5}}, - {'name': 'b', 'type': 'float'}, - {'name': 'c', 'type': 'cat', 'vals': ('', '', '', '', '', 'True', 'False'), - 'strings_to_values': {"": 0, "False": 1, "True": 2}}, - {'name': 'd', 'type': 'float'}, - {'name': 'e', 'type': 'float'}, - {'name': 'f', 'type': 'cat', 'vals': ('', '', '', '', '', 'True', 'False'), - 'strings_to_values': {"": 0, "False": 1, "True": 2}}, - {'name': 'g', 'type': 'cat', 'vals': ('', '', '', '', 'True', 'False'), - 'strings_to_values': {"": 0, "False": 1, "True": 2}}, - {'name': 'h', 'type': 'cat', 'vals': ('', '', '', 'No', 'Yes'), - 'strings_to_values': {"": 0, "No": 1, "Yes": 2}}] - - - -class TestFastCSVReader(TestCase): - # def setUp(self): - - # self.fd_csv, self.csv_file_name = tempfile.mkstemp(suffix='.csv') - # with open(self.csv_file_name, 'w') as fcsv: - # fcsv.write(TEST_CSV_CONTENTS) - - def _make_test_data(self, count, schema, csv_file_name): - """ - [ {'name':name, 'type':'cat'|'float'|'fixed', 'values':(vals)} ] - """ - import pandas as pd - rng = np.random.RandomState(12345678) - columns = {} - cat_columns_v = {} - cat_map_dict = {} - for s in schema: - if s['type'] == 'cat': - vals = s['vals'] - arr = rng.randint(low=0, high=len(vals), size=count) - larr = [None] * count - arr_str_to_val = [None] * count - for i in range(len(arr)): - larr[i] = vals[arr[i]] - arr_str_to_val[i] = s['strings_to_values'][vals[arr[i]]] - - columns[s['name']] = larr - cat_columns_v[s['name']] = arr_str_to_val - cat_map_dict[s['name']] = s['strings_to_values'] - - elif s['type'] == 'float': - arr = rng.uniform(size=count) - columns[s['name']] = arr - - # create csv file - df = pd.DataFrame(columns) - df.to_csv(csv_file_name, index = False) - - # create byte map for each categorical field - fieldnames = list(df) - categorical_map_list = [None] * len(fieldnames) - for i, fn in enumerate(fieldnames): - if fn in cat_map_dict: - string_map = cat_map_dict[fn] - categorical_map_list[i] = get_byte_map(string_map) - - return df, cat_columns_v, categorical_map_list - - - def test_my_fast_csv_reader(self): - self.fd_csv, self.csv_file_name = tempfile.mkstemp(suffix='.csv') - df, cat_columns_v, categorical_map_list = self._make_test_data(5, TEST_SCHEMA, self.csv_file_name) - print(df) - print(cat_columns_v) - print(categorical_map_list) - - column_inds, column_vals = file_read_line_fast_csv(self.csv_file_name) - - - field_to_use = list(df) - for i_c, field in enumerate(field_to_use): - if categorical_map_list[i_c] is None: - continue - - cat_keys, _, cat_index, cat_values = categorical_map_list[i_c] - print(cat_keys, cat_index, cat_values ) - - chunk_size = 10 - chunk = np.zeros(chunk_size, dtype=np.uint8) - - pos = my_fast_categorical_mapper(chunk, i_c, column_inds, column_vals, cat_keys, cat_index, cat_values) - - chunk = list(chunk[:pos]) - - self.assertListEqual(chunk, cat_columns_v[field]) - - - os.close(self.fd_csv) - - - # print('=====') - # with open(self.csv_file_name) as f: - # print(f.read()) - # print('=====') - - # df = pd.read_csv(self.csv_file_name) - # print(df) - # print(list(df)) - - - - - # def tearDown(self): - # os.close(self.fd_csv) From a7b477d9001501c5eee9ec32ac9e3527d7658a37 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 27 Apr 2021 09:00:46 +0100 Subject: [PATCH 098/181] minor update --- exetera/core/fields.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 3509fd2d..9a1e968e 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -22,7 +22,7 @@ from exetera.core import validation as val class HDF5Field(Field): - def __init__(self, session, group, dataframe, name=None, write_enabled=False): + def __init__(self, session, group, dataframe, write_enabled=False): """ Construct a HDF5 file based Field. This construction is not used directly, rather, should be called from specific field types, e.g. NumericField. @@ -30,7 +30,6 @@ def __init__(self, session, group, dataframe, name=None, write_enabled=False): :param session: The session instance. :param group: The HDF5 Group object. :param dataframe: The dataframe this field belongs to. - :param name: The name of this field if not specified in group. :param write_enabled: A read-only/read-write switch. """ super().__init__() From 29f736d5f9b265971ada9f87aa1438714c918226 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 27 Apr 2021 09:41:40 +0100 Subject: [PATCH 099/181] unit test for logical not in numeric field --- exetera/core/operations.py | 4 ++-- tests/test_fields.py | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/exetera/core/operations.py b/exetera/core/operations.py index fdca2150..5cc95034 100644 --- a/exetera/core/operations.py +++ b/exetera/core/operations.py @@ -68,7 +68,7 @@ def safe_map_indexed_values(data_indices, data_values, map_field, map_filter, em return i_result, v_result -#@njit +@njit def safe_map_values(data_field, map_field, map_filter, empty_value=None): result = np.zeros_like(map_field, dtype=data_field.dtype) empty_val = result[0] if empty_value is None else empty_value @@ -444,7 +444,7 @@ def apply_spans_index_of_last_filter(spans, dest_array, filter_array): return dest_array, filter_array -#@njit +@njit def apply_spans_count(spans, dest_array): for i in range(len(spans)-1): dest_array[i] = np.int64(spans[i+1] - spans[i]) diff --git a/tests/test_fields.py b/tests/test_fields.py index aeb7d7eb..07d10056 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -337,6 +337,35 @@ def test_tuple(expected, actual): 'f3', fields.dtype_to_str(r.data.dtype)).data.write(r) test_simple(expected, df['f3']) + def _execute_uniary_field_test(self, a1, function): + + def test_simple(expected, actual): + self.assertListEqual(expected.tolist(), actual.data[:].tolist()) + + def test_tuple(expected, actual): + self.assertListEqual(expected[0].tolist(), actual[0].data[:].tolist()) + self.assertListEqual(expected[1].tolist(), actual[1].data[:].tolist()) + + expected = function(a1) + + test_equal = test_tuple if isinstance(expected, tuple) else test_simple + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + + m1 = fields.NumericMemField(s, fields.dtype_to_str(a1.dtype)) + m1.data.write(a1) + + f1 = df.create_numeric('f1', fields.dtype_to_str(a1.dtype)) + f1.data.write(a1) + + # test memory field and field operations + test_equal(expected, function(f1)) + test_equal(expected, function(f1)) + test_equal(expected, function(m1)) + def test_mixed_field_add(self): a1 = np.array([1, 2, 3, 4], dtype=np.int32) @@ -407,6 +436,17 @@ def test_mixed_field_or(self): self._execute_memory_field_test(a1, a2, 1, lambda x, y: x | y) self._execute_field_test(a1, a2, 1, lambda x, y: x | y) + def test_mixed_field_invert(self): + # invert (~) symbol is used for logical not in field, hence different function called. Thus not using _execute_field_test + a1 = np.array([0, 0, 1, 1], dtype=np.int32) + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f1 = df.create_numeric('f1','int32') + f1.data.write(a1) + self.assertListEqual(np.logical_not(a1).tolist(), (~f1).data[:].tolist()) + def test_less_than(self): a1 = np.array([1, 2, 3, 4], dtype=np.int32) From 7fd9bdce7ba64e33ddcf9a0157cb7c2c6846878f Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 28 Apr 2021 16:44:56 +0100 Subject: [PATCH 100/181] patch for get_spans for datastore --- exetera/core/persistence.py | 24 +++++++++++++++--------- tests/test_persistence.py | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/exetera/core/persistence.py b/exetera/core/persistence.py index 4f69705f..314aaf15 100644 --- a/exetera/core/persistence.py +++ b/exetera/core/persistence.py @@ -848,16 +848,22 @@ def distinct(self, field=None, fields=None, filter=None): def get_spans(self, field=None, fields=None): - if fields is not None: - if isinstance(fields[0], fld.Field): - return ops._get_spans_for_2_fields_by_spans(fields[0].get_spans(), fields[1].get_spans()) - if isinstance(fields[0], np.ndarray): - return ops._get_spans_for_2_fields(fields[0], fields[1]) + if field is None and fields is None: + raise ValueError("One of 'field' and 'fields' must be set") + if field is not None and fields is not None: + raise ValueError("Only one of 'field' and 'fields' may be set") + raw_field = None + raw_fields = None + if field is not None: + val._check_is_reader_or_ndarray('field', field) + raw_field = field[:] if isinstance(field, rw.Reader) else field + return ops.get_spans_for_field(raw_field) else: - if isinstance(field, fld.Field): - return field.get_spans() - if isinstance(field, np.ndarray): - return ops.get_spans_for_field(field) + raw_fields = [] + for f in fields: + val._check_is_reader_or_ndarray('elements of tuple/list fields', f) + raw_fields.append(f[:] if isinstance(f, rw.Reader) else f) + return ops._get_spans_for_2_fields(raw_fields[0], raw_fields[1]) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index bde97cc9..0d33fd1b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -739,6 +739,17 @@ def test_get_spans_single_field_numeric(self): a = np.asarray([0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2]) self.assertTrue(np.array_equal(np.asarray([0, 7, 14, 22]), s.get_spans(field=a))) + bio = BytesIO() + src = session.open_dataset(bio, 'w', 'src') + df = src.create_dataframe('df') + num = df.create_numeric('num', 'int32') + num.data.write([1, 1, 2, 2, 3, 3, 4]) + session.close_dataset('src') + + with h5py.File(bio, 'r') as src: + pat_id = datastore.get_reader(src['df']['num']) + spans = datastore.get_spans(field=pat_id) + self.assertListEqual([0, 2, 4, 6, 7],spans[:].tolist()) def test_get_spans_single_field_string(self): datastore = persistence.DataStore(10) @@ -770,6 +781,17 @@ def test_get_spans_single_field_string(self): a = np.asarray([b'aa', b'bb', b'cc'], dtype='S2') self.assertTrue(np.array_equal(np.asarray([0, 1, 2, 3]), s.get_spans(field=a))) + bio = BytesIO() + src = session.open_dataset(bio, 'w', 'src') + df = src.create_dataframe('df') + num = df.create_fixed_string('fst', 1) + num.data.write([b'a', b'a', b'b', b'c', b'c', b'c', b'd', b'd', b'd']) + session.close_dataset('src') + + with h5py.File(bio, 'r') as src: + pat_id = datastore.get_reader(src['df']['fst']) + spans = datastore.get_spans(field=pat_id) + self.assertListEqual([0, 2, 3, 6, 9], spans[:].tolist()) def test_apply_spans_count(self): spans = np.asarray([0, 1, 3, 4, 7, 8, 12, 14]) From 04df757bb683f446d7b68f2d94ec9dc2e4766291 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 28 Apr 2021 16:49:15 +0100 Subject: [PATCH 101/181] tests for two fields --- tests/test_persistence.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0d33fd1b..3bb005aa 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -744,12 +744,18 @@ def test_get_spans_single_field_numeric(self): df = src.create_dataframe('df') num = df.create_numeric('num', 'int32') num.data.write([1, 1, 2, 2, 3, 3, 4]) + + num2 = df.create_numeric('num2', 'int32') + num2.data.write([1, 1, 2, 3, 3, 3, 4]) session.close_dataset('src') with h5py.File(bio, 'r') as src: - pat_id = datastore.get_reader(src['df']['num']) - spans = datastore.get_spans(field=pat_id) + num = datastore.get_reader(src['df']['num']) + spans = datastore.get_spans(field=num) self.assertListEqual([0, 2, 4, 6, 7],spans[:].tolist()) + num2 = datastore.get_reader(src['df']['num2']) + spans = datastore.get_spans(fields=(num, num2)) + self.assertListEqual([0, 2, 3, 4, 6, 7], spans[:].tolist()) def test_get_spans_single_field_string(self): datastore = persistence.DataStore(10) @@ -784,13 +790,18 @@ def test_get_spans_single_field_string(self): bio = BytesIO() src = session.open_dataset(bio, 'w', 'src') df = src.create_dataframe('df') - num = df.create_fixed_string('fst', 1) - num.data.write([b'a', b'a', b'b', b'c', b'c', b'c', b'd', b'd', b'd']) + fst = df.create_fixed_string('fst', 1) + fst.data.write([b'a', b'a', b'b', b'c', b'c', b'c', b'd', b'd', b'd']) + fst2 = df.create_fixed_string('fst2', 1) + fst2.data.write([b'a', b'a', b'b', b'c', b'c', b'c', b'd', b'd', b'd']) session.close_dataset('src') with h5py.File(bio, 'r') as src: - pat_id = datastore.get_reader(src['df']['fst']) - spans = datastore.get_spans(field=pat_id) + fst = datastore.get_reader(src['df']['fst']) + spans = datastore.get_spans(field=fst) + self.assertListEqual([0, 2, 3, 6, 9], spans[:].tolist()) + fst2 = datastore.get_reader(src['df']['fst2']) + spans = datastore.get_spans(fields=(fst, fst2)) self.assertListEqual([0, 2, 3, 6, 9], spans[:].tolist()) def test_apply_spans_count(self): From e47e15c760dbce74688e0ad0ee1f4b5cb12d2cf6 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 29 Apr 2021 11:34:13 +0100 Subject: [PATCH 102/181] add as type to numeric field --- exetera/core/fields.py | 23 +++++++++++++++++++++++ tests/test_fields.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 09374575..d0f05ca8 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -1422,6 +1422,29 @@ def apply_index(self, index_to_apply, target=None, in_place=False): self._ensure_valid() return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) + def astype(self, type, **kwargs): + if type == 'indexedstring': + raise NotImplementedError() + elif type == 'fixedstring': + if 'length' not in kwargs.keys(): + raise ValueError("Please provide the length for fixed string field.") + else: + length = kwargs['length'] + fld = FixedStringMemField(self._session, length) + result = np.zeros(int(len(self)/length), dtype = "U"+str(length)) + for i in range(0, len(self), length): + result[int(i/length)] = ''.join([chr(i) for i in self.data[i:i+length]]) + fld.data.write(result) + return fld + elif type == 'categorical': + if 'key' not in kwargs.keys(): + raise ValueError("Please provide the key for categorical field.") + else: + key = kwargs['key'] + fld = CategoricalMemField(self._session, 'uint8', key) + fld.data.write(self.data[:]) + return fld + def apply_spans_first(self, spans_to_apply, target=None, in_place=False): self._ensure_valid() return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) diff --git a/tests/test_fields.py b/tests/test_fields.py index aeb7d7eb..07d10056 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -337,6 +337,35 @@ def test_tuple(expected, actual): 'f3', fields.dtype_to_str(r.data.dtype)).data.write(r) test_simple(expected, df['f3']) + def _execute_uniary_field_test(self, a1, function): + + def test_simple(expected, actual): + self.assertListEqual(expected.tolist(), actual.data[:].tolist()) + + def test_tuple(expected, actual): + self.assertListEqual(expected[0].tolist(), actual[0].data[:].tolist()) + self.assertListEqual(expected[1].tolist(), actual[1].data[:].tolist()) + + expected = function(a1) + + test_equal = test_tuple if isinstance(expected, tuple) else test_simple + + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + + m1 = fields.NumericMemField(s, fields.dtype_to_str(a1.dtype)) + m1.data.write(a1) + + f1 = df.create_numeric('f1', fields.dtype_to_str(a1.dtype)) + f1.data.write(a1) + + # test memory field and field operations + test_equal(expected, function(f1)) + test_equal(expected, function(f1)) + test_equal(expected, function(m1)) + def test_mixed_field_add(self): a1 = np.array([1, 2, 3, 4], dtype=np.int32) @@ -407,6 +436,17 @@ def test_mixed_field_or(self): self._execute_memory_field_test(a1, a2, 1, lambda x, y: x | y) self._execute_field_test(a1, a2, 1, lambda x, y: x | y) + def test_mixed_field_invert(self): + # invert (~) symbol is used for logical not in field, hence different function called. Thus not using _execute_field_test + a1 = np.array([0, 0, 1, 1], dtype=np.int32) + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + f1 = df.create_numeric('f1','int32') + f1.data.write(a1) + self.assertListEqual(np.logical_not(a1).tolist(), (~f1).data[:].tolist()) + def test_less_than(self): a1 = np.array([1, 2, 3, 4], dtype=np.int32) From 5492b9413a7f921efa61f418622008fa8cd99d41 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 29 Apr 2021 14:52:21 +0100 Subject: [PATCH 103/181] seperate the unittest of get_spans by datastore reader --- tests/test_persistence.py | 45 +++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3bb005aa..81bb08bc 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -739,24 +739,6 @@ def test_get_spans_single_field_numeric(self): a = np.asarray([0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2]) self.assertTrue(np.array_equal(np.asarray([0, 7, 14, 22]), s.get_spans(field=a))) - bio = BytesIO() - src = session.open_dataset(bio, 'w', 'src') - df = src.create_dataframe('df') - num = df.create_numeric('num', 'int32') - num.data.write([1, 1, 2, 2, 3, 3, 4]) - - num2 = df.create_numeric('num2', 'int32') - num2.data.write([1, 1, 2, 3, 3, 3, 4]) - session.close_dataset('src') - - with h5py.File(bio, 'r') as src: - num = datastore.get_reader(src['df']['num']) - spans = datastore.get_spans(field=num) - self.assertListEqual([0, 2, 4, 6, 7],spans[:].tolist()) - num2 = datastore.get_reader(src['df']['num2']) - spans = datastore.get_spans(fields=(num, num2)) - self.assertListEqual([0, 2, 3, 4, 6, 7], spans[:].tolist()) - def test_get_spans_single_field_string(self): datastore = persistence.DataStore(10) session = Session() @@ -787,15 +769,22 @@ def test_get_spans_single_field_string(self): a = np.asarray([b'aa', b'bb', b'cc'], dtype='S2') self.assertTrue(np.array_equal(np.asarray([0, 1, 2, 3]), s.get_spans(field=a))) + def test_get_spans_from_datastore_reader(self): bio = BytesIO() - src = session.open_dataset(bio, 'w', 'src') - df = src.create_dataframe('df') - fst = df.create_fixed_string('fst', 1) - fst.data.write([b'a', b'a', b'b', b'c', b'c', b'c', b'd', b'd', b'd']) - fst2 = df.create_fixed_string('fst2', 1) - fst2.data.write([b'a', b'a', b'b', b'c', b'c', b'c', b'd', b'd', b'd']) - session.close_dataset('src') + with Session() as session: + src = session.open_dataset(bio, 'w', 'src') + df = src.create_dataframe('df') + fst = df.create_fixed_string('fst', 1) + fst.data.write([b'a', b'a', b'b', b'c', b'c', b'c', b'd', b'd', b'd']) + fst2 = df.create_fixed_string('fst2', 1) + fst2.data.write([b'a', b'a', b'b', b'c', b'c', b'c', b'd', b'd', b'd']) + num = df.create_numeric('num', 'int32') + num.data.write([1, 1, 2, 2, 3, 3, 4]) + num2 = df.create_numeric('num2', 'int32') + num2.data.write([1, 1, 2, 3, 3, 3, 4]) + session.close_dataset('src') + datastore = persistence.DataStore(10) with h5py.File(bio, 'r') as src: fst = datastore.get_reader(src['df']['fst']) spans = datastore.get_spans(field=fst) @@ -803,6 +792,12 @@ def test_get_spans_single_field_string(self): fst2 = datastore.get_reader(src['df']['fst2']) spans = datastore.get_spans(fields=(fst, fst2)) self.assertListEqual([0, 2, 3, 6, 9], spans[:].tolist()) + num = datastore.get_reader(src['df']['num']) + spans = datastore.get_spans(field=num) + self.assertListEqual([0, 2, 4, 6, 7], spans[:].tolist()) + num2 = datastore.get_reader(src['df']['num2']) + spans = datastore.get_spans(fields=(num, num2)) + self.assertListEqual([0, 2, 3, 4, 6, 7], spans[:].tolist()) def test_apply_spans_count(self): spans = np.asarray([0, 1, 3, 4, 7, 8, 12, 14]) From 25320bdc3e35750a466ff51b1092985b8f6ccdd1 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 29 Apr 2021 16:28:56 +0100 Subject: [PATCH 104/181] unittest for astype --- exetera/core/fields.py | 2 ++ tests/test_fields.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index d0f05ca8..101c1668 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -1444,6 +1444,8 @@ def astype(self, type, **kwargs): fld = CategoricalMemField(self._session, 'uint8', key) fld.data.write(self.data[:]) return fld + else: + raise NotImplementedError("The type {} is not convertible.".format(type)) def apply_spans_first(self, spans_to_apply, target=None, in_place=False): self._ensure_valid() diff --git a/tests/test_fields.py b/tests/test_fields.py index 07d10056..e1493642 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -256,6 +256,21 @@ def test_numeric_create_like(self): foo2.data.write(mfoo) self.assertListEqual([2, 3, 4, 5], foo2.data[:].tolist()) + def test_numeric_as_type(self): + bio = BytesIO() + with session.Session() as s: + ds = s.open_dataset(bio, 'w', 'ds') + df = ds.create_dataframe('df') + foo = df.create_numeric('foo', 'int32') + foo.data.write(np.array([65, 66, 67, 68, 97, 98, 99, 100])) + newf = foo.astype('fixedstring', length=2) + self.assertListEqual(['AB', 'CD', 'ab', 'cd'], newf.data[:].tolist()) + + foo.data.clear() + foo.data.write([0, 0, 1, 1, 1, 2, 2, 2]) + newf = foo.astype('categorical', key={'foo': 0, 'bar': 1, 'boo': 2}) + self.assertListEqual([0, 0, 1, 1, 1, 2, 2, 2], newf.data[:].tolist()) + class TestMemoryFields(unittest.TestCase): From 87df0bcc297c1d37f4702f6a095a577ea1f2f5f8 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 10 May 2021 17:07:44 +0100 Subject: [PATCH 105/181] update astype for fields, update logical_not for numeric fields --- exetera/core/dataframe.py | 102 +++++++++++++++++++++++++++--------- exetera/core/fields.py | 74 ++++++++++++++------------ exetera/core/persistence.py | 11 ++++ tests/test_dataframe.py | 27 ++++++++-- tests/test_fields.py | 28 ++++++++-- 5 files changed, 175 insertions(+), 67 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 19d044d9..e9a247c4 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -62,6 +62,9 @@ def __init__(self, for subg in h5group.keys(): self._columns[subg] = dataset.session.get(h5group[subg]) + self._index_filter = None + self._column_filter = None + @property def columns(self): """ @@ -84,6 +87,14 @@ def h5group(self): """ return self._h5group + @property + def column_filter(self): + return self._column_filter + + @property + def index_filter(self): + return self._index_filter + def add(self, field: fld.Field): """ @@ -212,7 +223,8 @@ def __contains__(self, name): if not isinstance(name, str): raise TypeError("The name must be a str object.") else: - return name in self._columns + return name in self._columns if self._column_filter is None \ + else (name in self._columns and name in self._column_filter) def contains_field(self, field): """ @@ -228,6 +240,13 @@ def contains_field(self, field): return True return False + def get_field(self, name): + """ + Get a field stored by the field name. + :param name: The name of field to get. + """ + return self.__getitem__(name) + def __getitem__(self, name): """ Get a field stored by the field name. @@ -239,28 +258,45 @@ def __getitem__(self, name): elif not self.__contains__(name): raise ValueError("There is no field named '{}' in this dataframe".format(name)) else: - return self._columns[name] + if self._column_filter is None: + if self.index_filter is None: + return self._columns[name] + else: + return self._columns[name].data[:][self.index_filter] + elif name not in self.column_filter: + raise ValueError("The column to fetch is filtered.") - def get_field(self, name): + def get_data(self, name=None): """ Get a field stored by the field name. :param name: The name of field to get. """ - return self.__getitem__(name) + if name is not None: + return self.__getitem__(name) + + if self._column_filter is not None: + pass + else: + for column in self._column_filter: + return self.__getitem__(name) + def __setitem__(self, name, field): if not isinstance(name, str): raise TypeError("The name must be of type str but is of type '{}'".format(str)) - if not isinstance(field, fld.Field): - raise TypeError("The field must be a Field object.") - nfield = field.create_like(self, name) - if field.indexed: - nfield.indices.write(field.indices[:]) - nfield.values.write(field.values[:]) + if isinstance(field, fld.Field): + nfield = field.create_like(self, name) + if field.indexed: + nfield.indices.write(field.indices[:]) + nfield.values.write(field.values[:]) + else: + nfield.data.write(field.data[:]) + self._columns[name] = nfield + elif isinstance(field, list): # TODO how to handle value assignment w/ filter? + pass else: - nfield.data.write(field.data[:]) - self._columns[name] = nfield + raise TypeError("The field must be a Field or list.") def __delitem__(self, name): if not self.__contains__(name=name): @@ -393,26 +429,42 @@ def get_unique_name(name, keys): self._columns = final_columns - - def apply_filter(self, filter_to_apply, ddf=None): + def apply_filter(self, filter_to_apply, ddf=None, hard=True, axis=0): """ Apply the filter to all the fields in this dataframe, return a dataframe with filtered fields. :param filter_to_apply: the filter to be applied to the source field, an array of boolean + :param axis: {0 or ‘index’, 1 or ‘columns’, None}, default 0 + :param hard: if perform the filtering when calling and write the result now :param ddf: optional- the destination data frame :returns: a dataframe contains all the fields filterd, self if ddf is not set """ - if ddf is not None: - if not isinstance(ddf, DataFrame): - raise TypeError("The destination object must be an instance of DataFrame.") - for name, field in self._columns.items(): - newfld = field.create_like(ddf, name) - field.apply_filter(filter_to_apply, target=newfld) - return ddf - else: - for field in self._columns.values(): - field.apply_filter(filter_to_apply, in_place=True) - return self + if not isinstance(filter_to_apply, np.ndarray) and not isinstance(filter_to_apply, list): + raise TypeError("The filter must be a Numpy array or Python list.") + + if hard is False: # soft filter + if axis == 0 or axis == 'index': + self.index_filter = filter_to_apply + elif axis == 1 or axis == 'columns': + self.column_filter = filter_to_apply + else: # hard filter + if ddf is not None and ddf is not self: # filter to another df + if not isinstance(ddf, DataFrame): + raise TypeError("The destination object must be an instance of DataFrame.") + for name, field in self._columns.items(): + newfld = field.create_like(ddf, name) + field.apply_filter(filter_to_apply, target=newfld) + return ddf + elif ddf is not None and ddf is self: # filter inline + for field in self._columns.values(): + field.apply_filter(filter_to_apply, in_place=True) + return self + elif ddf is None: # return memory based df + pass + + def clean_filters(self): + self._index_filter = None + self._column_filter = None def apply_index(self, index_to_apply, ddf=None): """ diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 101c1668..d4de4256 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -24,8 +24,8 @@ class HDF5Field(Field): def __init__(self, session, group, dataframe, write_enabled=False): """ - Construct a HDF5 file based Field. This construction is not used directly, rather, should be called from - specific field types, e.g. NumericField. + Constructor for Fields based on HDF5 data store. This initializer should only be called by Field types that are + subclasses of HDF5Field, and never directly by users of ExeTera. :param session: The session instance. :param group: The HDF5 Group object. @@ -126,8 +126,8 @@ class MemoryField(Field): def __init__(self, session): """ - Construct a field stored in memory only, often used when perform arithmetic/comparison operations from storage - based fields, e.g. field3 = field1 + field2 will create a memory field during add operation and assign to field3. + Constructor for memory-based Fields. This initializer should only be called by Field types that are subclasses + of MemoryField, and never directly by users of ExeTera. :param session: The session instance. """ @@ -184,7 +184,9 @@ def apply_index(self, index_to_apply, dstfld=None): class ReadOnlyFieldArray: def __init__(self, field, dataset_name): """ - Construct a readonly FieldArray which used as the wrapper of data in Fields (apart from IndexedStringFields). + Construct a ReadOnlyFieldArray instance. This class is an implementation detail, used to access data in + non-indexed fields deriving from HDF5Field. As such, instances of ReadOnlyFieldArray should only be created by + the fields themselves, and not by users of ExeTera. :param field: The HDF5 group object used as storage. :param dataset_name: The name of the dataset object in HDF5, normally use 'values' @@ -227,7 +229,9 @@ def complete(self): class WriteableFieldArray: def __init__(self, field, dataset_name): """ - Construct a read/write FieldArray which used as the wrapper of data in Field. + Construct a WriteableFieldArray instance. This class is an implementation detail, used to access data in + non-indexed fields deriving from HDF5Field. As such, instances of WriteableFieldArray should only be created by + the Fields themselves, and not by users of ExeTera. :param field: The HDF5 group object used as storage. :param dataset_name: The name of the dataset object in HDF5, normally use 'values' @@ -271,7 +275,9 @@ def complete(self): class MemoryFieldArray: def __init__(self, dtype): """ - Construct a memory based FieldArray which used as the wrapper of data in Field. The data is stored in numpy array. + Construct a MemoryFieldArray instance. This class is an implementation detail, used to access data in + non-indexed fields deriving from MemoryField. As such, instances of MemoryFieldArray should only be created by + the Fields themselves, and not by users of ExeTera. :param dtype: The data type for construct the numpy array. """ @@ -321,9 +327,11 @@ def complete(self): class ReadOnlyIndexedFieldArray: def __init__(self, field, indices, values): """ - Construct a IndexFieldArray which used as the wrapper of data in IndexedStringField. + Construct a ReadOnlyIndexedFieldArray instance. This class is an implementation detail, used to access data in + indexed fields. As such, instances of ReadOnlyIndexedFieldArray should only be created by the Fields themselves, + and not by users of ExeTera. - :param field: The HDF5 group object for store the data. + :param field: The HDF5 group from which the data is read. :param indices: The indices of the IndexedStringField. :param values: The values of the IndexedStringField. """ @@ -791,6 +799,9 @@ def __rand__(self, first): def __xor__(self, second): return FieldDataOps.numeric_xor(self._session, self, second) + def __invert__(self): + return FieldDataOps.invert(self._session, self) + def __rxor__(self, first): return FieldDataOps.numeric_xor(self._session, first, self) @@ -1422,30 +1433,15 @@ def apply_index(self, index_to_apply, target=None, in_place=False): self._ensure_valid() return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) - def astype(self, type, **kwargs): - if type == 'indexedstring': - raise NotImplementedError() - elif type == 'fixedstring': - if 'length' not in kwargs.keys(): - raise ValueError("Please provide the length for fixed string field.") - else: - length = kwargs['length'] - fld = FixedStringMemField(self._session, length) - result = np.zeros(int(len(self)/length), dtype = "U"+str(length)) - for i in range(0, len(self), length): - result[int(i/length)] = ''.join([chr(i) for i in self.data[i:i+length]]) - fld.data.write(result) - return fld - elif type == 'categorical': - if 'key' not in kwargs.keys(): - raise ValueError("Please provide the key for categorical field.") - else: - key = kwargs['key'] - fld = CategoricalMemField(self._session, 'uint8', key) - fld.data.write(self.data[:]) - return fld - else: - raise NotImplementedError("The type {} is not convertible.".format(type)) + def astype(self, type): + if isinstance(type, str) and type not in ['int8', 'int16', 'int32', 'int64', 'uint8', 'uint16','uint32', + 'uint64', 'float16', 'float32', 'float64', 'float128', 'bool_']: + raise ValueError("The type to convert is not supported, please use numeric type such as int or float.") + elif isinstance(type, np.dtype) and type not in [np.int8, np.int16, np.int32, np.int64, np.uint8, np.uint16, + np.uint32, np.uint64, np.float16, np.float32, np.float64, + np.float128, np.bool_]: + raise ValueError("The type to convert is not supported, please use numeric type such as int or float.") + self.data._dataset = self.data[:].astype(type) def apply_spans_first(self, spans_to_apply, target=None, in_place=False): self._ensure_valid() @@ -1544,6 +1540,11 @@ def __ror__(self, first): return FieldDataOps.numeric_or(self._session, first, self) def __invert__(self): + self._ensure_valid() + return FieldDataOps.invert(self._session, self) + + def logical_not(self): + self._ensure_valid() return FieldDataOps.logical_not(self._session, self) def __lt__(self, value): @@ -2268,6 +2269,13 @@ def function_or(first, second): return cls._binary_op(session, first, second, function_or) + @classmethod + def invert(cls, session, first): + def function_invert(first): + return ~first + + return cls._unary_op(session, first, function_invert) + @classmethod def logical_not(cls, session, first): def function_logical_not(first): diff --git a/exetera/core/persistence.py b/exetera/core/persistence.py index 314aaf15..f26e2697 100644 --- a/exetera/core/persistence.py +++ b/exetera/core/persistence.py @@ -680,6 +680,17 @@ def _aggregate_impl(predicate, fkey_indices=None, fkey_index_spans=None, return writer if writer is not None else results +class StorageWrapper: + import io + TYPE = {h5py.Group: 0, io.TextIOWrapper: 1} + + def __init__(self, file): + self.file = file + self.type = self.TYPE(type(file)) + + + + class DataStore: def __init__(self, chunksize=DEFAULT_CHUNKSIZE, diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 03993802..53acb1f5 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -226,7 +226,7 @@ def test_dataframe_ops(self): filter_to_apply = np.array([True, True, False, False, True]) ddf = dst.create_dataframe('dst3') - df.apply_filter(filter_to_apply, ddf) + df.apply_filter(filter_to_apply, ddf=ddf) self.assertEqual([5, 4, 1], ddf['numf'].data[:].tolist()) self.assertEqual([b'e', b'd', b'a'], ddf['fst'].data[:].tolist()) @@ -342,6 +342,7 @@ def test_apply_filter(self): src = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype='int32') filt = np.array([0, 1, 0, 1, 0, 1, 1, 0], dtype='bool') + cfilt = ['num2', 'fixed'] expected = src[filt].tolist() bio = BytesIO() @@ -350,14 +351,32 @@ def test_apply_filter(self): df = dst.create_dataframe('df') numf = s.create_numeric(df, 'numf', 'int32') numf.data.write(src) + numf = s.create_numeric(df, 'num2', 'int32') + numf.data.write(src) + numf = s.create_numeric(df, 'num3', 'int32') + numf.data.write(src) + fixed = s.create_fixed_string(df, 'fixed', 1) + fixed.data.write([b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h']) + + # soft filter + df.apply_filter(filt, hard=False) + df.apply_filter(cfilt, hard=False, axis=1) + self.assertTrue(df['numf'] is None) # get item masked + + # hard filter other df df2 = dst.create_dataframe('df2') - df2b = df.apply_filter(filt, df2) + df2b = df.apply_filter(filt, ddf=df2) self.assertListEqual(expected, df2['numf'].data[:].tolist()) self.assertListEqual(expected, df2b['numf'].data[:].tolist()) self.assertListEqual(src.tolist(), df['numf'].data[:].tolist()) - - df.apply_filter(filt) + # hard filter inline + df.apply_filter(filt, ddf=df) self.assertListEqual(expected, df['numf'].data[:].tolist()) + # hard filter memory + memdf = df.apply_filter(filt, ddf=None) + + + class TestDataFrameMerge(unittest.TestCase): diff --git a/tests/test_fields.py b/tests/test_fields.py index e1493642..3abed12b 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -26,6 +26,21 @@ def test_field_truthness(self): f = s.create_categorical(src, "d", "int8", {"no": 0, "yes": 1}) self.assertTrue(bool(f)) + def test_numeric_field_astype(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, "w", "src") + df = dst.create_dataframe('df') + num = df.create_numeric('num', 'float32') + num.data.write([1.1, 2.1, 3.1, 4.1, 5.1, 6.1]) + self.assertTrue(type(num.data[0]) == np.float32) + num.astype('int8') + self.assertTrue(type(num.data[0]) == np.int8) + num.astype('uint16') + self.assertTrue(type(num.data[0]) == np.uint16) + num.astype(np.float32) + self.assertTrue(type(num.data[0]) == np.float32) + class TestFieldGetSpans(unittest.TestCase): @@ -352,7 +367,7 @@ def test_tuple(expected, actual): 'f3', fields.dtype_to_str(r.data.dtype)).data.write(r) test_simple(expected, df['f3']) - def _execute_uniary_field_test(self, a1, function): + def _execute_unary_field_test(self, a1, function): def test_simple(expected, actual): self.assertListEqual(expected.tolist(), actual.data[:].tolist()) @@ -452,15 +467,18 @@ def test_mixed_field_or(self): self._execute_field_test(a1, a2, 1, lambda x, y: x | y) def test_mixed_field_invert(self): - # invert (~) symbol is used for logical not in field, hence different function called. Thus not using _execute_field_test + a1 = np.array([0, 0, 1, 1], dtype=np.int32) + self._execute_unary_field_test(a1, lambda x: ~x) + + def test_logical_not(self): a1 = np.array([0, 0, 1, 1], dtype=np.int32) bio = BytesIO() with session.Session() as s: ds = s.open_dataset(bio, 'w', 'ds') df = ds.create_dataframe('df') - f1 = df.create_numeric('f1','int32') - f1.data.write(a1) - self.assertListEqual(np.logical_not(a1).tolist(), (~f1).data[:].tolist()) + num = df.create_numeric('num', 'uint32') + num.data.write(a1) + self.assertListEqual(np.logical_not(a1).tolist(), num.logical_not().data[:].tolist()) def test_less_than(self): From 08751499315848668f010f6952b51f63769587a1 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 10 May 2021 17:10:03 +0100 Subject: [PATCH 106/181] remove dataframe view commits --- exetera/core/dataframe.py | 104 +++++++++--------------------------- exetera/core/persistence.py | 11 ---- 2 files changed, 26 insertions(+), 89 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index e9a247c4..99cc3ff1 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -62,9 +62,6 @@ def __init__(self, for subg in h5group.keys(): self._columns[subg] = dataset.session.get(h5group[subg]) - self._index_filter = None - self._column_filter = None - @property def columns(self): """ @@ -87,14 +84,6 @@ def h5group(self): """ return self._h5group - @property - def column_filter(self): - return self._column_filter - - @property - def index_filter(self): - return self._index_filter - def add(self, field: fld.Field): """ @@ -223,8 +212,7 @@ def __contains__(self, name): if not isinstance(name, str): raise TypeError("The name must be a str object.") else: - return name in self._columns if self._column_filter is None \ - else (name in self._columns and name in self._column_filter) + return name in self._columns def contains_field(self, field): """ @@ -240,13 +228,6 @@ def contains_field(self, field): return True return False - def get_field(self, name): - """ - Get a field stored by the field name. - :param name: The name of field to get. - """ - return self.__getitem__(name) - def __getitem__(self, name): """ Get a field stored by the field name. @@ -258,45 +239,28 @@ def __getitem__(self, name): elif not self.__contains__(name): raise ValueError("There is no field named '{}' in this dataframe".format(name)) else: - if self._column_filter is None: - if self.index_filter is None: - return self._columns[name] - else: - return self._columns[name].data[:][self.index_filter] - elif name not in self.column_filter: - raise ValueError("The column to fetch is filtered.") + return self._columns[name] - def get_data(self, name=None): + def get_field(self, name): """ Get a field stored by the field name. :param name: The name of field to get. """ - if name is not None: - return self.__getitem__(name) - - if self._column_filter is not None: - pass - else: - for column in self._column_filter: - return self.__getitem__(name) - + return self.__getitem__(name) def __setitem__(self, name, field): if not isinstance(name, str): raise TypeError("The name must be of type str but is of type '{}'".format(str)) - if isinstance(field, fld.Field): - nfield = field.create_like(self, name) - if field.indexed: - nfield.indices.write(field.indices[:]) - nfield.values.write(field.values[:]) - else: - nfield.data.write(field.data[:]) - self._columns[name] = nfield - elif isinstance(field, list): # TODO how to handle value assignment w/ filter? - pass + if not isinstance(field, fld.Field): + raise TypeError("The field must be a Field object.") + nfield = field.create_like(self, name) + if field.indexed: + nfield.indices.write(field.indices[:]) + nfield.values.write(field.values[:]) else: - raise TypeError("The field must be a Field or list.") + nfield.data.write(field.data[:]) + self._columns[name] = nfield def __delitem__(self, name): if not self.__contains__(name=name): @@ -429,42 +393,26 @@ def get_unique_name(name, keys): self._columns = final_columns - def apply_filter(self, filter_to_apply, ddf=None, hard=True, axis=0): + + def apply_filter(self, filter_to_apply, ddf=None): """ Apply the filter to all the fields in this dataframe, return a dataframe with filtered fields. :param filter_to_apply: the filter to be applied to the source field, an array of boolean - :param axis: {0 or ‘index’, 1 or ‘columns’, None}, default 0 - :param hard: if perform the filtering when calling and write the result now :param ddf: optional- the destination data frame :returns: a dataframe contains all the fields filterd, self if ddf is not set """ - if not isinstance(filter_to_apply, np.ndarray) and not isinstance(filter_to_apply, list): - raise TypeError("The filter must be a Numpy array or Python list.") - - if hard is False: # soft filter - if axis == 0 or axis == 'index': - self.index_filter = filter_to_apply - elif axis == 1 or axis == 'columns': - self.column_filter = filter_to_apply - else: # hard filter - if ddf is not None and ddf is not self: # filter to another df - if not isinstance(ddf, DataFrame): - raise TypeError("The destination object must be an instance of DataFrame.") - for name, field in self._columns.items(): - newfld = field.create_like(ddf, name) - field.apply_filter(filter_to_apply, target=newfld) - return ddf - elif ddf is not None and ddf is self: # filter inline - for field in self._columns.values(): - field.apply_filter(filter_to_apply, in_place=True) - return self - elif ddf is None: # return memory based df - pass - - def clean_filters(self): - self._index_filter = None - self._column_filter = None + if ddf is not None: + if not isinstance(ddf, DataFrame): + raise TypeError("The destination object must be an instance of DataFrame.") + for name, field in self._columns.items(): + newfld = field.create_like(ddf, name) + field.apply_filter(filter_to_apply, target=newfld) + return ddf + else: + for field in self._columns.values(): + field.apply_filter(filter_to_apply, in_place=True) + return self def apply_index(self, index_to_apply, ddf=None): """ @@ -676,4 +624,4 @@ def merge(left: DataFrame, d.data.write(v) if np.all(r_to_d_filt) == False: d = dest.create_numeric('valid'+right_suffix, 'bool') - d.data.write(r_to_d_filt) + d.data.write(r_to_d_filt) \ No newline at end of file diff --git a/exetera/core/persistence.py b/exetera/core/persistence.py index f26e2697..314aaf15 100644 --- a/exetera/core/persistence.py +++ b/exetera/core/persistence.py @@ -680,17 +680,6 @@ def _aggregate_impl(predicate, fkey_indices=None, fkey_index_spans=None, return writer if writer is not None else results -class StorageWrapper: - import io - TYPE = {h5py.Group: 0, io.TextIOWrapper: 1} - - def __init__(self, file): - self.file = file - self.type = self.TYPE(type(file)) - - - - class DataStore: def __init__(self, chunksize=DEFAULT_CHUNKSIZE, From c335831b16c4d0fb3bdf029eedf29ab57a241b11 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 11 May 2021 09:21:44 +0100 Subject: [PATCH 107/181] remove kwargs in get_spans in session, add fields back for backward compatibility --- exetera/core/abstract_types.py | 2 +- exetera/core/session.py | 21 ++++----------------- tests/test_dataframe.py | 4 ++-- tests/test_fields.py | 15 --------------- 4 files changed, 7 insertions(+), 35 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index 9e3e3dfd..06f03475 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -318,7 +318,7 @@ def distinct(self, field=None, fields=None, filter=None): raise NotImplementedError() @abstractmethod - def get_spans(self, field=None, fields=None): + def get_spans(self, field=None, fields=None, dest=None): raise NotImplementedError() @abstractmethod diff --git a/exetera/core/session.py b/exetera/core/session.py index f95086f6..8d626232 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -345,7 +345,8 @@ def distinct(self, field=None, fields=None, filter=None): return results def get_spans(self, field: Union[Field, np.array] = None, - dest: Field = None, **kwargs): + fields: (Union[Field, np.array], Union[Field, np.array]) = None, + dest: Field = None): """ Calculate a set of spans that indicate contiguous equal values. The entries in the result array correspond to the inclusive start and @@ -362,26 +363,12 @@ def get_spans(self, field: Union[Field, np.array] = None, result: [0, 1, 3, 6, 7, 10, 15] :param field: A Field or numpy array to be evaluated for spans - :param dest: A destination Field to store the result - :param **kwargs: See below. For parameters set in both argument and kwargs, use kwargs - - :Keyword Arguments: - * field -- Similar to field parameter, in case user specify field as keyword - * fields -- A tuple of Fields or tuple of numpy arrays to be evaluated for spans - * dest -- Similar to dest parameter, in case user specify as keyword + :param fields: A tuple of Fields or tuple of numpy arrays to be evaluated for spans + :param dest: Similar to dest parameter, in case user specify as keyword :return: The resulting set of spans as a numpy array """ - fields = [] result = None - if len(kwargs) > 0: - for k in kwargs.keys(): - if k == 'field': - field = kwargs[k] - elif k == 'fields': - fields = kwargs[k] - elif k == 'dest': - dest = kwargs[k] if dest is not None and not isinstance(dest, Field): raise TypeError(f"'dest' must be one of 'Field' but is {type(dest)}") diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 53acb1f5..6f02fc3d 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -359,8 +359,8 @@ def test_apply_filter(self): fixed.data.write([b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h']) # soft filter - df.apply_filter(filt, hard=False) - df.apply_filter(cfilt, hard=False, axis=1) + df.apply_filter(filt) + df.apply_filter(cfilt) self.assertTrue(df['numf'] is None) # get item masked # hard filter other df diff --git a/tests/test_fields.py b/tests/test_fields.py index 3abed12b..4a7ae468 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -271,21 +271,6 @@ def test_numeric_create_like(self): foo2.data.write(mfoo) self.assertListEqual([2, 3, 4, 5], foo2.data[:].tolist()) - def test_numeric_as_type(self): - bio = BytesIO() - with session.Session() as s: - ds = s.open_dataset(bio, 'w', 'ds') - df = ds.create_dataframe('df') - foo = df.create_numeric('foo', 'int32') - foo.data.write(np.array([65, 66, 67, 68, 97, 98, 99, 100])) - newf = foo.astype('fixedstring', length=2) - self.assertListEqual(['AB', 'CD', 'ab', 'cd'], newf.data[:].tolist()) - - foo.data.clear() - foo.data.write([0, 0, 1, 1, 1, 2, 2, 2]) - newf = foo.astype('categorical', key={'foo': 0, 'bar': 1, 'boo': 2}) - self.assertListEqual([0, 0, 1, 1, 1, 2, 2, 2], newf.data[:].tolist()) - class TestMemoryFields(unittest.TestCase): From ea20c6042600f403259357c4fe28d7c1d499f067 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 11 May 2021 09:42:34 +0100 Subject: [PATCH 108/181] remove filter view tests --- tests/test_dataframe.py | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 6f02fc3d..9afba229 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -339,10 +339,8 @@ def test_move_different_dataframe(self): class TestDataFrameApplyFilter(unittest.TestCase): def test_apply_filter(self): - src = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype='int32') filt = np.array([0, 1, 0, 1, 0, 1, 1, 0], dtype='bool') - cfilt = ['num2', 'fixed'] expected = src[filt].tolist() bio = BytesIO() @@ -351,32 +349,14 @@ def test_apply_filter(self): df = dst.create_dataframe('df') numf = s.create_numeric(df, 'numf', 'int32') numf.data.write(src) - numf = s.create_numeric(df, 'num2', 'int32') - numf.data.write(src) - numf = s.create_numeric(df, 'num3', 'int32') - numf.data.write(src) - fixed = s.create_fixed_string(df, 'fixed', 1) - fixed.data.write([b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h']) - - # soft filter - df.apply_filter(filt) - df.apply_filter(cfilt) - self.assertTrue(df['numf'] is None) # get item masked - - # hard filter other df df2 = dst.create_dataframe('df2') - df2b = df.apply_filter(filt, ddf=df2) + df2b = df.apply_filter(filt, df2) self.assertListEqual(expected, df2['numf'].data[:].tolist()) self.assertListEqual(expected, df2b['numf'].data[:].tolist()) self.assertListEqual(src.tolist(), df['numf'].data[:].tolist()) - # hard filter inline - df.apply_filter(filt, ddf=df) - self.assertListEqual(expected, df['numf'].data[:].tolist()) - # hard filter memory - memdf = df.apply_filter(filt, ddf=None) - - + df.apply_filter(filt) + self.assertListEqual(expected, df['numf'].data[:].tolist()) class TestDataFrameMerge(unittest.TestCase): From 611601ac8eab3f0c1d0b6d3fbe0096f53f38e16b Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 10 Jun 2021 18:32:04 +0100 Subject: [PATCH 109/181] partial commit on viewer --- exetera/core/viewer.py | 105 +++++++++++++++++++++++++++++++++++++++++ tests/test_viewer.py | 46 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 exetera/core/viewer.py create mode 100644 tests/test_viewer.py diff --git a/exetera/core/viewer.py b/exetera/core/viewer.py new file mode 100644 index 00000000..ff6f37dc --- /dev/null +++ b/exetera/core/viewer.py @@ -0,0 +1,105 @@ +import numpy as np + +from exetera.core.dataframe import HDF5DataFrame +from exetera.core.fields import MemoryField + + +class ViewerMask: + """ + Fetch dataframe through the viewer mask will only allow rows/columns listed in the mask. + """ + def __init__(self, indexlist, columnlist): + """ + Initialise a mask. + + :param indexlist: An ndarray of integers, indicating the index of elements to show. + :param columnlist: An ndarray of strings, indicating the name of column to show. + """ + self._index = indexlist + self._column = columnlist + + @property + def index(self): + return self._index + + @property + def column(self): + return self._column + + def __and__(self, other): + """ + Create a new mask by the intersected elements in both masks. + + Example:: + + self: index [1, 2, 3, 4], column ['a', 'b'] + other: index [1, 2, 5, 6], column ['a', 'c'] + return: index [1, 2], column ['a'] + + :param other: Another mask. + :return: A new mask with intersected elements in both masks. + """ + index = np.intersect1d(self.index, other.index) + column = np.intersect1d(self.column, other.column) + return ViewerMask(index, column) + + def __or__(self, other): + """ + Create a new mask by the union elements in both masks. + + Example:: + + self: index [1, 2, 3, 4], column ['a', 'b'] + other: index [1, 2, 5, 6], column ['a', 'c'] + return: index [1, 2, 3, 4, 5, 6], column ['a', 'b', 'c'] + + :param other: Another mask. + :return: A new mask with elements in both masks. + """ + index = np.union1d(self.index, other.index) + column = np.union1d(self.column, other.column) + return ViewerMask(index, column) + + +class Viewer: + """ + A viewer is a projected filter of a dataframe + """ + def __init__(self, df, mask=None): + if isinstance(df, HDF5DataFrame): + self.df = df + self.storage = df.h5group + if mask is not None and isinstance(mask, ViewerMask): + self._mask = mask + else: + self._mask = None + + @property + def mask(self): + return self._mask + + @mask.setter + def mask(self, msk): + if isinstance(msk, ViewerMask): + self._mask = msk + + def __getitem__(self, item): # aka apply filter? + if isinstance(item, str): # df.loc['cobra'] filter on fields, return a field + if item not in self._mask.column: + raise ValueError("{} is not listed in the ViewerMask.".format(item)) + else: + return self.df[item].data[self._mask.index] # return data instread of field + elif isinstance(item, slice): + print("slice") + elif isinstance(item, list): # df.loc[[True, True, False]] filter on index, return a df + pass + elif isinstance(item, MemoryField): # df.loc[df['shield'] > 35] filter on index, return a df + pass + elif isinstance(item, tuple): # df.loc[_index , _column] + if isinstance(item[0], slice): # df.loc[:, ] = 30 filter + pass + elif isinstance(item[0], str): # df.loc['abc',] + pass + + def __setitem__(self, key, value): + raise NotImplementedError("Please update field values though dataframe instead of viewer.") diff --git a/tests/test_viewer.py b/tests/test_viewer.py new file mode 100644 index 00000000..bafe55af --- /dev/null +++ b/tests/test_viewer.py @@ -0,0 +1,46 @@ +import unittest +from io import BytesIO + +import numpy as np + +from exetera.core.session import Session +from exetera.core.viewer import Viewer, ViewerMask + + +class TestUtils(unittest.TestCase): + def test_viewer(self): + bio = BytesIO() + with Session() as s: + src = s.open_dataset(bio, 'r+', 'src') + df = src.create_dataframe('df') + num = df.create_numeric('num', 'uint32') + num.data.write([1, 2, 3, 4, 5, 6, 7]) + + view = Viewer(df) + mask = ViewerMask(np.where(num.data[:] > 3), np.array(['num'])) + view.mask = mask + view['num'] == 7 + print(view['num']) + print(view[:]) + + + def test_mask(self): + idxlist = np.array([1, 3, 5, 7]) + clmlist = np.array(['a', 'b', 'c', 'd']) + mask = ViewerMask(idxlist, clmlist) + + idx2 = np.array([1, 3, 6, 8]) + clm2 = np.array(['c', 'd', 'e', 'f']) + msk2 = ViewerMask(idx2, clm2) + + m1 = mask & msk2 + self.assertEqual(m1.index.tolist(), [1, 3]) + self.assertEqual(m1.column.tolist(), ['c', 'd']) + + m2 = mask | msk2 + self.assertEqual(m2.index.tolist(), [1, 3, 5, 6, 7, 8]) + self.assertEqual(m2.column.tolist(), ['a', 'b', 'c', 'd', 'e', 'f']) + + m2 = m2 & ViewerMask(m2.index, np.array(['a', 'b'])) + self.assertEqual(m2.index.tolist(), [1, 3, 5, 6, 7, 8]) + self.assertEqual(m2.column.tolist(), ['a', 'b']) From fbe396fe4ca2036c1cf31ffb5bc409d49737f2d3 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 20 Sep 2021 12:02:16 +0100 Subject: [PATCH 110/181] remote view from git --- exetera/core/viewer.py | 105 ----------------------------------------- tests/test_viewer.py | 46 ------------------ 2 files changed, 151 deletions(-) delete mode 100644 exetera/core/viewer.py delete mode 100644 tests/test_viewer.py diff --git a/exetera/core/viewer.py b/exetera/core/viewer.py deleted file mode 100644 index ff6f37dc..00000000 --- a/exetera/core/viewer.py +++ /dev/null @@ -1,105 +0,0 @@ -import numpy as np - -from exetera.core.dataframe import HDF5DataFrame -from exetera.core.fields import MemoryField - - -class ViewerMask: - """ - Fetch dataframe through the viewer mask will only allow rows/columns listed in the mask. - """ - def __init__(self, indexlist, columnlist): - """ - Initialise a mask. - - :param indexlist: An ndarray of integers, indicating the index of elements to show. - :param columnlist: An ndarray of strings, indicating the name of column to show. - """ - self._index = indexlist - self._column = columnlist - - @property - def index(self): - return self._index - - @property - def column(self): - return self._column - - def __and__(self, other): - """ - Create a new mask by the intersected elements in both masks. - - Example:: - - self: index [1, 2, 3, 4], column ['a', 'b'] - other: index [1, 2, 5, 6], column ['a', 'c'] - return: index [1, 2], column ['a'] - - :param other: Another mask. - :return: A new mask with intersected elements in both masks. - """ - index = np.intersect1d(self.index, other.index) - column = np.intersect1d(self.column, other.column) - return ViewerMask(index, column) - - def __or__(self, other): - """ - Create a new mask by the union elements in both masks. - - Example:: - - self: index [1, 2, 3, 4], column ['a', 'b'] - other: index [1, 2, 5, 6], column ['a', 'c'] - return: index [1, 2, 3, 4, 5, 6], column ['a', 'b', 'c'] - - :param other: Another mask. - :return: A new mask with elements in both masks. - """ - index = np.union1d(self.index, other.index) - column = np.union1d(self.column, other.column) - return ViewerMask(index, column) - - -class Viewer: - """ - A viewer is a projected filter of a dataframe - """ - def __init__(self, df, mask=None): - if isinstance(df, HDF5DataFrame): - self.df = df - self.storage = df.h5group - if mask is not None and isinstance(mask, ViewerMask): - self._mask = mask - else: - self._mask = None - - @property - def mask(self): - return self._mask - - @mask.setter - def mask(self, msk): - if isinstance(msk, ViewerMask): - self._mask = msk - - def __getitem__(self, item): # aka apply filter? - if isinstance(item, str): # df.loc['cobra'] filter on fields, return a field - if item not in self._mask.column: - raise ValueError("{} is not listed in the ViewerMask.".format(item)) - else: - return self.df[item].data[self._mask.index] # return data instread of field - elif isinstance(item, slice): - print("slice") - elif isinstance(item, list): # df.loc[[True, True, False]] filter on index, return a df - pass - elif isinstance(item, MemoryField): # df.loc[df['shield'] > 35] filter on index, return a df - pass - elif isinstance(item, tuple): # df.loc[_index , _column] - if isinstance(item[0], slice): # df.loc[:, ] = 30 filter - pass - elif isinstance(item[0], str): # df.loc['abc',] - pass - - def __setitem__(self, key, value): - raise NotImplementedError("Please update field values though dataframe instead of viewer.") diff --git a/tests/test_viewer.py b/tests/test_viewer.py deleted file mode 100644 index bafe55af..00000000 --- a/tests/test_viewer.py +++ /dev/null @@ -1,46 +0,0 @@ -import unittest -from io import BytesIO - -import numpy as np - -from exetera.core.session import Session -from exetera.core.viewer import Viewer, ViewerMask - - -class TestUtils(unittest.TestCase): - def test_viewer(self): - bio = BytesIO() - with Session() as s: - src = s.open_dataset(bio, 'r+', 'src') - df = src.create_dataframe('df') - num = df.create_numeric('num', 'uint32') - num.data.write([1, 2, 3, 4, 5, 6, 7]) - - view = Viewer(df) - mask = ViewerMask(np.where(num.data[:] > 3), np.array(['num'])) - view.mask = mask - view['num'] == 7 - print(view['num']) - print(view[:]) - - - def test_mask(self): - idxlist = np.array([1, 3, 5, 7]) - clmlist = np.array(['a', 'b', 'c', 'd']) - mask = ViewerMask(idxlist, clmlist) - - idx2 = np.array([1, 3, 6, 8]) - clm2 = np.array(['c', 'd', 'e', 'f']) - msk2 = ViewerMask(idx2, clm2) - - m1 = mask & msk2 - self.assertEqual(m1.index.tolist(), [1, 3]) - self.assertEqual(m1.column.tolist(), ['c', 'd']) - - m2 = mask | msk2 - self.assertEqual(m2.index.tolist(), [1, 3, 5, 6, 7, 8]) - self.assertEqual(m2.column.tolist(), ['a', 'b', 'c', 'd', 'e', 'f']) - - m2 = m2 & ViewerMask(m2.index, np.array(['a', 'b'])) - self.assertEqual(m2.index.tolist(), [1, 3, 5, 6, 7, 8]) - self.assertEqual(m2.column.tolist(), ['a', 'b']) From c2c7185c561c610555de5c7651112232269b22ca Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 22 Sep 2021 10:02:32 +0100 Subject: [PATCH 111/181] add df.describe unittest --- exetera/core/dataframe.py | 161 +++++++++++++++++++++++++ tests/test_dataframe.py | 239 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 400 insertions(+) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 97f31e01..2416d0a0 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -564,6 +564,167 @@ def groupby(self, by: Union[str, List[str]], hint_keys_is_sorted=False): return HDF5DataFrameGroupBy(self._columns, by, sorted_index, spans) + def describe(self, include=None, exclude=None): + """ + Show the basic statistics of the data in each field. + + :param include: The field name or data type or simply 'all' to indicate the fields included in the calculation. + :param exclude: The filed name or data type to exclude in the calculation. + :return: A dataframe contains the statistic results. + + """ + # check include and exclude conflicts + if include is not None and exclude is not None: + if isinstance(include, str): + raise ValueError('Please do not use exclude parameter when include is set as a single field.') + elif isinstance(include, type): + if isinstance(exclude, type) or (isinstance(exclude, list) and isinstance(exclude[0], type)): + raise ValueError('Please do not use set exclude as a type when include is set as a single data type.') + elif isinstance(include, list): + if isinstance(include[0], str) and isinstance(exclude, str): + raise ValueError('Please do not use exclude as the same type as the include parameter.') + elif isinstance(include[0], str) and isinstance(exclude, list) and isinstance(exclude[0], str): + raise ValueError('Please do not use exclude as the same type as the include parameter.') + elif isinstance(include[0], type) and isinstance(exclude, type): + raise ValueError('Please do not use exclude as the same type as the include parameter.') + elif isinstance(include[0], type) and isinstance(exclude, list) and isinstance(exclude[0], type): + raise ValueError('Please do not use exclude as the same type as the include parameter.') + + fields_to_calculate = [] + if include is not None: + if isinstance(include, str): # a single str + if include == 'all': + fields_to_calculate = list(self.columns.keys()) + elif include in self.columns.keys(): + fields_to_calculate = [include] + else: + raise ValueError('The field to include in not in the dataframe.') + elif isinstance(include, type): # a single type + for f in self.columns: + if not self[f].indexed and np.issubdtype(self[f].data.dtype, include): + fields_to_calculate.append(f) + if len(fields_to_calculate) == 0: + raise ValueError('No such type appeared in the dataframe.') + elif isinstance(include, list) and isinstance(include[0], str): # a list of str + for f in include: + if f in self.columns.keys(): + fields_to_calculate.append(f) + if len(fields_to_calculate) == 0: + raise ValueError('The fields to include in not in the dataframe.') + + elif isinstance(include, list) and isinstance(include[0], type): # a list of type + for t in include: + for f in self.columns: + if not self[f].indexed and np.issubdtype(self[f].data.dtype, t): + fields_to_calculate.append(f) + if len(fields_to_calculate) == 0: + raise ValueError('No such type appeared in the dataframe.') + + else: + raise ValueError('The include parameter can only be str, dtype, or list of either.') + + else: # include is None, numeric & timestamp fields only (no indexed strings) TODO confirm the type + for f in self.columns: + if isinstance(self[f], fld.NumericField) or isinstance(self[f], fld.TimestampField): + fields_to_calculate.append(f) + + if len(fields_to_calculate) == 0: + raise ValueError('No fields included to describe.') + + if exclude is not None: + if isinstance(exclude, str): + if exclude in fields_to_calculate: # exclude + fields_to_calculate.remove(exclude) # remove from list + elif isinstance(exclude, type): # a type + for f in fields_to_calculate: + if np.issubdtype(self[f].data.dtype, exclude): + fields_to_calculate.remove(f) + elif isinstance(exclude, list) and isinstance(exclude[0], str): # a list of str + for f in exclude: + fields_to_calculate.remove(f) + + elif isinstance(exclude, list) and isinstance(exclude[0], type): # a list of type + for t in exclude: + for f in fields_to_calculate: + if np.issubdtype(self[f].data.dtype, t): + fields_to_calculate.remove(f) # remove will raise valueerror if dtype not presented + + else: + raise ValueError('The exclude parameter can only be str, dtype, or list of either.') + + if len(fields_to_calculate) == 0: + raise ValueError('All fields are excluded, no field left to describe.') + # if flexible (str) fields + des_idxstr = False + for f in fields_to_calculate: + if isinstance(self[f], fld.CategoricalField) or isinstance(self[f], fld.FixedStringField) or isinstance( + self[f], fld.IndexedStringField): + des_idxstr = True + # calculation + result = {'fields': [], 'count': [], 'mean': [], 'std': [], 'min': [], '25%': [], '50%': [], '75%': [], + 'max': []} + + # count + if des_idxstr: + result['unique'], result['top'], result['freq'] = [], [], [] + + for f in fields_to_calculate: + result['fields'].append(f) + result['count'].append(len(self[f].data)) + + if des_idxstr and (isinstance(self[f], fld.NumericField) or isinstance(self[f], + fld.TimestampField)): # numberic, timestamp + result['unique'].append('NaN') + result['top'].append('NaN') + result['freq'].append('NaN') + + result['mean'].append("{:.2f}".format(np.mean(self[f].data[:]))) + result['std'].append("{:.2f}".format(np.std(self[f].data[:]))) + result['min'].append("{:.2f}".format(np.min(self[f].data[:]))) + result['25%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.25))) + result['50%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.5))) + result['75%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.75))) + result['max'].append("{:.2f}".format(np.max(self[f].data[:]))) + + elif des_idxstr and (isinstance(self[f], fld.CategoricalField) or isinstance(self[f], + fld.IndexedStringField) or isinstance( + self[f], fld.FixedStringField)): # categorical & indexed string & fixed string + a, b = np.unique(self[f].data[:], return_counts=True) + result['unique'].append(len(a)) + result['top'].append(a[np.argmax(b)]) + result['freq'].append(b[np.argmax(b)]) + + result['mean'].append('NaN') + result['std'].append('NaN') + result['min'].append('NaN') + result['25%'].append('NaN') + result['50%'].append('NaN') + result['75%'].append('NaN') + result['max'].append('NaN') + + elif not des_idxstr: + result['mean'].append("{:.2f}".format(np.mean(self[f].data[:]))) + result['std'].append("{:.2f}".format(np.std(self[f].data[:]))) + result['min'].append("{:.2f}".format(np.min(self[f].data[:]))) + result['25%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.25))) + result['50%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.5))) + result['75%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.75))) + result['max'].append("{:.2f}".format(np.max(self[f].data[:]))) + + # display + columns_to_show = ['fields', 'count', 'unique', 'top', 'freq', 'mean', 'std', 'min', '25%', '50%', '75%', 'max'] + # 5 fields each time for display + for col in range(0, len(result['fields']), 5): # 5 column each time + for i in columns_to_show: + if i in result: + print(i, end='\t') + for f in result[i][col:col + 5 if col + 5 < len(result[i]) - 1 else len(result[i])]: + print('{:>15}'.format(f), end='\t') + print('') + print('\n') + + return result + class HDF5DataFrameGroupBy(DataFrameGroupBy): diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index a31fc80c..9e928b6b 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -4,6 +4,7 @@ import numpy as np import tempfile import os +from datetime import datetime from exetera.core import session from exetera.core import fields @@ -861,3 +862,241 @@ def test_to_csv_with_row_filter_field(self): self.assertEqual(f.readlines(), ['val1\n', '0\n', '2\n']) os.close(fd_csv) + + +class TestDataFrameDescribe(unittest.TestCase): + + def test_describe_default(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + df = dst.create_dataframe('df') + df.create_numeric('num', 'int32').data.write([i for i in range(10)]) + df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) + df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) + df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) + df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) + result = df.describe() + expected = {'fields': ['num', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], + 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], + '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], + 'max': ['9.00', '1632234147.00']} + self.assertEqual(result, expected) + + def test_describe_include(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + df = dst.create_dataframe('df') + df.create_numeric('num', 'int32').data.write([i for i in range(10)]) + df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) + df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) + df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) + df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) + + result = df.describe(include='all') + expected = {'fields': ['num', 'fs1', 'ts1', 'c1', 'is1'], 'count': [10, 20, 20, 20, 20], + 'mean': ['4.50', 'NaN', '1632234137.50', 'NaN', 'NaN'], 'std': ['2.87', 'NaN', '5.77', 'NaN', 'NaN'], + 'min': ['0.00', 'NaN', '1632234128.00', 'NaN', 'NaN'], '25%': ['0.02', 'NaN', '1632234128.05', 'NaN', 'NaN'], + '50%': ['0.04', 'NaN', '1632234128.10', 'NaN', 'NaN'], '75%': ['0.07', 'NaN', '1632234128.14', 'NaN', 'NaN'], + 'max': ['9.00', 'NaN', '1632234147.00', 'NaN', 'NaN'], 'unique': ['NaN', 1, 'NaN', 1, 1], + 'top': ['NaN', b'a', 'NaN', 1, 'abc'], 'freq': ['NaN', 20, 'NaN', 20, 20]} + self.assertEqual(result, expected) + + result = df.describe(include='num') + expected = {'fields': ['num'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], + '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} + self.assertEqual(result, expected) + + result = df.describe(include=['num', 'fs1']) + expected = {'fields': ['num', 'fs1'], 'count': [10, 20], 'mean': ['4.50', 'NaN'], 'std': ['2.87', 'NaN'], + 'min': ['0.00', 'NaN'], '25%': ['0.02', 'NaN'], '50%': ['0.04', 'NaN'], '75%': ['0.07', 'NaN'], + 'max': ['9.00', 'NaN'], 'unique': ['NaN', 1], 'top': ['NaN', b'a'], 'freq': ['NaN', 20]} + self.assertEqual(result, expected) + + result = df.describe(include=np.int32) + expected = {'fields': ['num', 'c1'], 'count': [10, 20], 'mean': ['4.50', 'NaN'], 'std': ['2.87', 'NaN'], + 'min': ['0.00', 'NaN'], '25%': ['0.02', 'NaN'], '50%': ['0.04', 'NaN'], '75%': ['0.07', 'NaN'], + 'max': ['9.00', 'NaN'], 'unique': ['NaN', 1], 'top': ['NaN', 1], 'freq': ['NaN', 20]} + self.assertEqual(result, expected) + + result = df.describe(include=[np.int32, np.bytes_]) + expected = {'fields': ['num', 'c1', 'fs1'], 'count': [10, 20, 20], 'mean': ['4.50', 'NaN', 'NaN'], + 'std': ['2.87', 'NaN', 'NaN'], 'min': ['0.00', 'NaN', 'NaN'], '25%': ['0.02', 'NaN', 'NaN'], + '50%': ['0.04', 'NaN', 'NaN'], '75%': ['0.07', 'NaN', 'NaN'], 'max': ['9.00', 'NaN', 'NaN'], + 'unique': ['NaN', 1, 1], 'top': ['NaN', 1, b'a'], 'freq': ['NaN', 20, 20]} + self.assertEqual(result, expected) + + + def test_describe_exclude(self): + bio = BytesIO() + with session.Session() as s: + src = s.open_dataset(bio, 'w', 'src') + df = src.create_dataframe('df') + df.create_numeric('num', 'int32').data.write([i for i in range(10)]) + df.create_numeric('num2', 'int64').data.write([i for i in range(10)]) + df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) + df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) + df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) + df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) + + result = df.describe(exclude='num') + expected = {'fields': ['num2', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], + 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], + '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], + 'max': ['9.00', '1632234147.00']} + self.assertEqual(result, expected) + + result = df.describe(exclude=['num', 'num2']) + expected = {'fields': ['ts1'], 'count': [20], 'mean': ['1632234137.50'], 'std': ['5.77'], + 'min': ['1632234128.00'], '25%': ['1632234128.05'], '50%': ['1632234128.10'], + '75%': ['1632234128.14'], 'max': ['1632234147.00']} + self.assertEqual(result, expected) + + result = df.describe(exclude=np.int32) + expected = {'fields': ['num2', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], + 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], + '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], + 'max': ['9.00', '1632234147.00']} + self.assertEqual(result, expected) + + result = df.describe(exclude=[np.int32, np.float64]) + expected = {'fields': ['num2'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], + '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} + self.assertEqual(result, expected) + + def test_describe_include_and_exclude(self): + bio = BytesIO() + with session.Session() as s: + src = s.open_dataset(bio, 'w', 'src') + df = src.create_dataframe('df') + df.create_numeric('num', 'int32').data.write([i for i in range(10)]) + df.create_numeric('num2', 'int64').data.write([i for i in range(10)]) + df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) + df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) + df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) + df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) + + #str * + with self.assertRaises(Exception) as context: + df.describe(include='num', exclude='num') + self.assertTrue(isinstance(context.exception, ValueError)) + + # list of str , str + with self.assertRaises(Exception) as context: + df.describe(include=['num', 'num2'], exclude='num') + self.assertTrue(isinstance(context.exception, ValueError)) + # list of str , type + result = df.describe(include=['num', 'num2'], exclude=np.int32) + expected = {'fields': ['num2'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], + '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} + self.assertEqual(result, expected) + # list of str , list of str + with self.assertRaises(Exception) as context: + df.describe(include=['num', 'num2'], exclude=['num', 'num2']) + self.assertTrue(isinstance(context.exception, ValueError)) + # list of str , list of type + result = df.describe(include=['num', 'num2', 'ts1'], exclude=[np.int32, np.int64]) + expected = {'fields': ['ts1'], 'count': [20], 'mean': ['1632234137.50'], 'std': ['5.77'], + 'min': ['1632234128.00'], '25%': ['1632234128.05'], '50%': ['1632234128.10'], + '75%': ['1632234128.14'], 'max': ['1632234147.00']} + self.assertEqual(result, expected) + + # type, str + result = df.describe(include=np.number, exclude='num2') + expected = {'fields': ['num', 'ts1', 'c1'], 'count': [10, 20, 20], 'mean': ['4.50', '1632234137.50', 'NaN'], + 'std': ['2.87', '5.77', 'NaN'], 'min': ['0.00', '1632234128.00', 'NaN'], + '25%': ['0.02', '1632234128.05', 'NaN'], '50%': ['0.04', '1632234128.10', 'NaN'], + '75%': ['0.07', '1632234128.14', 'NaN'], 'max': ['9.00', '1632234147.00', 'NaN'], + 'unique': ['NaN', 'NaN', 1], 'top': ['NaN', 'NaN', 1], 'freq': ['NaN', 'NaN', 20]} + self.assertEqual(result, expected) + # type, type + with self.assertRaises(Exception) as context: + df.describe(include=np.int32, exclude=np.int64) + self.assertTrue(isinstance(context.exception, ValueError)) + # type, list of str + result = df.describe(include=np.number, exclude=['num', 'num2']) + expected = {'fields': ['ts1', 'c1'], 'count': [20, 20], 'mean': ['1632234137.50', 'NaN'], + 'std': ['5.77', 'NaN'], 'min': ['1632234128.00', 'NaN'], '25%': ['1632234128.05', 'NaN'], + '50%': ['1632234128.10', 'NaN'], '75%': ['1632234128.14', 'NaN'], 'max': ['1632234147.00', 'NaN'], + 'unique': ['NaN', 1], 'top': ['NaN', 1], 'freq': ['NaN', 20]} + self.assertEqual(result, expected) + # type, list of type + with self.assertRaises(Exception) as context: + df.describe(include=np.int32, exclude=[np.int64, np.float64]) + self.assertTrue(isinstance(context.exception, ValueError)) + + # list of type, str + result = df.describe(include=[np.int32, np.int64], exclude='num') + expected = {'fields': ['c1', 'num2'], 'count': [20, 10], 'mean': ['NaN', '4.50'], 'std': ['NaN', '2.87'], + 'min': ['NaN', '0.00'], '25%': ['NaN', '0.02'], '50%': ['NaN', '0.04'], '75%': ['NaN', '0.07'], + 'max': ['NaN', '9.00'], 'unique': [1, 'NaN'], 'top': [1, 'NaN'], 'freq': [20, 'NaN']} + self.assertEqual(result, expected) + # list of type, type + with self.assertRaises(Exception) as context: + df.describe(include=[np.int32, np.int64], exclude=np.int64) + self.assertTrue(isinstance(context.exception, ValueError)) + # list of type, list of str + result = df.describe(include=[np.int32, np.int64], exclude=['num', 'num2']) + expected = {'fields': ['c1'], 'count': [20], 'mean': ['NaN'], 'std': ['NaN'], 'min': ['NaN'], + '25%': ['NaN'], '50%': ['NaN'], '75%': ['NaN'], 'max': ['NaN'], 'unique': [1], 'top': [1], + 'freq': [20]} + self.assertEqual(result, expected) + # list of type, list of type + with self.assertRaises(Exception) as context: + df.describe(include=[np.int32, np.int64], exclude=[np.int32, np.int64]) + self.assertTrue(isinstance(context.exception, ValueError)) + + def test_raise_errors(self): + bio = BytesIO() + with session.Session() as s: + src = s.open_dataset(bio, 'w', 'src') + df = src.create_dataframe('df') + + df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) + df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) + df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) + + with self.assertRaises(Exception) as context: + df.describe(include='num3') + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe(include=np.int8) + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe(include=['num3', 'num4']) + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe(include=[np.int8, np.uint]) + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe(include=float('3.14159')) + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe() + self.assertTrue(isinstance(context.exception, ValueError)) + + df.create_numeric('num', 'int32').data.write([i for i in range(10)]) + df.create_numeric('num2', 'int64').data.write([i for i in range(10)]) + df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) + + with self.assertRaises(Exception) as context: + df.describe(exclude=float('3.14159')) + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe(exclude=['num', 'num2', 'ts1']) + self.assertTrue(isinstance(context.exception, ValueError)) + + + + + + + + From 78cc2220c0174552fe471ac3ea0d6828f15cb9ca Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 23 Sep 2021 08:58:48 +0100 Subject: [PATCH 112/181] sync with upstream --- exetera/core/abstract_types.py | 2 +- exetera/core/dataframe.py | 272 +++++----------------- exetera/core/fields.py | 103 +------- exetera/core/operations.py | 300 ++++++++++++------------ exetera/core/readerwriter.py | 73 +++--- exetera/core/session.py | 12 +- tests/test_dataframe.py | 414 ++++++++------------------------- tests/test_dataset.py | 2 +- tests/test_fields.py | 58 ----- tests/test_operations.py | 27 --- 10 files changed, 369 insertions(+), 894 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index 9c41ab34..b78ed536 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -343,7 +343,7 @@ def distinct(self, field=None, fields=None, filter=None): raise NotImplementedError() @abstractmethod - def get_spans(self, field=None, fields=None, dest=None): + def get_spans(self, field=None, fields=None): raise NotImplementedError() @abstractmethod diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 2416d0a0..19d9e4b3 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -37,7 +37,7 @@ class HDF5DataFrame(DataFrame): For a detailed explanation of DataFrame along with examples of its use, please refer to the wiki documentation at https://github.com/KCL-BMEIS/ExeTera/wiki/DataFrame-API - + :param name: name of the dataframe. :param dataset: a dataset object, where this dataframe belongs to. :param h5group: the h5group object to store the fields. If the h5group is not empty, acquire data from h5group @@ -46,7 +46,6 @@ class HDF5DataFrame(DataFrame): Dataframe<-Field-Field.data automatically. :param dataframe: optional - replicate data from another dictionary of (name:str, field: Field). """ - def __init__(self, dataset: Dataset, name: str, @@ -93,7 +92,7 @@ def add(self, :param field: field to add to this dataframe, copy the underlying dataset """ - dname = field.name[field.name.index('/', 1) + 1:] + dname = field.name[field.name.index('/', 1)+1:] nfield = field.create_like(self, dname) if field.indexed: nfield.indices.write(field.indices[:]) @@ -312,16 +311,16 @@ def rename(self, renamed. Example:: - + # rename a single field df.rename('a', 'b') - + # rename multiple fields df.rename({'a': 'b', 'b': 'c', 'c': 'a'}) Field renaming can fail if the resulting set of renamed fields would have name clashes. If this is the case, none of the rename operations go ahead and the dataframe remains unmodified. - + :param field: Either a string or a dictionary of name pairs, each of which is the existing field name and the destination field name :param field_to: Optional parameter containing a string, if `field` is a string. If 'field' @@ -396,6 +395,7 @@ def get_unique_name(name, keys): self._columns = final_columns + def apply_filter(self, filter_to_apply, ddf=None): """ Apply the filter to all the fields in this dataframe, return a dataframe with filtered fields. @@ -434,16 +434,17 @@ def apply_index(self, index_to_apply, ddf=None): field.apply_index(index_to_apply, target=newfld) return ddf else: - val.validate_all_field_length_in_df(self) + val.validate_all_field_length_in_df(self) for field in self._columns.values(): field.apply_index(index_to_apply, in_place=True) return self + def sort_values(self, by: Union[str, List[str]], ddf: DataFrame = None, axis=0, ascending=True, kind='stable'): """ Sort by the values of a field or a list of fields - + :param by: Name (str) or list of names (str) to sort by. :param ddf: optional - the destination data frame :param axis: Axis to be sorted. Currently only supports 0 @@ -468,8 +469,8 @@ def sort_values(self, by: Union[str, List[str]], ddf: DataFrame = None, axis=0, return self.apply_index(sorted_index, ddf) - def to_csv(self, filepath: str, row_filter: Union[np.ndarray, fld.Field] = None, - column_filter: Union[str, List[str]] = None, chunk_row_size: int = 1 << 15): + + def to_csv(self, filepath:str, row_filter:Union[np.ndarray, fld.Field]=None, column_filter:Union[str, List[str]]=None, chunk_row_size:int=1<<15): """ Write object to a comma-separated values (csv) file. :param filepath: File path. @@ -481,7 +482,7 @@ def to_csv(self, filepath: str, row_filter: Union[np.ndarray, fld.Field] = None, field_name_to_use = list(self.keys()) if column_filter is not None: - field_name_to_use = val.validate_selected_keys(column_filter, self.keys()) + field_name_to_use = val.validate_selected_keys(column_filter, self.keys()) filter_array = None if row_filter is not None: @@ -492,7 +493,7 @@ def to_csv(self, filepath: str, row_filter: Union[np.ndarray, fld.Field] = None, fields_to_use = [self._columns[f] for f in field_name_to_use] with open(filepath, 'w') as f: - writer = csvlib.writer(f, delimiter=',', lineterminator='\n') + writer = csvlib.writer(f, delimiter=',',lineterminator='\n') # write header names writer.writerow(field_name_to_use) @@ -502,13 +503,12 @@ def to_csv(self, filepath: str, row_filter: Union[np.ndarray, fld.Field] = None, chunk_data = [] for field in fields_to_use: if field.indexed: - chunk_data.append(field.data[start_row: start_row + chunk_row_size]) + chunk_data.append(field.data[start_row: start_row+chunk_row_size]) else: - chunk_data.append(field.data[start_row: start_row + chunk_row_size].tolist()) + chunk_data.append(field.data[start_row: start_row+chunk_row_size].tolist()) for i, row in enumerate(zip(*chunk_data)): - if filter_array is None or ( - i + start_row < len(filter_array) and filter_array[i + start_row] == True): + if filter_array is None or (i + start_row 15}'.format(f), end='\t') - print('') - print('\n') - - return result class HDF5DataFrameGroupBy(DataFrameGroupBy): @@ -735,21 +576,23 @@ def __init__(self, columns, by, sorted_index, spans): self._sorted_index = sorted_index self._spans = spans - def _write_groupby_keys(self, ddf: DataFrame, write_keys=True): + + def _write_groupby_keys(self, ddf: DataFrame, write_keys=True): """ Write groupby keys to ddf only if write_key = True """ - if write_keys: - by_fields = np.asarray([self._columns[k] for k in self._by]) + if write_keys: + by_fields = np.asarray([self._columns[k] for k in self._by]) for field in by_fields: newfld = field.create_like(ddf, field.name) - - if self._sorted_index is not None: - field.apply_index(self._sorted_index, target=newfld) + + if self._sorted_index is not None: + field.apply_index(self._sorted_index, target=newfld) newfld.apply_filter(self._spans[:-1], in_place=True) else: field.apply_filter(self._spans[:-1], target=newfld) + def count(self, ddf: DataFrame, write_keys=True) -> DataFrame: """ Compute max of group values. @@ -757,21 +600,22 @@ def count(self, ddf: DataFrame, write_keys=True) -> DataFrame: :param target: Name (str) or list of names (str) to compute count. :param ddf: the destination data frame :param write_keys: write groupby keys to ddf only if write_key=True. Default is True. - + :return: dataframe with count of group values - """ + """ self._write_groupby_keys(ddf, write_keys) - counts = np.zeros(len(self._spans) - 1, dtype='int64') + counts = np.zeros(len(self._spans)-1, dtype='int64') ops.apply_spans_count(self._spans, counts) - ddf.create_numeric(name='count', nformat='int64').data.write(counts) + ddf.create_numeric(name = 'count', nformat='int64').data.write(counts) return ddf def distinct(self, ddf: DataFrame, write_keys=True) -> DataFrame: self._write_groupby_keys(ddf, write_keys) return ddf + def max(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) -> DataFrame: """ @@ -780,15 +624,15 @@ def max(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) -> :param target: Name (str) or list of names (str) to compute max. :param ddf: the destination data frame :param write_keys: write groupby keys to ddf only if write_key=True. Default is True. - + :return: dataframe with max of group values """ targets = val.validate_groupby_target(target, self._by, self._all) self._write_groupby_keys(ddf, write_keys) - + target_fields = tuple(self._columns[k] for k in targets) - for field in target_fields: + for field in target_fields: newfld = field.create_like(ddf, field.name + '_max') # sort first if needed @@ -802,6 +646,7 @@ def max(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) -> return ddf + def min(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) -> DataFrame: """ Compute min of group values. @@ -809,7 +654,7 @@ def min(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) -> :param target: Name (str) or list of names (str) to compute min. :param ddf: the destination data frame :param write_keys: write groupby keys to ddf only if write_key=True. Default is True. - + :return: dataframe with min of group values """ targets = val.validate_groupby_target(target, self._by, self._all) @@ -817,7 +662,7 @@ def min(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) -> self._write_groupby_keys(ddf, write_keys) target_fields = tuple(self._columns[k] for k in targets) - for field in target_fields: + for field in target_fields: newfld = field.create_like(ddf, field.name + '_min') # sort first if needed @@ -831,6 +676,7 @@ def min(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) -> return ddf + def first(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) -> DataFrame: """ Get first of group values. @@ -838,7 +684,7 @@ def first(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) :param target: Name (str) or list of names (str) to get first value. :param ddf: the destination data frame :param write_keys: write groupby keys to ddf only if write_key=True. Default is True. - + :return: dataframe with first of group values """ targets = val.validate_groupby_target(target, self._by, self._all) @@ -846,7 +692,7 @@ def first(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) self._write_groupby_keys(ddf, write_keys) target_fields = tuple(self._columns[k] for k in targets) - for field in target_fields: + for field in target_fields: newfld = field.create_like(ddf, field.name + '_first') # sort first if needed @@ -859,6 +705,7 @@ def first(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) return ddf + def last(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) -> DataFrame: """ Get last of group values. @@ -866,7 +713,7 @@ def last(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) - :param target: Name (str) or list of names (str) to get last value. :param ddf: the destination data frame :param write_keys: write groupby keys to ddf only if write_key=True. Default is True. - + :return: dataframe with last of group values """ targets = val.validate_groupby_target(target, self._by, self._all) @@ -874,7 +721,7 @@ def last(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) - self._write_groupby_keys(ddf, write_keys) target_fields = tuple(self._columns[k] for k in targets) - for field in target_fields: + for field in target_fields: newfld = field.create_like(ddf, field.name + '_last') # sort first if needed @@ -884,10 +731,11 @@ def last(self, target: Union[str, List[str]], ddf: DataFrame, write_keys=True) - newfld.apply_spans_last(self._spans, in_place=True) else: field.apply_spans_last(self._spans, target=newfld) - + return ddf + def copy(field: fld.Field, dataframe: DataFrame, name: str): """ Copy a field to another dataframe as well as underlying dataset. @@ -970,9 +818,9 @@ def merge(left: DataFrame, this is not set, all fields from the left table are joined :param right_fields: Optional parameter listing which fields are to be joined from the right table. If this is not set, all fields from the right table are joined - :param left_suffix: A string to be appended to fields from the left table if they clash with fields from the + :param left_suffix: A string to be appended to fields from the left table if they clash with fields from the right table. - :param right_suffix: A string to be appended to fields from the right table if they clash with fields from the + :param right_suffix: A string to be appended to fields from the right table if they clash with fields from the left table. :param how: Optional parameter specifying the merge mode. It must be one of ('left', 'right', 'inner', 'outer' or 'cross). If not set, the 'left' join is performed. @@ -1039,8 +887,8 @@ def merge(left: DataFrame, ordered = False if left_keys_ordered and right_keys_ordered and \ - len(left_on_fields) == 1 and len(right_on_fields) == 1 and \ - how in ('left', 'right', 'inner'): + len(left_on_fields) == 1 and len(right_on_fields) == 1 and \ + how in ('left', 'right', 'inner'): ordered = True if ordered: @@ -1129,7 +977,7 @@ def _unordered_merge(left: DataFrame, d.data.write(v) if not np.all(l_to_d_filt): - d = dest.create_numeric('valid' + left_suffix, 'bool') + d = dest.create_numeric('valid'+left_suffix, 'bool') d.data.write(l_to_d_filt) for f in right_fields_to_map: @@ -1147,7 +995,7 @@ def _unordered_merge(left: DataFrame, d.data.write(v) if not np.all(r_to_d_filt): - d = dest.create_numeric('valid' + right_suffix, 'bool') + d = dest.create_numeric('valid'+right_suffix, 'bool') d.data.write(r_to_d_filt) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 3747feb0..799ed4e4 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -23,15 +23,6 @@ class HDF5Field(Field): def __init__(self, session, group, dataframe, write_enabled=False): - """ - Constructor for Fields based on HDF5 data store. This initializer should only be called by Field types that are - subclasses of HDF5Field, and never directly by users of ExeTera. - - :param session: The session instance. - :param group: The HDF5 Group object. - :param dataframe: The dataframe this field belongs to. - :param write_enabled: A read-only/read-write switch. - """ super().__init__() # if name is None: @@ -125,12 +116,6 @@ def _ensure_valid(self): class MemoryField(Field): def __init__(self, session): - """ - Constructor for memory-based Fields. This initializer should only be called by Field types that are subclasses - of MemoryField, and never directly by users of ExeTera. - - :param session: The session instance. - """ super().__init__() self._session = session self._write_enabled = True @@ -177,20 +162,8 @@ def apply_index(self, index_to_apply, dstfld=None): raise NotImplementedError("Please use apply_index() on specific fields, not the field base class.") -# Field arrays -# ============ - - class ReadOnlyFieldArray: def __init__(self, field, dataset_name): - """ - Construct a ReadOnlyFieldArray instance. This class is an implementation detail, used to access data in - non-indexed fields deriving from HDF5Field. As such, instances of ReadOnlyFieldArray should only be created by - the fields themselves, and not by users of ExeTera. - - :param field: The HDF5 group object used as storage. - :param dataset_name: The name of the dataset object in HDF5, normally use 'values' - """ self._field = field self._name = dataset_name self._dataset = field[dataset_name] @@ -226,16 +199,11 @@ def complete(self): "for a writeable copy of the field") +# Field arrays +# ============ + class WriteableFieldArray: def __init__(self, field, dataset_name): - """ - Construct a WriteableFieldArray instance. This class is an implementation detail, used to access data in - non-indexed fields deriving from HDF5Field. As such, instances of WriteableFieldArray should only be created by - the Fields themselves, and not by users of ExeTera. - - :param field: The HDF5 group object used as storage. - :param dataset_name: The name of the dataset object in HDF5, normally use 'values' - """ self._field = field self._name = dataset_name self._dataset = field[dataset_name] @@ -273,14 +241,8 @@ def complete(self): class MemoryFieldArray: - def __init__(self, dtype): - """ - Construct a MemoryFieldArray instance. This class is an implementation detail, used to access data in - non-indexed fields deriving from MemoryField. As such, instances of MemoryFieldArray should only be created by - the Fields themselves, and not by users of ExeTera. - :param dtype: The data type for construct the numpy array. - """ + def __init__(self, dtype): self._dtype = dtype self._dataset = None @@ -327,15 +289,6 @@ def complete(self): class ReadOnlyIndexedFieldArray: def __init__(self, field, indices, values): - """ - Construct a ReadOnlyIndexedFieldArray instance. This class is an implementation detail, used to access data in - indexed fields. As such, instances of ReadOnlyIndexedFieldArray should only be created by the Fields themselves, - and not by users of ExeTera. - - :param field: The HDF5 group from which the data is read. - :param indices: The indices of the IndexedStringField. - :param values: The values of the IndexedStringField. - """ self._field = field self._indices = indices self._values = values @@ -683,6 +636,7 @@ def __init__(self, session, nformat): super().__init__(session) self._nformat = nformat + def writeable(self): return self @@ -802,9 +756,6 @@ def __rand__(self, first): def __xor__(self, second): return FieldDataOps.numeric_xor(self._session, self, second) - def __invert__(self): - return FieldDataOps.invert(self._session, self) - def __rxor__(self, first): return FieldDataOps.numeric_xor(self._session, first, self) @@ -1436,16 +1387,6 @@ def apply_index(self, index_to_apply, target=None, in_place=False): self._ensure_valid() return FieldDataOps.apply_index_to_field(self, index_to_apply, target, in_place) - def astype(self, type): - if isinstance(type, str) and type not in ['int8', 'int16', 'int32', 'int64', 'uint8', 'uint16','uint32', - 'uint64', 'float16', 'float32', 'float64', 'float128', 'bool_']: - raise ValueError("The type to convert is not supported, please use numeric type such as int or float.") - elif isinstance(type, np.dtype) and type not in [np.int8, np.int16, np.int32, np.int64, np.uint8, np.uint16, - np.uint32, np.uint64, np.float16, np.float32, np.float64, - np.float128, np.bool_]: - raise ValueError("The type to convert is not supported, please use numeric type such as int or float.") - self.data._dataset = self.data[:].astype(type) - def apply_spans_first(self, spans_to_apply, target=None, in_place=False): self._ensure_valid() return FieldDataOps.apply_spans_first(self, spans_to_apply, target, in_place) @@ -1542,14 +1483,6 @@ def __ror__(self, first): self._ensure_valid() return FieldDataOps.numeric_or(self._session, first, self) - def __invert__(self): - self._ensure_valid() - return FieldDataOps.invert(self._session, self) - - def logical_not(self): - self._ensure_valid() - return FieldDataOps.logical_not(self._session, self) - def __lt__(self, value): self._ensure_valid() return FieldDataOps.less_than(self._session, self, value) @@ -1959,18 +1892,6 @@ def _binary_op(session, first, second, function): f.data.write(r) return f - @staticmethod - def _unary_op(session, first, function): - if isinstance(first, Field): - first_data = first.data[:] - else: - first_data = first - - r = function(first_data) - f = NumericMemField(session, dtype_to_str(r.dtype)) - f.data.write(r) - return f - @classmethod def numeric_add(cls, session, first, second): def function_add(first, second): @@ -2053,20 +1974,6 @@ def function_or(first, second): return cls._binary_op(session, first, second, function_or) - @classmethod - def invert(cls, session, first): - def function_invert(first): - return ~first - - return cls._unary_op(session, first, function_invert) - - @classmethod - def logical_not(cls, session, first): - def function_logical_not(first): - return np.logical_not(first) - - return cls._unary_op(session, first, function_logical_not) - @classmethod def less_than(cls, session, first, second): def function_less_than(first, second): diff --git a/exetera/core/operations.py b/exetera/core/operations.py index 041fa1b1..33a8da70 100644 --- a/exetera/core/operations.py +++ b/exetera/core/operations.py @@ -99,9 +99,9 @@ def count_back(array): [10, 20, 30, 30, 30] -> 2 ([10, 20]) [10, 20, 20, 20, 20] -> 1 ([10]) """ - v = len(array) - 1 + v = len(array)-1 while v > 0: - if array[v - 1] != array[v]: + if array[v-1] != array[v]: return v v -= 1 return 0 @@ -229,7 +229,7 @@ def safe_map_indexed_values(data_indices, data_values, map_field, map_filter, em value_length = 0 for i in range(len(map_field)): if map_filter[i]: - value_length += data_indices[map_field[i] + 1] - data_indices[map_field[i]] + value_length += data_indices[map_field[i]+1] - data_indices[map_field[i]] else: value_length += empty_value_len @@ -241,17 +241,17 @@ def safe_map_indexed_values(data_indices, data_values, map_field, map_filter, em for i in range(len(map_field)): if map_filter[i]: sst = data_indices[map_field[i]] - sse = data_indices[map_field[i] + 1] + sse = data_indices[map_field[i]+1] dst = offset delta = sse - sst dse = offset + delta - i_result[i + 1] = dse + i_result[i+1] = dse v_result[dst:dse] = data_values[sst:sse] offset += delta else: dst = offset dse = offset + empty_value_len - i_result[i + 1] = dse + i_result[i+1] = dse if empty_value is not None: v_result[dst:dse] = empty_value offset += dse - dst @@ -282,7 +282,7 @@ def map_valid(data_field, map_field, result=None, invalid=-1): def ordered_map_valid_stream_old(data_field, map_field, result_field, - invalid=-1, chunksize=DEFAULT_CHUNKSIZE): + invalid=-1, chunksize=DEFAULT_CHUNKSIZE): df_it = iter(chunks(len(data_field.data), chunksize=chunksize)) mf_it = iter(chunks(len(map_field.data), chunksize=chunksize)) df_range = next(df_it) @@ -334,6 +334,7 @@ def ordered_map_valid_partial_old(d, data_field, map_field, result, invalid): @njit def next_map_subchunk(map_, sm, invalid, chunksize): + start = -1 while sm < len(map_) and map_[sm] == invalid: sm += 1 @@ -382,9 +383,10 @@ def ordered_map_valid_stream(data_field, map_field, result_field, # no unfiltered values in this chunk so just assign empty entries to the result field result_data.fill(0) else: - values = data_field.data[d_limits[0]:d_limits[1] + 1] + values = data_field.data[d_limits[0]:d_limits[1]+1] _ = ordered_map_valid_partial(values, map_, sm_start, sm_end, d_limits[0], - result_data, invalid, empty_value) + result_data, invalid, empty_value) + result_field.data.write(result_data[:m_max]) m_chunk, map_, m_max, m_off, m = next_untrimmed_chunk(map_field, m_chunk, chunksize) @@ -461,9 +463,9 @@ def ordered_map_valid_indexed_stream(data_field, map_field, result_field, # m += sm_end - sm_start else: # TODO: can potentially optimise here by checking if upper limit has increased - indices_ = data_field.indices[i_limits[0]:i_limits[1] + 2] + indices_ = data_field.indices[i_limits[0]:i_limits[1]+2] sub_chunks = list() - calculate_chunk_decomposition(0, i_limits[1] - i_limits[0] + 1, indices_, + calculate_chunk_decomposition(0, i_limits[1] - i_limits[0]+1, indices_, chunksize * value_factor, sub_chunks) s = 0 @@ -524,11 +526,12 @@ def ordered_map_valid_indexed_partial(sm_values, ri, rv, ri_accum): + need_values = False # this is the offset that must be subtracted from the value index before it is looked up v_offset = indices[i_start] - while sm < sm_end: # and ri < len(result_indices): + while sm < sm_end: # and ri < len(result_indices): if sm_values[sm] == invalid: result_indices[ri] = ri_accum else: @@ -537,7 +540,7 @@ def ordered_map_valid_indexed_partial(sm_values, need_values = True break v_start = indices[i] - v_offset - v_end = indices[i + 1] - v_offset + v_end = indices[i+1] - v_offset if rv + v_end - v_start > len(result_values): break for v in range(v_start, v_end): @@ -592,7 +595,7 @@ def apply_filter_to_index_values(index_filter, indices, values): if index_filter[i] == True: count += 1 total += next_[i] - cur_[i] - dest_indices = np.zeros(count + 1, indices.dtype) + dest_indices = np.zeros(count+1, indices.dtype) dest_values = np.zeros(total, values.dtype) dest_indices[0] = 0 count = 1 @@ -619,7 +622,7 @@ def apply_indices_to_index_values(indices_to_apply, indices, values): for i in indices_to_apply: count += 1 total += next_[i] - cur_[i] - dest_indices = np.zeros(count + 1, indices.dtype) + dest_indices = np.zeros(count+1, indices.dtype) dest_values = np.zeros(total, values.dtype) dest_indices[0] = 0 count = 1 @@ -651,16 +654,16 @@ def get_spans_for_field(ndarray): @njit def _get_spans_for_2_fields_by_spans(span0, span1): spans = [] - j = 0 + j=0 for i in range(len(span0)): - if j < len(span1): + if j src_values[minstart + k]: + elif src_values[curstart+k] > src_values[minstart+k]: found = True break if not found and curlen < minlen: @@ -802,31 +806,31 @@ def apply_spans_index_of_min_indexed(spans, src_indices, src_values, dest_array) @njit def apply_spans_index_of_max_indexed(spans, src_indices, src_values, dest_array): - for i in range(len(spans) - 1): + for i in range(len(spans)-1): cur = spans[i] - next = spans[i + 1] + next = spans[i+1] if next - cur == 1: dest_array[i] = cur else: minind = cur minstart = src_indices[cur] - minend = src_indices[cur + 1] + minend = src_indices[cur+1] minlen = minend - minstart - for j in range(cur + 1, next): + for j in range(cur+1, next): curstart = src_indices[j] - curend = src_indices[j + 1] + curend = src_indices[j+1] curlen = curend - curstart shortlen = min(curlen, minlen) found = False for k in range(shortlen): - if src_values[curstart + k] > src_values[minstart + k]: + if src_values[curstart+k] > src_values[minstart+k]: minind = j minstart = curstart minlen = curend - curstart found = True break - elif src_values[curstart + k] < src_values[minstart + k]: + elif src_values[curstart+k] < src_values[minstart+k]: found = True break if not found and curlen > minlen: @@ -841,9 +845,9 @@ def apply_spans_index_of_max_indexed(spans, src_indices, src_values, dest_array) @njit def apply_spans_index_of_max(spans, src_array, dest_array): - for i in range(len(spans) - 1): + for i in range(len(spans)-1): cur = spans[i] - next = spans[i + 1] + next = spans[i+1] if next - cur == 1: dest_array[i] = cur @@ -920,15 +924,15 @@ def apply_spans_index_of_last_filter(spans, dest_array, filter_array): filter_array[i] = False else: filter_array[i] = True - dest_array[i] = spans[i + 1] - 1 + dest_array[i] = spans[i+1]-1 return dest_array, filter_array @njit def apply_spans_count(spans, dest_array): - for i in range(len(spans) - 1): - dest_array[i] = np.int64(spans[i + 1] - spans[i]) + for i in range(len(spans)-1): + dest_array[i] = np.int64(spans[i+1] - spans[i]) @njit @@ -938,15 +942,16 @@ def apply_spans_first(spans, src_array, dest_array): @njit def apply_spans_last(spans, src_array, dest_array): - spans = spans[1:] - 1 + spans = spans[1:]-1 dest_array[:] = src_array[spans] @njit def apply_spans_max(spans, src_array, dest_array): - for i in range(len(spans) - 1): + + for i in range(len(spans)-1): cur = spans[i] - next = spans[i + 1] + next = spans[i+1] if next - cur == 1: dest_array[i] = src_array[cur] else: @@ -955,9 +960,10 @@ def apply_spans_max(spans, src_array, dest_array): @njit def apply_spans_min(spans, src_array, dest_array): - for i in range(len(spans) - 1): + + for i in range(len(spans)-1): cur = spans[i] - next = spans[i + 1] + next = spans[i+1] if next - cur == 1: dest_array[i] = src_array[cur] else: @@ -995,10 +1001,10 @@ def apply_spans_concat(spans, src_index, src_values, dest_index, dest_values, index_i = np.uint32(0) index_v = np.int64(0) - s_end = len(spans) - 1 + s_end = len(spans)-1 for s in range(s_start, s_end): cur = spans[s] - next = spans[s + 1] + next = spans[s+1] cur_src_i = src_index[cur] next_src_i = src_index[next] @@ -1016,8 +1022,8 @@ def apply_spans_concat(spans, src_index, src_values, dest_index, dest_values, # separate them by commas non_empties = 0 for e in range(cur, next): - if src_index[e] < src_index[e + 1]: - non_empties += 1 + if src_index[e] < src_index[e+1]: + non_empties += 1 if non_empties == 1: # only one non-empty entry to be copied, so commas not required next_index_v = next_src_i - cur_src_i + np.int64(index_v) @@ -1028,7 +1034,7 @@ def apply_spans_concat(spans, src_index, src_values, dest_index, dest_values, # so there must be multiple non-empty entries and commas are required for e in range(cur, next): src_start = src_index[e] - src_end = src_index[e + 1] + src_end = src_index[e+1] comma = False quotes = False for i_c in range(src_start, src_end): @@ -1058,7 +1064,7 @@ def apply_spans_concat(spans, src_index, src_values, dest_index, dest_values, # if either the index or values are past the threshold, write them if index_i >= max_index_i or index_v >= max_value_i: break - return s + 1, index_i, index_v + return s+1, index_i, index_v # ordered map to left functionality: streaming @@ -1376,13 +1382,13 @@ def generate_ordered_map_to_left_partial(left, # freeze i for the duration of the loop; i_ tracks i_ = i cur_i_count = 1 - while i_ + 1 < i_max and left[i_ + 1] == left[i_]: + while i_ + 1 < i_max and left[i_+1] == left[i_]: cur_i_count += 1 i_ += 1 j_ = j cur_j_count = 1 - while j_ + 1 < j_max and right[j_ + 1] == right[j_]: + while j_ + 1 < j_max and right[j_+1] == right[j_]: cur_j_count += 1 j_ += 1 @@ -1436,7 +1442,7 @@ def generate_ordered_map_to_left_left_unique_partial(left, l_result[r] = i + i_off r_result[r] = j + j_off r += 1 - if j + 1 >= j_max or right[j + 1] != right[j]: + if j+1 >= j_max or right[j+1] != right[j]: i += 1 j += 1 return i, j, r @@ -1462,7 +1468,7 @@ def generate_ordered_map_to_left_right_unique_partial(left, else: r_result[r] = j + j_off r += 1 - if i + 1 >= i_max or left[i + 1] != left[i]: + if i+1 >= i_max or left[i+1] != left[i]: j += 1 i += 1 return i, j, r @@ -1575,7 +1581,7 @@ def generate_ordered_map_to_left_right_unique_partial_old(d_j, left, right, left j += 1 else: left_to_right[i] = j + d_j - if i + 1 >= len(left) or left[i + 1] != left[i]: + if i+1 >= len(left) or left[i+1] != left[i]: j += 1 i += 1 # if j+1 < len(right) and right[j+1] != right[j]: @@ -1836,13 +1842,13 @@ def generate_ordered_map_to_inner_partial(left, # freeze i for the duration of the loop; i_ tracks i_ = i cur_i_count = 1 - while i_ + 1 < i_max and left[i_ + 1] == left[i_]: + while i_ + 1 < i_max and left[i_+1] == left[i_]: cur_i_count += 1 i_ += 1 j_ = j cur_j_count = 1 - while j_ + 1 < j_max and right[j_ + 1] == right[j_]: + while j_ + 1 < j_max and right[j_+1] == right[j_]: cur_j_count += 1 j_ += 1 @@ -1893,7 +1899,7 @@ def generate_ordered_map_to_inner_left_unique_partial(left, l_result[r] = i + i_off r_result[r] = j + j_off r += 1 - if j + 1 >= j_max or right[j + 1] != right[j]: + if j+1 >= j_max or right[j+1] != right[j]: i += 1 j += 1 return i, j, r @@ -1920,7 +1926,7 @@ def generate_ordered_map_to_inner_right_unique_partial(left, l_result[r] = i + i_off r_result[r] = j + j_off r += 1 - if i + 1 >= i_max or left[i + 1] != left[i]: + if i+1 >= i_max or left[i+1] != left[i]: j += 1 i += 1 return i, j, r @@ -1973,7 +1979,7 @@ def generate_ordered_map_to_left_right_unique(first, second, result, invalid): j += 1 else: result[i] = j - if i + 1 >= len(first) or first[i + 1] != first[i]: + if i+1 >= len(first) or first[i+1] != first[i]: j += 1 i += 1 @@ -2170,7 +2176,7 @@ def ordered_inner_map_left_unique_partial(d_i, d_j, left, right, left_to_inner[m] = i + d_i right_to_inner[m] = j + d_j m += 1 - if j + 1 >= len(right) or right[j + 1] != right[j]: + if j+1 >= len(right) or right[j+1] != right[j]: i += 1 j += 1 return i, j, m @@ -2190,7 +2196,7 @@ def ordered_inner_map_left_unique(left, right, left_to_inner, right_to_inner): cur_j = j while cur_j + 1 < len(right) and right[cur_j + 1] == right[cur_j]: cur_j += 1 - for jj in range(j, cur_j + 1): + for jj in range(j, cur_j+1): left_to_inner[cur_m] = i right_to_inner[cur_m] = jj cur_m += 1 @@ -2215,8 +2221,8 @@ def ordered_inner_map(left, right, left_to_inner, right_to_inner): cur_j = j while cur_j + 1 < len(right) and right[cur_j + 1] == right[cur_j]: cur_j += 1 - for ii in range(i, cur_i + 1): - for jj in range(j, cur_j + 1): + for ii in range(i, cur_i+1): + for jj in range(j, cur_j+1): left_to_inner[cur_m] = ii right_to_inner[cur_m] = jj cur_m += 1 @@ -2227,8 +2233,8 @@ def ordered_inner_map(left, right, left_to_inner, right_to_inner): @njit def ordered_get_last_as_filter(field): result = np.zeros(len(field), dtype=numba.types.boolean) - for i in range(len(field) - 1): - result[i] = field[i] != field[i + 1] + for i in range(len(field)-1): + result[i] = field[i] != field[i+1] result[-1] = True return result @@ -2240,7 +2246,7 @@ def ordered_generate_journalling_indices(old, new): total = 0 while i < len(old) and j < len(new): if old[i] < new[j]: - while i + 1 < len(old) and old[i + 1] == old[i]: + while i+1 < len(old) and old[i+1] == old[i]: i += 1 i += 1 total += 1 @@ -2248,13 +2254,13 @@ def ordered_generate_journalling_indices(old, new): j += 1 total += 1 else: - while i + 1 < len(old) and old[i + 1] == old[i]: + while i+1 < len(old) and old[i+1] == old[i]: i += 1 i += 1 j += 1 total += 1 while i < len(old): - while i + 1 < len(old) and old[i + 1] == old[i]: + while i+1 < len(old) and old[i+1] == old[i]: i += 1 i += 1 total += 1 @@ -2270,7 +2276,7 @@ def ordered_generate_journalling_indices(old, new): joint = 0 while i < len(old) and j < len(new): if old[i] < new[j]: - while i + 1 < len(old) and old[i + 1] == old[i]: + while i+1 < len(old) and old[i+1] == old[i]: i += 1 old_inds[joint] = i new_inds[joint] = -1 @@ -2282,7 +2288,7 @@ def ordered_generate_journalling_indices(old, new): j += 1 joint += 1 else: - while i + 1 < len(old) and old[i + 1] == old[i]: + while i+1 < len(old) and old[i+1] == old[i]: i += 1 old_inds[joint] = i new_inds[joint] = j @@ -2291,7 +2297,7 @@ def ordered_generate_journalling_indices(old, new): joint += 1 while i < len(old): - while i + 1 < len(old) and old[i + 1] == old[i]: + while i+1 < len(old) and old[i+1] == old[i]: i += 1 old_inds[joint] = i new_inds[joint] = -1 @@ -2336,8 +2342,8 @@ def compare_indexed_rows_for_journalling(old_map, new_map, # row has been removed so don't count as kept to_keep[i] = False else: - old_value = old_values[old_indices[old_map[i]]:old_indices[old_map[i] + 1]] - new_value = new_values[new_indices[new_map[i]]:new_indices[new_map[i] + 1]] + old_value = old_values[old_indices[old_map[i]]:old_indices[old_map[i]+1]] + new_value = new_values[new_indices[new_map[i]]:new_indices[new_map[i]+1]] to_keep[i] = not np.array_equal(old_value, new_value) @@ -2356,7 +2362,6 @@ def merge_journalled_entries(old_map, new_map, to_keep, old_src, new_src, dest): dest[cur_dest] = new_src[new_map[i]] cur_dest += 1 - # def merge_journalled_entries(old_map, new_map, to_keep, old_src, new_src, dest): # for om, im, tk in zip(old_map, new_map, to_keep): # for omi in old_map: @@ -2371,20 +2376,20 @@ def merge_indexed_journalled_entries_count(old_map, new_map, to_keep, old_src_in acc_val = 0 for i in range(len(old_map)): while cur_old <= old_map[i]: - ind_delta = old_src_inds[cur_old + 1] - old_src_inds[cur_old] + ind_delta = old_src_inds[cur_old+1] - old_src_inds[cur_old] acc_val += ind_delta cur_old += 1 if to_keep[i] == True: - ind_delta = new_src_inds[new_map[i] + 1] - new_src_inds[new_map[i]] + ind_delta = new_src_inds[new_map[i]+1] - new_src_inds[new_map[i]] acc_val += ind_delta return acc_val @njit def merge_indexed_journalled_entries(old_map, new_map, to_keep, - old_src_inds, old_src_vals, - new_src_inds, new_src_vals, - dest_inds, dest_vals): + old_src_inds, old_src_vals, + new_src_inds, new_src_vals, + dest_inds, dest_vals): cur_old = 0 cur_dest = 1 ind_acc = 0 @@ -2396,7 +2401,7 @@ def merge_indexed_journalled_entries(old_map, new_map, to_keep, ind_acc += ind_delta dest_inds[cur_dest] = ind_acc if ind_delta > 0: - dest_vals[ind_acc - ind_delta:ind_acc] = \ + dest_vals[ind_acc-ind_delta:ind_acc] = \ old_src_vals[old_src_inds[cur_old]:old_src_inds[cur_old + 1]] cur_old += 1 cur_dest += 1 @@ -2406,7 +2411,7 @@ def merge_indexed_journalled_entries(old_map, new_map, to_keep, ind_acc += ind_delta dest_inds[cur_dest] = ind_acc if ind_delta > 0: - dest_vals[ind_acc - ind_delta:ind_acc] = \ + dest_vals[ind_acc-ind_delta:ind_acc] = \ new_src_vals[new_src_inds[new_map[i]]:new_src_inds[new_map[i] + 1]] cur_dest += 1 @@ -2444,6 +2449,7 @@ def merge_entries_segment(i_start, cur_old_start, def streaming_sort_merge(src_index_f, src_value_f, tgt_index_f, tgt_value_f, segment_length, chunk_length): + # get the number of segments segment_count = len(src_index_f.data) // segment_length if len(src_index_f.data) % segment_length != 0: @@ -2473,8 +2479,8 @@ def streaming_sort_merge(src_index_f, src_value_f, tgt_index_f, tgt_value_f, # get the first chunk for each segment for i in range(segment_count): index_start = segment_starts[i] + chunk_indices[i] * chunk_length - src_value_chunks.append(src_value_f.data[index_start:index_start + chunk_length]) - src_index_chunks.append(src_index_f.data[index_start:index_start + chunk_length]) + src_value_chunks.append(src_value_f.data[index_start:index_start+chunk_length]) + src_index_chunks.append(src_index_f.data[index_start:index_start+chunk_length]) in_chunk_lengths[i] = len(src_value_chunks[i]) dest_indices = np.zeros(segment_count * chunk_length, dtype=src_index_f.data.dtype) @@ -2497,8 +2503,8 @@ def streaming_sort_merge(src_index_f, src_value_f, tgt_index_f, tgt_value_f, remaining = segment_starts[i] + segment_lengths[i] - index_start remaining = min(remaining, chunk_length) if remaining > 0: - src_value_chunks[i] = src_value_f.data[index_start:index_start + remaining] - src_index_chunks[i] = src_index_f.data[index_start:index_start + remaining] + src_value_chunks[i] = src_value_f.data[index_start:index_start+remaining] + src_index_chunks[i] = src_index_f.data[index_start:index_start+remaining] in_chunk_lengths[i] = len(src_value_chunks[i]) in_chunk_indices[i] = 0 else: @@ -2529,7 +2535,7 @@ def streaming_sort_partial(in_chunk_indices, in_chunk_lengths, src_value_chunks, src_index_chunks, dest_value_chunk, dest_index_chunk): dest_index = 0 max_possible = in_chunk_lengths.sum() - while (dest_index < max_possible): + while(dest_index < max_possible): if in_chunk_indices[0] == in_chunk_lengths[0]: return dest_index min_value = src_value_chunks[0][in_chunk_indices[0]] @@ -2562,7 +2568,7 @@ def is_ordered(field): return not np.any(fn(field[:-1], field[1:])) -# ======== method for transform functions that called in readerwriter.py ==========# +#======== method for transform functions that called in readerwriter.py ==========# def get_byte_map(string_map): """ @@ -2572,36 +2578,36 @@ def get_byte_map(string_map): sorted_string_map = {k: v for k, v in sorted(string_map.items(), key=lambda item: item[0])} sorted_string_key = [(len(k), np.frombuffer(k.encode(), dtype=np.uint8), v) for k, v in sorted_string_map.items()] sorted_string_values = list(sorted_string_map.values()) - + # assign byte_map_key_lengths, byte_map_value total_bytes_keys = 0 byte_map_value = np.zeros(len(sorted_string_map), dtype=np.uint8) - for i, (length, _, v) in enumerate(sorted_string_key): + for i, (length, _, v) in enumerate(sorted_string_key): total_bytes_keys += length byte_map_value[i] = v # assign byte_map_keys, byte_map_key_indices byte_map_keys = np.zeros(total_bytes_keys, dtype=np.uint8) - byte_map_key_indices = np.zeros(len(sorted_string_map) + 1, dtype=np.uint8) - + byte_map_key_indices = np.zeros(len(sorted_string_map)+1, dtype=np.uint8) + idx_pointer = 0 - for i, (_, b_key, _) in enumerate(sorted_string_key): + for i, (_, b_key, _) in enumerate(sorted_string_key): for b in b_key: byte_map_keys[idx_pointer] = b idx_pointer += 1 - byte_map_key_indices[i + 1] = idx_pointer + byte_map_key_indices[i + 1] = idx_pointer byte_map = [byte_map_keys, byte_map_key_indices, byte_map_value] return byte_map -@njit +@njit def categorical_transform(chunk, i_c, column_inds, column_vals, column_offsets, cat_keys, cat_index, cat_values): """ Tranform method for categorical importer in readerwriter.py - """ + """ col_offset = column_offsets[i_c] for row_idx in range(len(column_inds[i_c]) - 1): @@ -2626,18 +2632,17 @@ def categorical_transform(chunk, i_c, column_inds, column_vals, column_offsets, if index != -1: chunk[row_idx] = cat_values[index] + - -@njit -def leaky_categorical_transform(chunk, freetext_indices, freetext_values, i_c, column_inds, column_vals, column_offsets, - cat_keys, cat_index, cat_values): +@njit +def leaky_categorical_transform(chunk, freetext_indices, freetext_values, i_c, column_inds, column_vals, column_offsets, cat_keys, cat_index, cat_values): """ Tranform method for categorical importer in readerwriter.py - """ - col_offset = column_offsets[i_c] + """ + col_offset = column_offsets[i_c] for row_idx in range(len(column_inds[i_c]) - 1): - if row_idx >= chunk.shape[0]: # reach the end of chunk + if row_idx >= chunk.shape[0]: # reach the end of chunk break key_start = column_inds[i_c, row_idx] @@ -2663,10 +2668,9 @@ def leaky_categorical_transform(chunk, freetext_indices, freetext_values, i_c, c freetext_indices[row_idx + 1] = freetext_indices[row_idx] if not is_found: - chunk[row_idx] = -1 + chunk[row_idx] = -1 freetext_indices[row_idx + 1] = freetext_indices[row_idx] + key_len - freetext_values[freetext_indices[row_idx]: freetext_indices[row_idx + 1]] = column_vals[ - col_offset + key_start: col_offset + key_end] + freetext_values[freetext_indices[row_idx]: freetext_indices[row_idx + 1]] = column_vals[col_offset + key_start: col_offset + key_end] @njit @@ -2674,15 +2678,15 @@ def numeric_bool_transform(elements, validity, column_inds, column_vals, column_ invalid_value, validation_mode, field_name): """ Transform method for numeric importer (bool) in readerwriter.py - """ - col_offset = column_offsets[col_idx] - exception_message, exception_args = 0, [field_name] + """ + col_offset = column_offsets[col_idx] + exception_message, exception_args = 0, [field_name] for row_idx in range(written_row_count): - - empty = False - valid_input = True # Start by assuming number is valid - value = -1 # start by assuming value is -1, the valid result will be 1 or 0 for bool + + empty = False + valid_input = True # Start by assuming number is valid + value = -1 # start by assuming value is -1, the valid result will be 1 or 0 for bool row_start_idx = column_inds[col_idx, row_idx] row_end_idx = column_inds[col_idx, row_idx + 1] @@ -2693,10 +2697,10 @@ def numeric_bool_transform(elements, validity, column_inds, column_vals, column_ # ignore heading whitespace while byte_start_idx < length and column_vals[col_offset + row_start_idx + byte_start_idx] == 32: byte_start_idx += 1 - # ignore tailing whitespace + # ignore tailing whitespace while byte_end_idx >= 0 and column_vals[col_offset + row_start_idx + byte_end_idx] == 32: byte_end_idx -= 1 - + # actual length after removing heading and trailing whitespace actual_length = byte_end_idx - byte_start_idx + 1 @@ -2704,50 +2708,47 @@ def numeric_bool_transform(elements, validity, column_inds, column_vals, column_ empty = True valid_input = False else: - - val = column_vals[ - col_offset + row_start_idx + byte_start_idx: col_offset + row_start_idx + byte_start_idx + actual_length] + + val = column_vals[col_offset + row_start_idx + byte_start_idx: col_offset + row_start_idx + byte_start_idx + actual_length] if actual_length == 1: - if val in (49, 89, 121, 84, 116): # '1', 'Y', 'y', 'T', 't' + if val in (49, 89, 121, 84, 116): # '1', 'Y', 'y', 'T', 't' value = 1 - elif val in (48, 78, 110, 70, 102): # '0', 'N', 'n', 'F', 'f' + elif val in (48, 78, 110, 70, 102): # '0', 'N', 'n', 'F', 'f' value = 0 else: valid_input = False elif actual_length == 2: - if val[0] in (79, 111) and val[1] in ( - 78, 110): # val.lower() == 'on': val[0] in ('O', 'o'), val[1] in ('N', 'n') + if val[0] in (79, 111) and val[1] in (78, 110): # val.lower() == 'on': val[0] in ('O', 'o'), val[1] in ('N', 'n') value = 1 - elif val[0] in (78, 110) and val[1] in (79, 111): # val.lower() == 'no' + elif val[0] in (78, 110) and val[1] in (79, 111): # val.lower() == 'no' value = 0 else: valid_input = False elif actual_length == 3: - if val[0] in (89, 121) and val[1] in (69, 101) and val[2] in (83, 115): # 'yes' + if val[0] in (89, 121) and val[1] in (69, 101) and val[2] in (83, 115): # 'yes' value = 1 - elif val[0] in (79, 111) and val[1] in (70, 102) and val[2] in (70, 102): # 'off' + elif val[0] in (79, 111) and val[1] in (70, 102) and val[2] in (70, 102): # 'off' value = 0 else: valid_input = False - elif actual_length == 4: - if val[0] in (84, 116) and val[1] in (82, 114) and val[2] in (85, 117) and val[3] in ( - 69, 101): # 'true' + elif actual_length == 4: + if val[0] in (84, 116) and val[1] in (82, 114) and val[2] in (85, 117) and val[3] in (69, 101): # 'true' value = 1 else: valid_input = False elif actual_length == 5: - if val[0] in (70, 102) and val[1] in (65, 97) and val[2] in (76, 108) and val[3] in (83, 115) and val[ - 4] in (69, 101): # 'false' + if val[0] in (70, 102) and val[1] in (65, 97) and val[2] in (76, 108) and val[3] in (83, 115) and val[4] in (69, 101): # 'false' value = 0 else: valid_input = False else: valid_input = False + elements[row_idx] = value if valid_input else invalid_value validity[row_idx] = valid_input @@ -2760,16 +2761,16 @@ def numeric_bool_transform(elements, validity, column_inds, column_vals, column_ break else: exception_message = 2 - non_parsable = column_vals[col_offset + row_start_idx: col_offset + row_end_idx] + non_parsable = column_vals[col_offset + row_start_idx : col_offset + row_end_idx] exception_args = [field_name, non_parsable] break if validation_mode == 'allow_empty': if not empty: exception_message = 2 - non_parsable = column_vals[col_offset + row_start_idx: col_offset + row_end_idx] + non_parsable = column_vals[col_offset + row_start_idx : col_offset + row_end_idx] exception_args = [field_name, non_parsable] break - return exception_message, exception_args + return exception_message, exception_args def raiseNumericException(exception_message, exception_args): @@ -2784,7 +2785,8 @@ def raiseNumericException(exception_message, exception_args): def transform_int(column_inds, column_vals, column_offsets, col_idx, - written_row_count, invalid_value, validation_mode, data_type, field_name): + written_row_count, invalid_value, validation_mode, data_type, field_name): + widths = column_inds[col_idx, 1:written_row_count + 1] - column_inds[col_idx, :written_row_count] width = widths.max() elements = np.zeros(written_row_count, 'S{}'.format(width)) @@ -2793,7 +2795,7 @@ def transform_int(column_inds, column_vals, column_offsets, col_idx, if validation_mode == 'strict': try: - results = elements.astype(data_type) + results = elements.astype(data_type) except ValueError as e: msg = ("Field '{}' contains values that cannot " "be converted to float in '{}' mode").format(field_name, validation_mode) @@ -2826,7 +2828,8 @@ def transform_int(column_inds, column_vals, column_offsets, col_idx, def transform_float(column_inds, column_vals, column_offsets, col_idx, - written_row_count, invalid_value, validation_mode, data_type, field_name): + written_row_count, invalid_value, validation_mode, data_type, field_name): + widths = column_inds[col_idx, 1:written_row_count + 1] - column_inds[col_idx, :written_row_count] width = widths.max() elements = np.zeros(written_row_count, 'S{}'.format(width)) @@ -2835,7 +2838,7 @@ def transform_float(column_inds, column_vals, column_offsets, col_idx, if validation_mode == 'strict': try: - results = elements.astype(data_type) + results = elements.astype(data_type) except ValueError as e: msg = ("Field '{}' contains values that cannot " "be converted to float in '{}' mode").format(field_name, validation_mode) @@ -2880,6 +2883,7 @@ def transform_to_values(column_inds, column_vals, column_offsets, col_idx, writt return data + @njit def fixed_string_transform(column_inds, column_vals, column_offsets, col_idx, written_row_count, strlen, memory): @@ -2887,7 +2891,7 @@ def fixed_string_transform(column_inds, column_vals, column_offsets, col_idx, wr for i in range(written_row_count): a = i * strlen start_idx = column_inds[col_idx, i] + col_offset - end_idx = min(column_inds[col_idx, i + 1] + col_offset, start_idx + strlen) + end_idx = min(column_inds[col_idx, i+1] + col_offset, start_idx + strlen) for c in range(start_idx, end_idx): memory[a] = column_vals[c] - a += 1 \ No newline at end of file + a += 1 diff --git a/exetera/core/readerwriter.py b/exetera/core/readerwriter.py index 462c840f..4cb9c8bd 100644 --- a/exetera/core/readerwriter.py +++ b/exetera/core/readerwriter.py @@ -9,7 +9,6 @@ from exetera.core import operations as ops from exetera.core.utils import Timer - class Reader: def __init__(self, field): self.field = field @@ -27,6 +26,7 @@ def __init__(self, datastore, field): raise ValueError(error.format(field, fieldtype)) self.chunksize = field.attrs['chunksize'] self.datastore = datastore + def __getitem__(self, item): try: @@ -35,15 +35,15 @@ def __getitem__(self, item): start = item.start if item.start is not None else 0 stop = item.stop if item.stop is not None else len(self.field['index']) - 1 step = item.step - # TODO: validate slice - index = self.field['index'][start:stop + 1] + #TODO: validate slice + index = self.field['index'][start:stop+1] bytestr = self.field['values'][index[0]:index[-1]] - results = [None] * (len(index) - 1) + results = [None] * (len(index)-1) startindex = start for ir in range(len(results)): - results[ir] = \ - bytestr[index[ir] - np.int64(startindex): - index[ir + 1] - np.int64(startindex)].tobytes().decode() + results[ir] =\ + bytestr[index[ir]-np.int64(startindex): + index[ir+1]-np.int64(startindex)].tobytes().decode() return results except Exception as e: print("{}: unexpected exception {}".format(self.field.name, e)) @@ -62,7 +62,7 @@ def dtype(self): def sort(self, index, writer): field_index = self.field['index'][:] field_values = self.field['values'][:] - r_field_index, r_field_values = \ + r_field_index, r_field_values =\ pers._apply_sort_to_index_values(index, field_index, field_values) writer.write_raw(r_field_index, r_field_values) @@ -300,6 +300,7 @@ def write_part(self, values): if char_index > 0: DataWriter.write(self.field, 'values', chars, char_index) + def flush(self): # if self.value_index != 0 or 'values' not in self.field: # DataWriter.write(self.field, 'values', self.values, self.value_index) @@ -322,7 +323,7 @@ def write_part_raw(self, index, values): raise ValueError(f"'index' must be an ndarray of '{np.int64}'") if values.dtype not in (np.uint8, 'S1'): raise ValueError(f"'values' must be an ndarray of '{np.uint8}' or 'S1'") - DataWriter.write(self.field, 'index', index[1:], len(index) - 1) + DataWriter.write(self.field, 'index', index[1:], len(index)-1) DataWriter.write(self.field, 'values', values, len(values)) def write_raw(self, index, values): @@ -385,17 +386,15 @@ def write(self, values): def import_part(self, column_inds, column_vals, column_offsets, col_idx, written_row_count): cat_keys, cat_index, cat_values = self.byte_map - chunk = np.zeros(written_row_count, - dtype=np.int8) # use np.int8 instead of np.uint8, as we set -1 for leaky key - freetext_indices_chunk = np.zeros(written_row_count + 1, dtype=np.int64) + chunk = np.zeros(written_row_count, dtype=np.int8) # use np.int8 instead of np.uint8, as we set -1 for leaky key + freetext_indices_chunk = np.zeros(written_row_count + 1, dtype = np.int64) col_count = column_offsets[col_idx + 1] - column_offsets[col_idx] - freetext_values_chunk = np.zeros(np.int64(col_count), dtype=np.uint8) + freetext_values_chunk = np.zeros(np.int64(col_count), dtype = np.uint8) - ops.leaky_categorical_transform(chunk, freetext_indices_chunk, freetext_values_chunk, col_idx, column_inds, - column_vals, column_offsets, cat_keys, cat_index, cat_values) + ops.leaky_categorical_transform(chunk, freetext_indices_chunk, freetext_values_chunk, col_idx, column_inds, column_vals, column_offsets, cat_keys, cat_index, cat_values) - freetext_indices = freetext_indices_chunk + self.freetext_index_accumulated # broadcast + freetext_indices = freetext_indices_chunk + self.freetext_index_accumulated # broadcast self.freetext_index_accumulated += freetext_indices_chunk[written_row_count] freetext_values = freetext_values_chunk[:freetext_indices_chunk[written_row_count]] self.writer.write_part(chunk) @@ -439,9 +438,8 @@ def write_strings(self, values): def import_part(self, column_inds, column_vals, column_offsets, col_idx, written_row_count): chunk = np.zeros(written_row_count, dtype=np.uint8) cat_keys, cat_index, cat_values = self.byte_map - - ops.categorical_transform(chunk, col_idx, column_inds, column_vals, column_offsets, cat_keys, cat_index, - cat_values) + + ops.categorical_transform(chunk, col_idx, column_inds, column_vals, column_offsets, cat_keys, cat_index, cat_values) self.writer.write_part(chunk) @@ -499,7 +497,7 @@ def __init__(self, datastore, group, name, nformat, parser, invalid_value=0, self.flag_writer = None if create_flag_field: self.flag_writer = NumericWriter(datastore, group, f"{name}{flag_field_suffix}", - 'bool', timestamp, write_mode) + 'bool', timestamp, write_mode) self.field_name = name self.parser = parser self.invalid_value = invalid_value @@ -513,7 +511,7 @@ def write_part(self, values): Given a list of strings, parse the strings and write the parsed values. Values that cannot be parsed are written out as zero for the values, and zero for the flags to indicate that that entry is not valid. - + :param values: a list of strings to be parsed """ elements = np.zeros(len(values), dtype=self.data_writer.nformat) @@ -522,22 +520,21 @@ def write_part(self, values): valid, value = self.parser(values[i], self.invalid_value) elements[i] = value validity[i] = valid - - if self.validation_mode == 'strict' and not valid: + + if self.validation_mode == 'strict' and not valid: if self._is_blank(values[i]): - raise ValueError(f"Numeric value in the field '{self.field_name}' can not be empty in strict mode") - else: - raise ValueError( - f"The following numeric value in the field '{self.field_name}' can not be parsed:{values[i].strip()}") + raise ValueError(f"Numeric value in the field '{self.field_name}' can not be empty in strict mode") + else: + raise ValueError(f"The following numeric value in the field '{self.field_name}' can not be parsed:{values[i].strip()}") if self.validation_mode == 'allow_empty' and not self._is_blank(values[i]) and not valid: - raise ValueError( - f"The following numeric value in the field '{self.field_name}' can not be parsed:{values[i]}") + raise ValueError(f"The following numeric value in the field '{self.field_name}' can not be parsed:{values[i]}") self.data_writer.write_part(elements) if self.flag_writer is not None: self.flag_writer.write_part(validity) + def import_part(self, column_inds, column_vals, column_offsets, col_idx, written_row_count): # elements = np.zeros(written_row_count, dtype=self.data_writer.nformat) # validity = np.ones(written_row_count, dtype=bool) @@ -553,7 +550,7 @@ def import_part(self, column_inds, column_vals, column_offsets, col_idx, written written_row_count, self.invalid_value, self.validation_mode, np.frombuffer(bytes(self.field_name, "utf-8"), dtype=np.uint8) ) - elif self.data_writer.nformat in ('int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64'): + elif self.data_writer.nformat in ('int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64') : exception_message, exception_args = 0, [] elements, validity = ops.transform_int( @@ -574,6 +571,7 @@ def import_part(self, column_inds, column_vals, column_offsets, col_idx, written if self.flag_writer is not None: self.flag_writer.write_part(validity) + def _is_blank(self, value): return (isinstance(value, str) and value.strip() == '') or value == b'' @@ -643,10 +641,10 @@ def chunk_factory(self, length): def write_part(self, values): DataWriter.write(self.field, 'values', values, len(values)) - def import_part(self, column_inds, column_vals, column_offsets, col_idx, written_row_count): + def import_part(self, column_inds, column_vals, column_offsets, col_idx, written_row_count): values = np.zeros(written_row_count, dtype='S{}'.format(self.strlen)) ops.fixed_string_transform(column_inds, column_vals, column_offsets, col_idx, - written_row_count, self.strlen, values.data.cast('b')) + written_row_count, self.strlen, values.data.cast('b')) self.write_part(values) def flush(self): @@ -673,7 +671,7 @@ def __init__(self, datastore, group, name, create_day_field=False, self.create_day_field = create_day_field if create_day_field: self.datestr = FixedStringWriter(datastore, group, f"{name}_day", - '10', timestamp, write_mode) + '10', timestamp, write_mode) self.datetimeset = None if optional: self.datetimeset = NumericWriter(datastore, group, f"{name}_set", @@ -687,11 +685,11 @@ def write_part(self, values): self.datetime.write_part(values) if self.create_day_field: - days = self._get_days(values) + days=self._get_days(values) self.datestr.write_part(days) if self.datetimeset is not None: - flags = self._get_flags(values) + flags=self._get_flags(values) self.datetimeset.write_part(flags) def _get_days(self, values): @@ -857,10 +855,10 @@ def __init__(self, datastore, group, name, create_day_field=False, self.create_day_field = create_day_field if create_day_field: self.datestr = FixedStringWriter(datastore, group, f"{name}_day", - '10', timestamp, write_mode) + '10', timestamp, write_mode) self.dateset = None if optional: - self.dateset = \ + self.dateset =\ NumericWriter(datastore, group, f"{name}_set", 'bool', timestamp, write_mode) def chunk_factory(self, length): @@ -906,3 +904,4 @@ def write(self, values): + \ No newline at end of file diff --git a/exetera/core/session.py b/exetera/core/session.py index 9770dc96..32cc2e53 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -14,9 +14,10 @@ import uuid from datetime import datetime, timezone import time - +import warnings import numpy as np import pandas as pd + import h5py from exetera.core.abstract_types import Field, AbstractSession @@ -379,7 +380,16 @@ def get_spans(self, field: Union[Field, np.ndarray] = None, :return: The resulting set of spans as a numpy array """ + fields = [] result = None + if len(kwargs) > 0: + for k in kwargs.keys(): + if k == 'field': + field = kwargs[k] + elif k == 'fields': + fields = kwargs[k] + elif k == 'dest': + dest = kwargs[k] if dest is not None and not isinstance(dest, Field): raise TypeError(f"'dest' must be one of 'Field' but is {type(dest)}") diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 9e928b6b..54736fde 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -4,7 +4,6 @@ import numpy as np import tempfile import os -from datetime import datetime from exetera.core import session from exetera.core import fields @@ -68,7 +67,7 @@ def test_dataframe_create_numeric(self): values = np.random.randint(low=0, high=1000000, size=100000000) dst = s.open_dataset(bio, 'r+', 'dst') df = dst.create_dataframe('dst') - a = df.create_numeric('a', 'int32') + a = df.create_numeric('a','int32') a.data.write(values) total = np.sum(a.data[:]) @@ -86,7 +85,7 @@ def test_dataframe_create_categorical(self): dst = s.open_dataset(bio, 'r+', 'dst') hf = dst.create_dataframe('dst') a = hf.create_categorical('a', 'int8', - {'foo': 0, 'bar': 1, 'boo': 2}) + {'foo': 0, 'bar': 1, 'boo': 2}) a.data.write(values) total = np.sum(a.data[:]) @@ -111,6 +110,7 @@ def test_dataframe_create_fixed_string(self): [b'xxxy', b'xxy', b'xxxy', b'y', b'xy', b'y', b'xxxy', b'xxxy', b'xy', b'y'], a.data[:10].tolist()) + def test_dataframe_create_indexed_string(self): bio = BytesIO() with session.Session() as s: @@ -139,6 +139,7 @@ def test_dataframe_create_indexed_string(self): self.assertListEqual( ['xxxy', 'xxy', 'xxxy', 'y', 'xy', 'y', 'xxxy', 'xxxy', 'xy', 'y'], a.data[:10]) + def test_dataframe_create_mem_numeric(self): bio = BytesIO() with session.Session() as s: @@ -167,15 +168,16 @@ def test_dataframe_create_mem_numeric(self): df['num10'] = df['num'] % df['num2'] self.assertEqual([0, 0, 0, 0], df['num10'].data[:].tolist()) + def test_dataframe_create_mem_categorical(self): bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, 'r+', 'dst') df = dst.create_dataframe('dst') - cat1 = df.create_categorical('cat1', 'uint8', {'foo': 0, 'bar': 1, 'boo': 2}) + cat1 = df.create_categorical('cat1','uint8',{'foo': 0, 'bar': 1, 'boo': 2}) cat1.data.write([0, 1, 2, 0, 1, 2]) - cat2 = df.create_categorical('cat2', 'uint8', {'foo': 0, 'bar': 1, 'boo': 2}) + cat2 = df.create_categorical('cat2','uint8',{'foo': 0, 'bar': 1, 'boo': 2}) cat2.data.write([1, 2, 0, 1, 2, 0]) df['r1'] = cat1 < cat2 @@ -200,7 +202,7 @@ def test_dataframe_static_methods(self): numf.data.write([5, 4, 3, 2, 1]) df2 = dst.create_dataframe('df2') - dataframe.copy(numf, df2, 'numf') + dataframe.copy(numf, df2,'numf') self.assertListEqual([5, 4, 3, 2, 1], df2['numf'].data[:].tolist()) df.drop('numf') self.assertTrue('numf' not in df) @@ -235,6 +237,7 @@ def test_dataframe_ops(self): class TestDataFrameRename(unittest.TestCase): def test_rename_1(self): + a = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype='int32') b = np.array([8, 7, 6, 5, 4, 3, 2, 1], dtype='int32') @@ -295,10 +298,10 @@ def test_rename_should_clash(self): self.assertEqual('fb', df['fb'].name) self.assertEqual('fc', df['fc'].name) - class TestDataFrameCopyMove(unittest.TestCase): def test_move_same_dataframe(self): + sa = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype='int32') sb = np.array([8, 7, 6, 5, 4, 3, 2, 1], dtype='int32') @@ -314,6 +317,7 @@ def test_move_same_dataframe(self): self.assertEqual('fb', df1['fb'].name) def test_move_different_dataframe(self): + sa = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype='int32') sb = np.array([8, 7, 6, 5, 4, 3, 2, 1], dtype='int32') @@ -337,6 +341,7 @@ def test_move_different_dataframe(self): class TestDataFrameApplyFilter(unittest.TestCase): def test_apply_filter(self): + src = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype='int32') filt = np.array([0, 1, 0, 1, 0, 1, 1, 0], dtype='bool') expected = src[filt].tolist() @@ -360,6 +365,7 @@ def test_apply_filter(self): class TestDataFrameMerge(unittest.TestCase): def tests_merge_left(self): + l_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') r_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') r_vals = ['bb1', 'ccc1', '', 'dddd1', 'ggggggg1', 'ffffff1', 'bb2', '', 'ccc2'] @@ -380,7 +386,9 @@ def tests_merge_left(self): np.logical_not(ddf['valid_r'].data[:]) self.assertTrue(np.all(valid_if_equal)) + def tests_merge_right(self): + r_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') l_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') l_vals = ['bb1', 'ccc1', '', 'dddd1', 'ggggggg1', 'ffffff1', 'bb2', '', 'ccc2'] @@ -402,6 +410,7 @@ def tests_merge_right(self): self.assertTrue(np.all(valid_if_equal)) def tests_merge_inner(self): + r_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') l_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') r_vals = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven'] @@ -424,6 +433,7 @@ def tests_merge_inner(self): self.assertEqual(expected_right, ddf['r_vals'].data[:]) def tests_merge_outer(self): + r_id = np.asarray([0, 1, 2, 3, 4, 5, 6, 7], dtype='int32') l_id = np.asarray([2, 3, 0, 4, 7, 6, 2, 0, 3], dtype='int32') r_vals = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven'] @@ -447,7 +457,9 @@ def tests_merge_outer(self): self.assertEqual(expected_left, ddf['l_vals'].data[:]) self.assertEqual(expected_right, ddf['r_vals'].data[:]) + def tests_merge_left_compound_key(self): + l_id_1 = np.asarray([0, 0, 0, 0, 1, 1, 1, 1], dtype='int32') l_id_2 = np.asarray([0, 1, 2, 3, 0, 1, 2, 3], dtype='int32') r_id_1 = np.asarray([0, 1, 0, 1, 0, 1, 0, 1], dtype='int32') @@ -475,11 +487,12 @@ def tests_merge_left_compound_key(self): self.assertEqual(ddf['l_id_2'].data[:].tolist(), ddf['r_id_2'].data[:].tolist()) + class TestDataFrameGroupBy(unittest.TestCase): def test_distinct_single_field(self): val = np.asarray([1, 0, 1, 2, 3, 2, 2, 3, 3, 3], dtype=np.int32) - val2 = np.asarray(['a', 'b', 'a', 'b', 'c', 'b', 'c', 'c', 'd', 'd'], dtype='S1') + val2 = np.asarray(['a', 'b', 'a', 'b', 'c', 'b', 'c', 'c', 'd', 'd'], dtype = 'S1') bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, "w", "src") @@ -489,13 +502,14 @@ def test_distinct_single_field(self): ddf = dst.create_dataframe('ddf') - df.drop_duplicates(by='val', ddf=ddf) + df.drop_duplicates(by = 'val', ddf = ddf) - self.assertListEqual([0, 1, 2, 3], ddf['val'].data[:].tolist()) + self.assertListEqual([0, 1, 2, 3], ddf['val'].data[:].tolist()) + def test_distinct_multi_fields(self): val = np.asarray([1, 0, 1, 2, 3, 2, 2, 3, 3, 3], dtype=np.int32) - val2 = np.asarray(['a', 'b', 'a', 'b', 'c', 'b', 'c', 'c', 'd', 'd'], dtype='S1') + val2 = np.asarray(['a', 'b', 'a', 'b', 'c', 'b', 'c', 'c', 'd', 'd'], dtype = 'S1') bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, "w", "src") @@ -505,14 +519,15 @@ def test_distinct_multi_fields(self): ddf = dst.create_dataframe('ddf') - df.drop_duplicates(by=['val', 'val2'], ddf=ddf) + df.drop_duplicates(by = ['val', 'val2'], ddf = ddf) + + self.assertListEqual([0, 1, 2, 2, 3, 3], ddf['val'].data[:].tolist()) + self.assertListEqual([b'b', b'a', b'b', b'c', b'c', b'd'], ddf['val2'].data[:].tolist()) - self.assertListEqual([0, 1, 2, 2, 3, 3], ddf['val'].data[:].tolist()) - self.assertListEqual([b'b', b'a', b'b', b'c', b'c', b'd'], ddf['val2'].data[:].tolist()) def test_groupby_count_single_field(self): val = np.asarray([1, 0, 1, 2, 3, 2, 2, 3, 3, 3], dtype=np.int32) - val2 = np.asarray(['a', 'b', 'a', 'b', 'c', 'b', 'c', 'c', 'd', 'd'], dtype='S1') + val2 = np.asarray(['a', 'b', 'a', 'b', 'c', 'b', 'c', 'c', 'd', 'd'], dtype = 'S1') bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, "w", "src") @@ -522,14 +537,15 @@ def test_groupby_count_single_field(self): ddf = dst.create_dataframe('ddf') - df.groupby(by='val').count(ddf=ddf) + df.groupby(by = 'val').count(ddf = ddf) - self.assertListEqual([0, 1, 2, 3], ddf['val'].data[:].tolist()) - self.assertListEqual([1, 2, 3, 4], ddf['count'].data[:].tolist()) + self.assertListEqual([0, 1, 2, 3], ddf['val'].data[:].tolist()) + self.assertListEqual([1, 2, 3, 4], ddf['count'].data[:].tolist()) + def test_groupby_count_multi_fields(self): val = np.asarray([1, 0, 1, 2, 3, 2, 2, 3, 3, 3], dtype=np.int32) - val2 = np.asarray(['a', 'b', 'a', 'b', 'c', 'b', 'c', 'c', 'd', 'd'], dtype='S1') + val2 = np.asarray(['a', 'b', 'a', 'b', 'c', 'b', 'c', 'c', 'd', 'd'], dtype = 'S1') bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, "w", "src") @@ -539,12 +555,13 @@ def test_groupby_count_multi_fields(self): ddf = dst.create_dataframe('ddf') - df.groupby(by=['val', 'val2']).count(ddf=ddf) + df.groupby(by = ['val', 'val2']).count(ddf = ddf) - self.assertListEqual([0, 1, 2, 2, 3, 3], ddf['val'].data[:].tolist()) - self.assertListEqual([b'b', b'a', b'b', b'c', b'c', b'd'], ddf['val2'].data[:].tolist()) + self.assertListEqual([0, 1, 2, 2, 3, 3], ddf['val'].data[:].tolist()) + self.assertListEqual([b'b', b'a', b'b', b'c', b'c', b'd'], ddf['val2'].data[:].tolist()) self.assertListEqual([1, 2, 2, 1, 2, 2], ddf['count'].data[:].tolist()) + def test_groupby_max_single_field(self): val = np.asarray([3, 1, 1, 2, 2, 2, 3, 3, 3, 0], dtype=np.int32) val2 = np.asarray([9, 8, 2, 6, 4, 5, 3, 7, 1, 0], dtype=np.int64) @@ -557,14 +574,15 @@ def test_groupby_max_single_field(self): ddf = dst.create_dataframe('ddf') - df.groupby(by='val').max(target='val2', ddf=ddf) - + df.groupby(by = 'val').max(target ='val2', ddf = ddf) + self.assertListEqual([0, 1, 2, 3], ddf['val'].data[:].tolist()) - self.assertListEqual([0, 8, 6, 9], ddf['val2_max'].data[:].tolist()) + self.assertListEqual([0, 8, 6, 9], ddf['val2_max'].data[:].tolist()) + def test_groupby_max_multi_fields(self): val = np.asarray([1, 2, 1, 2], dtype=np.int32) - val2 = np.asarray(['a', 'c', 'a', 'b'], dtype='S1') + val2 = np.asarray(['a', 'c', 'a', 'b'], dtype = 'S1') val3 = np.asarray([3, 4, 5, 6]) val4 = np.asarray(['aa', 'ab', 'cd', 'def']) bio = BytesIO() @@ -578,12 +596,13 @@ def test_groupby_max_multi_fields(self): ddf = dst.create_dataframe('ddf') - df.groupby(by=['val', 'val2']).max(['val3', 'val4'], ddf=ddf) + df.groupby(by = ['val', 'val2']).max(['val3', 'val4'], ddf = ddf) + + self.assertListEqual([1, 2, 2], ddf['val'].data[:].tolist()) + self.assertListEqual([b'a', b'b', b'c'], ddf['val2'].data[:].tolist()) + self.assertListEqual([5, 6, 4], ddf['val3_max'].data[:].tolist()) + self.assertListEqual(['cd', 'def', 'ab'], ddf['val4_max'].data[:]) - self.assertListEqual([1, 2, 2], ddf['val'].data[:].tolist()) - self.assertListEqual([b'a', b'b', b'c'], ddf['val2'].data[:].tolist()) - self.assertListEqual([5, 6, 4], ddf['val3_max'].data[:].tolist()) - self.assertListEqual(['cd', 'def', 'ab'], ddf['val4_max'].data[:]) def test_groupby_min_single_field(self): val = np.asarray([3, 1, 1, 2, 2, 2, 3, 3, 3, 0], dtype=np.int32) @@ -597,14 +616,15 @@ def test_groupby_min_single_field(self): ddf = dst.create_dataframe('ddf') - df.groupby(by='val').min(target='val2', ddf=ddf) - + df.groupby(by = 'val').min(target ='val2', ddf = ddf) + self.assertListEqual([0, 1, 2, 3], ddf['val'].data[:].tolist()) - self.assertListEqual([0, 2, 4, 1], ddf['val2_min'].data[:].tolist()) + self.assertListEqual([0, 2, 4, 1], ddf['val2_min'].data[:].tolist()) + def test_groupby_min_multi_fields(self): val = np.asarray([1, 2, 1, 2], dtype=np.int32) - val2 = np.asarray(['a', 'c', 'a', 'b'], dtype='S1') + val2 = np.asarray(['a', 'c', 'a', 'b'], dtype = 'S1') val3 = np.asarray([3, 4, 5, 6]) val4 = np.asarray(['aa', 'ab', 'cd', 'def']) bio = BytesIO() @@ -618,12 +638,13 @@ def test_groupby_min_multi_fields(self): ddf = dst.create_dataframe('ddf') - df.groupby(by=['val', 'val2']).min(['val3', 'val4'], ddf=ddf) + df.groupby(by = ['val', 'val2']).min(['val3', 'val4'], ddf = ddf) + + self.assertListEqual([1, 2, 2], ddf['val'].data[:].tolist()) + self.assertListEqual([b'a', b'b', b'c'], ddf['val2'].data[:].tolist()) + self.assertListEqual([3, 6, 4], ddf['val3_min'].data[:].tolist()) + self.assertListEqual(['aa', 'def', 'ab'], ddf['val4_min'].data[:]) - self.assertListEqual([1, 2, 2], ddf['val'].data[:].tolist()) - self.assertListEqual([b'a', b'b', b'c'], ddf['val2'].data[:].tolist()) - self.assertListEqual([3, 6, 4], ddf['val3_min'].data[:].tolist()) - self.assertListEqual(['aa', 'def', 'ab'], ddf['val4_min'].data[:]) def test_groupby_first_single_field(self): val = np.asarray([3, 1, 1, 2, 2, 2, 3, 3, 3, 0], dtype=np.int32) @@ -637,10 +658,11 @@ def test_groupby_first_single_field(self): ddf = dst.create_dataframe('ddf') - df.groupby(by='val').first(target='val2', ddf=ddf) - + df.groupby(by = 'val').first(target ='val2', ddf = ddf) + self.assertListEqual([0, 1, 2, 3], ddf['val'].data[:].tolist()) - self.assertListEqual([0, 8, 6, 9], ddf['val2_first'].data[:].tolist()) + self.assertListEqual([0, 8, 6, 9], ddf['val2_first'].data[:].tolist()) + def test_groupby_last_single_field(self): val = np.asarray([3, 1, 1, 2, 2, 2, 3, 3, 3, 0], dtype=np.int32) @@ -654,14 +676,15 @@ def test_groupby_last_single_field(self): ddf = dst.create_dataframe('ddf') - df.groupby(by='val').last(target='val2', ddf=ddf) - + df.groupby(by = 'val').last(target ='val2', ddf = ddf) + self.assertListEqual([0, 1, 2, 3], ddf['val'].data[:].tolist()) - self.assertListEqual([0, 2, 5, 1], ddf['val2_last'].data[:].tolist()) + self.assertListEqual([0, 2, 5, 1], ddf['val2_last'].data[:].tolist()) + def test_groupby_sorted_field(self): - val = np.asarray([0, 0, 0, 1, 1, 1, 3], dtype=np.int32) - val2 = np.asarray(['a', 'b', 'b', 'c', 'd', 'd', 'f'], dtype='S1') + val = np.asarray([0,0,0,1,1,1,3], dtype=np.int32) + val2 = np.asarray(['a','b','b','c','d','d','f'], dtype='S1') bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, "w", "src") @@ -670,16 +693,17 @@ def test_groupby_sorted_field(self): df.create_fixed_string("val2", 1).data.write(val2) ddf = dst.create_dataframe('ddf') - df.groupby(by='val').min(target='val2', ddf=ddf) - df.groupby(by='val').first(target='val2', ddf=ddf, write_keys=False) + df.groupby(by = 'val').min(target ='val2', ddf = ddf) + df.groupby(by = 'val').first(target ='val2', ddf = ddf, write_keys=False) self.assertListEqual([0, 1, 3], ddf['val'].data[:].tolist()) self.assertListEqual([b'a', b'c', b'f'], ddf['val2_min'].data[:].tolist()) self.assertListEqual([b'a', b'c', b'f'], ddf['val2_first'].data[:].tolist()) + def test_groupby_with_hint_keys_is_sorted(self): - val = np.asarray([0, 0, 0, 1, 1, 1, 3], dtype=np.int32) - val2 = np.asarray(['a', 'b', 'b', 'c', 'd', 'd', 'f'], dtype='S1') + val = np.asarray([0,0,0,1,1,1,3], dtype=np.int32) + val2 = np.asarray(['a','b','b','c','d','d','f'], dtype='S1') bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio, "w", "src") @@ -688,8 +712,8 @@ def test_groupby_with_hint_keys_is_sorted(self): df.create_fixed_string("val2", 1).data.write(val2) ddf = dst.create_dataframe('ddf') - df.groupby(by='val', hint_keys_is_sorted=True).max(target='val2', ddf=ddf) - df.groupby(by='val', hint_keys_is_sorted=True).last(target='val2', ddf=ddf, write_keys=False) + df.groupby(by = 'val', hint_keys_is_sorted=True).max(target ='val2', ddf = ddf) + df.groupby(by = 'val', hint_keys_is_sorted=True).last(target ='val2', ddf = ddf, write_keys=False) self.assertListEqual([0, 1, 3], ddf['val'].data[:].tolist()) self.assertListEqual([b'b', b'd', b'f'], ddf['val2_max'].data[:].tolist()) @@ -711,12 +735,13 @@ def test_sort_values_on_original_df(self): df.create_numeric("val", "int32").data.write(val) df.create_indexed_string("val2").data.write(val2) - df.sort_values(by='idx') + df.sort_values(by = 'idx') self.assertListEqual([b'a', b'b', b'c', b'd', b'e'], df['idx'].data[:].tolist()) self.assertListEqual([10, 30, 50, 40, 20], df['val'].data[:].tolist()) self.assertListEqual(['a', 'bbb', 'ccccc', 'dddd', 'ee'], df['val2'].data[:]) + def test_sort_values_on_other_df(self): idx = np.asarray([b'a', b'e', b'b', b'd', b'c'], dtype='S1') val = np.asarray([10, 20, 30, 40, 50], dtype=np.int32) @@ -732,7 +757,7 @@ def test_sort_values_on_other_df(self): ddf = dst.create_dataframe('ddf') - df.sort_values(by='idx', ddf=ddf) + df.sort_values(by = 'idx', ddf = ddf) self.assertListEqual(list(idx), df['idx'].data[:].tolist()) self.assertListEqual(list(val), df['val'].data[:].tolist()) @@ -742,6 +767,7 @@ def test_sort_values_on_other_df(self): self.assertListEqual([10, 30, 50, 40, 20], ddf['val'].data[:].tolist()) self.assertListEqual(['a', 'bbb', 'ccccc', 'dddd', 'ee'], ddf['val2'].data[:]) + def test_sort_values_on_inconsistent_length_df(self): idx = np.asarray([b'a', b'e', b'b', b'd', b'c'], dtype='S1') val = np.asarray([10, 20, 30, 40], dtype=np.int32) @@ -756,10 +782,10 @@ def test_sort_values_on_inconsistent_length_df(self): df.create_indexed_string("val2").data.write(val2) with self.assertRaises(ValueError) as context: - df.sort_values(by='idx') + df.sort_values(by = 'idx') + + self.assertEqual(str(context.exception), "There are consistent lengths in dataframe 'ds'. The following length were observed: {4, 5}") - self.assertEqual(str(context.exception), - "There are consistent lengths in dataframe 'ds'. The following length were observed: {4, 5}") def test_sort_values_on_invalid_input(self): idx = np.asarray([b'a', b'e', b'b', b'd', b'c'], dtype='S1') @@ -768,21 +794,21 @@ def test_sort_values_on_invalid_input(self): dst = s.open_dataset(bio, "w", "src") df = dst.create_dataframe('ds') df.create_fixed_string("idx", 1).data.write(idx) - + with self.assertRaises(ValueError) as context: - df.sort_values(by='idx', axis=1) - - self.assertEqual(str(context.exception), "Currently sort_values() only supports axis = 0") + df.sort_values(by = 'idx', axis=1) + + self.assertEqual(str(context.exception), "Currently sort_values() only supports axis = 0") with self.assertRaises(ValueError) as context: - df.sort_values(by='idx', ascending=False) - - self.assertEqual(str(context.exception), "Currently sort_values() only supports ascending = True") - + df.sort_values(by = 'idx', ascending=False) + + self.assertEqual(str(context.exception), "Currently sort_values() only supports ascending = True") + with self.assertRaises(ValueError) as context: - df.sort_values(by='idx', kind='quicksort') - - self.assertEqual(str(context.exception), "Currently sort_values() only supports kind='stable'") + df.sort_values(by = 'idx', kind='quicksort') + + self.assertEqual(str(context.exception), "Currently sort_values() only supports kind='stable'") class TestDataFrameToCSV(unittest.TestCase): @@ -806,6 +832,7 @@ def test_to_csv_file(self): os.close(fd_csv) + def test_to_csv_small_chunk_row_size(self): val1 = np.asarray([0, 1, 2, 3], dtype='int32') val2 = ['zero', 'one', 'two', 'three'] @@ -823,7 +850,8 @@ def test_to_csv_small_chunk_row_size(self): with open(csv_file_name, 'r') as f: self.assertEqual(f.readlines(), ['val1,val2\n', '0,zero\n', '1,one\n', '2,two\n', '3,three\n']) - os.close(fd_csv) + os.close(fd_csv) + def test_to_csv_with_column_filter(self): val1 = np.asarray([0, 1, 2, 3], dtype='int32') @@ -842,7 +870,8 @@ def test_to_csv_with_column_filter(self): with open(csv_file_name, 'r') as f: self.assertEqual(f.readlines(), ['val1\n', '0\n', '1\n', '2\n', '3\n']) - os.close(fd_csv) + os.close(fd_csv) + def test_to_csv_with_row_filter_field(self): val1 = np.asarray([0, 1, 2, 3], dtype='int32') @@ -861,242 +890,5 @@ def test_to_csv_with_row_filter_field(self): with open(csv_file_name, 'r') as f: self.assertEqual(f.readlines(), ['val1\n', '0\n', '2\n']) - os.close(fd_csv) - - -class TestDataFrameDescribe(unittest.TestCase): - - def test_describe_default(self): - bio = BytesIO() - with session.Session() as s: - dst = s.open_dataset(bio, 'w', 'dst') - df = dst.create_dataframe('df') - df.create_numeric('num', 'int32').data.write([i for i in range(10)]) - df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) - df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) - df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) - df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) - result = df.describe() - expected = {'fields': ['num', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], - 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], - '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], - 'max': ['9.00', '1632234147.00']} - self.assertEqual(result, expected) - - def test_describe_include(self): - bio = BytesIO() - with session.Session() as s: - dst = s.open_dataset(bio, 'w', 'dst') - df = dst.create_dataframe('df') - df.create_numeric('num', 'int32').data.write([i for i in range(10)]) - df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) - df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) - df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) - df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) - - result = df.describe(include='all') - expected = {'fields': ['num', 'fs1', 'ts1', 'c1', 'is1'], 'count': [10, 20, 20, 20, 20], - 'mean': ['4.50', 'NaN', '1632234137.50', 'NaN', 'NaN'], 'std': ['2.87', 'NaN', '5.77', 'NaN', 'NaN'], - 'min': ['0.00', 'NaN', '1632234128.00', 'NaN', 'NaN'], '25%': ['0.02', 'NaN', '1632234128.05', 'NaN', 'NaN'], - '50%': ['0.04', 'NaN', '1632234128.10', 'NaN', 'NaN'], '75%': ['0.07', 'NaN', '1632234128.14', 'NaN', 'NaN'], - 'max': ['9.00', 'NaN', '1632234147.00', 'NaN', 'NaN'], 'unique': ['NaN', 1, 'NaN', 1, 1], - 'top': ['NaN', b'a', 'NaN', 1, 'abc'], 'freq': ['NaN', 20, 'NaN', 20, 20]} - self.assertEqual(result, expected) - - result = df.describe(include='num') - expected = {'fields': ['num'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], - '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} - self.assertEqual(result, expected) - - result = df.describe(include=['num', 'fs1']) - expected = {'fields': ['num', 'fs1'], 'count': [10, 20], 'mean': ['4.50', 'NaN'], 'std': ['2.87', 'NaN'], - 'min': ['0.00', 'NaN'], '25%': ['0.02', 'NaN'], '50%': ['0.04', 'NaN'], '75%': ['0.07', 'NaN'], - 'max': ['9.00', 'NaN'], 'unique': ['NaN', 1], 'top': ['NaN', b'a'], 'freq': ['NaN', 20]} - self.assertEqual(result, expected) - - result = df.describe(include=np.int32) - expected = {'fields': ['num', 'c1'], 'count': [10, 20], 'mean': ['4.50', 'NaN'], 'std': ['2.87', 'NaN'], - 'min': ['0.00', 'NaN'], '25%': ['0.02', 'NaN'], '50%': ['0.04', 'NaN'], '75%': ['0.07', 'NaN'], - 'max': ['9.00', 'NaN'], 'unique': ['NaN', 1], 'top': ['NaN', 1], 'freq': ['NaN', 20]} - self.assertEqual(result, expected) - - result = df.describe(include=[np.int32, np.bytes_]) - expected = {'fields': ['num', 'c1', 'fs1'], 'count': [10, 20, 20], 'mean': ['4.50', 'NaN', 'NaN'], - 'std': ['2.87', 'NaN', 'NaN'], 'min': ['0.00', 'NaN', 'NaN'], '25%': ['0.02', 'NaN', 'NaN'], - '50%': ['0.04', 'NaN', 'NaN'], '75%': ['0.07', 'NaN', 'NaN'], 'max': ['9.00', 'NaN', 'NaN'], - 'unique': ['NaN', 1, 1], 'top': ['NaN', 1, b'a'], 'freq': ['NaN', 20, 20]} - self.assertEqual(result, expected) - - - def test_describe_exclude(self): - bio = BytesIO() - with session.Session() as s: - src = s.open_dataset(bio, 'w', 'src') - df = src.create_dataframe('df') - df.create_numeric('num', 'int32').data.write([i for i in range(10)]) - df.create_numeric('num2', 'int64').data.write([i for i in range(10)]) - df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) - df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) - df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) - df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) - - result = df.describe(exclude='num') - expected = {'fields': ['num2', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], - 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], - '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], - 'max': ['9.00', '1632234147.00']} - self.assertEqual(result, expected) - - result = df.describe(exclude=['num', 'num2']) - expected = {'fields': ['ts1'], 'count': [20], 'mean': ['1632234137.50'], 'std': ['5.77'], - 'min': ['1632234128.00'], '25%': ['1632234128.05'], '50%': ['1632234128.10'], - '75%': ['1632234128.14'], 'max': ['1632234147.00']} - self.assertEqual(result, expected) - - result = df.describe(exclude=np.int32) - expected = {'fields': ['num2', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], - 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], - '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], - 'max': ['9.00', '1632234147.00']} - self.assertEqual(result, expected) - - result = df.describe(exclude=[np.int32, np.float64]) - expected = {'fields': ['num2'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], - '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} - self.assertEqual(result, expected) - - def test_describe_include_and_exclude(self): - bio = BytesIO() - with session.Session() as s: - src = s.open_dataset(bio, 'w', 'src') - df = src.create_dataframe('df') - df.create_numeric('num', 'int32').data.write([i for i in range(10)]) - df.create_numeric('num2', 'int64').data.write([i for i in range(10)]) - df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) - df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) - df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) - df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) - - #str * - with self.assertRaises(Exception) as context: - df.describe(include='num', exclude='num') - self.assertTrue(isinstance(context.exception, ValueError)) - - # list of str , str - with self.assertRaises(Exception) as context: - df.describe(include=['num', 'num2'], exclude='num') - self.assertTrue(isinstance(context.exception, ValueError)) - # list of str , type - result = df.describe(include=['num', 'num2'], exclude=np.int32) - expected = {'fields': ['num2'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], - '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} - self.assertEqual(result, expected) - # list of str , list of str - with self.assertRaises(Exception) as context: - df.describe(include=['num', 'num2'], exclude=['num', 'num2']) - self.assertTrue(isinstance(context.exception, ValueError)) - # list of str , list of type - result = df.describe(include=['num', 'num2', 'ts1'], exclude=[np.int32, np.int64]) - expected = {'fields': ['ts1'], 'count': [20], 'mean': ['1632234137.50'], 'std': ['5.77'], - 'min': ['1632234128.00'], '25%': ['1632234128.05'], '50%': ['1632234128.10'], - '75%': ['1632234128.14'], 'max': ['1632234147.00']} - self.assertEqual(result, expected) - - # type, str - result = df.describe(include=np.number, exclude='num2') - expected = {'fields': ['num', 'ts1', 'c1'], 'count': [10, 20, 20], 'mean': ['4.50', '1632234137.50', 'NaN'], - 'std': ['2.87', '5.77', 'NaN'], 'min': ['0.00', '1632234128.00', 'NaN'], - '25%': ['0.02', '1632234128.05', 'NaN'], '50%': ['0.04', '1632234128.10', 'NaN'], - '75%': ['0.07', '1632234128.14', 'NaN'], 'max': ['9.00', '1632234147.00', 'NaN'], - 'unique': ['NaN', 'NaN', 1], 'top': ['NaN', 'NaN', 1], 'freq': ['NaN', 'NaN', 20]} - self.assertEqual(result, expected) - # type, type - with self.assertRaises(Exception) as context: - df.describe(include=np.int32, exclude=np.int64) - self.assertTrue(isinstance(context.exception, ValueError)) - # type, list of str - result = df.describe(include=np.number, exclude=['num', 'num2']) - expected = {'fields': ['ts1', 'c1'], 'count': [20, 20], 'mean': ['1632234137.50', 'NaN'], - 'std': ['5.77', 'NaN'], 'min': ['1632234128.00', 'NaN'], '25%': ['1632234128.05', 'NaN'], - '50%': ['1632234128.10', 'NaN'], '75%': ['1632234128.14', 'NaN'], 'max': ['1632234147.00', 'NaN'], - 'unique': ['NaN', 1], 'top': ['NaN', 1], 'freq': ['NaN', 20]} - self.assertEqual(result, expected) - # type, list of type - with self.assertRaises(Exception) as context: - df.describe(include=np.int32, exclude=[np.int64, np.float64]) - self.assertTrue(isinstance(context.exception, ValueError)) - - # list of type, str - result = df.describe(include=[np.int32, np.int64], exclude='num') - expected = {'fields': ['c1', 'num2'], 'count': [20, 10], 'mean': ['NaN', '4.50'], 'std': ['NaN', '2.87'], - 'min': ['NaN', '0.00'], '25%': ['NaN', '0.02'], '50%': ['NaN', '0.04'], '75%': ['NaN', '0.07'], - 'max': ['NaN', '9.00'], 'unique': [1, 'NaN'], 'top': [1, 'NaN'], 'freq': [20, 'NaN']} - self.assertEqual(result, expected) - # list of type, type - with self.assertRaises(Exception) as context: - df.describe(include=[np.int32, np.int64], exclude=np.int64) - self.assertTrue(isinstance(context.exception, ValueError)) - # list of type, list of str - result = df.describe(include=[np.int32, np.int64], exclude=['num', 'num2']) - expected = {'fields': ['c1'], 'count': [20], 'mean': ['NaN'], 'std': ['NaN'], 'min': ['NaN'], - '25%': ['NaN'], '50%': ['NaN'], '75%': ['NaN'], 'max': ['NaN'], 'unique': [1], 'top': [1], - 'freq': [20]} - self.assertEqual(result, expected) - # list of type, list of type - with self.assertRaises(Exception) as context: - df.describe(include=[np.int32, np.int64], exclude=[np.int32, np.int64]) - self.assertTrue(isinstance(context.exception, ValueError)) - - def test_raise_errors(self): - bio = BytesIO() - with session.Session() as s: - src = s.open_dataset(bio, 'w', 'src') - df = src.create_dataframe('df') - - df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) - df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) - df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) - - with self.assertRaises(Exception) as context: - df.describe(include='num3') - self.assertTrue(isinstance(context.exception, ValueError)) - - with self.assertRaises(Exception) as context: - df.describe(include=np.int8) - self.assertTrue(isinstance(context.exception, ValueError)) - - with self.assertRaises(Exception) as context: - df.describe(include=['num3', 'num4']) - self.assertTrue(isinstance(context.exception, ValueError)) - - with self.assertRaises(Exception) as context: - df.describe(include=[np.int8, np.uint]) - self.assertTrue(isinstance(context.exception, ValueError)) - - with self.assertRaises(Exception) as context: - df.describe(include=float('3.14159')) - self.assertTrue(isinstance(context.exception, ValueError)) - - with self.assertRaises(Exception) as context: - df.describe() - self.assertTrue(isinstance(context.exception, ValueError)) - - df.create_numeric('num', 'int32').data.write([i for i in range(10)]) - df.create_numeric('num2', 'int64').data.write([i for i in range(10)]) - df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) - - with self.assertRaises(Exception) as context: - df.describe(exclude=float('3.14159')) - self.assertTrue(isinstance(context.exception, ValueError)) - - with self.assertRaises(Exception) as context: - df.describe(exclude=['num', 'num2', 'ts1']) - self.assertTrue(isinstance(context.exception, ValueError)) - - - - - - - - + os.close(fd_csv) + \ No newline at end of file diff --git a/tests/test_dataset.py b/tests/test_dataset.py index cc765574..ab35a03f 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -152,4 +152,4 @@ def test_dataframe_create_with_dataframe(self): self.assertListEqual(tcontents2.tolist(), df2['t_foo'].data[:].tolist()) def test_dataset_ops(self): - pass \ No newline at end of file + pass diff --git a/tests/test_fields.py b/tests/test_fields.py index 160def50..c6709c62 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -28,21 +28,6 @@ def test_field_truthness(self): f = s.create_categorical(src, "d", "int8", {"no": 0, "yes": 1}) self.assertTrue(bool(f)) - def test_numeric_field_astype(self): - bio = BytesIO() - with session.Session() as s: - dst = s.open_dataset(bio, "w", "src") - df = dst.create_dataframe('df') - num = df.create_numeric('num', 'float32') - num.data.write([1.1, 2.1, 3.1, 4.1, 5.1, 6.1]) - self.assertTrue(type(num.data[0]) == np.float32) - num.astype('int8') - self.assertTrue(type(num.data[0]) == np.int8) - num.astype('uint16') - self.assertTrue(type(num.data[0]) == np.uint16) - num.astype(np.float32) - self.assertTrue(type(num.data[0]) == np.float32) - class TestFieldGetSpans(unittest.TestCase): @@ -362,35 +347,6 @@ def test_tuple(expected, actual): 'f3', fields.dtype_to_str(r.data.dtype)).data.write(r) test_simple(expected, df['f3']) - def _execute_unary_field_test(self, a1, function): - - def test_simple(expected, actual): - self.assertListEqual(expected.tolist(), actual.data[:].tolist()) - - def test_tuple(expected, actual): - self.assertListEqual(expected[0].tolist(), actual[0].data[:].tolist()) - self.assertListEqual(expected[1].tolist(), actual[1].data[:].tolist()) - - expected = function(a1) - - test_equal = test_tuple if isinstance(expected, tuple) else test_simple - - bio = BytesIO() - with session.Session() as s: - ds = s.open_dataset(bio, 'w', 'ds') - df = ds.create_dataframe('df') - - m1 = fields.NumericMemField(s, fields.dtype_to_str(a1.dtype)) - m1.data.write(a1) - - f1 = df.create_numeric('f1', fields.dtype_to_str(a1.dtype)) - f1.data.write(a1) - - # test memory field and field operations - test_equal(expected, function(f1)) - test_equal(expected, function(f1)) - test_equal(expected, function(m1)) - def test_mixed_field_add(self): a1 = np.array([1, 2, 3, 4], dtype=np.int32) @@ -461,20 +417,6 @@ def test_mixed_field_or(self): self._execute_memory_field_test(a1, a2, 1, lambda x, y: x | y) self._execute_field_test(a1, a2, 1, lambda x, y: x | y) - def test_mixed_field_invert(self): - a1 = np.array([0, 0, 1, 1], dtype=np.int32) - self._execute_unary_field_test(a1, lambda x: ~x) - - def test_logical_not(self): - a1 = np.array([0, 0, 1, 1], dtype=np.int32) - bio = BytesIO() - with session.Session() as s: - ds = s.open_dataset(bio, 'w', 'ds') - df = ds.create_dataframe('df') - num = df.create_numeric('num', 'uint32') - num.data.write(a1) - self.assertListEqual(np.logical_not(a1).tolist(), num.logical_not().data[:].tolist()) - def test_less_than(self): a1 = np.array([1, 2, 3, 4], dtype=np.int32) diff --git a/tests/test_operations.py b/tests/test_operations.py index e5f63625..c618166e 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -1239,33 +1239,6 @@ def test_is_ordered(self): arr = np.asarray([1, 1, 1, 1, 1]) self.assertTrue(ops.is_ordered(arr)) - def test_count(self): - bio = BytesIO() - with session.Session() as s: - dst = s.open_dataset(bio, 'w', 'dst') - df = dst.create_dataframe('df') - fld = df.create_numeric('num', 'int32') - fld.data.write([1, 1, 2, 3, 4, 5, 6, 3, 4, 1, 1, 2, 4, 2, 3, 4]) - dict = ops.count(fld) - self.assertEqual([1, 2, 3, 4, 5, 6], list(dict.keys())) - self.assertEqual([4, 3, 3, 4, 1, 1], list(dict.values())) - fld = df.create_fixed_string('fst', 1) - fld.data.write([b'a', b'c', b'd', b'b', b'a', b'a', b'd', b'c', b'a']) - dict = ops.count(fld) - self.assertEqual([b'a', b'b', b'c', b'd'], list(dict.keys())) - self.assertEqual([4, 1, 2, 2], list(dict.values())) - fld = df.create_indexed_string('ids') - fld.data.write(['cc', 'aa', 'bb', 'cc', 'cc', 'ddd', 'dd', 'ddd']) - dict = ops.count(fld) - self.assertEqual(['aa', 'bb', 'cc', 'dd', 'ddd'], list(dict.keys())) - self.assertEqual([1, 1, 3, 1, 2], list(dict.values())) - fld = df.create_categorical('cat', 'int8', {'a': 1, 'b': 2}) - fld.data.write([1, 1, 2, 2, 1, 1, 2, 2, 1, 2, 1, 2, 1]) - dict = ops.count(fld) - self.assertEqual(list(fld.keys.keys()), list(dict.keys())) - self.assertEqual([7, 6], list(dict.values())) - - class TestGetSpans(unittest.TestCase): def test_get_spans_two_field(self): From 001134c15afebe8339bc6d3b6d11f683e9371b2f Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 23 Sep 2021 09:04:05 +0100 Subject: [PATCH 113/181] Delete python-publish.yml --- .github/workflows/python-publish.yml | 36 ---------------------------- 1 file changed, 36 deletions(-) delete mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index 3bfabfc1..00000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,36 +0,0 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Upload Python Package - -on: - release: - types: [published] - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} From eb0bb761bc910ffc4290d619d6407663f6572eb7 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 23 Sep 2021 09:04:46 +0100 Subject: [PATCH 114/181] Update python-app.yml --- .github/workflows/python-app.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index f1c89e6a..77469c0f 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -23,14 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 numpy numba pandas h5py - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --exit-zero --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with unittest run: | python -m unittest tests/* From d646ac2ad3d119a4a218a9020165b72336af1c18 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 23 Sep 2021 09:06:47 +0100 Subject: [PATCH 115/181] Update python-app.yml --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 77469c0f..085dc5f3 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - + pip install numpy numba pandas h5py - name: Test with unittest run: | python -m unittest tests/* From b55775baae24004561cb3d44af46e19685e308fa Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 23 Sep 2021 09:07:14 +0100 Subject: [PATCH 116/181] dataframe describe function --- exetera/core/dataframe.py | 162 +++++++++++++++++++++++++++ tests/test_dataframe.py | 230 +++++++++++++++++++++++++++++++++++++- 2 files changed, 391 insertions(+), 1 deletion(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 19d9e4b3..174b3db0 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -565,6 +565,168 @@ def groupby(self, by: Union[str, List[str]], hint_keys_is_sorted=False): return HDF5DataFrameGroupBy(self._columns, by, sorted_index, spans) + def describe(self, include=None, exclude=None): + """ + Show the basic statistics of the data in each field. + + :param include: The field name or data type or simply 'all' to indicate the fields included in the calculation. + :param exclude: The filed name or data type to exclude in the calculation. + :return: A dataframe contains the statistic results. + + """ + # check include and exclude conflicts + if include is not None and exclude is not None: + if isinstance(include, str): + raise ValueError('Please do not use exclude parameter when include is set as a single field.') + elif isinstance(include, type): + if isinstance(exclude, type) or (isinstance(exclude, list) and isinstance(exclude[0], type)): + raise ValueError( + 'Please do not use set exclude as a type when include is set as a single data type.') + elif isinstance(include, list): + if isinstance(include[0], str) and isinstance(exclude, str): + raise ValueError('Please do not use exclude as the same type as the include parameter.') + elif isinstance(include[0], str) and isinstance(exclude, list) and isinstance(exclude[0], str): + raise ValueError('Please do not use exclude as the same type as the include parameter.') + elif isinstance(include[0], type) and isinstance(exclude, type): + raise ValueError('Please do not use exclude as the same type as the include parameter.') + elif isinstance(include[0], type) and isinstance(exclude, list) and isinstance(exclude[0], type): + raise ValueError('Please do not use exclude as the same type as the include parameter.') + + fields_to_calculate = [] + if include is not None: + if isinstance(include, str): # a single str + if include == 'all': + fields_to_calculate = list(self.columns.keys()) + elif include in self.columns.keys(): + fields_to_calculate = [include] + else: + raise ValueError('The field to include in not in the dataframe.') + elif isinstance(include, type): # a single type + for f in self.columns: + if not self[f].indexed and np.issubdtype(self[f].data.dtype, include): + fields_to_calculate.append(f) + if len(fields_to_calculate) == 0: + raise ValueError('No such type appeared in the dataframe.') + elif isinstance(include, list) and isinstance(include[0], str): # a list of str + for f in include: + if f in self.columns.keys(): + fields_to_calculate.append(f) + if len(fields_to_calculate) == 0: + raise ValueError('The fields to include in not in the dataframe.') + + elif isinstance(include, list) and isinstance(include[0], type): # a list of type + for t in include: + for f in self.columns: + if not self[f].indexed and np.issubdtype(self[f].data.dtype, t): + fields_to_calculate.append(f) + if len(fields_to_calculate) == 0: + raise ValueError('No such type appeared in the dataframe.') + + else: + raise ValueError('The include parameter can only be str, dtype, or list of either.') + + else: # include is None, numeric & timestamp fields only (no indexed strings) TODO confirm the type + for f in self.columns: + if isinstance(self[f], fld.NumericField) or isinstance(self[f], fld.TimestampField): + fields_to_calculate.append(f) + + if len(fields_to_calculate) == 0: + raise ValueError('No fields included to describe.') + + if exclude is not None: + if isinstance(exclude, str): + if exclude in fields_to_calculate: # exclude + fields_to_calculate.remove(exclude) # remove from list + elif isinstance(exclude, type): # a type + for f in fields_to_calculate: + if np.issubdtype(self[f].data.dtype, exclude): + fields_to_calculate.remove(f) + elif isinstance(exclude, list) and isinstance(exclude[0], str): # a list of str + for f in exclude: + fields_to_calculate.remove(f) + + elif isinstance(exclude, list) and isinstance(exclude[0], type): # a list of type + for t in exclude: + for f in fields_to_calculate: + if np.issubdtype(self[f].data.dtype, t): + fields_to_calculate.remove(f) # remove will raise valueerror if dtype not presented + + else: + raise ValueError('The exclude parameter can only be str, dtype, or list of either.') + + if len(fields_to_calculate) == 0: + raise ValueError('All fields are excluded, no field left to describe.') + # if flexible (str) fields + des_idxstr = False + for f in fields_to_calculate: + if isinstance(self[f], fld.CategoricalField) or isinstance(self[f], fld.FixedStringField) or isinstance( + self[f], fld.IndexedStringField): + des_idxstr = True + # calculation + result = {'fields': [], 'count': [], 'mean': [], 'std': [], 'min': [], '25%': [], '50%': [], '75%': [], + 'max': []} + + # count + if des_idxstr: + result['unique'], result['top'], result['freq'] = [], [], [] + + for f in fields_to_calculate: + result['fields'].append(f) + result['count'].append(len(self[f].data)) + + if des_idxstr and (isinstance(self[f], fld.NumericField) or isinstance(self[f], + fld.TimestampField)): # numberic, timestamp + result['unique'].append('NaN') + result['top'].append('NaN') + result['freq'].append('NaN') + + result['mean'].append("{:.2f}".format(np.mean(self[f].data[:]))) + result['std'].append("{:.2f}".format(np.std(self[f].data[:]))) + result['min'].append("{:.2f}".format(np.min(self[f].data[:]))) + result['25%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.25))) + result['50%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.5))) + result['75%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.75))) + result['max'].append("{:.2f}".format(np.max(self[f].data[:]))) + + elif des_idxstr and (isinstance(self[f], fld.CategoricalField) or isinstance(self[f], + fld.IndexedStringField) or isinstance( + self[f], fld.FixedStringField)): # categorical & indexed string & fixed string + a, b = np.unique(self[f].data[:], return_counts=True) + result['unique'].append(len(a)) + result['top'].append(a[np.argmax(b)]) + result['freq'].append(b[np.argmax(b)]) + + result['mean'].append('NaN') + result['std'].append('NaN') + result['min'].append('NaN') + result['25%'].append('NaN') + result['50%'].append('NaN') + result['75%'].append('NaN') + result['max'].append('NaN') + + elif not des_idxstr: + result['mean'].append("{:.2f}".format(np.mean(self[f].data[:]))) + result['std'].append("{:.2f}".format(np.std(self[f].data[:]))) + result['min'].append("{:.2f}".format(np.min(self[f].data[:]))) + result['25%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.25))) + result['50%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.5))) + result['75%'].append("{:.2f}".format(np.percentile(self[f].data[:], 0.75))) + result['max'].append("{:.2f}".format(np.max(self[f].data[:]))) + + # display + columns_to_show = ['fields', 'count', 'unique', 'top', 'freq', 'mean', 'std', 'min', '25%', '50%', '75%', 'max'] + # 5 fields each time for display + for col in range(0, len(result['fields']), 5): # 5 column each time + for i in columns_to_show: + if i in result: + print(i, end='\t') + for f in result[i][col:col + 5 if col + 5 < len(result[i]) - 1 else len(result[i])]: + print('{:>15}'.format(f), end='\t') + print('') + print('\n') + + return result + class HDF5DataFrameGroupBy(DataFrameGroupBy): diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 54736fde..b13e03ae 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -891,4 +891,232 @@ def test_to_csv_with_row_filter_field(self): self.assertEqual(f.readlines(), ['val1\n', '0\n', '2\n']) os.close(fd_csv) - \ No newline at end of file + +class TestDataFrameDescribe(unittest.TestCase): + + def test_describe_default(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + df = dst.create_dataframe('df') + df.create_numeric('num', 'int32').data.write([i for i in range(10)]) + df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) + df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) + df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) + df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) + result = df.describe() + expected = {'fields': ['num', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], + 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], + '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], + 'max': ['9.00', '1632234147.00']} + self.assertEqual(result, expected) + + def test_describe_include(self): + bio = BytesIO() + with session.Session() as s: + dst = s.open_dataset(bio, 'w', 'dst') + df = dst.create_dataframe('df') + df.create_numeric('num', 'int32').data.write([i for i in range(10)]) + df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) + df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) + df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) + df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) + + result = df.describe(include='all') + expected = {'fields': ['num', 'fs1', 'ts1', 'c1', 'is1'], 'count': [10, 20, 20, 20, 20], + 'mean': ['4.50', 'NaN', '1632234137.50', 'NaN', 'NaN'], 'std': ['2.87', 'NaN', '5.77', 'NaN', 'NaN'], + 'min': ['0.00', 'NaN', '1632234128.00', 'NaN', 'NaN'], '25%': ['0.02', 'NaN', '1632234128.05', 'NaN', 'NaN'], + '50%': ['0.04', 'NaN', '1632234128.10', 'NaN', 'NaN'], '75%': ['0.07', 'NaN', '1632234128.14', 'NaN', 'NaN'], + 'max': ['9.00', 'NaN', '1632234147.00', 'NaN', 'NaN'], 'unique': ['NaN', 1, 'NaN', 1, 1], + 'top': ['NaN', b'a', 'NaN', 1, 'abc'], 'freq': ['NaN', 20, 'NaN', 20, 20]} + self.assertEqual(result, expected) + + result = df.describe(include='num') + expected = {'fields': ['num'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], + '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} + self.assertEqual(result, expected) + + result = df.describe(include=['num', 'fs1']) + expected = {'fields': ['num', 'fs1'], 'count': [10, 20], 'mean': ['4.50', 'NaN'], 'std': ['2.87', 'NaN'], + 'min': ['0.00', 'NaN'], '25%': ['0.02', 'NaN'], '50%': ['0.04', 'NaN'], '75%': ['0.07', 'NaN'], + 'max': ['9.00', 'NaN'], 'unique': ['NaN', 1], 'top': ['NaN', b'a'], 'freq': ['NaN', 20]} + self.assertEqual(result, expected) + + result = df.describe(include=np.int32) + expected = {'fields': ['num', 'c1'], 'count': [10, 20], 'mean': ['4.50', 'NaN'], 'std': ['2.87', 'NaN'], + 'min': ['0.00', 'NaN'], '25%': ['0.02', 'NaN'], '50%': ['0.04', 'NaN'], '75%': ['0.07', 'NaN'], + 'max': ['9.00', 'NaN'], 'unique': ['NaN', 1], 'top': ['NaN', 1], 'freq': ['NaN', 20]} + self.assertEqual(result, expected) + + result = df.describe(include=[np.int32, np.bytes_]) + expected = {'fields': ['num', 'c1', 'fs1'], 'count': [10, 20, 20], 'mean': ['4.50', 'NaN', 'NaN'], + 'std': ['2.87', 'NaN', 'NaN'], 'min': ['0.00', 'NaN', 'NaN'], '25%': ['0.02', 'NaN', 'NaN'], + '50%': ['0.04', 'NaN', 'NaN'], '75%': ['0.07', 'NaN', 'NaN'], 'max': ['9.00', 'NaN', 'NaN'], + 'unique': ['NaN', 1, 1], 'top': ['NaN', 1, b'a'], 'freq': ['NaN', 20, 20]} + self.assertEqual(result, expected) + + + def test_describe_exclude(self): + bio = BytesIO() + with session.Session() as s: + src = s.open_dataset(bio, 'w', 'src') + df = src.create_dataframe('df') + df.create_numeric('num', 'int32').data.write([i for i in range(10)]) + df.create_numeric('num2', 'int64').data.write([i for i in range(10)]) + df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) + df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) + df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) + df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) + + result = df.describe(exclude='num') + expected = {'fields': ['num2', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], + 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], + '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], + 'max': ['9.00', '1632234147.00']} + self.assertEqual(result, expected) + + result = df.describe(exclude=['num', 'num2']) + expected = {'fields': ['ts1'], 'count': [20], 'mean': ['1632234137.50'], 'std': ['5.77'], + 'min': ['1632234128.00'], '25%': ['1632234128.05'], '50%': ['1632234128.10'], + '75%': ['1632234128.14'], 'max': ['1632234147.00']} + self.assertEqual(result, expected) + + result = df.describe(exclude=np.int32) + expected = {'fields': ['num2', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], + 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], + '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], + 'max': ['9.00', '1632234147.00']} + self.assertEqual(result, expected) + + result = df.describe(exclude=[np.int32, np.float64]) + expected = {'fields': ['num2'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], + '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} + self.assertEqual(result, expected) + + def test_describe_include_and_exclude(self): + bio = BytesIO() + with session.Session() as s: + src = s.open_dataset(bio, 'w', 'src') + df = src.create_dataframe('df') + df.create_numeric('num', 'int32').data.write([i for i in range(10)]) + df.create_numeric('num2', 'int64').data.write([i for i in range(10)]) + df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) + df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) + df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) + df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) + + #str * + with self.assertRaises(Exception) as context: + df.describe(include='num', exclude='num') + self.assertTrue(isinstance(context.exception, ValueError)) + + # list of str , str + with self.assertRaises(Exception) as context: + df.describe(include=['num', 'num2'], exclude='num') + self.assertTrue(isinstance(context.exception, ValueError)) + # list of str , type + result = df.describe(include=['num', 'num2'], exclude=np.int32) + expected = {'fields': ['num2'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], + '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} + self.assertEqual(result, expected) + # list of str , list of str + with self.assertRaises(Exception) as context: + df.describe(include=['num', 'num2'], exclude=['num', 'num2']) + self.assertTrue(isinstance(context.exception, ValueError)) + # list of str , list of type + result = df.describe(include=['num', 'num2', 'ts1'], exclude=[np.int32, np.int64]) + expected = {'fields': ['ts1'], 'count': [20], 'mean': ['1632234137.50'], 'std': ['5.77'], + 'min': ['1632234128.00'], '25%': ['1632234128.05'], '50%': ['1632234128.10'], + '75%': ['1632234128.14'], 'max': ['1632234147.00']} + self.assertEqual(result, expected) + + # type, str + result = df.describe(include=np.number, exclude='num2') + expected = {'fields': ['num', 'ts1', 'c1'], 'count': [10, 20, 20], 'mean': ['4.50', '1632234137.50', 'NaN'], + 'std': ['2.87', '5.77', 'NaN'], 'min': ['0.00', '1632234128.00', 'NaN'], + '25%': ['0.02', '1632234128.05', 'NaN'], '50%': ['0.04', '1632234128.10', 'NaN'], + '75%': ['0.07', '1632234128.14', 'NaN'], 'max': ['9.00', '1632234147.00', 'NaN'], + 'unique': ['NaN', 'NaN', 1], 'top': ['NaN', 'NaN', 1], 'freq': ['NaN', 'NaN', 20]} + self.assertEqual(result, expected) + # type, type + with self.assertRaises(Exception) as context: + df.describe(include=np.int32, exclude=np.int64) + self.assertTrue(isinstance(context.exception, ValueError)) + # type, list of str + result = df.describe(include=np.number, exclude=['num', 'num2']) + expected = {'fields': ['ts1', 'c1'], 'count': [20, 20], 'mean': ['1632234137.50', 'NaN'], + 'std': ['5.77', 'NaN'], 'min': ['1632234128.00', 'NaN'], '25%': ['1632234128.05', 'NaN'], + '50%': ['1632234128.10', 'NaN'], '75%': ['1632234128.14', 'NaN'], 'max': ['1632234147.00', 'NaN'], + 'unique': ['NaN', 1], 'top': ['NaN', 1], 'freq': ['NaN', 20]} + self.assertEqual(result, expected) + # type, list of type + with self.assertRaises(Exception) as context: + df.describe(include=np.int32, exclude=[np.int64, np.float64]) + self.assertTrue(isinstance(context.exception, ValueError)) + + # list of type, str + result = df.describe(include=[np.int32, np.int64], exclude='num') + expected = {'fields': ['c1', 'num2'], 'count': [20, 10], 'mean': ['NaN', '4.50'], 'std': ['NaN', '2.87'], + 'min': ['NaN', '0.00'], '25%': ['NaN', '0.02'], '50%': ['NaN', '0.04'], '75%': ['NaN', '0.07'], + 'max': ['NaN', '9.00'], 'unique': [1, 'NaN'], 'top': [1, 'NaN'], 'freq': [20, 'NaN']} + self.assertEqual(result, expected) + # list of type, type + with self.assertRaises(Exception) as context: + df.describe(include=[np.int32, np.int64], exclude=np.int64) + self.assertTrue(isinstance(context.exception, ValueError)) + # list of type, list of str + result = df.describe(include=[np.int32, np.int64], exclude=['num', 'num2']) + expected = {'fields': ['c1'], 'count': [20], 'mean': ['NaN'], 'std': ['NaN'], 'min': ['NaN'], + '25%': ['NaN'], '50%': ['NaN'], '75%': ['NaN'], 'max': ['NaN'], 'unique': [1], 'top': [1], + 'freq': [20]} + self.assertEqual(result, expected) + # list of type, list of type + with self.assertRaises(Exception) as context: + df.describe(include=[np.int32, np.int64], exclude=[np.int32, np.int64]) + self.assertTrue(isinstance(context.exception, ValueError)) + + def test_raise_errors(self): + bio = BytesIO() + with session.Session() as s: + src = s.open_dataset(bio, 'w', 'src') + df = src.create_dataframe('df') + + df.create_fixed_string('fs1', 1).data.write([b'a' for i in range(20)]) + df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) + df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) + + with self.assertRaises(Exception) as context: + df.describe(include='num3') + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe(include=np.int8) + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe(include=['num3', 'num4']) + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe(include=[np.int8, np.uint]) + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe(include=float('3.14159')) + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe() + self.assertTrue(isinstance(context.exception, ValueError)) + + df.create_numeric('num', 'int32').data.write([i for i in range(10)]) + df.create_numeric('num2', 'int64').data.write([i for i in range(10)]) + df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) + + with self.assertRaises(Exception) as context: + df.describe(exclude=float('3.14159')) + self.assertTrue(isinstance(context.exception, ValueError)) + + with self.assertRaises(Exception) as context: + df.describe(exclude=['num', 'num2', 'ts1']) + self.assertTrue(isinstance(context.exception, ValueError)) \ No newline at end of file From 7774c6fd22abd78aea27108f27724d92b3a3a3d6 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 23 Sep 2021 09:15:46 +0100 Subject: [PATCH 117/181] sync with upstream --- .github/workflows/python-app.yml | 9 ++++++- .github/workflows/python-publish.yml | 36 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 085dc5f3..f1c89e6a 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -23,7 +23,14 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install numpy numba pandas h5py + pip install flake8 numpy numba pandas h5py + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --exit-zero --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with unittest run: | python -m unittest tests/* diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 00000000..3bfabfc1 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,36 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} From ae1d6214688ebfd4726f72a86f43edad7e2e001a Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 30 Sep 2021 10:12:28 +0100 Subject: [PATCH 118/181] Update python-app.yml --- .github/workflows/python-app.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index f1c89e6a..037c5770 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -12,7 +12,10 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v2 From 3d5738e710e1dc5595846dab6ffcf87ac22871bc Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 5 Oct 2021 09:39:28 +0100 Subject: [PATCH 119/181] alternative get_timestamp notebook for discussion --- exetera/core/utils.py | 27 ++++- tempnotebook/util.get_timestamp.ipynb | 158 ++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 tempnotebook/util.get_timestamp.ipynb diff --git a/exetera/core/utils.py b/exetera/core/utils.py index b2fcb858..85306924 100644 --- a/exetera/core/utils.py +++ b/exetera/core/utils.py @@ -393,4 +393,29 @@ def one_dim_data_to_indexed_for_test(data, field_size): length += 1 indices[0, i + 1] = indices[0, i] + length - return indices, values, offsets, count_row \ No newline at end of file + return indices, values, offsets, count_row + + +def get_timestamp(date): + """ + This is an alternative of datetime.timestamp() as such function will raise an OSError on windoes if year is less + than 1970 or greater than 3002. + + :param date: The datetime instance to convert. + + :return: The timestamp of the date. + """ + if not isinstance(date, datetime): + raise TypeError("Please use a datetime variable as argument.") + try: + ts = date.timestamp() + return ts + except OSError: + import pytz + anchor = pytz.timezone('UTC').localize(datetime(1970, 1, 2)) # timestamp 86400 + ts = (pytz.timezone('Europe/London').localize(date) - anchor).total_seconds() + 86400 + if date.year >= 2038 and 4 <= date.month <= 10: + ts -= 3600 + return ts + #else: + diff --git a/tempnotebook/util.get_timestamp.ipynb b/tempnotebook/util.get_timestamp.ipynb new file mode 100644 index 00000000..fdac0eff --- /dev/null +++ b/tempnotebook/util.get_timestamp.ipynb @@ -0,0 +1,158 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternative datetime.timestamp() that works on a wider range on Windows" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# the get_timestamp method that aim to be used as a alternative of datetime.timestamp\n", + "from datetime import datetime\n", + "import pytz\n", + "def convert_timestamp(date):\n", + " if not isinstance(date, datetime):\n", + " return \"\"\n", + " anchor = pytz.timezone('UTC').localize(datetime(1970, 1, 2)) # timestamp 86400\n", + " ts = (pytz.timezone('Europe/London').localize(date)-anchor).total_seconds()+86400\n", + " if date.year >= 2038 and date.month >=4 and date.month <= 10:\n", + " ts -= 3600\n", + " return ts" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Can not convert starting from 1900 1\n", + "Convert_timestamp method is wrong from 1970 2\n", + "Convert_timestamp method is correct from 1970 4\n", + "Convert_timestamp method is wrong from 1970 11\n", + "Convert_timestamp method is correct from 1971 4\n", + "Till the end 2100 12\n" + ] + } + ], + "source": [ + "# test the method against datetime.datetime 1900.1.1 to 2100.1.1\n", + "flag = ''\n", + "for year in range(1900, 2101):\n", + " for month in range(1, 13):\n", + " d = datetime(year, month, 1)\n", + " try:\n", + " ts = d.timestamp()\n", + " except OSError:\n", + " if flag != 'error':\n", + " flag = 'error'\n", + " print(\"Can not convert starting from \", year, month)\n", + " \n", + " else:\n", + " ts2 = convert_timestamp(d)\n", + " if ts - ts2 != 0:\n", + " if flag != 'wrong':\n", + " flag = 'wrong'\n", + " print('Convert_timestamp method is wrong from ', year, month)\n", + " else:\n", + " if flag != 'correct':\n", + " flag = 'correct'\n", + " print('Convert_timestamp method is correct from ', year, month)\n", + "print('Till the end 2100 12')\n", + " \n", + " \n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Output of the last cell on Windows:\n", + "=====\n", + "Can not convert starting from 1900 1\n", + "Convert_timestamp method is wrong from 1970 2\n", + "Convert_timestamp method is correct from 1970 4\n", + "Convert_timestamp method is wrong from 1970 11\n", + "Convert_timestamp method is correct from 1971 4\n", + "Till the end 2100 12" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Output of the last cell on Linux:\n", + "=====\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Output of the last cell on MacOS:\n", + "=====" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# the final get_timestamp method proposed in exetera.util, try to use datetime.timestamp first\n", + "def get_timestamp(date):\n", + " \"\"\"\n", + " This is an alternative of datetime.timestamp() as such function will raise an OSError on windoes if year is less\n", + " than 1970 or greater than 3002.\n", + "\n", + " :param date: The datetime instance to convert.\n", + "\n", + " :return: The timestamp of the date.\n", + " \"\"\"\n", + " if not isinstance(date, datetime):\n", + " raise TypeError(\"Please use a datetime variable as argument.\")\n", + " try:\n", + " ts = date.timestamp()\n", + " return ts\n", + " except OSError:\n", + " import pytz\n", + " anchor = pytz.timezone('UTC').localize(datetime(1970, 1, 2)) # timestamp 86400\n", + " ts = (pytz.timezone('Europe/London').localize(date) - anchor).total_seconds() + 86400\n", + " if date.year >= 2038 and 4 <= date.month <= 10:\n", + " ts -= 3600\n", + " return ts" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 4685c6b2cc9cf9a995406d8f43ba1b5cd87ee72d Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 5 Oct 2021 09:57:04 +0100 Subject: [PATCH 120/181] update the notebook output of linux and mac --- tempnotebook/util.get_timestamp.ipynb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tempnotebook/util.get_timestamp.ipynb b/tempnotebook/util.get_timestamp.ipynb index fdac0eff..e458e9ef 100644 --- a/tempnotebook/util.get_timestamp.ipynb +++ b/tempnotebook/util.get_timestamp.ipynb @@ -45,7 +45,7 @@ } ], "source": [ - "# test the method against datetime.datetime 1900.1.1 to 2100.1.1\n", + "# benchmark, test the method against datetime.datetime 1900.1.1 to 2100.1.1\n", "flag = ''\n", "for year in range(1900, 2101):\n", " for month in range(1, 13):\n", @@ -92,7 +92,11 @@ "metadata": {}, "source": [ "Output of the last cell on Linux:\n", - "=====\n" + "=====\n", + "\n", + "Convert_timestamp method is wrong from 1900 1\n", + "Convert_timestamp method is correct from 1902 1\n", + "Till the end 2100 12\n" ] }, { @@ -100,7 +104,10 @@ "metadata": {}, "source": [ "Output of the last cell on MacOS:\n", - "=====" + "=====\n", + "Convert_timestamp method is wrong from 1900 1\n", + "Convert_timestamp method is correct from 1902 1\n", + "Till the end 2100 12" ] }, { From dc38d285933437c8390e893617116fe6459a7792 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 5 Oct 2021 09:59:03 +0100 Subject: [PATCH 121/181] update format --- tempnotebook/util.get_timestamp.ipynb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tempnotebook/util.get_timestamp.ipynb b/tempnotebook/util.get_timestamp.ipynb index e458e9ef..8960a157 100644 --- a/tempnotebook/util.get_timestamp.ipynb +++ b/tempnotebook/util.get_timestamp.ipynb @@ -80,10 +80,15 @@ "Output of the last cell on Windows:\n", "=====\n", "Can not convert starting from 1900 1\n", + "\n", "Convert_timestamp method is wrong from 1970 2\n", + "\n", "Convert_timestamp method is correct from 1970 4\n", + "\n", "Convert_timestamp method is wrong from 1970 11\n", + "\n", "Convert_timestamp method is correct from 1971 4\n", + "\n", "Till the end 2100 12" ] }, @@ -95,7 +100,9 @@ "=====\n", "\n", "Convert_timestamp method is wrong from 1900 1\n", + "\n", "Convert_timestamp method is correct from 1902 1\n", + "\n", "Till the end 2100 12\n" ] }, @@ -106,7 +113,9 @@ "Output of the last cell on MacOS:\n", "=====\n", "Convert_timestamp method is wrong from 1900 1\n", + "\n", "Convert_timestamp method is correct from 1902 1\n", + "\n", "Till the end 2100 12" ] }, From 0df34bcb6be4ebbadff86a7372e284e73fd4a243 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 11 Oct 2021 08:37:45 +0100 Subject: [PATCH 122/181] update the to_timestamp and to_timestamp function in utils fix the current datetime.timestamp() error in test_fields and test_sessions --- .github/workflows/python-publish.yml | 36 -------------------------- exetera/core/utils.py | 38 +++++++++++++++++----------- tests/test_fields.py | 10 ++++---- tests/test_session.py | 2 +- 4 files changed, 29 insertions(+), 57 deletions(-) delete mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index 3bfabfc1..00000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,36 +0,0 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Upload Python Package - -on: - release: - types: [published] - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/exetera/core/utils.py b/exetera/core/utils.py index 85306924..c5c2834a 100644 --- a/exetera/core/utils.py +++ b/exetera/core/utils.py @@ -12,7 +12,7 @@ import time from collections import defaultdict import csv -from datetime import datetime +from datetime import datetime, timezone, timedelta from io import StringIO import numpy as np @@ -396,26 +396,34 @@ def one_dim_data_to_indexed_for_test(data, field_size): return indices, values, offsets, count_row -def get_timestamp(date): +def to_timestamp(dt): """ This is an alternative of datetime.timestamp() as such function will raise an OSError on windoes if year is less than 1970 or greater than 3002. - :param date: The datetime instance to convert. + :param dt: The datetime instance to convert. :return: The timestamp of the date. """ - if not isinstance(date, datetime): + if not isinstance(dt, datetime): raise TypeError("Please use a datetime variable as argument.") - try: - ts = date.timestamp() - return ts - except OSError: - import pytz - anchor = pytz.timezone('UTC').localize(datetime(1970, 1, 2)) # timestamp 86400 - ts = (pytz.timezone('Europe/London').localize(date) - anchor).total_seconds() + 86400 - if date.year >= 2038 and 4 <= date.month <= 10: - ts -= 3600 - return ts - #else: + DATE_TIME_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) + return (dt - DATE_TIME_EPOCH.replace(tzinfo=dt.tzinfo)).total_seconds() + + +def to_datetime(ts, tz=None): + """ + Convert an int/float timestamp to a datetime instance. + :param ts: The timestamp to convert + :param tz: The timezone info to set, default is in UTC. + + :return: The datetime instance. + """ + if not isinstance(ts, float) and not isinstance(ts, int): + raise TypeError("Please use a int/float timestamp as argument.") + DATE_TIME_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) + if tz is not None: + return (DATE_TIME_EPOCH + timedelta(seconds=ts)).replace(tzinfo=tz) + else: + return DATE_TIME_EPOCH + timedelta(seconds=ts) diff --git a/tests/test_fields.py b/tests/test_fields.py index c6709c62..41c514d4 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -676,7 +676,7 @@ def test_timestamp_apply_filter(self): from datetime import datetime as D data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11), D(2110, 11, 1), D(2002, 3, 3), D(2018, 2, 28), D(2400, 9, 1)] - data = np.asarray([d.timestamp() for d in data], dtype=np.float64) + data = np.asarray([utils.to_timestamp(d) for d in data], dtype=np.float64) filt = np.array([0, 1, 0, 1, 0, 1, 0, 1], dtype=bool) expected = data[filt].tolist() @@ -913,7 +913,7 @@ def test_timestamp_apply_index(self): from datetime import datetime as D data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11), D(2110, 11, 1), D(2002, 3, 3), D(2018, 2, 28), D(2400, 9, 1)] - data = np.asarray([d.timestamp() for d in data], dtype=np.float64) + data = np.asarray([utils.to_timestamp(d) for d in data], dtype=np.float64) indices = np.array([7, 0, 6, 1, 5, 2, 4, 3], dtype=np.int32) expected = data[indices].tolist() bio = BytesIO() @@ -1071,7 +1071,7 @@ def test_timestamp_apply_spans(self): from datetime import datetime as D src_data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11), D(2021, 1, 1), D(2022, 5, 18), D(2951, 8, 17), D(1841, 10, 11)] - src_data = np.asarray([d.timestamp() for d in src_data], dtype=np.float64) + src_data = np.asarray([utils.to_timestamp(d) for d in src_data], dtype=np.float64) expected = src_data[[0, 2, 3, 6]].tolist() self._test_apply_spans_src(spans, src_data, expected, @@ -1176,7 +1176,7 @@ def test_categorical_field_create_like(self): def test_timestamp_field_create_like(self): from datetime import datetime as D data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11)] - data = np.asarray([d.timestamp() for d in data], dtype=np.float64) + data = np.asarray([utils.to_timestamp(d) for d in data], dtype=np.float64) bio = BytesIO() with session.Session() as s: @@ -1263,7 +1263,7 @@ def test_categorical_field_create_like(self): def test_timestamp_field_create_like(self): from datetime import datetime as D data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11)] - data = np.asarray([d.timestamp() for d in data], dtype=np.float64) + data = np.asarray([utils.to_timestamp(d) for d in data], dtype=np.float64) bio = BytesIO() with h5py.File(bio, 'w') as ds: diff --git a/tests/test_session.py b/tests/test_session.py index 1120325f..4ff4fa15 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -76,7 +76,7 @@ def test_create_then_load_timestamp(self): from datetime import datetime as D bio = BytesIO() contents = [D(2021, 2, 6), D(2020, 11, 5), D(2974, 8, 1), D(1873, 12, 28)] - contents = [c.timestamp() for c in contents] + contents = [utils.to_timestamp(c) for c in contents] with session.Session() as s: with h5py.File(bio, 'w') as src: From 87353e33eca22ae8e035c705c52199f5c4dc6120 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 11 Oct 2021 09:04:13 +0100 Subject: [PATCH 123/181] add unittest for utils to_timestamp and to_datetimie --- tests/test_utils.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 44cc7f2a..e014027a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -14,7 +14,7 @@ import numpy as np from exetera.core.utils import find_longest_sequence_of, to_escaped, bytearray_to_escaped, get_min_max - +from exetera.core.utils import to_timestamp, to_datetime class TestUtils(unittest.TestCase): @@ -100,4 +100,25 @@ def test_get_min_max_for_permitted_types(self): for value_type in permitted_numeric_types: (min_value, max_value) = get_min_max(value_type) self.assertEqual(min_value, expected_min_max_values[value_type][0]) - self.assertEqual(max_value, expected_min_max_values[value_type][1]) \ No newline at end of file + self.assertEqual(max_value, expected_min_max_values[value_type][1]) + + def test_to_timestamp(self): + from datetime import datetime + dts = [datetime(1874, 7, 27), datetime(1974, 1, 1), datetime(2020, 1, 1), datetime(2021, 6, 1), + datetime(2030, 5, 5), datetime(3030, 6, 6)] + dt_ts = [to_timestamp(d) for d in dts] + expected = [-3011558400.0, 126230400.0, 1577836800.0, 1622505600.0, 1904169600.0, 33463843200.0] + self.assertListEqual(dt_ts, expected) + + def test_to_datetime(self): + from datetime import datetime, timezone + dt_ts = [-3011558400.0, 126230400.0, 1577836800.0, 1622505600.0, 1904169600.0, 33463843200.0] + dts = [to_datetime(d) for d in dt_ts] + expected = [datetime(1874, 7, 27, tzinfo=timezone.utc), datetime(1974, 1, 1, tzinfo=timezone.utc), datetime(2020, 1, 1, tzinfo=timezone.utc), datetime(2021, 6, 1, tzinfo=timezone.utc), + datetime(2030, 5, 5, tzinfo=timezone.utc), datetime(3030, 6, 6, tzinfo=timezone.utc)] + self.assertListEqual(dts, expected) + + import pytz + dts = [to_datetime(d, tz=pytz.timezone('Europe/London')) for d in dt_ts] + for d in dts: + self.assertEqual(d.tzinfo, pytz.timezone('Europe/London')) From 87abe47d39b467f5d33e51d99f313874e468b10e Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 11 Oct 2021 09:08:04 +0100 Subject: [PATCH 124/181] fix for pr --- .github/workflows/python-publish.yml | 36 ++++++ tempnotebook/util.get_timestamp.ipynb | 174 -------------------------- 2 files changed, 36 insertions(+), 174 deletions(-) create mode 100644 .github/workflows/python-publish.yml delete mode 100644 tempnotebook/util.get_timestamp.ipynb diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 00000000..400548b5 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,36 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/tempnotebook/util.get_timestamp.ipynb b/tempnotebook/util.get_timestamp.ipynb deleted file mode 100644 index 8960a157..00000000 --- a/tempnotebook/util.get_timestamp.ipynb +++ /dev/null @@ -1,174 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Alternative datetime.timestamp() that works on a wider range on Windows" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# the get_timestamp method that aim to be used as a alternative of datetime.timestamp\n", - "from datetime import datetime\n", - "import pytz\n", - "def convert_timestamp(date):\n", - " if not isinstance(date, datetime):\n", - " return \"\"\n", - " anchor = pytz.timezone('UTC').localize(datetime(1970, 1, 2)) # timestamp 86400\n", - " ts = (pytz.timezone('Europe/London').localize(date)-anchor).total_seconds()+86400\n", - " if date.year >= 2038 and date.month >=4 and date.month <= 10:\n", - " ts -= 3600\n", - " return ts" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Can not convert starting from 1900 1\n", - "Convert_timestamp method is wrong from 1970 2\n", - "Convert_timestamp method is correct from 1970 4\n", - "Convert_timestamp method is wrong from 1970 11\n", - "Convert_timestamp method is correct from 1971 4\n", - "Till the end 2100 12\n" - ] - } - ], - "source": [ - "# benchmark, test the method against datetime.datetime 1900.1.1 to 2100.1.1\n", - "flag = ''\n", - "for year in range(1900, 2101):\n", - " for month in range(1, 13):\n", - " d = datetime(year, month, 1)\n", - " try:\n", - " ts = d.timestamp()\n", - " except OSError:\n", - " if flag != 'error':\n", - " flag = 'error'\n", - " print(\"Can not convert starting from \", year, month)\n", - " \n", - " else:\n", - " ts2 = convert_timestamp(d)\n", - " if ts - ts2 != 0:\n", - " if flag != 'wrong':\n", - " flag = 'wrong'\n", - " print('Convert_timestamp method is wrong from ', year, month)\n", - " else:\n", - " if flag != 'correct':\n", - " flag = 'correct'\n", - " print('Convert_timestamp method is correct from ', year, month)\n", - "print('Till the end 2100 12')\n", - " \n", - " \n", - " " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Output of the last cell on Windows:\n", - "=====\n", - "Can not convert starting from 1900 1\n", - "\n", - "Convert_timestamp method is wrong from 1970 2\n", - "\n", - "Convert_timestamp method is correct from 1970 4\n", - "\n", - "Convert_timestamp method is wrong from 1970 11\n", - "\n", - "Convert_timestamp method is correct from 1971 4\n", - "\n", - "Till the end 2100 12" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Output of the last cell on Linux:\n", - "=====\n", - "\n", - "Convert_timestamp method is wrong from 1900 1\n", - "\n", - "Convert_timestamp method is correct from 1902 1\n", - "\n", - "Till the end 2100 12\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Output of the last cell on MacOS:\n", - "=====\n", - "Convert_timestamp method is wrong from 1900 1\n", - "\n", - "Convert_timestamp method is correct from 1902 1\n", - "\n", - "Till the end 2100 12" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# the final get_timestamp method proposed in exetera.util, try to use datetime.timestamp first\n", - "def get_timestamp(date):\n", - " \"\"\"\n", - " This is an alternative of datetime.timestamp() as such function will raise an OSError on windoes if year is less\n", - " than 1970 or greater than 3002.\n", - "\n", - " :param date: The datetime instance to convert.\n", - "\n", - " :return: The timestamp of the date.\n", - " \"\"\"\n", - " if not isinstance(date, datetime):\n", - " raise TypeError(\"Please use a datetime variable as argument.\")\n", - " try:\n", - " ts = date.timestamp()\n", - " return ts\n", - " except OSError:\n", - " import pytz\n", - " anchor = pytz.timezone('UTC').localize(datetime(1970, 1, 2)) # timestamp 86400\n", - " ts = (pytz.timezone('Europe/London').localize(date) - anchor).total_seconds() + 86400\n", - " if date.year >= 2038 and 4 <= date.month <= 10:\n", - " ts -= 3600\n", - " return ts" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From a3719efba3aac5f9c220a9f251d4dcd0a9986885 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 12 Oct 2021 09:50:28 +0100 Subject: [PATCH 125/181] setup github action specific for windows for cython --- .github/workflows/python-publish-win.yml | 40 ++++++++++++++++++++++++ requirements.txt | 1 + setup.py | 5 +++ 3 files changed, 46 insertions(+) create mode 100644 .github/workflows/python-publish-win.yml diff --git a/.github/workflows/python-publish-win.yml b/.github/workflows/python-publish-win.yml new file mode 100644 index 00000000..69c0836e --- /dev/null +++ b/.github/workflows/python-publish-win.yml @@ -0,0 +1,40 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +jobs: + deploy: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build setuptools wheel cython + - name: Set up MinGW + uses: egor-tensin/setup-mingw@v2 + with: + platform: x64 + - name: Build package + run: python setup.py bdist_wheel + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/requirements.txt b/requirements.txt index 3d49aac1..ba942618 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ numpy pandas h5py numba +cython \ No newline at end of file diff --git a/setup.py b/setup.py index 6c8ae893..dc66fb2e 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ from setuptools import setup from pkg_resources import parse_requirements +from Cython.Build import cythonize from os import path this_directory = path.abspath(path.dirname(__file__)) @@ -14,6 +15,9 @@ with open(path.join(this_directory, "requirements.txt")) as o: requirements = [str(r) for r in parse_requirements(o.read())] +pyxfiles = ['ops.pyx'] +pyx_full_path = [path.join(this_directory, 'exetera', '_libs', pyx) for pyx in pyxfiles] + setup( name='exetera', version=__version__, @@ -26,6 +30,7 @@ license='http://www.apache.org/licenses/LICENSE-2.0', packages=['exetera', 'exetera.core', 'exetera.processing'], scripts=['exetera/bin/exetera'], + ext_modules = cythonize(pyx_full_path), python_requires='>=3.7', install_requires=requirements ) From ed42f70d978190211ce8e9950b82e2dc76a0a80d Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 12 Oct 2021 10:05:54 +0100 Subject: [PATCH 126/181] minor workflow fix --- .github/workflows/python-publish-win.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/python-publish-win.yml b/.github/workflows/python-publish-win.yml index 69c0836e..16cf177e 100644 --- a/.github/workflows/python-publish-win.yml +++ b/.github/workflows/python-publish-win.yml @@ -9,6 +9,10 @@ name: Upload Python Package on: + push: + branches: [ master ] + pull_request: + branches: [ master ] release: types: [published] From 2157da2bbfe751f75ea93fc39c5c6562c693d9b5 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 12 Oct 2021 10:09:21 +0100 Subject: [PATCH 127/181] add example pyx file --- exetera/_libs/ops.pyx | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 exetera/_libs/ops.pyx diff --git a/exetera/_libs/ops.pyx b/exetera/_libs/ops.pyx new file mode 100644 index 00000000..40a18630 --- /dev/null +++ b/exetera/_libs/ops.pyx @@ -0,0 +1,8 @@ +def fib(n): + """Print the Fibonacci series up to n.""" + a, b = 0, 1 + while b < n: + print(b) + a, b = b, a + b + + print() \ No newline at end of file From 1abeaa7846d292be16675ddd15f2bc0a33f13eb4 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 12 Oct 2021 10:15:53 +0100 Subject: [PATCH 128/181] fix package upload command on win; as the git action gh-action-pypi-publish works only on linux --- .github/workflows/python-publish-win.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-publish-win.yml b/.github/workflows/python-publish-win.yml index 16cf177e..dd0065e3 100644 --- a/.github/workflows/python-publish-win.yml +++ b/.github/workflows/python-publish-win.yml @@ -38,7 +38,8 @@ jobs: - name: Build package run: python setup.py bdist_wheel - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + run: | + python3 -m twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} From e77562e11e49986945be867f0fc9304ecc5eb259 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 12 Oct 2021 10:18:43 +0100 Subject: [PATCH 129/181] add twine as tools --- .github/workflows/python-publish-win.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-publish-win.yml b/.github/workflows/python-publish-win.yml index dd0065e3..044b0ad1 100644 --- a/.github/workflows/python-publish-win.yml +++ b/.github/workflows/python-publish-win.yml @@ -30,7 +30,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install build setuptools wheel cython + pip install build setuptools wheel cython twine - name: Set up MinGW uses: egor-tensin/setup-mingw@v2 with: From 03208aa1b3f875fb07b26014e36ee34b333cec01 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 12 Oct 2021 10:24:33 +0100 Subject: [PATCH 130/181] add linux action file --- ...{python-publish.yml => python-publish-linux.yml} | 13 ++++++++++--- .github/workflows/python-publish-win.yml | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) rename .github/workflows/{python-publish.yml => python-publish-linux.yml} (76%) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish-linux.yml similarity index 76% rename from .github/workflows/python-publish.yml rename to .github/workflows/python-publish-linux.yml index 400548b5..83346db2 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish-linux.yml @@ -6,9 +6,11 @@ # separate terms of service, privacy policy, and support # documentation. -name: Upload Python Package +name: Build & upload package on Linux on: + pull_request: + branches: [ master ] release: types: [published] @@ -26,11 +28,16 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install build + pip install build setuptools wheel cython twine + - name: Set up GCC + uses: egor-tensin/setup-gcc@v1 + with: + version: latest + platform: x64 - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/python-publish-win.yml b/.github/workflows/python-publish-win.yml index 044b0ad1..aca44d8f 100644 --- a/.github/workflows/python-publish-win.yml +++ b/.github/workflows/python-publish-win.yml @@ -6,7 +6,7 @@ # separate terms of service, privacy policy, and support # documentation. -name: Upload Python Package +name: Build & upload package on Windows on: push: From 35430f21b8e23129232916b5d5930e1101434b32 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 12 Oct 2021 10:27:28 +0100 Subject: [PATCH 131/181] update the linux build command --- .github/workflows/python-publish-linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-publish-linux.yml b/.github/workflows/python-publish-linux.yml index 83346db2..ae2a5557 100644 --- a/.github/workflows/python-publish-linux.yml +++ b/.github/workflows/python-publish-linux.yml @@ -35,7 +35,7 @@ jobs: version: latest platform: x64 - name: Build package - run: python -m build + run: python setup.py bdist_wheel - name: Publish package uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: From de3e7e5437e771bb565f99ff5af1d814caf847f1 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 12 Oct 2021 10:31:33 +0100 Subject: [PATCH 132/181] build workflow for macos --- .github/workflows/python-publish-macos.yml | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/python-publish-macos.yml diff --git a/.github/workflows/python-publish-macos.yml b/.github/workflows/python-publish-macos.yml new file mode 100644 index 00000000..bdcf0005 --- /dev/null +++ b/.github/workflows/python-publish-macos.yml @@ -0,0 +1,39 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + pull_request: + branches: [ master ] + release: + types: [published] + +jobs: + deploy: + + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build setuptools wheel cython twine + - name: Build package + run: python setup.py bdist_wheel + - name: Publish package + run: | + python3 -m twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} From a8af75031ab95f93d6344430a900b3f613b3db99 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 12 Oct 2021 10:39:45 +0100 Subject: [PATCH 133/181] minor update the macos workflow --- .github/workflows/python-publish-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-publish-macos.yml b/.github/workflows/python-publish-macos.yml index bdcf0005..0ba03d47 100644 --- a/.github/workflows/python-publish-macos.yml +++ b/.github/workflows/python-publish-macos.yml @@ -6,7 +6,7 @@ # separate terms of service, privacy policy, and support # documentation. -name: Upload Python Package +name: Build & upload package on MacOS on: pull_request: From d41a24b62d573c183fcb4ca99ef1336ebf5576b6 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 14 Oct 2021 16:39:37 +0100 Subject: [PATCH 134/181] fixed timestamp issue on windows by add timezone info to datetime --- exetera/core/field_importers.py | 10 ++++++---- exetera/core/utils.py | 32 -------------------------------- tests/test_fields.py | 31 ++++++++++++++++++------------- tests/test_importer.py | 8 ++++---- tests/test_session.py | 5 +++-- tests/test_utils.py | 20 -------------------- 6 files changed, 31 insertions(+), 75 deletions(-) diff --git a/exetera/core/field_importers.py b/exetera/core/field_importers.py index 0b33e592..e42b05d7 100644 --- a/exetera/core/field_importers.py +++ b/exetera/core/field_importers.py @@ -5,7 +5,8 @@ from exetera.core import operations as ops from exetera.core.data_writer import DataWriter from exetera.core import utils -from datetime import datetime, date +from datetime import datetime, date, timezone +import pytz INDEXED_STRING_FIELD_SIZE = 10 # guessing @@ -307,14 +308,14 @@ def write_part(self, values): # ts = datetime.strptime(value.decode(), '%Y-%m-%d %H:%M:%S.%f%z') v_datetime = datetime(int(value[0:4]), int(value[5:7]), int(value[8:10]), int(value[11:13]), int(value[14:16]), int(value[17:19]), - int(value[20:26])) + int(value[20:26]), tzinfo=timezone.utc) elif v_len == 25: # ts = datetime.strptime(value.decode(), '%Y-%m-%d %H:%M:%S%z') v_datetime = datetime(int(value[0:4]), int(value[5:7]), int(value[8:10]), - int(value[11:13]), int(value[14:16]), int(value[17:19])) + int(value[11:13]), int(value[14:16]), int(value[17:19]), tzinfo=timezone.utc) elif v_len == 19: v_datetime = datetime(int(value[0:4]), int(value[5:7]), int(value[8:10]), - int(value[11:13]), int(value[14:16]), int(value[17:19])) + int(value[11:13]), int(value[14:16]), int(value[17:19]), tzinfo=timezone.utc) else: raise ValueError(f"Date field '{self.field}' has unexpected format '{value}'") datetime_ts[i] = v_datetime.timestamp() @@ -362,6 +363,7 @@ def write_part(self, values): flags[i] = False else: ts = datetime.strptime(value.decode(), '%Y-%m-%d') + ts = ts.replace(tzinfo=timezone.utc) date_ts[i] = ts.timestamp() self.field.data.write_part(date_ts) diff --git a/exetera/core/utils.py b/exetera/core/utils.py index c5c2834a..077ed779 100644 --- a/exetera/core/utils.py +++ b/exetera/core/utils.py @@ -395,35 +395,3 @@ def one_dim_data_to_indexed_for_test(data, field_size): return indices, values, offsets, count_row - -def to_timestamp(dt): - """ - This is an alternative of datetime.timestamp() as such function will raise an OSError on windoes if year is less - than 1970 or greater than 3002. - - :param dt: The datetime instance to convert. - - :return: The timestamp of the date. - """ - if not isinstance(dt, datetime): - raise TypeError("Please use a datetime variable as argument.") - DATE_TIME_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) - return (dt - DATE_TIME_EPOCH.replace(tzinfo=dt.tzinfo)).total_seconds() - - -def to_datetime(ts, tz=None): - """ - Convert an int/float timestamp to a datetime instance. - - :param ts: The timestamp to convert - :param tz: The timezone info to set, default is in UTC. - - :return: The datetime instance. - """ - if not isinstance(ts, float) and not isinstance(ts, int): - raise TypeError("Please use a int/float timestamp as argument.") - DATE_TIME_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) - if tz is not None: - return (DATE_TIME_EPOCH + timedelta(seconds=ts)).replace(tzinfo=tz) - else: - return DATE_TIME_EPOCH + timedelta(seconds=ts) diff --git a/tests/test_fields.py b/tests/test_fields.py index 41c514d4..4f380d99 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -674,9 +674,10 @@ def test_categorical_apply_filter(self): def test_timestamp_apply_filter(self): from datetime import datetime as D - data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11), - D(2110, 11, 1), D(2002, 3, 3), D(2018, 2, 28), D(2400, 9, 1)] - data = np.asarray([utils.to_timestamp(d) for d in data], dtype=np.float64) + from datetime import timezone + data = [D(2020, 1, 1, tzinfo=timezone.utc), D(2021, 5, 18, tzinfo=timezone.utc), D(2950, 8, 17, tzinfo=timezone.utc), D(1840, 10, 11, tzinfo=timezone.utc), + D(2110, 11, 1, tzinfo=timezone.utc), D(2002, 3, 3, tzinfo=timezone.utc), D(2018, 2, 28, tzinfo=timezone.utc), D(2400, 9, 1, tzinfo=timezone.utc)] + data = np.asarray([d.timestamp() for d in data], dtype=np.float64) filt = np.array([0, 1, 0, 1, 0, 1, 0, 1], dtype=bool) expected = data[filt].tolist() @@ -911,9 +912,10 @@ def test_categorical_apply_index(self): def test_timestamp_apply_index(self): from datetime import datetime as D - data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11), - D(2110, 11, 1), D(2002, 3, 3), D(2018, 2, 28), D(2400, 9, 1)] - data = np.asarray([utils.to_timestamp(d) for d in data], dtype=np.float64) + from datetime import timezone + data = [D(2020, 1, 1, tzinfo=timezone.utc), D(2021, 5, 18, tzinfo=timezone.utc), D(2950, 8, 17, tzinfo=timezone.utc), D(1840, 10, 11, tzinfo=timezone.utc), + D(2110, 11, 1, tzinfo=timezone.utc), D(2002, 3, 3, tzinfo=timezone.utc), D(2018, 2, 28, tzinfo=timezone.utc), D(2400, 9, 1, tzinfo=timezone.utc)] + data = np.asarray([d.timestamp() for d in data], dtype=np.float64) indices = np.array([7, 0, 6, 1, 5, 2, 4, 3], dtype=np.int32) expected = data[indices].tolist() bio = BytesIO() @@ -1069,9 +1071,10 @@ def test_categorical_apply_spans(self): def test_timestamp_apply_spans(self): spans = np.array([0, 2, 3, 6, 8], dtype=np.int32) from datetime import datetime as D - src_data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11), - D(2021, 1, 1), D(2022, 5, 18), D(2951, 8, 17), D(1841, 10, 11)] - src_data = np.asarray([utils.to_timestamp(d) for d in src_data], dtype=np.float64) + from datetime import timezone + src_data = [D(2020, 1, 1, tzinfo=timezone.utc), D(2021, 5, 1, tzinfo=timezone.utc), D(2950, 8, 17, tzinfo=timezone.utc), D(1840, 10, 11, tzinfo=timezone.utc), + D(2021, 1, 1, tzinfo=timezone.utc), D(2022, 5, 18, tzinfo=timezone.utc), D(2951, 8, 17, tzinfo=timezone.utc), D(1841, 10, 11, tzinfo=timezone.utc)] + src_data = np.asarray([d.timestamp() for d in src_data], dtype=np.float64) expected = src_data[[0, 2, 3, 6]].tolist() self._test_apply_spans_src(spans, src_data, expected, @@ -1175,8 +1178,9 @@ def test_categorical_field_create_like(self): def test_timestamp_field_create_like(self): from datetime import datetime as D - data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11)] - data = np.asarray([utils.to_timestamp(d) for d in data], dtype=np.float64) + from datetime import timezone + data = [D(2020, 1, 1, tzinfo=timezone.utc), D(2021, 5, 18, tzinfo=timezone.utc), D(2950, 8, 17, tzinfo=timezone.utc), D(1840, 10, 11, tzinfo=timezone.utc)] + data = np.asarray([d.timestamp() for d in data], dtype=np.float64) bio = BytesIO() with session.Session() as s: @@ -1262,8 +1266,9 @@ def test_categorical_field_create_like(self): def test_timestamp_field_create_like(self): from datetime import datetime as D - data = [D(2020, 1, 1), D(2021, 5, 18), D(2950, 8, 17), D(1840, 10, 11)] - data = np.asarray([utils.to_timestamp(d) for d in data], dtype=np.float64) + from datetime import timezone + data = [D(2020, 1, 1, tzinfo=timezone.utc), D(2021, 5, 18, tzinfo=timezone.utc), D(2950, 8, 17, tzinfo=timezone.utc), D(1840, 10, 11, tzinfo=timezone.utc)] + data = np.asarray([d.timestamp() for d in data], dtype=np.float64) bio = BytesIO() with h5py.File(bio, 'w') as ds: diff --git a/tests/test_importer.py b/tests/test_importer.py index 9a340691..f376a42d 100644 --- a/tests/test_importer.py +++ b/tests/test_importer.py @@ -169,10 +169,10 @@ def test_importer_date(self): importer.import_with_schema(s, self.ts, self.ds_name, bio, self.schema, self.files, False, {}, {}, chunk_row_size=self.chunk_row_size) ds = s.get_dataset(self.ds_name) df = ds.get_dataframe('schema_key') - self.assertEqual(df['birthday'].data[:].tolist(), [datetime.strptime(x, "%Y-%m-%d").timestamp() for x in expected_birthday_date]) + self.assertEqual(df['birthday'].data[:].tolist(), [datetime.strptime(x, "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp() for x in expected_birthday_date]) with h5py.File(bio, 'r') as hf: - self.assertEqual(hf['schema_key']['birthday']['values'][:].tolist(), [datetime.strptime(x, "%Y-%m-%d").timestamp() for x in expected_birthday_date]) + self.assertEqual(hf['schema_key']['birthday']['values'][:].tolist(), [datetime.strptime(x, "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp() for x in expected_birthday_date]) def test_importer_datetime_with_create_day_field(self): @@ -184,12 +184,12 @@ def test_importer_datetime_with_create_day_field(self): importer.import_with_schema(s, self.ts, self.ds_name, bio, self.schema, self.files, False, {}, {}, chunk_row_size=self.chunk_row_size) ds = s.get_dataset(self.ds_name) df = ds.get_dataframe('schema_key') - self.assertEqual(df['updated_at'].data[:].tolist(), [datetime.strptime(x, "%Y-%m-%d %H:%M:%S").timestamp() for x in expected_updated_at_list]) + self.assertEqual(df['updated_at'].data[:].tolist(), [datetime.strptime(x, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc).timestamp() for x in expected_updated_at_list]) self.assertEqual(df['updated_at_day'].data[:].tolist(), expected_updated_at_date_list ) with h5py.File(bio, 'r') as hf: print(hf['schema_key']['updated_at']['values'][:]) - self.assertAlmostEqual(hf['schema_key']['updated_at']['values'][:].tolist(), [datetime.strptime(x, "%Y-%m-%d %H:%M:%S").timestamp() for x in expected_updated_at_list]) + self.assertAlmostEqual(hf['schema_key']['updated_at']['values'][:].tolist(), [datetime.strptime(x, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc).timestamp() for x in expected_updated_at_list]) self.assertEqual(hf['schema_key']['updated_at_day']['values'][:].tolist(), expected_updated_at_date_list) diff --git a/tests/test_session.py b/tests/test_session.py index 4ff4fa15..beefa6e5 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -74,9 +74,10 @@ def test_create_then_load_numeric(self): def test_create_then_load_timestamp(self): from datetime import datetime as D + from datetime import timezone bio = BytesIO() - contents = [D(2021, 2, 6), D(2020, 11, 5), D(2974, 8, 1), D(1873, 12, 28)] - contents = [utils.to_timestamp(c) for c in contents] + contents = [D(2021, 2, 6, tzinfo=timezone.utc), D(2020, 11, 5, tzinfo=timezone.utc), D(2974, 8, 1, tzinfo=timezone.utc), D(1873, 12, 28, tzinfo=timezone.utc)] + contents = [c.timestamp() for c in contents] with session.Session() as s: with h5py.File(bio, 'w') as src: diff --git a/tests/test_utils.py b/tests/test_utils.py index e014027a..72f0c70a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -102,23 +102,3 @@ def test_get_min_max_for_permitted_types(self): self.assertEqual(min_value, expected_min_max_values[value_type][0]) self.assertEqual(max_value, expected_min_max_values[value_type][1]) - def test_to_timestamp(self): - from datetime import datetime - dts = [datetime(1874, 7, 27), datetime(1974, 1, 1), datetime(2020, 1, 1), datetime(2021, 6, 1), - datetime(2030, 5, 5), datetime(3030, 6, 6)] - dt_ts = [to_timestamp(d) for d in dts] - expected = [-3011558400.0, 126230400.0, 1577836800.0, 1622505600.0, 1904169600.0, 33463843200.0] - self.assertListEqual(dt_ts, expected) - - def test_to_datetime(self): - from datetime import datetime, timezone - dt_ts = [-3011558400.0, 126230400.0, 1577836800.0, 1622505600.0, 1904169600.0, 33463843200.0] - dts = [to_datetime(d) for d in dt_ts] - expected = [datetime(1874, 7, 27, tzinfo=timezone.utc), datetime(1974, 1, 1, tzinfo=timezone.utc), datetime(2020, 1, 1, tzinfo=timezone.utc), datetime(2021, 6, 1, tzinfo=timezone.utc), - datetime(2030, 5, 5, tzinfo=timezone.utc), datetime(3030, 6, 6, tzinfo=timezone.utc)] - self.assertListEqual(dts, expected) - - import pytz - dts = [to_datetime(d, tz=pytz.timezone('Europe/London')) for d in dt_ts] - for d in dts: - self.assertEqual(d.tzinfo, pytz.timezone('Europe/London')) From c98b87ce5e7291896be3a39febecc882548082e0 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 14 Oct 2021 16:40:26 +0100 Subject: [PATCH 135/181] finanlize workflow file, compile react to publish action only --- .github/workflows/python-app.yml | 5 ++++- .github/workflows/python-publish-linux.yml | 2 -- .github/workflows/python-publish-macos.yml | 2 -- .github/workflows/python-publish-win.yml | 4 ---- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index f1c89e6a..4776c941 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -12,7 +12,10 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, windows-latest, ubuntu-latest] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/python-publish-linux.yml b/.github/workflows/python-publish-linux.yml index ae2a5557..4e0d74f9 100644 --- a/.github/workflows/python-publish-linux.yml +++ b/.github/workflows/python-publish-linux.yml @@ -9,8 +9,6 @@ name: Build & upload package on Linux on: - pull_request: - branches: [ master ] release: types: [published] diff --git a/.github/workflows/python-publish-macos.yml b/.github/workflows/python-publish-macos.yml index 0ba03d47..28e90266 100644 --- a/.github/workflows/python-publish-macos.yml +++ b/.github/workflows/python-publish-macos.yml @@ -9,8 +9,6 @@ name: Build & upload package on MacOS on: - pull_request: - branches: [ master ] release: types: [published] diff --git a/.github/workflows/python-publish-win.yml b/.github/workflows/python-publish-win.yml index aca44d8f..817f78cc 100644 --- a/.github/workflows/python-publish-win.yml +++ b/.github/workflows/python-publish-win.yml @@ -9,10 +9,6 @@ name: Build & upload package on Windows on: - push: - branches: [ master ] - pull_request: - branches: [ master ] release: types: [published] From a57c4131aa2649a6f40021f1bd67ab931c879ff8 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 14 Oct 2021 16:51:13 +0100 Subject: [PATCH 136/181] avoid the bytearray vs string error in windows by converting result to bytearray --- tests/test_importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_importer.py b/tests/test_importer.py index f376a42d..b55e2c8b 100644 --- a/tests/test_importer.py +++ b/tests/test_importer.py @@ -347,7 +347,7 @@ def test_categorical_field_importer_with_small_chunk_size(self): with h5py.File(bio, 'r') as hf: self.assertEqual(hf['schema_key']['postcode']['values'][:].tolist(), expected_postcode_value_list) - self.assertEqual(hf['schema_key']['postcode']['key_names'][:].tolist(), expected_key_names) + self.assertEqual([bytearray(i, 'utf-8') for i in hf['schema_key']['postcode']['key_names'][:].tolist()], expected_key_names) self.assertEqual(hf['schema_key']['postcode']['key_values'][:].tolist(), expected_key_values) From 764650b7aa11e8e110bd812a93b3ee53d2500479 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 14 Oct 2021 17:00:29 +0100 Subject: [PATCH 137/181] fixing string vs bytesarray issue --- tests/test_importer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_importer.py b/tests/test_importer.py index b55e2c8b..58a169a2 100644 --- a/tests/test_importer.py +++ b/tests/test_importer.py @@ -347,7 +347,10 @@ def test_categorical_field_importer_with_small_chunk_size(self): with h5py.File(bio, 'r') as hf: self.assertEqual(hf['schema_key']['postcode']['values'][:].tolist(), expected_postcode_value_list) - self.assertEqual([bytearray(i, 'utf-8') for i in hf['schema_key']['postcode']['key_names'][:].tolist()], expected_key_names) + if isinstance(hf['schema_key']['postcode']['key_names'][0], str): + self.assertEqual([bytearray(i, 'utf-8') for i in hf['schema_key']['postcode']['key_names'][:].tolist()], expected_key_names) + else: + self.assertEqual(hf['schema_key']['postcode']['key_names'][:].tolist(), expected_key_names) self.assertEqual(hf['schema_key']['postcode']['key_values'][:].tolist(), expected_key_values) From 4676901b668afd72a444df87ef63fe725a4b1ad4 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 15 Oct 2021 09:47:55 +0100 Subject: [PATCH 138/181] update categorical field key property, change the key, value to bytes if it is a str --- exetera/core/fields.py | 10 ++++++++-- tests/test_dataframe.py | 4 ++-- tests/test_importer.py | 6 ++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 799ed4e4..e99a87b4 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -1557,8 +1557,14 @@ def nformat(self): @property def keys(self): self._ensure_valid() - kv = self._field['key_values'] - kn = self._field['key_names'] + if isinstance(self._field['key_values'][0], str): # convert into bytearray to keep up with linux + kv = [bytes(i, 'utf-8') for i in self._field['key_values']] + else: + kv = self._field['key_values'] + if isinstance(self._field['key_names'][0], str): + kn = [bytes(i, 'utf-8') for i in self._field['key_names']] + else: + kn = self._field['key_names'] keys = dict(zip(kv, kn)) return keys diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 54736fde..c173af68 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -70,11 +70,11 @@ def test_dataframe_create_numeric(self): a = df.create_numeric('a','int32') a.data.write(values) - total = np.sum(a.data[:]) + total = np.sum(a.data[:], dtype=np.int64) self.assertEqual(49997540637149, total) a.data[:] = a.data[:] * 2 - total = np.sum(a.data[:]) + total = np.sum(a.data[:], dtype=np.int64) self.assertEqual(99995081274298, total) def test_dataframe_create_categorical(self): diff --git a/tests/test_importer.py b/tests/test_importer.py index 58a169a2..fd4140f3 100644 --- a/tests/test_importer.py +++ b/tests/test_importer.py @@ -344,13 +344,11 @@ def test_categorical_field_importer_with_small_chunk_size(self): ds = s.get_dataset(self.ds_name) df = ds.get_dataframe('schema_key') self.assertEqual(df['postcode'].data[:].tolist(), expected_postcode_value_list) + self.assertEqual(list(df['postcode'].keys.values()), expected_key_names) with h5py.File(bio, 'r') as hf: self.assertEqual(hf['schema_key']['postcode']['values'][:].tolist(), expected_postcode_value_list) - if isinstance(hf['schema_key']['postcode']['key_names'][0], str): - self.assertEqual([bytearray(i, 'utf-8') for i in hf['schema_key']['postcode']['key_names'][:].tolist()], expected_key_names) - else: - self.assertEqual(hf['schema_key']['postcode']['key_names'][:].tolist(), expected_key_names) + #self.assertEqual(hf['schema_key']['postcode']['key_names'][:].tolist(), expected_key_names) self.assertEqual(hf['schema_key']['postcode']['key_values'][:].tolist(), expected_key_values) From e5d74c65b0e1d95c3b1bfa8699c3977b1b586cc3 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 15 Oct 2021 10:29:44 +0100 Subject: [PATCH 139/181] solved index must be np.int64 error --- exetera/core/persistence.py | 4 ++-- exetera/core/readerwriter.py | 2 +- tests/test_parsers.py | 4 ++-- tests/test_persistence.py | 6 +++--- tests/test_session.py | 8 ++++---- tests/test_utils.py | 1 - 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/exetera/core/persistence.py b/exetera/core/persistence.py index d591f427..24bbfd61 100644 --- a/exetera/core/persistence.py +++ b/exetera/core/persistence.py @@ -169,7 +169,7 @@ def _apply_sort_to_array(index, values): @njit def _apply_sort_to_index_values(index, indices, values): - s_indices = np.zeros_like(indices) + s_indices = np.zeros_like(indices, dtype=np.int64) s_values = np.zeros_like(values) accumulated = np.int64(0) s_indices[0] = 0 @@ -1029,7 +1029,7 @@ def apply_spans_concat(self, spans, reader, writer): src_index = reader.field['index'][:] src_values = reader.field['values'][:] - dest_index = np.zeros(reader.chunksize, src_index.dtype) + dest_index = np.zeros(reader.chunksize, np.int64) dest_values = np.zeros(reader.chunksize * 16, src_values.dtype) max_index_i = reader.chunksize diff --git a/exetera/core/readerwriter.py b/exetera/core/readerwriter.py index 4cb9c8bd..7710df68 100644 --- a/exetera/core/readerwriter.py +++ b/exetera/core/readerwriter.py @@ -60,7 +60,7 @@ def dtype(self): return self.field['index'].dtype, self.field['values'].dtype def sort(self, index, writer): - field_index = self.field['index'][:] + field_index = np.array(self.field['index'][:], dtype=np.int64) field_values = self.field['values'][:] r_field_index, r_field_values =\ pers._apply_sort_to_index_values(index, field_index, field_values) diff --git a/tests/test_parsers.py b/tests/test_parsers.py index d2dff732..ebc4e330 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -191,7 +191,7 @@ def test_read_csv_only_datetime_field(self): expected_updated_at_list = ['2020-05-12 07:00:00', '2020-05-13 01:00:00', '2020-05-14 03:00:00', '2020-05-15 03:00:00', '2020-05-16 03:00:00'] expected_updated_at_date_list = [b'2020-05-12', b'2020-05-13', b'2020-05-14',b'2020-05-15',b'2020-05-16'] - self.assertEqual(df['updated_at'].data[:].tolist(), [datetime.strptime(x, "%Y-%m-%d %H:%M:%S").timestamp() for x in expected_updated_at_list]) + self.assertEqual(df['updated_at'].data[:].tolist(), [datetime.strptime(x, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc).timestamp() for x in expected_updated_at_list]) self.assertEqual(df['updated_at_day'].data[:].tolist(),expected_updated_at_date_list ) @@ -204,7 +204,7 @@ def test_read_csv_only_date_field(self): parsers.read_csv(self.csv_file_name, df, self.schema_dict, include=['birthday']) expected_birthday_date = [b'1990-01-01', b'1980-03-04', b'1970-04-05', b'1960-04-05', b'1950-04-05'] - self.assertEqual(df['birthday'].data[:].tolist(), [datetime.strptime(x.decode(), "%Y-%m-%d").timestamp() for x in expected_birthday_date]) + self.assertEqual(df['birthday'].data[:].tolist(), [datetime.strptime(x.decode(), "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp() for x in expected_birthday_date]) self.assertEqual(df['birthday_day'].data[:].tolist(), expected_birthday_date) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index faf22d5e..59e1f6ab 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1042,7 +1042,7 @@ def filter_framework(name, raw_indices, raw_values, the_filter, expected): with h5py.File(bio, 'w') as hf: rw.IndexedStringWriter(datastore, hf, 'foo', ts).write(values) - raw_indices = hf['foo']['index'][:] + raw_indices = np.array(hf['foo']['index'][:], dtype=np.int64) raw_values = hf['foo']['values'][:] even_filter = np.zeros(len(values), bool) @@ -1098,7 +1098,7 @@ def index_framework(name, raw_indices, raw_values, the_indices, expected): with h5py.File(bio, 'w') as hf: rw.IndexedStringWriter(datastore, hf, 'foo', ts).write(values) - raw_indices = hf['foo']['index'][:] + raw_indices = np.array(hf['foo']['index'][:], dtype=np.int64) raw_values = hf['foo']['values'][:] even_indices = np.arange(0, len(values), 2) @@ -1390,7 +1390,7 @@ def test_sorting_indexed_string(self): vals = rw.IndexedStringReader(datastore, hf['vals']) wvals = vals.get_writer(hf, 'sorted_vals', ts) - vals.sort(np.asarray(si, dtype=np.uint32), wvals) + vals.sort(np.asarray(si, dtype=np.int32), wvals) actual = rw.IndexedStringReader(datastore, hf['sorted_vals'])[:] expected = [sv[i] for i in si] self.assertListEqual(expected, actual) diff --git a/tests/test_session.py b/tests/test_session.py index beefa6e5..463b6d63 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1011,11 +1011,11 @@ def test_write_then_read_numeric(self): a = fields.NumericField(s, hf['a'], None, write_enabled=True) a.data.write(values) - total = np.sum(a.data[:]) + total = np.sum(a.data[:], dtype=np.int64) self.assertEqual(49997540637149, total) a.data[:] = a.data[:] * 2 - total = np.sum(a.data[:]) + total = np.sum(a.data[:], dtype=np.int64) self.assertEqual(99995081274298, total) def test_write_then_read_categorical(self): @@ -1161,7 +1161,7 @@ def test_numeric_importer(self): def test_date_importer(self): - from datetime import datetime + from datetime import datetime, timezone bio = BytesIO() with session.Session() as s: dst = s.open_dataset(bio,'r+', 'dst') @@ -1172,4 +1172,4 @@ def test_date_importer(self): foo.import_part(indices, values, offsets, 0, written_row_count) expected_date_list = ['2020-05-10', '2020-05-12', '2020-05-12', '2020-05-15'] - self.assertListEqual(hf['foo'].data[:].tolist(), [datetime.strptime(x, "%Y-%m-%d").timestamp() for x in expected_date_list]) + self.assertListEqual(hf['foo'].data[:].tolist(), [datetime.strptime(x, "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp() for x in expected_date_list]) diff --git a/tests/test_utils.py b/tests/test_utils.py index 72f0c70a..ac807007 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -14,7 +14,6 @@ import numpy as np from exetera.core.utils import find_longest_sequence_of, to_escaped, bytearray_to_escaped, get_min_max -from exetera.core.utils import to_timestamp, to_datetime class TestUtils(unittest.TestCase): From 030d58752e7fcabb04ecc46a4ba1cbf97ef64b92 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 15 Oct 2021 11:05:25 +0100 Subject: [PATCH 140/181] all unittest error on windoes removed --- tests/test_csv_reader_speedup.py | 2 +- tests/test_parsers.py | 3 ++- tests/test_persistence.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_csv_reader_speedup.py b/tests/test_csv_reader_speedup.py index f3826e30..a2082d28 100644 --- a/tests/test_csv_reader_speedup.py +++ b/tests/test_csv_reader_speedup.py @@ -266,7 +266,7 @@ def test_read_file_on_only_categorical_field(self, mock_fromfile): # print(result) # print(df[field]) self.assertEqual(len(result), len(df[field])) - self.assertListEqual(result, list(df[field])) + self.assertListEqual([i.replace('\r', '') for i in result], list(df[field])) # remove \r due to windoes @patch("numpy.fromfile") diff --git a/tests/test_parsers.py b/tests/test_parsers.py index ebc4e330..5201f23d 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -231,7 +231,8 @@ def test_read_csv_with_schema_missing_field(self): missing_schema_dict = {'name': String()} parsers.read_csv(self.csv_file_name, df, missing_schema_dict) self.assertListEqual(df['id'].data[:], ['1','2','3','4','5']) - self.assertEqual(df['updated_at'].data[:],['2020-05-12 07:00:00', '2020-05-13 01:00:00', '2020-05-14 03:00:00', '2020-05-15 03:00:00', '2020-05-16 03:00:00']) + self.assertEqual([i.replace('\r', '') for i in df['updated_at'].data[:]], # remove \r due to windows + ['2020-05-12 07:00:00', '2020-05-13 01:00:00', '2020-05-14 03:00:00', '2020-05-15 03:00:00', '2020-05-16 03:00:00']) self.assertEqual(df['birthday'].data[:], ['1990-01-01', '1980-03-04', '1970-04-05', '1960-04-05', '1950-04-05']) self.assertEqual(df['postcode'].data[:], ['NW1', 'SW1P', 'E1', '', 'NW3']) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 59e1f6ab..0ecb2f69 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -540,7 +540,7 @@ def test_categorical_field_writer_from_reader(self): '', 'True', 'False', 'False', '', '', 'True', 'False', 'True', '', '', 'True', 'False', 'False', ''] value_map = {'': 0, 'False': 1, 'True': 2} - rw.CategoricalImporter(datastore, hf, 'foo', value_map, ts).write(values) + rw.CategoricalImporter(datastore, hf, 'foo', value_map, ts).write_strings(values) reader = datastore.get_reader(hf['foo']) writer = reader.get_writer(hf, 'foo2', ts) From 7cf7bae393844db0470bb1a1576443715e8df073 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 15 Oct 2021 11:19:03 +0100 Subject: [PATCH 141/181] minor update on workflow file --- .github/workflows/python-app.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 21896f86..610843f8 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -26,8 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 numpy numba pandas h5py - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install flake8 numpy numba pandas h5py cython - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names From 521142e5633eb2a675cf03671264c8ba8b036faf Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 15 Oct 2021 11:21:32 +0100 Subject: [PATCH 142/181] minor update workflow file --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 610843f8..e5353923 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -35,4 +35,4 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with unittest run: | - python -m unittest tests/* + python -m unittest From 9373fd21b8cf92bd09c5e37cd747fa036793043b Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 15 Oct 2021 15:22:05 +0100 Subject: [PATCH 143/181] minor fix: use pip install -r ; remove unused import in utils.py --- .github/workflows/python-app.yml | 2 +- .github/workflows/python-publish-linux.yml | 2 +- .github/workflows/python-publish-macos.yml | 2 +- .github/workflows/python-publish-win.yml | 2 +- exetera/core/utils.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index e5353923..ad45694f 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 numpy numba pandas h5py cython + pip install -r requirements.txt - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/.github/workflows/python-publish-linux.yml b/.github/workflows/python-publish-linux.yml index 4e0d74f9..ee561801 100644 --- a/.github/workflows/python-publish-linux.yml +++ b/.github/workflows/python-publish-linux.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install build setuptools wheel cython twine + pip install -r requirements.txt - name: Set up GCC uses: egor-tensin/setup-gcc@v1 with: diff --git a/.github/workflows/python-publish-macos.yml b/.github/workflows/python-publish-macos.yml index 28e90266..bf54a888 100644 --- a/.github/workflows/python-publish-macos.yml +++ b/.github/workflows/python-publish-macos.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install build setuptools wheel cython twine + pip install -r requirements.txt - name: Build package run: python setup.py bdist_wheel - name: Publish package diff --git a/.github/workflows/python-publish-win.yml b/.github/workflows/python-publish-win.yml index 817f78cc..443451f8 100644 --- a/.github/workflows/python-publish-win.yml +++ b/.github/workflows/python-publish-win.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install build setuptools wheel cython twine + pip install -r requirements.txt - name: Set up MinGW uses: egor-tensin/setup-mingw@v2 with: diff --git a/exetera/core/utils.py b/exetera/core/utils.py index 077ed779..a65bb4e7 100644 --- a/exetera/core/utils.py +++ b/exetera/core/utils.py @@ -12,7 +12,7 @@ import time from collections import defaultdict import csv -from datetime import datetime, timezone, timedelta +from datetime import datetime from io import StringIO import numpy as np From 6f67ac4bd940d930e5910390e762adc266f1561e Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 15 Oct 2021 15:29:26 +0100 Subject: [PATCH 144/181] update action file --- .github/workflows/python-app.yml | 1 + .github/workflows/python-publish-linux.yml | 1 + .github/workflows/python-publish-macos.yml | 1 + .github/workflows/python-publish-win.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index ad45694f..505930a4 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -26,6 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install flake8 pip install -r requirements.txt - name: Lint with flake8 run: | diff --git a/.github/workflows/python-publish-linux.yml b/.github/workflows/python-publish-linux.yml index ee561801..18550be5 100644 --- a/.github/workflows/python-publish-linux.yml +++ b/.github/workflows/python-publish-linux.yml @@ -26,6 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install flake8 pip install -r requirements.txt - name: Set up GCC uses: egor-tensin/setup-gcc@v1 diff --git a/.github/workflows/python-publish-macos.yml b/.github/workflows/python-publish-macos.yml index bf54a888..34ae492b 100644 --- a/.github/workflows/python-publish-macos.yml +++ b/.github/workflows/python-publish-macos.yml @@ -26,6 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install flake8 pip install -r requirements.txt - name: Build package run: python setup.py bdist_wheel diff --git a/.github/workflows/python-publish-win.yml b/.github/workflows/python-publish-win.yml index 443451f8..aff63278 100644 --- a/.github/workflows/python-publish-win.yml +++ b/.github/workflows/python-publish-win.yml @@ -26,6 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install flake8 pip install -r requirements.txt - name: Set up MinGW uses: egor-tensin/setup-mingw@v2 From 703a19a82109d151abb948e63c5c6fb6e4917a98 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 18 Oct 2021 16:10:49 +0100 Subject: [PATCH 145/181] remove change on test_presistence on uint32 to int32 --- tests/test_persistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0ecb2f69..65bd623d 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1390,7 +1390,7 @@ def test_sorting_indexed_string(self): vals = rw.IndexedStringReader(datastore, hf['vals']) wvals = vals.get_writer(hf, 'sorted_vals', ts) - vals.sort(np.asarray(si, dtype=np.int32), wvals) + vals.sort(np.asarray(si, dtype=np.uint32), wvals) actual = rw.IndexedStringReader(datastore, hf['sorted_vals'])[:] expected = [sv[i] for i in si] self.assertListEqual(expected, actual) From a5ab148410cb45f2a3ca9cb46a943d6511ba1b8d Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 1 Feb 2022 10:22:04 +0000 Subject: [PATCH 146/181] add output argument for describe function in dataframe, so that the result is not output to stdout during unittest --- exetera/core/dataframe.py | 21 ++++++++++---------- tests/test_dataframe.py | 42 +++++++++++++++++++-------------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index b3c09c3b..f067f4c7 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -567,12 +567,13 @@ def groupby(self, by: Union[str, List[str]], hint_keys_is_sorted=False): return HDF5DataFrameGroupBy(self._columns, by, sorted_index, spans) - def describe(self, include=None, exclude=None): + def describe(self, include=None, exclude=None, output='terminal'): """ Show the basic statistics of the data in each field. :param include: The field name or data type or simply 'all' to indicate the fields included in the calculation. :param exclude: The filed name or data type to exclude in the calculation. + :param output: Display the result in stdout if set to terminal, otherwise silent. :return: A dataframe contains the statistic results. """ @@ -718,15 +719,15 @@ def describe(self, include=None, exclude=None): # display columns_to_show = ['fields', 'count', 'unique', 'top', 'freq', 'mean', 'std', 'min', '25%', '50%', '75%', 'max'] # 5 fields each time for display - for col in range(0, len(result['fields']), 5): # 5 column each time - for i in columns_to_show: - if i in result: - print(i, end='\t') - for f in result[i][col:col + 5 if col + 5 < len(result[i]) - 1 else len(result[i])]: - print('{:>15}'.format(f), end='\t') - print('') - print('\n') - + if output == 'terminal': + for col in range(0, len(result['fields']), 5): # 5 column each time + for i in columns_to_show: + if i in result: + print(i, end='\t') + for f in result[i][col:col + 5 if col + 5 < len(result[i]) - 1 else len(result[i])]: + print('{:>15}'.format(f), end='\t') + print('') + print('\n') return result diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 9bfbc9ab..2e6ba83c 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -944,7 +944,7 @@ def test_describe_default(self): df.create_timestamp('ts1').data.write([1632234128 + i for i in range(20)]) df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) - result = df.describe() + result = df.describe(output='None') expected = {'fields': ['num', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], @@ -962,7 +962,7 @@ def test_describe_include(self): df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) - result = df.describe(include='all') + result = df.describe(include='all', output='None') expected = {'fields': ['num', 'fs1', 'ts1', 'c1', 'is1'], 'count': [10, 20, 20, 20, 20], 'mean': ['4.50', 'NaN', '1632234137.50', 'NaN', 'NaN'], 'std': ['2.87', 'NaN', '5.77', 'NaN', 'NaN'], 'min': ['0.00', 'NaN', '1632234128.00', 'NaN', 'NaN'], '25%': ['0.02', 'NaN', '1632234128.05', 'NaN', 'NaN'], @@ -971,24 +971,24 @@ def test_describe_include(self): 'top': ['NaN', b'a', 'NaN', 1, 'abc'], 'freq': ['NaN', 20, 'NaN', 20, 20]} self.assertEqual(result, expected) - result = df.describe(include='num') + result = df.describe(include='num', output='None') expected = {'fields': ['num'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} self.assertEqual(result, expected) - result = df.describe(include=['num', 'fs1']) + result = df.describe(include=['num', 'fs1'], output='None') expected = {'fields': ['num', 'fs1'], 'count': [10, 20], 'mean': ['4.50', 'NaN'], 'std': ['2.87', 'NaN'], 'min': ['0.00', 'NaN'], '25%': ['0.02', 'NaN'], '50%': ['0.04', 'NaN'], '75%': ['0.07', 'NaN'], 'max': ['9.00', 'NaN'], 'unique': ['NaN', 1], 'top': ['NaN', b'a'], 'freq': ['NaN', 20]} self.assertEqual(result, expected) - result = df.describe(include=np.int32) + result = df.describe(include=np.int32, output='None') expected = {'fields': ['num', 'c1'], 'count': [10, 20], 'mean': ['4.50', 'NaN'], 'std': ['2.87', 'NaN'], 'min': ['0.00', 'NaN'], '25%': ['0.02', 'NaN'], '50%': ['0.04', 'NaN'], '75%': ['0.07', 'NaN'], 'max': ['9.00', 'NaN'], 'unique': ['NaN', 1], 'top': ['NaN', 1], 'freq': ['NaN', 20]} self.assertEqual(result, expected) - result = df.describe(include=[np.int32, np.bytes_]) + result = df.describe(include=[np.int32, np.bytes_], output='None') expected = {'fields': ['num', 'c1', 'fs1'], 'count': [10, 20, 20], 'mean': ['4.50', 'NaN', 'NaN'], 'std': ['2.87', 'NaN', 'NaN'], 'min': ['0.00', 'NaN', 'NaN'], '25%': ['0.02', 'NaN', 'NaN'], '50%': ['0.04', 'NaN', 'NaN'], '75%': ['0.07', 'NaN', 'NaN'], 'max': ['9.00', 'NaN', 'NaN'], @@ -1008,27 +1008,27 @@ def test_describe_exclude(self): df.create_categorical('c1', 'int32', {'a': 1, 'b': 2}).data.write([1 for i in range(20)]) df.create_indexed_string('is1').data.write(['abc' for i in range(20)]) - result = df.describe(exclude='num') + result = df.describe(exclude='num', output='None') expected = {'fields': ['num2', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], 'max': ['9.00', '1632234147.00']} self.assertEqual(result, expected) - result = df.describe(exclude=['num', 'num2']) + result = df.describe(exclude=['num', 'num2'], output='None') expected = {'fields': ['ts1'], 'count': [20], 'mean': ['1632234137.50'], 'std': ['5.77'], 'min': ['1632234128.00'], '25%': ['1632234128.05'], '50%': ['1632234128.10'], '75%': ['1632234128.14'], 'max': ['1632234147.00']} self.assertEqual(result, expected) - result = df.describe(exclude=np.int32) + result = df.describe(exclude=np.int32, output='None') expected = {'fields': ['num2', 'ts1'], 'count': [10, 20], 'mean': ['4.50', '1632234137.50'], 'std': ['2.87', '5.77'], 'min': ['0.00', '1632234128.00'], '25%': ['0.02', '1632234128.05'], '50%': ['0.04', '1632234128.10'], '75%': ['0.07', '1632234128.14'], 'max': ['9.00', '1632234147.00']} self.assertEqual(result, expected) - result = df.describe(exclude=[np.int32, np.float64]) + result = df.describe(exclude=[np.int32, np.float64], output='None') expected = {'fields': ['num2'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} self.assertEqual(result, expected) @@ -1047,31 +1047,31 @@ def test_describe_include_and_exclude(self): #str * with self.assertRaises(Exception) as context: - df.describe(include='num', exclude='num') + df.describe(include='num', exclude='num', output='None') self.assertTrue(isinstance(context.exception, ValueError)) # list of str , str with self.assertRaises(Exception) as context: - df.describe(include=['num', 'num2'], exclude='num') + df.describe(include=['num', 'num2'], exclude='num', output='None') self.assertTrue(isinstance(context.exception, ValueError)) # list of str , type - result = df.describe(include=['num', 'num2'], exclude=np.int32) + result = df.describe(include=['num', 'num2'], exclude=np.int32, output='None') expected = {'fields': ['num2'], 'count': [10], 'mean': ['4.50'], 'std': ['2.87'], 'min': ['0.00'], '25%': ['0.02'], '50%': ['0.04'], '75%': ['0.07'], 'max': ['9.00']} self.assertEqual(result, expected) # list of str , list of str with self.assertRaises(Exception) as context: - df.describe(include=['num', 'num2'], exclude=['num', 'num2']) + df.describe(include=['num', 'num2'], exclude=['num', 'num2'], output='None') self.assertTrue(isinstance(context.exception, ValueError)) # list of str , list of type - result = df.describe(include=['num', 'num2', 'ts1'], exclude=[np.int32, np.int64]) + result = df.describe(include=['num', 'num2', 'ts1'], exclude=[np.int32, np.int64], output='None') expected = {'fields': ['ts1'], 'count': [20], 'mean': ['1632234137.50'], 'std': ['5.77'], 'min': ['1632234128.00'], '25%': ['1632234128.05'], '50%': ['1632234128.10'], '75%': ['1632234128.14'], 'max': ['1632234147.00']} self.assertEqual(result, expected) # type, str - result = df.describe(include=np.number, exclude='num2') + result = df.describe(include=np.number, exclude='num2', output='None') expected = {'fields': ['num', 'ts1', 'c1'], 'count': [10, 20, 20], 'mean': ['4.50', '1632234137.50', 'NaN'], 'std': ['2.87', '5.77', 'NaN'], 'min': ['0.00', '1632234128.00', 'NaN'], '25%': ['0.02', '1632234128.05', 'NaN'], '50%': ['0.04', '1632234128.10', 'NaN'], @@ -1083,7 +1083,7 @@ def test_describe_include_and_exclude(self): df.describe(include=np.int32, exclude=np.int64) self.assertTrue(isinstance(context.exception, ValueError)) # type, list of str - result = df.describe(include=np.number, exclude=['num', 'num2']) + result = df.describe(include=np.number, exclude=['num', 'num2'], output='None') expected = {'fields': ['ts1', 'c1'], 'count': [20, 20], 'mean': ['1632234137.50', 'NaN'], 'std': ['5.77', 'NaN'], 'min': ['1632234128.00', 'NaN'], '25%': ['1632234128.05', 'NaN'], '50%': ['1632234128.10', 'NaN'], '75%': ['1632234128.14', 'NaN'], 'max': ['1632234147.00', 'NaN'], @@ -1091,21 +1091,21 @@ def test_describe_include_and_exclude(self): self.assertEqual(result, expected) # type, list of type with self.assertRaises(Exception) as context: - df.describe(include=np.int32, exclude=[np.int64, np.float64]) + df.describe(include=np.int32, exclude=[np.int64, np.float64], output='None') self.assertTrue(isinstance(context.exception, ValueError)) # list of type, str - result = df.describe(include=[np.int32, np.int64], exclude='num') + result = df.describe(include=[np.int32, np.int64], exclude='num', output='None') expected = {'fields': ['c1', 'num2'], 'count': [20, 10], 'mean': ['NaN', '4.50'], 'std': ['NaN', '2.87'], 'min': ['NaN', '0.00'], '25%': ['NaN', '0.02'], '50%': ['NaN', '0.04'], '75%': ['NaN', '0.07'], 'max': ['NaN', '9.00'], 'unique': [1, 'NaN'], 'top': [1, 'NaN'], 'freq': [20, 'NaN']} self.assertEqual(result, expected) # list of type, type with self.assertRaises(Exception) as context: - df.describe(include=[np.int32, np.int64], exclude=np.int64) + df.describe(include=[np.int32, np.int64], exclude=np.int64, output='None') self.assertTrue(isinstance(context.exception, ValueError)) # list of type, list of str - result = df.describe(include=[np.int32, np.int64], exclude=['num', 'num2']) + result = df.describe(include=[np.int32, np.int64], exclude=['num', 'num2'], output='None') expected = {'fields': ['c1'], 'count': [20], 'mean': ['NaN'], 'std': ['NaN'], 'min': ['NaN'], '25%': ['NaN'], '50%': ['NaN'], '75%': ['NaN'], 'max': ['NaN'], 'unique': [1], 'top': [1], 'freq': [20]} From f50ab1f496ac646cc9014abaf3e5d72228aef05b Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 1 Feb 2022 10:29:15 +0000 Subject: [PATCH 147/181] comment all print function in unittest --- tests/test_date_time_helpers.py | 2 +- tests/test_journalling.py | 2 +- tests/test_operations.py | 2 +- tests/test_persistence.py | 6 +++--- tests/test_utils.py | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_date_time_helpers.py b/tests/test_date_time_helpers.py index eb012280..0a5f1596 100644 --- a/tests/test_date_time_helpers.py +++ b/tests/test_date_time_helpers.py @@ -189,7 +189,7 @@ def test_generate_period_offset_map(self): end_dt = D(2021, 3, 1) periods = dth.get_periods(end_dt, start_dt, 'week', -1) periods.reverse() - print(periods) + #print(periods) class TestGetPeriodOffsets(unittest.TestCase): diff --git a/tests/test_journalling.py b/tests/test_journalling.py index 577e2bef..98c2e788 100644 --- a/tests/test_journalling.py +++ b/tests/test_journalling.py @@ -30,7 +30,7 @@ def test_journal_full(self): d1_id = np.chararray(9) d1_id[:] = np.asarray(['a', 'a', 'b', 'b', 'c', 'e', 'e', 'e', 'g']) d1_v1 = np.asarray([100, 101, 200, 201, 300, 500, 501, 502, 700]) - print(d1_id) + #print(d1_id) d1_jvf = np.asarray([ts1, ts1, ts1, ts1, ts1, ts1, ts1, ts1, ts1]) d1_jvt = np.asarray([tsf, tsf, tsf, tsf, tsf, tsf, tsf, tsf, tsf]) diff --git a/tests/test_operations.py b/tests/test_operations.py index b76d0814..57ce3685 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -1335,7 +1335,7 @@ class TestFieldImporter(unittest.TestCase): def test_get_byte_map(self): byte_map_keys, byte_map_key_indices, byte_map_value = ops.get_byte_map({'a':1, 'bb':2, 'ccc':3, 'dddd':4}) - print(ops.get_byte_map({'Yes':1, 'No':0})) + #print(ops.get_byte_map({'Yes':1, 'No':0})) expected_byte_map_keys = np.array([97, 98, 98, 99, 99, 99, 100, 100, 100, 100]) expected_byte_map_key_indices = np.array([0, 1, 3, 6, 10]) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 65bd623d..70d0f32b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -933,7 +933,7 @@ def test_move_group(self): try: a.move('x', '/b/y') except Exception as e: - print(e) + #print(e) self.assertEqual( "Unable to move link (an object with that name already exists)", str(e)) self.assertListEqual([1, 2, 3, 4, 5], hf['/b/y'][:].tolist()) @@ -961,8 +961,8 @@ def test_copy_group(self): da = hf2.create_group('a') for k in a.keys(): da.copy(a[k], da) - print(da.keys()) - print(da['b'].keys()) + #print(da.keys()) + #print(da['b'].keys()) def test_predicate(self): diff --git a/tests/test_utils.py b/tests/test_utils.py index 29d0c6a1..97d884ea 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -30,17 +30,17 @@ def test_csv_encode_decode(self): from io import StringIO src = ['A', '"B', 'C,D', 'E"F'] - print(src) + #print(src) with StringIO() as s: csvw = csv.writer(s) csvw.writerow(src) result = s.getvalue() - print(result) + #print(result) with StringIO(result) as s: csvr = csv.reader(s) result = next(csvr) - print(result) + #print(result) def test_to_escaped(self): self.assertEqual(to_escaped(''), '') From 94a074a21ffd322ad941ebf4ad4e997e87b5b87d Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 1 Feb 2022 11:01:55 +0000 Subject: [PATCH 148/181] modify the remap function for categorical field and categorical mem field --- exetera/core/fields.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index c2df6443..05e4b66a 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -839,11 +839,24 @@ def keys(self): return keys def remap(self, key_map, new_key): + """ + Remap the key names and key values. + + :param key_map: The mapping rule of convert the old key into the new key. + :param new_key: The new key. + :return: A CategoricalMemField with the new key. + """ + # make sure all key values are included in the key_map + for k in self._keys.values(): + if k not in [x[0] for x in key_map]: + raise ValueError("Not all old key values are included in the mapping rule.") + # remap the value values = self.data[:] + new_values = np.zeros(len(values), values.dtype) for k in key_map: - values = np.where(values == k[0], k[1], values) + new_values = np.where(values == k[0], k[1], new_values) result = CategoricalMemField(self._session, self._nformat, new_key) - result.data.write(values) + result.data.write(new_values) return result def apply_filter(self, filter_to_apply, target=None, in_place=False): @@ -1626,12 +1639,29 @@ def keys(self): return keys def remap(self, key_map, new_key): + """ + Remap the key names and key values. + + :param key_map: The mapping rule of convert the old key into the new key. + :param new_key: The new key. + :return: A CategoricalMemField with the new key. + """ self._ensure_valid() + # make sure all key values are included in the key_map + if isinstance(self._field['key_values'][0], str): # convert into bytearray to keep up with linux + kv = [bytes(i, 'utf-8') for i in self._field['key_values']] + else: + kv = self._field['key_values'] + for k in kv: + if k not in [x[0] for x in key_map]: + raise ValueError("Not all old key values are included in the mapping rule.") + #remap the value values = self.data[:] + new_values = np.zeros(len(values), values.dtype) for k in key_map: - values = np.where(values == k[0], k[1], values) + new_values = np.where(values == k[0], k[1], new_values) result = CategoricalMemField(self._session, self._nformat, new_key) - result.data.write(values) + result.data.write(new_values) return result def apply_filter(self, filter_to_apply, target=None, in_place=False): From ef563e901c0ff377e0a7ddaeb20d23fa5f14c85a Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Tue, 8 Feb 2022 18:34:24 +0000 Subject: [PATCH 149/181] Added check to ensure ExeTera entry point actually works after pip install --- .github/workflows/python-app.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 14789e62..b0be6ba1 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -59,4 +59,6 @@ jobs: run: | python -m pip install --upgrade pip pip install . + exetera --help + \ No newline at end of file From e0caad04110dcca33acec85acd1b7a07c7ddd52e Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 9 Feb 2022 10:43:44 +0000 Subject: [PATCH 150/181] Attempted Fix --- exetera/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exetera/__init__.py b/exetera/__init__.py index 0d19bbfb..05871510 100644 --- a/exetera/__init__.py +++ b/exetera/__init__.py @@ -1,5 +1,5 @@ -from . import core, processing +from . import io, core, processing from ._version import __version__ diff --git a/setup.py b/setup.py index dc66fb2e..6d0386b1 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ author='Ben Murray', author_email='benjamin.murray@kcl.ac.uk', license='http://www.apache.org/licenses/LICENSE-2.0', - packages=['exetera', 'exetera.core', 'exetera.processing'], + packages=['exetera', 'exetera.core', 'exetera.processing', 'exetera.io'], scripts=['exetera/bin/exetera'], ext_modules = cythonize(pyx_full_path), python_requires='>=3.7', From ba06c9f573f1b429db7c8f3d6fe21cf88f97f66c Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 9 Feb 2022 10:48:10 +0000 Subject: [PATCH 151/181] find_packages --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 6d0386b1..7a1889f4 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup +from setuptools import setup, find_packages from pkg_resources import parse_requirements from Cython.Build import cythonize @@ -28,7 +28,7 @@ author='Ben Murray', author_email='benjamin.murray@kcl.ac.uk', license='http://www.apache.org/licenses/LICENSE-2.0', - packages=['exetera', 'exetera.core', 'exetera.processing', 'exetera.io'], + packages=find_packages(), scripts=['exetera/bin/exetera'], ext_modules = cythonize(pyx_full_path), python_requires='>=3.7', From 25a3cc3736f2d08edfbd4b47b4f46b67bf80fc13 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 9 Feb 2022 10:54:18 +0000 Subject: [PATCH 152/181] Tweak --- exetera/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/exetera/__init__.py b/exetera/__init__.py index 05871510..80bc7aa0 100644 --- a/exetera/__init__.py +++ b/exetera/__init__.py @@ -2,5 +2,3 @@ from . import io, core, processing from ._version import __version__ - -from .io.parsers import read_csv \ No newline at end of file From fff9a1954cb21ba909959f90fc164bb30767eeff Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 14 Feb 2022 13:04:17 +0000 Subject: [PATCH 153/181] fixing issue 214 --- exetera/core/session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exetera/core/session.py b/exetera/core/session.py index 32cc2e53..c58ab8f7 100644 --- a/exetera/core/session.py +++ b/exetera/core/session.py @@ -467,7 +467,8 @@ def _apply_spans_src(self, dest_f.data.write(results) return results else: - results = np.zeros(len(spans) - 1, dtype=target_.dtype) + data_type = 'int32' if len(spans) < 2000000000 else 'int64' + results = np.zeros(len(spans) - 1, dtype=data_type) predicate(spans, target_, results) return results From ffbc4f052e8f8326dcde7581ea11fd8058765852 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 17 Feb 2022 15:18:47 +0000 Subject: [PATCH 154/181] fixing bug on dataset set item --- exetera/core/dataset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 60f19edf..b09d0fdf 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -205,6 +205,7 @@ def __setitem__(self, name: str, dataframe: DataFrame): # rename a dataframe del self._dataframes[dataframe.name] dataframe.name = name + self._dataframes[name] = dataframe self._file.move(dataframe.h5group.name, name) else: # new dataframe from another dataset From 7f82d5816529ab5fca8da9cac78612c0b949ee56 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 17 Feb 2022 15:47:03 +0000 Subject: [PATCH 155/181] fixing apply_span_src in fields.py --- exetera/core/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index ec5c9e29..ee1923b0 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -2252,8 +2252,8 @@ def _apply_spans_src(source: Field, raise ValueError("if 'in_place is True, 'target' must be None") spans_ = val.array_from_field_or_lower('spans', spans) - result_inds = np.zeros(len(spans)) - results = np.zeros(len(spans) - 1, dtype=source.data.dtype) + data_type = 'int32' if len(spans) < 2000000000 else 'int64' + results = np.zeros(len(spans) - 1, dtype=data_type) predicate(spans_, source.data[:], results) if in_place is True: From 474425a6158e0decb2e442529d3ee3dc98f71ba6 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 17 Feb 2022 16:48:27 +0000 Subject: [PATCH 156/181] revert change on field --- exetera/core/fields.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index ee1923b0..f4eb0533 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -2252,8 +2252,7 @@ def _apply_spans_src(source: Field, raise ValueError("if 'in_place is True, 'target' must be None") spans_ = val.array_from_field_or_lower('spans', spans) - data_type = 'int32' if len(spans) < 2000000000 else 'int64' - results = np.zeros(len(spans) - 1, dtype=data_type) + results = np.zeros(len(spans) - 1, dtype=source.data.dtype) predicate(spans_, source.data[:], results) if in_place is True: From 1c5926578d42182722f334b008e93ba0a82c6e16 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 17 Feb 2022 17:06:10 +0000 Subject: [PATCH 157/181] add unittest for dataset setitem bug --- tests/test_dataset.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index ab35a03f..7340b7e9 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -69,6 +69,11 @@ def test_dataset_init_with_data(self): self.assertTrue(isinstance(dst['df3'], DataFrame)) self.assertEqual([b'a', b'b', b'c', b'd'], dst['df3']['fs'].data[:].tolist()) + # set dataframe within the same dataset (rename) + dst['df4'] = dst['df3'] + self.assertTrue(isinstance(dst['df4'], DataFrame)) + self.assertEqual([b'a', b'b', b'c', b'd'], dst['df4']['fs'].data[:].tolist()) + def test_dataset_static_func(self): bio = BytesIO() bio2 = BytesIO() From 5002c65904dbb03c46cda2254de11e2272e86d44 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 18 Feb 2022 16:00:32 +0000 Subject: [PATCH 158/181] examples using dataset generated by randomdataset --- examples/README.md | 17 + examples/advanced_operations.ipynb | 651 +++++++++++++++++++++++++++ examples/basic_concept.ipynb | 542 ++++++++++++++++++++++ examples/simple_linked_dataset.ipynb | 489 ++++++++++++++++++++ 4 files changed, 1699 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/advanced_operations.ipynb create mode 100644 examples/basic_concept.ipynb create mode 100644 examples/simple_linked_dataset.ipynb diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..638bf025 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,17 @@ +## Examples of how to use ExeTera + +This folder contains a few examples on how to use ExeTera in different scenarios. + +#### Names dataset +This example shows how to generate ExeTera HDF5 datafile through 'importer.import_with_schema' function, and a few basic commands to print the dataset content. + +#### simple_linked_dataset +This example shows how to import multiple CSV files into a ExeTera HDF5 datafile. The example datafile has a similar structure to Covid Symptom Study (CSS) dataset, including a user table and a assessments table. + + +#### basic_concept +This example shows how to use ExeTera, through the major components: dataset, dataframe and fields. Please note this example is based on assessments.hdf5 file, hence please go through the simple_linked_dataset example and generate the hdf5 file first. + + +#### advanced_operations +This example shows the intermediate functions of ExeTera, such as filtering, group by, sorting, performance boosting using numba, and output the dataframe to csv file. Please note this example is based on assessments.hdf5 file, hence please go through the simple_linked_dataset example and generate the hdf5 file first. diff --git a/examples/advanced_operations.ipynb b/examples/advanced_operations.ipynb new file mode 100644 index 00000000..a4615384 --- /dev/null +++ b/examples/advanced_operations.ipynb @@ -0,0 +1,651 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a133c850-1cc3-4ee5-830a-9440e70cd90c", + "metadata": {}, + "source": [ + "This example uses the user_assessments hdfs file from RandomDataset. User assessments file contains a user table and a assessments table, that imitate the data structure of in CSS (Covid Symptom Study) project." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ac06eae4-8214-4e96-a419-d361698825a8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "temp2.hdf5 temp.hdf5 user_assessments.hdf5\n" + ] + } + ], + "source": [ + "!ls *hdf5" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6b742fb7-5572-4c15-b0a3-7e5e4a00733c", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('/home/jd21/ExeTera')" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "4eae8d06-0872-4e30-b6c8-2e8ba86fe9f4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Columns in users table: odict_keys(['FirstName', 'LastName', 'bmi', 'bmi_valid', 'has_diabetes', 'height_cm', 'height_cm_valid', 'id', 'j_valid_from', 'j_valid_to', 'year_of_birth', 'year_of_birth_valid'])\n", + "fields\t bmi\t has_diabetes\t height_cm\t year_of_birth\t\n", + "count\t 10\t 10\t 10\t 10\t\n", + "unique\t NaN\t 1\t NaN\t NaN\t\n", + "top\t NaN\t 0\t NaN\t NaN\t\n", + "freq\t NaN\t 10\t NaN\t NaN\t\n", + "mean\t 31.70\t NaN\t 135.60\t 1965.40\t\n", + "std\t 5.14\t NaN\t 25.39\t 24.87\t\n", + "min\t 25.00\t NaN\t 107.00\t 1926.00\t\n", + "25%\t 25.02\t NaN\t 107.20\t 1926.07\t\n", + "50%\t 25.05\t NaN\t 107.41\t 1926.13\t\n", + "75%\t 25.07\t NaN\t 107.61\t 1926.20\t\n", + "max\t 39.00\t NaN\t 190.00\t 2004.00\t\n", + "\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "{'fields': ['bmi', 'has_diabetes', 'height_cm', 'year_of_birth'],\n", + " 'count': [10, 10, 10, 10],\n", + " 'mean': ['31.70', 'NaN', '135.60', '1965.40'],\n", + " 'std': ['5.14', 'NaN', '25.39', '24.87'],\n", + " 'min': ['25.00', 'NaN', '107.00', '1926.00'],\n", + " '25%': ['25.02', 'NaN', '107.20', '1926.07'],\n", + " '50%': ['25.05', 'NaN', '107.41', '1926.13'],\n", + " '75%': ['25.07', 'NaN', '107.61', '1926.20'],\n", + " 'max': ['39.00', 'NaN', '190.00', '2004.00'],\n", + " 'unique': ['NaN', 1, 'NaN', 'NaN'],\n", + " 'top': ['NaN', 0, 'NaN', 'NaN'],\n", + " 'freq': ['NaN', 10, 'NaN', 'NaN']}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from exetera.core.session import Session\n", + "s = Session() # not recommended, but to cover all the cells in the example, we use this way here\n", + "src = s.open_dataset('user_assessments.hdf5', 'r', 'src')\n", + "\n", + "users = src['users']\n", + "print('Columns in users table:', users.keys())\n", + "# use describe to check the value in each column\n", + "users.describe(include=['bmi', 'has_diabetes', 'height_cm', 'year_of_birth'])" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "67d79440-4c2d-42ec-8f62-3d10bc72e3e7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Columns in users table: odict_keys(['abdominal_pain', 'brain_fog', 'date', 'id', 'j_valid_from', 'j_valid_to', 'loss_of_smell', 'temperature_f', 'temperature_f_valid', 'tested_covid_positive', 'user_id'])\n", + "fields\t abdominal_pain\t brain_fog\t date\t loss_of_smell\t temperature_f\t\n", + "count\t 30\t 30\t 30\t 30\t 30\t\n", + "unique\t 1\t 1\t NaN\t 1\t NaN\t\n", + "top\t 0\t 0\t NaN\t 0\t NaN\t\n", + "freq\t 30\t 30\t NaN\t 30\t NaN\t\n", + "mean\t NaN\t NaN\t 1628912712.34\t NaN\t 101.36\t\n", + "std\t NaN\t NaN\t 10077317.46\t NaN\t 4.33\t\n", + "min\t NaN\t NaN\t 1613872118.68\t NaN\t 95.23\t\n", + "25%\t NaN\t NaN\t 1613975491.70\t NaN\t 95.24\t\n", + "50%\t NaN\t NaN\t 1614078864.72\t NaN\t 95.26\t\n", + "75%\t NaN\t NaN\t 1614182237.74\t NaN\t 95.28\t\n", + "max\t NaN\t NaN\t 1644821469.46\t NaN\t 109.64\t\n", + "\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "{'fields': ['abdominal_pain',\n", + " 'brain_fog',\n", + " 'date',\n", + " 'loss_of_smell',\n", + " 'temperature_f'],\n", + " 'count': [30, 30, 30, 30, 30],\n", + " 'mean': ['NaN', 'NaN', '1628912712.34', 'NaN', '101.36'],\n", + " 'std': ['NaN', 'NaN', '10077317.46', 'NaN', '4.33'],\n", + " 'min': ['NaN', 'NaN', '1613872118.68', 'NaN', '95.23'],\n", + " '25%': ['NaN', 'NaN', '1613975491.70', 'NaN', '95.24'],\n", + " '50%': ['NaN', 'NaN', '1614078864.72', 'NaN', '95.26'],\n", + " '75%': ['NaN', 'NaN', '1614182237.74', 'NaN', '95.28'],\n", + " 'max': ['NaN', 'NaN', '1644821469.46', 'NaN', '109.64'],\n", + " 'unique': [1, 1, 'NaN', 1, 'NaN'],\n", + " 'top': [0, 0, 'NaN', 0, 'NaN'],\n", + " 'freq': [30, 30, 'NaN', 30, 'NaN']}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "asmts = src['assessments']\n", + "print('Columns in users table:', asmts.keys())\n", + "asmts.describe(include=['abdominal_pain', 'brain_fog', 'date','loss_of_smell', 'temperature_f'])" + ] + }, + { + "cell_type": "markdown", + "id": "3be7ec97-a8d2-449f-9f57-e54a4effb52c", + "metadata": {}, + "source": [ + "

Filtering

\n", + "Filtering is performed through the use of the apply_filter function. This can be performed on individual fields or at a dataframe level. apply_filter applies the filter on data rows.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "dab8e873-cf1f-47c5-bebb-18bde4357543", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "9 adults out of 10 total subjects found.\n" + ] + } + ], + "source": [ + "with Session() as s:\n", + " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", + " df = dst.create_dataframe('df')\n", + "\n", + " # apply a filter to the dataframe\n", + "\n", + " filt = (2022 - users['year_of_birth'].data[:]) > 18\n", + " users.apply_filter(filt, ddf=df) # non-destructive with ddf argument\n", + " print(len(df['id']), ' adults out of ', len(users['id']), ' total subjects found.')" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "c336d758-878a-4df6-8458-cc3ddf280964", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ True True True True True False True True True True]\n", + "[b'0' b'1' b'2' b'3' b'4' b'6' b'7' b'8' b'9']\n" + ] + } + ], + "source": [ + "# Combining filters\n", + "# we can make use of fields directly rather than fetching the underlying numpy arrays\n", + "# we recommend this approach in general\n", + "\n", + "filt = ((2022 - users['year_of_birth'].data[:]) > 18) & (users['has_diabetes'].data[:] == False)\n", + "print(filt)\n", + "\n", + "# fetching numpy arrays\n", + "print(users['id'].data[filt])" + ] + }, + { + "cell_type": "markdown", + "id": "3316eb91-2b59-46d9-9f4f-1262192d6807", + "metadata": {}, + "source": [ + "

Performance boost using numba

\n", + "As the underlying data is fetched as a numpy array, you can utlize the numba @njit functions to accelarate the data process. For example in the case of summing up symptoms, use a seperate function with @njit decrator can speed up the performance. " + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "c89f52b9-f96d-4e36-a8e3-67953aaedae4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4.321831703186035\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "import time\n", + "\n", + "#sum up the symptoms without njit\n", + "test_length = 1000000000 # here we use the a test length rather than 50 rows in the dataset, \n", + " # as the difference comes with more rows\n", + "symptoms = ['abdominal_pain', 'brain_fog', 'loss_of_smell']\n", + "t0 = time.time()\n", + "sum_symp = np.zeros(test_length, 'int32')\n", + "for i in symptoms:\n", + " sum_symp += np.zeros(test_length, 'int32')\n", + "#print(sum_symp)\n", + "print(time.time()-t0)" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "202321d4-b1fc-47bd-8878-779df121c913", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0 0 0 ... 0 0 0]\n", + "0.12068581581115723\n" + ] + } + ], + "source": [ + "#sum up the symptoms with njit\n", + "from numba import njit\n", + "\n", + "@njit\n", + "def sum_symptom(symp_data, sum_data):\n", + " sum_data += symp_data\n", + " return sum_data\n", + "\n", + "t0 = time.time()\n", + "sum_symp = np.zeros(test_length, 'int32')\n", + "for i in symptoms:\n", + " sum_symp = np.zeros(test_length, 'int32')\n", + "#print(sum_symp)\n", + "print(time.time()-t0) # 10x faster" + ] + }, + { + "cell_type": "markdown", + "id": "2723a9c9-36a5-4737-870c-7b4d130307f8", + "metadata": {}, + "source": [ + "

Groupby

" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "acfec7b4-01ec-4bf5-b8d3-d99a9db98273", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10 30\n", + "10 30\n", + "10 30\n", + "10 30\n" + ] + } + ], + "source": [ + "with Session() as s:\n", + " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", + " df = dst.create_dataframe('df')\n", + " #drop duplicates\n", + " asmts.drop_duplicates(by = 'user_id', ddf = df)\n", + " print(len(df['user_id']), len(asmts['user_id']))\n", + " \n", + " #count\n", + " df2 = dst.create_dataframe('df2')\n", + " asmts.groupby(by = 'user_id').count(ddf = df2)\n", + " print(len(df2['user_id']), len(asmts['user_id']))\n", + " \n", + " #min/ max\n", + " df3 = dst.create_dataframe('df3')\n", + " asmts.groupby(by = 'user_id').max(target ='date', ddf = df3)\n", + " print(len(df3['user_id']), len(asmts['user_id']))\n", + " df4 = dst.create_dataframe('df4')\n", + " asmts.groupby(by = 'user_id').min(target ='date', ddf = df4)\n", + " print(len(df4['user_id']), len(asmts['user_id']))\n", + "\n", + " #first/last\n", + " df5 = dst.create_dataframe('df5')\n", + " asmts.groupby(by = 'user_id').first(target ='date', ddf = df5)\n", + " df6 = dst.create_dataframe('df6')\n", + " asmts.groupby(by = 'user_id').last(target ='date', ddf = df6)" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "29ef8595-6835-4f0d-a5bf-6ee15bf8eabd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "30\n" + ] + } + ], + "source": [ + "#transform rather than group by\n", + "with Session() as s:\n", + " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", + " df = dst.create_dataframe('df')\n", + " \n", + " symptoms = ['abdominal_pain', 'brain_fog', 'loss_of_smell']\n", + " sum_symp = np.zeros(len(asmts['user_id']), 'int32')\n", + " for i in symptoms:\n", + " sum_symp += np.zeros(len(asmts['user_id']), 'int32')\n", + " \n", + " spans = asmts['user_id'].get_spans() # make sure asmts dataframe is sorted based on user_id\n", + " max_symp = np.zeros(len(asmts['user_id']), 'int32')\n", + " for i in range(len(spans)-1):\n", + " max_symp[spans[i]:spans[i+1]] = np.max(sum_symp.data[spans[i]:spans[i+1]])\n", + " #write data back to df\n", + " df.create_numeric('max_symp', 'int32').data.write(max_symp)\n", + " print(len(df['max_symp'].data)) # note the field length is the same with transform\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "93e5314b-d130-4ded-a41a-ca7fda798ae1", + "metadata": {}, + "source": [ + "

Join

\n", + "ExeTera provides functions that provide pandas-like merge functionality on DataFrame instances. We have made this operation as familiar as possible to Pandas users, but there are a couple of differences that we should highlight:\n", + "
\n", + "\n", + "• merge is provided as a function in the dataframe unit, rather than as a member function on DataFrame instances \n", + "
\n", + "• merge takes three dataframe arguments, left, right and dest. This is due to the fact that DataFrames are always backed up by a datastore and so rather than create an in-memory destination dataframe, the resulting merged fields must be written to a dataframe of your choosing. \n", + "
\n", + "• Note, this can either be a separate dataframe or it can be the dataframe that you are merging to (typically left in the case of a \"left\" merge and right in the case of a \"right\" merge\n", + "
\n", + "• merge takes a number of optional hint fields that can save time when working with large datasets. These specify whether the keys are unique or ordered and allow the merge to occur without first checking this\n", + "
\n", + "• merge has a number of highly scalable algorithms that can be used when the key data is sorted and / or unique." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "85261f6a-ba35-45a0-ba88-60a99a16ebe3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "30\n", + "odict_keys(['FirstName', 'LastName', 'bmi', 'bmi_valid', 'has_diabetes', 'height_cm', 'height_cm_valid', 'id_l', 'j_valid_from_l', 'j_valid_to_l', 'year_of_birth', 'year_of_birth_valid', 'abdominal_pain', 'brain_fog', 'date', 'id_r', 'j_valid_from_r', 'j_valid_to_r', 'loss_of_smell', 'temperature_f', 'temperature_f_valid', 'tested_covid_positive', 'user_id'])\n" + ] + } + ], + "source": [ + "from exetera.core.dataframe import merge\n", + "with Session() as s:\n", + " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", + " df = dst.create_dataframe('df')\n", + " merge(users, asmts, df, left_on='id', right_on='user_id', how='left')\n", + " print(len(df['id_l'].data)) # note as there are 'id' field in both dataframe, thus a suffix '_l' and '_r'\n", + " # are added to the merged dataframe \n", + " print(df.keys())" + ] + }, + { + "cell_type": "markdown", + "id": "899c862b-9a2a-4e08-8a5b-d7c2cadaf7b1", + "metadata": {}, + "source": [ + "

Sort

" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "f72666d8-a5e3-48e9-859d-1d1e56b4a768", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sorted ('id_l',) index in 0.0002894401550292969s\n", + " 'FirstName' reordered in 0.3324275016784668s\n", + " 'LastName' reordered in 0.0013616085052490234s\n", + " 'bmi' reordered in 0.0007426738739013672s\n", + " 'bmi_valid' reordered in 0.0006847381591796875s\n", + " 'has_diabetes' reordered in 0.0023627281188964844s\n", + " 'height_cm' reordered in 0.0006353855133056641s\n", + " 'height_cm_valid' reordered in 0.0005815029144287109s\n", + " 'id_l' reordered in 0.0005600452423095703s\n", + " 'j_valid_from_l' reordered in 0.0005393028259277344s\n", + " 'j_valid_to_l' reordered in 0.0005180835723876953s\n", + " 'year_of_birth' reordered in 0.0005507469177246094s\n", + " 'year_of_birth_valid' reordered in 0.0006158351898193359s\n", + " 'abdominal_pain' reordered in 0.0019788742065429688s\n", + " 'brain_fog' reordered in 0.0019555091857910156s\n", + " 'date' reordered in 0.0005359649658203125s\n", + " 'id_r' reordered in 0.0005869865417480469s\n", + " 'j_valid_from_r' reordered in 0.0005660057067871094s\n", + " 'j_valid_to_r' reordered in 0.0005273818969726562s\n", + " 'loss_of_smell' reordered in 0.001955270767211914s\n", + " 'temperature_f' reordered in 0.0005567073822021484s\n", + " 'temperature_f_valid' reordered in 0.0005667209625244141s\n", + " 'tested_covid_positive' reordered in 0.00225067138671875s\n", + " 'user_id' reordered in 0.0005781650543212891s\n", + "fields reordered in 0.3544754981994629s\n" + ] + } + ], + "source": [ + "from exetera.core.dataframe import merge\n", + "with Session() as s:\n", + " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", + " df = dst.create_dataframe('df')\n", + " merge(users, asmts, df, left_on='id', right_on='user_id', how='left')\n", + " s.sort_on(df, df, ('id_l',))" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "1d77e233-16ed-4333-bec8-e8bc8f0e297d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sorted ('id_l',) index in 0.00018310546875s\n", + " 'FirstName' reordered in 0.005521535873413086s\n", + " 'LastName' reordered in 0.005347490310668945s\n", + " 'bmi' reordered in 0.0029451847076416016s\n", + " 'bmi_valid' reordered in 0.0027124881744384766s\n", + " 'has_diabetes' reordered in 0.00469517707824707s\n", + " 'height_cm' reordered in 0.002373218536376953s\n", + " 'height_cm_valid' reordered in 0.002882719039916992s\n", + " 'id_l' reordered in 0.002743959426879883s\n", + " 'j_valid_from_l' reordered in 0.002353191375732422s\n", + " 'j_valid_to_l' reordered in 0.0024633407592773438s\n", + " 'year_of_birth' reordered in 0.002484560012817383s\n", + " 'year_of_birth_valid' reordered in 0.002560138702392578s\n", + " 'abdominal_pain' reordered in 0.00513005256652832s\n", + " 'brain_fog' reordered in 0.0047473907470703125s\n", + " 'date' reordered in 0.0025992393493652344s\n", + " 'id_r' reordered in 0.0029125213623046875s\n", + " 'j_valid_from_r' reordered in 0.005130767822265625s\n", + " 'j_valid_to_r' reordered in 0.003270387649536133s\n", + " 'loss_of_smell' reordered in 0.004971504211425781s\n", + " 'temperature_f' reordered in 0.0033884048461914062s\n", + " 'temperature_f_valid' reordered in 0.003406047821044922s\n", + " 'tested_covid_positive' reordered in 0.0054056644439697266s\n", + " 'user_id' reordered in 0.0029478073120117188s\n", + "fields reordered in 0.08384275436401367s\n" + ] + } + ], + "source": [ + "from exetera.core.dataframe import merge\n", + "with Session() as s:\n", + " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", + " df = dst.create_dataframe('df')\n", + " merge(users, asmts, df, left_on='id', right_on='user_id', how='left')\n", + " df2 = dst.create_dataframe('df2')\n", + " s.sort_on(df, df2, ('id_l',))" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "4e1396c8-e851-4e77-ba43-9ec4f9d01062", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[b'0' b'0' b'0' b'1' b'1' b'1' b'2' b'2' b'2' b'3' b'3' b'3' b'4' b'4'\n", + " b'4' b'5' b'5' b'5' b'6' b'6' b'6' b'7' b'7' b'7' b'8' b'8' b'8' b'9'\n", + " b'9' b'9']\n", + "[b'0' b'0' b'0' b'1' b'1' b'1' b'2' b'2' b'2' b'3' b'3' b'3' b'4' b'4'\n", + " b'4' b'5' b'5' b'5' b'6' b'6' b'6' b'7' b'7' b'7' b'8' b'8' b'8' b'9'\n", + " b'9' b'9']\n" + ] + } + ], + "source": [ + "#sorting with an index\n", + "with Session() as s:\n", + " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", + " df = dst.create_dataframe('df')\n", + " merge(users, asmts, df, left_on='id', right_on='user_id', how='left')\n", + "\n", + " index = s.dataset_sort_index((df['id_l'],))\n", + "\n", + " # apply indices to a destination dataframe\n", + " df2 = dst.create_dataframe('df2')\n", + " df.apply_index(index, df2)\n", + " print(df2['id_l'].data[:])\n", + " \n", + " # apply indices in place\n", + " df.apply_index(index)\n", + " print(df['id_l'].data[:])" + ] + }, + { + "cell_type": "markdown", + "id": "06e2a9e4-400a-44e9-b08e-ef5f282bf6bd", + "metadata": {}, + "source": [ + "

I/O

" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "e7c67e89-1651-41b6-9f76-697875a82d7a", + "metadata": {}, + "outputs": [], + "source": [ + "with Session() as s:\n", + " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", + " df = dst.create_dataframe('df')\n", + " merge(users, asmts, df, left_on='id', right_on='user_id', how='left')\n", + "\n", + " #output a dataframe to to_csv\n", + " df.to_csv('merged.csv')\n", + "\n", + " #output to csv with row filters\n", + " row_filter = (2022-df['year_of_birth'].data[:]) > 18\n", + " df.to_csv('adults.csv', row_filter) # save the data you want without change the underlying data in df\n", + "\n", + " #output to csv with column filters\n", + " df.to_csv('column_filtered.csv', column_filter=['id_l', 'year_of_birth', 'date', 'tested_covid_positive']) # save the columns you want" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "id": "12c08d25-f2ee-41c1-a5e8-73a34f5d94a2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "adults.csv column_filtered.csv merged.csv\n" + ] + } + ], + "source": [ + "!ls *csv" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "a58d47e2-7237-4cd4-938a-4ad50d2ceda1", + "metadata": {}, + "outputs": [], + "source": [ + "# close src dataset as we open dataset using s=Session()\n", + "# this is not necessary if we use context management by with Session as s:\n", + "s.close_dataset(src)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/basic_concept.ipynb b/examples/basic_concept.ipynb new file mode 100644 index 00000000..9e83d770 --- /dev/null +++ b/examples/basic_concept.ipynb @@ -0,0 +1,542 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d220666f-dad8-4229-9f24-9d24925b5c25", + "metadata": {}, + "source": [ + "

1, Import

\n", + "

ExeTera utlize HDF5 file format to acquire fast performance when processing the data. Hence the first step of using ExeTera is usually transform the file from other formats, e.g. csv, into HDF5.

\n", + "

ExeTera provides utilities to transform the csv data into HDF5, through either command line or code.

\n", + "How import works\n", + "

\n", + "a. Importing via the exetera import command:
\n", + "\n", + "exetera import
\n", + "-s path/to/covid_schema.json \\
\n", + "-i \"patients:path/to/patient_data.csv, assessments:path/to/assessmentdata.csv,
tests:path/to/covid_test_data.csv, diet:path/to/diet_study_data.csv\" \\
\n", + "-o /path/to/output_dataset_name.hdf5
\n", + "--include \"patients:(id,country_code,blood_group), assessments:(id,patient_id,chest_pain)\"
\n", + "--exclude \"tests:(country_code)\"

\n", + "\n", + "Arguments:
\n", + "-s/--schema: The location and name of the schema file
\n", + "-te/--territories: If set, this only imports the listed territories. If left unset, all territories are imported
\n", + "-i/--inputs : A comma separated list of 'name:file' pairs. This should be put in parentheses if it contains any whitespace. See the example above.
\n", + "-o/--output_hdf5: The path and name to where the resulting hdf5 dataset should be written
\n", + "-ts/--timestamp: An override for the timestamp to be written (defaults to datetime.now(timezone.utc))
\n", + "-w/--overwrite: If set, overwrite any existing dataset with the same name; appends to existing dataset otherwise
\n", + "-n/--include: If set, filters out all fields apart from those in the list.
\n", + "-x/--exclude: If set, filters out the fields in this list.
\n", + "

\n", + "\n", + "

\n", + "b. Importing through code
\n", + "Use importer.import_with_schema(timestamp, output_hdf5_name, schema, tokens, args.overwrite, include_fields, exclude_fields) \n", + "

\n" + ] + }, + { + "cell_type": "markdown", + "id": "febce312-098c-436a-a73f-ecea78c91047", + "metadata": {}, + "source": [ + "Import example\n", + "
\n", + "For the import example, please refer to the example in RandomDataset. After you finish, please copy the hdf5 file here to continue." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2271e28e-21e1-4720-9c32-4aaf9b310546", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "user_assessments.hdf5\n" + ] + } + ], + "source": [ + "!ls *hdf5" + ] + }, + { + "cell_type": "markdown", + "id": "46d1a2cd-85cb-4ea5-b794-ea8e07779bfb", + "metadata": {}, + "source": [ + "

2, ExeTera Session and DataSet

\n", + "

\n", + "Session instances are the top-level ExeTera class. They serve two main purposes:
\n", + "\n", + "1, Functionality for creating / opening / closing Dataset objects, as well as managing the lifetime of open datasets
\n", + "2, Methods that operate on Fields
\n", + "

\n", + "

Creating a session object

\n", + "

\n", + "Creating a Session object can be done multiple ways, but we recommend that you wrap the session in a context manager (with statement). This allows the Session object to automatically manage the datasets that you have opened, closing them all once the with statement is exited. Opening and closing datasets is very fast. When working in jupyter notebooks or jupyter lab, please feel free to create a new Session object for each cell.
\n", + "

" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "74ad07d9-df92-4668-bb2f-8964ea91018c", + "metadata": {}, + "outputs": [], + "source": [ + "# you should have exetera installed already, otherwise: pip install exetera\n", + "import sys\n", + "from exetera.core.session import Session\n", + "\n", + "# recommended\n", + "with Session() as s:\n", + " ...\n", + "\n", + "# not recommended\n", + "s = Session() " + ] + }, + { + "cell_type": "markdown", + "id": "f771302f-0840-4cfb-856c-09dc2c39ade6", + "metadata": {}, + "source": [ + "

Loading dataset(s)

\n", + "Once you have a session, the next step is typically to open a dataset. Datasets can be opened in one of three modes:
\n", + "\n", + "read - the dataset can be read from but not written to
\n", + "append - the dataset can be read from and written to
\n", + "write - a new dataset is created (and will overwrite an existing dataset with the same name)
" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "88bd5317-bfa0-4a31-bf9c-3475f17c4afb", + "metadata": {}, + "outputs": [], + "source": [ + "with Session() as s:\n", + " ds1 = s.open_dataset('user_assessments.hdf5', 'r', 'ds1')" + ] + }, + { + "cell_type": "markdown", + "id": "887f2a4a-b14c-4485-8a67-f2fb5312208f", + "metadata": {}, + "source": [ + "

Closing a dataset

\n", + "Closing a dataset is done through Session.close_dataset, as follows" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "3a2eaf21-ddc7-4b1a-8c63-4471ee8aee6d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dict_keys(['assessments', 'users'])\n" + ] + } + ], + "source": [ + "with Session() as s:\n", + " ds1 = s.open_dataset('user_assessments.hdf5', 'r', 'ds1')\n", + "\n", + " # do some work\n", + " print(ds1.keys())\n", + "\n", + " s.close_dataset('ds1')" + ] + }, + { + "cell_type": "markdown", + "id": "5d23b356-93e8-4db8-a0fa-c1573cc3695c", + "metadata": {}, + "source": [ + "

Dataset

\n", + "ExeTera works with HDF5 datasets under the hood, and the Dataset class is the means why which you interact with it at the top level. Each Dataset instance corresponds to a physical dataset that has been created or opened through a call to session.open_dataset.
\n", + "\n", + "Datasets are in turn used to create, access and delete DataFrames. Each DataFrame is a top-level HDF5 group that is intended to be very much like and familiar to the Pandas DataFrame." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "8d7f668f-9630-49cb-83f0-6ac32da9feb6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dict_keys(['foo'])\n", + "Renamed: dict_keys(['bar'])\n", + "Moved: dict_keys(['foo'])\n", + "Copied: dict_keys(['foo', 'bar'])\n", + "Dataframe foo deleted. dict_keys(['bar'])\n", + "Copied: dict_keys(['assessments', 'users'])\n", + "Copied: dict_keys(['foobar'])\n" + ] + } + ], + "source": [ + "from exetera.core import dataset\n", + "\n", + "with Session() as s:\n", + " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", + "\n", + " #Create a new dataframe\n", + " df = ds.create_dataframe('foo')\n", + " print(ds.keys())\n", + "\n", + " #Rename a dataframe\n", + " ds['bar'] = ds['foo'] # internally performs a rename\n", + " print('Renamed:', ds.keys())\n", + " dataset.move(ds['bar'], ds, 'foo')\n", + " print('Moved:', ds.keys())\n", + "\n", + " #Copy a dataframe within a dataset\n", + " dataset.copy(ds['foo'], ds, 'bar')\n", + " print('Copied:', ds.keys())\n", + " \n", + " #Delete an existing dataframe\n", + " ds.delete_dataframe(ds['foo'])\n", + " print('Dataframe foo deleted.', ds.keys())\n", + "\n", + " #Copy a dataframe between datasets\n", + " ds2 = s.open_dataset('temp2.hdf5', 'w', 'ds2')\n", + " ds2['foobar'] = ds['bar']\n", + " print('Copied:', ds1.keys())\n", + " print('Copied:', ds2.keys())" + ] + }, + { + "cell_type": "markdown", + "id": "8d12f984-35d1-4a90-a1ca-655afe7c80f3", + "metadata": {}, + "source": [ + "

3, DataFrame and Fields

\n", + "The ExeTera DataFrame object is intended to be familiar to users of Pandas, albeit not identical.
\n", + "\n", + "ExeTera works with Datasets, which are backed up by physical key-value HDF5 datastores on drives, and, as such, there are necessarily some differences between the Pandas DataFrame:
\n", + "\n", + "- Pandas DataFrames enforce that all Series (Fields in ExeTera terms) are the same length. ExeTera doesn't require this, but there are then operations that do not make sense unless all fields are of the same length. ExeTera allows DataFrames to have fields of different lengths because the operation to apply filters and so for to a DataFrame would run out of memory on large DataFrames
\n", + "- Types always matter in ExeTera. When creating new Fields (Pandas Series) you need to specify the type of the field that you would like to create. Fortunately, Fields have convenience methods to construct empty copies of themselves for when you need to create a field of a compatible type
\n", + "- ExeTera DataFrames are new with the 0.5 release of ExeTera and do not yet support all of the operations that Panda DataFrames support. This functionality will be augmented in future releases.
" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "5221d1da-8f36-4259-a800-3fadfc50600c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "odict_keys(['foo', 'foobar'])\n", + "Original: [0 1 2 3 4 5 6 7 8 9]\n", + "Filtered: [0 2 4 6 8]\n", + "Original: [0 2 4 6 8]\n", + "Previous re-index: [0 1 2 3 4 5 6 7 8 9]\n", + "Re-indexed: [9 8 7 6 5 4 3 2 1 0]\n", + "Re-indexed: [9 8 7 6 5 4 3 2 1 0]\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "with Session() as s:\n", + " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", + " \n", + " #Create a new field\n", + " df = ds.create_dataframe('df')\n", + " i_f = df.create_indexed_string('i_foo')\n", + " f_f = df.create_fixed_string('f_foo', 8)\n", + " n_f = df.create_numeric('n_foo', 'int32')\n", + " c_f = df.create_categorical('c_foo', 'int8', {b'a': 0, b'b': 1})\n", + " t_f = df.create_timestamp('t_foo')\n", + "\n", + "\n", + " #Copy a field from another dataframe \n", + " df2 = ds.create_dataframe('df2')\n", + " df2['foo'] = df['i_foo']\n", + " df2['foobar'] = df2['foo']\n", + " print(df2.keys())\n", + "\n", + "\n", + " #Apply a filter to all fields in a dataframe\n", + " df = ds.create_dataframe('df3')\n", + " df.create_numeric('n_foo', 'int32').data.write([0,1,2,3,4,5,6,7,8,9])\n", + " filt = np.array([True if i%2==0 else False for i in range(0,10)]) # filter out odd values\n", + " df4 = ds.create_dataframe('df4')\n", + " df.apply_filter(filt, ddf=df4) # creates a new dataframe from the filtered dataframe\n", + " print('Original:', df['n_foo'].data[:])\n", + " print('Filtered: ',df4['n_foo'].data[:])\n", + " df.apply_filter(filt) # destructively filters the dataframe\n", + " print('Original:', df['n_foo'].data[:])\n", + "\n", + "\n", + " #Re-index all fields in a dataframe\n", + " df = ds.create_dataframe('df5')\n", + " df.create_numeric('n_foo', 'int32').data.write([0,1,2,3,4,5,6,7,8,9])\n", + " print('Previous re-index:', df['n_foo'].data[:])\n", + " inds = np.array([9,8,7,6,5,4,3,2,1,0])\n", + " df6 = ds.create_dataframe('df6')\n", + " df.apply_index(inds, ddf=df6) # creates a new dataframe from the re-indexed dataframe\n", + " print('Re-indexed:', df6['n_foo'].data[:])\n", + " df.apply_index(inds) # destructively re-indexes the dataframe\n", + " print('Re-indexed:', df['n_foo'].data[:])" + ] + }, + { + "cell_type": "markdown", + "id": "2fd4d459-d69d-4e33-be71-826f523a58d8", + "metadata": {}, + "source": [ + "

Fields

\n", + "The Field object is the analogy of the Pandas DataFrame Series or Numpy ndarray in ExeTera. Fields contain (often very large) arrays of a given data type, with an API that allows intuitive manipulations of the data.
\n", + "\n", + "
\n", + "In order to store very large data arrays as efficiently as possible, Fields store their data in ways that may not be intuitive to people familiar with Pandas or Numpy. Numpy makes certain design decisions that reduce the flexibility of lists in order to gain speed and memory efficiency, and ExeTera does the same to further improve on speed and memory. The IndexedStringField, for example, uses two arrays, one containing a concatinated array of bytevalues from all of the strings in the field, and another array of indices indicating where each field starts and end. This is much faster and more memory efficient to iterate over than a Numpy string array when the variability of string lengths is very high. This kind of change however, creates a great deal of complexity when exposed to the user, and Field does its best to hide that away and act like a single array of string values. " + ] + }, + { + "cell_type": "markdown", + "id": "2e56dc65-467a-45f7-a677-92d6386a345d", + "metadata": {}, + "source": [ + "

Accessing underlying data

\n", + "Underlying data can be accessed as follows:
\n", + "\n", + "All fields have a data property that provides access to the underlying data that they contain. For most field types, it is very efficient to read from and write to this property, provided it is done using slice syntax
\n", + "- Indexed string fields provide data as a convenience method, but this should only be used when performance is not a consideration
\n", + "- Indexed string fields provide indices and values properties should you need to interact with their underlying data efficiently and directly. For the most part, we discourage this and have tried to provide you with all of the methods that you need under the hood" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "a4c81326-19c0-4e2f-8ac8-6e44efc0a9e8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0 1 2 3 4 5 6 7 8 9]\n" + ] + } + ], + "source": [ + "with Session() as s:\n", + " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", + " df = ds.create_dataframe('df')\n", + " df.create_numeric('field', 'int32').data.write([0,1,2,3,4,5,6,7,8,9])\n", + " print(df['field'].data[:])" + ] + }, + { + "cell_type": "markdown", + "id": "6a43a716-d53d-44d9-bf54-150975a983cd", + "metadata": {}, + "source": [ + "Constructing compatible empty fields\n", + "Fields have a create_like method that can be used to construct an empty field of a compatible type\n", + "\n", + "when called with no arguments, this creates an in-memory field that can be further manipulated before eventually being assigned to a DataFrame (or not)\n", + "when called with a DataFrame and a name, it will create an empty field on that DataFrame of the given name that can subsequently be written to See below for examples" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "eaa861df-84ee-48a9-8e42-1e6332d8b824", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0 1 2 3 4 5 6 7 8 9]\n", + "[]\n" + ] + } + ], + "source": [ + "with Session() as s:\n", + " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", + " df = ds.create_dataframe('df')\n", + " df.create_numeric('field', 'int32').data.write([0,1,2,3,4,5,6,7,8,9])\n", + " df['field'].create_like(df, 'field2') # use create_like to create a field with similar data type\n", + " print(df['field'].data[:])\n", + " print(df['field2'].data[:]) # note the data is not copied" + ] + }, + { + "cell_type": "markdown", + "id": "b96d7ddb-26d9-4933-93e5-f97e8bad2592", + "metadata": {}, + "source": [ + "

Arithmetic operations

\n", + "Numeric and timestamp fields have the standard set of arithmetic operations that can be applied to them:
\n", + "\n", + "These are +, -, *, /, //, %, and divmod" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "28619c4e-8dd0-49e1-9f69-d2e82c848c9d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ 0 2 4 6 8 10 12 14 16 18]\n" + ] + } + ], + "source": [ + "with Session() as s:\n", + " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", + " df = ds.create_dataframe('df')\n", + " df.create_numeric('a', 'int32').data.write([0,1,2,3,4,5,6,7,8,9])\n", + " df.create_numeric('b', 'int32').data.write([0,1,2,3,4,5,6,7,8,9])\n", + "\n", + " df['c'] = df['a'] + df['b']\n", + " print(df['c'].data[:])" + ] + }, + { + "cell_type": "markdown", + "id": "cf8747c8-5fca-4b4b-bc58-5b0032a5a2e4", + "metadata": {}, + "source": [ + "

Element-wise logical operators

\n", + "Numeric fields can have logical operations performed on them on an element-wise basis
\n", + "\n", + "These are &, |, ^" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "ef5cfb09-4f5a-434d-9eab-4ddd7c2d9f0d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ True False True False True False True False True False]\n" + ] + } + ], + "source": [ + "with Session() as s:\n", + " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", + " df = ds.create_dataframe('df')\n", + " df.create_numeric('a', 'bool').data.write([True if i%2 == 0 else False for i in range(0,10)])\n", + " df.create_numeric('b', 'bool').data.write([True if i%2 == 0 else False for i in range(0,10)])\n", + "\n", + " filter1 = df['a'] & df['b']\n", + " print(filter1.data[:])" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "4731e2b7-7bf7-48e0-99ee-024bff1c6047", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ True True True True True True True True True True]\n" + ] + } + ], + "source": [ + "with Session() as s:\n", + " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", + " df = ds.create_dataframe('df')\n", + " df.create_numeric('a', 'bool').data.write([True if i%2 == 0 else False for i in range(0,10)])\n", + " df.create_numeric('b', 'bool').data.write([True if i%2 == 1 else False for i in range(0,10)])\n", + "\n", + " filter1 = df['a'] | df['b']\n", + " print(filter1.data[:])" + ] + }, + { + "cell_type": "markdown", + "id": "72a1c302-94d0-40ab-be13-e5f7eb6c171d", + "metadata": {}, + "source": [ + "

Comparison operators

\n", + "Numeric, categorical and timestamp fields have comparison operations that can be applied to them:
\n", + "\n", + "These are <, <=, ==, |=, >=, >" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "ae438918-d810-46c0-a1ad-376c8189444f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[False False False False False False False False False False]\n" + ] + } + ], + "source": [ + "with Session() as s:\n", + " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", + " df = ds.create_dataframe('df')\n", + " df.create_numeric('a', 'bool').data.write([True if i%2 == 0 else False for i in range(0,10)])\n", + " df.create_numeric('b', 'bool').data.write([True if i%2 == 1 else False for i in range(0,10)])\n", + "\n", + " filter1 = df['a'] == df['b']\n", + " print(filter1.data[:])\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/simple_linked_dataset.ipynb b/examples/simple_linked_dataset.ipynb new file mode 100644 index 00000000..c8964194 --- /dev/null +++ b/examples/simple_linked_dataset.ipynb @@ -0,0 +1,489 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "95087344", + "metadata": { + "tags": [] + }, + "source": [ + "# Simple Linked Example\n", + "\n", + "Uses shared state to recall unique IDs between datasets." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "035b3d58-846b-4ac7-8613-3480bf8ab5c3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting randomdataset\n", + " Using cached RandomDataset-0.1.4-py3-none-any.whl (14 kB)\n", + "Requirement already satisfied: exetera in /home/jd21/miniconda3/lib/python3.8/site-packages (0.5.5)\n", + "Requirement already satisfied: pandas in /home/jd21/miniconda3/lib/python3.8/site-packages (from randomdataset) (1.2.4)\n", + "Requirement already satisfied: pyyaml in /home/jd21/miniconda3/lib/python3.8/site-packages (from randomdataset) (5.4.1)\n", + "Requirement already satisfied: click in /home/jd21/miniconda3/lib/python3.8/site-packages (from randomdataset) (8.0.1)\n", + "Requirement already satisfied: numpy>=1.18 in /home/jd21/miniconda3/lib/python3.8/site-packages (from randomdataset) (1.20.3)\n", + "Requirement already satisfied: h5py in /home/jd21/miniconda3/lib/python3.8/site-packages (from exetera) (3.2.1)\n", + "Requirement already satisfied: numba in /home/jd21/miniconda3/lib/python3.8/site-packages (from exetera) (0.53.0)\n", + "Requirement already satisfied: pytz>=2017.3 in /home/jd21/miniconda3/lib/python3.8/site-packages (from pandas->randomdataset) (2021.1)\n", + "Requirement already satisfied: python-dateutil>=2.7.3 in /home/jd21/miniconda3/lib/python3.8/site-packages (from pandas->randomdataset) (2.8.1)\n", + "Requirement already satisfied: setuptools in /home/jd21/miniconda3/lib/python3.8/site-packages (from numba->exetera) (50.3.1.post20201107)\n", + "Requirement already satisfied: llvmlite<0.37,>=0.36.0rc1 in /home/jd21/miniconda3/lib/python3.8/site-packages (from numba->exetera) (0.36.0)\n", + "Requirement already satisfied: six>=1.5 in /home/jd21/miniconda3/lib/python3.8/site-packages (from python-dateutil>=2.7.3->pandas->randomdataset) (1.15.0)\n", + "Installing collected packages: randomdataset\n", + "Successfully installed randomdataset-0.1.4\n" + ] + } + ], + "source": [ + "!pip install randomdataset exetera" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b59623ed", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting user_assessments.yaml\n" + ] + } + ], + "source": [ + "%%writefile user_assessments.yaml\n", + "\n", + "- typename: randomdataset.generators.CSVGenerator\n", + " num_lines: 10\n", + " dataset:\n", + " name: users\n", + " typename: randomdataset.Dataset\n", + " fields:\n", + " - name: id\n", + " typename: randomdataset.UIDFieldGen\n", + " shared_state_name: user_ids\n", + " - name: FirstName\n", + " typename: randomdataset.AlphaNameGen\n", + " - name: LastName\n", + " typename: randomdataset.AlphaNameGen\n", + " is_first_name: False\n", + " - name: bmi\n", + " typename: randomdataset.IntFieldGen\n", + " vmin: 20\n", + " vmax: 40\n", + " - name: has_diabetes\n", + " typename: randomdataset.IntFieldGen\n", + " vmin: 0\n", + " vmax: 2\n", + " - name: height_cm\n", + " typename: randomdataset.IntFieldGen\n", + " vmin: 100\n", + " vmax: 200\n", + " - name: year_of_birth\n", + " typename: randomdataset.IntFieldGen\n", + " vmin: 1920\n", + " vmax: 2010\n", + " \n", + "- typename: randomdataset.generators.CSVGenerator\n", + " num_lines: 30\n", + " dataset:\n", + " name: assessments\n", + " typename: randomdataset.Dataset\n", + " fields:\n", + " - name: id\n", + " typename: randomdataset.UIDFieldGen\n", + " - name: date\n", + " typename: randomdataset.DateTimeFieldGen\n", + " as_string: True\n", + " - name: user_id\n", + " typename: randomdataset.SharedDataGen\n", + " source_state_name: user_ids\n", + " field_type: int\n", + " - name: abdominal_pain\n", + " typename: randomdataset.IntFieldGen\n", + " vmin: 0\n", + " vmax: 2\n", + " - name: brain_fog\n", + " typename: randomdataset.IntFieldGen\n", + " vmin: 0\n", + " vmax: 2\n", + " - name: loss_of_smell\n", + " typename: randomdataset.IntFieldGen\n", + " vmin: 0\n", + " vmax: 2\n", + " - name: tested_covid_positive\n", + " typename: randomdataset.IntFieldGen\n", + " vmin: 0\n", + " vmax: 3\n", + " - name: temperature_f\n", + " typename: randomdataset.FloatFieldGen\n", + " vmin: 95\n", + " vmax: 110\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "6d51c67b", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "import randomdataset" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bb32159e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Schema: 'user_assessments.yaml'\n", + "Output: '.'\n", + "Generating dataset 'users'\n", + "Generating dataset 'assessments'\n" + ] + } + ], + "source": [ + "randomdataset.application.generate_dataset.callback(\"user_assessments.yaml\",\".\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "869ebb9a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "id,FirstName,LastName,bmi,has_diabetes,height_cm,year_of_birth\n", + "0,\"Xavier\",\"Unknown\",26,0,193,1934\n", + "1,\"Peggy\",\"Nemo\",39,0,164,1982\n", + "2,\"Kylie\",\"Bar\",39,0,111,1941\n", + "3,\"Mallory\",\"Anon\",34,0,171,2009\n", + "4,\"Kylie\",\"Anon\",38,0,167,1949\n", + "5,\"Peggy\",\"Thunk\",23,1,197,1926\n", + "6,\"Uriel\",\"Blargs\",37,0,175,1961\n", + "7,\"Laura\",\"Bar\",38,0,174,2005\n", + "8,\"Alice\",\"Unknown\",25,0,128,1990\n", + "9,\"Grace\",\"Anon\",20,1,156,1940\n" + ] + } + ], + "source": [ + "!cat users.csv" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "723cdb3a-2fa9-4298-8199-282dfcf6086b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "id,date,user_id,abdominal_pain,brain_fog,loss_of_smell,tested_covid_positive,temperature_f\n", + "0,2022-01-31 13:10:19.470142+00:00,0,0,1,0,2,107.49023514725474\n", + "1,2021-11-12 04:17:56.782553+00:00,1,0,1,1,0,105.6942570124724\n", + "2,2022-01-01 14:31:02.775222+00:00,2,1,0,0,0,96.54598657012015\n", + "3,2021-06-11 19:21:16.500351+00:00,3,0,1,1,0,105.10060277211116\n", + "4,2022-01-24 17:25:45.546875+00:00,4,1,0,1,2,104.78580411362383\n", + "5,2021-05-19 19:28:55.347014+00:00,5,1,1,0,0,99.1889494912643\n", + "6,2021-05-11 00:53:23.349521+00:00,6,0,1,1,2,109.46725811260491\n", + "7,2021-07-10 11:37:36.951190+00:00,7,0,1,0,0,95.14845661460399\n", + "8,2021-11-12 16:39:51.889504+00:00,8,1,0,0,0,97.73890279974026\n", + "9,2022-02-11 04:39:26.788180+00:00,9,1,1,1,0,98.02984864593004\n", + "10,2021-06-06 21:30:19.920080+00:00,0,1,1,0,1,104.13524032621973\n", + "11,2022-02-08 03:18:02.527770+00:00,1,1,1,1,1,105.399991356878\n", + "12,2021-02-20 14:45:30.235950+00:00,2,1,1,1,2,105.81507610779525\n", + "13,2021-03-25 15:01:35.580187+00:00,3,0,0,0,2,103.39740560144419\n", + "14,2021-06-25 12:42:53.454412+00:00,4,1,1,0,0,96.42637849054682\n", + "15,2021-04-22 12:51:03.968833+00:00,5,0,0,0,0,105.55405159339217\n", + "16,2022-01-17 06:01:40.192258+00:00,6,1,0,0,1,99.86833477733987\n", + "17,2021-11-02 17:39:19.250052+00:00,7,0,0,1,0,95.18785494148366\n", + "18,2021-05-13 00:49:28.529711+00:00,8,1,0,0,0,109.91900084649247\n", + "19,2021-05-25 07:32:23.442792+00:00,9,0,1,0,2,96.64168142544378\n", + "20,2021-10-27 17:07:20.268680+00:00,0,1,0,0,0,109.68642285851755\n", + "21,2021-09-29 00:53:43.371019+00:00,1,1,1,0,0,102.94022684794106\n", + "22,2021-05-18 08:01:57.282126+00:00,2,0,0,0,1,100.35970556649822\n", + "23,2021-04-13 09:12:11.682754+00:00,3,0,1,1,1,104.13998240599659\n", + "24,2022-01-08 18:03:39.408368+00:00,4,1,1,1,2,100.98163078840906\n", + "25,2021-10-12 00:02:50.612215+00:00,5,1,0,0,2,100.93178587364822\n", + "26,2021-05-03 10:47:21.937449+00:00,6,0,1,0,0,109.75762878101882\n", + "27,2021-10-31 03:55:50.569103+00:00,7,0,0,0,0,105.49404619459615\n", + "28,2021-07-16 22:25:12.780141+00:00,8,1,1,0,0,100.03338355808135\n", + "29,2021-08-04 17:27:07.316704+00:00,9,1,0,1,1,108.0929488715657\n" + ] + } + ], + "source": [ + "!cat assessments.csv" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "4658fd72-a2a4-4e8a-ae7a-1875c5952fb9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting user_assessments.json\n" + ] + } + ], + "source": [ + "%%writefile user_assessments.json\n", + "\n", + "{\n", + " \"exetera\": {\n", + " \"version\": \"1.0.0\"\n", + " },\n", + " \"schema\": {\n", + " \"users\": {\n", + " \"primary_keys\": [\n", + " \"id\"\n", + " ],\n", + " \"fields\": {\n", + " \"id\": {\n", + " \"field_type\": \"fixed_string\",\n", + " \"length\": 32\n", + " },\n", + " \"FirstName\": {\n", + " \"field_type\": \"string\"\n", + " },\n", + " \"LastName\": {\n", + " \"field_type\": \"string\"\n", + " },\n", + " \"bmi\": {\n", + " \"field_type\": \"numeric\",\n", + " \"value_type\": \"int32\"\n", + " },\n", + " \"has_diabetes\": {\n", + " \"field_type\": \"categorical\",\n", + " \"categorical\": {\n", + " \"value_type\": \"int8\",\n", + " \"strings_to_values\": {\n", + " \"\": 0,\n", + " \"False\": 1,\n", + " \"True\": 2\n", + " }\n", + " }\n", + " },\n", + " \"height_cm\": {\n", + " \"field_type\": \"numeric\",\n", + " \"value_type\": \"int32\"\n", + " }, \n", + " \"year_of_birth\": {\n", + " \"field_type\": \"numeric\",\n", + " \"value_type\": \"int32\"\n", + " }\n", + " }\n", + " },\n", + " \"assessments\": {\n", + " \"primary_keys\": [\n", + " \"id\"\n", + " ],\n", + " \"foreign_keys\": {\n", + " \"user_id_key\": {\n", + " \"space\": \"users\",\n", + " \"key\": \"id\"\n", + " }\n", + " },\n", + " \"fields\": {\n", + " \"id\": {\n", + " \"field_type\": \"fixed_string\",\n", + " \"length\": 32\n", + " },\n", + " \"date\": {\n", + " \"field_type\": \"datetime\"\n", + " },\n", + " \"user_id\": {\n", + " \"field_type\": \"fixed_string\",\n", + " \"length\": 32\n", + " },\n", + " \"abdominal_pain\": {\n", + " \"field_type\": \"categorical\",\n", + " \"categorical\": {\n", + " \"value_type\": \"int8\",\n", + " \"strings_to_values\": {\n", + " \"\": 0,\n", + " \"False\": 1,\n", + " \"True\": 2\n", + " }\n", + " }\n", + " },\n", + " \"brain_fog\": {\n", + " \"field_type\": \"categorical\",\n", + " \"categorical\": {\n", + " \"value_type\": \"int8\",\n", + " \"strings_to_values\": {\n", + " \"\": 0,\n", + " \"False\": 1,\n", + " \"True\": 2\n", + " }\n", + " }\n", + " },\n", + " \"loss_of_smell\": {\n", + " \"field_type\": \"categorical\",\n", + " \"categorical\": {\n", + " \"value_type\": \"int8\",\n", + " \"strings_to_values\": {\n", + " \"\": 0,\n", + " \"False\": 1,\n", + " \"True\": 2\n", + " }\n", + " }\n", + " },\n", + " \"tested_covid_positive\": {\n", + " \"field_type\": \"categorical\",\n", + " \"categorical\": {\n", + " \"value_type\": \"int8\",\n", + " \"strings_to_values\": {\n", + " \"\": 0,\n", + " \"waiting\": 1,\n", + " \"no\": 2,\n", + " \"yes\": 3\n", + " }\n", + " }\n", + " },\n", + " \"temperature_f\": {\n", + " \"field_type\": \"numeric\",\n", + " \"value_type\": \"float32\"\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "88366fba-e472-4d9a-9f9b-c91891742583", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "read_file_using_fast_csv_reader: 1 chunks, 10 accumulated_written_rows parsed in 0.0049591064453125s\n", + "completed in 0.00712895393371582 seconds\n", + "Total time 0.007568359375s\n", + "read_file_using_fast_csv_reader: 1 chunks, 30 accumulated_written_rows parsed in 0.0041615962982177734s\n", + "completed in 0.006209611892700195 seconds\n", + "Total time 0.006479978561401367s\n" + ] + } + ], + "source": [ + "#Import csv to hdf5 through import_with_schema function\n", + "\n", + "import exetera\n", + "\n", + "from exetera.io import importer\n", + "from exetera.core import session\n", + "from datetime import datetime, timezone\n", + "\n", + "with session.Session() as s:\n", + " importer.import_with_schema(\n", + " session=s,\n", + " timestamp=str(datetime.now(timezone.utc)),\n", + " dataset_alias=\"UserAssessments\",\n", + " dataset_filename=\"user_assessments.hdf5\",\n", + " schema_file=\"user_assessments.json\",\n", + " files={\"users\": \"users.csv\", \"assessments\":\"assessments.csv\"},\n", + " overwrite=True,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d093380b-8394-4795-95f4-41935dd76422", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "total 259M\n", + "-rw-rw-r-- 1 jd21 jd21 23K Feb 18 15:46 advanced_operations.ipynb\n", + "-rw-rw-r-- 1 jd21 jd21 2.0K Feb 18 15:44 assessments.csv\n", + "-rw-rw-r-- 1 jd21 jd21 24K Feb 18 15:46 basic_concept.ipynb\n", + "-rw-rw-r-- 1 jd21 jd21 253M Feb 18 15:24 dataset.hdf5\n", + "drwxrwxr-x 3 jd21 jd21 4.0K Feb 18 14:39 example-css\n", + "-rw-rw-r-- 1 jd21 jd21 1.7K Feb 18 15:24 exeteraschema.json\n", + "-rw-rw-r-- 1 jd21 jd21 6.0M Jan 25 16:17 name_gender_dataset.hdf5\n", + "-rw-rw-r-- 1 jd21 jd21 676 Jan 25 16:08 name_gender_dataset_schema.json\n", + "-rw-rw-r-- 1 jd21 jd21 6.5K Feb 18 15:23 names_dataset.ipynb\n", + "-rw-rw-r-- 1 jd21 jd21 137 Feb 18 15:20 README.md\n", + "-rw-rw-r-- 1 jd21 jd21 18K Feb 18 15:47 simple_linked_dataset.ipynb\n", + "-rw-rw-r-- 1 jd21 jd21 1.8K Feb 18 15:25 temp.hdf5\n", + "-rw-rw-r-- 1 jd21 jd21 2.6K Feb 18 15:45 user_assessments.json\n", + "-rw-rw-r-- 1 jd21 jd21 1.7K Feb 18 15:44 user_assessments.yaml\n", + "drwxrwxr-x 2 jd21 jd21 4.0K Feb 18 14:36 users_assessments\n", + "-rw-rw-r-- 1 jd21 jd21 383 Feb 18 15:44 users.csv\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "bash: line 3: exetera: command not found\n" + ] + } + ], + "source": [ + "#Import csv to hdf5 through command line:\n", + "%%bash\n", + "\n", + "exetera import -w -s user_assessments.json -i \"users:users.csv, assessments:assessments.csv\" -o user_assessments.hdf5\n", + "ls -lh" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 825aaf15b767d7dce50dd8fe2be31aab7e53458a Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 23 Feb 2022 11:05:26 +0000 Subject: [PATCH 159/181] update on examples --- examples/advanced_operations.ipynb | 159 ++++++++++++++--------------- examples/basic_concept.ipynb | 29 ++++-- 2 files changed, 92 insertions(+), 96 deletions(-) diff --git a/examples/advanced_operations.ipynb b/examples/advanced_operations.ipynb index a4615384..2d6e1db0 100644 --- a/examples/advanced_operations.ipynb +++ b/examples/advanced_operations.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 5, "id": "ac06eae4-8214-4e96-a419-d361698825a8", "metadata": {}, "outputs": [ @@ -18,7 +18,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "temp2.hdf5 temp.hdf5 user_assessments.hdf5\n" + "dataset.hdf5 name_gender_dataset.hdf5\ttemp.hdf5 user_assessments.hdf5\n" ] } ], @@ -28,18 +28,7 @@ }, { "cell_type": "code", - "execution_count": 4, - "id": "6b742fb7-5572-4c15-b0a3-7e5e4a00733c", - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "sys.path.append('/home/jd21/ExeTera')" - ] - }, - { - "cell_type": "code", - "execution_count": 14, + "execution_count": 2, "id": "4eae8d06-0872-4e30-b6c8-2e8ba86fe9f4", "metadata": {}, "outputs": [ @@ -47,19 +36,20 @@ "name": "stdout", "output_type": "stream", "text": [ + "dict_keys(['assessments', 'users'])\n", "Columns in users table: odict_keys(['FirstName', 'LastName', 'bmi', 'bmi_valid', 'has_diabetes', 'height_cm', 'height_cm_valid', 'id', 'j_valid_from', 'j_valid_to', 'year_of_birth', 'year_of_birth_valid'])\n", "fields\t bmi\t has_diabetes\t height_cm\t year_of_birth\t\n", "count\t 10\t 10\t 10\t 10\t\n", "unique\t NaN\t 1\t NaN\t NaN\t\n", "top\t NaN\t 0\t NaN\t NaN\t\n", "freq\t NaN\t 10\t NaN\t NaN\t\n", - "mean\t 31.70\t NaN\t 135.60\t 1965.40\t\n", - "std\t 5.14\t NaN\t 25.39\t 24.87\t\n", - "min\t 25.00\t NaN\t 107.00\t 1926.00\t\n", - "25%\t 25.02\t NaN\t 107.20\t 1926.07\t\n", - "50%\t 25.05\t NaN\t 107.41\t 1926.13\t\n", - "75%\t 25.07\t NaN\t 107.61\t 1926.20\t\n", - "max\t 39.00\t NaN\t 190.00\t 2004.00\t\n", + "mean\t 31.90\t NaN\t 163.60\t 1963.70\t\n", + "std\t 7.13\t NaN\t 25.25\t 28.96\t\n", + "min\t 20.00\t NaN\t 111.00\t 1926.00\t\n", + "25%\t 20.07\t NaN\t 111.38\t 1926.18\t\n", + "50%\t 20.14\t NaN\t 111.77\t 1926.36\t\n", + "75%\t 20.20\t NaN\t 112.15\t 1926.54\t\n", + "max\t 39.00\t NaN\t 197.00\t 2009.00\t\n", "\n", "\n" ] @@ -69,19 +59,19 @@ "text/plain": [ "{'fields': ['bmi', 'has_diabetes', 'height_cm', 'year_of_birth'],\n", " 'count': [10, 10, 10, 10],\n", - " 'mean': ['31.70', 'NaN', '135.60', '1965.40'],\n", - " 'std': ['5.14', 'NaN', '25.39', '24.87'],\n", - " 'min': ['25.00', 'NaN', '107.00', '1926.00'],\n", - " '25%': ['25.02', 'NaN', '107.20', '1926.07'],\n", - " '50%': ['25.05', 'NaN', '107.41', '1926.13'],\n", - " '75%': ['25.07', 'NaN', '107.61', '1926.20'],\n", - " 'max': ['39.00', 'NaN', '190.00', '2004.00'],\n", + " 'mean': ['31.90', 'NaN', '163.60', '1963.70'],\n", + " 'std': ['7.13', 'NaN', '25.25', '28.96'],\n", + " 'min': ['20.00', 'NaN', '111.00', '1926.00'],\n", + " '25%': ['20.07', 'NaN', '111.38', '1926.18'],\n", + " '50%': ['20.14', 'NaN', '111.77', '1926.36'],\n", + " '75%': ['20.20', 'NaN', '112.15', '1926.54'],\n", + " 'max': ['39.00', 'NaN', '197.00', '2009.00'],\n", " 'unique': ['NaN', 1, 'NaN', 'NaN'],\n", " 'top': ['NaN', 0, 'NaN', 'NaN'],\n", " 'freq': ['NaN', 10, 'NaN', 'NaN']}" ] }, - "execution_count": 14, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -90,7 +80,7 @@ "from exetera.core.session import Session\n", "s = Session() # not recommended, but to cover all the cells in the example, we use this way here\n", "src = s.open_dataset('user_assessments.hdf5', 'r', 'src')\n", - "\n", + "print(src.keys())\n", "users = src['users']\n", "print('Columns in users table:', users.keys())\n", "# use describe to check the value in each column\n", @@ -99,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 3, "id": "67d79440-4c2d-42ec-8f62-3d10bc72e3e7", "metadata": {}, "outputs": [ @@ -113,13 +103,13 @@ "unique\t 1\t 1\t NaN\t 1\t NaN\t\n", "top\t 0\t 0\t NaN\t 0\t NaN\t\n", "freq\t 30\t 30\t NaN\t 30\t NaN\t\n", - "mean\t NaN\t NaN\t 1628912712.34\t NaN\t 101.36\t\n", - "std\t NaN\t NaN\t 10077317.46\t NaN\t 4.33\t\n", - "min\t NaN\t NaN\t 1613872118.68\t NaN\t 95.23\t\n", - "25%\t NaN\t NaN\t 1613975491.70\t NaN\t 95.24\t\n", - "50%\t NaN\t NaN\t 1614078864.72\t NaN\t 95.26\t\n", - "75%\t NaN\t NaN\t 1614182237.74\t NaN\t 95.28\t\n", - "max\t NaN\t NaN\t 1644821469.46\t NaN\t 109.64\t\n", + "mean\t NaN\t NaN\t 1628360314.24\t NaN\t 101.62\t\n", + "std\t NaN\t NaN\t 8270828.31\t NaN\t 4.21\t\n", + "min\t NaN\t NaN\t 1614692820.02\t NaN\t 95.29\t\n", + "25%\t NaN\t NaN\t 1614707205.26\t NaN\t 95.30\t\n", + "50%\t NaN\t NaN\t 1614721590.50\t NaN\t 95.31\t\n", + "75%\t NaN\t NaN\t 1614735975.75\t NaN\t 95.32\t\n", + "max\t NaN\t NaN\t 1641848736.34\t NaN\t 109.54\t\n", "\n", "\n" ] @@ -133,19 +123,19 @@ " 'loss_of_smell',\n", " 'temperature_f'],\n", " 'count': [30, 30, 30, 30, 30],\n", - " 'mean': ['NaN', 'NaN', '1628912712.34', 'NaN', '101.36'],\n", - " 'std': ['NaN', 'NaN', '10077317.46', 'NaN', '4.33'],\n", - " 'min': ['NaN', 'NaN', '1613872118.68', 'NaN', '95.23'],\n", - " '25%': ['NaN', 'NaN', '1613975491.70', 'NaN', '95.24'],\n", - " '50%': ['NaN', 'NaN', '1614078864.72', 'NaN', '95.26'],\n", - " '75%': ['NaN', 'NaN', '1614182237.74', 'NaN', '95.28'],\n", - " 'max': ['NaN', 'NaN', '1644821469.46', 'NaN', '109.64'],\n", + " 'mean': ['NaN', 'NaN', '1628360314.24', 'NaN', '101.62'],\n", + " 'std': ['NaN', 'NaN', '8270828.31', 'NaN', '4.21'],\n", + " 'min': ['NaN', 'NaN', '1614692820.02', 'NaN', '95.29'],\n", + " '25%': ['NaN', 'NaN', '1614707205.26', 'NaN', '95.30'],\n", + " '50%': ['NaN', 'NaN', '1614721590.50', 'NaN', '95.31'],\n", + " '75%': ['NaN', 'NaN', '1614735975.75', 'NaN', '95.32'],\n", + " 'max': ['NaN', 'NaN', '1641848736.34', 'NaN', '109.54'],\n", " 'unique': [1, 1, 'NaN', 1, 'NaN'],\n", " 'top': [0, 0, 'NaN', 0, 'NaN'],\n", " 'freq': [30, 30, 'NaN', 30, 'NaN']}" ] }, - "execution_count": 16, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -168,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 4, "id": "dab8e873-cf1f-47c5-bebb-18bde4357543", "metadata": {}, "outputs": [ @@ -176,7 +166,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "9 adults out of 10 total subjects found.\n" + "8 adults out of 10 total subjects found.\n" ] } ], @@ -194,7 +184,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 5, "id": "c336d758-878a-4df6-8458-cc3ddf280964", "metadata": {}, "outputs": [ @@ -202,8 +192,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "[ True True True True True False True True True True]\n", - "[b'0' b'1' b'2' b'3' b'4' b'6' b'7' b'8' b'9']\n" + "[ True True True False True True True False True True]\n", + "[b'0' b'1' b'2' b'4' b'5' b'6' b'8' b'9']\n" ] } ], @@ -230,7 +220,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 6, "id": "c89f52b9-f96d-4e36-a8e3-67953aaedae4", "metadata": {}, "outputs": [ @@ -238,7 +228,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "4.321831703186035\n" + "3.1172735691070557\n" ] } ], @@ -260,7 +250,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 7, "id": "202321d4-b1fc-47bd-8878-779df121c913", "metadata": {}, "outputs": [ @@ -268,8 +258,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "[0 0 0 ... 0 0 0]\n", - "0.12068581581115723\n" + "0.07697820663452148\n" ] } ], @@ -300,7 +289,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 8, "id": "acfec7b4-01ec-4bf5-b8d3-d99a9db98273", "metadata": {}, "outputs": [ @@ -345,7 +334,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 9, "id": "29ef8595-6835-4f0d-a5bf-6ee15bf8eabd", "metadata": {}, "outputs": [ @@ -400,7 +389,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 10, "id": "85261f6a-ba35-45a0-ba88-60a99a16ebe3", "metadata": {}, "outputs": [ @@ -434,7 +423,7 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 11, "id": "f72666d8-a5e3-48e9-859d-1d1e56b4a768", "metadata": {}, "outputs": [ @@ -442,31 +431,31 @@ "name": "stdout", "output_type": "stream", "text": [ - "sorted ('id_l',) index in 0.0002894401550292969s\n", - " 'FirstName' reordered in 0.3324275016784668s\n", - " 'LastName' reordered in 0.0013616085052490234s\n", - " 'bmi' reordered in 0.0007426738739013672s\n", - " 'bmi_valid' reordered in 0.0006847381591796875s\n", - " 'has_diabetes' reordered in 0.0023627281188964844s\n", - " 'height_cm' reordered in 0.0006353855133056641s\n", - " 'height_cm_valid' reordered in 0.0005815029144287109s\n", - " 'id_l' reordered in 0.0005600452423095703s\n", - " 'j_valid_from_l' reordered in 0.0005393028259277344s\n", - " 'j_valid_to_l' reordered in 0.0005180835723876953s\n", - " 'year_of_birth' reordered in 0.0005507469177246094s\n", - " 'year_of_birth_valid' reordered in 0.0006158351898193359s\n", - " 'abdominal_pain' reordered in 0.0019788742065429688s\n", - " 'brain_fog' reordered in 0.0019555091857910156s\n", - " 'date' reordered in 0.0005359649658203125s\n", - " 'id_r' reordered in 0.0005869865417480469s\n", - " 'j_valid_from_r' reordered in 0.0005660057067871094s\n", - " 'j_valid_to_r' reordered in 0.0005273818969726562s\n", - " 'loss_of_smell' reordered in 0.001955270767211914s\n", - " 'temperature_f' reordered in 0.0005567073822021484s\n", - " 'temperature_f_valid' reordered in 0.0005667209625244141s\n", - " 'tested_covid_positive' reordered in 0.00225067138671875s\n", - " 'user_id' reordered in 0.0005781650543212891s\n", - "fields reordered in 0.3544754981994629s\n" + "sorted ('id_l',) index in 0.0001461505889892578s\n", + " 'FirstName' reordered in 0.182114839553833s\n", + " 'LastName' reordered in 0.0009195804595947266s\n", + " 'bmi' reordered in 0.000553131103515625s\n", + " 'bmi_valid' reordered in 0.0005764961242675781s\n", + " 'has_diabetes' reordered in 0.002074718475341797s\n", + " 'height_cm' reordered in 0.0005881786346435547s\n", + " 'height_cm_valid' reordered in 0.00047326087951660156s\n", + " 'id_l' reordered in 0.0004417896270751953s\n", + " 'j_valid_from_l' reordered in 0.00044655799865722656s\n", + " 'j_valid_to_l' reordered in 0.00038623809814453125s\n", + " 'year_of_birth' reordered in 0.00038552284240722656s\n", + " 'year_of_birth_valid' reordered in 0.0004639625549316406s\n", + " 'abdominal_pain' reordered in 0.001451730728149414s\n", + " 'brain_fog' reordered in 0.0013928413391113281s\n", + " 'date' reordered in 0.0003643035888671875s\n", + " 'id_r' reordered in 0.00040459632873535156s\n", + " 'j_valid_from_r' reordered in 0.0004069805145263672s\n", + " 'j_valid_to_r' reordered in 0.0003635883331298828s\n", + " 'loss_of_smell' reordered in 0.0013535022735595703s\n", + " 'temperature_f' reordered in 0.00044083595275878906s\n", + " 'temperature_f_valid' reordered in 0.00040268898010253906s\n", + " 'tested_covid_positive' reordered in 0.0015273094177246094s\n", + " 'user_id' reordered in 0.00039386749267578125s\n", + "fields reordered in 0.1985929012298584s\n" ] } ], diff --git a/examples/basic_concept.ipynb b/examples/basic_concept.ipynb index 9e83d770..25795794 100644 --- a/examples/basic_concept.ipynb +++ b/examples/basic_concept.ipynb @@ -48,7 +48,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 7, "id": "2271e28e-21e1-4720-9c32-4aaf9b310546", "metadata": {}, "outputs": [ @@ -56,7 +56,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "user_assessments.hdf5\n" + "dataset.hdf5 name_gender_dataset.hdf5\ttemp.hdf5\n" ] } ], @@ -136,7 +136,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "3a2eaf21-ddc7-4b1a-8c63-4471ee8aee6d", "metadata": {}, "outputs": [ @@ -144,7 +144,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "dict_keys(['assessments', 'users'])\n" + "dict_keys(['participants', 'tests'])\n" ] } ], @@ -171,7 +171,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 6, "id": "8d7f668f-9630-49cb-83f0-6ac32da9feb6", "metadata": {}, "outputs": [ @@ -180,12 +180,19 @@ "output_type": "stream", "text": [ "dict_keys(['foo'])\n", - "Renamed: dict_keys(['bar'])\n", - "Moved: dict_keys(['foo'])\n", - "Copied: dict_keys(['foo', 'bar'])\n", - "Dataframe foo deleted. dict_keys(['bar'])\n", - "Copied: dict_keys(['assessments', 'users'])\n", - "Copied: dict_keys(['foobar'])\n" + "Renamed: dict_keys([])\n" + ] + }, + { + "ename": "ValueError", + "evalue": "Can not find the name from this dataset.", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0mds\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'bar'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mds\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'foo'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;31m# internally performs a rename\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Renamed:'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mds\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 13\u001b[0;31m \u001b[0mdataset\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmove\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mds\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'bar'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mds\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'foo'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 14\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Moved:'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mds\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/miniconda3/lib/python3.8/site-packages/exetera/core/dataset.py\u001b[0m in \u001b[0;36m__getitem__\u001b[0;34m(self, name)\u001b[0m\n\u001b[1;32m 163\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mTypeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"The name must be a str object.\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 164\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__contains__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 165\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Can not find the name from this dataset.\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 166\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 167\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_dataframes\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mValueError\u001b[0m: Can not find the name from this dataset." ] } ], From 47e2e7ee0e6fa21c9f90fa869b62df5abe71236d Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 23 Feb 2022 14:43:44 +0000 Subject: [PATCH 160/181] update example: added two csv files and one json files and one import example --- examples/assessments.csv | 31 ++ examples/import_dataset.ipynb | 138 ++++++++ examples/simple_linked_dataset.ipynb | 489 --------------------------- examples/user_assessments.json | 121 +++++++ examples/users.csv | 11 + 5 files changed, 301 insertions(+), 489 deletions(-) create mode 100644 examples/assessments.csv create mode 100644 examples/import_dataset.ipynb delete mode 100644 examples/simple_linked_dataset.ipynb create mode 100644 examples/user_assessments.json create mode 100644 examples/users.csv diff --git a/examples/assessments.csv b/examples/assessments.csv new file mode 100644 index 00000000..a88cd958 --- /dev/null +++ b/examples/assessments.csv @@ -0,0 +1,31 @@ +id,date,user_id,abdominal_pain,brain_fog,loss_of_smell,tested_covid_positive,temperature_f +0,2021-10-24 11:45:43.677374+00:00,0,0,1,1,2,103.22149054047082 +1,2021-12-16 13:09:58.380573+00:00,1,0,1,0,0,100.62518751030662 +2,2021-08-05 17:51:30.943546+00:00,2,0,0,1,0,105.18487884609749 +3,2021-04-09 14:47:54.599226+00:00,3,1,0,1,0,96.4302053852154 +4,2021-09-29 00:15:42.142405+00:00,4,1,1,1,0,109.63616106818489 +5,2021-04-24 09:53:44.215726+00:00,5,1,0,0,1,107.69840121429907 +6,2021-11-13 07:35:32.840341+00:00,6,0,0,0,1,97.00309019318361 +7,2022-02-14 00:08:04.885913+00:00,7,1,0,0,1,95.22598358524823 +8,2022-02-07 15:36:57.841132+00:00,8,0,0,0,2,95.48740949212532 +9,2021-02-21 01:48:38.675272+00:00,9,0,1,1,0,106.27664175133276 +10,2021-08-05 00:06:12.343504+00:00,0,0,1,1,0,103.07544677653925 +11,2021-11-07 21:52:41.868990+00:00,1,1,0,0,2,102.81942527899108 +12,2021-05-20 14:49:01.700189+00:00,2,0,0,0,2,103.25591242165508 +13,2021-09-28 03:13:05.410689+00:00,3,0,1,1,1,98.99925665317788 +14,2022-01-21 13:39:41.914258+00:00,4,1,1,1,0,104.73914713718412 +15,2021-04-06 10:40:50.447460+00:00,5,0,0,1,1,95.47080459402937 +16,2021-12-20 01:53:04.166355+00:00,6,0,1,0,0,97.79758064358536 +17,2021-10-11 18:45:24.349922+00:00,7,0,1,0,0,95.87860080008119 +18,2021-04-04 13:14:36.124810+00:00,8,0,1,1,1,108.78273531994027 +19,2022-02-14 06:51:09.464885+00:00,9,0,0,0,2,100.28032607623044 +20,2021-05-17 14:09:07.047752+00:00,0,0,0,1,1,100.79853200088986 +21,2021-06-11 01:13:23.001260+00:00,1,0,1,1,1,104.63677316034355 +22,2021-04-06 04:16:39.361154+00:00,2,1,1,0,1,107.68363442020022 +23,2021-06-27 01:21:21.633768+00:00,3,1,0,1,1,97.08478382878046 +24,2021-11-02 17:52:04.146347+00:00,4,1,1,1,2,103.44636506288838 +25,2021-06-12 11:58:35.491048+00:00,5,1,0,0,1,95.8026824345136 +26,2021-03-09 13:52:33.458577+00:00,6,0,1,1,0,100.76416498492172 +27,2021-04-27 12:37:30.891582+00:00,7,1,1,0,0,103.99752973377511 +28,2022-01-10 02:08:42.584960+00:00,8,0,0,0,2,102.30838309482037 +29,2021-03-18 03:06:36.735481+00:00,9,1,1,1,0,96.4187993279403 diff --git a/examples/import_dataset.ipynb b/examples/import_dataset.ipynb new file mode 100644 index 00000000..ffe58f8a --- /dev/null +++ b/examples/import_dataset.ipynb @@ -0,0 +1,138 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "164a6070-9bc2-4f46-a016-d25ac2141f22", + "metadata": {}, + "source": [ + "In this example, we will convert the csv files into HDF5 through the import utility provided by ExeTera.\n", + "First, you can see we have two csv files and one json file:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95c1811f-027a-4fab-9936-084ef6ca0a62", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "!cat users.csv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ee656fa-fce7-44b8-9ad9-c4761d78d9d3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "!cat assessments.csv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e1d9bc3-76cd-48f1-9ed3-94636da04f76", + "metadata": {}, + "outputs": [], + "source": [ + "!cat user_assessments.json" + ] + }, + { + "cell_type": "markdown", + "id": "f96b4e31-ca85-4321-8276-ca9121672500", + "metadata": {}, + "source": [ + "Then we have two methods to call the import, 1) from python function, and 2) from script:\n", + "
\n", + "(make sure you have exetera installed already, or you can do pip install exetera)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68f76d88-8f88-468e-b3b7-8e40bc5dbe17", + "metadata": {}, + "outputs": [], + "source": [ + "#1)Import csv to hdf5 through import_with_schema function\n", + "\n", + "import exetera\n", + "\n", + "from exetera.io import importer\n", + "from exetera.core import session\n", + "from datetime import datetime, timezone\n", + "\n", + "with session.Session() as s:\n", + " importer.import_with_schema(\n", + " session=s,\n", + " timestamp=str(datetime.now(timezone.utc)),\n", + " dataset_alias=\"UserAssessments\",\n", + " dataset_filename=\"user_assessments.hdf5\",\n", + " schema_file=\"user_assessments.json\",\n", + " files={\"users\": \"users.csv\", \"assessments\":\"assessments.csv\"},\n", + " overwrite=True,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2eff68d6-f15b-4213-8807-3085cb425170", + "metadata": {}, + "outputs": [], + "source": [ + "#2)Import csv to hdf5 through command line, make sure you have \n", + "#add the ExeTera/exetera/bin/ to system path\n", + "%%bash\n", + "\n", + "exetera import -w -s user_assessments.json -i \"users:users.csv, assessments:assessments.csv\" -o user_assessments.hdf5\n", + "ls -lh" + ] + }, + { + "cell_type": "markdown", + "id": "3c707511-9866-49d4-8a4c-d56ecb9830ad", + "metadata": {}, + "source": [ + "After either of the command, you should have a HDF5 file available:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2cdf941c-be2d-4cd4-ac85-25606b2a5a19", + "metadata": {}, + "outputs": [], + "source": [ + "!ls *hdf5" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/simple_linked_dataset.ipynb b/examples/simple_linked_dataset.ipynb deleted file mode 100644 index c8964194..00000000 --- a/examples/simple_linked_dataset.ipynb +++ /dev/null @@ -1,489 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "95087344", - "metadata": { - "tags": [] - }, - "source": [ - "# Simple Linked Example\n", - "\n", - "Uses shared state to recall unique IDs between datasets." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "035b3d58-846b-4ac7-8613-3480bf8ab5c3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Collecting randomdataset\n", - " Using cached RandomDataset-0.1.4-py3-none-any.whl (14 kB)\n", - "Requirement already satisfied: exetera in /home/jd21/miniconda3/lib/python3.8/site-packages (0.5.5)\n", - "Requirement already satisfied: pandas in /home/jd21/miniconda3/lib/python3.8/site-packages (from randomdataset) (1.2.4)\n", - "Requirement already satisfied: pyyaml in /home/jd21/miniconda3/lib/python3.8/site-packages (from randomdataset) (5.4.1)\n", - "Requirement already satisfied: click in /home/jd21/miniconda3/lib/python3.8/site-packages (from randomdataset) (8.0.1)\n", - "Requirement already satisfied: numpy>=1.18 in /home/jd21/miniconda3/lib/python3.8/site-packages (from randomdataset) (1.20.3)\n", - "Requirement already satisfied: h5py in /home/jd21/miniconda3/lib/python3.8/site-packages (from exetera) (3.2.1)\n", - "Requirement already satisfied: numba in /home/jd21/miniconda3/lib/python3.8/site-packages (from exetera) (0.53.0)\n", - "Requirement already satisfied: pytz>=2017.3 in /home/jd21/miniconda3/lib/python3.8/site-packages (from pandas->randomdataset) (2021.1)\n", - "Requirement already satisfied: python-dateutil>=2.7.3 in /home/jd21/miniconda3/lib/python3.8/site-packages (from pandas->randomdataset) (2.8.1)\n", - "Requirement already satisfied: setuptools in /home/jd21/miniconda3/lib/python3.8/site-packages (from numba->exetera) (50.3.1.post20201107)\n", - "Requirement already satisfied: llvmlite<0.37,>=0.36.0rc1 in /home/jd21/miniconda3/lib/python3.8/site-packages (from numba->exetera) (0.36.0)\n", - "Requirement already satisfied: six>=1.5 in /home/jd21/miniconda3/lib/python3.8/site-packages (from python-dateutil>=2.7.3->pandas->randomdataset) (1.15.0)\n", - "Installing collected packages: randomdataset\n", - "Successfully installed randomdataset-0.1.4\n" - ] - } - ], - "source": [ - "!pip install randomdataset exetera" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "b59623ed", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Overwriting user_assessments.yaml\n" - ] - } - ], - "source": [ - "%%writefile user_assessments.yaml\n", - "\n", - "- typename: randomdataset.generators.CSVGenerator\n", - " num_lines: 10\n", - " dataset:\n", - " name: users\n", - " typename: randomdataset.Dataset\n", - " fields:\n", - " - name: id\n", - " typename: randomdataset.UIDFieldGen\n", - " shared_state_name: user_ids\n", - " - name: FirstName\n", - " typename: randomdataset.AlphaNameGen\n", - " - name: LastName\n", - " typename: randomdataset.AlphaNameGen\n", - " is_first_name: False\n", - " - name: bmi\n", - " typename: randomdataset.IntFieldGen\n", - " vmin: 20\n", - " vmax: 40\n", - " - name: has_diabetes\n", - " typename: randomdataset.IntFieldGen\n", - " vmin: 0\n", - " vmax: 2\n", - " - name: height_cm\n", - " typename: randomdataset.IntFieldGen\n", - " vmin: 100\n", - " vmax: 200\n", - " - name: year_of_birth\n", - " typename: randomdataset.IntFieldGen\n", - " vmin: 1920\n", - " vmax: 2010\n", - " \n", - "- typename: randomdataset.generators.CSVGenerator\n", - " num_lines: 30\n", - " dataset:\n", - " name: assessments\n", - " typename: randomdataset.Dataset\n", - " fields:\n", - " - name: id\n", - " typename: randomdataset.UIDFieldGen\n", - " - name: date\n", - " typename: randomdataset.DateTimeFieldGen\n", - " as_string: True\n", - " - name: user_id\n", - " typename: randomdataset.SharedDataGen\n", - " source_state_name: user_ids\n", - " field_type: int\n", - " - name: abdominal_pain\n", - " typename: randomdataset.IntFieldGen\n", - " vmin: 0\n", - " vmax: 2\n", - " - name: brain_fog\n", - " typename: randomdataset.IntFieldGen\n", - " vmin: 0\n", - " vmax: 2\n", - " - name: loss_of_smell\n", - " typename: randomdataset.IntFieldGen\n", - " vmin: 0\n", - " vmax: 2\n", - " - name: tested_covid_positive\n", - " typename: randomdataset.IntFieldGen\n", - " vmin: 0\n", - " vmax: 3\n", - " - name: temperature_f\n", - " typename: randomdataset.FloatFieldGen\n", - " vmin: 95\n", - " vmax: 110\n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "6d51c67b", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import sys\n", - "import randomdataset" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "bb32159e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Schema: 'user_assessments.yaml'\n", - "Output: '.'\n", - "Generating dataset 'users'\n", - "Generating dataset 'assessments'\n" - ] - } - ], - "source": [ - "randomdataset.application.generate_dataset.callback(\"user_assessments.yaml\",\".\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "869ebb9a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "id,FirstName,LastName,bmi,has_diabetes,height_cm,year_of_birth\n", - "0,\"Xavier\",\"Unknown\",26,0,193,1934\n", - "1,\"Peggy\",\"Nemo\",39,0,164,1982\n", - "2,\"Kylie\",\"Bar\",39,0,111,1941\n", - "3,\"Mallory\",\"Anon\",34,0,171,2009\n", - "4,\"Kylie\",\"Anon\",38,0,167,1949\n", - "5,\"Peggy\",\"Thunk\",23,1,197,1926\n", - "6,\"Uriel\",\"Blargs\",37,0,175,1961\n", - "7,\"Laura\",\"Bar\",38,0,174,2005\n", - "8,\"Alice\",\"Unknown\",25,0,128,1990\n", - "9,\"Grace\",\"Anon\",20,1,156,1940\n" - ] - } - ], - "source": [ - "!cat users.csv" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "723cdb3a-2fa9-4298-8199-282dfcf6086b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "id,date,user_id,abdominal_pain,brain_fog,loss_of_smell,tested_covid_positive,temperature_f\n", - "0,2022-01-31 13:10:19.470142+00:00,0,0,1,0,2,107.49023514725474\n", - "1,2021-11-12 04:17:56.782553+00:00,1,0,1,1,0,105.6942570124724\n", - "2,2022-01-01 14:31:02.775222+00:00,2,1,0,0,0,96.54598657012015\n", - "3,2021-06-11 19:21:16.500351+00:00,3,0,1,1,0,105.10060277211116\n", - "4,2022-01-24 17:25:45.546875+00:00,4,1,0,1,2,104.78580411362383\n", - "5,2021-05-19 19:28:55.347014+00:00,5,1,1,0,0,99.1889494912643\n", - "6,2021-05-11 00:53:23.349521+00:00,6,0,1,1,2,109.46725811260491\n", - "7,2021-07-10 11:37:36.951190+00:00,7,0,1,0,0,95.14845661460399\n", - "8,2021-11-12 16:39:51.889504+00:00,8,1,0,0,0,97.73890279974026\n", - "9,2022-02-11 04:39:26.788180+00:00,9,1,1,1,0,98.02984864593004\n", - "10,2021-06-06 21:30:19.920080+00:00,0,1,1,0,1,104.13524032621973\n", - "11,2022-02-08 03:18:02.527770+00:00,1,1,1,1,1,105.399991356878\n", - "12,2021-02-20 14:45:30.235950+00:00,2,1,1,1,2,105.81507610779525\n", - "13,2021-03-25 15:01:35.580187+00:00,3,0,0,0,2,103.39740560144419\n", - "14,2021-06-25 12:42:53.454412+00:00,4,1,1,0,0,96.42637849054682\n", - "15,2021-04-22 12:51:03.968833+00:00,5,0,0,0,0,105.55405159339217\n", - "16,2022-01-17 06:01:40.192258+00:00,6,1,0,0,1,99.86833477733987\n", - "17,2021-11-02 17:39:19.250052+00:00,7,0,0,1,0,95.18785494148366\n", - "18,2021-05-13 00:49:28.529711+00:00,8,1,0,0,0,109.91900084649247\n", - "19,2021-05-25 07:32:23.442792+00:00,9,0,1,0,2,96.64168142544378\n", - "20,2021-10-27 17:07:20.268680+00:00,0,1,0,0,0,109.68642285851755\n", - "21,2021-09-29 00:53:43.371019+00:00,1,1,1,0,0,102.94022684794106\n", - "22,2021-05-18 08:01:57.282126+00:00,2,0,0,0,1,100.35970556649822\n", - "23,2021-04-13 09:12:11.682754+00:00,3,0,1,1,1,104.13998240599659\n", - "24,2022-01-08 18:03:39.408368+00:00,4,1,1,1,2,100.98163078840906\n", - "25,2021-10-12 00:02:50.612215+00:00,5,1,0,0,2,100.93178587364822\n", - "26,2021-05-03 10:47:21.937449+00:00,6,0,1,0,0,109.75762878101882\n", - "27,2021-10-31 03:55:50.569103+00:00,7,0,0,0,0,105.49404619459615\n", - "28,2021-07-16 22:25:12.780141+00:00,8,1,1,0,0,100.03338355808135\n", - "29,2021-08-04 17:27:07.316704+00:00,9,1,0,1,1,108.0929488715657\n" - ] - } - ], - "source": [ - "!cat assessments.csv" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "4658fd72-a2a4-4e8a-ae7a-1875c5952fb9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Overwriting user_assessments.json\n" - ] - } - ], - "source": [ - "%%writefile user_assessments.json\n", - "\n", - "{\n", - " \"exetera\": {\n", - " \"version\": \"1.0.0\"\n", - " },\n", - " \"schema\": {\n", - " \"users\": {\n", - " \"primary_keys\": [\n", - " \"id\"\n", - " ],\n", - " \"fields\": {\n", - " \"id\": {\n", - " \"field_type\": \"fixed_string\",\n", - " \"length\": 32\n", - " },\n", - " \"FirstName\": {\n", - " \"field_type\": \"string\"\n", - " },\n", - " \"LastName\": {\n", - " \"field_type\": \"string\"\n", - " },\n", - " \"bmi\": {\n", - " \"field_type\": \"numeric\",\n", - " \"value_type\": \"int32\"\n", - " },\n", - " \"has_diabetes\": {\n", - " \"field_type\": \"categorical\",\n", - " \"categorical\": {\n", - " \"value_type\": \"int8\",\n", - " \"strings_to_values\": {\n", - " \"\": 0,\n", - " \"False\": 1,\n", - " \"True\": 2\n", - " }\n", - " }\n", - " },\n", - " \"height_cm\": {\n", - " \"field_type\": \"numeric\",\n", - " \"value_type\": \"int32\"\n", - " }, \n", - " \"year_of_birth\": {\n", - " \"field_type\": \"numeric\",\n", - " \"value_type\": \"int32\"\n", - " }\n", - " }\n", - " },\n", - " \"assessments\": {\n", - " \"primary_keys\": [\n", - " \"id\"\n", - " ],\n", - " \"foreign_keys\": {\n", - " \"user_id_key\": {\n", - " \"space\": \"users\",\n", - " \"key\": \"id\"\n", - " }\n", - " },\n", - " \"fields\": {\n", - " \"id\": {\n", - " \"field_type\": \"fixed_string\",\n", - " \"length\": 32\n", - " },\n", - " \"date\": {\n", - " \"field_type\": \"datetime\"\n", - " },\n", - " \"user_id\": {\n", - " \"field_type\": \"fixed_string\",\n", - " \"length\": 32\n", - " },\n", - " \"abdominal_pain\": {\n", - " \"field_type\": \"categorical\",\n", - " \"categorical\": {\n", - " \"value_type\": \"int8\",\n", - " \"strings_to_values\": {\n", - " \"\": 0,\n", - " \"False\": 1,\n", - " \"True\": 2\n", - " }\n", - " }\n", - " },\n", - " \"brain_fog\": {\n", - " \"field_type\": \"categorical\",\n", - " \"categorical\": {\n", - " \"value_type\": \"int8\",\n", - " \"strings_to_values\": {\n", - " \"\": 0,\n", - " \"False\": 1,\n", - " \"True\": 2\n", - " }\n", - " }\n", - " },\n", - " \"loss_of_smell\": {\n", - " \"field_type\": \"categorical\",\n", - " \"categorical\": {\n", - " \"value_type\": \"int8\",\n", - " \"strings_to_values\": {\n", - " \"\": 0,\n", - " \"False\": 1,\n", - " \"True\": 2\n", - " }\n", - " }\n", - " },\n", - " \"tested_covid_positive\": {\n", - " \"field_type\": \"categorical\",\n", - " \"categorical\": {\n", - " \"value_type\": \"int8\",\n", - " \"strings_to_values\": {\n", - " \"\": 0,\n", - " \"waiting\": 1,\n", - " \"no\": 2,\n", - " \"yes\": 3\n", - " }\n", - " }\n", - " },\n", - " \"temperature_f\": {\n", - " \"field_type\": \"numeric\",\n", - " \"value_type\": \"float32\"\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "88366fba-e472-4d9a-9f9b-c91891742583", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "read_file_using_fast_csv_reader: 1 chunks, 10 accumulated_written_rows parsed in 0.0049591064453125s\n", - "completed in 0.00712895393371582 seconds\n", - "Total time 0.007568359375s\n", - "read_file_using_fast_csv_reader: 1 chunks, 30 accumulated_written_rows parsed in 0.0041615962982177734s\n", - "completed in 0.006209611892700195 seconds\n", - "Total time 0.006479978561401367s\n" - ] - } - ], - "source": [ - "#Import csv to hdf5 through import_with_schema function\n", - "\n", - "import exetera\n", - "\n", - "from exetera.io import importer\n", - "from exetera.core import session\n", - "from datetime import datetime, timezone\n", - "\n", - "with session.Session() as s:\n", - " importer.import_with_schema(\n", - " session=s,\n", - " timestamp=str(datetime.now(timezone.utc)),\n", - " dataset_alias=\"UserAssessments\",\n", - " dataset_filename=\"user_assessments.hdf5\",\n", - " schema_file=\"user_assessments.json\",\n", - " files={\"users\": \"users.csv\", \"assessments\":\"assessments.csv\"},\n", - " overwrite=True,\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "d093380b-8394-4795-95f4-41935dd76422", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "total 259M\n", - "-rw-rw-r-- 1 jd21 jd21 23K Feb 18 15:46 advanced_operations.ipynb\n", - "-rw-rw-r-- 1 jd21 jd21 2.0K Feb 18 15:44 assessments.csv\n", - "-rw-rw-r-- 1 jd21 jd21 24K Feb 18 15:46 basic_concept.ipynb\n", - "-rw-rw-r-- 1 jd21 jd21 253M Feb 18 15:24 dataset.hdf5\n", - "drwxrwxr-x 3 jd21 jd21 4.0K Feb 18 14:39 example-css\n", - "-rw-rw-r-- 1 jd21 jd21 1.7K Feb 18 15:24 exeteraschema.json\n", - "-rw-rw-r-- 1 jd21 jd21 6.0M Jan 25 16:17 name_gender_dataset.hdf5\n", - "-rw-rw-r-- 1 jd21 jd21 676 Jan 25 16:08 name_gender_dataset_schema.json\n", - "-rw-rw-r-- 1 jd21 jd21 6.5K Feb 18 15:23 names_dataset.ipynb\n", - "-rw-rw-r-- 1 jd21 jd21 137 Feb 18 15:20 README.md\n", - "-rw-rw-r-- 1 jd21 jd21 18K Feb 18 15:47 simple_linked_dataset.ipynb\n", - "-rw-rw-r-- 1 jd21 jd21 1.8K Feb 18 15:25 temp.hdf5\n", - "-rw-rw-r-- 1 jd21 jd21 2.6K Feb 18 15:45 user_assessments.json\n", - "-rw-rw-r-- 1 jd21 jd21 1.7K Feb 18 15:44 user_assessments.yaml\n", - "drwxrwxr-x 2 jd21 jd21 4.0K Feb 18 14:36 users_assessments\n", - "-rw-rw-r-- 1 jd21 jd21 383 Feb 18 15:44 users.csv\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "bash: line 3: exetera: command not found\n" - ] - } - ], - "source": [ - "#Import csv to hdf5 through command line:\n", - "%%bash\n", - "\n", - "exetera import -w -s user_assessments.json -i \"users:users.csv, assessments:assessments.csv\" -o user_assessments.hdf5\n", - "ls -lh" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/user_assessments.json b/examples/user_assessments.json new file mode 100644 index 00000000..e3dcf99f --- /dev/null +++ b/examples/user_assessments.json @@ -0,0 +1,121 @@ + +{ + "exetera": { + "version": "1.0.0" + }, + "schema": { + "users": { + "primary_keys": [ + "id" + ], + "fields": { + "id": { + "field_type": "fixed_string", + "length": 32 + }, + "FirstName": { + "field_type": "string" + }, + "LastName": { + "field_type": "string" + }, + "bmi": { + "field_type": "numeric", + "value_type": "int32" + }, + "has_diabetes": { + "field_type": "categorical", + "categorical": { + "value_type": "int8", + "strings_to_values": { + "": 0, + "False": 1, + "True": 2 + } + } + }, + "height_cm": { + "field_type": "numeric", + "value_type": "int32" + }, + "year_of_birth": { + "field_type": "numeric", + "value_type": "int32" + } + } + }, + "assessments": { + "primary_keys": [ + "id" + ], + "foreign_keys": { + "user_id_key": { + "space": "users", + "key": "id" + } + }, + "fields": { + "id": { + "field_type": "fixed_string", + "length": 32 + }, + "date": { + "field_type": "datetime" + }, + "user_id": { + "field_type": "fixed_string", + "length": 32 + }, + "abdominal_pain": { + "field_type": "categorical", + "categorical": { + "value_type": "int8", + "strings_to_values": { + "": 0, + "False": 1, + "True": 2 + } + } + }, + "brain_fog": { + "field_type": "categorical", + "categorical": { + "value_type": "int8", + "strings_to_values": { + "": 0, + "False": 1, + "True": 2 + } + } + }, + "loss_of_smell": { + "field_type": "categorical", + "categorical": { + "value_type": "int8", + "strings_to_values": { + "": 0, + "False": 1, + "True": 2 + } + } + }, + "tested_covid_positive": { + "field_type": "categorical", + "categorical": { + "value_type": "int8", + "strings_to_values": { + "": 0, + "waiting": 1, + "no": 2, + "yes": 3 + } + } + }, + "temperature_f": { + "field_type": "numeric", + "value_type": "float32" + } + } + } + } +} diff --git a/examples/users.csv b/examples/users.csv new file mode 100644 index 00000000..2fdefe97 --- /dev/null +++ b/examples/users.csv @@ -0,0 +1,11 @@ +id,FirstName,LastName,bmi,has_diabetes,height_cm,year_of_birth +0,"Grace","None",39,1,130,1967 +1,"Carol","Nobody",38,0,119,1975 +2,"Wendy","Random",28,0,128,1926 +3,"Mallory","Nobody",25,0,117,1944 +4,"Xavier","Unknown",29,1,190,1974 +5,"Olivia","Thunk",26,0,107,2004 +6,"Xavier","Anon",30,0,175,1973 +7,"Xavier","Null",37,0,140,1963 +8,"Ivan","Bloggs",37,0,134,1999 +9,"Trudy","Bar",28,0,116,1929 From 071be037433cfa719f827a64e29e167013adfebf Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 23 Feb 2022 14:44:53 +0000 Subject: [PATCH 161/181] minor update on readme --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 638bf025..f0bf2414 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,7 +5,7 @@ This folder contains a few examples on how to use ExeTera in different scenarios #### Names dataset This example shows how to generate ExeTera HDF5 datafile through 'importer.import_with_schema' function, and a few basic commands to print the dataset content. -#### simple_linked_dataset +#### import_dataset This example shows how to import multiple CSV files into a ExeTera HDF5 datafile. The example datafile has a similar structure to Covid Symptom Study (CSS) dataset, including a user table and a assessments table. From c908397bd3f59bc37e344837e07048b4cac23f93 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 23 Feb 2022 14:48:09 +0000 Subject: [PATCH 162/181] remove output from notebooks --- examples/advanced_operations.ipynb | 305 +++-------------------------- examples/basic_concept.ipynb | 144 +++----------- 2 files changed, 52 insertions(+), 397 deletions(-) diff --git a/examples/advanced_operations.ipynb b/examples/advanced_operations.ipynb index 2d6e1db0..83ce2c0d 100644 --- a/examples/advanced_operations.ipynb +++ b/examples/advanced_operations.ipynb @@ -10,72 +10,20 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "ac06eae4-8214-4e96-a419-d361698825a8", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "dataset.hdf5 name_gender_dataset.hdf5\ttemp.hdf5 user_assessments.hdf5\n" - ] - } - ], + "outputs": [], "source": [ "!ls *hdf5" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "4eae8d06-0872-4e30-b6c8-2e8ba86fe9f4", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "dict_keys(['assessments', 'users'])\n", - "Columns in users table: odict_keys(['FirstName', 'LastName', 'bmi', 'bmi_valid', 'has_diabetes', 'height_cm', 'height_cm_valid', 'id', 'j_valid_from', 'j_valid_to', 'year_of_birth', 'year_of_birth_valid'])\n", - "fields\t bmi\t has_diabetes\t height_cm\t year_of_birth\t\n", - "count\t 10\t 10\t 10\t 10\t\n", - "unique\t NaN\t 1\t NaN\t NaN\t\n", - "top\t NaN\t 0\t NaN\t NaN\t\n", - "freq\t NaN\t 10\t NaN\t NaN\t\n", - "mean\t 31.90\t NaN\t 163.60\t 1963.70\t\n", - "std\t 7.13\t NaN\t 25.25\t 28.96\t\n", - "min\t 20.00\t NaN\t 111.00\t 1926.00\t\n", - "25%\t 20.07\t NaN\t 111.38\t 1926.18\t\n", - "50%\t 20.14\t NaN\t 111.77\t 1926.36\t\n", - "75%\t 20.20\t NaN\t 112.15\t 1926.54\t\n", - "max\t 39.00\t NaN\t 197.00\t 2009.00\t\n", - "\n", - "\n" - ] - }, - { - "data": { - "text/plain": [ - "{'fields': ['bmi', 'has_diabetes', 'height_cm', 'year_of_birth'],\n", - " 'count': [10, 10, 10, 10],\n", - " 'mean': ['31.90', 'NaN', '163.60', '1963.70'],\n", - " 'std': ['7.13', 'NaN', '25.25', '28.96'],\n", - " 'min': ['20.00', 'NaN', '111.00', '1926.00'],\n", - " '25%': ['20.07', 'NaN', '111.38', '1926.18'],\n", - " '50%': ['20.14', 'NaN', '111.77', '1926.36'],\n", - " '75%': ['20.20', 'NaN', '112.15', '1926.54'],\n", - " 'max': ['39.00', 'NaN', '197.00', '2009.00'],\n", - " 'unique': ['NaN', 1, 'NaN', 'NaN'],\n", - " 'top': ['NaN', 0, 'NaN', 'NaN'],\n", - " 'freq': ['NaN', 10, 'NaN', 'NaN']}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from exetera.core.session import Session\n", "s = Session() # not recommended, but to cover all the cells in the example, we use this way here\n", @@ -89,57 +37,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "67d79440-4c2d-42ec-8f62-3d10bc72e3e7", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Columns in users table: odict_keys(['abdominal_pain', 'brain_fog', 'date', 'id', 'j_valid_from', 'j_valid_to', 'loss_of_smell', 'temperature_f', 'temperature_f_valid', 'tested_covid_positive', 'user_id'])\n", - "fields\t abdominal_pain\t brain_fog\t date\t loss_of_smell\t temperature_f\t\n", - "count\t 30\t 30\t 30\t 30\t 30\t\n", - "unique\t 1\t 1\t NaN\t 1\t NaN\t\n", - "top\t 0\t 0\t NaN\t 0\t NaN\t\n", - "freq\t 30\t 30\t NaN\t 30\t NaN\t\n", - "mean\t NaN\t NaN\t 1628360314.24\t NaN\t 101.62\t\n", - "std\t NaN\t NaN\t 8270828.31\t NaN\t 4.21\t\n", - "min\t NaN\t NaN\t 1614692820.02\t NaN\t 95.29\t\n", - "25%\t NaN\t NaN\t 1614707205.26\t NaN\t 95.30\t\n", - "50%\t NaN\t NaN\t 1614721590.50\t NaN\t 95.31\t\n", - "75%\t NaN\t NaN\t 1614735975.75\t NaN\t 95.32\t\n", - "max\t NaN\t NaN\t 1641848736.34\t NaN\t 109.54\t\n", - "\n", - "\n" - ] - }, - { - "data": { - "text/plain": [ - "{'fields': ['abdominal_pain',\n", - " 'brain_fog',\n", - " 'date',\n", - " 'loss_of_smell',\n", - " 'temperature_f'],\n", - " 'count': [30, 30, 30, 30, 30],\n", - " 'mean': ['NaN', 'NaN', '1628360314.24', 'NaN', '101.62'],\n", - " 'std': ['NaN', 'NaN', '8270828.31', 'NaN', '4.21'],\n", - " 'min': ['NaN', 'NaN', '1614692820.02', 'NaN', '95.29'],\n", - " '25%': ['NaN', 'NaN', '1614707205.26', 'NaN', '95.30'],\n", - " '50%': ['NaN', 'NaN', '1614721590.50', 'NaN', '95.31'],\n", - " '75%': ['NaN', 'NaN', '1614735975.75', 'NaN', '95.32'],\n", - " 'max': ['NaN', 'NaN', '1641848736.34', 'NaN', '109.54'],\n", - " 'unique': [1, 1, 'NaN', 1, 'NaN'],\n", - " 'top': [0, 0, 'NaN', 0, 'NaN'],\n", - " 'freq': [30, 30, 'NaN', 30, 'NaN']}" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "asmts = src['assessments']\n", "print('Columns in users table:', asmts.keys())\n", @@ -158,18 +59,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "dab8e873-cf1f-47c5-bebb-18bde4357543", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "8 adults out of 10 total subjects found.\n" - ] - } - ], + "outputs": [], "source": [ "with Session() as s:\n", " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", @@ -184,19 +77,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "c336d758-878a-4df6-8458-cc3ddf280964", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[ True True True False True True True False True True]\n", - "[b'0' b'1' b'2' b'4' b'5' b'6' b'8' b'9']\n" - ] - } - ], + "outputs": [], "source": [ "# Combining filters\n", "# we can make use of fields directly rather than fetching the underlying numpy arrays\n", @@ -220,18 +104,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "c89f52b9-f96d-4e36-a8e3-67953aaedae4", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "3.1172735691070557\n" - ] - } - ], + "outputs": [], "source": [ "import numpy as np\n", "import time\n", @@ -250,18 +126,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "202321d4-b1fc-47bd-8878-779df121c913", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.07697820663452148\n" - ] - } - ], + "outputs": [], "source": [ "#sum up the symptoms with njit\n", "from numba import njit\n", @@ -289,21 +157,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "acfec7b4-01ec-4bf5-b8d3-d99a9db98273", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "10 30\n", - "10 30\n", - "10 30\n", - "10 30\n" - ] - } - ], + "outputs": [], "source": [ "with Session() as s:\n", " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", @@ -334,18 +191,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "29ef8595-6835-4f0d-a5bf-6ee15bf8eabd", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "30\n" - ] - } - ], + "outputs": [], "source": [ "#transform rather than group by\n", "with Session() as s:\n", @@ -389,19 +238,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "85261f6a-ba35-45a0-ba88-60a99a16ebe3", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "30\n", - "odict_keys(['FirstName', 'LastName', 'bmi', 'bmi_valid', 'has_diabetes', 'height_cm', 'height_cm_valid', 'id_l', 'j_valid_from_l', 'j_valid_to_l', 'year_of_birth', 'year_of_birth_valid', 'abdominal_pain', 'brain_fog', 'date', 'id_r', 'j_valid_from_r', 'j_valid_to_r', 'loss_of_smell', 'temperature_f', 'temperature_f_valid', 'tested_covid_positive', 'user_id'])\n" - ] - } - ], + "outputs": [], "source": [ "from exetera.core.dataframe import merge\n", "with Session() as s:\n", @@ -423,42 +263,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "f72666d8-a5e3-48e9-859d-1d1e56b4a768", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "sorted ('id_l',) index in 0.0001461505889892578s\n", - " 'FirstName' reordered in 0.182114839553833s\n", - " 'LastName' reordered in 0.0009195804595947266s\n", - " 'bmi' reordered in 0.000553131103515625s\n", - " 'bmi_valid' reordered in 0.0005764961242675781s\n", - " 'has_diabetes' reordered in 0.002074718475341797s\n", - " 'height_cm' reordered in 0.0005881786346435547s\n", - " 'height_cm_valid' reordered in 0.00047326087951660156s\n", - " 'id_l' reordered in 0.0004417896270751953s\n", - " 'j_valid_from_l' reordered in 0.00044655799865722656s\n", - " 'j_valid_to_l' reordered in 0.00038623809814453125s\n", - " 'year_of_birth' reordered in 0.00038552284240722656s\n", - " 'year_of_birth_valid' reordered in 0.0004639625549316406s\n", - " 'abdominal_pain' reordered in 0.001451730728149414s\n", - " 'brain_fog' reordered in 0.0013928413391113281s\n", - " 'date' reordered in 0.0003643035888671875s\n", - " 'id_r' reordered in 0.00040459632873535156s\n", - " 'j_valid_from_r' reordered in 0.0004069805145263672s\n", - " 'j_valid_to_r' reordered in 0.0003635883331298828s\n", - " 'loss_of_smell' reordered in 0.0013535022735595703s\n", - " 'temperature_f' reordered in 0.00044083595275878906s\n", - " 'temperature_f_valid' reordered in 0.00040268898010253906s\n", - " 'tested_covid_positive' reordered in 0.0015273094177246094s\n", - " 'user_id' reordered in 0.00039386749267578125s\n", - "fields reordered in 0.1985929012298584s\n" - ] - } - ], + "outputs": [], "source": [ "from exetera.core.dataframe import merge\n", "with Session() as s:\n", @@ -470,42 +278,10 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": null, "id": "1d77e233-16ed-4333-bec8-e8bc8f0e297d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "sorted ('id_l',) index in 0.00018310546875s\n", - " 'FirstName' reordered in 0.005521535873413086s\n", - " 'LastName' reordered in 0.005347490310668945s\n", - " 'bmi' reordered in 0.0029451847076416016s\n", - " 'bmi_valid' reordered in 0.0027124881744384766s\n", - " 'has_diabetes' reordered in 0.00469517707824707s\n", - " 'height_cm' reordered in 0.002373218536376953s\n", - " 'height_cm_valid' reordered in 0.002882719039916992s\n", - " 'id_l' reordered in 0.002743959426879883s\n", - " 'j_valid_from_l' reordered in 0.002353191375732422s\n", - " 'j_valid_to_l' reordered in 0.0024633407592773438s\n", - " 'year_of_birth' reordered in 0.002484560012817383s\n", - " 'year_of_birth_valid' reordered in 0.002560138702392578s\n", - " 'abdominal_pain' reordered in 0.00513005256652832s\n", - " 'brain_fog' reordered in 0.0047473907470703125s\n", - " 'date' reordered in 0.0025992393493652344s\n", - " 'id_r' reordered in 0.0029125213623046875s\n", - " 'j_valid_from_r' reordered in 0.005130767822265625s\n", - " 'j_valid_to_r' reordered in 0.003270387649536133s\n", - " 'loss_of_smell' reordered in 0.004971504211425781s\n", - " 'temperature_f' reordered in 0.0033884048461914062s\n", - " 'temperature_f_valid' reordered in 0.003406047821044922s\n", - " 'tested_covid_positive' reordered in 0.0054056644439697266s\n", - " 'user_id' reordered in 0.0029478073120117188s\n", - "fields reordered in 0.08384275436401367s\n" - ] - } - ], + "outputs": [], "source": [ "from exetera.core.dataframe import merge\n", "with Session() as s:\n", @@ -518,23 +294,10 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": null, "id": "4e1396c8-e851-4e77-ba43-9ec4f9d01062", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[b'0' b'0' b'0' b'1' b'1' b'1' b'2' b'2' b'2' b'3' b'3' b'3' b'4' b'4'\n", - " b'4' b'5' b'5' b'5' b'6' b'6' b'6' b'7' b'7' b'7' b'8' b'8' b'8' b'9'\n", - " b'9' b'9']\n", - "[b'0' b'0' b'0' b'1' b'1' b'1' b'2' b'2' b'2' b'3' b'3' b'3' b'4' b'4'\n", - " b'4' b'5' b'5' b'5' b'6' b'6' b'6' b'7' b'7' b'7' b'8' b'8' b'8' b'9'\n", - " b'9' b'9']\n" - ] - } - ], + "outputs": [], "source": [ "#sorting with an index\n", "with Session() as s:\n", @@ -564,7 +327,7 @@ }, { "cell_type": "code", - "execution_count": 80, + "execution_count": null, "id": "e7c67e89-1651-41b6-9f76-697875a82d7a", "metadata": {}, "outputs": [], @@ -587,25 +350,17 @@ }, { "cell_type": "code", - "execution_count": 81, + "execution_count": null, "id": "12c08d25-f2ee-41c1-a5e8-73a34f5d94a2", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "adults.csv column_filtered.csv merged.csv\n" - ] - } - ], + "outputs": [], "source": [ "!ls *csv" ] }, { "cell_type": "code", - "execution_count": 82, + "execution_count": null, "id": "a58d47e2-7237-4cd4-938a-4ad50d2ceda1", "metadata": {}, "outputs": [], diff --git a/examples/basic_concept.ipynb b/examples/basic_concept.ipynb index 25795794..7aced09d 100644 --- a/examples/basic_concept.ipynb +++ b/examples/basic_concept.ipynb @@ -48,18 +48,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "2271e28e-21e1-4720-9c32-4aaf9b310546", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "dataset.hdf5 name_gender_dataset.hdf5\ttemp.hdf5\n" - ] - } - ], + "outputs": [], "source": [ "!ls *hdf5" ] @@ -84,7 +76,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "74ad07d9-df92-4668-bb2f-8964ea91018c", "metadata": {}, "outputs": [], @@ -116,7 +108,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "88bd5317-bfa0-4a31-bf9c-3475f17c4afb", "metadata": {}, "outputs": [], @@ -136,18 +128,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "3a2eaf21-ddc7-4b1a-8c63-4471ee8aee6d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "dict_keys(['participants', 'tests'])\n" - ] - } - ], + "outputs": [], "source": [ "with Session() as s:\n", " ds1 = s.open_dataset('user_assessments.hdf5', 'r', 'ds1')\n", @@ -171,31 +155,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "8d7f668f-9630-49cb-83f0-6ac32da9feb6", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "dict_keys(['foo'])\n", - "Renamed: dict_keys([])\n" - ] - }, - { - "ename": "ValueError", - "evalue": "Can not find the name from this dataset.", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0mds\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'bar'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mds\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'foo'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;31m# internally performs a rename\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Renamed:'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mds\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 13\u001b[0;31m \u001b[0mdataset\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmove\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mds\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'bar'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mds\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'foo'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 14\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Moved:'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mds\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/miniconda3/lib/python3.8/site-packages/exetera/core/dataset.py\u001b[0m in \u001b[0;36m__getitem__\u001b[0;34m(self, name)\u001b[0m\n\u001b[1;32m 163\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mTypeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"The name must be a str object.\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 164\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__contains__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 165\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Can not find the name from this dataset.\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 166\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 167\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_dataframes\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mValueError\u001b[0m: Can not find the name from this dataset." - ] - } - ], + "outputs": [], "source": [ "from exetera.core import dataset\n", "\n", @@ -244,24 +207,10 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "id": "5221d1da-8f36-4259-a800-3fadfc50600c", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "odict_keys(['foo', 'foobar'])\n", - "Original: [0 1 2 3 4 5 6 7 8 9]\n", - "Filtered: [0 2 4 6 8]\n", - "Original: [0 2 4 6 8]\n", - "Previous re-index: [0 1 2 3 4 5 6 7 8 9]\n", - "Re-indexed: [9 8 7 6 5 4 3 2 1 0]\n", - "Re-indexed: [9 8 7 6 5 4 3 2 1 0]\n" - ] - } - ], + "outputs": [], "source": [ "import numpy as np\n", "with Session() as s:\n", @@ -334,18 +283,10 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "id": "a4c81326-19c0-4e2f-8ac8-6e44efc0a9e8", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0 1 2 3 4 5 6 7 8 9]\n" - ] - } - ], + "outputs": [], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", @@ -368,19 +309,10 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "id": "eaa861df-84ee-48a9-8e42-1e6332d8b824", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0 1 2 3 4 5 6 7 8 9]\n", - "[]\n" - ] - } - ], + "outputs": [], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", @@ -404,18 +336,10 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "id": "28619c4e-8dd0-49e1-9f69-d2e82c848c9d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[ 0 2 4 6 8 10 12 14 16 18]\n" - ] - } - ], + "outputs": [], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", @@ -440,18 +364,10 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "id": "ef5cfb09-4f5a-434d-9eab-4ddd7c2d9f0d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[ True False True False True False True False True False]\n" - ] - } - ], + "outputs": [], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", @@ -465,18 +381,10 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": null, "id": "4731e2b7-7bf7-48e0-99ee-024bff1c6047", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[ True True True True True True True True True True]\n" - ] - } - ], + "outputs": [], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", @@ -501,18 +409,10 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": null, "id": "ae438918-d810-46c0-a1ad-376c8189444f", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[False False False False False False False False False False]\n" - ] - } - ], + "outputs": [], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", From 72cca743958613ba1d6a8a4795fcac97ce57ea54 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 25 Feb 2022 15:40:37 +0000 Subject: [PATCH 163/181] minor update on example notebooks --- examples/advanced_operations.ipynb | 350 +++++++++++++++++++++++++---- examples/basic_concept.ipynb | 233 ++++++++++++------- examples/import_dataset.ipynb | 269 ++++++++++++++++++++-- 3 files changed, 706 insertions(+), 146 deletions(-) diff --git a/examples/advanced_operations.ipynb b/examples/advanced_operations.ipynb index 83ce2c0d..f7f47ba7 100644 --- a/examples/advanced_operations.ipynb +++ b/examples/advanced_operations.ipynb @@ -5,25 +5,79 @@ "id": "a133c850-1cc3-4ee5-830a-9440e70cd90c", "metadata": {}, "source": [ + "# Advanced Operations Example\n", + "\n", "This example uses the user_assessments hdfs file from RandomDataset. User assessments file contains a user table and a assessments table, that imitate the data structure of in CSS (Covid Symptom Study) project." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "ac06eae4-8214-4e96-a419-d361698825a8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dataset.hdf5 temp2.hdf5 temp.hdf5 user_assessments.hdf5\n" + ] + } + ], "source": [ "!ls *hdf5" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "4eae8d06-0872-4e30-b6c8-2e8ba86fe9f4", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dict_keys(['assessments', 'users'])\n", + "Columns in users table: odict_keys(['FirstName', 'LastName', 'bmi', 'bmi_valid', 'has_diabetes', 'height_cm', 'height_cm_valid', 'id', 'j_valid_from', 'j_valid_to', 'year_of_birth', 'year_of_birth_valid'])\n", + "fields\t bmi\t has_diabetes\t height_cm\t year_of_birth\t\n", + "count\t 10\t 10\t 10\t 10\t\n", + "unique\t NaN\t 1\t NaN\t NaN\t\n", + "top\t NaN\t 0\t NaN\t NaN\t\n", + "freq\t NaN\t 10\t NaN\t NaN\t\n", + "mean\t 31.70\t NaN\t 135.60\t 1965.40\t\n", + "std\t 5.14\t NaN\t 25.39\t 24.87\t\n", + "min\t 25.00\t NaN\t 107.00\t 1926.00\t\n", + "25%\t 25.02\t NaN\t 107.20\t 1926.07\t\n", + "50%\t 25.05\t NaN\t 107.41\t 1926.13\t\n", + "75%\t 25.07\t NaN\t 107.61\t 1926.20\t\n", + "max\t 39.00\t NaN\t 190.00\t 2004.00\t\n", + "\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "{'fields': ['bmi', 'has_diabetes', 'height_cm', 'year_of_birth'],\n", + " 'count': [10, 10, 10, 10],\n", + " 'mean': ['31.70', 'NaN', '135.60', '1965.40'],\n", + " 'std': ['5.14', 'NaN', '25.39', '24.87'],\n", + " 'min': ['25.00', 'NaN', '107.00', '1926.00'],\n", + " '25%': ['25.02', 'NaN', '107.20', '1926.07'],\n", + " '50%': ['25.05', 'NaN', '107.41', '1926.13'],\n", + " '75%': ['25.07', 'NaN', '107.61', '1926.20'],\n", + " 'max': ['39.00', 'NaN', '190.00', '2004.00'],\n", + " 'unique': ['NaN', 1, 'NaN', 'NaN'],\n", + " 'top': ['NaN', 0, 'NaN', 'NaN'],\n", + " 'freq': ['NaN', 10, 'NaN', 'NaN']}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from exetera.core.session import Session\n", "s = Session() # not recommended, but to cover all the cells in the example, we use this way here\n", @@ -37,10 +91,57 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "67d79440-4c2d-42ec-8f62-3d10bc72e3e7", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Columns in users table: odict_keys(['abdominal_pain', 'brain_fog', 'date', 'id', 'j_valid_from', 'j_valid_to', 'loss_of_smell', 'temperature_f', 'temperature_f_valid', 'tested_covid_positive', 'user_id'])\n", + "fields\t abdominal_pain\t brain_fog\t date\t loss_of_smell\t temperature_f\t\n", + "count\t 30\t 30\t 30\t 30\t 30\t\n", + "unique\t 1\t 1\t NaN\t 1\t NaN\t\n", + "top\t 0\t 0\t NaN\t 0\t NaN\t\n", + "freq\t 30\t 30\t NaN\t 30\t NaN\t\n", + "mean\t NaN\t NaN\t 1628912712.34\t NaN\t 101.36\t\n", + "std\t NaN\t NaN\t 10077317.46\t NaN\t 4.33\t\n", + "min\t NaN\t NaN\t 1613872118.68\t NaN\t 95.23\t\n", + "25%\t NaN\t NaN\t 1613975491.70\t NaN\t 95.24\t\n", + "50%\t NaN\t NaN\t 1614078864.72\t NaN\t 95.26\t\n", + "75%\t NaN\t NaN\t 1614182237.74\t NaN\t 95.28\t\n", + "max\t NaN\t NaN\t 1644821469.46\t NaN\t 109.64\t\n", + "\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "{'fields': ['abdominal_pain',\n", + " 'brain_fog',\n", + " 'date',\n", + " 'loss_of_smell',\n", + " 'temperature_f'],\n", + " 'count': [30, 30, 30, 30, 30],\n", + " 'mean': ['NaN', 'NaN', '1628912712.34', 'NaN', '101.36'],\n", + " 'std': ['NaN', 'NaN', '10077317.46', 'NaN', '4.33'],\n", + " 'min': ['NaN', 'NaN', '1613872118.68', 'NaN', '95.23'],\n", + " '25%': ['NaN', 'NaN', '1613975491.70', 'NaN', '95.24'],\n", + " '50%': ['NaN', 'NaN', '1614078864.72', 'NaN', '95.26'],\n", + " '75%': ['NaN', 'NaN', '1614182237.74', 'NaN', '95.28'],\n", + " 'max': ['NaN', 'NaN', '1644821469.46', 'NaN', '109.64'],\n", + " 'unique': [1, 1, 'NaN', 1, 'NaN'],\n", + " 'top': [0, 0, 'NaN', 0, 'NaN'],\n", + " 'freq': [30, 30, 'NaN', 30, 'NaN']}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "asmts = src['assessments']\n", "print('Columns in users table:', asmts.keys())\n", @@ -52,17 +153,25 @@ "id": "3be7ec97-a8d2-449f-9f57-e54a4effb52c", "metadata": {}, "source": [ - "

Filtering

\n", - "Filtering is performed through the use of the apply_filter function. This can be performed on individual fields or at a dataframe level. apply_filter applies the filter on data rows.\n", + "## 4.Filtering\n", + "Filtering is performed through the use of the apply_filter function. This can be performed on __individual fields__ or at a __dataframe level__. apply_filter applies the filter on data rows.\n", "\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "dab8e873-cf1f-47c5-bebb-18bde4357543", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "9 adults out of 10 total subjects found.\n" + ] + } + ], "source": [ "with Session() as s:\n", " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", @@ -77,10 +186,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "c336d758-878a-4df6-8458-cc3ddf280964", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ True True True True True False True True True True]\n", + "[b'0' b'1' b'2' b'3' b'4' b'6' b'7' b'8' b'9']\n" + ] + } + ], "source": [ "# Combining filters\n", "# we can make use of fields directly rather than fetching the underlying numpy arrays\n", @@ -98,16 +216,25 @@ "id": "3316eb91-2b59-46d9-9f4f-1262192d6807", "metadata": {}, "source": [ - "

Performance boost using numba

\n", + "## 5.Performance boost using numba\n", + "\n", "As the underlying data is fetched as a numpy array, you can utlize the numba @njit functions to accelarate the data process. For example in the case of summing up symptoms, use a seperate function with @njit decrator can speed up the performance. " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "c89f52b9-f96d-4e36-a8e3-67953aaedae4", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3.257335901260376\n" + ] + } + ], "source": [ "import numpy as np\n", "import time\n", @@ -126,10 +253,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "202321d4-b1fc-47bd-8878-779df121c913", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.08656787872314453\n" + ] + } + ], "source": [ "#sum up the symptoms with njit\n", "from numba import njit\n", @@ -152,15 +287,28 @@ "id": "2723a9c9-36a5-4737-870c-7b4d130307f8", "metadata": {}, "source": [ - "

Groupby

" + "## 6.Groupby\n", + "\n", + "The groupby is similar to the groupby api from Pandas dataframe." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "acfec7b4-01ec-4bf5-b8d3-d99a9db98273", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10 30\n", + "10 30\n", + "10 30\n", + "10 30\n" + ] + } + ], "source": [ "with Session() as s:\n", " dst = s.open_dataset('temp2.hdf5', 'w', 'dst')\n", @@ -191,10 +339,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "29ef8595-6835-4f0d-a5bf-6ee15bf8eabd", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "30\n" + ] + } + ], "source": [ "#transform rather than group by\n", "with Session() as s:\n", @@ -221,27 +377,41 @@ "id": "93e5314b-d130-4ded-a41a-ca7fda798ae1", "metadata": {}, "source": [ - "

Join

\n", + "## 7.Join\n", + "\n", "ExeTera provides functions that provide pandas-like merge functionality on DataFrame instances. We have made this operation as familiar as possible to Pandas users, but there are a couple of differences that we should highlight:\n", - "
\n", "\n", - "• merge is provided as a function in the dataframe unit, rather than as a member function on DataFrame instances \n", - "
\n", - "• merge takes three dataframe arguments, left, right and dest. This is due to the fact that DataFrames are always backed up by a datastore and so rather than create an in-memory destination dataframe, the resulting merged fields must be written to a dataframe of your choosing. \n", - "
\n", - "• Note, this can either be a separate dataframe or it can be the dataframe that you are merging to (typically left in the case of a \"left\" merge and right in the case of a \"right\" merge\n", - "
\n", - "• merge takes a number of optional hint fields that can save time when working with large datasets. These specify whether the keys are unique or ordered and allow the merge to occur without first checking this\n", - "
\n", - "• merge has a number of highly scalable algorithms that can be used when the key data is sorted and / or unique." + "\n", + "- merge is provided as a function in the dataframe unit, rather than as a member function on DataFrame instances \n", + "\n", + "\n", + "- merge takes three dataframe arguments, left, right and dest. This is due to the fact that DataFrames are always backed up by a datastore and so rather than create an in-memory destination dataframe, the resulting merged fields must be written to a dataframe of your choosing. \n", + "\n", + "\n", + "- Note, this can either be a separate dataframe or it can be the dataframe that you are merging to (typically left in the case of a \"left\" merge and right in the case of a \"right\" merge\n", + "\n", + "\n", + "- merge takes a number of optional hint fields that can save time when working with large datasets. These specify whether the keys are unique or ordered and allow the merge to occur without first checking this\n", + "\n", + "\n", + "- merge has a number of highly scalable algorithms that can be used when the key data is sorted and / or unique." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "85261f6a-ba35-45a0-ba88-60a99a16ebe3", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "30\n", + "odict_keys(['FirstName', 'LastName', 'bmi', 'bmi_valid', 'has_diabetes', 'height_cm', 'height_cm_valid', 'id_l', 'j_valid_from_l', 'j_valid_to_l', 'year_of_birth', 'year_of_birth_valid', 'abdominal_pain', 'brain_fog', 'date', 'id_r', 'j_valid_from_r', 'j_valid_to_r', 'loss_of_smell', 'temperature_f', 'temperature_f_valid', 'tested_covid_positive', 'user_id'])\n" + ] + } + ], "source": [ "from exetera.core.dataframe import merge\n", "with Session() as s:\n", @@ -258,15 +428,47 @@ "id": "899c862b-9a2a-4e08-8a5b-d7c2cadaf7b1", "metadata": {}, "source": [ - "

Sort

" + "## 8.Sort" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "f72666d8-a5e3-48e9-859d-1d1e56b4a768", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sorted ('id_l',) index in 0.0001354217529296875s\n", + " 'FirstName' reordered in 0.2100369930267334s\n", + " 'LastName' reordered in 0.0011086463928222656s\n", + " 'bmi' reordered in 0.0004885196685791016s\n", + " 'bmi_valid' reordered in 0.0004451274871826172s\n", + " 'has_diabetes' reordered in 0.0017483234405517578s\n", + " 'height_cm' reordered in 0.0004525184631347656s\n", + " 'height_cm_valid' reordered in 0.00041365623474121094s\n", + " 'id_l' reordered in 0.000408172607421875s\n", + " 'j_valid_from_l' reordered in 0.00040650367736816406s\n", + " 'j_valid_to_l' reordered in 0.0003733634948730469s\n", + " 'year_of_birth' reordered in 0.00042748451232910156s\n", + " 'year_of_birth_valid' reordered in 0.0006887912750244141s\n", + " 'abdominal_pain' reordered in 0.0015702247619628906s\n", + " 'brain_fog' reordered in 0.002073049545288086s\n", + " 'date' reordered in 0.0006480216979980469s\n", + " 'id_r' reordered in 0.0005962848663330078s\n", + " 'j_valid_from_r' reordered in 0.00048804283142089844s\n", + " 'j_valid_to_r' reordered in 0.0003993511199951172s\n", + " 'loss_of_smell' reordered in 0.0014781951904296875s\n", + " 'temperature_f' reordered in 0.00043654441833496094s\n", + " 'temperature_f_valid' reordered in 0.0004210472106933594s\n", + " 'tested_covid_positive' reordered in 0.0016057491302490234s\n", + " 'user_id' reordered in 0.0004360675811767578s\n", + "fields reordered in 0.22782111167907715s\n" + ] + } + ], "source": [ "from exetera.core.dataframe import merge\n", "with Session() as s:\n", @@ -278,10 +480,42 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "1d77e233-16ed-4333-bec8-e8bc8f0e297d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sorted ('id_l',) index in 0.00015783309936523438s\n", + " 'FirstName' reordered in 0.003790140151977539s\n", + " 'LastName' reordered in 0.003641843795776367s\n", + " 'bmi' reordered in 0.002112150192260742s\n", + " 'bmi_valid' reordered in 0.0016775131225585938s\n", + " 'has_diabetes' reordered in 0.0033807754516601562s\n", + " 'height_cm' reordered in 0.0017406940460205078s\n", + " 'height_cm_valid' reordered in 0.0017364025115966797s\n", + " 'id_l' reordered in 0.0030646324157714844s\n", + " 'j_valid_from_l' reordered in 0.0018968582153320312s\n", + " 'j_valid_to_l' reordered in 0.001705169677734375s\n", + " 'year_of_birth' reordered in 0.0018277168273925781s\n", + " 'year_of_birth_valid' reordered in 0.0019350051879882812s\n", + " 'abdominal_pain' reordered in 0.003263711929321289s\n", + " 'brain_fog' reordered in 0.0048716068267822266s\n", + " 'date' reordered in 0.002129793167114258s\n", + " 'id_r' reordered in 0.00180816650390625s\n", + " 'j_valid_from_r' reordered in 0.0025141239166259766s\n", + " 'j_valid_to_r' reordered in 0.001980304718017578s\n", + " 'loss_of_smell' reordered in 0.0038106441497802734s\n", + " 'temperature_f' reordered in 0.002017974853515625s\n", + " 'temperature_f_valid' reordered in 0.0025076866149902344s\n", + " 'tested_covid_positive' reordered in 0.0039484500885009766s\n", + " 'user_id' reordered in 0.002045869827270508s\n", + "fields reordered in 0.06040382385253906s\n" + ] + } + ], "source": [ "from exetera.core.dataframe import merge\n", "with Session() as s:\n", @@ -294,10 +528,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "4e1396c8-e851-4e77-ba43-9ec4f9d01062", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[b'0' b'0' b'0' b'1' b'1' b'1' b'2' b'2' b'2' b'3' b'3' b'3' b'4' b'4'\n", + " b'4' b'5' b'5' b'5' b'6' b'6' b'6' b'7' b'7' b'7' b'8' b'8' b'8' b'9'\n", + " b'9' b'9']\n", + "[b'0' b'0' b'0' b'1' b'1' b'1' b'2' b'2' b'2' b'3' b'3' b'3' b'4' b'4'\n", + " b'4' b'5' b'5' b'5' b'6' b'6' b'6' b'7' b'7' b'7' b'8' b'8' b'8' b'9'\n", + " b'9' b'9']\n" + ] + } + ], "source": [ "#sorting with an index\n", "with Session() as s:\n", @@ -322,12 +569,13 @@ "id": "06e2a9e4-400a-44e9-b08e-ef5f282bf6bd", "metadata": {}, "source": [ - "

I/O

" + "## 9. I/O\n", + "You can output an ExeTera dataframe back to csv file." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "e7c67e89-1651-41b6-9f76-697875a82d7a", "metadata": {}, "outputs": [], @@ -350,17 +598,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "12c08d25-f2ee-41c1-a5e8-73a34f5d94a2", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "adults.csv assessments.csv column_filtered.csv merged.csv users.csv\n" + ] + } + ], "source": [ "!ls *csv" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "a58d47e2-7237-4cd4-938a-4ad50d2ceda1", "metadata": {}, "outputs": [], diff --git a/examples/basic_concept.ipynb b/examples/basic_concept.ipynb index 7aced09d..7f41eebc 100644 --- a/examples/basic_concept.ipynb +++ b/examples/basic_concept.ipynb @@ -5,53 +5,25 @@ "id": "d220666f-dad8-4229-9f24-9d24925b5c25", "metadata": {}, "source": [ - "

1, Import

\n", - "

ExeTera utlize HDF5 file format to acquire fast performance when processing the data. Hence the first step of using ExeTera is usually transform the file from other formats, e.g. csv, into HDF5.

\n", - "

ExeTera provides utilities to transform the csv data into HDF5, through either command line or code.

\n", - "How import works\n", - "

\n", - "a. Importing via the exetera import command:
\n", - "\n", - "exetera import
\n", - "-s path/to/covid_schema.json \\
\n", - "-i \"patients:path/to/patient_data.csv, assessments:path/to/assessmentdata.csv,
tests:path/to/covid_test_data.csv, diet:path/to/diet_study_data.csv\" \\
\n", - "-o /path/to/output_dataset_name.hdf5
\n", - "--include \"patients:(id,country_code,blood_group), assessments:(id,patient_id,chest_pain)\"
\n", - "--exclude \"tests:(country_code)\"

\n", - "\n", - "Arguments:
\n", - "-s/--schema: The location and name of the schema file
\n", - "-te/--territories: If set, this only imports the listed territories. If left unset, all territories are imported
\n", - "-i/--inputs : A comma separated list of 'name:file' pairs. This should be put in parentheses if it contains any whitespace. See the example above.
\n", - "-o/--output_hdf5: The path and name to where the resulting hdf5 dataset should be written
\n", - "-ts/--timestamp: An override for the timestamp to be written (defaults to datetime.now(timezone.utc))
\n", - "-w/--overwrite: If set, overwrite any existing dataset with the same name; appends to existing dataset otherwise
\n", - "-n/--include: If set, filters out all fields apart from those in the list.
\n", - "-x/--exclude: If set, filters out the fields in this list.
\n", - "

\n", - "\n", - "

\n", - "b. Importing through code
\n", - "Use importer.import_with_schema(timestamp, output_hdf5_name, schema, tokens, args.overwrite, include_fields, exclude_fields) \n", - "

\n" - ] - }, - { - "cell_type": "markdown", - "id": "febce312-098c-436a-a73f-ecea78c91047", - "metadata": {}, - "source": [ - "Import example\n", - "
\n", - "For the import example, please refer to the example in RandomDataset. After you finish, please copy the hdf5 file here to continue." + "# Basic ExeTera Example\n", + "\n", + "This example shows the basic operations in ExeTera. First please make sure you have a HDF5 file ready. If not, please go through the 'import_dataset' example first." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "2271e28e-21e1-4720-9c32-4aaf9b310546", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dataset.hdf5 temp2.hdf5 temp.hdf5 user_assessments.hdf5\n" + ] + } + ], "source": [ "!ls *hdf5" ] @@ -61,22 +33,24 @@ "id": "46d1a2cd-85cb-4ea5-b794-ea8e07779bfb", "metadata": {}, "source": [ - "

2, ExeTera Session and DataSet

\n", - "

\n", - "Session instances are the top-level ExeTera class. They serve two main purposes:
\n", - "\n", - "1, Functionality for creating / opening / closing Dataset objects, as well as managing the lifetime of open datasets
\n", - "2, Methods that operate on Fields
\n", - "

\n", - "

Creating a session object

\n", - "

\n", - "Creating a Session object can be done multiple ways, but we recommend that you wrap the session in a context manager (with statement). This allows the Session object to automatically manage the datasets that you have opened, closing them all once the with statement is exited. Opening and closing datasets is very fast. When working in jupyter notebooks or jupyter lab, please feel free to create a new Session object for each cell.
\n", - "

" + "## 2, ExeTera Session and DataSet\n", + "\n", + "Session instances are the top-level ExeTera class. They serve two main purposes: \n", + "\n", + "- Functionality for creating / opening / closing Dataset objects, as well as managing the lifetime of open datasets \n", + "\n", + "- Methods that operate on Fields\n", + "\n", + "\n", + "### Creating a session object\n", + "\n", + "\n", + "Creating a Session object can be done multiple ways, but we recommend that you wrap the session in a context manager (with statement). This allows the Session object to automatically manage the datasets that you have opened, closing them all once the with statement is exited. Opening and closing datasets is very fast. When working in jupyter notebooks or jupyter lab, please feel free to create a new Session object for each cell. \n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "74ad07d9-df92-4668-bb2f-8964ea91018c", "metadata": {}, "outputs": [], @@ -98,17 +72,20 @@ "id": "f771302f-0840-4cfb-856c-09dc2c39ade6", "metadata": {}, "source": [ - "

Loading dataset(s)

\n", - "Once you have a session, the next step is typically to open a dataset. Datasets can be opened in one of three modes:
\n", + "### Loading dataset(s)\n", + "\n", + "Once you have a session, the next step is typically to open a dataset. Datasets can be opened in one of three modes: \n", + "\n", + "read - the dataset can be read from but not written to \n", + "\n", + "append - the dataset can be read from and written to \n", "\n", - "read - the dataset can be read from but not written to
\n", - "append - the dataset can be read from and written to
\n", - "write - a new dataset is created (and will overwrite an existing dataset with the same name)
" + "write - a new dataset is created (and will overwrite an existing dataset with the same name) " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "88bd5317-bfa0-4a31-bf9c-3475f17c4afb", "metadata": {}, "outputs": [], @@ -128,10 +105,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "3a2eaf21-ddc7-4b1a-8c63-4471ee8aee6d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dict_keys(['assessments', 'users'])\n" + ] + } + ], "source": [ "with Session() as s:\n", " ds1 = s.open_dataset('user_assessments.hdf5', 'r', 'ds1')\n", @@ -147,18 +132,32 @@ "id": "5d23b356-93e8-4db8-a0fa-c1573cc3695c", "metadata": {}, "source": [ - "

Dataset

\n", - "ExeTera works with HDF5 datasets under the hood, and the Dataset class is the means why which you interact with it at the top level. Each Dataset instance corresponds to a physical dataset that has been created or opened through a call to session.open_dataset.
\n", + "### Dataset \n", + "ExeTera works with HDF5 datasets under the hood, and the Dataset class is the means why which you interact with it at the top level. Each Dataset instance corresponds to a physical dataset that has been created or opened through a call to session.open_dataset. \n", "\n", "Datasets are in turn used to create, access and delete DataFrames. Each DataFrame is a top-level HDF5 group that is intended to be very much like and familiar to the Pandas DataFrame." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "8d7f668f-9630-49cb-83f0-6ac32da9feb6", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dict_keys(['foo'])\n", + "Renamed: dict_keys(['bar'])\n", + "Moved: dict_keys(['foo'])\n", + "Copied: dict_keys(['foo', 'bar'])\n", + "Dataframe foo deleted. dict_keys(['bar'])\n", + "Copied: dict_keys(['assessments', 'users'])\n", + "Copied: dict_keys(['foobar'])\n" + ] + } + ], "source": [ "from exetera.core import dataset\n", "\n", @@ -195,22 +194,37 @@ "id": "8d12f984-35d1-4a90-a1ca-655afe7c80f3", "metadata": {}, "source": [ - "

3, DataFrame and Fields

\n", - "The ExeTera DataFrame object is intended to be familiar to users of Pandas, albeit not identical.
\n", + "## 3, DataFrame and Fields \n", + "\n", + "The ExeTera DataFrame object is intended to be familiar to users of Pandas, albeit not identical. \n", "\n", - "ExeTera works with Datasets, which are backed up by physical key-value HDF5 datastores on drives, and, as such, there are necessarily some differences between the Pandas DataFrame:
\n", + "ExeTera works with Datasets, which are backed up by physical key-value HDF5 datastores on drives, and, as such, there are necessarily some differences between the Pandas DataFrame: \n", "\n", - "- Pandas DataFrames enforce that all Series (Fields in ExeTera terms) are the same length. ExeTera doesn't require this, but there are then operations that do not make sense unless all fields are of the same length. ExeTera allows DataFrames to have fields of different lengths because the operation to apply filters and so for to a DataFrame would run out of memory on large DataFrames
\n", - "- Types always matter in ExeTera. When creating new Fields (Pandas Series) you need to specify the type of the field that you would like to create. Fortunately, Fields have convenience methods to construct empty copies of themselves for when you need to create a field of a compatible type
\n", - "- ExeTera DataFrames are new with the 0.5 release of ExeTera and do not yet support all of the operations that Panda DataFrames support. This functionality will be augmented in future releases.
" + "- Pandas DataFrames enforce that all Series (Fields in ExeTera terms) are the same length. ExeTera doesn't require this, but there are then operations that do not make sense unless all fields are of the same length. ExeTera allows DataFrames to have fields of different lengths because the operation to apply filters and so for to a DataFrame would run out of memory on large DataFrames \n", + "- Types always matter in ExeTera. When creating new Fields (Pandas Series) you need to specify the type of the field that you would like to create. Fortunately, Fields have convenience methods to construct empty copies of themselves for when you need to create a field of a compatible type \n", + "- ExeTera DataFrames are new with the 0.5 release of ExeTera and do not yet support all of the operations that Panda DataFrames support. This functionality will be augmented in future releases. " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "5221d1da-8f36-4259-a800-3fadfc50600c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "odict_keys(['foo', 'foobar'])\n", + "Original: [0 1 2 3 4 5 6 7 8 9]\n", + "Filtered: [0 2 4 6 8]\n", + "Original: [0 2 4 6 8]\n", + "Previous re-index: [0 1 2 3 4 5 6 7 8 9]\n", + "Re-indexed: [9 8 7 6 5 4 3 2 1 0]\n", + "Re-indexed: [9 8 7 6 5 4 3 2 1 0]\n" + ] + } + ], "source": [ "import numpy as np\n", "with Session() as s:\n", @@ -283,10 +297,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "a4c81326-19c0-4e2f-8ac8-6e44efc0a9e8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0 1 2 3 4 5 6 7 8 9]\n" + ] + } + ], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", @@ -309,10 +331,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "eaa861df-84ee-48a9-8e42-1e6332d8b824", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0 1 2 3 4 5 6 7 8 9]\n", + "[]\n" + ] + } + ], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", @@ -336,10 +367,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "28619c4e-8dd0-49e1-9f69-d2e82c848c9d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ 0 2 4 6 8 10 12 14 16 18]\n" + ] + } + ], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", @@ -364,10 +403,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "ef5cfb09-4f5a-434d-9eab-4ddd7c2d9f0d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ True False True False True False True False True False]\n" + ] + } + ], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", @@ -381,10 +428,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "4731e2b7-7bf7-48e0-99ee-024bff1c6047", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ True True True True True True True True True True]\n" + ] + } + ], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", @@ -409,10 +464,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "ae438918-d810-46c0-a1ad-376c8189444f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[False False False False False False False False False False]\n" + ] + } + ], "source": [ "with Session() as s:\n", " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", diff --git a/examples/import_dataset.ipynb b/examples/import_dataset.ipynb index ffe58f8a..9c41e0a9 100644 --- a/examples/import_dataset.ipynb +++ b/examples/import_dataset.ipynb @@ -5,60 +5,293 @@ "id": "164a6070-9bc2-4f46-a016-d25ac2141f22", "metadata": {}, "source": [ + "# Import Dataset Example\n", + "\n", "In this example, we will convert the csv files into HDF5 through the import utility provided by ExeTera.\n", "First, you can see we have two csv files and one json file:" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "95c1811f-027a-4fab-9936-084ef6ca0a62", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "id,FirstName,LastName,bmi,has_diabetes,height_cm,year_of_birth\n", + "0,\"Grace\",\"None\",39,1,130,1967\n", + "1,\"Carol\",\"Nobody\",38,0,119,1975\n", + "2,\"Wendy\",\"Random\",28,0,128,1926\n", + "3,\"Mallory\",\"Nobody\",25,0,117,1944\n", + "4,\"Xavier\",\"Unknown\",29,1,190,1974\n", + "5,\"Olivia\",\"Thunk\",26,0,107,2004\n", + "6,\"Xavier\",\"Anon\",30,0,175,1973\n", + "7,\"Xavier\",\"Null\",37,0,140,1963\n", + "8,\"Ivan\",\"Bloggs\",37,0,134,1999\n", + "9,\"Trudy\",\"Bar\",28,0,116,1929\n" + ] + } + ], "source": [ "!cat users.csv" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "6ee656fa-fce7-44b8-9ad9-c4761d78d9d3", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "id,date,user_id,abdominal_pain,brain_fog,loss_of_smell,tested_covid_positive,temperature_f\n", + "0,2021-10-24 11:45:43.677374+00:00,0,0,1,1,2,103.22149054047082\n", + "1,2021-12-16 13:09:58.380573+00:00,1,0,1,0,0,100.62518751030662\n", + "2,2021-08-05 17:51:30.943546+00:00,2,0,0,1,0,105.18487884609749\n", + "3,2021-04-09 14:47:54.599226+00:00,3,1,0,1,0,96.4302053852154\n", + "4,2021-09-29 00:15:42.142405+00:00,4,1,1,1,0,109.63616106818489\n", + "5,2021-04-24 09:53:44.215726+00:00,5,1,0,0,1,107.69840121429907\n", + "6,2021-11-13 07:35:32.840341+00:00,6,0,0,0,1,97.00309019318361\n", + "7,2022-02-14 00:08:04.885913+00:00,7,1,0,0,1,95.22598358524823\n", + "8,2022-02-07 15:36:57.841132+00:00,8,0,0,0,2,95.48740949212532\n", + "9,2021-02-21 01:48:38.675272+00:00,9,0,1,1,0,106.27664175133276\n", + "10,2021-08-05 00:06:12.343504+00:00,0,0,1,1,0,103.07544677653925\n", + "11,2021-11-07 21:52:41.868990+00:00,1,1,0,0,2,102.81942527899108\n", + "12,2021-05-20 14:49:01.700189+00:00,2,0,0,0,2,103.25591242165508\n", + "13,2021-09-28 03:13:05.410689+00:00,3,0,1,1,1,98.99925665317788\n", + "14,2022-01-21 13:39:41.914258+00:00,4,1,1,1,0,104.73914713718412\n", + "15,2021-04-06 10:40:50.447460+00:00,5,0,0,1,1,95.47080459402937\n", + "16,2021-12-20 01:53:04.166355+00:00,6,0,1,0,0,97.79758064358536\n", + "17,2021-10-11 18:45:24.349922+00:00,7,0,1,0,0,95.87860080008119\n", + "18,2021-04-04 13:14:36.124810+00:00,8,0,1,1,1,108.78273531994027\n", + "19,2022-02-14 06:51:09.464885+00:00,9,0,0,0,2,100.28032607623044\n", + "20,2021-05-17 14:09:07.047752+00:00,0,0,0,1,1,100.79853200088986\n", + "21,2021-06-11 01:13:23.001260+00:00,1,0,1,1,1,104.63677316034355\n", + "22,2021-04-06 04:16:39.361154+00:00,2,1,1,0,1,107.68363442020022\n", + "23,2021-06-27 01:21:21.633768+00:00,3,1,0,1,1,97.08478382878046\n", + "24,2021-11-02 17:52:04.146347+00:00,4,1,1,1,2,103.44636506288838\n", + "25,2021-06-12 11:58:35.491048+00:00,5,1,0,0,1,95.8026824345136\n", + "26,2021-03-09 13:52:33.458577+00:00,6,0,1,1,0,100.76416498492172\n", + "27,2021-04-27 12:37:30.891582+00:00,7,1,1,0,0,103.99752973377511\n", + "28,2022-01-10 02:08:42.584960+00:00,8,0,0,0,2,102.30838309482037\n", + "29,2021-03-18 03:06:36.735481+00:00,9,1,1,1,0,96.4187993279403\n" + ] + } + ], "source": [ "!cat assessments.csv" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "2e1d9bc3-76cd-48f1-9ed3-94636da04f76", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "{\n", + " \"exetera\": {\n", + " \"version\": \"1.0.0\"\n", + " },\n", + " \"schema\": {\n", + " \"users\": {\n", + " \"primary_keys\": [\n", + " \"id\"\n", + " ],\n", + " \"fields\": {\n", + " \"id\": {\n", + " \"field_type\": \"fixed_string\",\n", + " \"length\": 32\n", + " },\n", + " \"FirstName\": {\n", + " \"field_type\": \"string\"\n", + " },\n", + " \"LastName\": {\n", + " \"field_type\": \"string\"\n", + " },\n", + " \"bmi\": {\n", + " \"field_type\": \"numeric\",\n", + " \"value_type\": \"int32\"\n", + " },\n", + " \"has_diabetes\": {\n", + " \"field_type\": \"categorical\",\n", + " \"categorical\": {\n", + " \"value_type\": \"int8\",\n", + " \"strings_to_values\": {\n", + " \"\": 0,\n", + " \"False\": 1,\n", + " \"True\": 2\n", + " }\n", + " }\n", + " },\n", + " \"height_cm\": {\n", + " \"field_type\": \"numeric\",\n", + " \"value_type\": \"int32\"\n", + " }, \n", + " \"year_of_birth\": {\n", + " \"field_type\": \"numeric\",\n", + " \"value_type\": \"int32\"\n", + " }\n", + " }\n", + " },\n", + " \"assessments\": {\n", + " \"primary_keys\": [\n", + " \"id\"\n", + " ],\n", + " \"foreign_keys\": {\n", + " \"user_id_key\": {\n", + " \"space\": \"users\",\n", + " \"key\": \"id\"\n", + " }\n", + " },\n", + " \"fields\": {\n", + " \"id\": {\n", + " \"field_type\": \"fixed_string\",\n", + " \"length\": 32\n", + " },\n", + " \"date\": {\n", + " \"field_type\": \"datetime\"\n", + " },\n", + " \"user_id\": {\n", + " \"field_type\": \"fixed_string\",\n", + " \"length\": 32\n", + " },\n", + " \"abdominal_pain\": {\n", + " \"field_type\": \"categorical\",\n", + " \"categorical\": {\n", + " \"value_type\": \"int8\",\n", + " \"strings_to_values\": {\n", + " \"\": 0,\n", + " \"False\": 1,\n", + " \"True\": 2\n", + " }\n", + " }\n", + " },\n", + " \"brain_fog\": {\n", + " \"field_type\": \"categorical\",\n", + " \"categorical\": {\n", + " \"value_type\": \"int8\",\n", + " \"strings_to_values\": {\n", + " \"\": 0,\n", + " \"False\": 1,\n", + " \"True\": 2\n", + " }\n", + " }\n", + " },\n", + " \"loss_of_smell\": {\n", + " \"field_type\": \"categorical\",\n", + " \"categorical\": {\n", + " \"value_type\": \"int8\",\n", + " \"strings_to_values\": {\n", + " \"\": 0,\n", + " \"False\": 1,\n", + " \"True\": 2\n", + " }\n", + " }\n", + " },\n", + " \"tested_covid_positive\": {\n", + " \"field_type\": \"categorical\",\n", + " \"categorical\": {\n", + " \"value_type\": \"int8\",\n", + " \"strings_to_values\": {\n", + " \"\": 0,\n", + " \"waiting\": 1,\n", + " \"no\": 2,\n", + " \"yes\": 3\n", + " }\n", + " }\n", + " },\n", + " \"temperature_f\": {\n", + " \"field_type\": \"numeric\",\n", + " \"value_type\": \"float32\"\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n" + ] + } + ], "source": [ "!cat user_assessments.json" ] }, { "cell_type": "markdown", - "id": "f96b4e31-ca85-4321-8276-ca9121672500", + "id": "29858d70-b498-4053-b704-52eb0c7034d9", "metadata": {}, "source": [ - "Then we have two methods to call the import, 1) from python function, and 2) from script:\n", - "
\n", - "(make sure you have exetera installed already, or you can do pip install exetera)" + "\n", + "\n", + "## 1, Import \n", + "ExeTera utlize HDF5 file format to acquire fast performance when processing the data. Hence the first step of using ExeTera is usually transform the file from other formats, e.g. csv, into HDF5.\n", + "\n", + "ExeTera provides utilities to transform the csv data into HDF5, through either command line or code.\n", + "\n", + "a. Importing via the exetera import command: \n", + "\n", + "```\n", + "\n", + "exetera import \n", + "-s path/to/covid_schema.json \\ \n", + "-i \"patients:path/to/patient_data.csv, assessments:path/to/assessmentdata.csv,
tests:path/to/covid_test_data.csv, diet:path/to/diet_study_data.csv\" \\ \n", + "-o /path/to/output_dataset_name.hdf5 \n", + "--include \"patients:(id,country_code,blood_group), assessments:(id,patient_id,chest_pain)\" \n", + "--exclude \"tests:(country_code)\" \n", + "\n", + "\n", + "Arguments: \n", + "-s/--schema: The location and name of the schema file \n", + "-te/--territories: If set, this only imports the listed territories. If left unset, all territories are imported \n", + "-i/--inputs : A comma separated list of 'name:file' pairs. This should be put in parentheses if it contains any whitespace. See the example above. \n", + "-o/--output_hdf5: The path and name to where the resulting hdf5 dataset should be written \n", + "-ts/--timestamp: An override for the timestamp to be written (defaults to datetime.now(timezone.utc)) \n", + "-w/--overwrite: If set, overwrite any existing dataset with the same name; appends to existing dataset otherwise \n", + "-n/--include: If set, filters out all fields apart from those in the list. \n", + "-x/--exclude: If set, filters out the fields in this list. \n", + "\n", + "```\n", + "\n", + "\n", + "\n", + "b. Importing through code \n", + "\n", + "Use importer.import_with_schema(timestamp, output_hdf5_name, schema, tokens, args.overwrite, include_fields, exclude_fields) \n", + "\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "68f76d88-8f88-468e-b3b7-8e40bc5dbe17", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "read_file_using_fast_csv_reader: 1 chunks, 10 accumulated_written_rows parsed in 0.0040361881256103516s\n", + "completed in 0.007875919342041016 seconds\n", + "Total time 0.008165121078491211s\n", + "read_file_using_fast_csv_reader: 1 chunks, 30 accumulated_written_rows parsed in 0.00348663330078125s\n", + "completed in 0.005882978439331055 seconds\n", + "Total time 0.006022214889526367s\n" + ] + } + ], "source": [ "#1)Import csv to hdf5 through import_with_schema function\n", "\n", @@ -105,10 +338,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "2cdf941c-be2d-4cd4-ac85-25606b2a5a19", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dataset.hdf5 temp2.hdf5 temp.hdf5 user_assessments.hdf5\n" + ] + } + ], "source": [ "!ls *hdf5" ] From 62d0d69f5ca92caf62a1f4a65d13243d22e43ab9 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 7 Mar 2022 11:01:17 +0000 Subject: [PATCH 164/181] update examples --- examples/README.md | 10 ++-- examples/basic_concept.ipynb | 95 +++++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 37 deletions(-) diff --git a/examples/README.md b/examples/README.md index f0bf2414..b632eebd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,12 +6,16 @@ This folder contains a few examples on how to use ExeTera in different scenarios This example shows how to generate ExeTera HDF5 datafile through 'importer.import_with_schema' function, and a few basic commands to print the dataset content. #### import_dataset -This example shows how to import multiple CSV files into a ExeTera HDF5 datafile. The example datafile has a similar structure to Covid Symptom Study (CSS) dataset, including a user table and a assessments table. +This example shows how to import multiple CSV files into a ExeTera HDF5 datafile. The example datafile has a similar structure to Covid Symptom Study (CSS) dataset, including a user table and a assessments table. Content in this example is section 1, Import. #### basic_concept -This example shows how to use ExeTera, through the major components: dataset, dataframe and fields. Please note this example is based on assessments.hdf5 file, hence please go through the simple_linked_dataset example and generate the hdf5 file first. +This example shows how to use ExeTera, through the major components: dataset, dataframe and fields. Content in this example is: section 2, Session and Dataset and section 3, DataFrame and Fields. + +Please note this example is based on assessments.hdf5 file, hence please go through the simple_linked_dataset example and generate the hdf5 file first. #### advanced_operations -This example shows the intermediate functions of ExeTera, such as filtering, group by, sorting, performance boosting using numba, and output the dataframe to csv file. Please note this example is based on assessments.hdf5 file, hence please go through the simple_linked_dataset example and generate the hdf5 file first. +This example shows the intermediate functions of ExeTera, such as filtering, group by, sorting, performance boosting using numba, and output the dataframe to csv file. Content in this example is section 4 filtering, section 5 performance boosting, section 6 groupby, section 7 join, section 8 sort and section 9 input/output. + +Please note this example is based on assessments.hdf5 file, hence please go through the simple_linked_dataset example and generate the hdf5 file first. diff --git a/examples/basic_concept.ipynb b/examples/basic_concept.ipynb index 7f41eebc..2415b526 100644 --- a/examples/basic_concept.ipynb +++ b/examples/basic_concept.ipynb @@ -50,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 2, "id": "74ad07d9-df92-4668-bb2f-8964ea91018c", "metadata": {}, "outputs": [], @@ -207,7 +207,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 4, "id": "5221d1da-8f36-4259-a800-3fadfc50600c", "metadata": {}, "outputs": [ @@ -215,13 +215,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "odict_keys(['foo', 'foobar'])\n", - "Original: [0 1 2 3 4 5 6 7 8 9]\n", - "Filtered: [0 2 4 6 8]\n", - "Original: [0 2 4 6 8]\n", - "Previous re-index: [0 1 2 3 4 5 6 7 8 9]\n", - "Re-indexed: [9 8 7 6 5 4 3 2 1 0]\n", - "Re-indexed: [9 8 7 6 5 4 3 2 1 0]\n" + "odict_keys(['foo', 'foobar'])\n" ] } ], @@ -243,31 +237,7 @@ " df2 = ds.create_dataframe('df2')\n", " df2['foo'] = df['i_foo']\n", " df2['foobar'] = df2['foo']\n", - " print(df2.keys())\n", - "\n", - "\n", - " #Apply a filter to all fields in a dataframe\n", - " df = ds.create_dataframe('df3')\n", - " df.create_numeric('n_foo', 'int32').data.write([0,1,2,3,4,5,6,7,8,9])\n", - " filt = np.array([True if i%2==0 else False for i in range(0,10)]) # filter out odd values\n", - " df4 = ds.create_dataframe('df4')\n", - " df.apply_filter(filt, ddf=df4) # creates a new dataframe from the filtered dataframe\n", - " print('Original:', df['n_foo'].data[:])\n", - " print('Filtered: ',df4['n_foo'].data[:])\n", - " df.apply_filter(filt) # destructively filters the dataframe\n", - " print('Original:', df['n_foo'].data[:])\n", - "\n", - "\n", - " #Re-index all fields in a dataframe\n", - " df = ds.create_dataframe('df5')\n", - " df.create_numeric('n_foo', 'int32').data.write([0,1,2,3,4,5,6,7,8,9])\n", - " print('Previous re-index:', df['n_foo'].data[:])\n", - " inds = np.array([9,8,7,6,5,4,3,2,1,0])\n", - " df6 = ds.create_dataframe('df6')\n", - " df.apply_index(inds, ddf=df6) # creates a new dataframe from the re-indexed dataframe\n", - " print('Re-indexed:', df6['n_foo'].data[:])\n", - " df.apply_index(inds) # destructively re-indexes the dataframe\n", - " print('Re-indexed:', df['n_foo'].data[:])" + " print(df2.keys())" ] }, { @@ -354,6 +324,63 @@ " print(df['field2'].data[:]) # note the data is not copied" ] }, + { + "cell_type": "markdown", + "id": "ca5aae0a-7fec-4dc0-aac0-d115eaea315e", + "metadata": {}, + "source": [ + "You can also change the underlying data in a field via filtering or re-indexing.\n", + "To filter the data, you provide an array of boolean that is the same length of the field, then all the data according the True value will be kept.\n", + "To re-index, you provide an array of integers and the order of the data will be re-organized based on the integer array." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "29761620-2888-4e23-abee-b2faabce1da4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Original: [0 1 2 3 4 5 6 7 8 9]\n", + "Filtered: [0 2 4 6 8]\n", + "Original: [0 2 4 6 8]\n", + "Previous re-index: [0 1 2 3 4 5 6 7 8 9]\n", + "Re-indexed: [9 8 7 6 5 4 3 2 1 0]\n", + "Re-indexed: [9 8 7 6 5 4 3 2 1 0]\n" + ] + } + ], + "source": [ + "with Session() as s:\n", + " ds = s.open_dataset('temp.hdf5', 'w', 'ds')\n", + "\n", + " #Apply a filter to all fields in a dataframe, see section 5 for more detail\n", + " df = ds.create_dataframe('df3')\n", + " df.create_numeric('n_foo', 'int32').data.write([0,1,2,3,4,5,6,7,8,9])\n", + " filt = np.array([True if i%2==0 else False for i in range(0,10)]) # filter out odd values\n", + " df4 = ds.create_dataframe('df4')\n", + " df.apply_filter(filt, ddf=df4) # creates a new dataframe from the filtered dataframe\n", + " print('Original:', df['n_foo'].data[:])\n", + " print('Filtered: ',df4['n_foo'].data[:])\n", + " df.apply_filter(filt) # destructively filters the dataframe\n", + " print('Original:', df['n_foo'].data[:])\n", + "\n", + "\n", + " #Re-index all fields in a dataframe\n", + " df = ds.create_dataframe('df5')\n", + " df.create_numeric('n_foo', 'int32').data.write([0,1,2,3,4,5,6,7,8,9])\n", + " print('Previous re-index:', df['n_foo'].data[:])\n", + " inds = np.array([9,8,7,6,5,4,3,2,1,0])\n", + " df6 = ds.create_dataframe('df6')\n", + " df.apply_index(inds, ddf=df6) # creates a new dataframe from the re-indexed dataframe\n", + " print('Re-indexed:', df6['n_foo'].data[:])\n", + " df.apply_index(inds) # destructively re-indexes the dataframe\n", + " print('Re-indexed:', df['n_foo'].data[:])" + ] + }, { "cell_type": "markdown", "id": "b96d7ddb-26d9-4933-93e5-f97e8bad2592", From 40e8770c2c80934218ce176210e3335a380e03ad Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 8 Mar 2022 17:01:25 +0000 Subject: [PATCH 165/181] update the example notebooks --- examples/advanced_operations.ipynb | 45 +++++++++++++++++---------- examples/basic_concept.ipynb | 4 +-- examples/import_dataset.ipynb | 50 +++++++++++++++++++++--------- 3 files changed, 65 insertions(+), 34 deletions(-) diff --git a/examples/advanced_operations.ipynb b/examples/advanced_operations.ipynb index f7f47ba7..4013ba83 100644 --- a/examples/advanced_operations.ipynb +++ b/examples/advanced_operations.ipynb @@ -30,7 +30,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "id": "4eae8d06-0872-4e30-b6c8-2e8ba86fe9f4", "metadata": {}, "outputs": [ @@ -73,7 +73,7 @@ " 'freq': ['NaN', 10, 'NaN', 'NaN']}" ] }, - "execution_count": 3, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -223,7 +223,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "id": "c89f52b9-f96d-4e36-a8e3-67953aaedae4", "metadata": {}, "outputs": [ @@ -231,7 +231,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "3.257335901260376\n" + "9.901110410690308\n" ] } ], @@ -240,20 +240,23 @@ "import time\n", "\n", "#sum up the symptoms without njit\n", - "test_length = 1000000000 # here we use the a test length rather than 50 rows in the dataset, \n", + "test_length = 3000000000 # here we use the a test length rather than 50 rows in the dataset, \n", " # as the difference comes with more rows\n", "symptoms = ['abdominal_pain', 'brain_fog', 'loss_of_smell']\n", + "symp_data = {}\n", + "for i in symptoms:\n", + " symp_data[i] = np.zeros(test_length, 'int32')\n", "t0 = time.time()\n", "sum_symp = np.zeros(test_length, 'int32')\n", "for i in symptoms:\n", - " sum_symp += np.zeros(test_length, 'int32')\n", + " sum_symp += symp_data[i]\n", "#print(sum_symp)\n", "print(time.time()-t0)" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "id": "202321d4-b1fc-47bd-8878-779df121c913", "metadata": {}, "outputs": [ @@ -261,7 +264,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.08656787872314453\n" + "6.684003591537476\n" ] } ], @@ -277,9 +280,9 @@ "t0 = time.time()\n", "sum_symp = np.zeros(test_length, 'int32')\n", "for i in symptoms:\n", - " sum_symp = np.zeros(test_length, 'int32')\n", + " sum_symp = sum_symptom(symp_data[i], sum_symp)\n", "#print(sum_symp)\n", - "print(time.time()-t0) # 10x faster" + "print(time.time()-t0) # usually 10x faster dependents on data size" ] }, { @@ -337,6 +340,14 @@ " asmts.groupby(by = 'user_id').last(target ='date', ddf = df6)" ] }, + { + "cell_type": "markdown", + "id": "afdd38f7-e8a7-4ad6-80ad-d0ff45401524", + "metadata": {}, + "source": [ + "Apart from the groupby, pandas also provide the transform functions. In Transform, the data length is not alterd. Here in ExeTera, we do not have a dedicate API for transform functions, but the same operation can be done via the span:" + ] + }, { "cell_type": "code", "execution_count": 10, @@ -382,19 +393,19 @@ "ExeTera provides functions that provide pandas-like merge functionality on DataFrame instances. We have made this operation as familiar as possible to Pandas users, but there are a couple of differences that we should highlight:\n", "\n", "\n", - "- merge is provided as a function in the dataframe unit, rather than as a member function on DataFrame instances \n", + "1) merge is provided as a function in the dataframe unit, rather than as a member function on DataFrame instances \n", "\n", "\n", - "- merge takes three dataframe arguments, left, right and dest. This is due to the fact that DataFrames are always backed up by a datastore and so rather than create an in-memory destination dataframe, the resulting merged fields must be written to a dataframe of your choosing. \n", + "2) merge takes three dataframe arguments, left, right and dest. This is due to the fact that DataFrames are always backed up by a datastore and so rather than create an in-memory destination dataframe, the resulting merged fields must be written to a dataframe of your choosing. \n", "\n", "\n", - "- Note, this can either be a separate dataframe or it can be the dataframe that you are merging to (typically left in the case of a \"left\" merge and right in the case of a \"right\" merge\n", + "3) Note, this can either be a separate dataframe or it can be the dataframe that you are merging to (typically left in the case of a \"left\" merge and right in the case of a \"right\" merge\n", "\n", "\n", - "- merge takes a number of optional hint fields that can save time when working with large datasets. These specify whether the keys are unique or ordered and allow the merge to occur without first checking this\n", + "4) merge takes a number of optional hint fields that can save time when working with large datasets. These specify whether the keys are unique or ordered and allow the merge to occur without first checking this\n", "\n", "\n", - "- merge has a number of highly scalable algorithms that can be used when the key data is sorted and / or unique." + "5) merge has a number of highly scalable algorithms that can be used when the key data is sorted and / or unique." ] }, { @@ -629,7 +640,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -643,7 +654,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.7.11" } }, "nbformat": 4, diff --git a/examples/basic_concept.ipynb b/examples/basic_concept.ipynb index 2415b526..f41fa868 100644 --- a/examples/basic_concept.ipynb +++ b/examples/basic_concept.ipynb @@ -517,7 +517,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -531,7 +531,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.7.11" } }, "nbformat": 4, diff --git a/examples/import_dataset.ipynb b/examples/import_dataset.ipynb index 9c41e0a9..953fa01e 100644 --- a/examples/import_dataset.ipynb +++ b/examples/import_dataset.ipynb @@ -232,7 +232,9 @@ { "cell_type": "markdown", "id": "29858d70-b498-4053-b704-52eb0c7034d9", - "metadata": {}, + "metadata": { + "tags": [] + }, "source": [ "\n", "\n", @@ -241,18 +243,32 @@ "\n", "ExeTera provides utilities to transform the csv data into HDF5, through either command line or code.\n", "\n", - "a. Importing via the exetera import command: \n", - "\n", - "```\n", + "### a. Importing via the exetera import command: \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98df8bfc-166e-4a7b-91b7-da69112e07e2", + "metadata": {}, + "outputs": [], + "source": [ + "#This cell is to show the ExeTera import utility, not meant to run\n", "\n", "exetera import \n", "-s path/to/covid_schema.json \\ \n", "-i \"patients:path/to/patient_data.csv, assessments:path/to/assessmentdata.csv,
tests:path/to/covid_test_data.csv, diet:path/to/diet_study_data.csv\" \\ \n", "-o /path/to/output_dataset_name.hdf5 \n", "--include \"patients:(id,country_code,blood_group), assessments:(id,patient_id,chest_pain)\" \n", - "--exclude \"tests:(country_code)\" \n", - "\n", - "\n", + "--exclude \"tests:(country_code)\" \n" + ] + }, + { + "cell_type": "markdown", + "id": "51554888-055a-4266-9321-05057fb311bc", + "metadata": {}, + "source": [ + "```\n", "Arguments: \n", "-s/--schema: The location and name of the schema file \n", "-te/--territories: If set, this only imports the listed territories. If left unset, all territories are imported \n", @@ -263,14 +279,18 @@ "-n/--include: If set, filters out all fields apart from those in the list. \n", "-x/--exclude: If set, filters out the fields in this list. \n", "\n", - "```\n", - "\n", - "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "44465352-237b-46a7-977d-389ac2bb67c8", + "metadata": {}, + "source": [ "\n", - "b. Importing through code \n", + "### b. Importing through code \n", "\n", - "Use importer.import_with_schema(timestamp, output_hdf5_name, schema, tokens, args.overwrite, include_fields, exclude_fields) \n", - "\n" + "Use importer.import_with_schema(timestamp, output_hdf5_name, schema, tokens, args.overwrite, include_fields, exclude_fields) " ] }, { @@ -357,7 +377,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -371,7 +391,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.7.11" } }, "nbformat": 4, From 099c8f6e90dc435a0c5c4a876deb8c86de44de1c Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 27 Apr 2022 16:01:39 +0100 Subject: [PATCH 166/181] df view init commit --- exetera/core/dataframe.py | 129 +++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index a99af88a..56e56609 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -1656,4 +1656,131 @@ def _ordered_merge(left: DataFrame, if right[k].indexed: ops.ordered_map_valid_indexed_stream(right[k], right_map, dest_f, invalid) else: - ops.ordered_map_valid_stream(right[k], right_map, dest_f, invalid) \ No newline at end of file + ops.ordered_map_valid_stream(right[k], right_map, dest_f, invalid) + +class View(DataFrame): + """ + Similar to the HDF5 dataframe, the view contains a list of fields. But the fields in view is reference (from other HDF5DataFrame fields) + and filter index by default. Unless necessary, the view won't interact with HDF5 storage. + """ + + def __init__(self, mapping, readwrite=True): + """ + Initiate a view. + + :param mapping: The dictionary of field and coordinate filter. + :param readwrite: Read-write privilege of the fields. If False, view will create a separate storage copy when modifing the field. + + """ + self._dataset = None + self._columns = OrderedDict() # to support multiple filters of one field + self._filters = OrderedDict() + for field in mapping.keys(): + self._columns[field.name] = field + self._filters[field.name] = mapping[field] + self._readwrite = readwrite + + @property + def columns(self): + return OrderedDict(self._columns) + + @property + def dataset(self): + if self._dataset is None: + raise ValueError('This view contains field reference only. Please call to_hdf5_dataframe to write the content to dataset first.') + else: + return self._dataset + + def add(self, field, filter, name=None): + pass + + def drop(self, name: str): + pass + + def __contains__(self, name): + pass + + def contains_field(self, field): + pass + + def __getitem__(self, name): + """ + + Example: + + >>> view['num'] + [1,2,3,4,5] + """ + pass + + def get_field(self, name): + """ + Get the field data. + + Example: + + >>> view.get_field('num') + + """ + pass + + def __setitem__(self, name, value): + """ + + Example: + + view['num'] = 'num2' # rename + view['num'] = df.create_numeric('num','int32') # change the reference field + view['num'] = [True, True, False, False] # set a filter + view['num'] = np.array([1,2,3,4,5,6]) # set the data in field + """ + pass + + def __delitem__(self, name): + """ + + Example: + + del view['num'] + """ + pass + + def delete_field(self, field): + """ + + Example: + >>> num = df.create_numeric('num', 'int32') + >>> view.add(num, [True, True, False, False]) + >>> view.delete_field(num) + """ + pass + + def keys(self): + pass + + def values(self): + pass + + def items(self): + pass + + def __iter__(self): + pass + + def __next__(self): + pass + + def __len__(self): + pass + + def apply_filter(self, filter_to_apply, field): + pass + + def apply_index(self, index_to_apply, field): + pass + + def to_hdf5_dataframe(self, dataset, name): + """ + Convert this view to a HDF5 dataframe which write all fields in this view onto the dataset. + """ + self.dataset = dataset \ No newline at end of file From 3d6966e77f6aff16a49e4f678cdfe7d1715bc262 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 5 May 2022 10:28:47 +0100 Subject: [PATCH 167/181] dataframe view init commit: added the filter df present field by masking the data --- exetera/core/dataframe.py | 203 ++++++++++++++------------------------ exetera/core/dataset.py | 7 ++ tests/test_dataframe.py | 23 ++++- 3 files changed, 103 insertions(+), 130 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 56e56609..6eec36f0 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -58,7 +58,7 @@ def __init__(self, self.name = name self._columns = OrderedDict() self._dataset = dataset - self._h5group = h5group + self._h5group = h5group # the HDF5 group to store all fields for subg in h5group.keys(): self._columns[subg] = dataset.session.get(h5group[subg]) @@ -264,6 +264,80 @@ def contains_field(self, field): return True return False + def get_filter(self, field: Union[str, fld.Field]): + """ + Get a filter array specified by the field or field name. + """ + pass + + def set_filter(self, field: Union[str, fld.Field], filter): + """ + Add or modify a filter of the field. + + :param field: The target field. + :param filter: The filter, as list or np.ndarray of indices. + """ + if not isinstance(field, str) and not isinstance(field, fld.Field): + raise TypeError("The target field should be type field or string (name of the field in this dataframe).") + + name = field if isinstance(field, str) else field.name + if name not in self._columns: + raise ValueError("The target field is not in this dataframe.") + + dtype = 'int32' if filter[-1] < 2**31 - 1 else 'int64' + if name in self.filters.keys(): + self.filters[name].data.clear() + self.filters[name].data.write(filter) + else: + self.filters.create_numeric(name, dtype).data.write(filter) + + def delete_filter(self, field: Union[str, fld.Field]): + """ + Delete a filter from this dataframe specified by the field or field name. + """ + if not isinstance(field, str) and not isinstance(field, fld.Field): + raise TypeError("The target field should be type field or string (name of the field in this dataframe).") + + name = field if isinstance(field, str) else field.name + if name not in self._columns: + raise ValueError("The target field is not in this dataframe.") + else: + del self.filters[name] + + def get_data(self, field: Union[str, fld.Field]): + """ + Get the data from a field. The data returned is masked by the filter. + + """ + if not isinstance(field, str) and not isinstance(field, fld.Field): + raise TypeError("The target field should be type field or string (name of the field in this dataframe).") + + name = field if isinstance(field, str) else field.name + if name not in self.columns.keys(): + raise ValueError("Can not found the field name from this dataframe.") + else: + if name in self.filters.keys(): + d_filter = self.filters[name].data[:] + return self.columns[name].data[d_filter] + else: + return self.columns[name].data[:] + + def __getattr__(self, item): + """ + Rewrite the getattr method so that dataframe will return the data of a field directly. + + """ + fields = object.__getattribute__(self, 'columns') + if item not in fields.keys(): + raise ValueError("Can not found the field name from this dataframe.") + else: + filters = object.__getattribute__(self, 'filters') + if item in filters.keys(): # has a filter + data_filter = filters[item].data[:] + return fields[item].data[data_filter] + else: + return fields[item].data[:] + def __getitem__(self, name): """ Get a field stored by the field name. @@ -1657,130 +1731,3 @@ def _ordered_merge(left: DataFrame, ops.ordered_map_valid_indexed_stream(right[k], right_map, dest_f, invalid) else: ops.ordered_map_valid_stream(right[k], right_map, dest_f, invalid) - -class View(DataFrame): - """ - Similar to the HDF5 dataframe, the view contains a list of fields. But the fields in view is reference (from other HDF5DataFrame fields) - and filter index by default. Unless necessary, the view won't interact with HDF5 storage. - """ - - def __init__(self, mapping, readwrite=True): - """ - Initiate a view. - - :param mapping: The dictionary of field and coordinate filter. - :param readwrite: Read-write privilege of the fields. If False, view will create a separate storage copy when modifing the field. - - """ - self._dataset = None - self._columns = OrderedDict() # to support multiple filters of one field - self._filters = OrderedDict() - for field in mapping.keys(): - self._columns[field.name] = field - self._filters[field.name] = mapping[field] - self._readwrite = readwrite - - @property - def columns(self): - return OrderedDict(self._columns) - - @property - def dataset(self): - if self._dataset is None: - raise ValueError('This view contains field reference only. Please call to_hdf5_dataframe to write the content to dataset first.') - else: - return self._dataset - - def add(self, field, filter, name=None): - pass - - def drop(self, name: str): - pass - - def __contains__(self, name): - pass - - def contains_field(self, field): - pass - - def __getitem__(self, name): - """ - - Example: - - >>> view['num'] - [1,2,3,4,5] - """ - pass - - def get_field(self, name): - """ - Get the field data. - - Example: - - >>> view.get_field('num') - - """ - pass - - def __setitem__(self, name, value): - """ - - Example: - - view['num'] = 'num2' # rename - view['num'] = df.create_numeric('num','int32') # change the reference field - view['num'] = [True, True, False, False] # set a filter - view['num'] = np.array([1,2,3,4,5,6]) # set the data in field - """ - pass - - def __delitem__(self, name): - """ - - Example: - - del view['num'] - """ - pass - - def delete_field(self, field): - """ - - Example: - >>> num = df.create_numeric('num', 'int32') - >>> view.add(num, [True, True, False, False]) - >>> view.delete_field(num) - """ - pass - - def keys(self): - pass - - def values(self): - pass - - def items(self): - pass - - def __iter__(self): - pass - - def __next__(self): - pass - - def __len__(self): - pass - - def apply_filter(self, filter_to_apply, field): - pass - - def apply_index(self, index_to_apply, field): - pass - - def to_hdf5_dataframe(self, dataset, name): - """ - Convert this view to a HDF5 dataframe which write all fields in this view onto the dataset. - """ - self.dataset = dataset \ No newline at end of file diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index b09d0fdf..e5a12dc1 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -100,6 +100,13 @@ def create_dataframe(self, f.data.write(v.data[:]) self._dataframes[name] = _dataframe + + # filters + filter_name = '_'+name+'_filters' + self._file.create_group(filter_name) + filters = edf.HDF5DataFrame(self, filter_name, self._file[filter_name]) + _dataframe.filters = filters + return _dataframe def require_dataframe(self, name): diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 2e785a95..6f2fcaf9 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -1,6 +1,7 @@ import pandas as pd from exetera.core.operations import INVALID_INDEX import unittest +from parameterized import parameterized from io import BytesIO import numpy as np import tempfile @@ -8,7 +9,7 @@ from exetera.core import session from exetera.core import dataframe - +from .utils import SessionTestCase, DEFAULT_FIELD_DATA class TestDataFrameCreateFields(unittest.TestCase): @@ -1326,4 +1327,22 @@ def test_raise_errors(self): with self.assertRaises(Exception) as context: df.describe(exclude=['num', 'num2', 'ts1']) - self.assertTrue(isinstance(context.exception, ValueError)) \ No newline at end of file + self.assertTrue(isinstance(context.exception, ValueError)) + +class TestDataFrameFilter(SessionTestCase): + + @parameterized.expand(DEFAULT_FIELD_DATA) + def test_set_filter(self, creator, name, kwargs, data): + f = self.setup_field(self.df, creator, name, (), kwargs, data) + + if "nformat" in kwargs: + data = np.asarray(data, dtype=kwargs["nformat"]) + + f_data = self.df.__getattr__(name) # or self.df.field_name + f_data = f_data.tolist() if hasattr(f_data, "tolist") else f_data + data = data.tolist() if hasattr(data, "tolist") else data + self.assertListEqual(f_data, data) + + + def test_remove_filter(self): + pass From 735b7a56eb7942ee370fbef1f9257b0b7ab96115 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Fri, 6 May 2022 17:15:49 +0100 Subject: [PATCH 168/181] dataframe view updates 3: filters in dataframe is a h5group instance filter in field is a numeric field instance (need change) filter in field array is checked automatically (by location) --- exetera/core/dataframe.py | 46 +++++++++++++++++++-------------------- exetera/core/dataset.py | 6 ----- exetera/core/fields.py | 15 ++++++++++++- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 6eec36f0..67ad4fee 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -61,7 +61,13 @@ def __init__(self, self._h5group = h5group # the HDF5 group to store all fields for subg in h5group.keys(): - self._columns[subg] = dataset.session.get(h5group[subg]) + if subg[0] != '_': # stores metadata, for example filters + self._columns[subg] = dataset.session.get(h5group[subg]) + + if '_filters' not in h5group.keys(): + self._filters_grp = self._h5group.create_group('_filters') + else: + self._filters_grp = h5group['_filters'] @property def columns(self): @@ -284,12 +290,22 @@ def set_filter(self, field: Union[str, fld.Field], filter): if name not in self._columns: raise ValueError("The target field is not in this dataframe.") - dtype = 'int32' if filter[-1] < 2**31 - 1 else 'int64' - if name in self.filters.keys(): - self.filters[name].data.clear() - self.filters[name].data.write(filter) + nformat = 'int32' if filter[-1] < 2 ** 31 - 1 else 'int64' + if name in self._filters_grp.keys(): + filter_field = fld.NumericField(self._dataset.session, self._filters_grp[name], self, + write_enabled=True) + if nformat not in filter_field._fieldtype: + filter_field = filter_field.astype(nformat) + filter_field.data.clear() + filter_field.data.write(filter) else: - self.filters.create_numeric(name, dtype).data.write(filter) + fld.numeric_field_constructor(self._dataset.session, self._filters_grp, name, nformat) + filter_field = fld.NumericField(self._dataset.session, self._filters_grp[name], self, + write_enabled=True) + filter_field.data.write(filter) + + self._columns[name]._filter = filter_field + return filter_field def delete_filter(self, field: Union[str, fld.Field]): """ @@ -302,7 +318,7 @@ def delete_filter(self, field: Union[str, fld.Field]): if name not in self._columns: raise ValueError("The target field is not in this dataframe.") else: - del self.filters[name] + del self._filters_grp[name] def get_data(self, field: Union[str, fld.Field]): """ @@ -322,22 +338,6 @@ def get_data(self, field: Union[str, fld.Field]): else: return self.columns[name].data[:] - def __getattr__(self, item): - """ - Rewrite the getattr method so that dataframe will return the data of a field directly. - - """ - fields = object.__getattribute__(self, 'columns') - if item not in fields.keys(): - raise ValueError("Can not found the field name from this dataframe.") - else: - filters = object.__getattribute__(self, 'filters') - if item in filters.keys(): # has a filter - data_filter = filters[item].data[:] - return fields[item].data[data_filter] - else: - return fields[item].data[:] - def __getitem__(self, name): """ Get a field stored by the field name. diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index e5a12dc1..544d561a 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -101,12 +101,6 @@ def create_dataframe(self, self._dataframes[name] = _dataframe - # filters - filter_name = '_'+name+'_filters' - self._file.create_group(filter_name) - filters = edf.HDF5DataFrame(self, filter_name, self._file[filter_name]) - _dataframe.filters = filters - return _dataframe def require_dataframe(self, name): diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 109fabf7..3364e00f 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -55,6 +55,8 @@ def __init__(self, session, group, dataframe, write_enabled=False): self._value_wrapper = None self._valid_reference = True + self._filter = None + @property def valid(self): """ @@ -113,6 +115,10 @@ def indexed(self): self._ensure_valid() return False + @property + def filter(self): + return self._filter + def __bool__(self): # this method is required to prevent __len__ being called on derived methods when fields are queried as # if f: @@ -144,6 +150,7 @@ def _ensure_valid(self): raise ValueError("This field no longer refers to a valid underlying field object") + class MemoryField(Field): def __init__(self, session): @@ -300,7 +307,13 @@ def dtype(self): return self._dataset.dtype def __getitem__(self, item): - return self._dataset[item] + df_name = self._field.name[0: self._field.name.rfind('/')] + field_name =self._field.name[self._field.name.rfind('/')+1:] + if field_name in self._field.get(df_name+'/_filters').keys(): + filter_data = self._field.get(df_name+'/_filters')[field_name][self._name][:] + return self._dataset[item][filter_data] + else: + return self._dataset[item] def __setitem__(self, key, value): self._dataset[key] = value From 85169e66930cfd24dc6230dde684e802ea0ba202 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 9 May 2022 09:27:55 +0100 Subject: [PATCH 169/181] change filtered data presentation from data array to field __getitem__ --- exetera/core/abstract_types.py | 5 +++++ exetera/core/dataframe.py | 13 ++++++++++++- exetera/core/fields.py | 34 ++++++++++++++++++++++------------ tests/test_dataframe.py | 28 ++++++++++++++++++---------- 4 files changed, 57 insertions(+), 23 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index 48bede94..aaa9483b 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -65,6 +65,11 @@ def indexed(self): def data(self): raise NotImplementedError() + @property + @abstractmethod + def filter(self): + raise NotImplementedError() + @abstractmethod def __bool__(self): raise NotImplementedError() diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 67ad4fee..f8b00500 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -107,6 +107,17 @@ def add(self, nfield.data.write(field.data[:]) self._columns[dname] = nfield + def add_reference(self, field: fld.Field): + """ + Add a field without coping the data over the HDF5 group. + :param field: field to be constructed in this dataframe. + """ + if isinstance(field, fld.NumericField): + fld.numeric_field_constructor(self._dataset.session, self, field.name, field._nformat) + nfield = fld.NumericField(self._dataset.session, field._field, self, write_enabled=True) + self._columns[field.name] = nfield + return self._columns[field.name] + def drop(self, name: str): """ @@ -304,7 +315,7 @@ def set_filter(self, field: Union[str, fld.Field], filter): write_enabled=True) filter_field.data.write(filter) - self._columns[name]._filter = filter_field + self._columns[name].filter = self._filters_grp[name] return filter_field def delete_filter(self, field: Union[str, fld.Field]): diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 3364e00f..10de3170 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -55,7 +55,7 @@ def __init__(self, session, group, dataframe, write_enabled=False): self._value_wrapper = None self._valid_reference = True - self._filter = None + self._filter_wrapper = None @property def valid(self): @@ -115,10 +115,6 @@ def indexed(self): self._ensure_valid() return False - @property - def filter(self): - return self._filter - def __bool__(self): # this method is required to prevent __len__ being called on derived methods when fields are queried as # if f: @@ -307,13 +303,7 @@ def dtype(self): return self._dataset.dtype def __getitem__(self, item): - df_name = self._field.name[0: self._field.name.rfind('/')] - field_name =self._field.name[self._field.name.rfind('/')+1:] - if field_name in self._field.get(df_name+'/_filters').keys(): - filter_data = self._field.get(df_name+'/_filters')[field_name][self._name][:] - return self._dataset[item][filter_data] - else: - return self._dataset[item] + return self._dataset[item] def __setitem__(self, key, value): self._dataset[key] = value @@ -2577,6 +2567,26 @@ def data(self): self._value_wrapper = ReadOnlyFieldArray(self._field, 'values') return self._value_wrapper + + @property + def filter(self): + if self._filter_wrapper is None: + return None + else: + return self._filter_wrapper[:] + return self._filter + + @filter.setter + def filter(self, filter_h5group): + self._filter_wrapper = WriteableFieldArray(filter_h5group, 'values') + + def __getitem__(self, item): + if self._filter_wrapper != None: + data_filter = self._filter_wrapper[:] + return self.data[item][data_filter] + else: + return self.data[item] + def is_sorted(self): """ Returns if data in field is sorted diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 6f2fcaf9..a74c0609 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -1331,17 +1331,25 @@ def test_raise_errors(self): class TestDataFrameFilter(SessionTestCase): - @parameterized.expand(DEFAULT_FIELD_DATA) + @parameterized.expand([('','num','',[1,2,3,4,5,6,7,8,9,10])]) def test_set_filter(self, creator, name, kwargs, data): - f = self.setup_field(self.df, creator, name, (), kwargs, data) - - if "nformat" in kwargs: - data = np.asarray(data, dtype=kwargs["nformat"]) - - f_data = self.df.__getattr__(name) # or self.df.field_name - f_data = f_data.tolist() if hasattr(f_data, "tolist") else f_data - data = data.tolist() if hasattr(data, "tolist") else data - self.assertListEqual(f_data, data) + #f = self.setup_field(self.df, creator, name, (), kwargs, data) + f = self.df.create_numeric(name, 'int32') + f.data.write(data) + + data = np.asarray(data, 'int32') + + d_filter = np.array([1,3,5,7]) + self.df.set_filter(name, d_filter) + self.assertListEqual(f.data[:].tolist(), data.tolist()) # unfiltered data + self.assertListEqual(f[:].tolist(), data[d_filter].tolist()) # filtered data + + df2 = self.ds.create_dataframe('df2') + df2.add_reference(f) + f2 = df2[name] + self.assertEqual(f._field.name, f2._field.name) + self.assertListEqual(f2.data[:].tolist(), data.tolist()) # unfiltered data + self.assertListEqual(f2[:].tolist(), data.tolist()) # unfiltered data def test_remove_filter(self): From 9667dd710e788192a4dbdae4dcf27a11cd9542f3 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 9 May 2022 09:30:21 +0100 Subject: [PATCH 170/181] minor update --- exetera/core/dataset.py | 1 - 1 file changed, 1 deletion(-) diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index 544d561a..b09d0fdf 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -100,7 +100,6 @@ def create_dataframe(self, f.data.write(v.data[:]) self._dataframes[name] = _dataframe - return _dataframe def require_dataframe(self, name): From c333f6d6c3bc1d2a74bcbb7abf5d8a13376f1783 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 11 May 2022 13:26:53 +0100 Subject: [PATCH 171/181] updated view functions, 1, In dataframe, added to_view; changed behaviour of apply_filter 2, In field, added is_view, concrete_field 3, In field array, added register_reference, detach_reference and concrete_all_fields --- exetera/core/dataframe.py | 198 +++++++++++++++++++++++++------------- exetera/core/fields.py | 42 +++++++- 2 files changed, 170 insertions(+), 70 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index f8b00500..5ec036ec 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -107,16 +107,26 @@ def add(self, nfield.data.write(field.data[:]) self._columns[dname] = nfield - def add_reference(self, field: fld.Field): + def add_view(self, field: fld.Field): """ - Add a field without coping the data over the HDF5 group. - :param field: field to be constructed in this dataframe. + """ if isinstance(field, fld.NumericField): - fld.numeric_field_constructor(self._dataset.session, self, field.name, field._nformat) - nfield = fld.NumericField(self._dataset.session, field._field, self, write_enabled=True) - self._columns[field.name] = nfield - return self._columns[field.name] + view = fld.NumericField(field._session, field._field, self, write_enabled=True) + + self._columns[view.name] = view + return self._columns[view.name] + + # def add_reference(self, field: fld.Field): + # """ + # Add a field without coping the data over the HDF5 group. + # :param field: field to be constructed in this dataframe. + # """ + # if isinstance(field, fld.NumericField): + # fld.numeric_field_constructor(self._dataset.session, self, field.name, field._nformat) + # nfield = fld.NumericField(self._dataset.session, field._field, self, write_enabled=True) + # self._columns[field.name] = nfield + # return self._columns[field.name] def drop(self, name: str): @@ -125,8 +135,10 @@ def drop(self, :param name: name of field to be dropped """ - del self._columns[name] - del self._h5group[name] + if name in self._h5group.keys(): + del self._h5group[name] + if name in self._columns.keys(): + del self._columns[name] def create_group(self, name: str): @@ -281,46 +293,64 @@ def contains_field(self, field): return True return False - def get_filter(self, field: Union[str, fld.Field]): + def _write_filter(self, filter): """ - Get a filter array specified by the field or field name. - """ - pass - def set_filter(self, field: Union[str, fld.Field], filter): """ - Add or modify a filter of the field. - - :param field: The target field. - :param filter: The filter, as list or np.ndarray of indices. - """ - if not isinstance(field, str) and not isinstance(field, fld.Field): - raise TypeError("The target field should be type field or string (name of the field in this dataframe).") - - name = field if isinstance(field, str) else field.name - if name not in self._columns: - raise ValueError("The target field is not in this dataframe.") - nformat = 'int32' if filter[-1] < 2 ** 31 - 1 else 'int64' - if name in self._filters_grp.keys(): - filter_field = fld.NumericField(self._dataset.session, self._filters_grp[name], self, - write_enabled=True) + filter_name = '_filter' + if filter_name not in self._filters_grp.keys(): + fld.numeric_field_constructor(self._dataset.session, self._filters_grp, filter_name, nformat) + filter_field = fld.NumericField(self._dataset.session, self._filters_grp[filter_name], self, write_enabled=True) + filter_field.data.write(filter) + else: + filter_field = fld.NumericField(self._dataset.session, self._filters_grp[filter_name], self, write_enabled=True) if nformat not in filter_field._fieldtype: filter_field = filter_field.astype(nformat) filter_field.data.clear() filter_field.data.write(filter) - else: - fld.numeric_field_constructor(self._dataset.session, self._filters_grp, name, nformat) - filter_field = fld.NumericField(self._dataset.session, self._filters_grp[name], self, - write_enabled=True) - filter_field.data.write(filter) - - self._columns[name].filter = self._filters_grp[name] - return filter_field - def delete_filter(self, field: Union[str, fld.Field]): + def _get_filter_grp(self, field: Union[str, fld.Field]=None): + """ + Get a filter array specified by the field or field name. """ - Delete a filter from this dataframe specified by the field or field name. + filter_name = '_filter' + return self._filters_grp[filter_name] + + # def set_filter(self, field: Union[str, fld.Field], filter): + # """ + # Add or modify a filter of the field. + # + # :param field: The target field. + # :param filter: The filter, as list or np.ndarray of indices. + # """ + # if not isinstance(field, str) and not isinstance(field, fld.Field): + # raise TypeError("The target field should be type field or string (name of the field in this dataframe).") + # + # name = field if isinstance(field, str) else field.name + # if name not in self._columns: + # raise ValueError("The target field is not in this dataframe.") + # + # nformat = 'int32' if filter[-1] < 2 ** 31 - 1 else 'int64' + # if name in self._filters_grp.keys(): + # filter_field = fld.NumericField(self._dataset.session, self._filters_grp[name], self, + # write_enabled=True) + # if nformat not in filter_field._fieldtype: + # filter_field = filter_field.astype(nformat) + # filter_field.data.clear() + # filter_field.data.write(filter) + # else: + # fld.numeric_field_constructor(self._dataset.session, self._filters_grp, name, nformat) + # filter_field = fld.NumericField(self._dataset.session, self._filters_grp[name], self, + # write_enabled=True) + # filter_field.data.write(filter) + # + # self._columns[name].filter = self._filters_grp[name] + # return filter_field + + def remove_filter(self, field: Union[str, fld.Field]): + """ + Remove filter from this dataframe specified by the field or field name. """ if not isinstance(field, str) and not isinstance(field, fld.Field): raise TypeError("The target field should be type field or string (name of the field in this dataframe).") @@ -331,23 +361,23 @@ def delete_filter(self, field: Union[str, fld.Field]): else: del self._filters_grp[name] - def get_data(self, field: Union[str, fld.Field]): - """ - Get the data from a field. The data returned is masked by the filter. - - """ - if not isinstance(field, str) and not isinstance(field, fld.Field): - raise TypeError("The target field should be type field or string (name of the field in this dataframe).") - - name = field if isinstance(field, str) else field.name - if name not in self.columns.keys(): - raise ValueError("Can not found the field name from this dataframe.") - else: - if name in self.filters.keys(): - d_filter = self.filters[name].data[:] - return self.columns[name].data[d_filter] - else: - return self.columns[name].data[:] + # def get_data(self, field: Union[str, fld.Field]): + # """ + # Get the data from a field. The data returned is masked by the filter. + # + # """ + # if not isinstance(field, str) and not isinstance(field, fld.Field): + # raise TypeError("The target field should be type field or string (name of the field in this dataframe).") + # + # name = field if isinstance(field, str) else field.name + # if name not in self.columns.keys(): + # raise ValueError("Can not found the field name from this dataframe.") + # else: + # if name in self.filters.keys(): + # d_filter = self.filters[name].data[:] + # return self.columns[name].data[d_filter] + # else: + # return self.columns[name].data[:] def __getitem__(self, name): """ @@ -402,8 +432,10 @@ def __delitem__(self, name): if not self.__contains__(name=name): raise ValueError("There is no field named '{}' in this dataframe".format(name)) else: - del self._h5group[name] - del self._columns[name] + if name in self._h5group.keys(): + del self._h5group[name] + if name in self._columns.keys(): + del self._columns[name] def delete_field(self, field): """ @@ -563,19 +595,25 @@ def apply_filter(self, filter_to_apply, ddf=None): :returns: a dataframe contains all the fields filterd, self if ddf is not set """ filter_to_apply_ = val.validate_filter(filter_to_apply) - if ddf is not None: if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") + ddf._write_filter(np.where(filter_to_apply_ == True)[0]) for name, field in self._columns.items(): - newfld = field.create_like(ddf, name) - field.apply_filter(filter_to_apply_, target=newfld) + # hard copy + # newfld = field.create_like(ddf, name) + # field.apply_filter(filter_to_apply_, target=newfld) + # soft copy - view + newfld = ddf.add_view(field) + newfld.filter = ddf._get_filter_grp() + return ddf else: for field in self._columns.values(): field.apply_filter(filter_to_apply_, in_place=True) return self + def apply_index(self, index_to_apply, ddf=None): """ Apply an index to all fields in this dataframe, returns \ @@ -602,16 +640,31 @@ def apply_index(self, index_to_apply, ddf=None): if ddf is not None: if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") - for name, field in self._columns.items(): - newfld = field.create_like(ddf, name) - field.apply_index(index_to_apply, target=newfld) - return ddf - else: - val.validate_all_field_length_in_df(self) + if ddf == self: + val.validate_all_field_length_in_df(self) + for field in self._columns.values(): + if ddf == self: + field.apply_index(index_to_apply, in_place=True) + else: + newfld = field.create_like(ddf, field.name) + field.apply_index(index_to_apply, target=newfld) + else: # + nformat = 'int32' if index_to_apply[-1] < 2 ** 31 - 1 else 'int64' for field in self._columns.values(): - field.apply_index(index_to_apply, in_place=True) - return self + if field.name in self._filters_grp.keys(): + flt_fld = fld.NumericField(self._dataset.session, self._filters_grp[field.name], self, + write_enabled=True) + if nformat not in flt_fld._fieldtype: + flt_fld = flt_fld.astype(nformat) + flt_fld.data.clear() + flt_fld.data.write(index_to_apply) + else: + fld.numeric_field_constructor(self._dataset.session, self._filters_grp, field.name, nformat) + flt_fld = fld.NumericField(self._dataset.session, self._filters_grp[field.name], self, + write_enabled=True) + flt_fld.data.write(index_to_apply) + field.filter = flt_fld._field def sort_values(self, by: Union[str, List[str]], ddf: DataFrame = None, axis=0, ascending=True, kind='stable'): @@ -1066,6 +1119,13 @@ def describe(self, include=None, exclude=None, output='terminal'): print('\n') return result + def view(self): + dfv = self.dataset.create_dataframe(self.name + '_view') + for f in self.columns.values(): + #fld.numeric_field_constructor(self._dataset.session, dfv, f.name, f._nformat) + nfield = fld.NumericField(self._dataset.session, f._field, dfv, write_enabled=True) + dfv._columns[f.name] = nfield + return dfv class HDF5DataFrameGroupBy(DataFrameGroupBy): diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 10de3170..b8117d3a 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -286,6 +286,7 @@ def __init__(self, field, dataset_name): self._field = field self._name = dataset_name self._dataset = field[dataset_name] + self._references = list() def __len__(self): """ @@ -302,6 +303,25 @@ def dtype(self): """ return self._dataset.dtype + def register_reference(self, field: Field): + """ + + """ + self._references.append(field) + + def detach_reference(self, field: Field): + """ + + """ + self._references.remove(field) + + def concreate_all_fields(self): + """ + + """ + for field in self._references: + field.concrete_reference() + def __getitem__(self, item): return self._dataset[item] @@ -2533,6 +2553,8 @@ class NumericField(HDF5Field): def __init__(self, session, group, dataframe, write_enabled=False): super().__init__(session, group, dataframe, write_enabled=write_enabled) self._nformat = self._field.attrs['nformat'] + if self.is_view(): + self.data.register_reference(self) def writeable(self): """ @@ -2580,6 +2602,15 @@ def filter(self): def filter(self, filter_h5group): self._filter_wrapper = WriteableFieldArray(filter_h5group, 'values') + def is_view(self): + """ + Return if the dataframe's name matches the field h5group path; if not, means this field is a view. + """ + if self._field.name[1:self._field.name.rfind('/')] == self.dataframe.name: + return False + else: + return True + def __getitem__(self, item): if self._filter_wrapper != None: data_filter = self._filter_wrapper[:] @@ -2587,6 +2618,16 @@ def __getitem__(self, item): else: return self.data[item] + def concrete_reference(self): + if not self.is_view(): + raise ValueError("This field is already a concreted field.") + + self.data.detach_reference(self) # notice field array + del self.dataframe[self.name] # notice dataframe + concrete_field = self.create_like(self.dataframe, self.name) # create + concrete_field.data.write(self[:]) # write data + return concrete_field + def is_sorted(self): """ Returns if data in field is sorted @@ -4119,7 +4160,6 @@ def timestamp_field_create_like(source, group, name, timestamp): else: return group.create_timestamp(name, ts) - @staticmethod def apply_isin(source: Field, test_elements: Union[list, set, np.ndarray]): """ From bb764bd4de2a9373001a663dbd20cabad1a78c72 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 12 May 2022 09:46:21 +0100 Subject: [PATCH 172/181] modify the association between view fields with field array, by assign the field array to the view in dataframe when adding view --- exetera/core/dataframe.py | 5 ++-- exetera/core/fields.py | 21 ++++++++++---- tests/test_dataframe.py | 58 +++++++++++++++++++++++++++------------ 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 5ec036ec..b9cca3c2 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -113,6 +113,7 @@ def add_view(self, field: fld.Field): """ if isinstance(field, fld.NumericField): view = fld.NumericField(field._session, field._field, self, write_enabled=True) + view.data = field.data self._columns[view.name] = view return self._columns[view.name] @@ -1122,9 +1123,7 @@ def describe(self, include=None, exclude=None, output='terminal'): def view(self): dfv = self.dataset.create_dataframe(self.name + '_view') for f in self.columns.values(): - #fld.numeric_field_constructor(self._dataset.session, dfv, f.name, f._nformat) - nfield = fld.NumericField(self._dataset.session, f._field, dfv, write_enabled=True) - dfv._columns[f.name] = nfield + dfv.add_view(f) return dfv diff --git a/exetera/core/fields.py b/exetera/core/fields.py index b8117d3a..eec7ea6a 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -333,6 +333,8 @@ def clear(self): Replaces current dataset with empty dataset. :return: None """ + if len(self._references) > 0: + self.concreate_all_fields() nformat = self._dataset.dtype DataWriter._clear_dataset(self._field, self._name) DataWriter.write(self._field, self._name, [], 0, nformat) @@ -363,6 +365,8 @@ def write(self, part): :param part: numpy array to write to field :return: None """ + if len(self._references) > 0: + self.concreate_all_fields() if isinstance(part, Field): part = part.data[:] DataWriter.write(self._field, self._name, part, len(part), dtype=self._dataset.dtype) @@ -2553,8 +2557,6 @@ class NumericField(HDF5Field): def __init__(self, session, group, dataframe, write_enabled=False): super().__init__(session, group, dataframe, write_enabled=write_enabled) self._nformat = self._field.attrs['nformat'] - if self.is_view(): - self.data.register_reference(self) def writeable(self): """ @@ -2589,6 +2591,14 @@ def data(self): self._value_wrapper = ReadOnlyFieldArray(self._field, 'values') return self._value_wrapper + @data.setter + def data(self, FieldArray): + """ + Setting the Field Array (data interface) directly. This can also associate field with an existing field array to enable a view. + """ + self._value_wrapper = FieldArray + if self.is_view(): + self.data.register_reference(self) @property def filter(self): @@ -2606,10 +2616,10 @@ def is_view(self): """ Return if the dataframe's name matches the field h5group path; if not, means this field is a view. """ - if self._field.name[1:self._field.name.rfind('/')] == self.dataframe.name: - return False - else: + if self._field.name[1:1+len(self.dataframe.name)] != self.dataframe.name: return True + else: + return False def __getitem__(self, item): if self._filter_wrapper != None: @@ -2623,6 +2633,7 @@ def concrete_reference(self): raise ValueError("This field is already a concreted field.") self.data.detach_reference(self) # notice field array + print(self.name) del self.dataframe[self.name] # notice dataframe concrete_field = self.create_like(self.dataframe, self.name) # create concrete_field.data.write(self[:]) # write data diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index a74c0609..6aaa1dd6 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -1331,26 +1331,48 @@ def test_raise_errors(self): class TestDataFrameFilter(SessionTestCase): - @parameterized.expand([('','num','',[1,2,3,4,5,6,7,8,9,10])]) - def test_set_filter(self, creator, name, kwargs, data): - #f = self.setup_field(self.df, creator, name, (), kwargs, data) - f = self.df.create_numeric(name, 'int32') - f.data.write(data) + @parameterized.expand([("create_numeric", "f_i8", {"nformat": "int8"}, [i for i in range(20)] ),]) + def test_apply_filter(self, creator, name, kwargs, data): + data = np.asarray(data) + f = self.setup_field(self.df, creator, name, (), kwargs, data) + view_df = self.ds.create_dataframe('view_df') + filter_to_apply = np.asarray([i%2 == 0 for i in data]) + self.df.apply_filter(filter_to_apply, ddf= view_df) + for field in view_df.values(): + self.assertTrue(field.is_view()) # field is a view + self.assertListEqual(data[filter_to_apply].tolist(), field[:].tolist() ) # filtered + + # + view_df2 = self.ds.create_dataframe('view_df2') + filter_to_apply &= np.asarray([i < 10 for i in data]) + self.df.apply_filter(filter_to_apply, ddf=view_df2) + for field in view_df2.values(): + self.assertTrue(field.is_view()) # field is a view + self.assertListEqual(data[filter_to_apply].tolist(), field[:].tolist()) # filtered - data = np.asarray(data, 'int32') + def test_remove_filter(self): + pass + + @parameterized.expand([("create_numeric", "f_i8", {"nformat": "int8"}, [i for i in range(20)]), ]) + def test_concrete_field(self, creator, name, kwargs, data): + data = np.asarray(data) + f = self.setup_field(self.df, creator, name, (), kwargs, data) + view_df = self.ds.create_dataframe('view_df') + filter_to_apply = np.asarray([i % 2 == 0 for i in data]) + self.df.apply_filter(filter_to_apply, ddf=view_df) + print('a',view_df.keys()) + for field in view_df.values(): + self.assertTrue(field.is_view()) # field is a view + self.assertListEqual(data[filter_to_apply].tolist(), field[:].tolist()) # filtered + + new_data = data + 1 + f.data.clear() + f.data.write(new_data) # data changed, view should be concreate automatically + + for field in view_df.values(): + self.assertFalse(field.is_view()) + self.assertListEqual(data[filter_to_apply].tolist(), field[:].tolist()) # filtered - d_filter = np.array([1,3,5,7]) - self.df.set_filter(name, d_filter) - self.assertListEqual(f.data[:].tolist(), data.tolist()) # unfiltered data - self.assertListEqual(f[:].tolist(), data[d_filter].tolist()) # filtered data - df2 = self.ds.create_dataframe('df2') - df2.add_reference(f) - f2 = df2[name] - self.assertEqual(f._field.name, f2._field.name) - self.assertListEqual(f2.data[:].tolist(), data.tolist()) # unfiltered data - self.assertListEqual(f2[:].tolist(), data.tolist()) # unfiltered data - def test_remove_filter(self): - pass From 7803d76a7077ee9d78f6f2aa9ec59f4f114e4a47 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 18 May 2022 11:14:02 +0100 Subject: [PATCH 173/181] update the view: 1, move observer/subject to field, from field array 2, update field array constructor, let field array aware of field so that field array can notify field during change of data 3, update create of view and binding of filter in dataframe --- exetera/core/abstract_types.py | 15 ++++ exetera/core/dataframe.py | 157 +++++++++++---------------------- exetera/core/fields.py | 127 ++++++++++++-------------- 3 files changed, 124 insertions(+), 175 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index aaa9483b..ad98763e 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -496,3 +496,18 @@ def ordered_merge_right(self, right_on, left_on, left_field_sources=tuple(), right_field_sinks=None, right_to_left_map=None, right_unique=False, left_unique=False): raise NotImplementedError() + + +class SubjectObserver(ABC): + def attach(self, observer): + raise NotImplementedError() + + def detach(self, observer): + raise NotImplementedError() + + def notify(self, msg=None): + raise NotImplementedError() + + def update(self, subject, msg=None): + raise NotImplementedError() + diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index b9cca3c2..2ba1c184 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -107,27 +107,62 @@ def add(self, nfield.data.write(field.data[:]) self._columns[dname] = nfield - def add_view(self, field: fld.Field): + def _add_view(self, field: fld.Field, filter: np.ndarray = None): """ + Internal function called by apply_filter to add a field view into the dataframe. + + :param field: The field to apply filter to. + :param filter: The filter to apply. + :return: The field view. """ + # add view if isinstance(field, fld.NumericField): view = fld.NumericField(field._session, field._field, self, write_enabled=True) - view.data = field.data self._columns[view.name] = view + + # add filter + if filter is not None: + nformat = 'int32' if filter[-1] < 2 ** 31 - 1 else 'int64' + filter_name = view.name + if filter_name not in self._filters_grp.keys(): + fld.numeric_field_constructor(self._dataset.session, self._filters_grp, filter_name, nformat) + filter_field = fld.NumericField(self._dataset.session, self._filters_grp[filter_name], self, + write_enabled=True) + filter_field.data.write(filter) + else: + filter_field = fld.NumericField(self._dataset.session, self._filters_grp[filter_name], self, + write_enabled=True) + if nformat not in filter_field._fieldtype: + filter_field = filter_field.astype(nformat) + filter_field.data.clear() + filter_field.data.write(filter) + view._filter_wrapper = filter_field.data + return self._columns[view.name] - # def add_reference(self, field: fld.Field): + # def change_filter(self, field: fld.Field, filter: np.ndarray): # """ - # Add a field without coping the data over the HDF5 group. - # :param field: field to be constructed in this dataframe. + # + # :param field: + # :param filter: + # :return: + # """ + # pass + # + # def remove_filter(self, field: Union[str, fld.Field]): # """ - # if isinstance(field, fld.NumericField): - # fld.numeric_field_constructor(self._dataset.session, self, field.name, field._nformat) - # nfield = fld.NumericField(self._dataset.session, field._field, self, write_enabled=True) - # self._columns[field.name] = nfield - # return self._columns[field.name] + # Remove filter from this dataframe specified by the field or field name. + # """ + # if not isinstance(field, str) and not isinstance(field, fld.Field): + # raise TypeError("The target field should be type field or string (name of the field in this dataframe).") + # + # name = field if isinstance(field, str) else field.name + # if name not in self._columns: + # raise ValueError("The target field is not in this dataframe.") + # else: + # del self._filters_grp[name] def drop(self, name: str): @@ -136,10 +171,9 @@ def drop(self, :param name: name of field to be dropped """ - if name in self._h5group.keys(): + del self._columns[name] # should always be + if name in self._h5group.keys(): # in case of reference only del self._h5group[name] - if name in self._columns.keys(): - del self._columns[name] def create_group(self, name: str): @@ -294,22 +328,6 @@ def contains_field(self, field): return True return False - def _write_filter(self, filter): - """ - - """ - nformat = 'int32' if filter[-1] < 2 ** 31 - 1 else 'int64' - filter_name = '_filter' - if filter_name not in self._filters_grp.keys(): - fld.numeric_field_constructor(self._dataset.session, self._filters_grp, filter_name, nformat) - filter_field = fld.NumericField(self._dataset.session, self._filters_grp[filter_name], self, write_enabled=True) - filter_field.data.write(filter) - else: - filter_field = fld.NumericField(self._dataset.session, self._filters_grp[filter_name], self, write_enabled=True) - if nformat not in filter_field._fieldtype: - filter_field = filter_field.astype(nformat) - filter_field.data.clear() - filter_field.data.write(filter) def _get_filter_grp(self, field: Union[str, fld.Field]=None): """ @@ -318,68 +336,6 @@ def _get_filter_grp(self, field: Union[str, fld.Field]=None): filter_name = '_filter' return self._filters_grp[filter_name] - # def set_filter(self, field: Union[str, fld.Field], filter): - # """ - # Add or modify a filter of the field. - # - # :param field: The target field. - # :param filter: The filter, as list or np.ndarray of indices. - # """ - # if not isinstance(field, str) and not isinstance(field, fld.Field): - # raise TypeError("The target field should be type field or string (name of the field in this dataframe).") - # - # name = field if isinstance(field, str) else field.name - # if name not in self._columns: - # raise ValueError("The target field is not in this dataframe.") - # - # nformat = 'int32' if filter[-1] < 2 ** 31 - 1 else 'int64' - # if name in self._filters_grp.keys(): - # filter_field = fld.NumericField(self._dataset.session, self._filters_grp[name], self, - # write_enabled=True) - # if nformat not in filter_field._fieldtype: - # filter_field = filter_field.astype(nformat) - # filter_field.data.clear() - # filter_field.data.write(filter) - # else: - # fld.numeric_field_constructor(self._dataset.session, self._filters_grp, name, nformat) - # filter_field = fld.NumericField(self._dataset.session, self._filters_grp[name], self, - # write_enabled=True) - # filter_field.data.write(filter) - # - # self._columns[name].filter = self._filters_grp[name] - # return filter_field - - def remove_filter(self, field: Union[str, fld.Field]): - """ - Remove filter from this dataframe specified by the field or field name. - """ - if not isinstance(field, str) and not isinstance(field, fld.Field): - raise TypeError("The target field should be type field or string (name of the field in this dataframe).") - - name = field if isinstance(field, str) else field.name - if name not in self._columns: - raise ValueError("The target field is not in this dataframe.") - else: - del self._filters_grp[name] - - # def get_data(self, field: Union[str, fld.Field]): - # """ - # Get the data from a field. The data returned is masked by the filter. - # - # """ - # if not isinstance(field, str) and not isinstance(field, fld.Field): - # raise TypeError("The target field should be type field or string (name of the field in this dataframe).") - # - # name = field if isinstance(field, str) else field.name - # if name not in self.columns.keys(): - # raise ValueError("Can not found the field name from this dataframe.") - # else: - # if name in self.filters.keys(): - # d_filter = self.filters[name].data[:] - # return self.columns[name].data[d_filter] - # else: - # return self.columns[name].data[:] - def __getitem__(self, name): """ Get a field stored by the field name. @@ -433,10 +389,10 @@ def __delitem__(self, name): if not self.__contains__(name=name): raise ValueError("There is no field named '{}' in this dataframe".format(name)) else: - if name in self._h5group.keys(): + del self._columns[name] # should always be + if name in self._h5group.keys(): # in case of reference only del self._h5group[name] - if name in self._columns.keys(): - del self._columns[name] + def delete_field(self, field): """ @@ -599,22 +555,15 @@ def apply_filter(self, filter_to_apply, ddf=None): if ddf is not None: if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") - ddf._write_filter(np.where(filter_to_apply_ == True)[0]) + filter_to_apply_ = filter_to_apply_.nonzero()[0] for name, field in self._columns.items(): - # hard copy - # newfld = field.create_like(ddf, name) - # field.apply_filter(filter_to_apply_, target=newfld) - # soft copy - view - newfld = ddf.add_view(field) - newfld.filter = ddf._get_filter_grp() - + ddf._add_view(field, filter_to_apply_) return ddf else: for field in self._columns.values(): field.apply_filter(filter_to_apply_, in_place=True) return self - def apply_index(self, index_to_apply, ddf=None): """ Apply an index to all fields in this dataframe, returns \ @@ -1123,7 +1072,7 @@ def describe(self, include=None, exclude=None, output='terminal'): def view(self): dfv = self.dataset.create_dataframe(self.name + '_view') for f in self.columns.values(): - dfv.add_view(f) + dfv._add_view(f) return dfv diff --git a/exetera/core/fields.py b/exetera/core/fields.py index eec7ea6a..442bde01 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -16,7 +16,7 @@ import numpy as np import h5py -from exetera.core.abstract_types import Field +from exetera.core.abstract_types import Field, SubjectObserver from exetera.core.data_writer import DataWriter from exetera.core import operations as ops from exetera.core import validation as val @@ -39,7 +39,7 @@ def isin(field:Field, test_elements:Union[list, set, np.ndarray]): return ret -class HDF5Field(Field): +class HDF5Field(Field, SubjectObserver): def __init__(self, session, group, dataframe, write_enabled=False): super().__init__() @@ -56,6 +56,7 @@ def __init__(self, session, group, dataframe, write_enabled=False): self._valid_reference = True self._filter_wrapper = None + self._view_refs = list() @property def valid(self): @@ -228,9 +229,10 @@ def apply_index(self, index_to_apply, dstfld=None): class ReadOnlyFieldArray: def __init__(self, field, dataset_name): - self._field = field + self._field_instance = field + self._field = field._field self._name = dataset_name - self._dataset = field[dataset_name] + self._dataset = self._field[dataset_name] def __len__(self): return len(self._dataset) @@ -243,7 +245,11 @@ def dtype(self): return self._dataset.dtype def __getitem__(self, item): - return self._dataset[item] + if self._field_instance.filter is not None: + mask = self._field_instance.filter[:] + return self._dataset[mask][item] # note: mask before item + else: + return self._dataset[item] def __setitem__(self, key, value): raise PermissionError("This field was created read-only; call .writeable() " @@ -283,9 +289,10 @@ def complete(self): class WriteableFieldArray: def __init__(self, field, dataset_name): - self._field = field + self._field_instance = field + self._field = field._field # HDF5 group instance self._name = dataset_name - self._dataset = field[dataset_name] + self._dataset = self._field[dataset_name] self._references = list() def __len__(self): @@ -303,27 +310,12 @@ def dtype(self): """ return self._dataset.dtype - def register_reference(self, field: Field): - """ - - """ - self._references.append(field) - - def detach_reference(self, field: Field): - """ - - """ - self._references.remove(field) - - def concreate_all_fields(self): - """ - - """ - for field in self._references: - field.concrete_reference() - def __getitem__(self, item): - return self._dataset[item] + if self._field_instance.filter is not None: + mask = self._field_instance.filter[:] + return self._dataset[mask][item] # note: mask before item + else: + return self._dataset[item] def __setitem__(self, key, value): self._dataset[key] = value @@ -333,8 +325,8 @@ def clear(self): Replaces current dataset with empty dataset. :return: None """ - if len(self._references) > 0: - self.concreate_all_fields() + self._field_instance.update(self, msg=WriteableFieldArray.clear.__name__) + nformat = self._dataset.dtype DataWriter._clear_dataset(self._field, self._name) DataWriter.write(self._field, self._name, [], 0, nformat) @@ -365,8 +357,8 @@ def write(self, part): :param part: numpy array to write to field :return: None """ - if len(self._references) > 0: - self.concreate_all_fields() + self._field_instance.update(self, msg=WriteableFieldArray.write.__name__) + if isinstance(part, Field): part = part.data[:] DataWriter.write(self._field, self._name, part, len(part), dtype=self._dataset.dtype) @@ -2138,7 +2130,7 @@ def indices(self): self._ensure_valid() if self._index_wrapper is None: wrapper = WriteableFieldArray if self._write_enabled else ReadOnlyFieldArray - self._index_wrapper = wrapper(self._field, 'index') + self._index_wrapper = wrapper(self, 'index') return self._index_wrapper @property @@ -2149,7 +2141,7 @@ def values(self): self._ensure_valid() if self._value_wrapper is None: wrapper = WriteableFieldArray if self._write_enabled else ReadOnlyFieldArray - self._value_wrapper = wrapper(self._field, 'values') + self._value_wrapper = wrapper(self, 'values') return self._value_wrapper def __len__(self): @@ -2379,9 +2371,9 @@ def data(self): self._ensure_valid() if self._value_wrapper is None: if self._write_enabled: - self._value_wrapper = WriteableFieldArray(self._field, 'values') + self._value_wrapper = WriteableFieldArray(self, 'values') else: - self._value_wrapper = ReadOnlyFieldArray(self._field, 'values') + self._value_wrapper = ReadOnlyFieldArray(self, 'values') return self._value_wrapper def is_sorted(self): @@ -2586,31 +2578,17 @@ def data(self): self._ensure_valid() if self._value_wrapper is None: if self._write_enabled: - self._value_wrapper = WriteableFieldArray(self._field, 'values') + self._value_wrapper = WriteableFieldArray(self, 'values') else: - self._value_wrapper = ReadOnlyFieldArray(self._field, 'values') + self._value_wrapper = ReadOnlyFieldArray(self, 'values') return self._value_wrapper - @data.setter - def data(self, FieldArray): - """ - Setting the Field Array (data interface) directly. This can also associate field with an existing field array to enable a view. - """ - self._value_wrapper = FieldArray - if self.is_view(): - self.data.register_reference(self) - @property def filter(self): - if self._filter_wrapper is None: + if self._filter_wrapper is None: # poential returns: raise error or return a full-index array return None else: - return self._filter_wrapper[:] - return self._filter - - @filter.setter - def filter(self, filter_h5group): - self._filter_wrapper = WriteableFieldArray(filter_h5group, 'values') + return self._filter_wrapper def is_view(self): """ @@ -2621,23 +2599,30 @@ def is_view(self): else: return False - def __getitem__(self, item): - if self._filter_wrapper != None: - data_filter = self._filter_wrapper[:] - return self.data[item][data_filter] + def attach(self, view): + self._view_refs.append(view) + + def detach(self, view=None): + if view is None: # detach all + self._view_refs.clear() else: - return self.data[item] + self._view_refs.remove(view) + + def notify(self, msg=None): + for view in self._view_refs: + view.update(self, msg) - def concrete_reference(self): - if not self.is_view(): - raise ValueError("This field is already a concreted field.") + def update(self, subject, msg=None): + if isinstance(subject, (WriteableFieldArray, WriteableIndexedFieldArray)): + self.notify(msg) + self.detach() - self.data.detach_reference(self) # notice field array - print(self.name) - del self.dataframe[self.name] # notice dataframe - concrete_field = self.create_like(self.dataframe, self.name) # create - concrete_field.data.write(self[:]) # write data - return concrete_field + if isinstance(subject, HDF5Field): + if msg == 'write' or msg == 'clear': + if self.is_view(): + del self.dataframe[self.name] # notice dataframe + concrete_field = self.create_like(self.dataframe, self.name) # create + concrete_field.data.write(self.data[:]) # write data def is_sorted(self): """ @@ -2972,9 +2957,9 @@ def data(self): self._ensure_valid() if self._value_wrapper is None: if self._write_enabled: - self._value_wrapper = WriteableFieldArray(self._field, 'values') + self._value_wrapper = WriteableFieldArray(self, 'values') else: - self._value_wrapper = ReadOnlyFieldArray(self._field, 'values') + self._value_wrapper = ReadOnlyFieldArray(self, 'values') return self._value_wrapper def is_sorted(self): @@ -3263,9 +3248,9 @@ def data(self): self._ensure_valid() if self._value_wrapper is None: if self._write_enabled: - self._value_wrapper = WriteableFieldArray(self._field, 'values') + self._value_wrapper = WriteableFieldArray(self, 'values') else: - self._value_wrapper = ReadOnlyFieldArray(self._field, 'values') + self._value_wrapper = ReadOnlyFieldArray(self, 'values') return self._value_wrapper def is_sorted(self): From dfb36ab96d9c7beefcd8213265621f9adb43495e Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 18 May 2022 20:40:21 +0100 Subject: [PATCH 174/181] fixed the data[:] for indexed string fields fixed unittest errors --- exetera/core/dataframe.py | 48 +++---- exetera/core/fields.py | 288 ++++++++++++++++++++++++++++---------- tests/test_dataframe.py | 9 +- 3 files changed, 243 insertions(+), 102 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 2ba1c184..0c1981c7 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -119,7 +119,16 @@ def _add_view(self, field: fld.Field, filter: np.ndarray = None): # add view if isinstance(field, fld.NumericField): view = fld.NumericField(field._session, field._field, self, write_enabled=True) - + elif isinstance(field, fld.CategoricalField): + view = fld.CategoricalField(field._session, field._field, self, write_enabled=True) + elif isinstance(field, fld.TimestampField): + view = fld.TimestampField(field._session, field._field, self, write_enabled=True) + elif isinstance(field, fld.FixedStringField): + view = fld.FixedStringField(field._session, field._field, self, write_enabled=True) + elif isinstance(field, fld.IndexedStringField): + view = fld.IndexedStringField(field._session, field._field, self, write_enabled=True) + + field.attach(view) self._columns[view.name] = view # add filter @@ -138,6 +147,7 @@ def _add_view(self, field: fld.Field, filter: np.ndarray = None): filter_field = filter_field.astype(nformat) filter_field.data.clear() filter_field.data.write(filter) + view._filter_wrapper = filter_field.data return self._columns[view.name] @@ -552,7 +562,7 @@ def apply_filter(self, filter_to_apply, ddf=None): :returns: a dataframe contains all the fields filterd, self if ddf is not set """ filter_to_apply_ = val.validate_filter(filter_to_apply) - if ddf is not None: + if ddf is not None and ddf is not self: if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") filter_to_apply_ = filter_to_apply_.nonzero()[0] @@ -587,34 +597,20 @@ def apply_index(self, index_to_apply, ddf=None): :param ddf: optional- the destination data frame :returns: a dataframe contains all the fields re-indexed, self if ddf is not set """ - if ddf is not None: + if ddf is not None and ddf is not self: if not isinstance(ddf, DataFrame): raise TypeError("The destination object must be an instance of DataFrame.") - if ddf == self: - val.validate_all_field_length_in_df(self) - for field in self._columns.values(): + for name, field in self._columns.items(): + # newfld = field.create_like(ddf, name) + # field.apply_index(index_to_apply, target=newfld) + ddf._add_view(field, index_to_apply) + return ddf + else: + val.validate_all_field_length_in_df(self) - if ddf == self: - field.apply_index(index_to_apply, in_place=True) - else: - newfld = field.create_like(ddf, field.name) - field.apply_index(index_to_apply, target=newfld) - else: # - nformat = 'int32' if index_to_apply[-1] < 2 ** 31 - 1 else 'int64' for field in self._columns.values(): - if field.name in self._filters_grp.keys(): - flt_fld = fld.NumericField(self._dataset.session, self._filters_grp[field.name], self, - write_enabled=True) - if nformat not in flt_fld._fieldtype: - flt_fld = flt_fld.astype(nformat) - flt_fld.data.clear() - flt_fld.data.write(index_to_apply) - else: - fld.numeric_field_constructor(self._dataset.session, self._filters_grp, field.name, nformat) - flt_fld = fld.NumericField(self._dataset.session, self._filters_grp[field.name], self, - write_enabled=True) - flt_fld.data.write(index_to_apply) - field.filter = flt_fld._field + field.apply_index(index_to_apply, in_place=True) + return self def sort_values(self, by: Union[str, List[str]], ddf: DataFrame = None, axis=0, ascending=True, kind='stable'): diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 442bde01..49d23915 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -58,6 +58,7 @@ def __init__(self, session, group, dataframe, write_enabled=False): self._filter_wrapper = None self._view_refs = list() + @property def valid(self): """ @@ -116,6 +117,13 @@ def indexed(self): self._ensure_valid() return False + @property + def filter(self): + if self._filter_wrapper is None: # poential returns: raise error or return a full-index array + return None + else: + return self._filter_wrapper + def __bool__(self): # this method is required to prevent __len__ being called on derived methods when fields are queried as # if f: @@ -146,7 +154,14 @@ def _ensure_valid(self): if not self._valid_reference: raise ValueError("This field no longer refers to a valid underlying field object") - + def is_view(self): + """ + Return if the dataframe's name matches the field h5group path; if not, means this field is a view. + """ + if self._field.name[1:1+len(self.dataframe.name)] != self.dataframe.name: + return True + else: + return False class MemoryField(Field): @@ -207,6 +222,10 @@ def indexed(self): """ return False + @property + def filter(self): + return None + def __bool__(self): # this method is required to prevent __len__ being called on derived methods when fields are queried as # if f: @@ -293,7 +312,6 @@ def __init__(self, field, dataset_name): self._field = field._field # HDF5 group instance self._name = dataset_name self._dataset = self._field[dataset_name] - self._references = list() def __len__(self): """ @@ -311,11 +329,12 @@ def dtype(self): return self._dataset.dtype def __getitem__(self, item): - if self._field_instance.filter is not None: + if self._field_instance.filter is not None and not isinstance(self._field_instance, IndexedStringField): mask = self._field_instance.filter[:] - return self._dataset[mask][item] # note: mask before item + data = self._dataset[:][mask] # as HDF5 does not support unordered mask + return data[item] else: - return self._dataset[item] + return self._dataset[:][item] # as HDF5 does not support unordered mask, not efficient def __setitem__(self, key, value): self._dataset[key] = value @@ -475,16 +494,17 @@ def complete(self): class ReadOnlyIndexedFieldArray: - def __init__(self, field, indices, values): + def __init__(self, chunksize, indices, values, field): """ :param field: Field to use :param indices: Indices for numpy array :param values: Values for numpy array :return: None """ - self._field = field + self._chunksize = chunksize self._indices = indices self._values = values + self._field_instance = field def __len__(self): """ @@ -515,27 +535,49 @@ def __getitem__(self, item): """ try: if isinstance(item, slice): - start = item.start if item.start is not None else 0 - stop = item.stop if item.stop is not None else len(self._indices) - 1 - step = item.step - # TODO: validate slice - index = self._indices[start:stop + 1] - bytestr = self._values[index[0]:index[-1]] - results = [None] * (len(index) - 1) - startindex = self._indices[start] - for ir in range(len(results)): - results[ir] = \ - bytestr[index[ir] - np.int64(startindex): - index[ir + 1] - np.int64(startindex)].tobytes().decode() - return results + if self._field_instance.filter is None: + start = item.start if item.start is not None else 0 + stop = item.stop if item.stop is not None else len(self._indices) - 1 + step = item.step + # TODO: validate slice + index = self._indices[start:stop + 1] + bytestr = self._values[index[0]:index[-1]] + results = [None] * (len(index) - 1) + startindex = self._indices[start] + for ir in range(len(results)): + results[ir] = \ + bytestr[index[ir] - np.int64(startindex): + index[ir + 1] - np.int64(startindex)].tobytes().decode() + return results + else: + mask = self._field_instance.filter[:] + index_s = self._indices[mask] + index_e = self._indices[mask+1] + results = [None] * len(mask) + for ir in range(len(results)): + results[ir] = self._values[index_s[ir]: index_e[ir]] + return results[item] + elif isinstance(item, int): - if item >= len(self._indices) - 1: - raise ValueError("index is out of range") - start, stop = self._indices[item:item + 2] - if start == stop: - return '' - value = self._values[start:stop].tobytes().decode() - return value + if self._field_instance.filter is None: + if item >= len(self._indices) - 1: + raise ValueError("index is out of range") + start, stop = self._indices[item:item + 2] + if start == stop: + return '' + value = self._values[start:stop].tobytes().decode() + return value + else: + mask = self._field_instance.filter[:] + if item >= len(mask) - 1: + raise ValueError("index is out of range") + index_s = self._indices[mask] + index_e = self._indices[mask + 1] + results = [None] * len(mask) + for ir in range(len(results)): + results[ir] = self._values[index_s[ir]: index_e[ir]] + return results[item] + except Exception as e: print("{}: unexpected exception {}".format(self._field.name, e)) raise @@ -574,7 +616,7 @@ def complete(self): class WriteableIndexedFieldArray: - def __init__(self, chunksize, indices, values): + def __init__(self, chunksize, indices, values, field): """ :param: chunksize: Size of each chunk :param indices: Numpy array of indices @@ -592,6 +634,10 @@ def __init__(self, chunksize, indices, values): self._index_index = 0 self._value_index = 0 + self._field_instance = field + + + def __len__(self): """ Length of field @@ -619,32 +665,48 @@ def __getitem__(self, item): """ try: if isinstance(item, slice): - start = item.start if item.start is not None else 0 - stop = item.stop if item.stop is not None else len(self._indices) - 1 - step = item.step - # TODO: validate slice - - index = self._indices[start:stop + 1] - if len(index) == 0: - return [] - bytestr = self._values[index[0]:index[-1]] - results = [None] * (len(index) - 1) - startindex = self._indices[start] - rmax = min(len(results), stop - start) - for ir in range(rmax): - rbytes = bytestr[index[ir] - np.int64(startindex): - index[ir + 1] - np.int64(startindex)].tobytes() - rstr = rbytes.decode() - results[ir] = rstr - return results + if self._field_instance.filter is None: + start = item.start if item.start is not None else 0 + stop = item.stop if item.stop is not None else len(self._indices) - 1 + step = item.step + # TODO: validate slice + index = self._indices[start:stop + 1] + bytestr = self._values[index[0]:index[-1]] + results = [None] * (len(index) - 1) + startindex = self._indices[start] + for ir in range(len(results)): + results[ir] = \ + bytestr[index[ir] - np.int64(startindex): + index[ir + 1] - np.int64(startindex)].tobytes().decode() + return results + else: + mask = self._field_instance.filter[:] + index_s = self._indices[mask] + index_e = self._indices[mask + 1] + results = [None] * len(mask) + for ir in range(len(results)): + results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() + return results[item] + elif isinstance(item, int): - if item >= len(self._indices) - 1: - raise ValueError("index is out of range") - start, stop = self._indices[item:item + 2] - if start == stop: - return '' - value = self._values[start:stop].tobytes().decode() - return value + if self._field_instance.filter is None: + if item >= len(self._indices) - 1: + raise ValueError("index is out of range") + start, stop = self._indices[item:item + 2] + if start == stop: + return '' + value = self._values[start:stop].tobytes().decode() + return value + else: + mask = self._field_instance.filter[:] + if item >= len(mask) - 1: + raise ValueError("index is out of range") + index_s = self._indices[mask] + index_e = self._indices[mask + 1] + results = [None] * len(mask) + for ir in range(len(results)): + results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() + return results[item] except Exception as e: print(e) raise @@ -774,7 +836,7 @@ def data(self): :return: WriteableIndexedFieldArray """ if self._data_wrapper is None: - self._data_wrapper = WriteableIndexedFieldArray(self._chunksize, self.indices, self.values) + self._data_wrapper = WriteableIndexedFieldArray(self._chunksize, self.indices, self.values, self) return self._data_wrapper def is_sorted(self): @@ -2100,9 +2162,34 @@ def data(self): if self._data_wrapper is None: wrapper = \ WriteableIndexedFieldArray if self._write_enabled else ReadOnlyIndexedFieldArray - self._data_wrapper = wrapper(self.chunksize, self.indices, self.values) + self._data_wrapper = wrapper(self.chunksize, self.indices, self.values, self) return self._data_wrapper + def attach(self, view): + self._view_refs.append(view) + + def detach(self, view=None): + if view is None: # detach all + self._view_refs.clear() + else: + self._view_refs.remove(view) + + def notify(self, msg=None): + for view in self._view_refs: + view.update(self, msg) + + def update(self, subject, msg=None): + if isinstance(subject, (WriteableFieldArray, WriteableIndexedFieldArray)): + self.notify(msg) + self.detach() + + if isinstance(subject, HDF5Field): + if msg == 'write' or msg == 'clear': + if self.is_view(): + del self.dataframe[self.name] # notice dataframe + concrete_field = self.create_like(self.dataframe, self.name) # create + concrete_field.data.write(self.data[:]) # write data + def is_sorted(self): """ Returns if data in field is sorted @@ -2376,6 +2463,31 @@ def data(self): self._value_wrapper = ReadOnlyFieldArray(self, 'values') return self._value_wrapper + def attach(self, view): + self._view_refs.append(view) + + def detach(self, view=None): + if view is None: # detach all + self._view_refs.clear() + else: + self._view_refs.remove(view) + + def notify(self, msg=None): + for view in self._view_refs: + view.update(self, msg) + + def update(self, subject, msg=None): + if isinstance(subject, (WriteableFieldArray, WriteableIndexedFieldArray)): + self.notify(msg) + self.detach() + + if isinstance(subject, HDF5Field): + if msg == 'write' or msg == 'clear': + if self.is_view(): + del self.dataframe[self.name] # notice dataframe + concrete_field = self.create_like(self.dataframe, self.name) # create + concrete_field.data.write(self.data[:]) # write data + def is_sorted(self): """ Returns if data in field is sorted @@ -2583,22 +2695,6 @@ def data(self): self._value_wrapper = ReadOnlyFieldArray(self, 'values') return self._value_wrapper - @property - def filter(self): - if self._filter_wrapper is None: # poential returns: raise error or return a full-index array - return None - else: - return self._filter_wrapper - - def is_view(self): - """ - Return if the dataframe's name matches the field h5group path; if not, means this field is a view. - """ - if self._field.name[1:1+len(self.dataframe.name)] != self.dataframe.name: - return True - else: - return False - def attach(self, view): self._view_refs.append(view) @@ -2962,6 +3058,31 @@ def data(self): self._value_wrapper = ReadOnlyFieldArray(self, 'values') return self._value_wrapper + def attach(self, view): + self._view_refs.append(view) + + def detach(self, view=None): + if view is None: # detach all + self._view_refs.clear() + else: + self._view_refs.remove(view) + + def notify(self, msg=None): + for view in self._view_refs: + view.update(self, msg) + + def update(self, subject, msg=None): + if isinstance(subject, (WriteableFieldArray, WriteableIndexedFieldArray)): + self.notify(msg) + self.detach() + + if isinstance(subject, HDF5Field): + if msg == 'write' or msg == 'clear': + if self.is_view(): + del self.dataframe[self.name] # notice dataframe + concrete_field = self.create_like(self.dataframe, self.name) # create + concrete_field.data.write(self.data[:]) # write data + def is_sorted(self): """ Returns if data in field is sorted @@ -3253,6 +3374,31 @@ def data(self): self._value_wrapper = ReadOnlyFieldArray(self, 'values') return self._value_wrapper + def attach(self, view): + self._view_refs.append(view) + + def detach(self, view=None): + if view is None: # detach all + self._view_refs.clear() + else: + self._view_refs.remove(view) + + def notify(self, msg=None): + for view in self._view_refs: + view.update(self, msg) + + def update(self, subject, msg=None): + if isinstance(subject, (WriteableFieldArray, WriteableIndexedFieldArray)): + self.notify(msg) + self.detach() + + if isinstance(subject, HDF5Field): + if msg == 'write' or msg == 'clear': + if self.is_view(): + del self.dataframe[self.name] # notice dataframe + concrete_field = self.create_like(self.dataframe, self.name) # create + concrete_field.data.write(self.data[:]) # write data + def is_sorted(self): """ Returns if data in field is sorted diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 6aaa1dd6..1780a65b 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -1340,7 +1340,7 @@ def test_apply_filter(self, creator, name, kwargs, data): self.df.apply_filter(filter_to_apply, ddf= view_df) for field in view_df.values(): self.assertTrue(field.is_view()) # field is a view - self.assertListEqual(data[filter_to_apply].tolist(), field[:].tolist() ) # filtered + self.assertListEqual(data[filter_to_apply].tolist(), field.data[:].tolist() ) # filtered # view_df2 = self.ds.create_dataframe('view_df2') @@ -1348,7 +1348,7 @@ def test_apply_filter(self, creator, name, kwargs, data): self.df.apply_filter(filter_to_apply, ddf=view_df2) for field in view_df2.values(): self.assertTrue(field.is_view()) # field is a view - self.assertListEqual(data[filter_to_apply].tolist(), field[:].tolist()) # filtered + self.assertListEqual(data[filter_to_apply].tolist(), field.data[:].tolist()) # filtered def test_remove_filter(self): pass @@ -1360,10 +1360,9 @@ def test_concrete_field(self, creator, name, kwargs, data): view_df = self.ds.create_dataframe('view_df') filter_to_apply = np.asarray([i % 2 == 0 for i in data]) self.df.apply_filter(filter_to_apply, ddf=view_df) - print('a',view_df.keys()) for field in view_df.values(): self.assertTrue(field.is_view()) # field is a view - self.assertListEqual(data[filter_to_apply].tolist(), field[:].tolist()) # filtered + self.assertListEqual(data[filter_to_apply].tolist(), field.data[:].tolist()) # filtered new_data = data + 1 f.data.clear() @@ -1371,7 +1370,7 @@ def test_concrete_field(self, creator, name, kwargs, data): for field in view_df.values(): self.assertFalse(field.is_view()) - self.assertListEqual(data[filter_to_apply].tolist(), field[:].tolist()) # filtered + self.assertListEqual(data[filter_to_apply].tolist(), field.data[:].tolist()) # filtered From fe9cee832e00ca923bf2c7011ea7dbf072e74247 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 23 May 2022 16:39:33 +0100 Subject: [PATCH 175/181] update Eric's comments --- exetera/core/abstract_types.py | 12 +++++ exetera/core/dataframe.py | 44 +---------------- exetera/core/fields.py | 87 +++++++++++++++++++++------------- exetera/core/utils.py | 7 +++ tests/test_dataframe.py | 16 +++++-- 5 files changed, 87 insertions(+), 79 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index ad98763e..d9222986 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -500,14 +500,26 @@ def ordered_merge_right(self, right_on, left_on, class SubjectObserver(ABC): def attach(self, observer): + """ + Attach the observer (view) to the subject (field). + """ raise NotImplementedError() def detach(self, observer): + """ + Detach the observer (view) from the subject (field), this is to remove the association between observer with subject. + """ raise NotImplementedError() def notify(self, msg=None): + """ + Called by the Subject to notify the observer on something. + """ raise NotImplementedError() def update(self, subject, msg=None): + """ + Called inside the observer, to perform actions based on subject and message type. + """ raise NotImplementedError() diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index 0c1981c7..e8016e33 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -117,17 +117,7 @@ def _add_view(self, field: fld.Field, filter: np.ndarray = None): """ # add view - if isinstance(field, fld.NumericField): - view = fld.NumericField(field._session, field._field, self, write_enabled=True) - elif isinstance(field, fld.CategoricalField): - view = fld.CategoricalField(field._session, field._field, self, write_enabled=True) - elif isinstance(field, fld.TimestampField): - view = fld.TimestampField(field._session, field._field, self, write_enabled=True) - elif isinstance(field, fld.FixedStringField): - view = fld.FixedStringField(field._session, field._field, self, write_enabled=True) - elif isinstance(field, fld.IndexedStringField): - view = fld.IndexedStringField(field._session, field._field, self, write_enabled=True) - + view = type(field)(field._session, field._field, self, write_enabled=True) field.attach(view) self._columns[view.name] = view @@ -148,32 +138,10 @@ def _add_view(self, field: fld.Field, filter: np.ndarray = None): filter_field.data.clear() filter_field.data.write(filter) - view._filter_wrapper = filter_field.data + view._filter_wrapper = fld.ReadOnlyFieldArray(filter_field, 'values') # read-only return self._columns[view.name] - # def change_filter(self, field: fld.Field, filter: np.ndarray): - # """ - # - # :param field: - # :param filter: - # :return: - # """ - # pass - # - # def remove_filter(self, field: Union[str, fld.Field]): - # """ - # Remove filter from this dataframe specified by the field or field name. - # """ - # if not isinstance(field, str) and not isinstance(field, fld.Field): - # raise TypeError("The target field should be type field or string (name of the field in this dataframe).") - # - # name = field if isinstance(field, str) else field.name - # if name not in self._columns: - # raise ValueError("The target field is not in this dataframe.") - # else: - # del self._filters_grp[name] - def drop(self, name: str): """ @@ -338,14 +306,6 @@ def contains_field(self, field): return True return False - - def _get_filter_grp(self, field: Union[str, fld.Field]=None): - """ - Get a filter array specified by the field or field name. - """ - filter_name = '_filter' - return self._filters_grp[filter_name] - def __getitem__(self, name): """ Get a field stored by the field name. diff --git a/exetera/core/fields.py b/exetera/core/fields.py index fa5f5722..16a8b6eb 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -20,6 +20,7 @@ from exetera.core.data_writer import DataWriter from exetera.core import operations as ops from exetera.core import validation as val +from exetera.core import utils def isin(field:Field, test_elements:Union[list, set, np.ndarray]): @@ -158,10 +159,7 @@ def is_view(self): """ Return if the dataframe's name matches the field h5group path; if not, means this field is a view. """ - if self._field.name[1:1+len(self.dataframe.name)] != self.dataframe.name: - return True - else: - return False + return self._field.name[1:1+len(self.dataframe.name)] != self.dataframe.name class MemoryField(Field): @@ -264,9 +262,12 @@ def dtype(self): return self._dataset.dtype def __getitem__(self, item): - if self._field_instance.filter is not None: - mask = self._field_instance.filter[:] - return self._dataset[mask][item] # note: mask before item + if self._field_instance.filter is not None and not isinstance(self._field_instance, IndexedStringField): + mask = self._field_instance.filter[item] + if utils.is_sorted(mask): + return self._dataset[mask] + else: + return self._dataset[np.sort(mask)][np.argsort(mask)] else: return self._dataset[item] @@ -330,11 +331,13 @@ def dtype(self): def __getitem__(self, item): if self._field_instance.filter is not None and not isinstance(self._field_instance, IndexedStringField): - mask = self._field_instance.filter[:] - data = self._dataset[:][mask] # as HDF5 does not support unordered mask - return data[item] + mask = self._field_instance.filter[item] + if utils.is_sorted(mask): + return self._dataset[mask] + else: + return self._dataset[np.sort(mask)][np.argsort(mask)] else: - return self._dataset[:][item] # as HDF5 does not support unordered mask, not efficient + return self._dataset[item] def __setitem__(self, key, value): self._dataset[key] = value @@ -549,13 +552,22 @@ def __getitem__(self, item): index[ir + 1] - np.int64(startindex)].tobytes().decode() return results else: - mask = self._field_instance.filter[:] - index_s = self._indices[mask] - index_e = self._indices[mask + 1] - results = [None] * len(mask) - for ir in range(len(results)): - results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() - return results[item] + mask = self._field_instance.filter[item] + if utils.is_sorted(mask): + index_s = self._indices[mask] + index_e = self._indices[mask + 1] + results = [None] * len(mask) + for ir in range(len(results)): + results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() + else: + s_mask = np.sort(mask) + orignal_order = np.argsort(mask) + index_s = self._indices[s_mask][orignal_order] + index_e = self._indices[s_mask + 1][orignal_order] + results = [None] * len(s_mask) + for ir in range(len(results)): + results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() + return results elif isinstance(item, int): if self._field_instance.filter is None: @@ -570,12 +582,11 @@ def __getitem__(self, item): mask = self._field_instance.filter[:] if item >= len(mask) - 1: raise ValueError("index is out of range") + mask = mask[item] index_s = self._indices[mask] index_e = self._indices[mask + 1] - results = [None] * len(mask) - for ir in range(len(results)): - results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() - return results[item] + results = self._values[index_s: index_e].tobytes().decode() + return results def __setitem__(self, key, value): raise PermissionError("This field was created read-only; call .writeable() " @@ -674,13 +685,22 @@ def __getitem__(self, item): index[ir + 1] - np.int64(startindex)].tobytes().decode() return results else: - mask = self._field_instance.filter[:] - index_s = self._indices[mask] - index_e = self._indices[mask + 1] - results = [None] * len(mask) - for ir in range(len(results)): - results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() - return results[item] + mask = self._field_instance.filter[item] + if utils.is_sorted(mask): + index_s = self._indices[mask] + index_e = self._indices[mask + 1] + results = [None] * len(mask) + for ir in range(len(results)): + results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() + else: + s_mask = np.sort(mask) + orignal_order = np.argsort(mask) + index_s = self._indices[s_mask][orignal_order] + index_e = self._indices[s_mask + 1][orignal_order] + results = [None] * len(s_mask) + for ir in range(len(results)): + results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() + return results elif isinstance(item, int): if self._field_instance.filter is None: @@ -695,12 +715,11 @@ def __getitem__(self, item): mask = self._field_instance.filter[:] if item >= len(mask) - 1: raise ValueError("index is out of range") + mask = mask[item] index_s = self._indices[mask] index_e = self._indices[mask + 1] - results = [None] * len(mask) - for ir in range(len(results)): - results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() - return results[item] + results = self._values[index_s: index_e].tobytes().decode() + return results def __setitem__(self, key, value): raise PermissionError("IndexedStringField instances cannot be edited via array syntax;" @@ -2138,7 +2157,7 @@ class IndexedStringField(HDF5Field): def __init__(self, session, group, dataframe, write_enabled=False): super().__init__(session, group, dataframe, write_enabled=write_enabled) self._session = session - self._dataframe = None + self._dataframe = dataframe self._data_wrapper = None self._index_wrapper = None self._value_wrapper = None diff --git a/exetera/core/utils.py b/exetera/core/utils.py index 8fb0a132..79c84eba 100644 --- a/exetera/core/utils.py +++ b/exetera/core/utils.py @@ -214,3 +214,10 @@ def guess_encoding(filename): else: return "utf-8" +def is_sorted(array): + """ + Check if an array is ordered. + """ + if len(array) < 2: + return True + return np.all(array[:-1] <= array[1:]) \ No newline at end of file diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 1780a65b..046de1b2 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -1329,10 +1329,19 @@ def test_raise_errors(self): df.describe(exclude=['num', 'num2', 'ts1']) self.assertTrue(isinstance(context.exception, ValueError)) -class TestDataFrameFilter(SessionTestCase): +class TestDataFrameView(SessionTestCase): + + def test_get_view(self): + """ + Test dataframe.view, field.is_view, register + """ + pass @parameterized.expand([("create_numeric", "f_i8", {"nformat": "int8"}, [i for i in range(20)] ),]) def test_apply_filter(self, creator, name, kwargs, data): + """ + Test dataframe.apply_field, field.data[:] + """ data = np.asarray(data) f = self.setup_field(self.df, creator, name, (), kwargs, data) view_df = self.ds.create_dataframe('view_df') @@ -1350,11 +1359,12 @@ def test_apply_filter(self, creator, name, kwargs, data): self.assertTrue(field.is_view()) # field is a view self.assertListEqual(data[filter_to_apply].tolist(), field.data[:].tolist()) # filtered - def test_remove_filter(self): - pass @parameterized.expand([("create_numeric", "f_i8", {"nformat": "int8"}, [i for i in range(20)]), ]) def test_concrete_field(self, creator, name, kwargs, data): + """ + Test dataframe.apply_filter, field.detach, field.notify, field.update + """ data = np.asarray(data) f = self.setup_field(self.df, creator, name, (), kwargs, data) view_df = self.ds.create_dataframe('view_df') From c1ad9ba0097010ac347fc8ee3cab8b6a8d969aeb Mon Sep 17 00:00:00 2001 From: deng113jie Date: Mon, 23 May 2022 16:42:35 +0100 Subject: [PATCH 176/181] minor update --- exetera/core/fields.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 16a8b6eb..bc863170 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -579,10 +579,9 @@ def __getitem__(self, item): value = self._values[start:stop].tobytes().decode() return value else: - mask = self._field_instance.filter[:] - if item >= len(mask) - 1: + if item >= len(self._field_instance.filter) - 1: raise ValueError("index is out of range") - mask = mask[item] + mask = self._field_instance.filter[item] index_s = self._indices[mask] index_e = self._indices[mask + 1] results = self._values[index_s: index_e].tobytes().decode() @@ -712,10 +711,9 @@ def __getitem__(self, item): value = self._values[start:stop].tobytes().decode() return value else: - mask = self._field_instance.filter[:] - if item >= len(mask) - 1: + if item >= len(self._field_instance.filter) - 1: raise ValueError("index is out of range") - mask = mask[item] + mask = self._field_instance.filter[item] index_s = self._indices[mask] index_e = self._indices[mask + 1] results = self._values[index_s: index_e].tobytes().decode() From 80c033941357eb32792ae719aa80f26e4d668cd3 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 24 May 2022 17:49:03 +0100 Subject: [PATCH 177/181] update unittests for dataframe view --- exetera/core/dataframe.py | 4 +- exetera/core/fields.py | 18 +++--- tests/test_dataframe.py | 112 ++++++++++++++++++++++++-------------- 3 files changed, 84 insertions(+), 50 deletions(-) diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index e8016e33..bf71c8a8 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -123,7 +123,9 @@ def _add_view(self, field: fld.Field, filter: np.ndarray = None): # add filter if filter is not None: - nformat = 'int32' if filter[-1] < 2 ** 31 - 1 else 'int64' + nformat = 'int32' + if len(filter) > 0 and np.max(filter) >= 2**31 - 1: + nformat = 'int64' filter_name = view.name if filter_name not in self._filters_grp.keys(): fld.numeric_field_constructor(self._dataset.session, self._filters_grp, filter_name, nformat) diff --git a/exetera/core/fields.py b/exetera/core/fields.py index bc863170..7e09bc5d 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -267,7 +267,7 @@ def __getitem__(self, item): if utils.is_sorted(mask): return self._dataset[mask] else: - return self._dataset[np.sort(mask)][np.argsort(mask)] + return self._dataset[np.sort(mask)][mask] else: return self._dataset[item] @@ -335,7 +335,7 @@ def __getitem__(self, item): if utils.is_sorted(mask): return self._dataset[mask] else: - return self._dataset[np.sort(mask)][np.argsort(mask)] + return self._dataset[np.sort(mask)][mask] else: return self._dataset[item] @@ -561,9 +561,9 @@ def __getitem__(self, item): results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() else: s_mask = np.sort(mask) - orignal_order = np.argsort(mask) - index_s = self._indices[s_mask][orignal_order] - index_e = self._indices[s_mask + 1][orignal_order] + #orignal_order = np.argsort(mask) + index_s = self._indices[s_mask][mask] + index_e = self._indices[s_mask + 1][mask] results = [None] * len(s_mask) for ir in range(len(results)): results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() @@ -672,6 +672,8 @@ def __getitem__(self, item): if self._field_instance.filter is None: start = item.start if item.start is not None else 0 stop = item.stop if item.stop is not None else len(self._indices) - 1 + if stop <= 0: # empty field + return [] step = item.step # TODO: validate slice index = self._indices[start:stop + 1] @@ -693,9 +695,9 @@ def __getitem__(self, item): results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() else: s_mask = np.sort(mask) - orignal_order = np.argsort(mask) - index_s = self._indices[s_mask][orignal_order] - index_e = self._indices[s_mask + 1][orignal_order] + #orignal_order = np.argsort(mask) + index_s = self._indices[s_mask][mask] + index_e = self._indices[s_mask + 1][mask] results = [None] * len(s_mask) for ir in range(len(results)): results[ir] = self._values[index_s[ir]: index_e[ir]].tobytes().decode() diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 046de1b2..1dc104b6 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -913,6 +913,7 @@ def test_sort_values_on_other_df(self): self.assertListEqual(list(val), df['val'].data[:].tolist()) self.assertListEqual(list(val2), df['val2'].data[:]) + self.assertListEqual([b'a', b'b', b'c', b'd', b'e'], ddf['idx'].data[:].tolist()) self.assertListEqual([10, 30, 50, 40, 20], ddf['val'].data[:].tolist()) self.assertListEqual(['a', 'bbb', 'ccccc', 'dddd', 'ee'], ddf['val2'].data[:]) @@ -1331,56 +1332,85 @@ def test_raise_errors(self): class TestDataFrameView(SessionTestCase): - def test_get_view(self): - """ - Test dataframe.view, field.is_view, register - """ - pass - - @parameterized.expand([("create_numeric", "f_i8", {"nformat": "int8"}, [i for i in range(20)] ),]) - def test_apply_filter(self, creator, name, kwargs, data): + @parameterized.expand(DEFAULT_FIELD_DATA) + def test_get_view(self, creator, name, kwargs, data): """ - Test dataframe.apply_field, field.data[:] + Test dataframe.view, field.is_view, apply_filter, and apply_index """ - data = np.asarray(data) f = self.setup_field(self.df, creator, name, (), kwargs, data) - view_df = self.ds.create_dataframe('view_df') - filter_to_apply = np.asarray([i%2 == 0 for i in data]) - self.df.apply_filter(filter_to_apply, ddf= view_df) - for field in view_df.values(): - self.assertTrue(field.is_view()) # field is a view - self.assertListEqual(data[filter_to_apply].tolist(), field.data[:].tolist() ) # filtered - - # - view_df2 = self.ds.create_dataframe('view_df2') - filter_to_apply &= np.asarray([i < 10 for i in data]) - self.df.apply_filter(filter_to_apply, ddf=view_df2) - for field in view_df2.values(): - self.assertTrue(field.is_view()) # field is a view - self.assertListEqual(data[filter_to_apply].tolist(), field.data[:].tolist()) # filtered - - - @parameterized.expand([("create_numeric", "f_i8", {"nformat": "int8"}, [i for i in range(20)]), ]) + if "nformat" in kwargs: + data = np.asarray(data, dtype=kwargs["nformat"]) + else: + data = np.asarray(data) + + view = self.df.view() + self.assertTrue(view[name].is_view()) + self.assertListEqual(data[:].tolist(), np.asarray(view[name].data[:]).tolist()) + + with self.subTest('All False:'): + df2 = self.ds.create_dataframe('df2') + d_filter = np.array([False]) + self.df.apply_filter(d_filter, df2) + self.assertTrue(df2[name].is_view()) + d_filter = np.nonzero(d_filter)[0] + self.assertListEqual(data[d_filter].tolist(), np.asarray(df2[name].data[:]).tolist()) + self.ds.drop('df2') + + with self.subTest('All True:'): + df2 = self.ds.create_dataframe('df2') + d_filter = np.array([True]*len(data)) + self.df.apply_filter(d_filter, df2) + self.assertTrue(df2[name].is_view()) + d_filter = np.nonzero(d_filter)[0] + self.assertListEqual(data[d_filter].tolist(), np.asarray(df2[name].data[:]).tolist()) + self.ds.drop('df2') + + with self.subTest('Ramdon T/F'): + df2 = self.ds.create_dataframe('df2') + d_filter = np.array([np.random.random()>=0.5 for i in range(len(data))]) + self.df.apply_filter(d_filter, df2) + self.assertTrue(df2[name].is_view()) + d_filter = np.nonzero(d_filter)[0] + self.assertListEqual(data[d_filter].tolist(), np.asarray(df2[name].data[:]).tolist()) + self.ds.drop('df2') + + with self.subTest('All Index:'): + df2 = self.ds.create_dataframe('df2') + d_filter = np.array([i for i in range(len(data))]) + self.df.apply_index(d_filter, df2) + self.assertTrue(df2[name].is_view()) + self.assertListEqual(data[d_filter].tolist(), np.asarray(df2[name].data[:]).tolist()) + self.ds.drop('df2') + + with self.subTest('Random Index:'): + df2 = self.ds.create_dataframe('df2') + d_filter = [] + for i in range(len(data)): + if np.random.random() >= 0.5: + d_filter.append(i) + d_filter = np.array(d_filter) + self.df.apply_index(d_filter, df2) + self.assertTrue(df2[name].is_view()) + self.assertListEqual(data[d_filter].tolist(), np.asarray(df2[name].data[:]).tolist()) + self.ds.drop('df2') + + @parameterized.expand(DEFAULT_FIELD_DATA) def test_concrete_field(self, creator, name, kwargs, data): """ - Test dataframe.apply_filter, field.detach, field.notify, field.update + Test field.attach, field.detach, field.notify, field.update """ - data = np.asarray(data) f = self.setup_field(self.df, creator, name, (), kwargs, data) - view_df = self.ds.create_dataframe('view_df') - filter_to_apply = np.asarray([i % 2 == 0 for i in data]) - self.df.apply_filter(filter_to_apply, ddf=view_df) - for field in view_df.values(): - self.assertTrue(field.is_view()) # field is a view - self.assertListEqual(data[filter_to_apply].tolist(), field.data[:].tolist()) # filtered - - new_data = data + 1 + if "nformat" in kwargs: + data = np.asarray(data, dtype=kwargs["nformat"]) + else: + data = np.asarray(data) + view = self.df.view() + self.assertTrue(view[name] in f._view_refs) # attached f.data.clear() - f.data.write(new_data) # data changed, view should be concreate automatically + self.assertListEqual([], np.asarray(f.data[:]).tolist()) + self.assertListEqual(data.tolist(), np.asarray(view[name].data[:]).tolist()) # notify and update + self.assertFalse(view[name] in f._view_refs) # detached - for field in view_df.values(): - self.assertFalse(field.is_view()) - self.assertListEqual(data[filter_to_apply].tolist(), field.data[:].tolist()) # filtered From 6cb1d3e367f90e41bc02a86af58b497d270b6301 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Tue, 24 May 2022 17:50:40 +0100 Subject: [PATCH 178/181] minor update --- tests/test_dataframe.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 1dc104b6..a942ceeb 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -1330,6 +1330,7 @@ def test_raise_errors(self): df.describe(exclude=['num', 'num2', 'ts1']) self.assertTrue(isinstance(context.exception, ValueError)) + class TestDataFrameView(SessionTestCase): @parameterized.expand(DEFAULT_FIELD_DATA) @@ -1410,8 +1411,3 @@ def test_concrete_field(self, creator, name, kwargs, data): self.assertListEqual([], np.asarray(f.data[:]).tolist()) self.assertListEqual(data.tolist(), np.asarray(view[name].data[:]).tolist()) # notify and update self.assertFalse(view[name] in f._view_refs) # detached - - - - - From e8cf7f22b21419f7a07fcbc4b2b46aa1245daa17 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Wed, 25 May 2022 17:23:31 +0100 Subject: [PATCH 179/181] add persistence over view so that view 1) has hdf5 group but no dataset 2) can be re-recognized in a new session --- exetera/core/abstract_types.py | 9 ++ exetera/core/dataframe.py | 53 ++++++----- exetera/core/fields.py | 162 +++++++++++++++++++++++++++------ tests/test_dataframe.py | 15 +++ 4 files changed, 190 insertions(+), 49 deletions(-) diff --git a/exetera/core/abstract_types.py b/exetera/core/abstract_types.py index d9222986..556ef915 100644 --- a/exetera/core/abstract_types.py +++ b/exetera/core/abstract_types.py @@ -508,6 +508,13 @@ def attach(self, observer): def detach(self, observer): """ Detach the observer (view) from the subject (field), this is to remove the association between observer with subject. + This method id called by the observer. + """ + raise NotImplementedError() + + def notify_deletion(self, observer=None): + """ + Delete the observer from the subject, but called from the subject side. """ raise NotImplementedError() @@ -523,3 +530,5 @@ def update(self, subject, msg=None): """ raise NotImplementedError() + + diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index bf71c8a8..d62e9e67 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -117,7 +117,8 @@ def _add_view(self, field: fld.Field, filter: np.ndarray = None): """ # add view - view = type(field)(field._session, field._field, self, write_enabled=True) + h5group = fld.base_view_contructor(field._session, self, field) + view = type(field)(field._session, h5group, self, write_enabled=True) field.attach(view) self._columns[view.name] = view @@ -140,7 +141,7 @@ def _add_view(self, field: fld.Field, filter: np.ndarray = None): filter_field.data.clear() filter_field.data.write(filter) - view._filter_wrapper = fld.ReadOnlyFieldArray(filter_field, 'values') # read-only + view._filter_index_wrapper = fld.ReadOnlyFieldArray(filter_field, 'values') # read-only return self._columns[view.name] @@ -524,17 +525,23 @@ def apply_filter(self, filter_to_apply, ddf=None): :returns: a dataframe contains all the fields filterd, self if ddf is not set """ filter_to_apply_ = val.validate_filter(filter_to_apply) - if ddf is not None and ddf is not self: - if not isinstance(ddf, DataFrame): - raise TypeError("The destination object must be an instance of DataFrame.") + ddf = self if ddf is None else ddf + if not isinstance(ddf, DataFrame): + raise TypeError("The destination object must be an instance of DataFrame.") + if ddf == self: + for field in self._columns.values(): + field.apply_filter(filter_to_apply_, in_place=True) + elif ddf.dataset == self.dataset: # another df in the same ds, create view filter_to_apply_ = filter_to_apply_.nonzero()[0] for name, field in self._columns.items(): + if name in ddf: + del ddf[name] ddf._add_view(field, filter_to_apply_) - return ddf - else: - for field in self._columns.values(): - field.apply_filter(filter_to_apply_, in_place=True) - return self + else: # another df in different ds, do hard copy + for name, field in self._columns.items(): + newfld = field.create_like(ddf, name) + field.apply_filter(filter_to_apply_, target=newfld) + return ddf def apply_index(self, index_to_apply, ddf=None): """ @@ -559,21 +566,23 @@ def apply_index(self, index_to_apply, ddf=None): :param ddf: optional- the destination data frame :returns: a dataframe contains all the fields re-indexed, self if ddf is not set """ - if ddf is not None and ddf is not self: - if not isinstance(ddf, DataFrame): - raise TypeError("The destination object must be an instance of DataFrame.") - for name, field in self._columns.items(): - # newfld = field.create_like(ddf, name) - # field.apply_index(index_to_apply, target=newfld) - ddf._add_view(field, index_to_apply) - return ddf - else: + ddf = self if ddf is None else ddf + if not isinstance(ddf, DataFrame): + raise TypeError("The destination object must be an instance of DataFrame.") + if ddf == self: # in_place val.validate_all_field_length_in_df(self) - for field in self._columns.values(): field.apply_index(index_to_apply, in_place=True) - return self - + elif ddf.dataset == self.dataset: # view + for name, field in self._columns.items(): + if name in ddf: + del ddf[name] + ddf._add_view(field, index_to_apply) + else: # hard copy + for name, field in self._columns.items(): + newfld = field.create_like(ddf, name) + field.apply_index(index_to_apply, target=newfld) + return ddf def sort_values(self, by: Union[str, List[str]], ddf: DataFrame = None, axis=0, ascending=True, kind='stable'): """ diff --git a/exetera/core/fields.py b/exetera/core/fields.py index 7e09bc5d..da46ec1c 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -56,7 +56,7 @@ def __init__(self, session, group, dataframe, write_enabled=False): self._value_wrapper = None self._valid_reference = True - self._filter_wrapper = None + self._filter_index_wrapper = None self._view_refs = list() @@ -120,10 +120,10 @@ def indexed(self): @property def filter(self): - if self._filter_wrapper is None: # poential returns: raise error or return a full-index array + if self._filter_index_wrapper is None: # poential returns: raise error or return a full-index array return None else: - return self._filter_wrapper + return self._filter_index_wrapper def __bool__(self): # this method is required to prevent __len__ being called on derived methods when fields are queried as @@ -159,7 +159,7 @@ def is_view(self): """ Return if the dataframe's name matches the field h5group path; if not, means this field is a view. """ - return self._field.name[1:1+len(self.dataframe.name)] != self.dataframe.name + return 'source_field' in self._field.attrs class MemoryField(Field): @@ -247,7 +247,11 @@ def apply_index(self, index_to_apply, dstfld=None): class ReadOnlyFieldArray: def __init__(self, field, dataset_name): self._field_instance = field - self._field = field._field + if 'source_field' in field._field.attrs: # is a view + data_h5group = field._field.file.get(field._field.attrs['source_field']) + else: + data_h5group = field._field + self._field = data_h5group # HDF5 group instance self._name = dataset_name self._dataset = self._field[dataset_name] @@ -310,7 +314,11 @@ def complete(self): class WriteableFieldArray: def __init__(self, field, dataset_name): self._field_instance = field - self._field = field._field # HDF5 group instance + if 'source_field' in field._field.attrs: # is a view + data_h5group = field._field.file.get(field._field.attrs['source_field']) + else: + data_h5group = field._field + self._field = data_h5group # HDF5 group instance self._name = dataset_name self._dataset = self._field[dataset_name] @@ -537,7 +545,7 @@ def __getitem__(self, item): :return: Item value from dataset """ if isinstance(item, slice): - if self._field_instance.filter is None: + if self._field_instance.filter is None: # This field is not a view so no filtered_index to deal with start = item.start if item.start is not None else 0 stop = item.stop if item.stop is not None else len(self._indices) - 1 step = item.step @@ -2149,6 +2157,18 @@ def timestamp_field_constructor(session, group, name, timestamp=None, chunksize= DataWriter.write(field, 'values', [], 0, 'float64') +def base_view_contructor(session, group, source): + """ + Constructor are for setup the hdf5 group that going to be a container for a view (rather than a field). + """ + if source.name in group: + msg = "Field '{}' already exists in group '{}'" + raise ValueError(msg.format(source.name, group)) + field = source.create_like(group, source.name) # copy other attributes + field._field.attrs['source_field'] = source._field.name + return field._field + + # HDF5 fields # =========== @@ -2215,21 +2235,40 @@ def detach(self, view=None): else: self._view_refs.remove(view) + def notify_deletion(self, view=None): + if view is None: # detach all + self._view_refs.clear() + else: + self._view_refs.remove(view) + def notify(self, msg=None): for view in self._view_refs: view.update(self, msg) def update(self, subject, msg=None): if isinstance(subject, (WriteableFieldArray, WriteableIndexedFieldArray)): + """ + This field is being notified by its own field array + It needs to notify other fields that it is about to change before the change goes ahead + """ self.notify(msg) - self.detach() + self.notify_deletion() if isinstance(subject, HDF5Field): + """ + This field is being notified by the field that owns the data that it has a view of + At present, the behavior is that it copies the data and then detaches from the view that notified it, as it + no longer has an observation relationship with that field + """ if msg == 'write' or msg == 'clear': if self.is_view(): - del self.dataframe[self.name] # notice dataframe - concrete_field = self.create_like(self.dataframe, self.name) # create - concrete_field.data.write(self.data[:]) # write data + field_data = self.data[:] + del self._field.attrs['source_field'] # del view attr + self._index_wrapper = None + self._value_wrapper = None + self._data_wrapper = None # re-init the field array + self._filter_index_wrapper = None # reset the filter + self.data.write(field_data) def is_sorted(self): """ @@ -2513,21 +2552,38 @@ def detach(self, view=None): else: self._view_refs.remove(view) + def notify_deletion(self, view=None): + if view is None: # detach all + self._view_refs.clear() + else: + self._view_refs.remove(view) + def notify(self, msg=None): for view in self._view_refs: view.update(self, msg) def update(self, subject, msg=None): if isinstance(subject, (WriteableFieldArray, WriteableIndexedFieldArray)): + """ + This field is being notified by its own field array + It needs to notify other fields that it is about to change before the change goes ahead + """ self.notify(msg) - self.detach() + self.notify_deletion() if isinstance(subject, HDF5Field): + """ + This field is being notified by the field that owns the data that it has a view of + At present, the behavior is that it copies the data and then detaches from the view that notified it, as it + no longer has an observation relationship with that field + """ if msg == 'write' or msg == 'clear': if self.is_view(): - del self.dataframe[self.name] # notice dataframe - concrete_field = self.create_like(self.dataframe, self.name) # create - concrete_field.data.write(self.data[:]) # write data + field_data = self.data[:] + del self._field.attrs['source_field'] # del view attr + self._value_wrapper = None # re-init the field array + self._filter_index_wrapper = None # reset the filter + self.data.write(field_data) def is_sorted(self): """ @@ -2745,21 +2801,39 @@ def detach(self, view=None): else: self._view_refs.remove(view) + def notify_deletion(self, view=None): + if view is None: # detach all + self._view_refs.clear() + else: + self._view_refs.remove(view) + def notify(self, msg=None): for view in self._view_refs: view.update(self, msg) def update(self, subject, msg=None): if isinstance(subject, (WriteableFieldArray, WriteableIndexedFieldArray)): + """ + This field is being notified by its own field array + It needs to notify other fields that it is about to change before the change goes ahead + """ self.notify(msg) - self.detach() + self.notify_deletion() if isinstance(subject, HDF5Field): + """ + This field is being notified by the field that owns the data that it has a view of + At present, the behavior is that it copies the data and then detaches from the view that notified it, as it + no longer has an observation relationship with that field + """ if msg == 'write' or msg == 'clear': if self.is_view(): - del self.dataframe[self.name] # notice dataframe - concrete_field = self.create_like(self.dataframe, self.name) # create - concrete_field.data.write(self.data[:]) # write data + field_data = self.data[:] + del self._field.attrs['source_field'] # del view attr + self._value_wrapper = None # re-init the field array + self._filter_index_wrapper = None # reset the filter + self.data.write(field_data) + def is_sorted(self): """ @@ -3110,21 +3184,38 @@ def detach(self, view=None): else: self._view_refs.remove(view) + def notify_deletion(self, view=None): + if view is None: # detach all + self._view_refs.clear() + else: + self._view_refs.remove(view) + def notify(self, msg=None): for view in self._view_refs: view.update(self, msg) def update(self, subject, msg=None): if isinstance(subject, (WriteableFieldArray, WriteableIndexedFieldArray)): + """ + This field is being notified by its own field array + It needs to notify other fields that it is about to change before the change goes ahead + """ self.notify(msg) - self.detach() + self.notify_deletion() if isinstance(subject, HDF5Field): + """ + This field is being notified by the field that owns the data that it has a view of + At present, the behavior is that it copies the data and then detaches from the view that notified it, as it + no longer has an observation relationship with that field + """ if msg == 'write' or msg == 'clear': if self.is_view(): - del self.dataframe[self.name] # notice dataframe - concrete_field = self.create_like(self.dataframe, self.name) # create - concrete_field.data.write(self.data[:]) # write data + field_data = self.data[:] + del self._field.attrs['source_field'] # del view attr + self._value_wrapper = None # re-init the field array + self._filter_index_wrapper = None # reset the filter + self.data.write(field_data) def is_sorted(self): """ @@ -3456,21 +3547,38 @@ def detach(self, view=None): else: self._view_refs.remove(view) + def notify_deletion(self, view=None): + if view is None: # detach all + self._view_refs.clear() + else: + self._view_refs.remove(view) + def notify(self, msg=None): for view in self._view_refs: view.update(self, msg) def update(self, subject, msg=None): if isinstance(subject, (WriteableFieldArray, WriteableIndexedFieldArray)): + """ + This field is being notified by its own field array + It needs to notify other fields that it is about to change before the change goes ahead + """ self.notify(msg) - self.detach() + self.notify_deletion() if isinstance(subject, HDF5Field): + """ + This field is being notified by the field that owns the data that it has a view of + At present, the behavior is that it copies the data and then detaches from the view that notified it, as it + no longer has an observation relationship with that field + """ if msg == 'write' or msg == 'clear': if self.is_view(): - del self.dataframe[self.name] # notice dataframe - concrete_field = self.create_like(self.dataframe, self.name) # create - concrete_field.data.write(self.data[:]) # write data + field_data = self.data[:] + del self._field.attrs['source_field'] # del view attr + self._value_wrapper = None # re-init the field array + self._filter_index_wrapper = None # reset the filter + self.data.write(field_data) def is_sorted(self): """ diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index a942ceeb..677b1ce4 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -1355,6 +1355,11 @@ def test_get_view(self, creator, name, kwargs, data): self.assertTrue(df2[name].is_view()) d_filter = np.nonzero(d_filter)[0] self.assertListEqual(data[d_filter].tolist(), np.asarray(df2[name].data[:]).tolist()) + + d_filter = np.array([True]*len(data)) + self.df.apply_filter(d_filter, df2) + d_filter = np.nonzero(d_filter)[0] + self.assertListEqual(data[d_filter].tolist(), np.asarray(df2[name].data[:]).tolist()) self.ds.drop('df2') with self.subTest('All True:'): @@ -1364,6 +1369,11 @@ def test_get_view(self, creator, name, kwargs, data): self.assertTrue(df2[name].is_view()) d_filter = np.nonzero(d_filter)[0] self.assertListEqual(data[d_filter].tolist(), np.asarray(df2[name].data[:]).tolist()) + + d_filter = np.array([np.random.random()>=0.5 for i in range(len(data))]) + self.df.apply_filter(d_filter, df2) + d_filter = np.nonzero(d_filter)[0] + self.assertListEqual(data[d_filter].tolist(), np.asarray(df2[name].data[:]).tolist()) self.ds.drop('df2') with self.subTest('Ramdon T/F'): @@ -1393,6 +1403,11 @@ def test_get_view(self, creator, name, kwargs, data): self.df.apply_index(d_filter, df2) self.assertTrue(df2[name].is_view()) self.assertListEqual(data[d_filter].tolist(), np.asarray(df2[name].data[:]).tolist()) + + d_filter = np.array([i for i in range(len(data))]) + self.df.apply_index(d_filter, df2) + self.assertTrue(df2[name].is_view()) + self.assertListEqual(data[d_filter].tolist(), np.asarray(df2[name].data[:]).tolist()) self.ds.drop('df2') @parameterized.expand(DEFAULT_FIELD_DATA) From 3153f2b3a1a181966db118af3811d905e9cdf706 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 26 May 2022 10:09:44 +0100 Subject: [PATCH 180/181] add unittest for view presistence add document --- docs/index.rst | 1 + docs/view.md | 40 ++++++++++++++++++++++++++++++++++++++ docs/view_arch.png | Bin 0 -> 507440 bytes docs/view_life.png | Bin 0 -> 124270 bytes exetera/core/dataframe.py | 24 +++++++++++++++++++++-- exetera/core/dataset.py | 9 +++++++++ exetera/core/fields.py | 4 ++++ tests/test_dataframe.py | 22 +++++++++++++++++++++ 8 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 docs/view.md create mode 100644 docs/view_arch.png create mode 100644 docs/view_life.png diff --git a/docs/index.rst b/docs/index.rst index b83b4b0b..8da4b065 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,6 +36,7 @@ Getting started dataset.md dataframe.md field.md + view.md .. toctree:: :maxdepth: 1 diff --git a/docs/view.md b/docs/view.md new file mode 100644 index 00000000..4ec04c00 --- /dev/null +++ b/docs/view.md @@ -0,0 +1,40 @@ +# View +## what is a view +A view is a special field that has a ‘source_field’ in it’s hdf5 attributes. In this case, the view will initialize the dataset from the destination specified by the ‘source_field’ rather than in it’s own hdf5 storage. + + +The benefit of using a view is to reduce disk IO during apply_index and apply_filter operations. The index or filter will be stored in the dataframe, the view can read from the source field and combing the index or filter to achieve the filtering operation without writing an extra copy of data. + +Specifically, the dataframe._filters_grp is the hdf5 group where the index or filters are stored. The boolean filter will be transferred as an integer index during apply_filter function. The index filter is stored as a NumericField in the dataframe._add_view function. + +![View Structure](view_arch.png) + +## How to generate a view + +The user can either 1) call apply_filter or apply_index to generate views with index filters, 2) or simply call dataframe.view() to generate views without index filters. The internal function is dataframe._add_view() which take care the construction. Please note the view can only be created from the field that co-exist in the same dataset/file, as associate view with field from a different file will bring lots of uncertainty. + +The view is only a special instance of field, so that the construction is similar: 1) call the field.base_view_constructor to setup the h5group and, 2) call the specific field type constructor to initialize the field instance. However, there is also a special action, that is 3) attach the view to the source field so that the source field can notify the view if the underly data is changed. These three actions can be seen from in the upper part from dataframe._add_view(). + +You can tell if a field is a view by field.is_view(), this method will check the ‘source_field’ attributes in the field’s hdf5 group. If this attribute is present, then this field is a view. + +## Fetch data from a view +As a view is just like a field to users, you can still use field.data[:] to fetch the data from the view. In the field implementation, the member ‘data’ is a FieldArray. Hence, the difference of a view and field is during the initialization of the FieldArray. Normally, FieldArray will load hdf5 dataset of the current hdf5 group (where field is stored); however in case of a view, the FieldArray will load data from the hdf5 group specified by the ‘source_field’ attribute. + +Also in the case of where there is a filter/index for this view (field.filter is not None), the FieldArray will fetch the filter/index first and mask the underlying data first. These can be found on FieldArray.__getitem__ or IndexFieldArray.__getitem__ for indexed string. + +## Life-cycle of a view +### A view from a field +Step0: you have a field in the dataframe, and called dataframe.apply_filter(), apply_index() or view() + +Step1: the view will be created and attach to the source field. The attach method is in the field, but call in dataframe._add_view. + +Step2: When the view.data is called, the view will initialize a FieldArray that point to the soure field rather than it’s own dataset. + +Step3: When the field.data.write or field.data.clear is called, means the data will be modified, the data.write or data.clear will call field.update() to notify the field of the action. And then the field will pass the notification to the views in field.notify(). Once received the notification, the view.update() can perform certain actions. + +Step4: At the moment, the view.update() will copy the original data to it’s own dataset, , re-initilize the data interface and delete the ‘source_field’ attribute (so that it’s not a view anymore). + +![View Structure](view_life.png) + +### An existing view +As the view is stored in the hdf5, the view relationship can be presistenced over sessions. Upon loading a dataset (in dataset.__init__), the dataset will check if there is a view and call dataframe._bind_view() to attach the view to the field during initialization of dataset/dataframe/fields. This is why the view can only be created from a field that co-exist in the same dataset (hdf5 file). diff --git a/docs/view_arch.png b/docs/view_arch.png new file mode 100644 index 0000000000000000000000000000000000000000..7f36f47230abe4a7e7906f476ceff8488be1119e GIT binary patch literal 507440 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≥5;z%{rENwoSU@7 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
UBVN5P`ff)M~hWb)YZ#o zh|*H@273R7#ZE1Xj@~{Y-sKGb-3&cE5!^7i0mxHh>P-p{W~DN>Qq@K0S-bB(b#g_s zJ(a3c=x_D7l|CY^D-8V9iRp+Ete?LhH%Rgj^SHnE&}-+C?rUv z%bHIg%yY@}>C^W5k>2qmFo)6RhwC26EXy*~=K*OZ1ljya5D<9fm;o|_7}C{s^P9IZ z*T)+ZCFU*CMcO4ZTE;u~lD~8!F|i{p(KBpQmhGL}b;~oPOtBF2E5B7+XgcD_aRZ!! zvqqzILKqkW1tAs?jRDS$+N412d6(@CrjS?y)G`{w3fQ3z_^OQmab{zz3;+~oJtSc# zxj9=J{#+&LS04PM^p?u|3!rLI`Y2lwP`n5D!m(pQ3mg_wrFnnaOXN0!zuo~mF=xxz zv1BJsJ9RqX_)xtcT0p)taE+0u^h@FME@vB2Q(_=F&|{8JW&k_?=oE-Ch^S zEb`%S$L!kjLM=S{80m`qd??Z3FYYtZ(@}i&h7t9L;ZJGvx~5mD&!h+1Lop>PX?T1G za)k7%zG1O`HTOKyCI)^!@IQege2bmPw~MSFWF6U|U8AMOTqTsgI810Oee>VLHWkV1XKhH9z`_WN@=t|Ronh7TH#vkGC3>AD}4st6mj zgOeMjAs>fp+G3lX(~sZ0eCCo0n+~X?^=q=s&`c(jpD9-3W-X%bZ}?)wy!!3Ja7Z9w z2!soFFlXW}(*V)yindPu5qhjSIm^_fsBm$xRGTGIXlJ{mueYoHyN>j{IZ~5mMkwPH zuDxv9e4lmPpgcd-Woz^ONh$fy45s^FJGJOREAN*9FWt1gCe=FWZE6KIOXI zdLa8R_f%`lTYlQ%2$%`Fiv8_aeb%WPeaiNuh2hu1q>rbsp0eyhxcoc?SVYX4?cO6gt`0ev^f(*FLCjC+^2-!kInBpF_Lw*!;5=TX5rEzuHu1nhkndhg)3*e z12hnMX5De6aec%X*j2w zaC75bSpAUt#mM*o-;K;yn7K2^gnzfCl3VYi2hxfm3%7Di2$knD1~BN?w??d8yGWB~ zr$DtpxopzbjUZOdnns*Fi6TWGoTCj+(I!tBUH?fjFYXa7Hz4Mjjb9}ZywFb&4dcj| zagu1KZ`{L04c5{gud8hwp57-cb51d!jj|X{rJ`7IEiX69z%RCV+1*KaQm>OVt`IIB z!zP!lyq>jS60CHRpgoLeH{vWno}t@?F7X3FL>3l5@=|EhncZ~qWb%Y-uW_HV`~!Wz zvE@56=S7!O3M{llDV79%O{!!aigZ0{tSFhDG^`D)vBmU>3RK^D@f7Y%w)Cjo#(h;- z$;Fl$q)3Yae$r_Eky+_u_8xT9{x?m=5Dv10{VNLV!DN|WQC+IW&4lqWT*7e-QoobM zAAeOu13#dkZ%DE~KPoG#;V!e@mCuI&pZva>4=-1$!7tN5%_EE&AboO;UH=(cp!QzD zrzM+t2KF{#k8w(qnEO3=4awmZa|p!_$1CBN5Xq)M#3S-<&aI9!JEIrSj$`$Y2>sz} zeAr7{O=utDpGGK_QE;3C5Z@V`>8varT(ywVpkKal<@rh??|Wo?XGWdIEDs**(a@I^ zd#f!TAWgXn%G+D(TU-3}iBA*5ALzU9+~-r0WsqTQzMSDM)LPRF$^tia-tYZYP*qe# z8zkQd3g23@vV9U_`*Chq;Gy|<+JQ)&68yEHrk#W&iQHc*F4@jo_+>8=l_VGqvx8Z2 zd*TLWFB$~*9&@^PX)^vw+8r&a8QVFo5%T75l3`Q-_ByZG;76HFtVHvenHlgR%#iYB zU}ocM6JJu^{MCbLOD$kgM+I7(di$Mu*x76)^)O*BAQGPgp{JShzqG4{+ zuqU->negdh!FOh%zgT3}L>N1SZ9OuRt3tu>&ziN>)E$MXM3V=}P8e>2B1$0d)j?K9 z${(D-Md`Meowe+1Dmb9_9O^?K+p#PRXKch3hdWCI3NFiMt2Y24sWd~PbCL4B^x97m zK!4*i7@2C-5>wboI}ksL6;tvZfx;PXBtQzcn)z9(yK#5HygB9>?diMu{$!7Vm9VZnl%}DFMwxERb(o+c;ZUnW~io3Ay6@L07k_a!`yN!i3jdQIc zb>3dbB{F+{792wMgh}%1oZ`I;6cb}Xlw2)MGl6}MUANr^B4_Q7d}F9b1F!q*6uNS; zP(z2e@HH2gOg(G@AE)Uhz@9U15M+wJ#i85);0m>D&5si(VDkT}{P^m#&eE6IGyN6v z{iU|%^kJxI>-^f~?bivRC#oGYn#+3xXV~`f#NQlu*K~1e1#k`}78=Ym{(rnJ)<*Ws zK|Rm>Q$XDlO%|CzZ>|UQ9rrM^ZM@zRIAvrVsqlg_{^Zuw;Ou^W+*_~2*F(Mwy|A1x zhx@^a<|xW}8h7if>WKeFOINEmY^Wo=!m%!9)Gh1e z;kIMKob%hF?UW@~sRm1P8?6c>i1uBiDs$$jK(HpOD9;aZPs40g+hB3iaA8)*88?wO zq>hvIF;iI@@Y@$ zZcu7tlB zUPqKnlz}kz>dOI%GHnKVYrVAZHh%ahjxBA7#k>Yd3)TinA<#TpY-ov5e1t{$vdesu zYPQr~OFnN8DS3k}+A>`ID4@WQJv-(-=0l&dM?n7j6kK6Ay6RQ&3^m@yJ*#Y_JtIV* zK^U9@xM)_w>vTDq)%}y5ne&fX!f8;`aB)v;ctW_NLjy<+;y2a*?~6Y9a%BVWwD?G` z8Zl_zGUm=+I9IJ?cbO&|Ich4{ZX2s!C(WKt{T>w;S!NK4q9cR7rWkY0>+AE*YGKEF zv%g*hsb_9bZ?0nwi)K!>#N~ zlA>aG9|vRAva!Qa^(}F_)^=cGmm`dT!Q?Xyy zZV2k;qt4uAnLEY7_=Anv6&Ozg@j)SOz+7(G5*5M*danu6x~7Yu_Uu9D79Y_m&UMWc zezFw`oC%z8GkY;M#=-ttgmeGf#)~Jrj`6oE)&uUGd5h&LodP9u)yD7n4i;}bPI_~( zV)TOSQ#675J4ScP@erSE1MmI&_J-Vl+>(?qTAsQe!$7@j%%K^YamVrFmJ_yS$ z$#7gJc|kr7c>d~}3EFp2uyd5uBHSE}&!8ArDVEAQ-v_bjw-ty1*s3ZxghB2)Wf>tR zZ;&AgEcExFQvIx{lg5ta)e6E8lg$ugFbAwp#Y6Tp^>*Ll-u4;k=Pmms*7<&P31BRN zdh~#aziET;56!L1TVyz=lo`AAN(*d%0RPTp?>Bj0smh_St10$CT+F56gr-QDrf%^7=2bvz-Xa3dF?{G| zk^1(IE=!=${XD9$Bv+ZoQ=;U~N!|7nxT#Hq6{mAQt|o{m;=AJkMZ9>%wX|1vU;WTTYj1(7lVyA|-S5^^|X)N9gZE zDU#l98e&?d%*jv?Cn{DA=|Dk)51lcLx41a?)ze^Y-n6tbdK%_*X0kz$$&~=*TSaw5@^) zqKd44C@aWZ8w1DFVQw3)4{Nr+qg z$%Fd}njM!3S0>;xftRZ{r=KuaZ_?ySWCz5&fk`~)dTXBPvJyPDK?Y!vI^VOZXZaat zYnm{V*ZQh?`)q{QzI{^tv4$&UaD*g}9f+EknkwLnW;lVG3ebzC22uKDr86q5q{^uW zfukP^pU#{-EaT^!qw-_*NESJ0<;~c#^;2Bmv{??$R z821&oJ=qV#d2uG9L3A8si2V{dK*q`+1t^zA0i1XvF#VneUjfTL(EQ%JUGDKG^z{kJ zpZoYq9gTq3W0*u?I1sL)zIi;Sp5V$cW642LBsO^bv5L2)AlV0h_^tUAKcd4O zkq4E3`;+Hzi>axh^$IswOK=2+*Q^ZD?~8_YZjGU-GKkWy8?;CZOo+kJoO`o0c9U`U6vyMv9#}HzHD+m3a zO?T3x^WYki+vg{+yc$`uYXTrL$VeDas;zA`@X_ z^d@VQ&pbOO&~J_1;4vRY9*}v}k2dr(>dT_T7c$lkcL`uP?c)I%OrTnOmk4v-^MqOG zi^3^uF%FyEII(8M{92^K^U%sTu6zt;nmcOd$y30u4W#)Z7s_z77^25NL^%JkHY-c6 zOg6&HGEmI?-B#1Dksb305mUp!t3|T-VLPmdJ~p(s5U!etxzR>F;F>#UsAbIHblYc{ zQ;M+?r&2m?G}L9?otZ9R#G6NK>@k~3D)72Lq6F%P`j!ljTq971XC_acCKzgqZ_vpu zQp9Vm)w=iGU9j*7u_(4yey^_`2^D%EN}1!bUnVc7tC=FptX$I*0|F-C)jC&;maUbU z@&B3K@D#kodwdLn;L zkX)~$H6xApOt&r(Ae_@L={DI5f(H&JjbFdNnYwd)WWWbs-4;87x`t1cBIs5* zqqvY)#F~aDmprm>%GqZ{y~(#bZ?4$6F|?o@(B`wZin0=C(zJ#*g`9YdnyM5hvTdHl z4ZHu7H_8t2&$pLpXpKQa)ZFT+U7wM7rQPQznI5ggq6H=SenFJ+AaqrC?^{aiDOAr}*=5bi zTJsRhR!{8T{-~TzylxI!OquxT;B=Qrr(b~(5n*(2ONHt8XH$*eJsMxXHFTORV52a^ z`jL*Ed2`N5;PC6G?%V*&*FO?JLwm5f1l#(nT4B~^ub^3O0_oj1XFj=q^Arlzx3}zb z+m}gD`zclW$rHwZI6ffM!iW&599L-m6G7eDbv5=HiFEN5=!lP%nR3`j6r`A|zW;QM z?Qdx9OK7gZP;2Y`XCV6D<}4JMa*gmHIw9@r>A zf?ERtV3ENN?l?xSy@M|xs#&^Q)7VOB9AdTwe4YtMDb|Lz!xC$xl^b%1(KmKj()bNs z_VJJBEzEkNR7RU6<4u4nDS=WR*|2BI5nS^-FSumPu#erpK9UG_EtF|eE1a{KJZU%+ zz=XT%+nCNeBj|DK+k7QM_o^qv6Be0q4hcL~Tm5)O;+`|;%#LJ|^DWQfV(XiSa<7I8 z=(QaDOd!^_b=+ORTMRpLZa8x+O@83EBeZ<)>x4C*30L+gZstI*#zd9H!f$`9Zu=-8 zhVo=k;wS*|7YNWm%w}Y=Z*bdv!_E{6q@_d=2|5ekv=|&5e7j8c!X$J5hA)@I{{G!I zcD?*&$?HU{vND=dZ?6x%&>R&JEvXyp{hAB4to$d?SXZvBA-g!6aP<4z6*Rv|6Gwf> zl=eRpvh)6JtX-NfK@!!cr|#D>{QP6ubj@P-2V%bc*Cem@gu4zP+NY11>gp%mZl3tT z*8h@@nZAWbJKj9qNV8O9M|^Nt$^-TP$62+(P3!0YBam578|-}6*njuol|Sp!mxC=53~c^`Fr3w-nVRz28|2=&bp}h$7Yz zsYZ)&_fua3E}^{KaGtqtFHMm%*yIfqVV7v@0l-phB(Ve-Em0w#0KO#9H&eglZh!iu zarZ_W&66bLi8=8MdG%)M-*5~dKo)NsXk!dSq8^nO1LHb=SigfPZ8;qhaxi6zGzD{K zj^TnN0$JK~3QEm0Vu&K*^%lU%I{mf8oXhULfMs=Gn2ws_dQTvL1ndR?9f|-UJ1M%N zs*7*-UeKO%kNG45$P`YuX_)KVBvPT@jSfKx+2pZ(55pK`iHWq}V_9LS9yC>uk{!(ZuhBw?R8lm`C2zBC%-93tSsXTTZ{@{$IG2@(+YY>6bvZAHyyJ&3>^& z+;Zfq+9oHDy9ok zE!}zOL;NibV_63?wq~ z8?)loc^nY$S)(jEOqlvy{z^=4{_=g%(&wfpVndq|3lFefuv%AjNxGUe=(hl`y=f>$SkYhk0slH@EjaAOwlvtAA z2w>vfGXU@o<;hiO<1ASHqFjyD21hIln9MuGcyX)u|QD zS^DRrA@$n}D=sM{nYiN7p!LDOsmUe{F)gsXYo!P*$}7V!10AU#JEvPm1^!j~xzWSx zOsd1hX+_aR1X)D9w?jcNvY-w~z39S6^}Ev7ocBakomEir;M13GeU%UBwi`E zGflXKKcnio3<$NdNd4~8 zkjnc@o!E`oI{0Fib)rk~A{f_dp!3?>EV?Rc_~sYUwbpCu9hZAa9BQAmQphV8j?FKi z<}+r;SK{Q^5_o*j=i=fwV?PTsP0?(Fyi70r7!V&s$@U)!3YNnI5Hqa!iuAdV&5czB zdK|bny2bX{VlwA(FZGa5{^I4ea>3)?0rSThn%Q1;zg%BS96}?ioU) zVQMT{qxqL1Z79Ksx&R>;*6{_zbFNrRvB&qKywSJzj)$+G8@%QN_Zbo={e7!tJErre zt0rxArpoju%GAN{!9(_7Q-U2VAti3iMo2#H_GpfMGaUo{=Z?kv zIZH=P?!4I&@4ZXsX-Y!rgrUkGKO7@_fAQFHDmlC>O7sX#^##q}m%mL{w0p`cD(v35 zgai^l*hY%|>T%}In@fKp3^#}SuOzA8Fw!fCA#OIrU|$EEgi$FkDE|~_N4(3njK%sP z@OOgMfKvFhxAdx00e3oyoNRr$7?)d0L>$7~Y6jb@pg@GlNGuZlR#BL#tgAHQzs;iV zj9e}7VpV+7=d=xnKz`*2lA5}Md8C%OeoN;y4hkjS@5_6MBh~S`EuywInHsEKvv~;r zQ`J@up{>) z8^a2Hqn%-jOd7u~Nk#kzZCo2@rKl%ptr68l26&LsdHJ)ds`q%Yr{a38n(@xy4iop2 z`{udyRO#Bs#GsIdv=c8GYRvar7`eXpUNKj>%H@9)E?&3^+`%d#9#GZ|gQ%AEdH4SU zi{w!smHgwymk641yek0BxayCeymMr5W!ojp+#7JZgV8vK@A+h@Fr=M!0`<{lw>jPK zSfaJki~a6CUJh|x`<-8#oD-B}w8FOA0vtd2O?;zv;Pwn9c*7hkCYOcy67Xmu`E=N7 zc9`LXAP+QldMwI!dwZ~eLSvb^dbG&+th?>7S)ql~PjPUEzRDa{!YoD7EVbr)@L6;o zh(E@WhHD}DfHVUAEWkx$a3=<6I*XPQHnTh7DA5SLi8}zS(_+xXYZ@7D0VbR~{DOS! zqn90s%DNn3)U4KgH&lQl?M>AEFqR9snWTuh&UJWt$hC()dRZ?u3-gj8rmb%)=)xKs zzocfHP{C3bcVKuXm5lC9X)p{Py;PgfliAJd@Cw%aK|TNUF!$6mI0&C(6{p+QyReKZ z!BhplpE^;|{}^r%eX_-(&2t}PhUU>zwi0x-%m5|?`0-!kS~-gRlEO1_D5m$o(35%L z=pz7L>veYr6>bdVV%o3Jxi<8u88UiDuN{(}e)K%4lE*cN4j~oBw)%xgLvh_XEtHVy7G$U$FP|${1}2R zW=d)KXSH)#tegL6o9cOS}8_(siIcNCr0b*MMcSFG=F=IRb#3|3IGFR_DGhvn4G zhDK*|@JwKN!6~oP%hTc(Q;N__Z zZeojey9QK$0M{sJ-vjdQtl`lQCIMTaVGXEI;sh|+>|{Rds4KD#S- zKklvC>B8wf7JPa8*RuB6lK;;O@aZ;~eJu{E)^i=b!NeWEgvG?;>$YfvVBXKW>M;b? zJo_;&_?Id+$8ntQio5EA;{VGAE`S`fEg$QhCrjO-iMo4@J$GE&>YFR$E6dFmMqdcB zXQ{3W?m7&B@H}XgiTdWB@SL78_ifnu;LG+ML)h&vpF=LMs$V~7urHIMMSLq+;60O2 zEYZa(<a*bi%mZbn7V8pv~L;bau#1 zuYHm@RJk6qQT++$xt>|S{KCOBe|xb?n{&u|=mr4jGfl!!VU78m6L`UnFkZ5cislg@ zLyP)W@_y7ReJ9pWzF2juQ~pU117Gj;>h+#WM%E$U`pqpxOs-#_1v_cH9%Z_w5%h69 zoVpg26iW(N0I1l&PA81k8_97XEh$*%%*2g3{TI-g@$XHA0&iLK$m*i#fIb_Xn*0rE znwn5HLBDO(exhKyLJ7bWJ+7U6dyzHwR8Z2g0)}z>_0+8k&6mlb<{B` zefxgLD@i{0{33GPa^{P*+VN?pyr1Bo_%~)`J)^FgHm3)xzFlL-=)XK8XSd!yjz2w| zz^`hx>`(Nhy3e0M^Hu)2JzX&Yr~4Wm4C0qB)=N)e7wZeMKJMx{=U?L z^<0)2rcN39dQt$;CRE=2@sRG96uj=-1UpbDU-HpHseMZ_eZIGu7ERNESYlN;GWqCq zl`F8Zd6P8x#&P(7p<>0<;emKkG&)$UV8M>pv?>vyN#j?NW+{_;&M0xCm#d@J+=jgP%s}(2 zCjg`bT@G)7tHYQD`)~Fm5A$!o7B|&x0i{-R=>IeOoxYUnM;U1f+?e>0syNzguwj5O zz|^M-`jH@uNq&cb?BFpPp{}PEz$yb}!an!RhUt_*8ixOOOI*3|HqbY8cnTg+z6b1* zOcfZin{XkrItGd1(J zK3vPfk?9q$*^AUT4^A&r#K=owNvBpY>@Xt-%1{eVB%ff|CHhv2H)*)nR5=S3a~K#N zq1;Bde8-s!? z_x7=(GK-9>{JJJ*{oD5E0L+Sy(n$Nx{ptq213D_8T=^&5{EvoSbMI8&Yo#XBC56MX zZY;aMt*Sqt_Md9~KLBmF3e@l`i2@3ozG4l_q*#(a9{%BMs@qAM!;tkvZ-*FFtUkgG zJETX!fVIBLef;WM-Fp?NFV!{Y#!ie}qK{3R!@F}jPs4tIvzq}F1alWCA4a+;*cAcs zZ$PkTw^2#IM_c_}`^@0Yyx&n_lFQS4yZ!Elh(m@mg)&8llu+6b_s=_V`W4p{Hi4Y3 zNvvUlYs)`FI#?*BSg~B5S9CO9xphBUN2mIH{@D3RFf_f;$@M^#opyLdRvVf5MwsOT z0+f&sn&!9{uu}b$zJ3cY@UT^_p`|Iz5%tBX1H>JdNp|EpmmiFgXYO5V4)IUWr(4D& z*VLyi5?@Js-S(#r+5KPdNuG{52oH}~E>&5FwJVmPaU)ZJ*t#j}#{9opjl=Bki4ynx z&iyIgr4;%iLai^9hQW?M?h=$dHy9bV|3b*YvMzz^1psR}I=F&XSClzp=rL52><@j_ ziMT1U?CFLc2mCA6VrQpxAVEYg)GhQ`|gb*KW4Cr`2Q1UJLLC5zLx2Y zkESFJh6z>+7rKw!t|@0+vgSzy1Si{IrxdaUq6vX(WtLl~X3E^r(g^)6>|acI?Zg0# zW@aGdh%y9I*Q(j@P(h%rPQV-5fr2Hw$q{GIG&TI6tzs$?BxKt(b)wovF+JPQ)#_th zOSb?lp8AjgPN8QCLYZ!lGz=zIoQ*m!i=d`gM|=^%)C8hQ(H>}C*L(yMZ3X}Af{fNH z2Jrhge2~7%H;aqc1S>RL}&Vx)FLBl_*1wXz>Lm1AIDJQvu4Hwd7kwFb% z6$&E|R(&Va{drD>dYW!jEmU0pFwYWy&;$#J)U9s;=N9c5y%ES4gMwo5bAb{9BH1!^ zwtnIm(*La^K{tfgY#P4uj*6y5-`>GBs4AV3RoVo5&a_P*j;y_FSZ*cWJtTQF6g>As ztwf9Kpy^r?NCwYJ{!%&0rIckx3^p8wJX7mUwEQoff3;fXc5blKwezhmINi*OrhI!| z9xdwFc43|0oHsRnS0-zwc#ZWqP6(2>CNO7w^Kx;AQ`C7D z34kyKrYNi!w^H@7CI(`F!2dR1vpLc?fR(oxyYxgw6vcSPxN902-PrK9!${>m$`c=7^|M9^`! zy(`$DM6N%_zMow^5IL}#_D}!E=c27ggRKv?$cM<{L!U%bCEqr;TpW1jpvCqKJI?kTYPv7)bv9GZWio0k{`_v{Nc{T{mHJsIVLYe z3s3i)qw;CNmCF;TI#CLm(0%friA;lPKZK7Rltr^1br6rVd9RKt1vGn>9DaYVQA;2ngdvQVgD51YMr#6LGpJ z$4Kj5)6pIrfuEVBc#~%^};c4MuV7<(_u z%Gd}m0K*;$0e&SSOlkhkYaeXvR3P3eX^O)+6x=+v4sE&f<}Ds6(yHM`Z)bBCDtFJ~ z#fmxe*o?`?&V#|$-il;svhkWL)NPG&?CDMWXuup@54o#g@mrf@-8d%f2OH zVPFe|+5JyR05V%)G?cbX>X&2JNM`&9BPN^}vq=GeM%BhH8Adwq^dPRN;=I>|msBB! zIMj<%&R@qjTtVZ{Lq~b0kBM$g=Qr`S!=P*?elVW+)?Dj|Ka$|2uF*BO@Y1VC88Iz| z;nk7~kchelpYMPGPY@^hY$p()i}y@KS~N|$@x~X;O5Xz~n0j@8Um>_-QLdN=mgRwl z1VqKA>&ev`;4l!1?@XQt(|)rAr+w`wM%^I?0xoi|NkP+1C-7s>I=`|_lkv?40>k!M2fYJ{+Pm+J!xK?43x*uJAc%5P+ZAt{P`KAYd!P%Q;pBH;G_Px#7pS)TCFuZ`@ zQbLb~zc6c2=8Db2cLgRgCk#qvC|5hW2sHZ-Fl$9-3+Z@D+#YMThm-i!x z13hB})mEy0J98Uy;wrq)f?Pnx-P<8SYAvx_)EW!!1N(oxfO>CjIq67o+a7!3<;;T& zjFlUsMM30>KSf|fllT3=Z;wZHj}qszN@fF&`_iSMU+Gx5S1n2M*R;7r%*3dGX|I*m zHo@kod38>u1EP8aaB4ABO`rS%;gD|GH9HdWnvf&N6xJ5pANrr-4F$wz)!>5(t6s&@PR&5m4szwtoX<;XbJh&E^dNm z>WEZSO${A_?KxRbrbx5#0fz43Rh%er<^i;hiRAe zsQVo*3XDz?!t>WyeOeTlws6&4a*r*H4rQ=gqQMw%)q=z30Luj~uP;PPgYqmS|99Uq zDvJ!Fs_K8a>IMkr7}5RdF)c}Qxsw&i4#JABI(Jn=uPQINT~J&f2Tb?Ume#SGl5Ifr z28&aKr%sfECe-?iUA7-SNWwIA-uF}*)Eod9p0(luUUpLCs!X_5=|7^UUvw?(nFX3X zrz|a`Z>-?TbZ}e^CC)eZ1Uhv|37^ZAj5*{ zr-^}Z+H!_@^aUb)!*jyk2>I_dHM;6mc>z?b*09pe%xJas>R+O!4n;<*-T|LL{byPr z?)ZRqB8wH1whbOvH%_3%!5_E`Ff4)Tvb6VO*-mIy4&jCun^8EH-0$96BIk$Y z)vBl3;g+)ZtL5&{mM6G2QNTR`MW(wAbWuO>%XieIY4Hy2y@Um_EkLTT0E0bxJE?4i zL(_xlU^K|kX#l6He8&5Bwd2_>wa$cB6mHX3s?X`Xs4CVCe>waLqP`)=CL zp58TgSnn_z91V>X>O}qSg#Xs*4?{myqCPMqEAX!t&)F()dV)?AAW*pf!n-I+8!bYD z)%}1F>irn{6bldr58AWo1&|I2W7R<4)IXa$H`Q+?4zUFOU|i(pnUn_5`+nFp&RC|E z-)}kj^E25Jb-!;z$RTrPk{Vb{qRj0x=FANQogWT6G-;&`ko$knzC5*hVxt9=#KAP% zf}Ua$Kbe;KO`MK9FG&kDp(PE#8jqg)0C}cluFU&!usbDm$(8XJBxYw01v?QF}NfcSD0X*jYfzF`N+Ew#OCEkeGhq_rR2re|+}d@)oF zs(y8%b!{r#@EYF}3g_j2dnAv+V2bOf4iQpZ%Iv{BSYhya?Ed&5())}ezv?$A>C4`) z_2k$av}I9p1dRQz@n*GNtGW#W2=4;*Ss#46H)6kWd($ot4u}0klIug347ISYDZr|p zvqR#bDEpLAWSog#ML@aREYk$E|Tye0-1Y&|63B())|7@}V{ zr~ZA)Mp9wI10xPm2w~rjqZ0D_o7}C3u`#~Km>e0{=Jvt6GyWp1nfB>6C!k?$d zW&SBM9;g(p9zd_dUemY(lnsY}(Vj(&9A^r7qCV{JpM^%%Hx8v`A(ocAo(TQOps8M) zYhYv-891p32*9$NU)IRP&8-8=#PiWTN4$FadkO8#P$=yX@q8~8(ljMTq~W67oJTBx zxPQTE%KV6r0%kt~2Lo;Da8wcT7ij_pn-wZ7S#X<9HI%mJ3-epQ-;Hxc+D+dDPI2*A zkT`^RiF+JmRN5JSUHLH+{4N>$wpgTh!gj&%(;k}!mwJ8wZ=bhs7r}Mf<%eCj1$G{a zkPW<3#;M<`h*5B^%tGww)M!5@-hJ=C;~DJFyhy*YLh`a{UMB0FOiC~QrXd(*pW<(f z_J_y4`$M#6YP?;6D7JloVOLCqY3uQ&E=d{-(NDaY$O|uH&pOgFOzMOP47k6zt2^#l z$c8iSXU*xWs2jZxa6Wqvr!P_MSPd5Jm=EjsobQlm-m@_V&{d;?@L&fn*BP=CPS~ng z>ZPPdiBYoA124u8B03@9)D)N*Mp>S8PjR=3*V+p5Z1T5+h3idd1hN zD1EzbR)7@8gllZHg}Ww_w{Vx_W^di%9XDnrc2N9q-oN^gGM&hVk?t}Txd;LQP0mn< zOD11$<_`3kGyrkYzBQ0z@3b=%b^dRAw2$!0ND$gwY5=eX0c-cCLI_B_7=?*1CIS4X zbeLUx2%fhd0{R4CG%c=`0ohLSkopuodvpyykZJ?s_6kr~!JpFP`n~T~p9lad0!43+ z@+}S{&FWF1^mrx<9&)K}*4a($lh7~k2u!$mEe&JHvHalXoc)qr3EXCf24RYh*;$50 zy)g1!hoL0m^{S-^hCldjYZ#F3vwVw`O?_Z_T3%D z{y2J3AQ);^K8BO)+x260qZ3x3cWiJe2W{OfK@QnYtbBfRut4C<+YOdIa7HL(X^47o z2_uEFD#9&T;6;Z>!+lVj-yQN!FH7>NxX9eeSz@kDO;W7!iv^D5u@aQo*wPb$EmV?R zdlosv)kpn7yOpiAj7Xwvnir#Yy0-fcls8FeS@$sSgGQ}f- z)4=V=^<#8h8?5zFy@i_%p}x?2{le3+r{7aXU$xUi-(=hHyZfcuYV@~0jI(ptIed}K zEAu{ydiY9i5X18~|MKyvZ_PJX_xk;Q7vu5cLrX=)r-4@ssM)bAXr3heO}4Ik!7jCK zPwBno4UJ7D-+VSWjYvtIwf@4AOfLBRE)a*!ivFmxfwFaZaVJrzR60|>Gpc)cWDH|Y zbF)IIJFZ|^p-@`9ny6H&vTw>-K`8x&<7=Ahm(({JEABh_p_hH=qVS^Av}*X)OOJTw zu(9)t%Ty@BDe7~jA2V?HdN}klCQk7iV@Buq;GsN=KjpM<@_r{LA5_@3OE=XA85U;F zDx<;hUpe>b zkdfP0M;|;*wGWiS{nN z>U}C!ss@8w;1YriwLAdNk%=B~%!s%}XmQY5}`%r6d*%5|oj< zKbDJ+i%QaHXcj-C%1~1i>(Y@>{(Z7k8-rzWh&nxjFt43l!}9uB;5a9rO+1}SDc>Hu zm03HDC3mvt+XPN(gz;)8Ni{TIk=`~toL6l3^YDSoUW9gDgvrt>zo_SQbHi|uti?ZY z(llnnX7PIe?(~KqzTvF!=d=~ z%9Gy{ePCzWUZea!;3K%|L;wb*;95T1q9e;)UlRZ&%1BNWM-?g?Wu4645iF$LZ_+kZnPu6=I7V56cR7c zRc&wxr;}ihQU1MDn@0{c`%dJ?mD|15#KGjor{Yyd&r`VWk%S%=#PYepc0;1&cKLdR z?Ym`tLoH2y=!V~#gIke2 zwR9L=-nD307EjK+4exp}m-EU%LvvF)gEyO!#Sj-w1P$I}#wdP!v^>d*is6js{Sf_I z<|+jp3#CE4-B)3DVvM;n!%3WIgc~R%P8Yi@P@dPGgRg{q;*&2;_scV}QgBLfOpu!3 zJG1VJVhGikGu{qq!mD~9(ui$-o$IsVTi>k{u>a9Ex^fn$R~;(=3196JmU|9CnZr)z zKVx0-uA7tIBeJW1Vi|EJ(R)5Y)@5c2Rg5fnu8LShe#gxF+3`$sv?}`&d*&x|fb}SB ziV|qpBRs+QD_l>^{~7I#?TRl>rE3#+T8G?TRh#<2HE+>0OMfWr;gK~PZdAz!>stFQ z?BqU#be+BY&@|6Lcc;v;J>qG_Ovl2JsUd_WA#_z$^fu>98t}VsRa7a=)#%jAz=R-< z%QV?LnSMUU;Qe44M9?oAOB6Kzl)D0s80Y71RjpSjP2O8mDj~AqaX6AmBL?!NsuT=8 zHa-Q=PT3H}QL-S?&(S6Ahb07&wpI*9+rg}Y$t%z-BAaQhM;?fLm4~{A9?!ol4vh`pz+E2sM!Qg0_OHN-dg~5h zWr?f<>ThTsd(7=(LLEo>?9yEo4O_MEMe#yldC_Vf3ZMPlopO3rM& zO6t#j%$YYCxNPy0wOKMUH~q45!{=kFT)kfH*}HZ zbJO*Gr7G{=!Ucpe&|}fd+O}Ue?8dV3-ru;%i1XD3vu2oq82Ul>ZEkTkO>HnjW`ken z3PpN97;-E2@M*U1)x7st@0uXWLIPLi(i#1tpUU0CAJe1UPv`pyi4T=4sugrsNVRDx zB+qd`5Z-smm zs-~i%E4Up1fx~E*vG8XvT{D$5g7Zn3poucV_!y|wCS`bwB3+Kiz=R!dimdr6*i@vg z&+!wfjEM+eXob|SooZ;vo3K-A8_(+}`cQ42!>i#D2*}O)onfBtxqqD(iRC=~zyg%c z&|`~|Ihpr4^BOFjkdi8weNfWZ5oVk6Q3J2$vdp5-<0bu@G$g3A97@TD3cZUTD;j3h z<3qoAdK7>6cR=a992e;2lv;at{V9#577nTpV3fZmK=Dk0}M7ctZQ;C#v z%C>Tf7UHtOOSn@4eYO>Y4rK7Iw9rON{#Y?EFcuJ4p7JD1>5>Qd(@JTiGei!n!%?7{ zPZiJc=P$H6P8Lv{Zbw1Q!EOlnEj-PLumuzB$agdr2OfbbK7S8u2svU4rmSeg%c(CA zWkYbsySuDH=K@VXg89@To38E8%YLtGJk%EObJ*2O-`X4H+EqGs$%?@96Mz|ZZv?wW zHZ7}kx-%CEk9=eJ@TR!Eql(f?GQJjN?hvpsb#CJbqA`y!L~qF+&ZlL=`Wf-2*L1W9 zf6zA&R-L?bmIZ-(HXIK zB#(ePCGQ*U)SM%^k)0T)&DviS3T<*VR?uV1tgkEXAAt+K5Kb%qv9*xjzkkO0SgFix zT(#UKU4*S<$JSj2%-!r9f-63Umv+!z|NSxHS9P9E&u{afRi7K z%rDr*&PW#LKthrgi|-{#E>8$@GO~->8T(O;a(q7srFs>Hk#Cfb>v3Tb@DUXbBdp^g znX|*z$eW28gCXjUI{i+yYDw$2^VKeiCg*DsI!{Q)G&nXY`;Lg-v7r5wH9M)~Z)V2Y zTUNDt_eo)C@L5BKt;P)YpVRG6uspf`Xl%KQID8cxDP;sn`;k$wL?)v>u7sdZA+7p; zMMY^zhVP`X_r`a4zuoFZjM!`HptfYu0NwsM(Jj;2i}#ZmM#3O zC^2yq#g`_~@w-;PkaPRHI4???r}RbPy*iU;7zisac~f%8e#HItpe@GMsR8Put3ou= zufmU3k?KPf&5B2ZV(qzoI6O zNNs5Z)%oC?w7orZ-n$Mz@p2}^>c$RO*c_I5umQ@|y`W-Dn^FRFk zC-#g;mW#c9c2BOcWZUM}@bzCB8>vu!49uyFtE^Pg@d@plLMV?EMTiI*NR1}$vP6VV z`mNiW=X!a;=smta@Vow^R;9&`Jirfyh1^Zt@;y|J-#)$$jUk>r2P@1!ngBx~wC|(`$NZ(c^7m43~!6e-FCNhD4C^9Yqf<2Z;BI=rP*P zg_CFhA|WB+#NI`k+}OA@ZAq9G3ZFiOzJ77DD|mj4$d8d(6}}Jd`tJNO@XfhX0t-ev zXD=(&V$Ymo2oQQAg4Hzgf7pkLX|7B4^@fv5C9A}FMGA(dmb-Z^ZTraCPE~}j8Zbw z)Uc&ric0yX+4Gz6^LD03r;lOYo6k|_`eO5yHqt~Oao1XX+s6hCrb`hbC`rS| zZ%;k98wuwqUag;Ku-~fX!#JWW4=I;mv&bTghJ`Oz6NX+h_MZ_3SdnG8jL76T%Jom^ z9h+i?fO0;}I2K)k87a~FgFLm@O8)g04Lh%w9#K*I*Ka$6vt?;`n6myS*`ks!!lTV& z)15szolbutWF!}>{A6}{`w}gm>t<)ih56>Zm3q}p?=9XNqS=we(mx#)7YQrS6*xSQ6!MuJ;^FatQmu0I>BqN=Nu@%q$0zepmbZG?BslcPY6l zpD-p?;t)mVkns_vFIseu{myB&eeXA4e)2tk;l;9%)xx$w1KKABI|go_Q-cDJwurIy zKduDh%}dFI8rHY?^qFw^zuhOq6hGn*gFxc$SI-#}-sOMxt^4s$KY;uclB`>Ite3sW zL%*Dj)xiGoAy7<0Qw{en9owI6p>!De=G5grQ8c~KKZHGibC7D5SVSvTk|CJ4WN@?~ zA4e757U~Bs0EPV-L~j!MYW*p;tnF`K=}d!kLJ%czH^p{Z1RQvNK7Tln0?~im)p@CC z8IL)8K4=Y9|GON|$9s>%`J}cZaX?H`lyNr=y$W_DqE~O+&GRYVv^0gxCrz?bqOt83 zz8#UlmJU2Y)$jc*@lVbkYTj5MN|XU77Nb)cWQkYuN`kjk$4)-YrJ0j0)B|a)VTA<1 z6A?5j5lz7aRnRqmQbwg4Xar!aFvdTRwQ{D|FI^VvYj-_F@PFL`dVEDH$ol$RY&O zN(Z`M+PdXt%k!HiRm9OfF-q}uA)1f|nM82#&6_`)$(k{GRuFb7849DAwJr#ufBy>1_3SEr45-@*i57tf@T&kgai{`7%GC1)P3U__T-;Y|)DN8+rLyaqY zI^@3@u-J9PyLqujL)apUV3qAq8FG+Ix*$x=mfFNYbGjYWnReS4%7(|19@2P4sM#*a z@MnNvD%Bz00_7MFvhvBC2bIeiXTZ!aMi2S){-mRk-o|!9fVP0enCfYN-l`OBrVq>}(Qqv*ob^sV3;<~oEVxBZi zay04p`50)_2muzPqg0zRur1~rp6G)dAC4e@W4`ay;5SU5#l4qRQ5M6wi&`XCYW_<1 z95AcHPnwXpi~nwr%?kws?v7E`;+ZZ|ms9)b7!0a*FrwpZ=fkn{$Wl)JK1{-WzP`pI zD=3R7BTW#g)IQlwCzve2qQ)08ezIH2SE|e_!B23;n^V(^e#ZK^v7z2qXP|W-v4W+o zg6`6HC4Di_9(|AMelUh6c1!e^ry)Xx>-j7Samm(~uupMnFA#^?7%F3iUnCi&+xJy$YjK2O%} zn!!`7k}7K!&1e+Xw2Bq#d^eC72RguZBT8%wC;S@$XHkvH0FT7JKHZgvmfYHAEei3^ z%-}x^r0JiG-d$w6>QXW!GQA`L6UBy^Z2xGMwJ$}~e&sNq5$Mj29>j@I*{5PZQiXBI z;i~58w?@WBBXDcHJ$Z_mjaGe6p0?Vso%9#&h>hzu2=?iY2W64Dk)DAA zalro{s^JZK;F0{z1{9T(^O#jW2S?)uf#tPP_Ed6<_5FiDZF9|70LuJ@^1%@n7qdVY zzt6yNKZ%30{qK2;K86vDgSnEbYDcIYKCWVJ-Rgact+>I&{m10XmixB za9F;q`VvZRVzki6$dlOL%y&36`u$d9A{|-%cD2&+!}0PHM+S!0_NWHKAD2L_g2BDX zmt)pJIP+kty8{-P)KswPmS>ghj!7&ixVwfv0tzM7tnR;TEtX-ZsIOywEHi(1h$;eV zcfH4yL`B?xe=(aYpNC#Ott}`&^KUf{km`X=X&k=RXJT@Lukzmp^uxm#dVyf(#xg9W z;>dQkzD~2bbN9%U&OBv=cXK7*(rwz2I4YWG{Agwa?U!GX#OsAAl<Jv?-(_l0Q3)T44jI($> zzkTMr!hn$!*qt})jrIH+1))A#e4%M1@e^^{VmkNlhBF(+>GyUBaeYVZp=|s&4-_#m zUQE~m=nQfDSk+5L41cI9uBcPm0?cnnxSr{++TX2(vM_;P^t6Aw>)8>-ey-V#}Nt*wQ6d^XwkT8jc* z>Og~DHl>Wqu_!3(M#Pn%rLOnKQ&^o>=tT?)uGdqKmW6|pqTgCj2qi9NtbnQWDOv8* z+Uj3E+-dWdP0rE1zf6Y;8A1pt-YDNYAet{3pP2OgSTtwN#N5e!h{AjUuUYq_43X0R z8(x@@|Ako)U>pFgg41)oEsudO#!#{JO-EbArx054{P%Bnf^)z%?2%1CX*M3G?OFW( zHl44c|62kRzdhM-OJku?au<5Y*D8Ln8RmKrWK{{UtzdJ~7y4?k=8rd2=RT-j+-rxM zX@AaGVm4}OT$7W_-3GAVgGr{2E3}S=z7dOFzxOr-esGkM4GM_T+Bi;+x1A36tYCFq zhfQ{d9xjJkzWhGwN80g55J?{ALdb2P;*u`A@_L>tVRy_WDl53GbfD`tpIGSpdslP> z<-vdyVlSs4A5YO7xKw$lq_0spS)%-Ya}XDyjKQ}}?3%6rSKDH@8sr;(T1L=$emR*H zlG^J!vyEUfbQXC2uZzZx=kVsS0wyh$ETK)0mYle&BuBF?O2$%C-ZoE`PM)1HK(?Y ziP;Ay=yq3Xn%=zj!Ra!GVE<1r|D(g9LNy(dHFQ7{=;oIkbcdpMX;;mDF5e zoa`sORUb%sB$E2LZgomwnIF;qlW*P1fAQvog#dnqU}j!h&7^joFx?tOGsU6JzlqKG zcw_?UU~3swUjZxlO?xOG0b3?Yc)q`PjaW@{+z0DtX@dT{6-?nid4XOe^J%&)%-=ee zoG%3>`0K+GSM4uWmWmb^il-L@IvVzj6 z!P8IaAW~QpkUm&o)i+FsaE?C^A@xbWNJqP{EY~Sx77^7mo;g7MIKKQxk z!ZY&VZ)6j{u)a_zaVPDaY2P?}v1^QS5{rk+zqVgJ$l(7ZG~CGY53`xCh$h$JYBz1U zCUAepa>Q2o?O1^U8FP1THuK-M{>kD+LJ{g1>#TkstUbv?DSw(}JW5Hf6Naknd`+5* z1;)E<)xH9kGQ>!gVN*vuOkwo6+)`u6Qm2EXV?TSsTF?4JyA8inV?FXbRFbI^8S@>%@dM5&%1?0^z3OL9WOD+ z5OwnQ5Pelp=4T9aaWcbyYfuP8B=7|KaW#~b94QeZGGKQ<=0O8S$%KQcv%IksVk>CH zb+Y;dte1R2;3vogMUvi`sEYro+f{>M%M_ee!8)@fml0ym{r8K6h4&_^yG?ma3}$Kp ze=B+s@VJGVADJmL-MvR79CnFgIc-tXT|*jkLeuz2cIGf>-keF^zYF8-l1vq0Pft~& zok=aJ{z#{Gu&Wkqt=1i9?j9)ItPRAb8y0aQB5i#Gk2PvUbdutm~OHu6i2=iGXckemM5J}N6q5Q`dK^C{0sTn3I)swAC1sSQ6iSV7@ zMt=W@ci02-ggDW`prgQy@;ZeIrGcRx-H#@(D@3E3yQfBZoAt%COO=?c0N*DN8Ce#r zp+!?%R-uLNP631Jmi`vo$TIMLNM6IIcx+z?esCb~j@a{ps5yd1i!wx9X zt?EAa0K5BVfCFGOSU~yz^fHzPBG#s<#KxzFUoqeVAPiE%f#GQd1bBd%Ml$?_#{x7p z#w6wnjt<;@Q7{ZZv@)T3W|GY#U_S289!sI)+hyj->7WoMotS}tREF|2hrGVOSFVhg znqrgbr*)ddd@NrtyEmymFonPvp%*m%?urIwEk+7l8JaLgV$Vy-T})55*7>;ki>S#- zl)%@~1+cOR(ry~<(CZ4_wy{2=97i?b&0FZUC}XTvTePaKNug03hrWID(A$YB1udPt zgpVvixRR$57_rPz@HG-6R24(74i1o|dPi_()!JPM)i9gt#cbAi(Kk#HU&Zc~gr3L^ z?m&|0RoO39so7i2B$4vu2}6Qvq6|I_R6j;}S4uJ>{&BkgctDCZ`00;Ei3wfF>FYlP z%BeEDQRzg!Vs1N_SMo4FAIP`Qb@xXP*y-sRKBQ{W?!wjg+z;snF=)(E_d3GpdJ))} z?u3iSFZOahhW#1iBVKum+DJ4Gxk`#hhQR6BY5k7GeDhK<0d(|JDik9hP2<6&F8Cm- z>3al^yc2G0L*4y>623W|O1mU(Fj)_(@ysAFh38wCKujHj-5O?b@)6ZM-4KH_T!yG#@=2|Pslsq8V$X*PEVr+}Ox3_$bS@bjxq1Jhv)uJ=)}fKv zIaWC$?)H|}O?~a#KUEJ&lo2f`-r1TZn+0I49EYI1vaYp9vdp#&F%HdWqfkviQGUmJHaMCNA{pRy}T}R_JIJrKb+? zH~db4qoq{~KU^R}_=RtZID&&j1aC%E+A3yKBhjbOK`bC?@}^wQ_LA@X7x(N*L@pc* z+*{U5)eQM{nj!kAToFV6FY@U~El50!Xm8F7@sqb%_sfx*X;^w*)|@cA6>%zOhvC54 zj+^~+Z`2b(qghpAIN)^%>7hX}US}qS4;=}Oweax{dtf&Bv$r8bw9PLP5K^)|0g-i# zmbEHZ*KBK-tkfef_ovRT-hmwpnp{I^E5HD=bT^S|)sB7&KD_1{1c>ccud-NGh5_7^Wgrp++f# z*ixGaTyG==H@y-vj9>#^XRR5d5T7`>A8wfWiLDKJE#2Mf{`rGFu#d1X4NM- z`0mG(+f^(yi;(}D&GxR12G$qTc4+35`u0XAN?RbNSiM}-C15=9FS1i6kq*)X(v&RGm>0W1Y6kiKy=Z#UagLD*|gc`T`0Xc}um4 zl@%$|a=;6rnsb}Y*eUC4cI+CqogvW=cO`#`S^z%WtZxV0zqZMH9k=pBulgFZcqCSs ze6B{&3k3+^5?cyMT0TJd1ddl_Ilp4#w6?Kdfi$uR5L>2 ze|w})qCM;*b)Uhz`}CK1i#9bVb#fam5=^_=K}~@pMO3TEq_LdR!|pPJjUi(>$y~Gy zojuDsSzed0(4BejRS?(>i%jt>MI%%Hf`2OEN{SyCF&NL=*ZOSJ$&Fzkk_zna!f<|1 z)dnYw7_|jN&Fs%vKY5%*o^;dosle{qAvz7P4ryZVNYu%?9qQvBFKE+?Ts8+cnf?|Y zNgl*vGc}E6ZZSKYNoxI0JfKT+rN7sMS@gmlqIyx!7d4k%Q}3D4;1uq)M4IwVOAUeB z=Y}t_ZbWOBq38~~u{?qw{_)ofqhgcbo{{(W{KlTenaPTvOwtX$DR-)1eTMfUXpX$L>%QAVg-AzQq^eh%(g*1{R|G zA?&h9FGjvfUl;oC#=A`VqJc%aYU$;XAw=SRrhE;&yn+QP<#qKf$y6NAX+A6*UhI1_f^KiA#`~#fhX_56--o^h3f&8jp;JN~d11*kj(=kI`FyGV4g{(b zce_Nry+zTGRrctDRT3E@*HK`5APiv=i;WF>?}&>*$(9G9YIOI58cZMF`uqXva5N`@dcQZgc9`Ah4q_NK$+)vzK6lnl&&vq+Hx%8zJi$ zT@nF;>^N=CcfxY?F-^YTgZ0-MRx-XPA|9SZDyGM0svqoQQT1K<7u2fly}-WYBu02>I;<`^$BU!It(Q39w%T%`9L;%p@GkxFUz zF$XS^H+Gom1i-*{T}|zlPH$Y zyxt-(=wK|!ys|o$dJ?(>x(Lv#K#^fpcEwcizw&wW#bk4lHXryE=(~I>*q(-Z>xxL< zWW~fdQ-PC3+h!puh;)r`CW_$gGAMH%CR5(!;1u}NQ1WfOMhj*XVed25DZAtk#BvDO9T7Tk;vzq}952+`cRSxsnKIyy8b$-0kvM#FIIY*}og_ zp)nb|&w^9Ym+Uf^5;r->64`Kf0SzFq@Hw?$_Z`NE31aqB7e{kc1LRGl)dg5niWm*8 z{?VPmBdFzI`)S$s)axvlIPM;jri%a=%~EnJ?W>~5yW!gPiFIyNLebWNl`NJ9{XGL| z5E8sKHswMu2k7c5XfcBock}{-h_)##ZYY6UoP+Y&X|1u9C+lmq=2QjLZriug;y)Sd z;=qccS#Uf-?|G$75J{Q5XY$gr=kFPLgD za$^LDFub@lsKhz|adkawxaxmS6f}$hwmgmQGQ)hQUHxPmWzRD!toH~I2*d?i`?+zW z%jU*bY3ru>rBF6LjKu|^Csmv#&#YQmw{?8_Rv|rE7LuZ@CcVQje9N|rCdwOFfv;7a z<2)RU)X(x7IW#2zNIt3g2BNcn3DOr#=f~_$&X~?8_^H~KR3Q*`#6FD1?d=JRQ7W#pE=F;X23%#{^auS zikfH5P+?%<`GOsi)cQZAy71cNT9-<9XLzqQlMcVVPAneg=sN+3ehi6t^CO@ag`^eg z>6*OsD*>o29zGr*wEij>quFOOFZ3@qTGq3iyJn$EiuQ8pX6sTNp7);^Ub`PE23o~G zy|7kg6Yx4xO31CTBDd%o<^WE4Y2GErSwbd`L4;V|B=rs9UcQ+50Et7(b?k?1qUlqg z?ygWa=K+x>zi?CSM9#mG5O$34Ux$&h$VFm-(0E znq;(Fa*b`e-P=zL8oAFaeDC%zF=ZgJhGNgX&USpoPx2x9SG)Lq*3he?q-{rYR{rw* zZ;83>69aY$11(oCL&y#WEQ2KM?zWN?A9JrpXL=IEQ+Oia17jV|;urP1rH!!}bJX!? zlNoA5f~X|GYiLcK6D13UGaXVEF2gzYGslXX)#s##ytUJ*lO$|u@@b0*-!w!f7KZjY zuc&+&+h}M=v8O8eZu|K172{4{V;}B`Qfx`_73xhARK{~mzdj}{C_M85$?4n&ERIlm zOWh?*$|anvu0+8WcWX$9v0`&0pmMW_UG~H?+=WBdtiO>($|;7vqJaf?(n(Yrg)$oK zES^;6^p>RRvA2jzZ6f^#6bh0s+hpt$7TAHb0e2yAa`8rr{Cs@`nXFDewm&1)wMjLt za_HzpK>~-R7wUYq@x@xSs&sdgd}3`@p_hxF`2(ljcm1f}UnD&ofMKj*1$3#i=LQKT zMy0^QQgcqAa!%Pfwq=9ACJQ?MEBdq#o&cG7@9w0HG$KxH4jBgGQyAV@E&H#3{ALl| z)b#hlxOa5L=rZN4cX&>rYA5GH>^L8c3YL>q?KA?v>W2<|3?EDGE8fF+ifCrg@GG0RMqYrF+-v**$PFiog?aXJe*p>*( zlCk?`G4TzcCNgI1ns`1581rYvhtU;2BF^p6rTaCPMf-8TkBtLi3TRTPTF0i4K4oG> zfwjPH!#nJB(`_-Swwr_}?7S>ImK}lC>LuxBwMF5*+(=sRc>x8xQ|#(aeSCd5`slCy z!sEIFR6mtKA-z9gtcf{QDK$$}85TO5!|w5ejlT*IP>6FCnZ=FaC?_^q1iE1i00d=_ zL~nRNiTPw5R&60%GX}`-gc9QeI5$ctdsCkJpS$TJ{B{rw0$YZ$o*RpF|15h5R-z6c-&)nC6Hw(CzbbVj=ZQaAD*CaU<&gSB702C0M95-WN}~hEY!P3LFC8Akee_Sq&&tupC?n71SM{R{_lY-h6BIfD z-c8>&{jh1Xt}#_)k-_ zF;tR{AM1ac#SCSeMpG^}vk zUG~l_L;R5s*j#KKZyyz8lYpEpGMPiClNd_){Wsuty_@W2{nyPHcWX>vGLU$694~si z*1$?xUrS=fux4?_kVPaGz2LgRfZmYIuqyxZnbgvB0bUjnPoF$B+Nf#5_({-Cec!>3 zh(fc!%sTa|3Dk@M1EVe0D2p2m*USd;-+7Jr=SzsCiIm2G6=|4sqGT)NYre`x7xElx zYZg1di5;qus!}U#aoBAb9SI5o8iJxRMoi>Xo8Mg-qkZE^N#V)kc|<%Kjj+gbNRKHi z`cddqljcgJ%ee=cR&!?c@(dVNMy+uHAiUt0YWeex_YrTj#j9d`-uORDRGMoe(-t6% z88n%!O#*va!g)K&qx{KLMx_%}nbM@vG&;#X&EmOw!ZRo5QI+tu0Y=ymHVL*Cr?s^0 za{p=;(#)ufV$2m%vP6_fA9Q*{Vf93N+7qgP95Nv5jY*2CJQtpe-9EtX0}oSBb(>$H zhruF)ruYQblxeWNxHK$Y5JP-?4-2O}qX;sg=z{r~ga=tKqP=Ym7{NDhDb|Ru_tJ-O zLL!Zj-Nttu9Yb;N55?@1WsIjq_uw78Ud$xX=HmNCq__v7;Kwjh$FOd^s?*JJSQDUn z7D$U3uDb@hnuc5~&af4FsOk@IOWqSCpKAF9t`Y33J3VKp1-_q{xHc@55Twr`bF{|R zbThFm`aA#ZW-uqK;1Ea;o)ys77tl|ZU`t4o_isl!ANLD0#riIx8S*OlP6+K6-hJjT zYI4Qr(I-?xQQ4*gY{zxI-=u1BcOo&EC` zwzQ@|Sw^g2Y1Yoq0%iFyi+fyK|H1Z&Gbe_HFb<>v5tAySXBAN*X zEho;NvjOW{9_Esv02{{?R(L6vVB#L{#AmSU+o+XC@MOC%TJcApA4=$V2!UY`OHz(d zuksK(yqJnl`WHSB&Oqz-C_Bw?sF;A~Lim-AFF+cqj=icCLv99>%+}Po+o7w{q>Jy= zdUq`I?`E!AuNVLNpE=C zU2>FB_hlGHT_ic2jg;A{q%5*5;8|deR(wzOulK0ZSE+73b zIn6`cO6ON=ECC+l3n4!_fB5!0!>L~vpZ#98U2SKeM7yW#qJ{o;E(&GV=(feISzfR0 z!cZLlykD~OdI?DTCI7P0+IF8J^m)Xf5b?lM8l7;{_1vl6Hv#Zuc;%zdcnT-(?HmkB zqO{ir{1Kd(?#F6mGPG!}6JHHtKE~Z5L-wC=cvAIy8_<7z;A4VyC^iFg{K^yj5g4TCiFr67dHwWM44W`- zIN5RY`E?5?WwAcJgkLQ7I(PhH6}V_vx`_Mlv7-oxb8LNoyT^X=U;NF&{L}4$|NQk2 zXQqkxdFLx;V6n?i#4JU|8H+%B&+9lN;6lZVEFKLE0&5C4O#Przye7PzyVqK38+R}JaSvIII?&u$6!~_<1Rt<(K(*FotK6Rm!7Ab3djGYS}O(L zWnQDCy!e+Gv=U#dpMfJpR(+;K&1NxrtWAjQ#$xB;yS=4!eArOQ_j3h6ZIW*X*ig*) z_`o`=*^tP^`-c?2KKy!i$yA#c53V*86WbW$`SpaC|B8N^#5m8a=ifv4@ZXDfjkEys z`#!=mUTf1^Fwf0l_K9(;I-d=WGJUm_SdXu5Q~!M7JT-*@ zmwkteat4gp#OyNEnKxN3Pc&H?!i!|>nl|U4(bX2@)3HT7T611f8V_#lytC&rH#=sN zE6>24N3yJk=sQ$xFxffoUg67CBDOpih@N4ff21}4gB)|;>|7i(A;$Zi-2$r4HEPZ< z{g>u`rkq01nbF*_R&=1TsS*9p)JQc4DRG%}WX~H`^?fI#Hah`EWQZFrL z&=CyB2Q490GK1uWgBZ2-q;k0)!St!(SqY)&R=DvNv{N40H~DXv_N~J|DRYJPZiT^u z!Ryo)pQ$>&t%il=XL2z4rwkZ=fQ?nzp_S2#HJ@lCrj!ujl;@ZcRmGb1nVPII>I#5G zG7z|8=*m_I3w$@@=2(WJc6u8@B&Zqml|PiBco5c-vwjcdq!4ACxpnr(z>o6U+%8+a zoW{9aRaKUTKE;g&5piXg@O<>1xTx%39Lebnm944s&XE$|-%UdYTr#45SvPUF=>6(i zu#3{j^yR118U9$50-r0Eu6`(o$!=|UI-_yrtQBzTMLY?9BR@$XeojsZB=+aGPS9k& zNbvU!9E%!vt^&4G8a0K1N-kDAfSf8b?JzZY!603@P`IlGES?~4zCv#=+1)d)TgmUT zt`P5b`NIRn5#x(m8zW!_R&Ob2zY_jS{L`|31~$%WVok} zQ4`*tvGM7A=IQ*cqNAfL*oooyz5qT>#Wa9lNOe?`(^N)Mk`%%5Zb;>AM*=1y7!96m zur`qgHLiiMhvTO$!7ijssG8PHSdel8F-SSilt@AVX>jCS-ilg}cF-#=wV^4gD8~8M zrS^}!%de5A%x6xsMC?kiv~;!jxy1nt%wONrxb7^y&sXx7vNv_8^$g~Gf)YuDqKZwB zplC}_Iyfc#nE;}>#iH{JpjYA%;5FglgioTZCk2l=A;ytAWqWx2-r4x9c>l%G-6G|S z{~*6&_;efcjVxObyQ%-E&q_~rRJf`LuAaGbyG)&R()+*sK^w6(_jsXAg4-+vQ#y|X zk!D?eQsJ)=Fjwf8wkd0OB+Uz)X*dfqHaN}bO*GXdkqm@vLa#w*r($k-Xw7py$@Cl* zi9<%cvq%W}v0%l)T7+C~V?p9APj+${3R$F`20qz2@g!kvT97KndF*Dp63;U8!Nfus zea2r$*{-J2pEzOU`O>1bJYaM{neDIS*=SEnXM>b6XhT7ON^Nwm zM7Z8Mw$?=(IF(2`E?pZSI!Nl>3kby-EI(j8u4$WpiPSeM7MpLT9@X(GgdvNIZxOM! z-cv_63cXkhP{roumLPaBL8K8Q*d*0mdw&h^hsgwwQCz=wTADO!NY*On5OKBA;HzENmed-DiGk0pZUKAc~&c- zVf9*Lj+IW?YSb{kEU6_BI|YR!Viwj$?9P{bqASv=ZOj&fu>HJLW#0$W z5PdjrSw9l(p{~;=+~Jux?IwXA`v6Vu-@o!Pd@HgoHueTl#}(_(bV(_D#Nua;tFYj3 z75xDxn^^|D?GGPAerW5ixf>vww6z7;&X+KA)||437TS9+@&|c{AB^Efk9b-(I1#JM z`H9y^yp!pt%@Pf6zLdt zmvR`~p><-Ve^It^D`}dqS*40w*#CPGtCJsY)~aZ?|Di3s&t}KK*nq5>t511NJS} zdiubgQsM`Lq|Rg^&z~R(#pEKGS7duWayiVhTkPAyB%FBYBoNY&PT!f{=a)zI@yaHa zT?;hu)koS#+7Hv2Gd&1oAG15A(}e3jsWp}T@)Y?NT~T5g5^$ADvO3R}`Ijnu!OYwK zqrczqFClZDoA6JpA%la?5X5pBy4t+FLAK$~dbyO^l5;2=W5UUa#rt3|f*O{LI>Z!V zH+LfCey+l$90y|%;K${Z4QmUYIff=d)UYHVu|v6ogX|(%{teGrBpC4pIc4|}WrC9W z+E5^{j0D6Kq%E2!4C0l5v=|G$Ir_N=p@Fm|s|IOKs(97itM3}!8o2a#1C zr&z^5ALer>x5Rtxh(We1!pX9OO81g%?-V4Q;|dPqdp~_Xu2MJ5I`sipv&a;Eh?l>A z(_$`)*B?o)2%a2dCw{{Y=U-R7Q_zSqzIU0ac(|<2 zjchRLG)3eY=_nqq(a4x;%VJ0s$mq@Q2A6Quy_A1UjJnF^nD_2{Ij3utzn(xrIgmV^Zka8ud==a)8|GnAX^|Uvgk%C zRD9c4^7<1@0a)Nyl z3;SXsJMe`uinclI<;f&XWW-<6{(1czN7!#RebV#*$I?IKb!<~2BaGNsA{G4j|az&?mTsdvLV>RG8h`z^h zW~BcV^RNDR+Sk`O+Eq_}(hvCYHrjPJ@OBn5chmd6IbAP(2 zGXt8Oj07p`+m9{%D!ZGWiL#d34gpPXAEM9w`f-}>d48nB+&$GZx9Km077@(i6A?Bk z5i6cu9C;XUaZ`z4fX)r5 z@J$nnGwtww-T6Qzr~15v>(50cm!R_P=X`;RO$RQep0>n%dFq^5dbut`_OX%d1lrrp zyIoY{^==N(&On&>nq~oxs4^Gt;Gj|oNK+oEwNBQ|GYOdaF~v^sPz`v9Q~Q5!o7OWKaJ|IybhFVfDx#PHH&v`W8UpI;M0shD4pzQj zT>lZuJahe`o|lSPU{JvkO-o=|a(6Ft)Q5YYi}aIQ19AyeZE5rvnY_&Ccn*1aHSaU#gMQb|wO9o=zaoE2_Nh1=; zV}SL=;n|qYseoDAMw3LJwg1-?`i9cQBF(&9vW_bMqhzm0b{Y=*M>21`EgU|w20cB3TRZf_4F^Z zDd-OTrntVtBf&q|M0olja7N@OA0YCw0z3MM;U?ybih^P|R{l(cAyX6(3->(kl*I9Td)7c_Bi8gW$aYVnEN>m#r=STreA@6vECD3RiDILOG3k9r%uyg^<4 z!5O4sY*!hgTdfS)wE9CI2OIVPe4IEI$Bz9cwgP5jJ5c}E3sBX0EvTD8UvI|}fiPs6 zGIhzdbOJL$f=-E$+n-N>55`i%3ErPNx!M9ijU%;FD1rP6Xez>vLhtXPXs({W52U@OL$tJ&=rcY-A^gnFIC%~W6B_`B_|g7584_-*VC7z5m3k@BEEfC_*;X; z?rkHM4ngLrv%2(Z$8w8j-B*tPHfuj=`4MB!7xHi1+ZSj1@^HnY#bSBC#n?wvK+F$k zT8hE9*9&M8!|l2x4h1Kyc&J(i+5qMgv^$1g_urlIyhrzZmJ~)J3z+UlJzq7OB&qS+ z9bl+5^kB!w56T%-y<#9&AoZ0ue$ea{7EL$b@q}Q{3J&~^FG!=AQy=ZeA1hN(Aeg*{ z3rcuzkhy`xQYh8C(sA21PFGEdC!;ak+j%3LMI$hhZTkN5=IatGVYZ}vuh;|ToG`1c z%Sd0@EGy$Nga4JD;zw-^c7l%=7MLLQ9=Gy;@!dMi;0)T@R%V0!+?I+yEsNCTaatcMvS_TRDL;6hyZBlW?xP6eQ za+nl}4k&*G&N@ zhjP+~bY!q_i%>VzH;)so;CFFI5nD3Zk5@AmicrAL|ARBd=bF9HL0>j&x6m1eF1{j{ z24ffrt@KGyIzF=EXs|3ATwY?1QtlkJS9s6USyrRwxx7Ic3j(c5I+x+X9ULX#<9A>< zse%x+nQ~}|$glE9aY%{lh`)1CDyd%OX>o8cc)(xo-fHhH7;Jw3SCrXvcqXGi5L*#K z%3W(Rhi!1!eK0U!Yr|-^(7TLAYcAls!~UN95SGS|x6VsrD*E%g){an2qV-Lb6mbwk zJUJFYBsaapji9_yvSBG_?Xl{7?*7N2n2NtVX!J7hD~;li6Iy&g0e(ry_d^aAj&ItL zLt9^m^kQk5+`RGrILbAZj#At!!t-bUtd6CpN=p7-wqrVjE`x?%h!_v)`%-P9Iovr@ z03|M){I_>4h~_3dFO_F#@G5V#Sr)N2FW(ZoW!Hpn*E^wbsKk(qIptx8BNHQ95k3H> zvxHn%9lw&V(N3_II4^9!;F?>hOaQ_aSVrbLkN+(`qT;BeCCYrB#qPrZYo|6zdT4AU zd^=u#oD#W9{UcjCRlbDS)nHGIiqn!vWyR-aPV4A^OZhSUYWC@+PbJY-tzk8!;S}WS zJuIgSs-}0ad3bOQcWhsp6g1%fVKF^=eYpB+ZPiXs?dH9mvyPbGuV>V?`er0-V_KiO zt~>&BMjys%3`)BF@IXj1+q}j`Ti%E4FZzTYK{Ar@DRf8^F)EItbG9qiSWPB*<>JJyewo>OQy|a5 zWg<0igksmMy*=-W?9Sdkq~m&mB$bE94_<4)y-(ru75_gBv6#OMkR>WE5=RN{%ACwV zqq&%EsWpMK62_%6)fOR3l%m2Ghr|{lSmDRv(Ear9Jvp;qA}J$8j*+oYZDXMr!cu17 zD2`fP&TDpziYgcSY-bmhR2JY(RmhOXt9_6CdP$^6#Y$m@@-*FPd@a?vwcLQ3A&1(6 z==4*zu~wx01zqv=t%m*bL2vjXqVD&e*w18UETbzJT0%13iIT zU?z)kyVOpB?C(_{KN&jk!L7H94o7wFJ;(U<-$vonyCUhw#SNh?@ikZ{B;!FEgp6PJ zRr>>V3D-XLRoT^={Nl*EcP^nJl~FFkI=Sx5yUjOrID5u8w!_g+P!0;LIWD2GtP`l; zqU!kp-Q9&jy-M4lsMwyrvCL(ep{8-H_b}qV7)dsFWa$>{HRj2s3BBPbtggXW9}%H# zXYz-yh$){2U(}to+%9tuqpE15L?VsBGnR$iQNOK6o*gcmgDRk9OBNkGk^Yu{>hA_ z=s&_gOY-lTkMZkkaMxHo;WxS zQ%H7}gS8-gL8@pd`9CjCGi~sVf&{@dgS9~nq4H(M9QYLm-k}=B!4yIw5Cfa9uHW{K z@a3?|)f99y&$vIL3wsH(bo6zd>YkiM;?_jhw{^|;F=~wK=Qsb9DXuapKaQ2G(Bp#C zQC1}JRYnBlc6>w14OULXT#Ec>nKj{M9G!I7g z&6^y^h=Qn{CN)7vLjmK%|7`A2clhPHnl&dqE*jFFxv0Su?mic>JP*` zs>H&tR8L$uS+iNf(rBte3wA#9dGv;RA#OiI_>lx`W?T^s4iEp;2_HOle*hF)QUy#P zVjD<D0-qyEEtE07+v|e+`uLrRVC3t0y> z$I&dJU_bIe_-kK;;Q_Tn08^ZvxRrmT`xn1bSY9ko1kIJy>b=wOim3M2L}0=b4!FSE z$fjZX1ouS7=jCakrkmxLkeZQ#2>NVQtWr4$CX}nlWTa~&Dl7+OUlTpN(^68*Q1la~ z5(%ec8BoKoGsQD^JBaxt<17k*(F(5}&pVB<8U_!yUTtWq#QXFGRSw&Krz@017G|iR zHb(w2RncynHVO*41e3XGf?Y`dN8ZCVVR|G zc?nzEWEyy3#T24jzr5k|a{d0e%sF56n%NMA!iA5YUdVsbPCfeNh3LMqb0fb!^S5Vc zl;MlLt$s&;AKZbp_C0k(5cx;x8PNLuztFI!&0}Y$PPv-vY~EkjQm-!8Ozyy8dlmyf zP1`rVjtS7>9tN;)Z%H8ScO<+`Ya1^|3Rj!|pSB7=lP{Afn$&1(=*mN=|HfG{$T7MDZ^9zylPDkfmqR6q%t>;~m>tllBMT-xhQYkJ#ZuSa&`JtOhOu~+hmY_nne{uA35^Zl7zuMzmvEB~0PmVBE z6m047Ff`Vz+9VOu4OQh^qu@DkWQz!o-`5sb!)Bt7h&EdS>_>E=Vf<9#&hZ5+ssehY z&-(k6HtHbv1XX}8+i^h7wMsSO# zBmjVTzLAb|9n3Nis=kO2>Yslo`_3-~0jVj7dEudR$BD91W}VKgzhZiM4eP-jm!hCJ z4@Tpiru;}2*}N*TQ`Sbyh$}?*d~bzZS0#H!D_w`KeSldAYej_?{8c7p>LO^!G%{N{ z$QXHF^N@pHi!L@uyhm*BMU|>UzJj@~-Y+Q$NmJY4r=@>0S;>1lWc0kdSLxZNU*098 zwF7T%hGjRVY!NzL2JPZ6E1b*p_@_rND4&}lG5M@B3Hp-M`bCaOdCt-E%{I1rEu*qx zxmJQ6P`X+=k`gVFJ1CYb{H9!=j9t8B?ajo0<*HgvG`HytDyNo6CJZl2r0e2XHF8>? zy?CY>X+V}KZpIY$dc;;*i~nA$_V?iR$jDE#jKIXGiMiXqI2kw; zMAXL#H9#nRHIobk`p^n0B#e01yU!RjlD%sPFZp6#0x`ngADf(`8Y;hM{?RI~O19Ra zM4c37hKih`dUY8b3&bqy5r7M%J=N~7y}W_T=SK~Ldm4QjFsvHbeL8kr%DZhqIl?Fc znLOG24TNiSjW}svLgbET$UlTDD^uCQlZaCWF*6}&S>2w@gF+t~t}S`c_~WOM*(<@37c49oCnb zUJJoA)@;gxUo_@Gl)HoNI6i-Yii2!iTp_Z!WY%|;sQb<1K(-=6FK4I;<~fT7>ap@1 zY0Ry|R0lpP#qxqY2I5C2-tV9Y1W*31dx|z`@&!GY@_@opt>JRU5{gp%h(E&Rvp-Q_ z1r%hSV`=1tD|45)hPm~`VI=V5VKJw{)Y#bZ=El5q!>ZNSPxqW3X6AoJLh+`@J*qyl z^eO#*)$t!m+qOGmCy7aoDF(|#Os=0i&jr|rqWPq))BgxDxcd6fS~jl&;*}wcFGMIa zH^Sw*BG?S@YK)QH>kYIksukn=CB=WjmKB*gA&|#IF=oc*2yn_xh-rJne{(VA;PVwh z9YLd_G}F+4@f4;YYD6;0Bj>z-euqDCewuJlVphps5i)iAi}#fM#(wYCv~;YL3WEm= zSBsrnXti`f$Du+`J|t-5@tLdN&bfz8bkvte4+c+$ni;t9NdxN*H?GU>4o?0f&1 zqaoSDL6879GiaGg6I!1`J>>%8p}$OZ`AR%(3GCHYC7M1*tOc;7hdCUq*8AEmH<)?p zqV08(#k(eWM;{Hz9rxfN1?V!FvEag<6f_K=bv*MGjL{2`kKZ%33j1HD%mwI_#b2wX z`$shQ* z;i&~4Ux)`npEz{v=;kEEdIIdfp#?CEq$*GWo0$p6-QUAL!y30Uy5yHFbrJ8%) zXdrFwO}@vcKg_Io^8gbLZ>FaInz)%^s3an~%u+g0qgaB8hB&|G8aRQ~$8#%QDEF&g zgVs}V9B!a^6=5+MZaaee}g{62w%76i{_~dzpYoIZh3Ai)vim;)P$*>F% zM25MfJVQ`XL}Sf(wXrG2Pg084_#*1MhWiL+^)ba#;@D$s{d#>IiCRE`u+~Hg;3#N7OcKxeztNDei;wyLe4t5 zHU8Qb^o4om05A)2xNLVdq(G@ooBnp;H|3L)o znU+4bNjzBnvMJx$liG_=DX-LaGDD7Qoz**JQVq5>w+=$Cg zE#pC)G~%F{Q7_9a$xhlsyT@Yr3rfeH0nI%$mi7`0-3_AT6VVZX|N7a`)!YDAW}Y{k z^E24iWzhu_fWT&Oa_D#Y-%BxrNwuA5HMwk9(d}DBXlhb>eJy z_AL>s&(+X#)-sTVi$U4TW(i~<{TTs#QQb!4K76KpCjl^kk0W`3xeZ;m^go|onTZzD z$T3bFI1PxsbZnEeIOT6^5pYoIt*5WB)i<7=0nA5zwRU{vHXP}dDFXHXT)Q5Wo_{j& z^H_i|x35dKl7(xcKdl#&=pxG6*Nz6rKcJ(>xF5ca_?x)>*;Ix;Ee#L7df|)V&y&e! zafDYFxot*KgL46EOKF&y7Cyf+Uu*V>!M3k^8kmU%&NWNEw7(@eYp(aTZ78GXxnNJ2 zh!e+AvfR%ruc{|A>lJJKWNCPM0Dc>Rm|g>+qkv3$BNf>qpmJC(?UHhfX!-cj+YLBG zxrosoozq7M3gy~gZMQyGb2NY0Br5XGzB&~0`z>NR>{PTuFp-A*C=Um{#p5R{2EF4bz5J;Q zD0^b~VDdu7psSE1V60-mJZAiesW6CX>QdF54k$#LA(~6@cO&bC+OJH3;h_rxMlQ8o z&kXGof7F8i_4f zXWkt#ePjqiz5qOafnQQY+=X%bKtdNUrd>AUz=3EVkMa1=Lnt&H5)#Is08vL7V~N}4 zF3csP{&AnW@<8-Z5LGt+lTOIYQ^7(`hLm{Y_T%B{7j|O5;URtsRFg~%qUL3zKA3w3 zfBM=Wa%Qx_l8BHp%fWa(?EUcP3g5i?t$5D>u_e6bb?U_@`%ItL_arZeTpvD*sI-T9 ze+i75<8qafU^ef)rtO(opZO?>obN{d2Az^!GVldC`Ta}IeLZ*wPX zXp^yjRrTx#;gk>`>DD@C8PonT;_ZIDDHy>N&y-dI1AP;`dyyYzZ1Dm$rVu^e^o$*wxv1jA1qLGU}q@Rt#OOp15v*-y`DnXcLQykfRW@c9_4!PR*7yX37lU)6o$#D14 zJ4IW_9lh8s&aGT*l@om^%;Xt;`-jlg<+v=Erc$G4&adkk;V6YR# z;Gue%;ba5yKGTe4We1^IJh_x9t0zV*4gSs*JAat0S?KgxJp-Tr8oXntx1lKf!{KVy zx|IQ`6}nSqd_m!^jTN4rm^+tMmUp&m2#+;%;ZK?UXZ2(a4XXuxLNQS+vcr?qrJo~1 zX9~5MEtcrxa!`1P`=$!aM4eEj|M|_T z&OmF92JAtsj>+*?nOLKgD6{xw+5@f%dQzJZ*VXv4Ts}^hrhw7_j9##aFeqr}fxaR{ zy8ta_D1bGKWE&Pwn}Za8y|DE(Uy+#o?f^tpoEf<%ewnG4zC9uE>?xgmq~sx+j5Y0< zg$BnSKnkX}9`u4OH!*Idi;38mDT~&zGnq~235ljmqB^ptiw7s6a+F1sS;OaMxxRMd z4vVVAfTJf-FlXFwzRg?Y>8*>J?r4i<;lMA6vmP(rQkGR>S!4WKzpT_WWtHHkMBN7l zdO=AM?#;0ylJH}aO*W|^5ubZOGQVwkDA3_#U5REYA$6w6jL_(4-l}`;g2v==OCy#o zA@mbu*OzUvGz+vhTt%vdt2I9r0Cj<$9I3eXn-m&MuZ&)F{<~N?B>TDlc?-<`EG9=+ z(HZ`U0S#r^`iX9MM*0SemTxdIe*QGRkcim%P_rhm!bfxk6d?ip;B{bQ$0&*L!boBu`a~_D~W0J#Aezas94%ktIsaflXyF+rd z!=@C$VwZkMC|0*)z$12NCw*DOsTwf70!Bp$(Hy|}x(6?b61pbAGO7_PebjwD8 zH35N*xjD`W(`R|7n&utKJ90;ynEl_wrbz>@=m_ZlCWhT8CgXUoIWF7nt>Gbed8w7= zNI3(49#%ukVkQHHXcn)iZ!v#}q6+qcY4GeI_JD_xc>`I(m5to5PpDJ{4D&w_WFv474EpO*-V&5L0o0?3u zqneX;`Bw*bA+ZI+b-f(ZedJree!mWt$8B<$rNPQy-wsgWba{c97jtXfeKvu;-;yv( zSXh9*bkR}&+*uh2IWsK=Z|P!Ygs9<|hu{#y(^NS%k)_1V*=#r|GD%&#yt4XIFgjbq zX`NK0n5#w!Ev6JoN9x=gHWC{qnf;104v6GLyr*TJd zl)JoAvMj&!&wAeAy94v)`qukvhdb7tXm&RzGDb^Gi;Ll8!P0?l{0Y7uYW;vXtRJ@zL>E&Tf@IQxtIIpHyOz{o#CZ%-;MJNRg%*{nyKg?6dD z$M4~@dh)tS&LHI7NXGccxmX~A?7Le>kKrTeNqMQ!hdq4vvyeo0dJU?;50s=V|JWhYXq_-(~yT#}!sd}x`G(CUn70hZ_;|3AiEkRij49*)@vzk@-^AywPI%{^)pv^;3vd1J?U)eL0A&J z=FX=i^~2JY1_dqG3{4`|bqh6x8Bz_?Lr2{s_+Ld`XNb~IlqHGc@rd7&W;lyz^fDUl z+-u&WH^3o66N8Idj6zhocNwJqlPjH5-dEePdr}a$H>f{z5|)=Yi?x`((IF%U#&6Xh z6BuKneGXqgp;|pI4$ni{*-2%q2L(g6rSfdcij&FF$>FgQ#>zFqmMo%{91!GaC&`z@ zt5xmtBEI-iojsWX$yK?aq-JDyGia|!&k4VF5KXXFa4uaRV#Qzj#n-_5P5CbM`IJ>v zzHI&&@EiQ-YtPOhGQ%DPOwc>F#yhTF7OSdfSi~-}&^0x$IJRpN?NLGe0LVs_$}q-% ziar*Pz1KI3TyQEO^d^!;M{)dAJfe*jP0GUtd{6n##8xRgz@VAMB;OQ-4k@=bV94U~ z9@rsmg@ZxQ9If@&1xY6@sFaRYT%3YDh8e@buyj>#NZD$=J8-b`A`kDZmFV8nCDTFk zw4H}j^rM%>Jjf&i92)+nOhDj{ZZUpBecJt$;qzNe?#K0PcsPE4S*!}F>(zi#oA7wv zwSasmykaBm1u4v=##$lA#6j=5G;-paWN_mR|+HkuBTZeh%MGrdo%n zge56E!0>TrdF_+@7bNj5N+sj@zezNUvofb_)DwWvs)8fP{nk@8*bT`e>P$wFz|Ia9 z*|CY=l_q@5YL%b)QzO7Agf~7HK)$VXF0v69X9wgMm@VAzBAPu|3<_4|3nQnSB{*$l_jWB$@e^!1B36-B%udJrnrN;n7Uk z;HwQSk}O(AuHWR+=VjnQ3k%2En?9me`tKKq48iS(PP54n_5kq7;MzxI_ltRmQXd3` z>1FInPr5tWg|oBsU)|dD1j=z)wImPRzKR{rrV9H2N#A$ObbgCbm|OirvfNzO_V+-L z7uXK>HVvdTvUoK9VA5|FsQR@{ zMfny9jTmX$r|9QJ`rE@J8ZXkR&eu;A!gP_{lnsjNuZq$%w;VW+AEe++kzA4Wj;Z%f ztndwp7;8nJ%TH?FcPB*=9y$-IZqbqe?DNdq zKfRumh*tqWl_W-tvRZ_lEpipNbmx`h@r~b-{%My_NpPR)lgkjm&5$%|-yI z`Cv*X=zgINRA)5fMQy4$e8wUtw_wxlQ0iPTsV>lXJwYlA^d4UEPN?@q`-9}+IqIO^ zeAS5qKcqHnF9^Il%GDjL$8n;cKEWx>5VS+8oleFD|0!eXW^uMgqW;W5O#9(el(W zU?aY;g%AJo4TmC(lnMTo;<`ky}MbN~HMa z_4rL&xfRJRXwB5~&1{qO`F2};J`kh&U~u@yj1DqaS1S>XiCcVsbB2DO!V~J~BkdL} z#nql^{tRDSv~aXvIU4Y4@C<`F!O{NSzqsJg>#Tfvp)~k7xIPa_-N|6@pjmE*jKLZV z;VUu`Hypa31C(GOy zo0siz>G*s;jFMmyJ+8sw))$v9u7Xz7=iizFM}De&!hEeUF10uXXrdoe?dpa2C>6Le z?A32Q;45093$0xn$dVPK&yq*Yo16G47G2xbY_^Oq>Y*rTO@q^nInV>pW?qw5b`H;o z(RfjR(+Ye>KQwM#%?zE|H^$&q#!{%u3IO-n?=xf1`Nt!=51su)FFza@%LX0y5F0`F zo~QL716tfh_@64yi=V+`?ddC<gHQU*=hG`?yG7v1i){J9r{;rfE&$;sCxdHt z&c5Q+GuDe7b|U1&S@bRAj#HCP4Q>2-p1)aBeBR!9^>ZBd&C;zd2<@sqR$_t$eAYi1 zQn`gKUTxzVPsYxIZ^Fa#y<(#LLWvZ4Nk%(_9Of;8^mgkrrgX-kb~R6wgLas<^12pe-(Iyb5MaOWQpSG@K3?Cwf z7+>J-WRPIptZn_!77kaMsow>w^O`4Td&F7n)z{vs^wXPdM!$92b0i^?cMY}XYzz4f z3;KB${itfkyFV@84>hF-QB6X@*8SdeBW*dBS`AUh^Ka+tBYxifFV6)0+&nfk1H@uU zIdRM~^I`Y`vN2518&Mc8xhO{>CzRB`3j`Lyx#TapV**JQK@_=feu(|%Hj0=fK)D)P zW;y~8Itx6n0$GKu_{0zcdod@u0%kHJY&C)J4huqlX_3|YPSP6}yN41}ajp`6+1N@#e*Tf0Z6BCZYHLvl{N))!bngzMP?F~(Bw6t{yph@z3~6?gG8 zf6-+}^(L+HlESY9iU_{Vc<25-A+w~cI; zNWt`EzT3n*9^Jb~i==!8sdN<%gA>#-+h5p7S#z1Lpul#ws9m;^mN8RybN(1zZ<6R6 zh>H1Q>rp&hskj)2UNc=MTst5=_b3Yw^|wdL_y{R(OQIx2`M;OkK=G zNhEO*%B$OhKHi&Q92xIJV;A>R4%A+SXOAixgZQpz;nJCG^MKxe|G0;mKckA?;fUO2 zkjrA$3&?{sF=b=$H>)Ce2-$Ps#!EGl{~=ae#wF94b)0;CoAK#rG)ORZJ2XkSEs#k? zP}h|x$8ag77@>|X%jdTme{t(z{jOZ9B+e$kT%9nJq-smdsW6!M603RLpg%N)PR?)? z9#(k_ZDf?4`4-7UBe7-X958G@Tc%p6?%&z_E5i4*MU&y*U}9GA zoP@=A7|r($&22V`wB}qCJl55T3yz+;rt-HerZ3g17uf!{D(GA*1~4n51YNoWxQ3eI zEMFLm5E9(wJ!HGyc(>SU(Ygzle&2@`@c1=4hKZ&Nf1El}5HH*&#YxJ3wP_0Np=+HS zs?i}8NeZt%AkAg(ey`gr>cm@1C0?(FsHLbc@f8MS@VDGpiixJB?#r{qGger#5XHmL z+50j1UOiZ9N9e0(M(P%q-0`JD?^4~3FZ%I!YyjSUZqRi-Z{6tZ7d(n2cBfy|;7@QF zPgQdrKQ117d!mkrArWi-VOCMzZ~hVa&As28(-QsXuzXenb71d5(vL!>egKcDk$#dBuZ->ozP%6=^R80A>d20nl&AFQ_<3U7E#F zed`^XW&G$lB3sdCJfZu5mI&+Kz$Z;qNoe<&Z?66oIfA6CHAuHz>~K}9sc9{hc#m+0 zV?mnEu-E3^+GA4394Wj=AohalOm4{A3I>@<3{eI;1gzgDjx*-tjvaNvAc%VIG~N6| zmNLrMWX%7?eteg@QZQ8E)i0T@x=Yt{lmU)ceGibdc2ctr93Fh2KFy(zuL!8$OjkzR zTv0*=r;$%PnT_!zuA?-qU0!vyxrZ;s5YBsw^dMnP;y&_`#9mD~x?!peOH;wdM^g+G zeq7r*C^!gh(QGQw}fP3xY41)tQ#5%I_f=pvyuFGsh4RjXXFjDf^w?-0uHPK3Qy zmDC(DGTDst{oq(-<)}drCD|(_ulR6vR=9i=Rnr$I4+wD#gA0R zc)7huUM2Wm{Ml{tP@&nD2*7y8Gn)w@$%3t7p6WD*o#f%yq#sT_KK@da-o4-@J!Ir- zXbcBzCq#&xw7H|IwSkp`SytR6LCu$UQ#Vrv01`=^$iM9v7QPh9@Y%VkIHz%f8(23v zwWOFeoqIw~eTP-0Lxi9nKCw%PlzM-kz0hBpWvi`Xr#BH*)qI=`D)V-$h%FF1eu}?u z@b_jS_v)cwhlUh!PQ8EKL%}DMf|DS1*9w-~q&tnuv_T|U>0#8>CUWhH5ks0);M5SP z(oW{Si!V+?)a1CVfXukk^ZcI$PfZc;^o*xdp_N6)1ev8|p`oFon6ToF)9y$cI*9aL zE+(2?>c9fBQ`l(7{9!~8I=tLu&Odjk3D)T^T({)JnB&SvB*d-(!e#{lv2!hxs|Tq1 zr^NE3dyrGys?rL$M0SYin!HP7VZ=#Xdc zGHAebvZoI&J1=K$^(|4_3U|XVDfw|tY8W(XrB@L%(A;42mut9g8T8RfVopRb@v2 z;>!uD;2Xj!x)(`SuM}?b{#_@ngC82V#Hh#b54&_1?~+OeLIW0x$@O06nYUATS{y*WEXH&h*~-CKc7Z zWqSQy?+yi*I6*(SYOJoewZAHzlxf$|%Q8l*O9~-_Wb1K10JxR6%Zv_F z|5z3Q$Xb&BC3Yud>v})%vvz%ra5_bAxWsUgo2NXDn2`!PaQN+wd$T8WZ3sVQCbDOq zhUs6Rj-V}3N?N7&Gn9}*Y5!bSY&`^8q{{>sV!Cjl-ij!baoJ3b+8;pSF?ou>XVQD^ z=Ui|HvtgP4MviTq`1VeYj*o zmMot;l8cdlg_?T~-d%jkuy6LTG*xJRBv0;RE-|U@Ve~jB?DS3X3assIdnT-GXUn=| zC@}M^QcIh(ZWzPjfTKhI7&5m0FtN6-jP*t-FQ`uvm8Q&Of#W(EBI(!Lq5#5o8bW#HAe$cz zoznh1t#WcuuWr$ij|M2g(+n|E8(xs2;otehS_!Xc%oOgsNqRY-AmU>tJ5m%U<|TdU zQ@{|HUSa}BnUTc7LtPS|2O7t*Ug5bXWGIC<@YJf?xNoNP7XG(ycygf(jjhbmQBG1X ziE7SZ$G-=^s)Cz8L!^#^Uk;PDGTM-K4h)fEy_^-ebZzAz>JbFCX1 z>cWP2Bf~OvPC67woXx~<-2E|>eqAGwer0pX)L=>_-c!F?BoMmIggNJAE8@lsqTD(R zb)WE)pyE%^Nx*i5RD1*U2rhauDmkH(rMdhv!Kc4=fo+Gb#1L0iyj{1 zRI8DAMW8$t`_nX3nRk|Ng#U{b%~{UanH_bTRp{>uJNd3~GYMV;;I?RkmC_~wx0zlSrUjM}94ZX!6FYU=zgBDCj9 zzX@xL=eMc8p<7Apj+VJ;zT!M4JmLwq3{Cj|)US+UHMkQeE(0l-7+7|06sad8wE2T` z=>Hk;Dg>BzK(17p*}sR&7ftGc5(K+tE)RZyA+%Fq4reR?W?-YFFQV6lM=hgLrbSEaLzGd~sWoDz!aA&TZCP*F__ zIjJUZf|;b)rq6K}FDxpvzOJ;nmjuU?`8T?kX^W?d6f`*8>_8$8)V9_y74f`f zr|jSojS(at({ca<#X+R+T|OT+=~U*wTCZM72e)5y}eW<@o*L=eE5S` zT@0Jp_}@dc(n`N8C3yQ{3yIJsBPT~$rd2d7l{aN(gkRILd`=7j0Apf}MD9{zQ5w6v z6tNVun&epcHh`2+HWz|DID{GJa8(7(HBvRYcKc&$dJOgDK0=Z?fq=ixPxxi;64Hk< zidZ2wyZ^p_}B|M)=7uEG)Jp$+d#W;iZ_*k8x7%z)VSQsg<>y$P!AC^@C-TIe1+$Ow;4SdRuF6;v2t=pnZVz7?HOMxJsVY4n&q&|9SH_}?gSdZ}8WK?}bi#x=Ga#vZo& zo5|e52*}NetmV@lrX8h%lY^<4aQqmKTuN7`H{eW|N)%EEQ#r1Ig5tzqYegX^YYtym zMckEf$#KX8fe5NH)8$U9C>kDNhfhnEOsG)vLj!AWm_z+8iZ9BJM`O>Gux;n2t=L5P z+360Ovv`G&i(wvsDUlqouN2KH;bZRBm@YKh9FSNISz2lu38RjgER7-8q-lj?sw+HtwfEOnJpE6QqwtycHLzjp-2ZGG^;2--i z1&EU(ieuMZmyFSFcs6Xeb$boAX5|)FKTQnet}@K7Zp2=T1Q!DIx#lJHXEw@p`o6Nl zd?^1aq8<80YeJyeA_#m0bb;gfiq+)Uuxt%58VUYNOC7BS&Xq=15?u5Amn9Mn_7ss* zA_W00z`HVnL*oRiE-iC9NpfAV2>9&t#)s!nA}0@c3bm;@yO&6^pmPBv1KFYr^DFtq zJ5*|Y@y!u*2{48rOpZhXe)|iXd{GlmHJ|!^!I)zW$^a_M?Zz_3$Oe<&-U{5^LCmp= zMq?J@u|tEoM%LiB@%Im+P8ei(O#vdh`UgeEli6E+E#8^`8u9d7GXqKL6sgtb&!dA>fpSHaL*6rBIurB#ewAx!fudMYpdMpEWy4H zQeWi`4l<*MJgfKL=Z(OF?T4ka974ue4{lJh8}9#k0ai)>bou^_1EW91#QJD*iCF1^ z$UmdxE}7p`BB5RpK}IH>mAh>(`pQk_vc<(}e@!_hK@#$(4v2(Hy6y9YMw+bkY--)o zTNy=`#?t~DR?fiXz$=8xwPFohb$Ujvb^unD$M`{Z+^I$EpXj=kVy(s7*9`^=PB^ud zlJbgSW#pa2o@(E53c?E4u-(Dh<&XRU?e=I>oEdS_qR-$xD~w{d;1Q#X2A5Zchbb5h zj}+xoDDJeS5(Cv~_B`*7sp3@A_1Ea@XOh0X)OP_Mbm=h9in!CGE!1qGy$*3{%-HLx zZqq-XFurIiyd-PSKI`wH)pqGe^M;IkK>ci10i|P-_zbjC=s65E0EJLv-mn*n_{?EUIlzdHY^qIKh-2+@q0tfTR6U!{I%Mf30Ki<5ZQsx!wB*o2TiVPIs468G=jm*)9IK+sBhKlSrCui9 zGjC)_BG27(@{QYZy4A5D)FU>IU?RCGHe~<#i!O`lVlDWa)Z+dV!!=%nHR%ggdb?i2 zAtq%FgWMjiu~EF_yOlH*`Cu078JizJYTuQ zVpBnS7L#*C)Ww!J&aiT=+tlF{^OPwaL@6SqTpy;^u4wmF3b9hh4%?Or`<-hyF9DsP z96NTFdx9ZQdi4)H3zvYvHrC- zo3EMOREvtHORf)ETQ06|6stBGY9vPH6NeVU>1@vyaDLh*c_bA!Z(~_z*bwFZ{e4p_ zM(v$0N@M`ZGLoBIP#(E**@ z+~x=^3k%Zw>xslGg(T%asQy!HI%e1sL*px3sLFu~_=dp*6RVE?%%5o%KA8O4bl9GR zRlmPK04)%_ByV?1opC!+e%HXuab?fgh10;a8TI2g6TXk6Jg*B6w~%P6EW)YGaD-D;9#dMpx5n6J?B*l9>sN!-PN>s}QW-C&6~+@q+4;Z= zO681}4bs$33eEPa7ryLCwkUQH&nz~Ne7gmO-lPs(MdKzjAvvbR@>=4*r6uLAN!7c< z4`a@9QcghsDh`^)C?6IV)3N}d0R58ZX<{*Z@zj5ndELn&-{Oz>g_90Et?X^2IsHf# z@P;1{=it<23b9r`n}FZTxGEHVr(-#S#?0fqxekFANtV;ZD#)@1#fFD^&rg#H@jE?F zaX2oln8gw3%8x5xV%s3k#B%Dya|GS#5);}WipieHgw{ziR?zx5Vp0Ditpjx?igBQa z2bB)fiYFEZ>HVeWrdt|gaaq&JMJA^VjZTEQK@#XpLqQjp(zst2kISG%i0Vt_4ZhQ@ zo|QxXi0#kY8Da}4eP<*f*u>VCV`NhULg(bd|j{bH9Uj+rcp^fnOsJPzE!=JvX! zWAu9?o6I7}eGx^3Hn$NaC<9-*;GkWF^YA^tk>BcL)Za4S9z%)*LlYO^vTdL`p3=un z*cr5W{jSh;Zr#?D{NZD~f zy&!dVhU|K$E6Fjw-l-L~@G6s-nP$}rA|tO}DChpYJb*d=vc5&?;(UdDX!)z&jw`yF z9sBuwXjpxOLX(^BYfb&_4xB+(htdZ3AFZHRiAp4Q0ctJikQ9=U?+Mp4hLa3T)B>o& z+_3r97UdtwAeS%^J74IEc>5(V>SxI(?ki-$kCS{IsqEq(ZZpfYn&uNy^|rhv;It_W zIcKI;l{UDll~9!5`u?3sm$-m34!l)0&B#G;o${{VBE>#iRPNpnL6>o zCmdS54-tvqNX7o_Mjai`Nk>b+ODhSkne?mI@i{STc2@C)7I(n)(pkV0f&fZZcr}>< z9nhn)8pp9U^dzTu+6;)k~g#2a#Is=KL~b3clt-sX1g_gVrNq9Fc}?~ zHU6jNGcbE^WfLUxTGm}5^jTN*Y7ejy1GJ=)1NvQ-A7lHoc1Sl(Kwjy$bwjd_gnmWT#u^8scEp;rdkE+2Vf^l`Fz?Bfr;5ALjPKB45C#O+^G z?DA5P8%KaF3o@C*3$~9ibWhFB292>w`CV;@=aT3$FuhfbykLNYjeq36SBq^taVbsF zz*Fu5DJ7)U-2r{l&0~0JaSoL#2^zJc?^6|-OHX6>Fad=H0KE*D^ul-830@G?Aa-;* zf#02z$i8cF;;NE+yKL*#@^u5dW}uXAdvEO;NAS2PbBzf?U*U-;oQBfiM81MU;;%S3;Weh#_^t;j z($9h<3+bI)%m)@-=Gl#$DX3c^5TCAGv5V@J`$&yxGY=`O80oryphKhGIroPWm}HHza*89=8vN_CUxC zUg10v3+~ZBVe1)91PNwNbL+b9Zvp-P37oKE+j9aUVD*X|0cMQe3|Ag+ajF*U;HNAl z$U9zS)9U8REuM6J#~hvGr>)MpgDs)HCJ~`bKsyp!46eCR`hdA;El-aEilhzJZ=HgZ zavc>94PauJMpKR5w&1Z}60>DJDT>3b&}-N%xfl>C z`OMbfgT>qX7w9=*Him0A{|; zlPVS^daw5p-e*LR8(I1f-M8#Ynd{1D_Jge@=#aV}Ndj>l%uy-_#Pz)0BRxwbBjurk z3_7SjW#Hj+WadW&ZZfS3x1>elfP&1gP~hE2Fo{XpvUWgn6v zldb(7O%ZiRXVT*$@7bdjoiPOf8W-at&|G4f>1a@zfPSQC2HcGd$~=V#O-{+&9pb2; zB9_Fd=&Y?L zz^QN7kcG|Y>MO@8Ix$@xIa;nz^r82eMONT3zr&23>CE5kMoEKe4V)h8G`Lj5YPFf{|J>tt?j#1uGg!t|O8>eF<&tuoDP_>u!F$EL45H>#E7?AnorXiIipNU> zC)nrA;@ZnFVt7PCMC?L=1+?Eyg_TTqc}KMf0?dJOirW*{o%nK?vSBn=chu31Q*yhKpxxw{53+|h?j-iM#aWz*GHRmOYod8k;!5D zIf)U;WGG`aYf5lvk6e*PGd{?LfOjgs*;}m>GkqN4U1BE<6;-t}2DLjn{J1+om)&5I zo{>T?R>wWzmlDejjTHSkf0LaMO(15n4^;0l7^tyZhM24?+K6Ngx;cUQWFa@~!j&=k zQF#h3MQ@77bC-lL#gLgA`Md7r9mgijLz!wz*Kb;$X3wxZd5O)c8a5p-;d7&>HdKFAPB^0-Gb?gjypZs=%C#O)oh#xg`so}?*orH z(V1xli<6S-CBmEN$8g4poEa)|RV@@bqjUZLHV+bRYMq~5Nj#80f`O9E2aoG-m3QZ# z&rft}jK#gwO)HkaGZQH&Ik5_8X?O8=J8Ph{u;?<`US1x@!w>IZ;S?uP@mYjTaTs*e zBynVby7Khk9Y}PQpo;7bp^2*2f|}Mj6`Kl#HQ6z|l(cp~$3ae#1%ez=5)eEI@eqwg zhcb#>3huvB$*HRW@TFjn3qQywWI@eLWGtd>cxB$#yj1v-oAb<)h`N$#(#2avP+L>s zjA7=jMsuk^h1<6D|KC$%H}n5^YRdXyU3n~Z)snEOyq5H@l>)UMbRrg_m&9k>*X46~ zGdOZ%kG-Z`m&WtRey9{Q`O_?NR=X=T44}218 zcG^62uS(HwIx^^*JhM+sZM$ww}IFr;mQ=k0!CqLgEHuSa3+`cm|9R zbqIcnn52DWzA>~iGoXhi7L{~b#BbPJ*eXe@d(THBh@w)v02~p=NL+wA2|pNd_rl>* z6l$v*U~8)ThwS2FFNc)%22!vwRJU5AYLV5QSPE0NGF+7lK#p!BPXrxmBAO{|$ea}} zJhKg`^%@<&)h(9lk9mO7{UVnzEpYg0btx1`)jBKKNwXWubG$KeddVa+<>)!(KJ`VM z#nMv-^N$Y1OG{vZFnloUk1t}uUu^!gbIe(0PEhMIdUdMg;cp&GY|?}LlC-3>wGJf= z0Dp%gW3gP=ce8K1E-!g_^VnKsb+k0Sfk8}h;+F3kgYP8uIur{E8T<6gt#6k4Ib*Qp z4xGMGwiBhUY&-6tn82>yfq(q>@T>gg301$buR|L?x%L9`Hju;$xojK_ixyin(*h@# z=qtmuYE06JHQ6XonDHKHkY6F{%z~e~ZLg9RM_ zuGk)GvTEJN*L>qoxJ5aFE*<#I`%vZWBCq>kzLw1CJ;A-!Z+xmoTemWc#XZB_r}u18 z1mtx{_;F71H0hxo_)e<3<+(yDA@xsGoVR1_`10fH!-e0-c}#5Aqv@lLW|QjDXYT!P z(%avUt5ux1y%0z^ZzW*IWQlFe%yw+b`x?B9SaL@v3`(V=YBdX6dDAc`8{9a0bDDGK zY`0KxzBi_iGXICO5$G{fMls$hqD5r(k@(oT44n8`-ArkjVsYG&Pd@fXL2KO>N^35+ zdoTE_I0=f}WT49DMfc4D?v}IHkePaw30G}>Q)mB+9>SVW-06-K&dX?o-_zDRwq2>zz1(%mG$mhWgqL5!$FiBaX406)nJuxx9IH1(T?#JQ6nh~n zS)7@sk_->iKdIRhH@ZRun@a}iGMTdrU+=;w|NZoN{xQa%fBjk4vuZs{)c!UsnXQvz zmBR8`R3*osBc{qM{Q92=qB60FU#ke{bC(t!|=v0XN5hjWyTEBe=Bu)Dt<Qcx>wH92hmys+*&@%9u72e%-tJGv8sRJeVnV-&h2I?v&7RP| z2D#MY)Dmn^er(OeZ?zJN92Rrm*X*r#jz`o}SFb)S;aq$^?MRo?TdVZ#X zqYWa!STZ$+@-&4KdozL)6%NM~PINAJ->8Ed#E_lTkav#W+bp7UdlNiFO^3>~XbRCu ztebgCf-`oZC#JG8i3?OhS*>aQQ{I6X-R6WF4urk{KZdgT>d z-A+>CBvV(Dj&3gAqay>Utjf7Q1>ox@6~9>^wXz%Nbp@Q!9V=uRAf5>hLRTa}j-)PA zj6f{17iLcn4X;|It>o|S4(!qDkrHnH`;!@0f2*1y=|A=_K~gBYYi`Ff2XhSFY$$Jz z{+x&rGS^pe)mVPdLw^E|xcgtq`CfF@9V%eDPP=Z=$&rpN>JCgmx0utV!u(SL(ftGO z+QLYfvYS83Z~Oc;5?wGwirj=Y$<9Lacd$d{@`=~D$B*aWuqfNq@_9l{J1uvIhc{Dj z9JwwRpgr8on)jp+YJ);a){3%l1y6)d78|KZbAayajOIPU9tJ*;SgPJd-ooIk%ugYZ zb?haLH$*WD<;sJ>LA`_SjGBT6a&vf%wIH`e8X!>~zA@C|G($Q_@Jbdcfou|9V_0)@ z8NnlI(7YaE#66Of>M1FKd&A0e7nJbVE7x!L_=sd@AR`GkDHIII?!Dc8_OHw38j&lw z8c|2vLPDU3GUz-@7)*KDE7|>JYnookTf*M9Dgkghqw+6G0v?O0iK_kkC-u=rr5b>4 zL%@-R3?G7Dy1$!hTb_CCiNo>593pfGy0ezA>z?IIQ4fCqP`2@R2X6OM2xCHNf2R}P z`Fm~-IP(U=<(?OqZ<2v6Qem|H=oNqJAF%(ruTeRfYK}K$< zqr;^0=ZmUljyjk({1g724jlN>H2{MmP76QQ<2rhjrjVqFhX#iXC&X!J4ESH_7}8C- zMxTjGIB6)Yxbyi5|Gggm8u!emxeE^2lv^Pya17d_Ias5jpgblxoK#+|81PF4GyZx| zXwV-a=aSzAC$Y@jMiK`uG{n#{;&XiGJZoLx?wE>e{Nk0-@cqZaKJ0nrN44kdso*6i zSr*Ao-nsKEp?a)DY^hXQgd|l+asgTK*s_)@zE#U3Hr!+UvEiT!y@EnR+R1YG$7pcZ z_q3Eya^1}Qa)QhcpA8A1&BY0PBJO~h0*up`XH+O7K01mgU%Q(uOHH8+w~~mMyTeU# za3bQc_>Fh(zbjN4-=XXWQ&R_+q9(;gTMZ8&=l*Npr+$5oOp{xy=Vz)^$RTkc_vTPYE+7N)MrzYa`;6+gd~v{LqC74XNV8D-AEe9sVw!Hdy>! zIa-n#2}q8f9ID7RWigUVt;6kgGkF`z86}&NAsDzQ_!#i;Twf>yP6ki~cw9iT?3;g= zPs1^%0|e8Q(@r5p#!!$eam*qk%86mpi_5}4@4Q6ePPouV2MbP(+`jRd&A#2dYJfOt z#&U5`nMioq&B3I$5der^L`6=zWBWT(i)5OZRuTZ4+#H}Jle$=!E^PANiTa!Ak#FgC z)z&a z8u$YK>?xz?+wkB$!@M4Y^^5VWBvS}@b?Xz0=HycP0^GtbWWkh`Nd6WoB5dL3B6@j5 zf{P2~Qxx30QTVXwwCwlEQB&QhONUfNDrO?dE>C;@=<+p!vH5aWX^MCAy?Z(9qW4Zh zSv$=}i--v2pS`pozq$LJmGZ--vs2lWXvXcJx;9>|ZMXRv= zYP+X;1h_sh+(yXpvpI3*TI=PvVdes68l)>fHCUw=?i#p07jbdjaZ9TC<|$(C?;BSg zN8eOUu7GO$iuf=~7dOk?*Jmc({1HWwYidP`&d5`ak1y{YANWCaNYj3nEuH;p@y}FJ zZaH>|`~SQELEG5w0t+>&t52y1<3;VSpBOpoCRYoe68!C=<24-mw(9lJdcst>7aW3b z9B_djP;f&e{!M>YN$Z+6EzDMW*nhTCRj-LLG?(+zPfF#b?1{KK+7Y>n^0dvPOecev zVmO_BwJR%pdsmFqa<0hx>YXn(QYfG=6Tld&r}odVn78b;YdFQU)p??z+yvPnucOP4 zs2mojy1=yE3PMf2rK2W;HZ4}o{3~+4Z$7RmYoBj$@xCxAH#C}yj#tMrgzj;Oh#~_m zN)SZIWeSI*GTvf;H=l`7eFM!OyiYlPNqO;Q4QTV! zFtVy}VL&&g;vb-k!lv3iPnILi3-%nMx$ak$vmwf}Gk?o5luByr<3w-oh@KTxI5_{w zmte+}KY+;Xy4N%s;Sn&2$y_uL2dh7Q_1_1%LALAa|3}kVaK*KC%{qYqL4yZ(clY4# z4vkA=!QI{6-Q9x+2o~HuxVyXi-RHgI`vo+;d(6F7&8m8UMHICZGwI=miBpk%7ERFo z>lRrQoR$gNV70O%RLOlRZ$eHbqNHR3pR2iBswqYU{5|{XK7~eeTjOzYE5@6n3vB`a z(=DzC<;&_H?~uX7Y}lyf7bjoCO`Yu8cpUVKKv22SG6!HEp@9&iA=0PRZIv% z_qk!;TJwA~BftOlj#H^z*ojB7#|%nJB!?JgQes2X{pxfCd!OQmOcSCzU^hZFLVbOM zI>eak2sHc-L!8CC01#8W;65c$t*?L=kP{KXm_Oh9*wDn^G!1C{Ac-b|4=aq~3L#{= ze<#q9PYVty(Z2b6g*F$nk6Bb-m0wPi;n9`GKq2QaQ_8U0jtyY7Fhj`l6;cL*dVetR zf9R*J(K~`A4;9j@cf%#lv$s4^NEHUFzKAM`;|b;baL*n6;h)9_;8?QAP&U)3Lc)LX zbqGk;3j$5cYoZtsh=l=0LSai5C4=b{&tQe^4&TKwv(;9Z`j#q zjy^34zF4dgmRQ*jppU(c+P7{PI!>A-sP_5G>d;=5i5R}~*M+$0 z=^xORGHew{DR2%f#5s5@{GGkS)dG$l1T&+5(nc>Dd`yy~fjRJY{OpxuMrO~(Xs)Qr zrt4b`g;-CPAMpgGz%oPtVvHk)JyZ54k>6N73#Rz{FK#!@tybpamZ6)f*wyFDh3#gO z%zt**+pnGvhifkpp8DtZJo8eP8Wz7g29?)}Ep8Nr#xrL+HGXGH#0g@u)YILNNfrG6 zmrG6VOR$7kPNL`*pAfZY_2-2D%11OW16yQwCmm2Q1$OrmDNg_ftxrf%4!UP4E!{JLJh2N6Iky3@!TUD`~ z*sKKdu#ULJP2whZ#vJ^w+}bM|niafB(S!$0wA364Z82-ubspBSTrU~BZUb%}nd>&P zY&-_4EOn~L?t*+L%s&JS_@ZH>ovY?U>DC_O#;ct($ZBWCV=ldm1NVc=K8|J=$f~b?VI5Ttl^$|5bhqU$wP%?v}scg4g zo~vxmQ7B+e-C@2+P{SN2<>{B2N|`SufgQrWhmg78SEq=4AUH_s37vq~glu&R|8N-Z z98Eo1vaE-^GM0d_))88r4gMW|SMC6D*y6wdFCT;oWt?mk}U^;me8tPGym&t{4a)zKsYwWbL#9w~gVEow5Bn_$@O7;Y{(R#I-4;>yz=a&)B}Zq&<@n)0#;;u03)t%MWTp3!-}Jbl1Kr z-<~CRKaGFWKM|#ycz&1XM^aKY@<$Xchh0P4lnyT6;}_f#3?HUlvh7J`BHvo7(qVU8 z4!^SYO!aD14()w0n+n}_xO$Ig@8Ziqy|&<_{l;6}M#s#!)t~rB1;Ynd zu&>~HBzK%?79J9;`|ZeBHY~6)wx|wD#M~_d>fkFPODR59eny~qOY!k;-Jc_^-==l< z$LaZs|3jFLd>Mg2m4vHTR3x+b#qtoeBL$3ECv;~5rsSOTq6xk>9i=#QC}OEtYRoYV zJhT})Qbr)0h%P3i)W?c8OD-iI_H+QI&g6wN>1Rzs*CXGckuR%w77Ra~p7aw;Ko6qR z*PF~L7-FE!*|0JjDWDpf4dRnJIM70mlK@8lmEhb00DfIV1hq}#%&l$oH}cF<3yn( zB}2x5nWguWBUI%ZK_%y}+Im%yy`x~)gnbg;Q7&?r8;dzv0geaW!)=K!gKh<2Yt`F0 zjR;Q`P6v1h;>coU!vTyL@ILkTyNQ#Y_bM3y~2HjOx3fx%$Hw}YsZ7HQOGprkk zkLHs#rtf-TLqRcnNp&*peFb@2Fe>2B!M*{$4318Yf_6UzR26l1soilB9!UePxYwOV z+-Tw}WRg;BraG@-HP*Yjmj_VQv@f4~H6Y0Re>Rs_c?%CJfCznr?DoxYQm0A3U`8%117y#nT00OEJimK>pFX~yF7^G5p znApX1Vv6L-HJWtMMU}4vV>NcE zrTK&uC>NDsW#;t{A3=3#Rg9XKh4$10>gZ5XsFMrMxz%)y{}n{T`>5`tG}GZE$|9vj z58e*O(U@dS0HSSFrL=i0h1mft*m%Zn$O@AM-h`!Jh*eisty>WzXp0CJDWoJX9{JI@ zXDN;84Eg&_%V}V`FoDQCb2~MGLm1V;-rnv7l|+=LDKQXU@;-}zr7;k2*61J zID_zaeoy3YsAUZ0Ba1E_8!#@}!Im1&6LjZVP3lMQj{8Tr9m*_}Pm*ckR2fxLRt8M^ zoZ&(tQxH@~gpsDM~ErQ;dPI=gLkc&g&c6baELc85@etr>JenGz#2bs zYSRGyxu;%hl2hklulP2Q$8m_ul^tc-^_5vhKW}|`ugEpDd;AB34K7tiFtAXsq*YzP zwfaR3e$z!_JdZ=rr&D@+LTnJ!?*}Ec+*=A+9O166@MdM_vUD_$ZmNst$e;s}Y@tIF z)d#bGybFhVAKarSnN=LLN$0dbq@ahatCkTt_@8jeX33xS;}W|M@ZL^R@n9jvoTsX3 z)LZ3R=klC^lE}iIr^~Mv2%pDb<`ww#A(N0?tfQ-AX;jaGhP&kkUVb2%qP;SaNae@r zf={65_P%L89WGSa>KCif8cH$cu8g4x-O!QTmj*T9ZNS17CeeKysi2Zq2k5*79lM0b ziUkjx)4SZqwj6GNFUNDar5tCn+*z6m3Xe^0kImRpe@%B>!KW;6f_^32=@C)meOmhB zj9rS~@V11|p(C-nx^;LOx3oqo?g~|3kgo+;M_J}3IAj)R!;XkMj!?{c-HAsEv9m3> z`UIXJ4>)}X7hEauz7;%$@P+h|#z2_r$w~|V{;2t4M&Yd{jKV_1q(!I{J|F&%BRfKP z4%B|WAw~vNE{95{mLLKD@R1hw>0vi9N(!3F9;11_8EXw z#iCHJj9fWa{vCrQFTB0<)t7@rMg)$e1Ht*<(Yq)VB^-a&DXKwg7N0jOO z@4Uu50-Es1`Iy&rN#JmnkbtGrF;*`n9$irqLW)^28czHKC;OZu`-E^-cf+7nrT97s zFDEsB2!4i*c#;(iB0|SFV%JoYp3(8%7(vt8`Xn*}TZM{dIHdr^!i`T|gcm&>`h7RS zVE&MWL)v!#p2S$)g4EW6tUPbEvws*UlIY+?=xXVg%1QhL9IS&vEK;be^^7}O`$}a@ zIMi7xY~Z8^m@vXPtG#P}wk1rbM#`jPDyk3~yZG^H7Ysu?i@E3TyZ|>BU~=n_1;3`ZKq<|RJy?ii$Rs-0Wx>rk|jMeVgTDpm*#N#g&pRwI~p({THLu$7VIGgK+c#$&aboNBy=!FJU9Spiq z3*_Nln5KCBuWHpY&s0f zYL|Gx8S;U0ukKYI@0-%&yAqk#Z5(i>IjK$eztAaNu`55!8?n>B(qOLSk|CVF2LB$# zPF_-}Zw7y^k5IH$ZHQ7kSG2KF)L$>?=#*xHMu!l_D#)QUh9mBt?%JbX6>@KYOhJv=quU#gs2&sqswhmo=jQ!k z#zw#jPtt?uU|4{qR&~viJkL>`OiO2LI~n-(*!9d=4H@Hh20FSL3*`Zl+wBKPuk7*gCuvymgnZMbRTg>LAu^c$^G&v9i|P%7oo9^YYzmV%UW|$aI0x);P=4n- z%BV+-u72Tkh0)2e<7sP7%#;k)_(QqvvQ;@^{WCQE!a&MJSD9*F)PHs5?@@n~Yg5(D z8+GX3cws|&1X*~~5TnV}y6q~d<~dsi2X8}+DS-a8U;wl=Mx$=BzOh`Xq1jOnJnCMh zF4iqninP(R>IYEgufh`RDw||=sl3V=zIgQ(^K-bn@yR%+P*v615ec0rK2s%eit?|O z(Q_@*#wNjY)i@rDJ@_&9(3|t>WxsW+E+oyRgvB^U4sFnye=2X;t|1BcS$Moed1cTrtoKqn3Pf zpXVv9oWICDxEIzuHE5c1WatVn2&v?#P1$WsZ!6o2f(+WNi^0KclX~^6RG{O_+Ta5 ziSq^eZw@%REnEcZBqXNsftbuMOsuCkwrgxTc;zhv_GQiAfyI7WUy#3jHARDljBS}x zq_qZE>4-1jEd)l{Auw*g?ts%slC}RYbVafvKup3#fhI7-Lg}Zr;P7Q28n2EG2B4Eb zWo0*$oUEF-JJaxpe-arM8iM@g8x|uYaVB}i9bAeDA03{ZZ;iP$%a5L1_b9Jq9oj99 zOwE{R;=rsi$x_P4oWUtT90}KURZ0%%!A<-^V2F-Z`j8#L2A&gU)j?>Eu*gWuh=GsJ9hgVGZI4Oen%pJ2v8CBK0|=#Ie-ePLlVT@TSC*oybD+(b@uMxQZy;+ZL!-g(45YL!PGJGKy6LLx5bZs;B z-zVYtrJQIo(HmAsX5@;;guucA2Obx%X{1);ZLE;7ny+ZrUjVU?MA=F5K|aY@Z&WTa z2*dRkYoA~Hn2B0e7-GX#KE;)osbud>uIf9m8qEdEP=*Gp^!~Tv0f3}Xpo}qN30uFZ zwf`D}>D+?&Lfiz|miLS}!FjS_($)!lVn-=qqdXOH@x#>hVgCe3jz8ji+c>$~C9%Fc zkKI$;?XlCO1k*dAx5k(@mCcW+PRMg#eY={lwRc$Use_`I=~ai@r6bG6N7eJ{KYC~A z1x4jn)_|)hPB=Wg;hVjs3mwLs7JCKWGgKD#7WG8vE3h&I;~j=bS!{F59-t_Ju6BIW zv0Z2vv%rCCIFs4FB=8p;VPJBoxTJZ!1N(eI@NMwHggV$wdK)n<>NzTeAy~048A7fm zZ%KwNRRas56z?<_C1+#T+zh@~G4H2J(?9oL5;<|DKrdwKQMWDk4}1sLMZt=IdC;JH zW35Sdz`~GMhe}=b&GtY%{S(T?% z5Q=?5oavH@@E+f1F?BiB-tO9wn=k0PPu=0mwgj>38oAMnqAVjxT5@#Qx8E(wz=4Z- zjXHZBzK?!)$VMIhHmT)mXM;3i!FmJcK9aL*fAy}GzKQ_(rg4Y*QA@@(h4*P|r_t_4 ze5bo%p)XR-N;C^)hcY_0-UC|5$mG}-4C8YTYG`wI%xr|(9ivZD5jyDTK+Z=51*e`& zpoPH0IdgvUopquqZmv&=MLJ2Fo$koKtvqx>ByDJ)WkNZf{Q#~S7Z^tIMK}CvMEaP| zW0Mk`_;d%@XEV}Na}Q-WwCxM;mK*_bTvEdfx=>{OfJGJrol;3$n#q<;D+((rjyukv z!iddhuc`K_N64jS(}vBQgFkqRG6HQTtF>b(aHmbPm?h+W$X%MF3^z%m!s~Lw_phmD zoVl}8sciM}>n5CGQ={x&#|O6cc;V3rx(96YTpw!cn^L`>+Nyjw?#C97WxS{r+w>XS z-Stj{Wx{b@QZx05^>5tVTD&iP_QDDL9bIc(jNf4GEZECF5;Ax@oaC`npC zM23J?e|nU$1xi7A{w#!?xQjzY5yHT9>rPMkFsYmI9rF^OGJ;aSB!F#PGBJvdF=zVM zOAHsY;UQYcXV5v{5rV%T-$VFL5LfaLIV6aE8r{F7oy4qQA&6{Wy|}`V6djg2ks6Gf z4+sSmcgHt5hK?r@?smE&m`jdutOwyJz;@{;OT`cotYohr)p%HhOmeUUzD7e5SsDrq z@C>xj+1f{e$U3?knSD?NHdaXomAM=7J{00EvwK3K8Ov8hbRg2GU%kVf9H@>Qk&%4-^4aAuqGkSfWB<>$V zS0guiHQ(cM-_9tjYMcqw+5V8F6EHtL`y;SJZJh2KL3`JfUySbZ}%VF z>ub92)`Mg&DJ{>O7fQ}7Gue~rc;|Lr%;4SNzVuJk0qRYC*hWhHLA zo(gGRH?gN{t-}Xx*qz?l$z9J1Rl2R+>KKij`mb)?ync%42&37QJ_E#tOr)d7fF$~6 zz&4(zY&q*lBTH!l^weB@7{nS4ZygF=RY}(f|IJl6|Tf{l_2GlAtAN z8a}h<@#a-!1tG(QpIQ0glenk<62x7kkXEZF@@+0#l zKmgGU(URPNG>V{4!GQt3m9P){REAUNrQALwwKxv|@onDpumw>8zpr7oeN_V5=m+JyhjS+Gv6eT9Kywb`2JJ!{6RK(I_a~&mW`MT|P6d4i_|_wO{N|dMIREDb z=%hF@+1(H28bzB=wMB&*JU#tDli2bTxPhlgbmqZ;l!zui-pcjueaikAS)U*dMGGwY z)4!aITV-pj=wO8oCR0Tp&sViosURpg%Voy(?wvk6!TJ-}p(o-@p5!FExU#i9alyU* zl;9{}T)wD4P#UgRm8(cF!!lMx0PZ%21^ z%dOd;#^G2zTx+;ky?Hu|;f$7W3+l7yfh*VPTm0Gv_T!sl&zT(0I9v>pG!IGkDHG2h zAjjgn2_~}7_YxPgs@Fpw8p))`Yu#KERZ^uZ6~z=pejF&fvPWk= z)Md(+WjaN5X$>yus3M+`AwocwNgF^l-3cWWpVw*p6R93b$#}D&@vCNwZqCjvgXEER z8>8u@9Ow#0>u0kc<57@?eI4DzSksV#0ti;>=6_BmKUxY3sp!$$g6~)+m-zh0D5JgF z%O$d4;T#bwe@?Stu5-s88R9~pU5+yiD2=Et&B3{#+KpFRYBf8A6`1sxe$O(I8%9;?EF)Wsq13%wJ!HhOh`KJ3?W|6^s4Y3lic!%jo1WR((j( z!qgqkbEJ>{lhni6wWag-{Effnd}kiO4;^RNc&H?3_VdoHO1mP-Ki)%|Xw%;A-;ELx zulssmK&bIip^rPTJ|yvQXluWLb6`IDP(+}i#i2(`lB`r9tX|S~u>qedVzLlQqNuU> z8-H(i7*))$zQZ9oHBE$ykn8ATM$qv~&<=mYv>#V_f+7FRuUCK62PZyLo@gmJ7(>-s ze8j=+`oAK=m=K_$Auu{%ZNe;~=A%<&)4-aX0ff%}%yG$?Bppw6pIi_O3lC$>VR~J4t1=ajIye49Js++q6WWuZG52I3YR2p~k4bLEkCdU^S0jBY6*` zk1be?d2pEUqr(!@UNQbHGq^bdw+gCTsl=~pb5qXnp!SEuBf2pb$4}rUknrCrjPUnDIL#e_+8_pXMFHdBQurE zdQaIpLfP>8tVA#2`C(g5mO3m4`nm<%-7gnoI#{dWH#`^8Ix&`OR zVg(!HDJcakLmJ*&c;ZUwDy+$AWA}dP0%&_Ug3+Cg{CExA@A+_9Ivo ziaaMC!G?AHBOtrRsbm0EnY{?K5HGI)E1?1qB4Qx6$tdn9WJ4iM6Jhm8(1Mtc=P<~M zgQz#(8)+0w`+7ZPV>&l4T?QJf3UuzuMC0hT=iz!g|8~K755FRGUSKbe;I`s>Byx%FX(EoJx2#4Wpr{ee??4KZVhsil>B9 zl$c!GRoUN4F{V9&B{QN6td%NyzTJ$ZQPPx|LOK^;MHy2OwVKDsjDvj6EXx=-ejVWH zPb(!+M6C6B;9u2U$S{B3ql(zRZhm-P#g2e5l43%k*vt=xC4pn28fw6sgikXGsMF1; z>FQU>y4oFI*9+1V?x%A4w+vU&7GMZid1Zg3ZtYKJaq>6rEBUW6A~81v>+Vnsiql1Dd^p>&3|Lur>EaXx ztwPpstiONR)OG5Vf+O6O%fW?9x<5|{>vy5(JYggg(M(}-9gz`-p4p#&N2NrEm!d%% z3jw;53_mq-;1R9n2|7hWN5GsEFW_g0T>mA?!E2<&QY*^hdPtDX#D&9h zO`kMRF?W~S(Aqljvn{}SHMRjBy)a;{RlZul^l0~-kHYtsGUjqhoIQjbqx$QxDI~=K z|E#1~1Adl+s4vC-x|o>I)uN2j=Otk!IXsDeMZN%l@UcT6HQ5(O`gI~*7tijufGO6W zvTEFazxhp_Tl@bEQNDB7Y)itzJWdGfODEZ#wzT%T%&}u_r@HPA`yWofkzBGYhi(pNk>w!GLZ3r zIiA$?4Q*lqvb`Wi-BPh)u1*e7w|%K69)4XesK8*EL5Tnj`g6lk5}S9%^K08MuvvzZ z+zvDB8gyb2sHgtY4B$v7a^E+6w>E{1mS1skH2AktbFCXp z=iC-24bI8pzNm;xyxhjbVnegk=@si$tCY`8-y(?78Ya`x(Mq&(QRZZ^Tnmdcx3KUr z{h&4)3JqGPE*+G}%F)?`L212mhn|O}OJ@$Q!=U?Ga-3*rX$=$gpC~-%V(R&y*;FCvr~E#~{5W$^FmkZdv5&68 zB1D?Lq!iM_h#>g8)1>@ijx#7|+7VT$u~{hRfXrSpg8~cK4)98r2g>MMJ-yRK zM6uBhIr=A{_Qp{W$BFq<)tf*~7%g2(jC|MN95&V6fB0af%>DSr|1^t_1Px{Hp1QdV zjaw;K(dnNY8%{yWApa45-aZ+8%$(B~C5)T7xAAB*t)xTICPPH2eVb1BPW$8(Y_C2* z{{cmtNl-k`(LI237V?*!lJ41+>wWg4w}X)%0QQBUuuB7tG#1rURhn+%aKmZ+mtk95 z8VD2e?sg8M4~Kqnz6OS0tTZs@at~EeA&S0Gql%)`V^QGwpt9)emi}8T@z`N()pfc0 z=aBE^0ATLga_!eYz9Yei41E8Q^zg8&m(Fhcv<8r9JQm+3;NcuL2AL5N%D+1_<;JSQ zjb1VBs{mgA7|gEYAZX&0h1i%Al%xtOOaLJi5(r<>1U+)+b0qSQP0o|_ZJC!7$Tv>eMnPy z_ISl7Xr94hPD34ts50`azPPhko-}!iR-|i~?j-Cex5y}Yn~6ew%DbPnI|(^OaVaMsv-q8iH+c^cTL zw^4(=`H((ZdBOW8VB&DV+WF^zj(6e-@pTzG`L3(PTCObWUEgC6CM!oOVIeyeNYFL^ z9$eOcB}}`+AA2svBgCKXdi*{Sqa5Qf1pisM0O4CNh*Wc9YhdC6wAML1p&qFYyDC9q zlDzPD>6x^8b?>d*-}U0N#P^TrhS?AKhe^zmGh<%0_G6m83Fiu2%hM3WCVA|8D2sTd z-TrHC?^{bHDr9IElj!{yU#W&I1E&k*Z0~_UUX_vtC?&i^Op4&?zxu%NiS9DId4|!^ zx16%?b_b{DQL$Wug;3?w6j)7vil-G7Jtab6Zm7fA)xB|yuYF4&J8%kd4gceM(;V9AV{`HnK ze=Ij|^Fe;H;G$WR3fq^7Z(=7>5D{(_4#0m#NYi+XU-s4w>sfIX;N$TZImTb8Ezwg#CHgQ?I2;E=nH1y~` zq=-|xC1Q2lI?O*ZN;+gxK9_^ojO?%2-`=PPe+&~vlqaKIgR|?6x2VF&Lg~n&C1jBV z_IA%X@v>yoD>zKOGu2Dw+fWQvutRk7SAVKh=WaT|jDnQwphu;2Nr@cb_&&{7HLs*J}#I&P%znwHBy*7!DV+Rx%4Y3qrbb&!$`0SM;u zkn1{O=lG6V*h;$MB^EtJ1`5qWjUmA@o;x_G4m!f&bOzc=gX5Kh2Kxdhxi`8-hsB1t zXbsut%NsSQxsiUg$j%H2hrt9jzy@9gP&F(0zz@Y6s)fYI#{{Ol1(!}v!$)i3Z! z75E!v?6Fr95>bQS4$n7=b*`^g7{mRje|~ia_!y0w*}kp(Ny!$}?<~GAg1x#MuO_ zToK$_nxaaQ38vsyEf-rRYlM}-Ynm1x!Ob?!5`T1}p}ep#GfD)4*YreNezOm*Fy`R|Z_0*T+0|XV zSmV7N`~0%uHQL|7*c$fbLBqxvS}wySc&7JQK%jGP&j+GEifjTa7KbaErTW=pv{y8PUwx{_ z(n?7maPX~%I`QBylLy=WcpikBp)uyPQS1ft($#k0oDXAnOh>sF42{F7*Gd;RjE$Ho6+&xp$q$F#2%V$bM_ zK45?&`cAt__akm%WUf8O?Ol$ruwKSp*TFi&h(~QKk=3J@`sRznk-;MQsdDJ`v78o$ zrVQ)PJv4C$Vc$%beo93J=uY^*f9-ybA|Y3P?!-XFriOx+gdtTiP_wVl{*HGaT&f)^+YSJEbha;mF z_V#0*xqE`_@e?b(jl#Jqv@`?Vj*^+ro>0y1kxiN0G*yPx&4;R0doS{{KBCIln!gm- z8en_Fo~*?0X&El>Xl`kaWF!;Prt>DPc#2!~NTy&Bmuu3H^B4*#YakX3vB%C=Ac%>i ziKI5B>}SbH{xV^Z@KN${?>*X$JUtQ9e1si2MVlHJ9iSCl@dpMfa6g`+-N#ZQEOpzi znHpDi4`c+N__&Ha4UAn#;C=@`B{II(BUh^u6$|nuTwNBtS!UcLDef6~r*W7lUUJ2X~n1!}kIUTEd!$+ohO`EK@$y{8@5UjTLKK&zR=u&uDzF1r~uU%)d zw}$MkR@PrBcK4BgT6vLu(RX+p_9f1gQ}nbPJ3)#`ONe>E=6?)2ANGzp;3jPo{{@Y; z%}y&MLwJhcu9Z99p`aP0KehG|b+tyF+lHOEP|2UrN)hb{# zgtOlCO|#>Q;LIVt;){x3BVN`roKLmTFn<9jPtnI&F84Lwe73a?_T~uG({0@FbS}5& zCHF&{qx^RU5P?yLfnEf6#UdraAFnr9S(qQctgEJ|KwSMvMZT%;9lpEW3$BY76tbsC zchM(RR@N%lUa}03W;>sCUc&(%$YX9U!E7`x7q9Q&aZ_e{GsaZKtMJ0_85l?08V-Y( z0R|LGnD^J%qFH{O4e%<$rK&75uc6De*`L7&u2VCo8=Q13 zuMcNh)FFw!2L87lhS}jPtf8n2p)=p12i06)yQ zLrn2H=P;USUiO@jyHpXqAf#PT)qJYo|?=+f<81&=7e0p%i}$Wgi~ z9wLsUt~c2h!C@RafRT+MIhQTLLpSz#LrHX(%z$O`LZ}JeSS;y$!kuq^SX={@f@pPf zK2XqgdzOexU%qj1=P+ZmwNw&1?zsAnN3Se^uB@dghP*NxmrCZ?%6!!>hkaY~1agG8 zeLuJu(Qx8M``YO=#tLX+8pfNx37FctMwS?FuW*bwDQyvpU#OIX`tEnz=kB!l*S&Pr zd7NxqZP{|P{MPDb#t0cB)~BqTGiS%};WprSG((brxjkR&hR--{vYo1OpS&Qu`4fR2 zL7j$C<||rzrJ=LYHZfrezrj%lM(T{EXZ4a-BFSHA8y&v&Yj`u3zO%lUX1~<^gwQ2Y z`#`v)ldhyIX!^*Nj1&Wt!w^vZCR>!0mJ+SJ&tgFnP8C7vd-?v1RU)SN3r$X?WR@D> zuHBCuGkR+RuL;|%=3>^~WWGtMgi$2Kl0cIaB*YAty$ThP<7c`IT0f#XU6#=jMd)B< z51rV=FBqaqid15z%K|U%6~M{eBIDjz$p^;l#n-IxiSv;Vy@=+ob=5Z1rhxU^T7}S= z@$tpkRp>+zRdf8F^XlK?ZSRLRWh{c3jiN?9vr>o1 zeV!1cuQLuGQR&V7`L}*Ab2l~5KNn%?3y~79S9O?4D2?t1;h5s+#*D}66Dz0rt9oqC zf;6u2l9$J6*H~>d)7v7RlGuACVn*UBy?S0kX+3MLPX#@k#%=7ytN`^>5j!QXTIp zyMU2*9Z?Kg0vjdmr+nlQ;dF+SGh@%sddLQu3S?zzYGq!cXY)wDORw5JW5Oxw_JMv8 zH6k@lgbQke4kpW@N_|Gml%q5H&$T}4&$@T030$UZcucwDwfMPtIt%{|{1_UCDw1wg z>=de@U@!%pGBLB5zjj~Jfg?J0e50m8NBT~GR9L_eym_ z!iTfmJjPmF$3TpvbjMX}9E&;^mxe0!Y7tDu!NeQ2PjBWs0qH}E4(aih5eQ0jOxM^x z%>8CD)O1nbPX)w}C@{CWAV({@L2QVr*HgW z|F$4WgvW9b9WETK`%7uqU^zEFe4$F6QC5-tbcCb&#J8Kz|HdviQOcIP1}y2NRB}RH zWhKw^!`Q(k_1&PmXRXK5=kN3Q%BfY2$~T>?%0OoCsAC49($V8(>IS0$gIn@mTjlde z5lk}^Jk~&S$$tVS95K{@>CMD{Ei+z;D8dW->+2!{6p>O$Fo?kz=xz4sxwV@Ze)X%P zXPTj&zKrYc_GWm7t?Lr8xZ|`svuS8rFWSx6fa)uSPa~}7dWi~T?Aj;N>D9O(9a&fe zJwa7|f=5%uX!DBRUmqMx%)05I}V0Y=a)ep7SDz?vF zu5PJX@%gt!J6JeSa7nFwB9!gd_@W0B`4Zs_;d;KWYtNf`u08u-b|Et3ZJ|`AF%%*E z8sfBX2fGk`g==vUSTp?7huRYtKK)M?u@;~0;{^e!0*~b|8hhe9SwHo5&*tt#P>aR@ zdL9_|AJ1G9endnpT)W9bQSS58LQVx09DwecWGzyr>h-GRVcWe5s`_9L^zLu2?>T+k zvw5csd>Q*T!F?nVbfu!6L?~xIEr+~w4en7ACD2ysMpVst^ zO+F#4{f#+QE>Op@iK`eBW=5)hoXdoR#Ldr}=j%1**4bTfsmCaEQM^mF(rbOL3~Cjt zoGSp`5gfg48-}y<)eVB}WY^k6x)#i4PFN1_L8s5)7D+)Z{*p zUQrsc924N+AK!%ti%22VDjq+PE@u`M0-5l*_7lGfn8bXx+GZiN?s{=#*dRRv&3l%7 zp<{jD4CoD@6x%BG5j%Ez$K!RaLhe7w#bS9yHtn-Gd!A14^{7=Io}GsMn%cbfqoXlr*L7^NvNmIxIDv~I~S`F%E+Qpa-x=)XnSJ>#xiJ&p6jD|Eya0?af z-`@yUkV&5mU;n!6qo4*wZ!KcUXO|{5KKrL+6DC=2EwQ&#u<5nPp}9T3C0yv1!az42w_uPSxL8O9wH$w zKNMT^IPn=V$JXK#cB%F=ISB{epBQ1lHdzU$L+Pa;Q)j9a%2!!Qi%VN@KnS!E`Aot$j!~ax82PQR;<6=x8AA{K4sF;I3(dQByBme#JkY0%qg_djW)) zY3#s-oP~OpE|e;uUlL}14bkqSZ|fZr$@2vPXns8JFcJJ5EGms=(aM$laCrI_GBNLY zy1r;!jHc*7*c!w^tN0W*Q?fZAp(1-BbDrZR^RZI%WVNvkKPs|ha;lt}*C|%0WoEgT zB`$`N^MHj3C6*F@fQ^3aIY`@qGcm6}d+4M&dR}PidgSBY{qEKSs0FiGc#wr&+0W80a_kh2-B&-so9*=bb-gxBCcAhS_j% z^XcBa^?U10A{&*0N1oA9uf1KkbhVvYZXdt*ylD76ov-c?H@b|=NLGbYf6qK;ow_lW zuk)70`=a}&msK7**RIMc2-^UL_DeK=Ms?;XJB(<@idHwn3oP3IS?b@;`z>t@z44NsTGQV@M6^Ul~h#MSq1CTr9c1A3qTboOtR-{xM>?`wQl0`Az<(H&M3>zs{aT7XU^D=sV?SC z(sY6Fk>|G|;_%@_lh6>Zn6MDtB!{rf1uFK;Mlbk5D#fP&@QjJWJ!0TH^TET!V~J~@ z9rRoH=FYeIx)WycRm$v#ba~_b^vJ4TTR>ql55wN5JomTqPwgj*O)AUTaNiHioZ1+D z){^$N*%zod7s|L8{N38GrEw&pUVenUG2iq_Bx(O!an3mVH0nN*5aHCVHwed3HV09$ z9yPlJwUswY7Lm4V@R9VyEbMPrmjyR>4>zX{KluA;9c0C&sMlRprRak1Wn?!liMx)sxGc@24S81vyIkWIp^%>YXCL%Vf0 z_Aek7=>fu+WwMrCX?-2f|KBkaIB5cB)dYwkNmJc{Z|^W24Ge?5pGI)2-u6W@XwL@W z6)>35A-Bs$FAFB^(9tScV)O?c>GOU*iVUPzJ^AT zjF)xP?Le_l8kTm1Ga@bnl{_T->%)FF9HddC4i?7HYVFdY($P_~fGZ=Z3S`J&3A+op|*O}8{PwqoE+H0-sT z=QEwJQAR!D||9EY1B#b7+ z8E#|qNAL48LuNM2oe;l6)R{uxF>(bB}zY{Rfv_*(UM+UQUy?f(>A@=$RrQ z#~#zJ9D2uE^x}NnLv0zic287_R7`EUME>k0VW8IA_mjwk!(i^MILLh0o_MzPe~oAq zZm#V_Skg?l9O}c7$zh%!tyEbEbi@I#aq3D?EiF|` zfK?_JOVO8+!ANF@l!QFYs7B6Eq)*hTD*=E;(E<06*5!#ZHihlCAsmm%5O0bdWQ%fM zHglj-Pz5*l0;+O&xheg-0ZoxM^ps|x-U(6s($S@76#Kb6#LgX+>_-uuo z0d3$Hg`SkLT^OU`S`~x=)vmD^A1wAz0$f0ZJ*ch{XSpuDp+ZjLpd+I`C6uMYcEf6%uMu$efAv_CChP|4?uJWL3b`Lh5z;& zCbal@aW&F}ZE&6^Q_(6cwB)-x|Bb45=yru#54LulaNf;V7;S{iqWu*l6u|Z^B--Ff zT^@Sm=8Dg3Rrk8?aBM;hx+S>f>O#G$NAhSmi%DDcdOH4#iP2Ai6tzD27<)#t0X7Yn zQZeErf3`&vzpKOuO!@{c!}MMQeuZ&OJ)IFy9%1VNSu6e zl&h_OFiogwwSMpHH_tI^N32&8g&c%bxqmvR(as=tt)9 zXJk(e=p*H_W=ps2tBCXQA6u0^pi)PCU(eyG!)rb&h%cxX&uO;kE9{qNX@+~QNep_N z8oDEG8KH2!8-?{tyMexsVcDwzt$wE_J)FUiEWHRc%4_g^`5mmU+Yv1f?UL>6S;?_K zD$i>hG)oA$W}YS?M*Xvl9S2_@zdCTDjc z!Sp6(T#NbmYJv`)CWdm%rcVGr(O7GZdE)t^qe?J-lg5>UD#`3_K%5+CLQ996L*Ux{ zil;a~P$!31A%srsUY{kICGws)TtWZQopcTx4Ca3R^Hu>(U6S&dy-&#-gjBtPK&EIs~o#NCle+epKKK>XyE zdv%q|^}6rWEr7>L#bM8BJG;7Chz*g%c(Kr}ar1IDqPP3KWrgkRjJ#-&iN44nu7uhF zn8vzT#@EW8y#)M1o%&>W0n!gRx;;azlW6d7D%JP=+O>G%LFC_ZtL6r*iu zv^rirfeHt8a4eHQhb*ZSxtprC6JuOMQjtk*;`Q_Tc$oW?osM(OK?dSG8__lWu6%6&!VgH zj<)03KQ^^b*kSQox~&5dR=^dnq^%50BgURynBABmDiw_4Jo@{j!@b3YIoGn^;#3Rt2dt+rC>~cP^FvQ=7gTjq@+LjJALF z3gEyOyfksQcxH!b_U#|_phI=;KaWf@BD7*nAAQB=0h<3QjK$>{AY{@3e!@8ap$=R> zJx|{-gQLWPSPu(}0!btv4%Gzf%hRq8Yy=6ZENVSnF%_8z4wY!$qAR*{w=W))NE%j1 z|2LwDB$BVA*8!D}KWNXTkkm$~bA7W!{FT3u$FYQx5+5bg1IZw(jU=-Z8@qT{a z(#WJDJ{3{biic1;!m-qg)VqNXJ57vKS1VZ%G^k!A<~*e5dQXe# zzFJAVLI3CyDF!%?OizH#`gViQ_hdSKF@mF3(A)J|Di(vpErON`4L5O>y4r&j0Fq)M z(@>4;IiAX}2AhM7JPM2KOL`)qIJmn|fS!Dv9X#*+tH@SzhK-RrWqh%>>q zNch%y9ok!y_%7xiLnoU)V=874WWkLU607`7z2K7#L)_OG_KM?8M35bifWVXxH(S93 zn-N7@Q4})8m%*BQfBV?MM-Uxu23~J0Q>MuB)eI7-5P3wHi+Nm84=jkDRRYMZqF5`tHaAvR{+M^gkf_{JtqxYO5C832}VMsIr9spp=9g#L*e7rN4Ekm1P zK2RL`A@iGhkD%cAgDt{J$=I|h(6CUHaIl70^mRumdGEC~Tu^GWX{M%Os_wzg-{ai* z40+_z3t7)qu z+R#9Zs3LWe;*8YKrT;s^g$)X|x2E|w;eoh)DBzW~+*pnMnn^P2+UsiuCI_M%n>`Xv zO=4rJcBijCl@74>Cc-MF>Z#0#iWpMVeyQue17S??eInd+j(v0ieBMKuoO6)cuc(-= zACrP_H=KN2$BAfhUHe@`h0T!-Z-i|CSbyE>LO8jvNTyz7JA3&L-H}aPPXu_jiw3$- zPbgwUVk_VosB_F;E6BK-&H5AvKHy@PDs#{L%!5S7#ARmRSW4-b>b(nlKnJ;7jdl6>_J*32H14~3X zgE~(}B4>F0%i~8IKQEXNQE)#K2yQ?qB8%?p*n9w8un5>DZApiA5ko`}Ft5W(73BYo zLIygiv(usvae}`U#!CyThyNn}X>SvFeHNXkYE$;Mfn*3B{V0nx@zkCJOt{C`&OF=M zlC+6FuRzHj(g<>7Fi>IzQrX0fwV>RsNUk`WT4&k2)#yj6i#;lThCGGRY3k_l}9sBoY+6MPbh{ zC(EJjQdL}!K0+3|1>H?vVcLAqyfC7wsUnF7vGN-8rka zXDWp_Ep)>pQkKNhUtu1rs0i0de9=+`+zwpi<9d|&2JVs*_M&203C4`=dc_uNh$?81 z?kNcr(7+UP3H1~2y~M(n{!F|Hwn8gWxDnhyf1^&(;@@(_x=LwC>%6`>Zx|mbtBvCT z39Ai%^+@WDPjdX89}-#fgKTay@D)M^C1Aw_BQekYA?RADNk>GToEjjZqWE=skazh;$siuVHj-y2-Mu$p;$IIyk zbI*u3r-mpasME`Ow?)0pc`!W{mx+!1Ft+4 z?8ue9H7vDZ0vj0u>o(GLsY~3bAEbuB-z8z{anhen+SF6}SyJ$6SyE(Y89U%~{uOpJ zU1de#sU}Dht|R)2IS2^*^(o~tuuzFy(~-rnZiC`+5#}9hi~66zMgU5;B3{OTmmtIf zd4ze61knF{4guVXXGIq+fQNS7+d-k6C{0p1RxTyS;ld^LQ{b z&4u%G_Z&WBMextt$KHdrO^u?`AD}nzf5=HL=^$=!cLCbr0!?eUk%#)uF?;kLIgpuUNBW zHO%B>F@@IR8i#M4sZwQ4{Bnkve7GTjAt8bOl&QL7Knqt1N-`5mm^I;3BL1fA^atgJ zKnpy`EHN)$p(8m?Rf`SB$*VNd(W`P5cAqnIo4{8Tg~Iq9CVaEx(n`Or#-%$Qz061K z&LecwqsPpOk8;XAI-oiAVd@u@sduVoq4;Wx-gmQ%5`OWN;-E$PmUo0|++%EFciMuH{F2 zLktj8T1c{$=-MYoygj;@BYz32wp`MD$MFw0vxl`#ghV!THH{-;J(mT)#b75oVGK#KLOavFLaYJa4U`JlPd{G)` z6!TU3p2>R;@H_xe_pwxz0xgJOW4PxeQb22D#2-8jSnmBL{~BDYSO*Ft3|CiVzxdC3 z*Rh?E)1m%MbWfe@E6wtH0xWpJ>0FQN&n-4(!1bNZt10egYU?sEBP7nE=i;F*2Di@< ze57EL84`_ZUt*49;nSx-23ay3fluE3ef;t>BU!q}K@NQ< zC5JdEqO*tJs#ol%z(cmM-);!HZjc>{KYdlr9ow%3T52v9I@0W!ImeD|ehtH*kL1&l zmDEZHMH|S#B@-vc@xg>IM764i9D6?GHF+biC*_nUky?3h>vQsF-_0|%lGS_>aFMu8 zzi)Q!yg(nAwZgq}?2N-StkHz4k#kn532p^_+AFYzo%v#a{mKqr$SBJK6CMYpD4n!u z1(Y+56vun-WLt%?CtSW={OKb3i;E|gdzdXUNQ(Vgtb=ANl>YsL-@KGl^d(98(ha$h zW3HLtafRjCa}**ZS{q?pS7Ro#OcJP&pqdi|-7rj$sh`HjSM@soaq(PReXg-h{ z8imT6c?)ZXH4EE?WVdciRN?nx0-#8^Vq{*&j|a4IY!mvqnCzkfp^aBkvfd3Ila$A)~~x zqoyxnUA{ZJeVkbcEG6~fJ@s-<-hmM(+c4q%BG?Sogn!(jB$&z4b68pr9USKR5$Qm( zL^nZn$H>ta^Mdawkylv`WYX7|^!rlR=JL)CsaFos3ob#VT93`))J|d;32^7EJs<3p zFO;{KP~i)C%a#k~OAgj22LF5|n2|CwqcdftfE|S+ES>MW^5Sz^rI%W0RCCs_kdz5l zgbGVyla)?{ASXX8paGJu;*Gs&IQ!LT1(~FLq+u1q^^WgGiVHrqbU}|v_RfP(7P*lQ zV0-9ZlKQdF>XAxNlxEKB;QMzt8q)kJvu~{dbU1ZfF9DHui0<~jp#^7k{IODQe0`l# z`)GxqL9sph9}^oNGaKF?QjYuXKE9zXN8Tx%Hc}j+My_Ud%a#x4eu)@J_J04oc|W6) z{VDp*e`fKWwZmu>+oN9RXV86CF6-A;&<2E*DB zqGpM##V~9c)1HCq3ETMfcxh&3C-fxN_Z4+Lwx`p(&Ua?}(b!Zq*u7iW7*iVV1h;21 zPT5#P9D_6na;uGKjHSG!@CYiRoJ6mfy%RJzvV;nZ;5(x~FpR-Ol!wQd2qQrVBjx-c z`vM&TerhbwZ5D?fF)3?PPuADPC3cpL3O}4jleDgMeYgD3o>QSby0o@PAFB3EW6v@Q zm(-A~(i9!X=JlB)q98FB&Y_cg-<{CP(6B~My60fzA^Iel~*6 z;+Sfmxv#!ZmV%}?3Z1-CUp|YPD&pEa0No@XaZQTx?Y8<-?1~+7Rlg0nu>%JM^BjRef?eE1r9EHhPH%5-|PJre-+ei6g8>#);E65h{V+C^!yln zn)4Rm0pHvxejnbP+xTTt_zxs=zA#cZvvkTgc6jkIpo^9(X4V=1>f}ybY_pPx??QLk z7z%iSzdhIvc)*7D^8jRGV_4fAR1Dno+h!b@l0>%AbMp62{J<}$@B}yBG~g)P_Q9Mu zf1*Q!ws2thT#D!EV8STD1Frnsk!oj&eKM+>Pva?(^vr;N_%_9K zrli>dO-!OkspF_={L90iC)8bg3|N43?RNr!%-#M5s_ir~={OU}Cz?;XNV?`0|6HMM zX0i}~)EPt#+0Z(|bSw;bx$){yN`d#VCK{tmfCj$wAaQ%CtgLweeiFOV?o$)3OZJ2dmN{?a~%WN1)7v8%4Xr3DX&QBi?_c zFDjvHIVxzb52~iOxw`vIF5%g@T?LKI%q@bcn%1+hyiSUF{l@VHDb^%V^B&pAZ(1SXF-rA7^LTu7E&tpKSj+c6OTr4J&`jD6h5`s&lRXkP4l?y zpfA^a_%W%d<9UJ(TCnkC$TqNVI>W2PkYd#3c%r6T*RZHj6r6z&VzW z8)cyOOK-5H0=fGxId%$LxLdQg_CyQra}AgB(_i*W)sP_K$et{VvxC^$P9rDlZzl*J z6XJ>{k|%XW3=4n%S=)fJ$Vl*h2mgi?WT$~PM3*8^kvubw7`C&E+w@RR$qG2XMwWzL z(o+JalPs1jX}Tqfy*n+m%CszB+?t*|K_1gJ4I@4~X4nZd_$ z^95Ui$wtRx=y=(U4JJw}gSTuKj6q*ngIeJ(MIpyV4OVb|a%0L)eN)UeW+zwopi7ng zBuEyK_*F^G1X5$}+jrclNrRsV%~ts*I1z)TV;=sJY!|h+d?k@lV&Ch=>8&|K3nO3L z|2lP|FPprG2{@okF@^XlKF3;;MDzr0e`QXvQ8e*Iz3q13s!vEo*?R!x1F6IyI=Ogb zY;jdFk-*FC$WkWX*sCtr$2VW!%)&%w0?5ic!rCh{-{(WWlJvU$Fp%3#@-QY;Tm3WX$A}Ls zY`|rG99*0gSF)D%2WY7JV#n(i?7-{*&P;Mrq6M3RnVLwNrrwhD#J%#k0@&4CSob|K zyNh2l`jfllj^mgNW3lPx_^wlR|oa zPlP4`lu<$^O#*cyxZ5Xley5q9`!RO1Xjxn_!|xCSvDLwm?tz!>%*B7=xN*e#kg{3E zA3q74)rxC+84|-m)K^68EL|pe?fH3xy{41K2(UKsY+oL%Y7YgvAiF5dR$%?I`Yp4xBFl~BMlDzrRhh{je`@R(axsAce3U{y+j1T{rfG* z(%+DU*klb1uJ2cV4V*1ZH#PMxXY4+gm^BOI?qv_}{^wn!8|_vot?c(7)~Q)_hF2R} zQS#5uW)w0z6u3Q)h}NgP*1IT``lHvcI#86Z+&p~6E4VDVFLZ%nRN%_6Qw%z~7tOZc zHh-BI*=5U!52Ofpx0~Qk@lFapGvWcOK6yi_WzBM%az7ZY&kuf53(q~1c6t402>q!1 zQkGK2!HZ0clssh_Ws!d`zpU)v*Z?QhFrk77h?OUpAcNC%_A;>AE$GXzJ;niT?p%+T z^tPM0bwr{3kp6D;K8~?e zdG5>Ur+=>JNpDquc1zO8PToLbv{6JV!g9+INqofC7nX>4bi}J~nlcCfIKq1@qJSUtQwT*!sDzG4@3H<1lqaM6xLL&TZqI_0STi z6&`{2cW6V5`87-CXwINBhwvyCDV}l_(v{`-Xubr)?R}XnJ#D6}$*b2OCKaJNpx4LN zBy)TXe(j~joDFx;31*t~s5fCh55r}wX3W~1IYUG2#2FjJmx5eWA8xtJx#i9SubP zJyHD0uG1&`&gCt(OE%zg{_r*gD?|b-1Wt0HG-sA50UJWt%~Av6D9e7%$bLD0Q?FL} zL1BDdx!h)L34Pi++M1)68y{Ez!+IEV*yLrIA@R<-(uobM34>zoUy9me=|w~_L_uRG zx%U1f=*L97NCd4m*gfJ{r{AWDLP?Weln^9q=qp|JSGU`nk$ZN8nZ z(m0+~khz_m!*ns7Ug@Yl{YfwidH}s{#f-as;7QWX(F5q&KF_-PMHzvz%|<@P5_doq z3Ej;(lsTzBZ6=l=h9ui4i@|+zEue$L04*6hIlXakDnH(tSD>r+4UP!qE%1M_K~GH@ zesp}N16=%VuNpRhs55Br+ZJQIN%~_UaVHsrT6?&vl4+?ccJ_uDUJxA`bQ)@?&htTO zl9g6vIsANQ$m2sZ}oCMgxmhfc)f+lo>@vK*|w1ZV&Ga-@$ zAYrFneauKb8WB^eVO|nxfyqPH`*aNPJh?6Y=dsw_BD%rlQPUOL760x-T1B? zzeS2ex1Os9q(!C_Plm6Otguu84X=p|WB0ASZs2Q$Z*cD6Xd6b#iK2au{S~oA9vV}G z=c1kRsNHK8Jn)Nv9tcwLb{+=sC3~hyyb`TmHgvc`Be}Uc!B1;rn zR$#;ZJ#Mfkqn;9Smxc|aSuGCa;0-PJYRWz-@l@QE*Er1+2NV`S8siLF&u{CFOsHa_ zdnspByp<53IavGE+ALnX`fPL&LDb0>BhM2xtTa9Sa`?PZgGBY;H9tZrP5G}zjg#qsKv#pF6ElxVqhh)n%>D^I9c?I#6rjefWp0YF zJ|%_!SyWA7x8g+qU)1m)b_hD~!=iZ=TNM>782S9@9L(jwqsylOmoC|Q$jZlSGMiba zOfy={_f#2LFnReR88ywBc7C8L&aaTdZvK2ymS4W(u8!-y3l2PkPV>qlc{DaW2gp72sE@<#POo)yXdX3cUYa|uA34M3sUwHo8CnlH)0RMEr4 zh;F1PoB%V8PUFGrAco;dMtSTU8>V&UD^^swPmu6McAvt>*>Ckh3ygTQD5)J%E0rClG57+A&%P>)NqyvqRMG~culW_}= ztCHnUTU!?HMkI0Q=4ey$ow?Hq~{~Y|83y$%v?%I>R}K(|1OYxQd7IipfG`p z<@l*DP2X|wau;EI+^AN&UVBnw8Y9R-WPEQ$V%46x{3Xdnxf$q0vawvkBS&?f_ALi{29#(uEQLW6jeG;&7HJKQO*4e85SQ zcrtaW(e+UPxb=)i*mlm$QcrI9K^`;E^I5aWHaUrs%1K0+pdAlo2zdu3B z4$m5nra){Yx!26lB!!(pWmIpCJB4AoZNLc5N#D_wB$jH{*G*06*3tp;lGt10Ms4ca z=aOj1l+*xpC?cWeiLlMj0Ecd$L(LpXs ze$E|~{Q^bTg*|YU@DFZRtN$6sXu3WXvN3GHP?Cn7y}Pz5{BNdFD*?xP!#efaYte$w zFEmzE8PTQ~O!$~1P0Vm-<)qg`DMPaJPs|fr$iMpn@-Ol#f9Gkl{Tzo9+hH(!n7R5K zdK{+s7fmtIh(;pCF#P_sFm_y^i;*dtk_Im}{PSW@EUK<$MNCRUCaCtRGhO zcMJDZz}c%Na_L%cGg_swSZ5Y94*eHa{HmWq3&w1|Qrn(%uVG>?-<+(}ZT$lZ^Cplr zc6GW04=PnFCLYYP5={SLp`wYf%)>$mNgfzSXMzXUY114+C(;#WGCq>Sj=_BV;h$k; z&;}V16GENRhWwZEFs`3@Q$YK!;OmCV950L%B#S73P|v7-^zx$9qdk{uEPRZ=_5Iw@ zqwEhaeoDzO3yeiO1-FhcfU(TQI{AuRuaV6#QA23q+kTR$*(uqBwD)cj>v}dvKBkTn zZdY?lE%cCr%t&KMI-elOwU9@)2t8eaOVZ5@z#={;lDa3zBq0psDv8PI7+}*{S}=!n zzIdBP=?0yCI!t|*zrx65je^}W7CZ*t!?gN6;!&(_;WoK?bJP!MzaX*-st|w6jIxLb zl8MoThyru-kZ@a^Lq7z+h=TO8@`!>_se}kJE2@xa%T;qIb2ZmhB?4{FVxgd-BdQ^r z(`WeODyXTD$jMSW1w9#JXzA@RvL$K2H{)xt&cVSB%zs0NemVwA*)u$Wc{&R4QWtU5 zu@-&X?Jln5J!}uu4R~$v2>)M5yNKO&H{S<%S#2GmQ_r2Tk9j+uMU~ zgWM1B1)gg@`k%h_t-ta*CiAUCuQT!fx$koF5Qgr#hh5`sc^tyUwa!RozOdjT#qk+! zbqd{i3DwrtX379vsS+C$09wisI>QWh->h{@+v+Cn&!Lta!Ia*eze)35{Q3PZ$ldOl zyGf`C5qxs`HndsS)FQhc@T+jMCIIZ#XY6v?^Yyq26>1e~kMQHd`V_Nu^`8MA8gSnL~iQw7hrja>j&*%eV~U7(Ou97klI?k#tPE>rLYdAiu$4Y4m8f{bRh`Gj?D)sklk>JozK}u1l-4*+7Av zZ+nct1kju8{9w+$xtVx4AR&q(PIfjP5T0STuUJ>+w5|0y!-)3Cu7_R@9Dog({oo97 z-P}>@;myNcuC4i?5t&{=MYAd-@Syw_yc1&EH=1Qsc-VRpOcN zkjRf%PwrYTW3eTeyCMl*wB#(^QAxGpCu7pHdxnm>5L=yij3#-Ohri==yeR9Wc0y#_ ze$V-MDi@qCUH9sKH#q~q0uUDY=>M9K5@13@N#b$Q&=~m7l%=;k1hf{2U5#>G>4gd^ zOmNV%etaP+h0Wc==3Ilvh-q@bURw|~2T3`7bmx6q(!xa$TtbLxI~vaSh-jJ+120n+ zaKFlO^O!(`3aaZcdl#vLX&n%&q-{&I!o&_1%<~ew^3bFKhizU8>Z!7*B0Su>(B6<< z)c^kDN0hKq{9XGf!7e1re zg|O|3{9wgzC-j0PZ2^yzp!_)I));Umn1z>N_2py`zm z6tSf564MXUbs;xgeX--9kWo^_&1|GW;FcAIn%9@-dQ2Oe>%jB+Q(s=*TEj>+f+T2b zFWwn6#5{4{k>z#Ee6X4%?isNZl=lfqn1lQ_2K$$1;?6?Y)@1Rl<_OJ<{Q`60Sk%xs zCETZeDq`7?%6%gWR75*wN)?lSWrHf?1kNSQMn*nJ!|kP@)hDnW`A`RaWNvzyXpFNd z6qV{Na#9&pOuIP~M+fe&^wJy%bAZ~=9NUzK;#v)~j~%|PuZH`xI8KKqk|WRW-Kl_C+EB{ zWDlE`5?(x3mMg}ow;>p=%Bng5t2z-Uo*~+NjaC6^y)Nbr>UYJN3^&D3*+@82BwuoB zZNh1`GNK`pB)ED#ZpC2vVY#j9bZ9BDFBh&V`$|L-mH2gj$Ye^QMSD^kd7-}68C%Ys z`9d@P(YO!teK+cSD6WrZ1)l(3)`UNkQ*F zfmUF^_voenjcnru&_}yA!=HQhy7WF0JS9n?6j6&?B|V)2WyXe7MK4F_JX$oeCtJO9 zm@`3GQ{MA1*+Ai+WL}RIf^X~&YlGOuVI+eVM2FuH%nl^+TrU;~M{aecpH^d= znX5?FTpdfu;*uuhEu;hVL5seL!izhB7TF$6a_%3LY&RGwD2eop>fZ(PTz&fcu4)cJ z(`k|v4axpDn&2zWxeB2N4H)1A@8w@FUeh80tFXpzs+B8LC~DH}-{V;=_}XI)}q4;^RQ54uxXiHtk3J{&I!2ingXnpd+$6 zL4#>J-Y!9pPRH+pdfj|;5R=;Y-b+%FF}tDr^Jm4HF}g9MJzp`W0SdMCVI4aqMkw_o zXx)Q{U++_b4i~jE*z}`45}rNMKhg%2g~w8gD!?WbLi>)qO&>hG*ut>yQq6s@pfc9U z7z2sN@(!umgO7IWlJQD@6k4fWF#ZpHKN7grUUw&p12Q})vx$+Dpk>>pW2qDcTSX5Hd&^uWS2gvUYd`)9iP?&v4l#H<7S4@il9rh#o~K3Ur2uZzm1m~37Niq z67gN=;gI9=NGXshcajg*M;|MXvb+2^dyn%FC`9fW>+~EZ6p&G0<+Xl&+N`hZnFs+- zSut@xAmnOu9n^A_`_wZ0o_m(Ss!F%v8967Ndp;><@ymDz^TR*dJ2e{5+!A6eO5dyh z&qjwf9^vc}0+t+Kr-@YBXkLc73v|cNf}b0Obss!aE}5zdq@{f7bAnJ9l_7*q_a`6y zV^?W`mr(m_6|a{wdS*@wgJ8Fkw#Z@)2selyj4>AkSJMtTWzN}eJ@2v{4=v_cc{3e! z=da)+uCb2;-ifm|^&gSANxy;~-#dV`_ULvP)CrXMgc$P>N@Ai&c5 zox}i+zFOP#fZ`pk=LM{=(3OjaYsbqn|MXPpw|Dm032&n%2T&OX#ik$1dYGZgcaZLP zAy*IyIke%Od|akt{N?qHvFm?QX50h2n0h+Jf7D4kb;$i#9&D`Gz2UZICBF8U{JL4< z^nj0m;Gqpcern~J^k7@p)+ODLFtChLlou;FzQF6}=G=dDiSXMKUUA99ra`1yx#(7B z(mc+MDdXV!RGj$ATB=;-k`}$}S6+RCWQRgQ9_46-Qwl!5WMsM?UR@T1^M!Aip~}zo zQHAnWbry5RwbZV~(8Bn6^@(+PZf2=O2%*(UY|Rk$Iz}ayR46xp#h@ zw{-#E9~6bkEkCujt_S7kgeWeNw zYX{wNQnd7`nIfN>8VRKaYI0^{XWJ$9r8@v$ZS`G22(^~Ll^}py_=96-0FnE(R%Dim z>eXDD+g}Ds^AI`uoSEMzcHjY?uOeI-rcC8~>mf2|KlNyPD}pK_ybF87eA1A^40){b z73pjZCm~}*QJ_ok=Cva$FB`(?^_;k28hKzf;YdbA7am&T;c$aMc=P%6lkpft0V`A& zwydvsXM~0}9!pgd_KenK0ShE#(#0#^m|qCl2~~Ljb>?Ta|LX;)l!?U`qOjnwG|2qr zF}_R-i?$@eeLRZ~(tsjWEu`+|?8nt(I?G`>&tb;7{n6Z#*0Qc#A$}d>pYJR1_P==H z22Gv1lYZMzu{4g4(cag;7>|xF?P0)x@h`sZ4eT}qiluusADmKuy@aulNMi;IciY6D z-Jq*+68a6}ozK2w8&-C;oAzbxFy&~8>}!jfauE+30eWww!e*0X(9o<5yjFz7VD)f{ z3CMD&jfVc$w7%oR(V5n2bTTpFm00GQ0r~Woolesi-h*R(kOO7m^KRK)xv%7&oOZPe zwbU6n5m~~Dls1*|6Uhz*VE1RD@3T)~N76Yvkt_D)xLD$~J&{WHm(~l)3D?iKHmGSW9rsMFy2fj-iXNzCN?drzz7IsX^pWK?RCl&C(!#85uY$c8cuJQxA z)E-^bq?;lezlvu!O1E3l&{3nILK@_&mh`!>oGx~gR4(E+^o5Hd0KUiO-n;2urbqzduFe|wS1Cbg4ZKn7CyaYr^)r(nmo0PwUv zH$JHgb;7{Xz2*mS-Itr(x~47(4KUeXdniERt*)3_11-VBslN|01WnOuFbUS=(}3{x zf%*s`DPl)ZckJ%|4;#uP8sc}A-vaMg7C*_ay&o}7Vg-C}M{fkXa|)aBm8j6vfi2hz z_RvbOn>KZ~s$|WP&0Uap9N|jB(MpbQ1Z_v&zcd0AX%yGxsRW1KgEz1*0)qa54ZlsRREiI&%#X741&o}pw4f%KN!@!VVbiPNZm&(+TVM67 zXrw=Bw83CKgzxi95HfzD6*u&T^{yH01|~{=70uHrGLt&H510FfPG^2_ekSyp{7ebP z8nFwo-MV()s1JW?ewu^x^_t$@W!SNe-I?YNY+(KL&fk4pucA!lrS=e59iu=YL88wd z!}Sq3Q~rCn%2p8&1+d+vrz7`M$#s7Si{vGoC4Bd9xJp{J4>NqD|B+m}Cv+Z+UFe*C ziOfJ5C69=eg~zN%$4Hrh_sXj9A-8{Fg|$Ws{?zbgo_x!3*o(^)oo8^&8v|3$V`qh$ zzSp_;ae5|k&_rfHC1?Y)Dvb7rU!cOzVvh>HhAX2rKs9d*vhW>pVa{*WIRF@MxSKx8 z>K-}z>|Qwxe_Zp%zRsz;x6}pm{kM;B;vVFG8C#7wtVtF_4Sps~JMaxA3K zv&J_eOdX2n?$+`Vy-J-$i*yZHLGLp!T$y0PkfBjp-z{h48Cnkc^qCrF6>pxoZ#&@K zJydto>_^ksie7$K)6DWd6gqt6R~{%QAxQqa+0aZnN`Bkid#vbKtlQGt5#p2P=ihSX zkGq>ZR4Q<;*~e=XQ+FKXs99fqV4c}`CFf2SESJ)5U+`jhDu+v>4rnQ8smA_vGHu>f zqX>SAE%VLQcnIrb>njXe_*qonEHT~8WP)x#S4FdT&y%eC`M`P7%>@K7K8x4pSQL<= zU3|Y2;D9pc@nJ?pHT7uyr-X>gHL8$6TfZy)PeJ8{0u~*gFAhRcIg8+ovz2QK4>&EF zIW+XN29gOPTw?s`C?ZLL!sXa*MYEjp@4i|%XBCOIh?gnr)J_|Hm0>E;utpT}mBH5b zIzQ`xrT#|E#`2j{alk+yVHoUg#s43o-YOujrim5}A-D&3cY+S?5Zv7fZXvi!Ah<(t zx50u-2oAwrhruPdy9K{H|98&0FZ0&DXLs+es;*VF)+iYysp-J)x@N_{Yqb0KJJ%sl z;_%tomSz@TTuia(J)REU2U#6|Z3Up%h5|womGbtNWxi9(#KH~)9#iE4AB8SBP?wYA zh+A>6J6Rmpa|W>KFJ*;_Jqp~j3-qFdW}#$ZR`1#T6Axlc2(kN-%kwtl5D09~7g{$< zw>v{y@8R5U6pjd6E)cn5Ye7l6zvP$<`t&mxCRD$mG^t5^*p46<`dW<{j%>=3)I99?VUoC69gO;DqAT3fZtEjy(Vv9w3mH_d zoTTS@ph!%4Z>FOoT$f)Lxw2!4IeIVxHsbt(we zag92Y1W0r-p!wUC-yZl+oF)6+U-fmW5z>%CXVu2vdwA!l`^JbKaUFQ3qqC;Rv?@Lr z4RoM`jq}uA6TJU4B-$0#OE%hVY@gtB=^6iLIE%ka;3b79^z^?AXffG&d}k_~NJGPz zW?pN71_*zlBCN8S==1z427gkz?l2^oljd>VFApx3P@7u7`W*^)Ik}Bc3;P#F4?00S~Wd4qMSGK3vCO7w?g4R4rRbh<>l1q+S*ESus6K z18)ndp-#rov#THb1`dS6zdpdw$k>7>bv6Anbd*6?seutsAsLw`>=+B)yxluuPu%`D zz3|~=2SOw{7o9Z%s`4b^BTrp}w0DP`e^rRBzfW!EPQs_<5+T0Q>kYxNG*bAz!R^n) zhRnROSEpP?NYc|Ej_tDi8kM?#Iu*;nPmU0!!{LnfYnpE#z$I-p9q{qsw;U~41=oX3 zjXGLr$HgZcw=%!*Fei)%wC?2FnyFR;AFd}Bd;h1KX1)`J^9YNj!T^qf zl?a>iCF+1#Q8uHSNadG|3Ay<794)y^LEzHqP*-O>xLc6be1U^^#?j*Wd%26~52HI# zz%wuCjwF+)nQh2l^B=~bL^qs&@J=}r3sa>{_SrleibxwNi<4BRVLfewO^7aioD?_d zB)B*9Np-T*H0Od)dGE<}lAH*A7rK>K^77^6HR<+D@AwsxyI!;Of`1ZV!AD>#%X0>1 znV7z0v|^=lDbEj}S3v(SK!5ws+gbxPCz4P7gsWTFtP}VwMWmIt#K+J3DhpjL3ccJz zwiLx#G>t^ia*2Cz@_riiKm(lmw$QJGQJP>xM$#l%ckSJZSt3#3Zrx^ItyK%cm4v9F z!Y-0i-HDnwKZp$OnG2W_y5F(ctUe*tbgWr5X#JkW)#MNTsie-NT2;;mm$mcvE?a4W zV6829CwcM5iP{G}zf$u*4e#pvccRL74ET;jEgb{mt74Jk3JOLE`Ow3%O(2pf)bPQN zhGQxR$9H`q6Rir~s_#lJ(HhRfU&iC$M) z_*9w;z&Q-w(W-X@TH!mhpzXQB*5D?vfVfoZ)QdG)P50N*YUsfGvL1v`#;r_&>f0e= zS|rMJ%$fnO(LgOTYC+3NKR6lQ+HDz~%9b(B6mD4RPXizC&==n>1`T94G*(k{HOO3>E>A5N@5m_~nAur(6v^)WvKQpm57W zx=XMZ9GdK?QN8^G2c$_$9l14nf(zT&{LM%Yoa`IFVK(;DckJ9RdeJyn>(ER@;Kv_9 zN*K;4tNa}%-FwcGlB*WDK`oSQ_n1qb*g1cz>6nebumhgv=*g@bWm2?fn*uzU4h4if ztsD7SaFsz}<#rcX62MF@4{z zWJ?e9NLkrZ(RE)p_wwrLo+i^9fS^qc<2tsb7RK-t&(WIppH@Nceh9-;C7v z>5h0ec%R{-fEjqkxZL}UqJ$@m+tW(9IA(?O$MxrjQ?kd@@4jZ#-Hn+>P>3*BRc`KF zy6tsg4HH+!ffSewl3u9@fof=3IKxEHa((}NToHbD>Vp{gZulLH6Eoe>i;bx)@MnzU z&tuM;_i|kov&3){cJ{A0Z0p71si+j#Ju_K88>uh%qT)qBgFqJ?UyB|aOYZ`n@92NY zTnnGRG7eb7qg>}5{Qczqy6zKjOg)1r92?=9A{21XNGF#_BRBJjXE8&(RUJ!GfbJGS z5gwbaA*gS$=V+y%tzq!iBP|hmx1ohx2Yd`?v5^cB^_Tt>lfks!PVijseAO!XqmVe_ zP(=9=ZD|#qIHv|rUmjm%`aHdV2MKH&Rs$+?>=1vPGvfJWxO7vaRq67sSY2^*a(`CD ziYLc;TF}y^bChajZ28FIX4D_=h>7!w~cnrUfk$_YAfFe%;+#2_a5ce zMo7QOkQX~;j3H-b-p@a?@x`**77+8NZjRuKC|iB$#G>Pk)ioe44QA{6!)=ycy`lPp^z@)-T0vFErm* z%L1zC!N-D71^FpM)uzM9u_vvv$uRWL9>;Vjzh08=n$rGTx5T2nSRvztF>-eP`w*~- z==Sw?ayOL;*2f}S`BZ$IG77(0z?X8Ei(s6mi9|FFC8Kj2m{jmvP%;oTx20Q!+I1*4 zu(0pk6arJZ#SX<+eWpb)ixQ%XcB%iEugjBZYL5Kvhtp19Ii^6WLH8w-%@VEMMZI$A zTU26bEr;f}?)mpPmpFyA-pw@18k|xl={&zI!6+A-!ymO^A(D-Ts7m6@W-|;Fac?j4 zs5Hb7*}rIB!42Nnr~ebWw`mpAqPk?Eh~wN$|1`H5M-{l8?Q{_`dIggPj#;qZg7TCJ z5@~&qP@?qq7;Aac5$)7ut0^oECWLfzL^phq9u?Ntiw^ty<2}*4ay#JBVYc9Mo#edf zQPoZbTfv=MUrEg^a}i;`q5bYXKOg)^ z<-Wv^{gmUrn)H8=|F`MaTy%n5vN|K!5p+crNTZfLObu$xM%mJ7QC~FFyXF=Ry#B2H zuDr4Nl1zB>i2AtB!&nql2sR)~QO>d}jkL{NkB}*L9y1fN5XfziCt+wR^RxpkvF>Uy zdxya7(zQ0pRh_Dz++U}f_Y5aU5j2c-KXIUBGO3^>W#E>d@IXDbMz$ol3Ra{meljl7 zv2s-GgqVm>$}Z8I2i`<{T{aE_zrTVgmi?&9> zPOS(lE6-oL_LBdIoQr?iY`e*Jn=WPV)8yvo*RSKP?Q^bmeb!g$_tE56C+ffHjdMA? z&l$J$Mr6Cb>8)?>8V5hDy3289$-{9XDQO)^6;lTI}$t~LcCRf4GD?(}0vMj=bc zU=c~cG!6|l06i9rQBQ-Pe*b-P{-HxMpGN#oVYf_7`Mb4+o_MPGMNUQa(-Y2cl;77e zJ2%+?zG6|dh-}*3%bAO986PD08QW znu=<9xC#Hp6h9U^mvsF7E>)RRI*nxgTFK$p;pBbje7kv>YA@XP6u2Ua2d4>+(Mx5 z-zNt8fR24E1}nnS#eT*XKK*Y)lJzF?I4pYi%smWLa$NQ86l`y#@{wTy3MFkqh_ka0 z0%WS|vNJ~pC9xl_Vki5coC2Hn_yI3Yr-%D@^XYeEz>?y7&;96jK@5?$NyAbTD?qMS z4_&E!!w~YfVJ**p@-g6a>#=U|HTd||Hpr`h7)OUTTH)Veq@e`PBHMRb%nw7saHh$a z_+&j5rYWBnT3;-!!>l!7XX#oYCF;y-Rq6ZK7a#gLHB}WuPB&8(nl15-@IOUg;8@s> z*>PAmB1SYYm8%U@EhIC+XMXK>z=i!u)T&piS${D`;?6^VvG>kdB0>BX947S6q~L%| zxh2HFW9o+4Oh*EtA;^hoAvLZz3}+*VPZ!gg5ouodPnP)igm>mv)+z*fXz4OMQr<}e zyXeG%2AW3#)pv~<+gjp?!(>8JO|<%tsQ6c_X? zCFWMx7@Dz)^Bjt#ns5b3A#b#hhPDVRh9ml&`g*YUAg6WqPvp zLgtGN`=p|DdPen%B|r+WD@snobu~l$f?X(hX$>#bPQBVoH_Y(p-;73V^S)n?*d|(C zT*Rf7FIG_k+hZ;pmz(TwD>;WN#`mfRyE>mUIAM)l6xUaDg*u)uN2K;P61O)gM;m}} zO(86rQT&`z&S~h(msX@9wubb+71x1i42fP?y8=g4P&&hLrY&9Fk>1|^NOW~%DlHd$ zG_TSi=q31?n=@im&Kou+Eld~8RRUJ9YM>llOyAkWy1Lji6#085bbI^b^o7`C-7C*i zw7FNi>02*!RR#FZFYR-A70&F|eh=y0i*S&#kJ=q_=2uEQ61J7G1%03DMuCjY+elMTMTpmQxYvs>s{U3$bULWrkukmc+UsZhKkWFGlzvWef?FG@2gilMm(qXDY5p0g zZ12}@2s7a4P%bJpBnyy{A^P+JLQRP>YiwsSyq!;a|2uaFa2D3^2L?NYA7Q+4FiQ!4 zFipjjlBV`+alqnEVPytyCD)STQ+9qR5P!o{ZU-iY52IA=;nFDvdo7a$Xl&eow~N`1@?0n~!BtFv@oy>iLYYS|UPIRyFaSl+ivhBCQ< zG^F+k2}qg7zZ~LbwRWHn7dODXfL}-YFR<=ZDBW&tJ6_}6 z?hHMcI19x8Thu*z7%Q5K^hE&vLyzo;2P6e}Pdj_74B9l&&=j;(0eO;>1P4dRffkKI~KDm-%s9Asz%-{kr?2mFr zN#vIiNoZg#JxtYfxtBN+#Q^nv9Q<7-L^gfWIqJnBri6%QGZbIN&&6aaemPfSy6=L+ z(qhFq7z8t8F8D~!A=O40$otk-NX$rtp?WEtI$1Lv0EdI?t*v~#V&vt$d zGCfPY_v-g9`(CKAd}xyP+2&VJ_3)@4@cvd&?=JbL>j(clWxxEkF8)#gb)2$yXn-1S zj5iJM`L8sZtLDczy!dz{7+Tl@?TDID~NCya)afMH21DUE{CTPym z?wDQNqyJ~Rb(o`WvcDBFq?af+swd_U7FFQFSujdvR=0gFqynK)f|8kGnUB2E%5C;r zBddwabZWK(>~h`|l)+12mo3d3q>AROGjZ0l+*|p{RAza75=KXpn#PjIM^gQNjDmH7 zo;8AX;KQ=a%abk5zE<-e<@rqgKG}NmFQ7H|D&iPx7%4AhKH-9OVyfHBv`KqUp(9v1(L_(Kc5}mmD%?9(cRn;<95(Bekvd zSD-N%DI-2%4-5t^oNeil7#c`wxg@(uk)i6v`JnF66+nxBIftK)`)6w5t9(LufY``^ z{D!vARY?4SLh`{rgRX%?vC`|+Pk8@DvIm2gKl7cxC(;5OK_W(1r3E&0nFl8NbiY_t zaa9>$-R;}rex&MFB5Ny?Ruq}qjree6nNiKUyYl`U-dg?d3|<<-5gFx2y9bG-_R)AN z>kC!=zHA(xU{ONSQRsmtf)s|KXZ&?P>`Z*hQo{>uBZPV}-Ud+sJPj_!Q;i$8p}8!m zNb0gUu%q>Lm8G~oE}a0hx-`6@M;}!IP?%?070La|r3hGbwj>OJ@y5ioCgeD>zCLoz z_s+$VfdN>0@ZV~s6O^=8|GA2mwb8YH2oukHSw!l%;uoJASX|isvnu$LH`9xWEtg~h zc4&}=4TJv`LQWW}@iaP!YLGSNs28%J`-Hk~h*MX+d-99nh|I%3O`0U3PsABCc(!Wp z@|wr*^AsYQvtsRYc)oq@e|(5u9QQi~2`!Q4d1aefRv8gr=9(2yJ!G==PP-gH)8FA- zEe~sP6=Ux#x?L<~(XmEVVfz81U#Xt`?YopIRu_owutj6t#7xuzE3PuqvgKkaF}g5(Jb@4vvrs)W5(_~_lzNXjuU-8jTa*9`v?_WSkAKN zp2rRZPjfJyEVi| z`0r!+-*0C*gDyThlb9B+eD3JWY02>z#dC(xyqbZUaRc$)v^w^&=a#SYx$skHP6WmjKMMGctQ~Gxu-{&?}tO) zwZb<2>0DttA`;F{QS&u&_G7D$p_2X$%0ILR*8HNE#;lLLA_)JBtWYsd_m)YBf2GGosLu+e<2b*9BKhXFb-AE*Z__dX8NVQFVRzEOJ@wyE zf9p$)6Md|}=yGq}xNo+*48uX7^eq|2ugIwbu1UFo7>FDQP+aFAREgY7UwP>4W&d3X zUUWAc?3sH{_mXZR(i6{vGBlH%=CxI@NTH)2K=B*(IAl zA+ebGX(zjwJQ4414cTKGvDe!e zd?j>z=AnVHIEC~rdWKTw-6Tn?w5*stL7-b!;_)F%;bo`lFO};~R8H5S2U}Vxr5-zF zoLSIEgz+9$Z-SCw2fp+*d=y(Qs$D;pyrF5FbW`012ZM! zE?y%Bc*#0$bHy56cL4hdl6k;QoZ}9R+ zy~{<;)ReKMiYhNdZffkBytP2f*Ui8u*Q;GJ%iexGIB7LXc8$#q85Vkt+?a}@HJ(^v z@>_3h>%A$QLR8ckbSEu`at7SI5Z7}ByKzgw zXa(weVyX@e0|KE6j8PRM+ErUZQr{ElrXlECY|OOf)sAe(KlAg;-YP++L&VfLFNJ6o zTCXQ3Z~p+IuS4Z|j87nBM^{mKy>jyr21KjX+%u2uH|1@e&$KW}R~kidiBu3U zymL>Oh7YSP{?WmI9UToXoz$&Hp>bFJ6r(vi4--{6{clq`-PBZ9?o`6}5QUO7>lk1E zr>T4=Vy_#boz`$G8-^cICB*xgVXEpwW}6K3adYFOoyo9B&Oa>#j=4zDkhUULyoQ^T zaPNqD`c{b$gr*juLlE^%wZgmj@ih6 z3y4$tJ}jVCXB!7VPmGoD6m^ZBaxYBA4t}Dp(Jec!6|I$Toj+Yv8EAbD3kTYv96(Mn zWInkG*8B%3u!z9I1sLnq(g+0%Jsl@(P?`Uj|KWaT`{ne{5_)EJVe%aZvGq>_%Yg=d z$cDMqo{{v|a4X}|@*vf@ly1#c2{s)LW-S&-4-PX3+Q)8`EuZ32B_Ri8$5lm%9sBZU zfhV<^S~JP;YN~_IqcQX|lA&b2T@Vf?a@oj3D~*OgWu>QWQ^0smTN9vOySYykW|~ik zXG>6@eXXl(#R3tq+fi!ue2vO-?CLij&bLC;EDrPwZ}*MXKf;S0fd|_vegf4pztzy` z+&?dBA#Klk$kVrwFkCd+8P;jK-R-A@Demo3q!uyw!l zRKPp>Clv-&gAWBV%c-JbXwYT10yisQa!co&b6JeAs_;HC=LtQEKmm$H0|{>S4+s!W z)s%D)T10RRUr5CjEhh!d#oeQvM4DMjjB~l zzjR8}ou}Z+=6FuEL$gjvTgl&r*oue6F_9=X#mtwP9$NOr2K4MdeK=i)`{7iVMM`o8 zcNISZc^5ZBmqN@sH_!$OPT*6PZAfOBWJHF;LDSkt1fmT?#likt!?Bt^6LB_o#*X_u z{)HFc!>{5c-U~OZaR{fQW~Kib$eFxupPWR!wW9mob?gds$T>y?J$=mSC(f@XuG^_j zBkt`>@H-E5&|TKp36ZKQ|HDQmOB4%Ncwnmz(xkusG-7^>Lp)xo>d^ROj;$6RTN^?R zPamE1m;iQGRCn}bL*Z)Rs@SMlS-rZISFFQL28$)F4 z$qQi_Ya2JKyqGfiZG9k1E{EWAc62ZH#3NQgXCzc8f3xXU&v^ucUs;A1owJbnBRFFfs4T2FUS zquB7S#Rg$-10c8_zyIOgj|Qn9oeEXks*S&!=SYciJw_r@X@Vv3(o_h>IaB3{eYXO4 zCq59x@<8kF66NkmhA?BlMCFi{Y>AxiL>wF=1-~Q=*htN@&%>Y=fDyl`x+Z%`37ypu z<<&75uxJu%c}}_D8u^Ews84;OuFIE&z1(4Z*`X|zAu4E;E3#w(e<6wst@wO&C@&@b zeb53zIc?wFqaufl((hDfPdaL-V@icWnTz>=LU$F&z>DR8GRmmt0aDc4l?LFXF$lV1 zcf{g7(H zaDr&2`Zgt@bfA#(LZ-J`5DrC@-g@>xcT`2m{aaVuvv*N;6p9Gr9_)%!dB&C^M@D-KGP<>>jifWr zFqxLTDNwF6(D;)rsJVQ@jDVNjPW4{Sm>kI9cA-k)sejZcgl6~qbu&c(4gZGjza2Kc zcy5p*-1_TzO>i0ev{4{cGi3{Wsc$%EOr+UG%p0S0t!OnXBpS7Pomt#=eAq_5|1W}U zov9aTuX>WqnCo#a6Iyf-*kqObq=MrtGRP>{vy+Y z+d@E(JxhvaSSu2vMpAqqUin9qfeXja2=?5KNk6=Xh}oGgJyy|pO5*P!)Vo)KQe}w- zxGXhM0DLVWuJJB|3KKA_$jL0lvKl}Vn>qilJUeYL0I{aNs|ZXXPb${2Vi0QbmnCAm zuy#%5$}2Ke&-dq+PU^?$PM*!HM>g+~kf|Fba3GX~Ot4_c63 zkfS-z{Vm6TZdpzgTLrtmifCd$5X2Qr+!vKfVNNCyG@jNj?{Wi z*no;US`w=*}@<+BysK>$;jY#;m}hd3aI&<;a-Y3x@@vFN6ez*8W&b-cuP(8 zyw;N!i+IPoXye6473}oe&MWU_V^HYfIwU$nPCX!P;^pz!gULQ^9#=lZ)D+6&^chWl zm#}w{ajQ3;&z0p@rN2G2S9?Bs7T=nDTJR+q+#NOgn{Kq%@r>yWg^Q*jT^#Ga|FuEi zPZf`=K<@^{#UWLo$0{x0&(m~XvnObivrNr?y`bJVT)X9?5yF=KCs>h_WUR4_Try4p zL2JD9ZRNCqUDlH-^k0C`P-wwTXrn{g4+ruo0wdM?Ck?y`6zS5v>nzwFI~02upe0zJ z;BB%^&*hT_eB7UGrjMkRhv*KJ44-RibKdUEM_8B1#}y1<-lXScyAUQ5z;WsCc`huk zq`tE9X1uTd1E|`Y;$Je!x;mny2#i4pfI%H9 zqI=hVzui)>G6c)AUgJ)(1fNdYOdbdQUVvI7Qbgk{`XF{w~40hPFWDY3q z(v?7H3)_V{k&+t35=;U&4eH$s)~_(Ell{8&xPMg;>1D1TP*`OMO%2ceeL!z)!a|pA zH0)-buCaH!1DzfF%0%hQle@XQ9s=#*0E*n>kZGG?ui_-=@RE~K!s%qB{_lE|ht zijFgxh9X`9=(}M3!+d}&_3l1`1_&3&)0HF=`SwB^9Q#|ZBuEqq?^p@$d_HE8y8Nse z%fNO1C~}G&|1cIDk+Nx`nb{1bD{;ubfW_fT`tP-94oZucOdK{m6pojhY}O8(eccA! z+y=bdhTS+#`19&?$+(Eutk3s9c^}(rEI;Eul0VU-JFM+WD-2FmySoF(7Z5lHDcUq^ z&!G%+H-s&FXYH;fu43n1+YkGB<(Zzk64@B?+l8_7^7WgO%&msolD(K?k9zx7BZ zT=uCGx}B4|g>!232M)u^bQLlV9|EU#JFXhO1*&ix33g#C0TT9z&)ZO=)}}S{vxzS} z#I)tR4<9w%@T+`aYq;{;ydyvTRkXTG7m?>zS4Q%&WDyRwkq345l1Sos_cqMgSW zUkz=*N#~q!iaH?~V)uSA*KD2rY$^SM9B@rY&HabL7OUDpW4QvtlC?0$Y_Ie7(fvIU z2&LoaHw{~j4>?>Y(v_@d&V=I(bV>uc!3E!jsH41;**h$JHnYjq&k01t@xk~@39dI~*kCnUW@$zJ8_53nGB3e$nSOd(;#G$ACnXDGd(kvb9N z>mUkt?|BH;Ap77_S=3z}kQQ5NkRX#Vf#{!iz)q#bQt5#*>RvbNo}%+#Jbn;vodK`i zrX2o|=gTnTo==cJKfQ_v^$C@)8I*DgTlXF6w?&zU+6W3%GpJrh*n3M)0T|0LN0Q|j z)BJGM8TmIgC4`0+l^+nWT89Y-VVyviO4qjY!LbRB7K>y)BmXwm!DX_|IZ~QT{DBf@ zsvT@on}WG-biKJbBhy>hg2jd0=o1^8`xv)Ha{&?#GHz#KSR;f+*`Xe+>8FH?d#o+* z-z@5~F2>nYfp5#UFQUBuM%;U+j4H=4X7ZK973?NR6(!OC4Ucd3a~Zavm92t@YDfE2 zgjo{J3wr!ln*-dde&C>eL|SE5yZOb?bSH-|{B*#-wZRXWhJ$R-CT;M8HyF33lhqs* z+2n^65X!7lO6K#o1rRL~8-P7Z{r}r^em>gbD(a+ zWI-!?1ipB{0A#?#+o7@BG5FV7m`-$o@6aZ8eX7}L%fC_jY9A(EMH-bpVAGG_ie|Jx zRU9hbG?mSB+YOVo>{$D{D`@}4ahhTIlXl!Rv#NLaOkB^&UE2PHFqtt4r-O&ogp(+rau{V_ znao-VsVvm3iMjLGBpaIwnofJgZZK-2Rb3GuN@aMWq%BQTD?exq1iLCvY5;2CqcBnw zA>XD*LB1%Sdx6UcT~@BI=)7%8`gG|`M;|kQ>UUU)fC(lB%ARz-;i2fw8)Fn8YB?*A z^}8I#s(8UlGtVPv^j zBe0IA`&@wdTUv`0kWk@$cZfTT(Boio^7LVR_RqLn)WvxM{qwqu_lw{_o|kShkw>xA zkEPIr?v|X85nw*T%UK41xE6&* z|Ewy9lqg~_9rK~@8J72-U#+u5WU2Q=8dwLYPw^Qd0^7ao=z38+Ij`sO_A73SQ{739 zkN@4&60i44)5WkMr!z~(?1XWAB0giApuG+94`%Duje6h>i-@wpa?i2;U|-aUyjm8q zs#f-g7fOYG7VxjNo>k-ewj2KG!$dHB*A5H8);Y{oKRU4%u}(ZCN&TUy$@!m8UYCDk zAQ@8nf+ml1q#ll^YL_G+O&mA3Uhu4qMs<#<5aAyca+ljb93=;v$IvNxWPJg85;88o zzX+^k0n+O#dNyQ{g&oaTAyi_{YKiYS?$iJ9ZFlmR^Q@XS7?oHQm6^uxELt1-J;}sj zun|^{Fc1s=Y!Y;98qvKydgt+fJfz%1i#iCUYPOr3dB4&Bu#HlV^@#MeHCuqPHV`=USjFxz9Z;HDWX!KvS94%*?*u^QWUZHm^RT8t z`zG8Pt)9DOB?zb3KEWJUS`x{`K!2AqcInq({icsitAK8(p0ZJiEwMFN0s^~L&SlW3 zT8Wmu1hcwI%U%32Q#`dIpUx%&9#G%>rQm->%5w)qK=Z{=r`k?Wo#?z^_hNErrAgzd z`s8~j(R`0Bo-DNZ@)i+nG|+1QwLV|0(V;=pV*3qxPAvKdoj*}~ar38U+-J4U zk}{D7d|7DvxUg4I>ui8~>SYehX356BCgW=3vjAOXz$4r7(PRo0-I>I2{_N6E`H&^% zGE&BCjvH1a9<}Z}c5N0tY?kCblTRh~wC`wngs#FsS5AkS8RgO+UQyU)lR})_gnWBi zjoBLR<0tJ1>W`Oxw?(+&(&yqgTp7^Q>^WewVBOv_Y37<`1(sgqu8VQ>2uJ`vudj$e zQpc9hlB3;(C%10Qp%CN#$m&R<6ND*sQI1iVJcaupzZc_HZJmCAorjdE@+0 z;Sja(zCaqhgC84%P40RiZbzNT8;u!gc(S_h{y+6=&;2uE&KxUfhmVeR8{zh>Dgd?A zW}aW4Er^bIjrsEBm8AxvBHkRLhCyRCQUe*7+sf zUWAJ8Io;Lo+BVi!ua|*prawfA%>?U!AUE6~cFG?&G^miF^Xc?FWp7*@@}WI3?ne#c zKs+H0^-Mu%@c>SR$Y?j=Gd&kwLHJsW5`FWFRt1>O|=_NUY|WlsN$m-4*SGF#riwp0_k6Z2>;LC`72}App$c{mw+jtc0=f`HfbZP-ZNpM_=<%8wH)O^ zWaos)^A)mu9Lis(-Rs0WOJvDn+$7M1uIWT{R(s`%Msz0C{=KElBUHZO8~A`gKD6+b zj`)n1VcoEvyreJVZTU9`o!W9z5}846CI+_Ui5aI9fGHmK>*^Ci&zRT;cR*Mm$`@*| z2L%Z9T7l&viDVD<`o|=kKm}Neh(LE zTcl3Znu7?NhE?tsv8!o#XIp1h+eAR6ZThKf?rik@_#wE0jU5N=oSm81w#%r0`Gwm6 zqEG@x0o-*-63o?oNWc9^p(h|pDU?6gO^*0NY=&NtuH1~{>qNwx2i z-&MD<^b!+r>7v?VS37%mUq0Mocre()$Ca4v^GhujJWB`bBmdY1it?+g8cwgzk-J#q zWJ@IrhXbWZ>OjUf-$lO%)v(GZk_-Kx1m>rroA!8PJB`+>vqeyO$bCSmo7Y(XH5Xo9 zz`$l*EW3iPGHGe2oaLjV4Kd0{!)scdk9qhgU246We~Q|MjHE&gOB@ z#XiRMr22&G5DFX)ZUMi|9Q^4mOcVOo{(ANgR&OK!ZHaOp-Yqz2wPEy{v2Ys3=-PE#kIJcAJlh`!E?S8TaC)_d-)*Sa3D@-`9u{8DQ%FYsRsF4t;&iJ3^& zeVznz1yq1+CZRLI9|T55GHF+g$@)6?sZQO5W@EkYaqu7Unau*E5)W`%gH4GEQlpqm zj9^MaHd1W71VP5GLQ%S&A+;v}Vf`b?qK_rG9AK@CRtvji7O9_Y-wgN)oH6TlXt69e_Vn8w6NWF=IQ$pl!PSBLxqqpwXW4&9(DY?c_dVhBOLmI7_=k*S zBW%bJ1qxzt1zF{{b0^~baJW&+cng=Ye9=M|5n%YlAjIo^r2IBhf=PB#5^Uh%q&cMf#XfdlK4|sFTW8Bq>3K%^S^fsJ@j?VAZFb1ai)oeqI>Ij|CkXio1(##BL5@NNdDmd4U7@ZmhhPZd|yNRwjEe+ydCJA{1g&AE8={8YL&LY^M3o9^tH(%5p z;|=iZv=Z3;-&%ljGM|FrtuzL2q7tH{L9W7`n0~(zmzH+MzbY?3@`;pW08*gQUKeD( z(D%PAP3_i`%zP+AFeMN#V2krJ;VPHVcMPq$hHBe6Z@trwE)u0GzaL1ANkpq(;f?1! zCO3+#U9d&^BDa=*&o2P*;)izfu!DlMCdSG=`!S8(37(2c?9y?zFB2C}orxOYDGt@D zx8+t_xOXkh`2Qo{fN?eVaerE(gEkszdy-G-8x05(4kaR543*bS{w`#jeGfv<8y;wC zxOG^Fk9-`uT|};pNlNYLXU)0Osm@1%lScH|`fqF ziAV7SM!EF?oaWWle@@z!sv4!+5aEt=YTl_Q+H@DKoBIh<$=aWfdPxy?Ft9p{Pr5pn( z8u$DKZvOH;4)kLq(}HDMCZuo*)kX?NjgL;>kV6O{(eQi?+kyZBM&yu2zll?~bA2>z zH1;Be983v6!9q&Zb<5g2g|+*^yG7sD!F<=E8# zm0;VH%@J1Sqb9xej$V1N^z@sfhO=0kH}hxgUbh@)0B(9zDRD3JnycPjH1+J8x@t{g z9HYnA#IW-CNpIPEDA+x`m7AJS*xc_Wx%YX{FguTnAU0!MvhW>e|Hnq5F$wIhPAUIq z*3V7kGlqHvb^)CzmH*pz%$n>yBqVLy=%tSLYXd5_Y_L6iwBxug{bJBBF?({u_fFkU zEPx2_kg#UI*u16)3c-9k-ECD@KE#XoOkBCpo9~9M8sk*w%dQUURFrOt!R)6B^@YE% zb3uU&KvmL{w?y!5%eK`Zn;HKnS%;T$cQ_c+5;2Pa5{sW<8C=oSy=SJ179fTFY`YHugA_6|2|tx ziHHF7C=ZCH*6`3SP(sAU2ej$UonoSX0z>qQ)oY#6aJl1U@pazy(z{`ZY#(fJuUf*n zn+Lmm4zAzYWTset1*Y&L9|B+MsNqS>(i4b)+dL)M%1uMbz)~Ughu?9~+_+_i-_AIV zZC@kd%UBy;Q%jhWw$&>)9;@AGDc+;}TSj9fZxB5xDf=1u4DlG@0PXcHEK$?C#h!Rp zqx41v3ehIn*r=fo5hPk+f)3USpZ=P7PmzZ-XyxG&6wIb1^iY}|I=S;JtsT&tyv$&u#!IPxg;|J{2 zhY^bSVMqiYa%4{g-?)Q1Msf%XQe-cjX)Q%G6;PO|U|x8@F+cgWdpL4r=aNU^6)&0{ z{;P&~udBCw>bsBKG53B0W-S!T?k@clv3NAyK|`pKTOmLq?&{qo?eulb!M%(jnegsn z5-e+t&sb0bg}m%c42zB_hbccf7cpA8O87gu3A2`Qp9;m|bD;xDG>T@EMc=+fARB0q zTGom-l0p@_26J&k_`DHRq$#BbNPfxUu^|R)giQk4UES2TaT6^>F-V0Wd~(C;L%6t> z91>aa>G2ONCD^Hv-JCqekK3yyI+<|Js(9bMr2TeWo0n5uvxwDXBAua zUP;2B;vQC#iCk@j94;~RvW|rm(k!Q?*p4(man`2E7U=!GgO3(cNinn za+7DI8SU@Mw;f(J$fs0?=l&pgscBUZe(z^h7Vfg8uB(vH|4z%gRup<>JfLJh0m|*c zLp(cjx`)Dp13UY^M61}4QJr;pVGq-S(K~V7p6GeN6$^Na&oNZVry47_-eomvb}xyL zyO-3f3b?cf;q19Dr@|{{?k9N4rb+tHeB)!zvo8DDI^^{9el+B}7uZxSm@7x8F|J(W zhoXK1`{!;ITgSEHQ*K)jK=T0Sm%2Yq2p+xjdcX)6Z|E?OqhtNvAz|*C1+|MU1e;lL zCHB)oUIC2h+68|DZyloLTLPs5^CcIDmAjHQ(Zk)!2MM`yepZ69Osy*}_$lXD*iVD}k zAY=Maig9nKn=nMn@ru^iKPKQczDC zW5+Ek|Kv;F6}pLe&@4N+SmikUHJUc-^Bq15FOCx?{7gBxOLIgoceSN~1$B9XFn;2I zbV?uo++m<8T>54K4+L)C4>ck1dJ8$`b09NMrze`{J5fSey>$S5cdIfGXy8wPnWL51 z%{24f_=tI3(MM?x{?Hk)>=4^O%LF9Wx3T!ExrUlTX}e~6R9xBvH~he9bdhfb4A^lU zc_|YgtbaCd7`?9hF5rem9vif?Yq^Lrd-NVW_#)s7Y#uP1=yi(by~Zz`fiJAksOZAe zfD*=Iv9h%^Hn}Lwbz|lDf6LTqes7A^t~U0>PRR#S_)_Y~gl}R*4Lxb&I(ffVihtS6 zStggN;VczlckvHI%3Kb+c=KgY(e|6Z@*CQ z(WZTt5&x(tll0~8`vb|}aVL=;o@YI2Ha<%?Ai}CZ=F(1U8zhQ4cS8qL94aM+k0^;Q zej}IT;Kyb58Iy8X6^5oFNe4AcCR;5Cu+S6k|xUCbv?=pDc z>hb!+ykJA*l3&ml+eE8p{Hj}R8ynWL6kNFPcF2FM6!G)2zNi;i2M_8Z{Mmb*SlMHE z$#aQI4T#C(CT)zBghE_Vg>^!t#ahGKepaV@XyLWxz@~DJvJKNGucH3Xf$#FZ`gBFq zvz0J5$3j1~PkUuMK8s#umaf+K10dz)XfyhNX44{CF=yzqwhv$&4n076?q8~s=Pa-f z$|DTRF^`Z__Q$=wvj`TL8ly9G3$g@c2X*SYx7P7V7or zTN>@e`&ETl1q3Bj99#ytT%@g!Ah8@n_q>R^a5P)YN22PY=t>jeC(2A=W%~?|Pp4Utq#;e*k{610Hr|?J3U(GcwLWA(( z7{$~@3iu1iXe-%^wCx-G-Vs}ycI&m+r&Sdsh7)DWo7k}DxvMIWc8`fY%YyB(OR*>u z7)k%OPkLN1xp`#B$Fz{IfAu%aS>N^O~zMVzBD z;K8r9Hm1xSbxr)dwK)8a*DgxI?#^?-_zy)yicX`HEBu_iSBqeBc(joS1eoPTq-H=UH0~#(US!kg2-W8OJuM za8hbW-Rp%q$6K;u(G=b;AkD(fEJw>kQ*XR6cS=llv+ptb+IcFRX58&2URPlIGas7N zNPaC9?xItDSXo?2EBip6q1j*7Or=sysfVC38Q01`6TYWwMusuPH2o7U0c$yb>f|Y5 z{U>oP&(DmSy0%W)f*nPV9FgQ;<=$foxVZKG-;$G+m`E+}pUfe&LN+!5XB$p{3lOub zvM8$J%&{`DqRFZ%sNjjVFxp_uxz+teqTX#MGL@>#rZNE5C$tFfA}g%O3M54hIxa&4 zjX$PY2#G&I%FrWV>gX`Tjgj8UTH&~BtgaVp{w(!58>a40r;Pj%>7;C!n_;pCW9AsNa0F?cjnMUB&;0%)3ZeXPVjAg$r1S0G>#VKI zTpu8h@w)f=VOSTGZ@u|5)MD{74jOe=3>hNf-$$dLMp}qXr}B#0?=;ZKv%iqX$n!DH zj#CJ05fdE{;wH_!P1iqV!KKyIz~Q`I)L2_HbaoD@H#Z#Ns{8w7ecpPOn-O%L(M=6p z6~xzt1`*&MdEo;TfIxR=A!h0n;(Q>bpwvaytVRHd-$H*8av%e6vsa?0evYvklA zYR^VA%x86q9pVICaveYFBDQZbcy$qZeC^(P0Agl1XNE;o%XzCUITgzPSk=yX43VQlu?#P*ewCz;({CWrg@%5{bynB%oYnZn5AE+ z_Jf{M=nN4-R8g9vI+-Z_(2|1_j|o5~q^<)O9nl9?@D@KqNyeUmS;StHAUrZL^D~Kc zg8$t*LsHMKKwX`x4SUjN(8OcFW11q`hnXK_QJRim*C|`KUTnhi0CT|L_g)UUM8-Na zw1D#SlB{6+{`-XvJ*>OKDY@#@4<>spSjx~atzW$@t2RFgm1Sjy`WU+ytN*iCHJ|6p z3>)9m+LX58rBo2_xVkVIJGi%ZcsB{BMnD+U$89m1t+Km4z8Tszb>9A&cje+U$>OPh zEXaQz@a1KCShq(#EkB)73vIR9DAj+X;gOzMkjn6pjMjhqo(~K7%PkzQ9009IY5$fc zR}k6e3%aX9P~GV98dq~>LAG1E0Z}QE6)in!vX||{cqvbh?7rqgI$>n z5-c*} z!CiA>tyr;U3+LS>p#q!V%rw)8!iY{z(_^)f8h=2ar|ZwADSvp3Ce2|4ji<|_lU`=| zted*L@b51}Bw{3@@+Zm@@$5gnWQ;gTqV^ea8*$;GP%u$}AnK_DBF!`~L$KxQB5l*U z`E$+!T0&LV^1ruDk>)t?y##OS^?qF-!>&}Kd3wl+GUUcgiL&%ebAv81$vr;0w*T@^ zl%X4WmXEe@$)qvHV$&V1$XVn)Zw@rqKPf8>2sTA7g^-6N+gfGTG_*BOnzc)Ox405+ z)h)>27jq4pE$&sTtfRG=EB8W`nfCu=AMno({@F`?!ar^k_+`T|QdP9-HL1-tOu$AS zS_!2?6}MZY$e`;bpd4`lp(utM@$E>qSvX{=+}Mk(91XFrHt&v{?HS#^`7Mh_NF1O7MavtE^{)H{{tjtI zoH76<=wa!y>B0VO@PIeE%I|h8i!CIGDOxD`CBt-wGWUQ(QYAFV&{Ly2?)X_l|G>sK z@!_JocJ9&HN?<~=>4RL8v-6An#U0>L9%4t7hf_^P0tv%E6@%CR7A|feStoeGlgESA z-IQh*C5vZ(-ZwVu#FwrTuGdKjmENKy+a*8TZ}>>0%EkoDayu!go-XbiqUL$T^=kE= zay*spiTgO!4&BaC_&8}9OwnwkTRrTe4`^jN`36ZjM|m|2j7miW+U1e_vxr-&?#^$O zTx`GY;w)3_m6tSYaYXuoe`IebGBW?Dv@OQaJIxMy)$tM+JegpSt8S0O(f*z6xWXAm zKuN+Cv1Yyh^2axdmjB(dEhq`Qs6Wo~i%+U0LgZ?XD^DxUMz2RCv=&;0ifNI{v9z8+ zz+A7ZR_pBsbBbOsZBi-vLaE>UF)cyGhk+UnAq5xKhHx|VI+~02kBVh#U}Mw<^LbZ1 z(Lgw`)egw&*7K4}DkZRzj4@x)lEK6v64O%B(uDSjkgrPyhSbZ?+2@C`> zi?*-D$CO8@(ZyN@RR)V^wO;0e)ypDBELlJ~@|vv2WyFKYgulzk$#?MLC`s==>v(~D z`<6~EgGPl0f_GKqdG-U`KgSsjru-}y9XI;JL-+Zi4^&xkoYpOsYTnIr&NvMNMO13x z7*UT;uMMEXXuNepUbaBnfIMV2jbetfJlkDZ{@epJL z)qOI`C#4vJXDta=w>HW&fYjaE&6+t02}IrKn|bF_GBh4e6}e`bJ7bIb%c%Y9Ax2p; zF7C4_!Tr9VRDD)0ZdQYa$oSlca^*r5hRrtiP@U8_vqR+G|CFtT;?phsU`rGVcd(ZB z7Jc@wFs~psVL{d=YD}-h^O0f4X&V!yL-y9M|88GDhJ;YifA5-}FARqy^c`O$4bLy{ zTA>QX-kpZVkeUb8hTy#_86K;5L85Wx(ndJ?M?(`3-$HM!;zMEVJWn0gtJ(u+1BZO{3xugz(oyS3idyanE_ z*>dA!ZU7^;UBNF&hXdlgZw&rJmg=UhPv$?}@GtIgDk_&lQgIm}KBNSd=)9b~?>$)j z@N>Xu@C9+a-5%T6tf2lST`k?M!x`|#19!*2X?~7718=qD{uViT`rJ{c+|OQB!y(rd za1(yGEbjBf@&02(bh7qJxx5|mp$}x&aQ6S$0Yz$q{BrWPyaH|Z@|7ReC$nv@xHmR_@uuOEt<(fr%yNL_do)lL+wS2iICd6$ zb%s>OxM@$7frOm|&;A%BRT80Gnacwc`j)*~|DSjYu&H#O9}j~ad_?TeQ*r&Dn5Gay zZ;GKY@vCu=$*=%w;(el-%Awx}@#~+&R$-;!C9W|BrNKYZYv}Rqoy<%v`*%zoa++tf zTDZaNatS4U7Nxq(muKWP_b6)kMEw&3$q+{K4afYMC;Th)=1J0V=RGHv+9AE0##1J3 z&<;*?Di{;MdvkJPT%Ds+E3jC*&3$Yi!U~2MfO-}0!DPCAj8u%RnHO}3Mx|l!rr?fM zNc54f_t;#A?>T&16n4xTwi6#&WLX*xRhXSDLzj{JDJ?1}DM~CcWOc8HnRvU{WS?hW zTf6(;1Agf#0MuCiJ^R5PK&9<>-Vub&P;WEqPYEGa+HoVinwh-HfLHAyMMV`nRRGNi ze>(=`6;^uK76_q>xfTzc^YB(P37i=yAt-xm;&24-90>Puj~6>k1KK@q*s$G>i&EZ<-rrPF4RHWh{7xz8v)Y-m4j^7VXW;6bsnsqs0(TK8yQN^rG7sG$ zi;adj^-=frwG(fnjW_ru<8i(O7f==&yX_@wyYE(9r*?9OuoC#G3t5f;;K9%x@L)OS z6kK+3eu=bNPUl*(Hy;~$GqqLc`6cGPk@;F5WBj{7{O4g_6+-_Fr#2i%G5i8D(L=he zluLuQ3UEX_0^{}%4|<|mvye!!k?`A!wtVUY3FC;gKSP0CYl3${4I@E7kIrO~InFViR!l9#u#UI|}`f?l_!pSGRF3LpwJ6TO|Ma zW^|I{tX*nc0g7qHqxTccQU&cs2t*BnuPNI5$8OO{cP z&ae&Ms){>?@VA!ZG%L8OVmCMG)&1fBPF(nT0q@jEeHN|vBdP$E#lPnesGp@gz3i)Z zX-o&i8LCk(S|v%FW2&dp4vF8iksM8=Y4zpiT7w8!wFNzTJKRS$*8@DY`gQ#!zR#Ex zm3H1tB)D*yI9}2Rtq060BE7I%*PJTYsyKq(s!!dh{t)EweK~>|(1Kq)F#-T88Q%)y z_q!iC|J~IbX%r2)VRYqQN)k3})CK(MCBHA~apzN%r$A}$+$;U+K6q`2QobUJ^jg*MIE)C3gwq2?nCP_w$wAfGR&M86?NrfiqU#fvlSj?fuQkPJzO!%Ds zc}kkNGoZ6K;sBZ%2*c&~6LV)Y#LUz%CotmSf-;UWo#>~*qBQwffMscy|3sJdn7JC$7Hqzu^^s{ivH?cj|TAx%WB2n2rnfAqx-@a_IGtwsJBjp!M?==9V*hnP+ zpJaelE*3(Ya{rwV??z8O9BYU^&8OkHaNZmgC>HbvtR7~<$SsV)Sw%MRH5aBV;^yN` zFZANP0&0drG7Icd`tn`lBl_5~-=6fb=6Q$I@nF!GPQ6!83A?lgIjxj(-@Y`29&>5H zSVTfbMwPc~qjeJnE`73F|;ArV)vjxZO$K_J*Z zz!n=IFH^rz&sdU!Houm|zJ|aFTkHpZx<6(WtL$JtR36gNg6Z#dc2!9_=v~8N_Rv?u=*FBb^-+W z-rA6kfNyF+0j!r0QqgRl(tX(?tIYLo9K%0BnScaWJdMeBvFf9P&{#VQN>F?C7||4A z)JQ#@bd&{ARLE02zX6R=(O+7rvyB(&~lA)WY#b^p= zbVI4YuU5&m(sVF1EgH*9hQ9agG0D^r;FYPiZlIyhh|txD2mF4Zr~==~c3;TE0K$dg z7-BQaWGC~)d7gI8o&?6H+u;zGbu!KuoTs>r-L*IbPRm1as zfk>MJ$A6y?aB!?*(tl@>qG3Brtzkqr$34SyT*hP65(!R^wMwa#wnkk6V9BSouI4df zYd>BS0^@me5~J_2^m(^;mhn@~F?0|IWiXf&i@%;3c>#4vZlErifdkae*~(O1;Phx! zpoxx=>Ac~du;xOdd>8W-*r(yq!oPBj9S>!_zP|U3G|)peu-!}o+5`9Y4RxJj@D9mv z;Ss%9L1$=*hS6u9obxcDmR~qYPhSJy)+t`D_MdNhVfN1Sr`LUd;Iz;z%}>35-I)ou zdEj64Cb(Xox@*mPti3dEZ03GBPzzw<`+e#E4mH_EwY?=zX)o|wT%dCzLwR5?M3!V3 zc?;IeoZ$NA{4HhfnoND+7?qviYH;;l^Ssr=Xa2tyJKSCj2K!G9HAfKrGnDdP0a4Ur zG+|@}OOOtYktf+`3`})_K_N>1K*A*@kt*#kE3{QtM%3vf+~JWeh>;F?Mi65*19WdN ztFprQ2Qf#qMJCEalzN9j14fc@+eV}P;_L7NBdm=(auBYBWQuW05DR)#yr!056R2ja ziK(lwV9Y+5o(Q)#OSTn%y|RyTt+L>tRHMyC?McZK?lH@O-^^*=9BD_y41Nq?ynASk zjp&8NGxjT(;@*tbM4;E`n1(KCRT{eel_|&hegf;IS##9!w>Rq2EjKhyUVUgnlL|2| zZjdgfAaKB^Q?O?1jgi%|`=(VDpxPmtdi>y)s`nAI^$h zhpEfVd(MCbHxK_BKojz{x zje(y+V_&W;dYxQG4vZ!_>5W^?6}oN%+!{nWbB(axY3+;t2`_xQfAQK8PZ5cOsQ@-c z6^&ngDmN`RwN47B@}xsfW0tb2(c&jBu5uVFNtAHHMX|9zYKe)aoSYOoKu?nOIhzqC zVlJ9g=d-SWvCj=r7|3Fr24?cLg-Sf0^F=xA`4FPvTK)i^-}HTiC{=y&-}XH((7q>0 zi%XAzq%9^1{D|IzZe4}E+q>vXfBW*N#z=U=m*4%41V!NcL0(~aXgP%-e{8K{sCo~eh{4i(Yc@h}#1Q(&U6DIDb zfSy9Z1Y4AJS9UG*VB|7JwLnD&o z4qJ-2jgD5z9X9<2tfpOX;l1&kQX?h8fg|CU7K1+QBy*};G6l+Y)3&dBSW4LO;>Ib9 z;yMH!LjyA`v9awpP$(&eGFc{t-(g0hE%*u3V$-7c#*0yAU-%8{r3!X_^AX68%w-y( zsY2P~jZrIf^BjJ6o6DCjxDSsm)aL zAE>cR9%buIxcN-on6_?oOg~%wTyeGOmgt+A22Jt75B0we!6EpKY*G+7{an-;%*b2XxlmezfGCgjTG}kke`ug*h<8n3zh1x?$=YI`C+Yk(&cc3d1-6EEBBRbtOsRs<8cZX@7ANaHE7xI)l4_D z0u}7o^cb-hOsBi=c3u3XZwfRcosp#X9#?tVHQ5;}R6wXb+pWSMb`w9c4CV8*@fN#1 z7At1+_0+~@zW-=P#~7{3^hzdRNpog;?0Y6xxz~?K zIw+=E5A>KiAH?NijSv!p;gWS46yXW_&VJk5IJ-YnG`5^MRLtd`1yniCDO{3Kxa-DHug7K8KJ&&r|Tz#(N8oS9}tnQ)lNMp%EMv)gzAU(t47(yW@R{gDtF z`cJric{<}$Yp=YbwYm9zLju$``^ya3Z~!4nmPgM^n;9uwlB9m z-Fe@?k9p0T70nkmDh|cS&2(99-HCEI%)qbF0buKcf(?tW(9gz4pUg9zrl5gSSzh2;RDJeO{0VPGzwQMLTZ2 z8#5{@Rdis~Lb2sP6&r^RozLK8At882r?MO7Z2RB2aj~5=wODRmlFSb}6I{NP=!&sY z2|U1N;MCM<-dk3s{amT-@DWjZCR_Fy)x3}2)Xbd8ALV7(0@H^hk_qDK*+ps?V-b_i zp0=8{aW8e(TC$UaQ=`=RN}?r-p&euBIB<1@)Y+hn5cmG= z>>yCnh)FlY12NO1*-u#pDP|;;fO+MajNt7aP+!YrX68QF4Xa$YRigJ?(h5aJ2oD9| zlzfwMQ=Dh`Oljmd6c^iJ6&h}vd*4;6h4u`d@X8UT9wMd7bPIlez3!ca6`h;0B^aaI z>W7e79Gqok<@at*2FJ=!9y|jE74sCWTph~UyQ=a^$!-RZlQ@j+Hn_w*oqB+AltXK5 z@fI~ktJ_SG&m^HX!$vN>BLF$u%}#>h!WVr&4bGeAWW8OqF3l*7s~z#~t2i7#zlocT zW2-;Tn)O!ovPbBL{{A`oZ3=;s=L2K8Fmh%0u)98jhSw)wHEK~_GNCE|=l?n2RI=3M zFX1(0QN{g>aaj0oSwV3do4*eNk3}e$QOAuWc0GqOXo0ZgE?qa;p^)RoKHKxIr#Di6HcgeLvz}gLL!)=CWC1n>!!a1Hn~TkDWs(g#S&ikJl_y# zJ|ZXVvduLBp&8%qGHYL~waxhX%vM7z6$6!S?^g8gJVh1|w9R=$X}xY3u_-qjXHRK; zD(Xpc@EK&5Nw0usZI@^=*z9mAzPJ~KTMqW{8@zY`pF~ie#G%`p#6}EqWwu-YzCjO# zoM15g0&H~<34#|(bgj&FTuSA|?1cM!NJsfKI&f|J??I59n0isOgK?VkX8nWodL=?L zb$XQCYyrq>hCxr0@*3!<9rJ8QG!QytzkyO>?AcFBm-XEA%K8St2-zuL;#Th0D992t zIc3gz^)7V6&GYR#rPPm0rRhOz+}F9_>CP)QelRnPIZF9@meNDHnHt-Abc-oVP8*oU zx!6N#_(b#21b>vh>f%N)YsFUPjeTaGsicBai7xj_f69ZUrOTwnEtg~bh!(wV+);sFFp}l@8Hpmkg1l|h@Dcvk3grBx?MgeMWpf5AFFa6`dHOd} zf7d}pxuI73J}ULNQ1j|sta`zN$S4CZf0{TBbwWU=QO@=-LEOxUI4feYxowWzMC$w#b?^!MtM%&t*hmdtTWJbcFVBUYURcglXkmyr5YZL*rzD^hB5GQ zQ&7}fn&f9DZ7os;Twx}@n^AT(?7Huq#F)DE?u=&DDi><{C4T#Kc1^M?Y_)6QngBm; zIU_-qGZT-go9poH0YZ>ztRYoe_e#vg3zrU~=?b5@3!6y_+OTwmN`~qYJmM<*CsZ_5 zFo4M1N!Co|la*@H3g81)8#BpZ#6sIT3>+=Du%YM1j8po;D>^AG++t2+J|t%Z6_D=$ zaTNd-zC95}K&)=nd01+1DLwmT?&}?soDo-1LOF(SxKYsZ&UE=(n0{!gbw!(O5w>wv z^u_6GAVP6|(3ar?`R;_O(|o?dUG2owETzg15%(jYk}^B47O`)8v%t-^(X-odV+%1o zeZp*aO>b6Rph}Zg9a|KbzeMSU<8CSgNJ94#ZW#f*OZVZ|1z{Qk*aPqcJefT%ppi$E zSm32F%N5#XU7v}paVi?sM>(+%Drc5FhmEkN`5)uwmYH7r%r*2v)bfJNtu-X$c5lHj zfcX|sOd9rTS_R^P&4!V2r_czv@@`AzzZvMiuD+?ifhh6Sz18Oi;aS|bO)NFoK?(CfuTkIpq1GTQnlc1k|%-}^T{V^glK^w+x6{| zdu$il`+L5gjl@ixu5vSDZ~9ma;{)vSD_12fJr`$Tjd?nmnt)w2#xV4Z`~oT%Y6hP5 z?j&{B^*{dbVO?1!H*oa<>DYgzuvwD*j;J_+3yPYLh~0TZf%YznS8Yc6%T1VapltNN{zjH3!(iKA=UFG8X%k6POu2jV>2`10$?IQlP$!qEiE?g)pY3JTI(glml-&h zlJKq686S*|x@lK%)rOf)YPT|>ynF>pi_$is{LA+ScXNRRUH8C^Zg+j-xZ&_$4Qgt2 zk+c!S^BF1d^4>z=@NhN;f4v7???8cv;@Gts8erSSv92LYMu?(YPw=rY8|$BUJrY%v z8r7jkR%me(5Q2^dW1OdAS~VYe_V1s+b?i;G{LI|Kd&!wOHa^7anc7NwvwFe@x zChDrlh-!)9LexX-d8_1N6vfvU{ z*r<+6l%vIs{k_vnopsHg88lX|AeR`eD!GOPrKZ$}ZT`LhhI5fr)oEOnir{VDY(!Dv z1X|SgSWP>{^3fzI!V0dCIWtq=|c^@}S zfat-yZt6E%D6o;AqGDl*|9BOBEq>4G>oiJf2Mkf{jY4(rT*X*ds?v@YM-rn+Xhv0b zij~dwPVh;|x4pc*AZ>$(&9yLWHuG2@9ArRhVaZOG9Uv1H9343yvs3>zSg4+UvkV#4 zMp{h|v1x0Kq26oofPyi|$HtbM%M<)E3vYsj8d1U_DooQ<)^Q7VirPH&1}olY9EvC) zu7%bH{rDyxjlZ#53Sv@YFWc~l%U(UNRjN_&YU8KTRBdbmDcrME=eT^kE8*pO z3$D(0YTGne#)f})MlS)?wR7xP9Oc@*Cf!Yd>Ke3+J8v4ngKmnCF#zS8|NI2oi;=Pd zDlCu~bna%J>FwKCNR zroWneH5?3RiO1OK!h2Fq_)dDUjC%D~>@0z|g8(t!4s~efmx9hOG;o*_?0~YPG&8b3 z2cy?F%)E58(QH|(zUZq)nY{9Ygclv_4FR@ywIRAcib}m80SHpqiRMxp*V2wFPWVEY zQ0*Rv7Cp!D1_XF`DZyvcFbV8Zv>T-mFUz1OoS~Ayt$Z%}jJe_$SNcO!RFouyl;s{P zpKJ`B7erMxI+MjO&ONWi=YK!@Z^c1A^NVJ9i zQLKZjkF3lr1-k!%hB~;Q?yDEB`1nwuGXy@khM!m~wX%}m<(qsvmTHlVW;J+CQEFt# zMD~ciX(V&nz>+e?GS~_Bx}H^Aq>!{;iy!|HHHvj=uLJ6p(lA>yKDt%%cLjK$Yp}GZaU6P&t zvF+|+wI|Hn1k4AtOKx~D+T9-KyrC`<9VbjDQ)#rMk+a-(-ceoy#b#gCcsUd#p^9QR zt7m{Aw9e%`t6l;txC4u3=_0WIL7$*Y4C_xF?(?4RCK!EEffi{D48i4WZ6Ae z@_H(_s~7Z9aN_?$(^Q44I#zE2unld?qg`RSGsf-!+ zifgTXgA0(W)@&*hBFRF=u*({Tb1{;z@XM9ChcLx==8ehi;dyECV#0eUagVGdY}W+d zDgyi?A58JunGN?>5X$(sT{MK1=};vfNmds0RNmNrp{Z4`kxSskJ`>gD8ILei`8&&y8;4sgcAqT1g3*2skX#!~D7HIAlTj04_;ZL-s7@Ofym~*s6M|CL6_O zSQRdkF+I$zd}S4$Qb5~LyGMsUEe>jYNH~9qC~UDh0##CmALpBlIBkBC1TY-VlY~Qq z8G1QC`j`Q+tJ?TPog67TpvkD(^{7gd64co&>**jiamWmcO?PdF)AkLVXS>SqV+#EV zbslWt)N6st7=;^MWou=1^ z(6_l793_+zWqn51U;>m`6t{ovML+TAa1JqRX>B;xW$IA8RKNPB3PUl`3I$M-hn zL06Hz|1J5&D+)AxAYq3Pn8}W(6tXxe6{;6}!_Q)^KXauHT^4aLqB-A+iM|Pt)AXhSP3s=RC8 z$kZVrd}RTMi)hgE^%4@xvDAP0ASt?F!&(uUJmuJ%R2Y!&arRO3ugx@rC3EJL?HiyQ z28ua|`=N|qz(Wpo#`M?`p=g&Y1wktY$hlOqO=rQ}W<=Q@)>v$32q7gtuyo8|b%A~> z3)M;g%%JB+8rg}Sj=PM6skc+4qA699Y1Q})0j8Lcum_P#SVmrc20+qJoFL{{r<2WX z?Ln!+-x3D+>o~~ZcmoLm6lOgw1?IAs~0QF`ejnCGuE+t=@)Owq57~F*^m4?BM{6~#Ae;~3De~+|kS2cWzjUf^c(8t)Q0GirzlYkOcRR3QEU&>~B-Ed`g*|ztHZvb_)zZEtA z`{}P38BH7&{t75l54+Tpj>^`KYBY!1243A(5c`HH{UDLuqcz7LOP9~^K-G&ooGHmd z+ovUc7+tt90`p9DbRXya6pr!U z>q$!!ydczQ(GeGXNGsorf#-UWkBHmOrKY-^g4+7OYPZ*oYw*Fut-?gB%)LsT%u>gD$; z4JjHa+JtUVn{Q?_Mtw66;VB;T`qaTLj3*IfdkQw$rDh6^K*QLFXylx{tKD^<3hndSj8o!u`bv+}B z?7t@u=tdnqttQ#OKs7H~|DxKUz~S3!>0T}ZHP_j{i!ao4^i{#fBr}ZG>U8L%M_)|w z8+%;E2Ni&Oy@EA(3Vy~gNeH!3VI0GwKBcMDY3n- zb}qrz0I38~!T3>N+)!~jzwHf04F@LGmOvor<}oyHF{~X9l5dZw zKk7Ymce(rbT7#vHPQK@HjYAUd<4g^7N1)*?#!vdrv8j#j-V{~wZL-W&kelBHq&wHc zynurT@3ux;Os8uQ=_@37q(R`xniCN`VV$X`b!v7g)CyLa!@(JmjoZM-CH1-c4Q=Q(+VT;+ zJav7y^S-9Rl8w>xEY%brAVt}_p^x)_mB)1zej3D+(q8<2eN9(B8*n)EKK!T%>fFV) zdPe7Xe@eB{U&*%&cpZot4G3_!O1qAG%jxSx!I#&b?igtDzvr;gcRN@z=(PHUTuL19 z|G&O3AyoC@yx5zvv0Q6w+czG;>Z}Sh)<2KPn_vzE#DpG+gNO;G!Sfl3wB&|^ylFd) z&tl(0C{i5jitmyaiv$HvCN%HFgGZ??KgWxLNv3?u>uUa?n6nQw;#MQN_Kciwf1sjuktI#q5K)E=^3nNqN@Me@b)sl>u~c1G=FoIt;=jqo$IW&MKh8p5{aX-Ow{pZD3H<`6n;A{Fg0C@>rC5-Vy+xPfINAvsFt7^GDoS_5;j& zQtmrf38KIz=X##X`}vF(mU^N9Z`)Nr$@OF!h%f*7fJ83tjtgHVo%SUP0Um27A$o@a zIz>2eH#h{NPKS259w=zQw@2Q~U!aO_|Pnw4E=p0B%E@gVfDkAB#^*Nsr&=CA-A++AB z3$F0*EWmq~qyI>bosy((bG1up^N~tYstV4iaQM`p@8f@pfAs}zU%|x8vK!(iqLTng z+N`%ruN@{K$J3HiL`E_rUTiuYcpxKTtkJBCRBcv$TC@iJesF?V+Rm`GYEpd!^;a2$ z!otmXi^eekR}qHDU$n|+${3{q$zQWlTcVSaH=wBSfIDPsb*Y|NDkCT3N_u34INWKj~wG0zUv!XW1 zn7ZN>bwygIV=8@x12}-1IgLEoH(O~~Vo@`!Wo24=Ba3pa{^HVPCBs@b?|4V@2Ko1H zD)ec$OU-;`h1$VPK-x4(T~|ys!8NS3E8u3UObG!Rp*+$LJ3RpFm^ZG#L-<-JnD`@) zhS#{-lk4E?6Z%}B1K^7?NeN-egV&>XXGuPxB;b;E2J7h!(I=N_Fk77=O-+U;1PWT` zVUkfdN9OT8H|{F@K{z|G6JMk}0Npus6N^$ch?1_zq9FY2Zx7PsIU>a=4jHyKL%vV1 zZ`Cy{UwbL8%xJy9UnGdxwK3(A#RhucRuCV|bKDf5%|31``v{n&Cx5GAo;O2VWuNJH zznHI&n&j$fnB+?TdX+rMr7!$Va1+UsW$%^9Pvi43eDppys^rThbi_vc`Oy=^=Bmf# zbHhyp3_Z80FHU4cp(+FaR18eBZ7yL>*#F_lND+zBkpMAe_j@k?E37bYlF3x^l~J~G zAX5R5yA49mifTV#DU~95i6)q58*?dPB6&L{ORWE6=cBth_B9ALcYAS1$8klZ5 z?ycw}18K*G>!a8>B$X6Uu|&V=QA*s6i(WZtC(05i9Q8`ECTPFVVO7@_jt{=jN#^Y{SiMb&W*v zPJ?_J?i&C$f7V~(%VJJqS2*TaT3XLnWH7GaQevjaalhOLziUGo>?3M0l~8;%%Fp#l ztgpIuD>h1@JM(|Q>ijPE>k;BMrlr->GNdVnE$a?_)(6A?r=a8>?tL8e)ALQSd+D>m zl0gTiyR1(d0aBbirW{GJCRT%M`qPoAl^Pa@3g!s%4!)UeO@4}6X^M>r1gSGr zOVjV8DvBMS$%8liDC6wc&X(yDgvrp50!hIy^}Oa!4;)znFG<@GlfSwKpH|PEceC9! z%T`%QLymaPNhBfmx2Fy!&z)Zm&?6N~OskX~rAh^nWF#i}&D!3Xyv3a-d(juP1v!>%scIa(_h zpyIQFqXs?Hd$dqQmbGnlP&oxaqf=}>O_>f+gA+lDOEIQbLaQ&zUlm^fU@ZM)$jL5^VnzP?fLFJ&qv+2 zr)<}gn&*J`UQwuIgO~6#`*%bHMDdeQj+sR}=l$-^zB{!r=5Y4a8sr!Qml>{oeg0qj zyf%iq&phmU2U*u*AyCU;*K_T^-lw{^4tULV-XUVf&aa~GYicd$6(3Q2(EZdG89r?c z!ft^2I!QvQ9v)kGs!ZJCY1}p1pXkqDZ*AF6(QN*{NdFk53vfv@#`mP+_|x zQx-V3ojp~xjaPR-z;HsWLQEDM7$v~wtLV*3{oxa(^vuj*Z$E99tiSH~Jdk>9ifSGt zRUuqpQ}EeGfT=bx%4~U~`L7SmxrR$<*f`paq2Rv?@O!R;XO@$Myn90NNy13P_QAyx z!}~P{w3drCn=G{qI&72*G5ppgwv60B(!L&NxAm$+I`09agQnH?$(BhQs3q;3AS({0 z%*C>1>ZB8bg4-b$tj`HW_+9&(-kYLOalBtGzHG@~1O*d@o-DI=h~*Ub!^T-`B9%~C zeoLT`u6Gcs{q(6Lxbnu8zwW<=IPhZa^Peo3a-7g{qd(TnKmx*ID6tuSG9_YBwg^+Ca{?(seMgWk`w>Q zhXZ8<#L0t6+$3prOTF)XciKFaEu%|PGMk_}-YKjsnae@g{DCCWh;NZ4b%p~Uozcc9 zH4=+S*8|prbkXxdRNB9IUh}!x+0{18h&16ckM(V6k!0OEdV!+@Vgy0ZF^>I_<-SEb zo`3*R0Wc^ng-$`&ExtC7 zwhK0Sb;=i3a!g*MQaP>_0V5X=kYc$*t0#Epulz1E#aS{^RMdjVwXO)g($XgGwkkg^ zPvNE?>p!f#ZPrT-2jR!7*t0V(i>Q|p_HtF+9PqFv>I2I-gH~zxd!&*Q6!aCP*b7XqH z=KDLz*pjSlzMb)E$F4mECpKy=i8@ZRj(6z&FsPd#3m*S?w({O>s3mm$AY68x9Iq-eZN_VF@K6MH6`*b`#WrF|&dI;_u-&g;-a+#YJ~hA!6tm%0b+{G7nCN&VkKrmy<9H}naRn6d7LD5 zJQ-PkPxUC?d z{~uXj8Ps<7t@~066ff=`+})kv?(XjH(&A1?aCdhp+ESo+arffxEiN}b_x|5=&fFPh z!sN?ulAXQwUhAL|}LK zo^#@iBWw(_U`S5?Fu?|G`%?K za>)*DAo7G6CR-|J=r$!hGSV<%tZ|?B7++mnVO2#_WQmq){>I>)&36Y z{E*6JsKwyA$jd@}g(l3wSgIv>$|l(RN6DfEso?H8L&6^%{c0Q(#?X;wzn>I|YNOPr zxlq=}Q_vS5{Tf~)PbCR@Vx+Fv*iar(v9Sxz6=53uFMKEC6*ZQcI@rW^$|9YU*S|>- zh9oIz2rcTX%61Zr`x+HTpmo_TXv?v5_b%mke3G#5q` zSU7O23b%NOkt_l0V!`aTRf`2zq}(@zqDH9E?yv)PW-BaKPkLw0aC#n7RSfblk>-^2 z1tND3;9A0!=PT;U_H8{J@`;Mea>o=K6$6Z=CzidxmD27El*K^3*Q*q{~KAeu>MDRwsA9&IIA@Tr^D?NTgeQJXSr%* zOu*}!)A!OpVJZ8w7J{<_7f-sCyRl~)k`!hpq1mO>;gxyTinH*gZpN@yjr(1WYZM=N zX+l%NCMvZmtoki8C^+szER69RX~5gF3QAD+{otFt(*Z88_G>5>Api#(Pm9&eK){=Y zs&a-*P40|BS2)sNJLS#>v01}*4Xnv! z?Fk0-%hwL4D%037)(QS?`@_jV$DskJJ{o%~tirvGQY|+FW`>JUQaqq?I5xD1!9{TjWJRDgtICZd zCqL2*g4$8-gbRhpMZ8c(^ihqaNr_8gTkp=au^Nyc@wj^sRR_s8KhkSVT5uh5i16rf z1#Umkhq?Wrto>EvIHSAyz15M+7{g?k^8@TP8~Ft1cDc^=cB5Y&))0joaWWoKQ&n*` zM`ta$0Zo%#ksVSM69iA7g2me_!oK>E8*Bmq=!t_{oAsb?u zKqLnblvzOWl!StRmU^c)sd{X`ne5ZN{%zOdA;a3>{~6gwb=@&zHSp!7sP@UM^?CTjdE!Z5MOU@D4>g!Sd)F2mB=tM42~~>g7>q7 zZ26I2nDJK9>Dp9mQ%IrdVtRRGochaVjip#PcqBBtHaPuZ>=5JhD_8LX#{z9$JD%zm zd-%w-Yhc2mMC6(j_i@Ssi^gWB;@3(ww|pRt65ANvIQ!#1P24o%`3C>ScFu_Hge@V) zqT+QQ72zqgYb_B~&csn7bl|l$=@>cA8k5(0yY|#)XeVgoE!KF7elS;T^Z8lP*uPyX zpr+Ob(!>S=+e|zRu(4{i6dh9M_p8ysAZW)tZM=-HqwbV4gX?TFvEONh!Lv$3Kty_?2N?gsSh}J zE4_@Ww0l-g4B8`i?L(VP>qWXb0l`EG^OlMQmT$Bx`OssUGSpIxZ@KjD-a#s%n-jGqaVIYF^_1{CuIn{GB*YudM}- zrobYzrV=pe$upx6;B)WZc0d;Xj;}(Wplx?sfNHK$2G?IIMj}4J zepvSyS*@1xGZT4?#Iu!zeg&O3fzm>9@&-OE1RH1Gd)kRel3o=IvSlAKVUmSh0~bRN zOh7>FEs!O<@MoQ5D!9z~SlOz_pJP;ez_=|$@QUCf}#*EOr-A*NJZXA7t~05sM+x#_nZ!{Cjg z=2wZY%SFaSrnf^8Hde}8OcAOzBX_k_2kp~8Kr1iizW6(@73SLV3<6M z<)K7@4SO@vl1-tdazRz-Er_&P-6!K`8ZlZP8^4hiF_$fl*662R;K1cy^$jY%k_Lwj ziO(KpZI$HMJISq%A4sq%G@Y#yUysyXY!4rGil1YxO&7A3TH(KJNG7~{9?042qf*kG zVKU`E5mr*5YHI>MjSl&SW{s&)J4HRZ)G^=D9=g3IRi=&In{SmF{sU0_?&`)B@4 zUfOtfj$%2#7ZPojx0DI{OzU}RE$$Rb$;5Zpwi3kNnabsgWMm*hB>|V#%B=H&gQnsC zg6ZWvaF!j_^}2vhGb(!PGj@xpP|kp~mw0Z?K7CJPihN~3wHaTbxwp9_7 z^dX&-qyIIRG)RU)1*Wnqz<1_qxF(l9J54f%p1U@0cy|F)hJMEsK` zEf+)~odWkU_ZPZ9u}I!8E9%p615F)m+ccaxUI3A7R-}HVnPycA3S2+>rHtf$KZTkj z$RhB_<(?94c!*Z&1#7VRlN;0%uwQ#pdsTgSHf{oOK%ojO2S4nDVv~DoA9v*2yCIjp zca>q`5u?5v4;HY8I$V_ZjmAb^Wz|3^5r=JPrFO6eZ{plmXfjMron>K?09hO)37Lsh zMctpxdFWF;N2#&K4PnY9Yt1xBm=4F2E}u?N_80A>rZ~ic%-W6nUT=pXNJK|K{psg;tG&P$|T}sd-i4^+0(jV*u z5N~!#QKVO75w%YM-HW0>Z>*Gs;vd6x=va8_%SiQ>s5A1|vt^jDNIDD$-R1FP zV@H#klPhH{n)Wg**?W^PpNC~kh#Q3%-NHjJz6EL?c_z8CSsViaW>SR8SKaumpXP$s zCD|kTXcdG5Spm@i_u5cv9XCZ(nSjRFxc3zmSrHm_TWO5=D<4bmBX6j)P3z1wdGzdf z2@(Pm%V~YXf~02+;=_n%L2M=e6mb^$rBB=Ta!pMyv{gV|y!d&j&mtUBp!ivZHO?Zp z8_6DHufacZ^QyT>od$iH;#XAWo3y`!ntoC!P=yZQyNJ-p6K~rpv-y6v6}7xTfj8l| zfd}~7451u|7-M5SS!@sBi^d=#`3^8Y=RvpgK#hfu^UQDDQ%7O)^^2y>mlQql&O;D( zb4-&rWPjdA59+C9#LZ^Hp)BX2+DbB7&H1HngG!#1lP|EZo6KX0?32E^-8TiN9a=|6 zr^e;VZ9>YAOo2n!C}5CKq8vwzp&J`?HbdypP7Svj*r}Vc3mU?%l*x*}N+W%J_@|hg zW~CDJo_%B&`|X+28sUjJCsFr3dnGZ5Y2p$-;5RiYe}y@(HoF%hXBPNg!TP_T2W(h_ zX|<9A%+y$@qXFRehcsk~O{!QZu0=f-7w0V26j`JPb(k{ifFk_;Q`459woKPeslfPvNX<4!Zn*eC( z_Zy*XAEI}5(GS3yP$t@Ze%_?FD>0UlMsn#va2q|+Fi(@7gboRlQlFb6(~y*!WuBI@ zLMTD+A<8E5l{(P~gNCu4xgbyE``G`F?4gCjk4jg>p2uq=poeKK(6st3!$fCj&cqNO zjeZ;PGp~h!@y8OvC1$48vSXp##c2>ZQ#5plq*18PXlC%3o-GXxYk+AKwPGj!6-AJ2 z2yA84Ji&2P5T7^kfsQ#OLI1$XX{-L>P1F7FZ5AFvl{Z+yg=5q_eN1vXuVwuei>Y#w z5zm3& z`xrz&&oTVI`9I)+V zZgJ5TC9-{ps$1>D|97m7;0>k7%V$*|=uA@mwxKT>9HXRD98>@;g=V!>hT?mkC#EV}C$LNu#Mf6T*e zAek!8fEhan#fzGtI{A%j^CDn&zz{~;Sh>KvMn3jrlW11s*Eo& zG(ow43W#IF=g2a^hC{ z4;STX@|;7Yd&tgZgs!H@#iydq(^UxzLS}>Ioor=kCX4xKX{X4?Z+SWLHt62gS4nTy zy?vBMOWLv6e@9uC_gM=yx+tEnbLr{Hp8q4{fL$&H0zhiVt+F$#K#Kf*J>CkgDIol7 zjzEuOe~U_FAS_&~>%u{()JPN5eS{JA8Y)d&JgrG5C!&}X9YqVJN!}F2%&ru6 zbVfKVo1Favjnvc|?u@82d7os`DR;l@mK9U=NgeP*PKrYu z<1OI(Llj!syA_=Yy?^-a_-M0owxei}ahITGB?>V;-lq40GzJB=J6v@h4+~1}67c!w z>__FkDRfRIRyTuPh8K=TK zdKur#BoxxOw50R1#sJqrUp^{^vSG3pTInPKVgu%t6I7Prvj5F!G-#b z{fSg0l6Bt)zGk@hNf~FGd*^R=2Ax=8uI6~j$p7I2?1WhE&CrN;VH8c?`QGP;0DYL6 z*Pe;0YH`~b4{Uo81G*4${zNLigwyFHw<1}4#a$me2?#E>2c8wJcfEkaL6myn3Gd8Y z(S6%Hii35Q{*m|yGAtXU3Z71lp-LAGqoAmXeJgL4Ry)o0xKTqBKvg*CE;>wPM|?ua zmCXs_t|d8ZMGlkpzW6EU`DD|hWzoW=F{U#(JNvDlb@^L25UZh-ST?$!ddE2bflIoT zwl2{dv`*iU1((nDPaw;O=GLZ5Yscxn;kF58hD z4BO-!4HRihW7;?HJf^856rZx07K9Q=KD{1=ldDV-ho$r2rs7N~S@uimi0La{b)w@7 zFc^(~tTWZ*bu~-?kKmoSSMB|(anT_8U9tX@Gvb#&EhlZvOi^36<;jBNs%6sY{Tl%c zrZPZun=1~vGf^v(Y^q*m%CeI8k0j39$=}>N9kCHwA)CFaeiIG2>)0JmE%ST5(C;VF z4^OLj5Uzf4ZeDL(&JhsU}cm9JQvZi^X=Rd-u{Lmh@NC7R=p!|zZ(vjoD^gPT#Z9U&( z!br)w^|vnZ+S48U0vcHG7RJl$P>#?&cq~!M;3-;c!&G<-G~brHX+R_MGhos#_;r&{ z^xRdzu3+JI>80z5Yr)~Z8z|w2E6!ud!?(4aJ<0Fy`h9?loZ-CLDixNy^8bVClN2&Ui`KUp*KAJq;J0lfD$tLR6Qa233umeqX({B za9YcQwed?GHz2MciMxk3l)V1lW27ddAC4Ki8>`H$F+)l;DsRZNllhnP&7hc8$(O~q z%vPhMTFUneQux+Q~JNYGR^FpqiokMw&B0ZUmCXlBk*x*7TN63 zi7M&dFPpr_Y+|B_;RH*)(*tU2YRX9@8^7cN!Il842R=D)NbeotUr_?fFJ2zH7M2lV zQEQ%#f>lQkFK+W$kLW!_x2FuXE;$$Lrm3owYAyNJcWIS{mym%D=&_$za!l70PuX+c zmEJ($SFbYclOzVu9qf{_!s?&raiUL ztXRPK!*el2Q&mWlGYRkBH!-yP5ED(pIx8$o;LsR`_XC^P18RNdiLzo=9EA1P0}qWw zh8KPWE|_s=8BVb$+JaCw_I`r#8H0)SVDW93d}R3g`alMY6^tei)4y5LeM8}4LQ%n0 zfo%jO9LuZE_=*!x=!RpM<$%fv2oUYKsEieL3mSEQLI<_J?6k)sr6JeUtfW=(u!tuU zn;13XmRFKdPf%a>8<%z=|DQLstnJRDNoxqqPH_52#gNcM#>5xB zYPj96RCvq9n^)aqMSngm#MSfN?|USwOxh}$mnDY-j_9N%!oy2KYpgTOup+4j z-K3wd@xC!d002gch(nV55cV5BN-1%u1~v|{a2GR@bK6paqo*k3nLpPok#Ct|PQh^w z1?ltW$EuQWTg91>!OP#siKM}!(*@C%&p7(bG_eS5B4sbn8i^`Y0+M(=-(t1!x66b( zUnn5UIj)V0dBb3|X0@c?ke|LBlv-AOnr754V9xnHFWO8{*cC@AySs?NR9?3=#jawv|hM^i;;FyU>Js3&?XM8;X4?Fl64+h6a60b_N?mfcbtW z80(3>Of?*nOjQwrOLY;Yp){-z{-Pu5VhAfA^pj7L<$I*oU}EoMbI5WIZ_|$)W?TW2 zARd|L5`}YgW}PB9(A#p_Qy$Z!)LE3LN#JO27%RWMgCb#g38Gc0dSpPVku-wwm#X_ z%U#qAm@+{0fi)6vi_HzFF>OfCDX3Fp_z%ogg3w74rvGHQCoMt?x@F%Le}8vEF)iH{ zXuwk1#kqNEwA~q^n3@s3JgJgn|H(l7t{NMO^y^5x+yT3h-=LbVIr}=r(y)f6d&UYn z9BR{fLHZZhYCP4W}FVU3>>~ZMWmff_S=Jk9a>wVLsARz+>^t{(lreF=H|?EL8s?GlMg8#7#-H?eaW8FT znU4Q&O=W1Tk%m@LU6zlAD2eTl*KfO*TjhZPd?oy>Ab2vh<%bMGpSIA5{RW4l2OT)NK~cU)hMxgHc4C~#(b9UZq? zC+Rz7Py(Fb(`{N9@O~0{;f_X|-DEgdkJB+HKV)M&z@)}TnhjshhQb5V2#AneUT;&4 z-Zpo~5y&&i@!UpN9bb<{T2l$Xq3kP}oC1(j-Z?jl*XGcI#6zM?;Ai=0h!gbUhr-qxtS02c^o8vGN74Y$#SuY@%OKc z-Bhs%`y#K>spzjew5O4njcR?a9@wzR+@aTB-{P|jsC1rEW^QZ(U)l#$tJK@?dejXz zZE8)`Yv(^Kj4o{_kWJ=p>x&ChXnZka4r$wQN6f@9B0P*F{O}c-(*6{?(fv_!1~fd*!CRS!`^l zI=<>F9ZHM}Rq*^y3{j{K80}s(XXDJYS-yiP#%ldAd=az<_YXD2Scv|Li&A=2PyIC! zQt-tsJ)uaU*j}}!|8(16d%L^@LQ$@R;5n&tsH70jNtT{(asqw`_qzeP-K4xKTA68A zF6hv6+h;7~D5`^~5pji!7Rpd{ ziKm2Y{rujcGr+yR7jCvhdln>j@!;}s9P-S`IuZ4Io_h{Tuy`T-d|}Q&e_bTG6rx=3 zHSlot)~|N+X@agyI5g>^)-;2Zf!qqI1h=zGeyc8tw&Om5G7ok^p&nUIO$`-ceZmY# z+UZoth{Uj|WXWt3+hmioVp*HTGId;vO7YXLY&}AUy~xfR2uzd)Up_ z9$~%cs?AKS^p3LViu$Get#5Cqp8x0wXyu^4JNUpsx?CUE3~`ih(t0vKcYw`&_wn$)ho0~YRF|q-v23UoVXxl zDFu}+Dp^UD>q1E`TPDrvq6pCeS%1;kXSHZ6Zm7;nJ1C5qxpR*76K~8$mhveJl z*^C2Jils%O#|g5+ng88boYN1g3zdejI^N7y|{ZRM+2b}5edMMDV&WgFGx70z?7#kyK&ocIyI0sbXBm34Z z9WYLl-hAnaxHA}UCVo!^@>Gq>5XazLdR?y9DE(v*LZ+nQTEa73(tlIT&RH7X<(@xU zo3vC(KX;8Ad6xXg^eU3ql`t{7OXZL(WF&4#g|E|)joxvKd!zVEK+?Q)L};z4CDry7 z`-=vwh1AXCUy`n%Efg#ItKOa z-|{3B#gwy{dXL;+{}Z%8d4_`j6EEIIpz#(;hM6&%dq$J)G_~*_t_kU##e*GGhbec< zzuu@{wzK6zGF`!d*#2nAvRTV@-y&b27A4OtA%@!!d8B>zG5{ z!?FWD!al}*d4P+19~}PPQcYIq6l4$FAE3l`QCnLnR2$zpLA*ZAIf_Uu9*Ad2%DBbj zLMJWK1$r+CT?2^}63rH@-EVQm4@|DNfsmWZsManevnSz&FAJ48!j9bHStjf*f-OH? zq617^MSSIN{K6D(Y`Fs0zV~bNf;2&fo;tKWjS@6OyMcHbZ*{6>nWn{hR_=^V{C-$iKf35?AU zndxrqy#uK=PvjOS{6P}Gh@<<41_-H*4*COksueE=4{3{yFI1kc*$8B7h(3WW0~Mbx z2_iKL(hU09aelXt)V$=}9H{UKp0Lk&PoI!Abbjq0w2$}>W3(T+^WPG5XmNaHvf}}_ zMH*!{cy~ot*Qi(S(k&JAaBa|^u;`m1gh-lms%QAE&LN@Yp;i67lbhKW4*E%$T4m(u5O^I!|?rtH##x)?MzBY1J+~q+8ld2(Tsu^}FiOp9c?T(e5nh z&}kbnX;|?mmKA0BtqmG+3{Wa7p{vLR(GIAD7b5D$uaJU0sTigq>vF?u2* zbXBx6_zivkqc z&C->&7T?ppxr_~2!w{j^a&uc3(LVYU9)B~`wzaZf`NjB1ku1#U3W^c`J#4KfNBXP( ziP?K6v0kmw{wE0_4b$zyFW~I4_8fn{!>T0`lB9~0#8#sHygpEVyJ+(2awALI@&%$P6~2&>Erh~S%wD9YhGj|#IVnwEgt zy>x}0jJt+(Yxf+s2_bx@)Pi3{6w zk5_DcZd>}SAP##{9@o~)4^S$DrY;8G;7sPX9#ynW^J077L|M8)5l>b-sw>CR*i)S4 z5k~wp`~jTTXW7_D`v+O)PunZ)Up`ma%f?0=W3#<57?UU8U|VWYrBV3z-)_q#U2T=O zpnuzMfpBnnSN6OmIj%i`3=)L2HYIcbmXuAt5#L!=zZ~p-MNkwGwU`S0eso2v`${;# z9k-?ghdT1UEb3?1BwvrM2Y-vKJekJIlh0bm9cdSlmZOn41WQ(X7y&rik@207^T==} z?`nsDMh|Wr$nDXdnBYKA5gW^-SD_6v%eVu_k%thQ6{=DErAeY^c7Qa8^XzNEGNJk* z^?M-P-BV~JK0ek&S4%eYN-n>L6IN}5a)+hHNLXR-^V1_jDwikwPn=^PQL~$K;RSgX zpo|QJ6EO7M)d*A1#@$Qy`Z>0I-3CR4(GZn)LpT!pKjwIj35J0!Fg)ipVkPRT%P}RDS4w$T%+?TV5wwlP4L6k(?E~nchtiOaF&J zE99ZG?j4JDgUng+KW_`6<4Y1e!fWcJ3XkW(rT_dT72=X`{3R8>ev5X@oGR(y4t?n< z!tEa%2#L65uN~SXA7f$c2s&+-@xLb$m=rmv$ec=Dq+ff)$-Y=G&q5zI+9ac9OwxYK z=;JbbJ4JZ_CY6`xyOmYs)OBX0N!vZ5_TSX|db|A`=4+kfx*_w9!`i6~F-L7@|^(~?G@%@EljFgwFyjD#< z4K&XlU#q5REVfnI&1cjc19N~~4h*)&9EReF1;@CNd*&VdC>p)K{77O=pR~F=hicy% zCRd0lo8!;-=^hu*lOU+jAWkbt=QMc_uT{V|@_0!4Yg&wRM@C)}4RBa1|53=2+dcUw zaWMb)*Wf^BJz2B;yUGn#&?-qYyFxT3A~x#$s2RMLBhjhOIE>LGw_Ilev7>1U4_!L( zs0c4_R3G&qGT0@{gSqXL_!6cJ{<8w?R7pVq+yX*>C7+E=)YAv+qrArsDyRC@f~ z_8125c9_4E^I4tNhM3kqGj<2CH#!y{I4spgI&>E`h(T4D-&a4h@2r_wBx6*h2#k!J+Twwc+J~ern^!XshdEczaP{WMf2| zi;Idfw6?m}80Dg3K8~iy!)MBW`B2?py1&p9Z5ma46~V8$4TO0qc2w*?4rRGAp8hKB zLL!4G+DmlD*dC99dL^`#09SQ5i!FC zf75!A@qf9gf?X7+H|5hB{ll=Xjn^D7QdB$3uc0?gwKC5CGS$met6otLZf0hB;NA(- zd+(O-yI~=e#hC~Ak@(!K&1pxBl@$3?hRQlwsz#R&YFE!k7aJKGlgjvO*B#$}m?@JZ zyX>bVDsYyaWRJE!8M`TiFQ*Y7HO3qo9|CMso%3N&%M`=KRAM2>4-~(-q!_BrAVm#L zNTS2zI)1@`jA;n4=CKQK@Cobpd%7Q55v@|B+e%}XrjZju;>(d)hh|ajf&O$GrhG$Zk^vdc>hBxQkUz7oQ(qL4; zy3`D}rKS7qJ{4kY8`2rM2k_l$HA{gF~cMms3WF)RS3n>sLbei0&*WoWW5#z*VC>VIIX zc_(g>YkuIK-ToJSk6^z0*Mvo`f_H+JX?zIHS{{T2w_g#nXXM zunAIhlog@FNUi2*1IF!dw=o9ZxDXti_2Bgj9PEA>`pvQi5p#TNDs!(R+)u{QVkmu0E1xX zCERt~3|)`wLo#Z*hT-sf{&^+3Q-RY0YLIZ1OPu>`7CR8it~7n>H7dSe=_&tYUcADS zXEYd)t#qL9Pm1xw*dAZHJ>h23PU@yPM6Ze5pyfl9lXNji@3;u3ZJ7HD9X#5%4yIIJ zK@PBeaR>LBJZanY!rPS%04SsGiNcwL@%*~lvFwA`O5HL)bMewWmK8?KXA9%fhAIa; zKXe&{67rIYKJs-O%lD<*!JA;5Sej+t%dfT7;9c@edC7oZc()l6VRX8!;9OkAv?9awdwNzS_~atM)T#esD~Z)zEL>eT`=#+p3N%z2Ws(^_SEt0H&2yLQt8E5Ftt=N zXAco{oIY1R5{f-im^v@9v`&`rf-=NntupIb1x9c|Er>Qcp7n#C1{e-iw>!_4wy^q%dCQZ}|WyEXs(@fR&^kLnV< ze0Dyk>+REw1<0m*xP0wBVxkKc9dse?`=KR;3n`{Jnhz4K1_B^H+|k+J_eXz%nJ!Z3 zSufsHiEgo)YrinK_l~5b1&pX5bzfRc2w5+6nNK-2e=t(tFnpg1M4l0k zYuL0FO46%93ocAFsWCsSZx^x0-6gbrfdA}*v4rtr;tJ7VLI|*e7(T^!dyJY+S-S>H zSI=2$YslR!Mtr`)rlAdg!CJ|s51Z=M-7J|}0=NjatN-3XKhKKDaWLH_+BJ&%{{Bqn z$uHNmVEMgxUbl59%$C7{#9pEf#8u*-3h&3o(wU0i5X;1s6v)fXslZ8;R-w63%}FOz*dFek=mT zKN`z{wE)XN9W74-i|L*MZ2T5MBjvK7vVNF2I?8<#BiTB^J>3YU<9W<5xAf^>^ezA`z*SFuXw8VU#jonE!AAKE+tW z-=`8j%<$jNf7-f?Uby0P&kNIN95%!O%2SYHOimu*`D}=>oXqa}s_zBB7B2kC~o>7;9AN z@G4Ub6?ahkZ`G09WxEgA1p=gp?rcY>=a!^(s8g5#34@9(`R-nxKL_Tc5&f|1#S0HL zP*6}vc7G1iIf1KlHRyBLoV_w~MN5wOa`>}t+;i73&|(l&4)aBR9X=2o zoW*nXsJPuoYHKg(Ivuvh!5y3T`MVeo+0q9K zU0nZxY0!vkpCuo4B8?@4=4FSKUxgK4UKBa{v*Y_C{@fe1{^fYK(4&e;M=tVMD$Ekz zZ#qqUGuxQS}%6~DC8%m*s7eH<{RLjt=QBROFNq9yvc(iPdOsX7lPiRwx4|pRUSC42$yI2|GFQHm>QnuQ za)yb-8#wM9s(_H9Ovs2IRFvl2mOW5;trSxicc0FMe}Lvq(|Bm3*U4tF9)6Q;SHzjI zj?Jsas@XIP^2yg{moXg`r4QGmk)X4N>GF=6vHY?BNVSc3(9QRjC7!~OX-K~lSbe_wAu?e8MW`Wa?^0GV~rtVibwKuTJU5K?@ zDwKbws6i&Ll3;3OSoa7Td(el6W^jD zAKclMZ!&3U1n@Ip1Zpz5Flcu{DoLM;KdFFpQ}VPPU?_ zU+$u}{s+XR`YwJAzBD%}Q<`3<*(S$3;AcIl8!7t8BMga-OsaX4UzFcPba1((LltC+ zI+Q$#tQ|k=F%1qCGZw>CXS2AS-R#9y(Q~bQHALcWIl6yVXC%&9c%o)PuYU|QEUP4AB%bS z0U-*dZi4?}OtD4ly85EIGay@fgv!OpMtZ{DoYtaG4YD;)@v@Nujat*8J~+5E!cux6(Qp32nUTlg5Iim>4^PE_C1s+9;dK;2Q(8kiKfj^9 zy&&irwLEgne))SNbD-p4w_PN4X76eI@;t2aUqD9=cKsg5-K;m4&F|@k?s*l&f|oUk zfaf`p?{@X#e%)sJ~ajwcirDcSumXSlIKbe6oTz^#FjJd}KU;aqKCRvZstO`#m@o;6PUuS|r<3 z6OdgJrr+mzqIt~SS&IgJFjHq#OPAzIKb$xVFzb%MIB(VgJ#&=WG9AFaP??a{?n*DJ>0Nfh+_c<$e43>W``1C!6>Ik3!F#rC z*{_!D8`PWaz)Hh1u!iaY!?9?C6>PTcjHT=Ir->tq+*j(_YK5!=&Otp4hclMemoBbj zKlwZc+@?3`j=VQ6cb?Q*p$w7FHfC|2&|UZc&h0I3slTn~TNS-BLBI`}Ek0_4NU8Vl z(AjFVuVYw=Di%cm>IpG0H&m?TL&&!pkh199?03MDFwfvODMjF9`x>FMTb#OIey(~F@Ck&Vqx47s63;TCHIT)nLuCqy znqN1vj$_Xap=<}t9rL)jSwP*D5*8%3C!Q09w8V1D$ zT0nBnOgHEF^?Af=x3CwMgGI?Nq)=2y+4-D3I7m5iS)s>UTpYq$bhCqpsNo}D3_3bEOnSa0e*~y|tnmAbE z+PyLUGM)Cg|5D)*I(;4sc}%fEP}1i_?eL5ne0s|phh{S|?$O&WM#x@(-`n!DU4W`j zT@N+{TAPy4c&%GxJCn9+w$cf)J$R6s+d6~aCYth}pEoSUB-`G}GiGb9Ft0(7t&6c% z@Qmshs1|;I^xG&~cbqw5j?TfU8O7cEUg-GwsuMzag{#KGDyqBnq)2(>zgWlG0VSX~ zK6CdTqqQvr%JbjUekotE`Z?o-WJNX^oc{R=k&P;5NRifp211|)29;3!?2?*gnS}-h zhKp^@yWz4zvm9Nytl3Gi2HD+kA<`D$2hg(Zs3UQ5J0Wk6`TwF=h$0QZTfmlcSlxW} zQ0<}icauN42bJTYl_OkeyiS>(e2J>f=Brl#-~@~6y;}|+_!Yen_BOgzOiPT8311mz zdA41@Zg%!+()g`QlKWy^$b|Yio6?S_spsG245AbgWZ4@+Z_SH z*`VLjVciyHu3vA}*pq7nya@=OI5o#$pFY+voAYI|C*eTxv`ZaC?Q^x-1KoOWIeECg zZ$MUtLf-2XVK_-6Zy1?TGvAXGZxQRyy+3ThCzFU)$m4g=pyx5&GK+4{P_ZVpR^qyY z?FZ5<3X4Ol*aV!>KS+sI-Tb**U1!5+od(oB-^7{m%%le%eDoXwV(P+`TI8xg zNO(hl5Hf~0Tv(Bc^JCOqHyqS(iV`))DTSV9D_C+WLslqcFYH10O@W9=b+H08r^pz1 zt&!0s=cfSDy{{5a!sc4UlY)26S4u_|8>W7ap`6q?F65vo98t8oxgIifRXT2x<}2>> za>FuT->3ZIXfQ-*+xO zTQhdA4$N_Y z%Tvujs50tsv+f;th?XNGsXI>C`C${QIHYRe0jQNQT9`P;1(OF2D*msFslM!t0{^t( zOBT`#&);Qf0ndo{irHIm2ID=m1`1C@>u7x!du|>wbbl&vinNDh9Su4oG}rSxT*E8X zS7wii?Uvz9w|ndiScA8!e;t-y3`dbGjyihY<$4PqK7t^9PxG@4*IpQ+q(>%h_S-XioR3aQ0fzyl zq$+O_rp3XPth#oU??QB8l|$b!uF8S0KxLf-SF+-dXs7>g|N! z2o1J){<6o}bi325G&rF@&r@`Kv27u>s7)EN9Y7S%JM&iu7fcN$>hRZ5GyLtuk1Se- z7<-Bo9(DwXxzZi0cfylGZN$}dhG--?QNDRD#^Q*x-OEg6xF^qrOm*g1CiE5c+$Ooa zWgEH;Cm$AY6!fK)!4*nbYR|7XWZ|?wr3eHB&x3a*BcDciuA={M~`2!u5Jc<;~*bZ2Hh*(+kS7YXLK(9o}udS z3m5yHuwhs$qC)D*xbo{!Hg50%8w`cfI&gK}Td)1STjrIOSP5ss19t^r6WN@iN0)Sg z>@Be0$x-a0$$BfPAczP7?;Wb}ZPl|wF|pJ8KySBu@A3$IIsR9M=Zkur3c-kH$e!vh z$~s?1GfbR+gS(efGj*GU za0dapztg(%`n^C>F+rs8gfZ;?O~7rkAS0!-x^s-Uy2_mV*gHC0k^XX}JbN)o{&s+k z&6uL|6fJ@c1$F|~NlQ`WkU5D){Fi-(8IaH;`n5WGw>Vl77+^z3s6i=GzxO5IYUD7< z&mwiCD71>aqz*KhpB8+kJeqvNlCg4v9aHgR9INhUen-FJ+i1n`>n@8!mgg8SA39kV z{Nr9TOitqYmSGzpFok~}Gm0Q+47EU|bA3Z2pvw68vD9Awi>-vK_;0BdL`3HG0Y4eSCG() zaGGYgtwM~G^IFX8h}#DKOkdR;@#2VnT5iG|Hs4pP&Z_O|nN{qkr^^Z-A?4qfj4P+r zD#S6Z)ok>+T&<_liPcuRdM%poutqRcE$H(cfzCHp+}en#>Mw9SM99;#Qzx+> zd&~+nro;%+-+qcvVH!OJOL`vWDBd&u-Gxybf;RCpP@=Ks^^^QUH~oAvxjHX9$?=8V zJIJij`CIGPaiT&dH4s~A5G$UT<5mjNBGY3c^~ipfZ2OU0Qdsz--@**$$KOGP#(8eK+d7+APUa8{_G0_%>?)6hy0&NR!;OYYbgl| zmp%O6(0$^fMT|v?6=W=bo3@a?@~j3)I;05Z-_3ug9wPIX8{psJ@68R;A4p-!%5utR zx@aYlKsKfe#k^8NAe|{5!Zs;(R~h?t9Hxt>-U0?dUH|^;^a97u1uq_Dp3M3D4>=@b z#f4sN-Q!KTFTs^wPYyOcW~vJAx9-jS6T5KlJyNU^kn!=bwacNwmr;VCYr5hKe1>zb zy*!Rw`hYTo-t5k)8%2J9-6S%_@`Ny9jDzBdV0VoS_!1`lGDm;jkG{G`3rN6cV7R0CM^4AwIXku_Nm2&t!oI{qaoh$)@!B#Yrx>$@N`W_I;Z9F38|nrC0Q zr&~T|=XW&ib~&B{@>l{-r8R=RGNnFrtB5doGvpP`q9v=DlgK4*5CL@2a3d>ck1W|1 zwbRL8tp0btm5%2cwkszg_BrzX!%<}Q_N~QZ1*-z+uW58Jt`TuA6PaVGNi6D7)f|o}`asZcmAnQUxmY2;(IQ!>RH)-~ zcQ9opGEDmtFuHWHYE)8m$qJqqJ;$pxd0v1lJTCB(mR#KBj2P1RrDY5= z-!=J&QI9^D=#6??{xfnvwY*8CJR483oy)|NL-crN@aYP3U<)Mbe|wpOM#=eSKSJvJ z39~%tiDx+&{Ty%U6}n1oz(B3YVt;z69ZS$OsfFdKLA|yY$!w})ws(+KrMjeK1q9)} z2=)G&Qs1U90uf;ciC$)c6#u}XND9a^Bn(Cj9!98MgxK~PRj`BJqbh6+tdcNat5aP5+y4cGAISlvy*u6Yls4_yemX>s1+1X?Dp+gSID-s z`~t$6%aC!<$qUb!4cnZ^DmbT3jLn9OjigAH?`ComS?L%px%iOfA| zJ7gQA8CpECeBL_sj=#3O+p=-Fwg8#`l3M@t)Ah}*a22^Q(N>C$O|J(Uoj8sHM^AO4 zEe^IotCSuuq^kryhq2eK`;;?}(S^&*3-y_LHNcG7w3T|-IwpW&xxJeJ&(gm3 z?Q1_Z@Or@(7hHH;7Sxq~i3}NxU*GIFaAhc^)FpX0lww$scOkXvfRG= zcu5#VnVp{$eB!ErdlpR*;h}gN4n4XXuw*cylNM{n?bPaj0_8%I5{LP}MH_(^Zvn^` ze4AMAGq|L)?c1jXxj*~C64T%a&Q?0A2(#fbx3-#4@^d`Rz{W=>16vVJc75(H9nLd4 zR!=eK6J?y!teUl((XwXMjgHY{Bd87gU91lBI+Q%NFui=1^&c`}C`n@pV>;gbRd?IN z(y~kn<9y8)4yd=B#M5?lDA(phU>%fg$^+e3katHBb8FOBTX=85-qMWhL09H*98_k$ zp_M@fTg34<#TnM!;{e&{H7mr4pcne`?#9LJ{l%bNQkeRvko!&O%SbO&5uTdA@&_7*UhLty$qJxnKMU_78z9Utp zS2JWzFL8K}ZS7qwdM+5vbNyP?C|0D|QqTDX9tS}1&lkqw^oWkn>I-~lCbrCML(U~Q zM!tG@750LK8IH zMecQj0Kw=RAmKCAFkBIq>JvgCwsl=yn?y0*rdr+m*<3CdbC7&~jmFTNhuGeyjnAXT z-$4}fqWnYlG%&L)O-r*6TpDiq$xroe;WNhxcO1n9V+co^ahIq2heY4m}^J!)J`a9@q|Id z=o=EU@dN+|DW2P}k?dLC{6!QJ=xp8%X41_));ee_3S$s$KuqAdK-+P~T8k8GyP#_& zza+&rMm8~HNl~!sT8}l3JcY+VAoq?-QM?O6;xql;Jw3!iuWWbMOq5L{$O9wuST+}WYBYyZKmZE>dzb@#-BqQAJ`YUqSnjh%am@jkt9JLH(GIH z*8IrQre$}_1`Z2z;;#2)uqAlgB_n;;C{A?YXJ0aMc#U~0ZpX=@4ta%#DX#{qs^-Z* zT*3WOKz~*25XzpKOqb{n=@)j7_=bz%%ugAnwnPfEKiV8u>++V5Phozz0i0Cvw}$ zN6&rlFb>Lw&eN27$C>+K;HOCQ(+Wy05&9E)?#EwIc%8#g?)Fl`ScihE&s0j*A90E4 z53Q{Vg-1SAOGMQOb0Dim&8itYJ3GPQOB@HHQW1~nQc!QnE1JbS|FAhv--|&wh-5zW z>tn1}Yr-lnCO6KUxok^+5%+Zt{@kKtawc-6!ufc%>dmY8GlG+C;{1uX|2C6I<+nv1 z&i(D{=8UYKQlmU|?k4TQ>PngQ7GYwZ;YE&ocjg?9i60;Qrm(=^8Oc?r=)}h231jJ{ zBOX0k?tRf_K5LYEy$wG2q>{Gjz?%xa07dRXEgC!3+9M|}#%9sZdWn4CfbFSuMMFM@ z;Ow^fmotP1EP>aK@v9^G`)5KDAOzqi!6;v2xyl(w!-Dp~e!8=&X1P2}#q=_Zc*!0M zsWsYTs8U|j3pB@zu!aLZ!2Olrm#9$O=dh?UbqpoM@>k}&d&{Kr_lT)w_Dr$Q+E4p} zuhZke0HU&OwvQ>~6|6*OQKqgPu zqrlz%u2l2;?0hD9r~(DS-skf zO*TbhE}P+LqI1avvejv3+^MEb%bPV0DS2g4uqOFT4c|wRwd#|i@C{tu>oLKX%O`CQ zSIvL((tpaaZPXvo@cbd>vXh_6or15y=7rhX+9MZm>BC@&nj#G`Ov=cy$GV6>nMIYm zNPCc~DB<)|eT$gu6LqFLPcBfkNS5(3(%MWE!oi`~t&z}q#>n0Y+uk&^?m*}2j>9kf z0#gubQrNl2x$8t{AUf+DgavUqBQtk4|FTRTDdbxQyKf%~QFf%SU?r<2tEz1>;h)|> z!3}{7muOtQ@`*vE$gZE)SzHI@D%?FoTZ0xOG9{4Cw#r|2;1>IFPSGq=-NC9$sC=$q z-?SyWYhw>mW&lR1)Us=mQ|5Acd=~3-m7z@|yNYmg`J;GstU0ylz7BfR)fCNP+>?4A@ti+5nHsL7Lwl4ILHTdio7h z@@L-{`Z+%5v6e!F!Xc8$#Po{Q)dH>Z%?|?CO?HKzjyAl?_E}@Y`5}tKOlIPRq5U>! zRdQ0aHuYg`J5x;a+4FVdy&?L!o}Q=v$)AC!b|X!yC#z&Tz~x)V`nF%5u^m zCXOw|oOpH5sM6A#RX`j3((%E0c-U4xI?Gpfd!>s^U;8lj(i{lLB-=NR60^3HM5fng zh`Bzxv_f4hm@xA68`%G)=n7E||66we9m-9%nKZ3B?EQz{pueHf*&4-|Lr_+Wlxhtr z_QWK*Y_Qk=$-Un6?$>(3R=dd%G|R!FF2dgYVu&-`(>l*TR-RdwIcAx({9KJ~X%93pRUC+aCZ(?7|3hx` zV0}@8MMinbo}`?o=0ObyH5_C4Wg&zZPDDJ3Dg(#Z-|OIUniG&aP%?=m zt~(%xl%t~Uk=-FyP&4jq+9_LD!?q=qQX_&YBMDa=qG`6>wwbDJE2r3VD-+IAW5!}K z%ZVje(@n<)UzOBZZsLU6>=~>i`KyI)&#B=@RDC(ODP-D~9=jmU^ze-)<=%KXsV3Fz zMu7>_Qbi}9F}HDT2#9pf2nA}=XQ-Zv-P3L5uDa02q)|%UIz9=GmhT+C)=nttK(nK; zmCXPP6oOa&e1dgms)hV8v{57X0_z6_9lEWTxWd}Sr{g*LCUeu%#i!di!FBPOr7t2- zYsiLD%qu8P2qpakH~janhqOhltD8hhZVp*q#Nub4J!dloynt0P3zTwRnaZ7I9Zf<) zfp5o;&F|oqs|Qn0AHpnje>70%c@Z>V?lG-bdfE z6~ER{d9(+4PqF|xBS-QX(01&Hb|7vzdrBr>T&I@XGRaRdYl^XOn-jv6d*=F5;EP*r^?FhN+B& zUK?K2RmJm(cq|`Y{BQX_-Dn$jU9=&}xC}~`KN(xEv&*AUX{LGC*T}TB;IvF$cBXGBZWDL``^jMeBt=x{l%r5Pvd&n?OG>`?TQ!bFb}fU3&;{q_`Nu7+ z5=&Ak*!hFi;_LtTxCX5ee|`3W=-4t}-TX9?prm{f`MzH0{Byb;GvCs^yc5W@qCB#k zk$~Z5N+|j|Mgq;>1-2qc)6g&3wCgYMP#hc?QX2g3k%fDT?3s`d!(P6}aNAH+lB4}( zu4(ZvxDK77qt0Zib>rFpe8&V@vZ`MJWV#6rb|greqR$0#048_mq6$ zi@GMv~zSqbGz4rZ^%&_ALNlM1a5SYZPVI0z5WEpMGnZAFtt&H-Fi9rAK${i zi!Q>@LzUACVId$G?iPW-v$}hHU$5# zh}dR>`=%Q4a+}DUyP%uD3!wD5qj&zS8j<@DumlJ`Jv*&)PW_&EA#bv((dSKK)3XD)h;TQm`5W;GBrDEtfKK#mSGN(py-*I(O)WSXc3xh6M^_o5TK$@? zk@uAghYjSjV47%k-C`)n4KPZ*gFwPFdL=*mVv{>_6|WnyT%KH;_YUKrp7dx*3v!Qe=nkL19G!i3v&68+|ht%0J5x@aQ!TOrV4d{e?aOW&dn8 zaNY~)CNpfc!umdr**yWWhS!Q2g0`*7XIF&Dbf0Be&*?P#Q=wuK6j?AJe6!Vm{-=TP*hKTc|KJe{`>cjp-ood3V{DMt-?!Y$g-r{? z*GNSvAh5t2iy57~36$GJ3zF}lX1rtqpS0MXw+6P@W=F2Dm^PlEz|nSHVPWthQm7O* zJu%<#_L|ekn}2CZq73-x1;C+H20*85I8y%eSddx(;UI!1nKES*AS)5Kx7!Mt=4;i4VTCw+1FA`ZvofP1 z2cX1C2aJ!#JUg%Rp4F#U|6WdT^_Z~qgm8dCS`TK`7cNmr{YSFFCgX%Ei4jr7`{9#SS+2UM=(*dTeM zn-m3mfIl(Lbm1nI44VFsG5Q2y;6P=*IvRw1RN6$Ulu^KzAM50Z3?I`UOMGDYW(z_W zxEdCcBxC>(`i7P$3<#R8M4q#$T?Lb>jW}CW!8+6)!59HTxN;IB6i=5tkzxFoOxO7 zshjSZZ0!@TfBf?bzW({|f?yzeX3o!TvUG>~gZS*)(@$Og90c)#48znxV{h>l0K}3- zTW9Kl8FoBluRfNeMY0rgiYUm<@z;Y{RQo8C>S60>7-Q27LMVMzgE)Ub!&M>-4AdOH z7E=hH5}-{zRt8iX$Az4O#4 z{$zRcx;+JCz6ngx1WWWKur|w7(pxzuUXPtLEU>6-SjaJ0!s-3Bk$1oIEO|fTN#9Si zapKQl2T+EvzTJKF9_nUtwewoOeWvJsm9ALt7*s=i^^jQgl=v_S_dnyBlA1pH79_Dx z7yg@YLDQi*LR~d)u}jc?57HcaT%jDY3Ff&Nb_w0{@IB}~KDgt>`sKC`sj&Gyjb8^T z;|==;h+Pd=kiod##p{elc_U-u;ke`d}JEk zYr&V0UgCsVT`TH%gUeoy%e$j>uBMy@MgUJdGfEY&_403sewV``kI2Go4z z;8z+h>oDC~guO2W@ZN8#Svkcq(bkI^u6$TFS_kXod-q!hQI~MGkx7K{uXeNFWBYg5 z4(3n3m^VL;PA8L{{A;oiZy{f(E*na0tkzn-F6J0}5)C|q+Y=YA>H4W13PkHAMQnNJXNmoo6*@@ph6 zHG^-(0KB%e8o@JW;WORsT;K^mle7n}mAeBU|9R0soBksnKoi9qqM1$asVO*-k*q;X z0Ss4O`A7HS1EPEf_D)kmNgzDC3EI zHd|I=ICM&C1k5`(IAh2gS#-~eZMqz1H>qkKBlhT!e6nzxDZNEQ7u!)693(77y}==^ z`?8(e#GgfJohpiUNP z1v4ijmGHQJoq-f5KH}t10Kkt_X|y54f=(ZC|F`aM9AhHYp*j3S6{>u88FQt}w9ieI z-d)>+uL+meWX0;yi`g`YUoLbO^z#JoPjfOj3QbYCEjqk^nH8596w9{t6`g#!# z+otH@cUDqUg@~IuX1s{%r zUys*trTW>YD!cWc_RTwLRSWJceq~&wTUUPkr!4sEw-)BXs=Ov|zP0XY?aJv8JM7Ls zeRo`8SqQaYWsqfIWd;U7`y!4H5GaiAP?0QoJ?9GO1Fy_Cy+6?D8MM;9b-EkS*-7wn zLx+M*WCYP(Doa`+<9^ALkD3IXh|t5&>bDo5?M=mr_i9p20BA1*HybmvZ)zWwC0I0m&N3Vp7HW7^(QW$9xBB_Aj0Ir1+T_FYcaOXC&pU zMl`>c@2GcL#jZD`ePXJa)%6k2*tk7|MQI@)xaV-{EC5=X0{RfqC(`>I^S5v#DhRSu(f=_^g2l zC!3Cg?bzu1MeAWUApP(*nC)KMLWc$Xv`-U;h4H+IU8#5Ls+wot!T_mxBe%mNy#2&+ znWDVq3{{K?d>TeJ9?9tdUEu_4PzkD7Z)autjK8oXC(R_t{|urfE!t8YBPgtl+=E>@ zbc_Fr**D2fSz8zuD*^1_SZ>kWB@#OQThplP?~8m*k{7i0mF7R4BM~%&kE@zHdsKI4rvTkowKMpEh=!*M?ak#pIjgm^)EMc^K5O!}q zUtL{boOVo~zo*2PgxMchvVZd$6DyYhj@LF;hhoQo!T(nzh48EuZ;)PJFv1Y<{*|X8 zh-0KxB&H9~E$z7jdY>aYAdkj7X!by7@;m?@y0mxy`tZe#f%m{5kuaSnXJ;THb;x!E zWjyw7Gj;e6OeCpdIBG(q!6B>JTCNLw;;4ETG0s(ehgSlq>;O1$8J6KMFgo|lJSa>3ud-rw}>hw&*1}o9Vx>CON$tUs7 zFijDEK}0rBJkQGa#!{p$80Vph)qD8|kSj@v;&Jb%Avoh6c`l!6?-Mvo*8PpOz(pnrKYZzBi0Bo#enk*$4@IofLU{iC8d0IN{eguIy-djO$DM+ug5)t}7$1LxDG~k6 z-Tvb^`wzzjJ%({Xqhc7v!_lgyeyQpJ1E2WwKtFjh2Y!qeo{dB8_})DsO8LZdCP|_Y z?3#tHvi>q$NUcgc&_SU+=ajzkb&gV=WpJS`s6 ze5HK=N?^ld@AdLk zCLp|1k4+dW+V3Ec4Ll9^&$7dur$Zy3la!X!usj~~19#50=gK?nza@l!f6(;KNxhJf znIbz;F55Qsz+5Q_==?9IpyNiw*(xpO4p(inJOa5C2WEB!UW)SvhVLnZXl5^FM;l(gObBPR{ZqI`{0 zOM?8XBxz@jZ&1m1TlwlpHy6bl&e!$t-X+xxll?054)ByX`&6_Eu{5u5cLUxhbSo<5 z$U26OpGmOIC1IL;OP@EF3*wf{mIpsOKl65Z78&|YJFNaG6JK+NR=j*a@eS>lV|Y^stlV zy6$+s<@S!-jGz`?QYL^Mgm^vZsFza;G?T7iM8@(hTGk1YlglMrU-H0Ysoxg9 ztUekO-*aUCp7;g)o97)>+Qa(3P`ku0pYkIm{0{ZUN;Z}Y>v#%vxMn=M)%itDZT<&1 zE41lR5d84t67l7Q(CA#8fm0U*wrcH~Y$DinK|l zFN9GwW{(`?WUJbqN%i9huH@b{gfn7DPFn{#|4cKKrHMI%>W&pFKT34%Ubu!&D zOFyB^9zYEwf&XaAl8aadk#gvlvOqmG(js3|laex+vM96V!=zwCf5RvO+_;IUL%eJ| zFGPBV8f%jK%R zwCp3Q738^yV-#X5fP8@WKH6biBJh)E0?N6EQQgbTZ&VNw|vYA?gG9&5laaLe11t zihPU6OTu8%0`*uilKc(Tr!}o{LExRWgO4*S2NK(ncZAH)eAd>pzt4C==4^^n8^% ziSs6_9+9doo0l%>^;hy!&e?h8`me{_PfNc}@g!SPpjgwLO*XoQ<=VwMb@V!T|Nlsn z6WKSA%@%Z-(4p12G)NvO1GE7P3|>k)0+`-hA&>khRbvb=TQPO!&SliR{P*Sz=aa?` zgCeU;F_xWc>^KkV10Wv(#7w*4AqF<0v)wu!At@;_9;3Et>V2A?@z-$d(#4CVp?f8V zkR>#6!8$0pw^&x@Q^#Y$q@IcXe$iI!q{k1=EqCfC-Tw;Se;3nQSZfUca!Wrm?CHAN zt()*pMR*A)5L=ZAo@;t!JB2k$v_1l^WxmF{&F3)A#jaO}))+<2pb|mKGPUkNj=tw{ zHDhKj>N#i+i7_qVSkc3L#DvaZ8K3@GGW28}e{#mW$Y0$Yd3FVb3QGF(+sS?z9CD*d zKlvPK3kk%|%`in-kQv)yl7DGxOOn54DRP4OV$o=^`^QHvm=CDqkR&KKcc09(?FVSw z<_kSMO1Y0#w=`Qz@7i9<7udCGJ@!qEX{h@OuE`!J4U6GE8F@5Q2`nCSnwRatXKuhJ zYzhPcrz1U)uzkbP~w@6qBK{ldf zP{cG%3JnTUbEq!X<1t1YS^Q`QEz{@(Ruj4m#Y)Q4>CGDpu9zTvCd8JB1YMvkF-7KF0D8I4lGN;l-QPqTfnPZPC)GOrLUJBv! z=1)m0@j?bk6W5X!H?V};rXpR34A}^nw?h-i;H%GbSH6+@9^!B4|Kn6QAfA_VOwy)3 za38MHb~PNoUIx|{m3J~t{%Lm?G@V-yfxRn?`0CcsFP37;~F7EtQHuUHd~sxdyG z)NQfV>Sv~+5=bvq8=}8|xP!Un(&s@>D0#}muEjtZ!4JVgx`Q!foQTELg3R zM(xY}Gfg`6TQf%1utWSUxhT=}Xtw2#q}yzIj&obKuu#2`oAUcQlq&D*ZdjO{kB`$e z(`bF;q|JwqHRW@Imle;jWn8&905NX9#&kGDR`5(!!*=)Syj_K>qis{zB|d7>Y2wD-f;`$<}YyK$s?jl}(=da1)J z`9+<&(E{e`tG7$vT<>thG=gbU`z@q?QFzRcSl-${r$3#%68nxF%NEuPy4r@!vqwqC=!BtEFDt?zWcFoj(mCGXHw z+I~cZQox(?xcz?F_A3WOlzt1{&M{^gsp=H6TIMIFBs` zI*LVNGgfUBE0x#B$;#Z_K4k9%d-sp#Ke*b>Y*%m<@&uW9i~>L0vv0mN(=j-kYIG03 zdD~}Cs);!{iP4$@A8?AyQo{^m>iJ%%yD>_kMALO_1-s*EwOCV{M44%FOOeq!gEPD@ zoip&XZCN!1E7x@|rZ@EM-Tmu6-(S!-($(}}bM9RCckk_9W*GOwBwX0MO~1$8l?l1t zbMFABpAP%Mt6ssL&$VltULYTwVP?bTlcgyzV&J&*oG&r)J3kWU8Nso=MLiGT*VoJI z(lQC9y?p)$hWzi~c|@fA^+}{IERU=tErQ5%^4vSru(20Cf3$nr;he}%M`(Nc>vo7r z5P4si|HP`xeF_C)Cob6b<#agGJpY#JzAMz}Ojrx2aAyVCWsmnc6Z+$R9%VPfS zC#nr?eYt{lcVKo=Lp3*{i8cUFRAf(ab;Y{Y?ZEOl5sVldPImG?(}}OpNGl8wThfsQ z{&E8&maQagp5BD{9S!C@Z#vg6Ha4ftEXL>dLF>@ zbfhpK1LE=gt&{Pq5%5ZW58n8i1~;?bzq{1p`}TxF{2+q z#K`97;K##N?0$)8jE)Fi9id*DbMj&mPLf&9{ig7<0d4m!hBc}SsXJQZI2K@!Qt1P* z88GPTc`$ygXY1Y0u&owwKBUXM2kgQRIf*#fP8F;fa%OHp!18;K^__foN|OlEFNzia zKH4(R-j+BR!yBt70^X;Z?s}rXEF8V)ZO40Z9Tt1Q@a^X|MjVQqD1X{5#+{mkkmtVd zga;ylwPGv`Nzg={ztk?G3(#nE?*5~6(?SP>?Rt=Mufi`^H0I0q+e=8O2I=$wPyGC@&|{Pua3|c#1u{H>XO+C9yuw7 z{@rU=Ng*l{D~p1vs2EH9bh5$kX3PId(!ctG}dS_f<1rYmQiUtZ}*S)0dsvzpYk zkWviMQQ|QZk*Jzh{e`V&oQ@D9hq1#|LVa-d3@6u)9jzh#H<9=RR?$#k0Hb6!I^*L2 zAOG!O8XC_{FPt`UX^sJi{*dZ{wjdkR`MH-=Czp-1)IW=AbKgFtZ9*BU_9F{4MBU~u zPZ~G6q#~gUFy2-dsfsX1r?n@>ksp@xPN}Z^PkeV+{Y7nfEtRF$S^v?jy0`A_7QDv$ zlYR`tDoSqs$uk}c+5po4t-veZmc!@+2$ye`DDfnzG!Lu6Lfl{=Ld#a*Y}*6yFTR~3 zIfbHzmHwIS4&JWDuk9V(CcB?`Ef6NnZT!(ceIz02H}^nVwP9eLFXHJ4UT&A^G5SM) zO64|d=Jn9~;1VE!r|}3PMVhV_s+G!49W9!D*-9C_^xJwuhCCCOQbTv3p1ORLrc_9- z?BHLINzybU!ZZkF%=#{gM<@1zgBaVWG-|O{sGuQCv8q?7MWBx@@{WErE&3)ulo|3i z%v9E>Xm|IZ(yePNo*Ew^g^AlKeXq$x4yQF($I zgc!L5r*@BSb<;umLidywt~DYz-$UZ#(9`jeQvdCp<{qDC&sNy5D2ddnW7MrxVqJ-j zIC}Ad{w&Mh(Qf4MF|Ih1?Ct#P;4>_db7ozUt^sJk`RyB#+vnTv;Takirz0fe<|VfW z^a`u>f>VW31+&>OOdK}eB=eL(ICX17kqEFx{-}yuO;{`x5lV+(UfsC&M-46@Tfy+Ze1nJ?14w(5#?U|N0 zSmttwV+eS{7ZTI&b~n$^^yr)&uT;2#dj{n}S`~k6c!6LdkQ2eh8QjkqJ8vX!2B;vA zM&?TUYKs`+Ghnnul!?UfGqU&dy!}f1MTM7PQmsta4;x4NGiBoP7lR+^ zPcgzc8?jSK6$vy-BLZ6?@GjR1ek@_4aJnsvhy%e#UH&-8?~^JLLK7gc&BzbF4}=Z6 ze>o(4k2?f^mp7o4t=2b@OnbASWygE_B0&5DUIaw4HKDs3-%U`YZ{(Vv$Eq-Jq%{4_ zsG4gj8Zi0GWe3gpU|J@FeC>rJ;lw=Qnq$`ielYR}#vgCH9!7_lu!Yge?*98Ttn@pi zvP6O81B_m_w|jx9Y|u3pfLQqo;d)GROx?P^wh?sf-9ASA;`vt^Z~R;g^_R6f@P;_b zy!p>6y&<9Skj@f;a8j(H2w}91gCIGZ%b+C89K4x&ISM(GM7sp@iB3AwR`NDJ(x9tR zu7F8Zyn6ejS*OS-T{a3q5eleB@GmEMdr!bzhzV~lIbYTH^h~VF*B^sLx`?|9 zjNH+*@mMWf@!-udC^_%sb1LK`q0mo*SbgS-GvWL5mT;sqV#*`E%S@{2o_^Q4!blnJ zXQ>MkVTk-|{ZvCrjqF9z1f~GAUFQ1=e}UpOGyC?jN-bh0TT|L1B76<1b1K*J*)wZh zU4FYT7tFcwk5k*U9>>@+*`*1yv*ml=LMQ(k)}W$)Jcnm)epp;y%8PRzQGuxms;4(N z8^X)N|AW(j;e=;r>-LRM{bzGluS7BJy78<>-;r(lJ})2ndV#|k_sL0lMUtikM8P7_ zoHwY5*%C$+I7X%}ns5W}`z<)81h7C?2R~ae3iGyxE+B~I8N|L*nJAj*^!Xtc3XrM9 zk*bY4dtW>t=up7oXYbFB#L#w3btm)H>a(#!fu(LL^e$%lLzMDeQf;DGAF@6{WmE@s zp=J#(3gLO$YSk6&cR)B|xL5eotUlV3h*Hw280JcCGIEU9G4MZzv7p^VOZsoZ+hIvv zLH10I+?(XjH8r&U%2M^k~26uu44elP?fch5QZe*gLhgVCe++O<~Is+zNr3uO2R+Bf|V!~|w*i0U71-o2Fz^6Puf2EGZwfB)Y8DB`IO>ss=U=IGWNYF+6{@0kixWb}>>dvWfTR4C z6H740G2P{g%JqRTsgP_!T4sGJBR6PNwo#cKTwwI%^~>ossojZVsp^1*6IN4+RF{GY zt1JaOyK2n-FVsO1dt0P({W=>z{UYsw=#I|;A*h&#ig3W@fpDPHxIXX-N1rMt#GITS zE|c2ShHUG6Te&AewHkT(v_I3I5K z*OoukX-yPVvLUkCHpuo&s1(M016a?1nC^miy^?l4a6Ge|l$WnUQ^ZRS=))=#6_=u?Y=DwXiLa>*(>UW7Qaw#+x^>yOhb~sjrslO(!TPHX^RiLY*(0uCwt+V+2@EU1JODPZ~%*4Wq8_ zLyPf-g(aRt{+i1K$ncNz!4tlkC`4Y)Zo@=w6e!yiNs;9hqkJ zUGV)`F}!_!@v}Et%|~7mZVxy4?Pm?;-6|M2gvx{H&W*SXJ+}la91L~Q$^2B;4GxSTD3%7Q&2dJ zk@UR-V`GgW_9l^_lYa5d|cf4Hk(MBAWVh8Z$SK<#%@uu zXKZ?4h={La{9>a{vdz`_3PqC_Fu2vo6mUa5STf$UO>2u)xA_?BNaxn7B{`C^!~{M} zld(bk57;h9?_W@@O|b|GX?8@5}c)P)iuQDgR(B)c}4v$@RCLA{yu zsolV?zhQM87fUA2^0~<)`V$JGPk8K&Kp<5D)8a@9y#+FgdNgRn$D&fs=bK!$7CC(= zEtJl%Bj}q(VPXqwiad1>j1%z|V_FkM8KOzSGH1i_U3oe(-8V1ruNIrCJ+NhWhdPgG zG`|DOS@xCQhQ9?_??OFQR>B9dA(_JcQ1CDVAZYu)$fCUE#iVdu9mGQFU!!!VaCx5-@JST@pgg<%d-nZSGS+nyRJ`6b+V z4e=Z=-&PsBh&W3^bctEeZ09zIa!e|iPJCZv&9Px7D|=w z?{7eBVnFXq@PqR>dEysMT=ru#YX^-RPC6UX5O+DOcnClOwq~u@>z$}oH_|U^)}x}o zc#>VT{cu4joXeUw2~otAn7NXKT*p)$j1@DK)(0-e?xQhAa8PD9W#j}5&d*>@){w*5 z#;xrKj8ertnv9-YhP=*@&6(%KB*T?tN!IEd#@}}Qv;--iBz%CM!pj!ix-J{rQ;~LD zE7V|iu2!Q2WwW%}u^tK)dU#{@jU+9~=|qH6cUo-!7k*Cq{1xMg6BgK_lA!EB6BUW{ ztGjZDnXi${?7FJcib(4mDtmae2vL1uJYgJAMy%WfutI=8)HG;Zz&)`6JDOf{l%A>W zAxS^Y{&RiA85bDK;PN(hOc%{*+I0I#0ps{iRzqZ#!QvBkW1SqUx$5`({rc)8R6Fzu zx}+)nT{pzc+g_wm#6z40N5C-GW?Hy!cldy>J4Z<$#QTMDj4iXK$QH2y>ubM|D&*tK z8Q-!=_0BJO12?DHC`+}@U@KmWD&Z>RL@exi&l!7o@l@5&L@@G)-b6}O^)J>{Vvj^UO+M0E>AjXz6)DI|LN@yu_b-4I5HF+1J$6wbjf2Fv@>+xKrQam-~zt zO0s)Q9XZ3}f3jXdABV zAmOaL!Cg5-kYP39E{3G}bWUfnw%N1UcxCwTmFxF2?vJDqYV3IE6&epFm$1raCzT*3 zSry>nWEST8�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�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 tEWbU)#JjP0$G#JbTCA?;Sv5 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 L;Z4kq+tL3E6gqFV literal 0 HcmV?d00001 diff --git a/exetera/core/dataframe.py b/exetera/core/dataframe.py index d62e9e67..f5e3af2b 100644 --- a/exetera/core/dataframe.py +++ b/exetera/core/dataframe.py @@ -17,6 +17,7 @@ from exetera.core import fields as fld from exetera.core import operations as ops from exetera.core import validation as val +from exetera.core.utils import INT64_INDEX_LENGTH import h5py import csv as csvlib @@ -125,7 +126,7 @@ def _add_view(self, field: fld.Field, filter: np.ndarray = None): # add filter if filter is not None: nformat = 'int32' - if len(filter) > 0 and np.max(filter) >= 2**31 - 1: + if len(filter) > 0 and np.max(filter) >= INT64_INDEX_LENGTH: nformat = 'int64' filter_name = view.name if filter_name not in self._filters_grp.keys(): @@ -145,6 +146,19 @@ def _add_view(self, field: fld.Field, filter: np.ndarray = None): return self._columns[view.name] + def _bind_view(self, view: fld.Field, source_field: fld.Field): + """ + Binding view is when the view (reference field) is already set, but has not attach to the original field yet, for + instance during the initializing of an existing dataset/dataframe. + :param view: The view field. + :param source_field: The original field. + """ + source_field.attach(view) + if view.name in self._filters_grp.keys(): + filter_field = fld.NumericField(self._dataset.session, self._filters_grp[view.name], self, + write_enabled=True) + view._filter_index_wrapper = fld.ReadOnlyFieldArray(filter_field, 'values') # read-only + def drop(self, name: str): """ @@ -1037,7 +1051,13 @@ def describe(self, include=None, exclude=None, output='terminal'): return result def view(self): - dfv = self.dataset.create_dataframe(self.name + '_view') + """ + Create a view of this dataframe. + """ + view_name = '_' + self.name + '_view' + if view_name in self.dataset: + self.dataset.drop(view_name) + dfv = self.dataset.create_dataframe(view_name) for f in self.columns.values(): dfv._add_view(f) return dfv diff --git a/exetera/core/dataset.py b/exetera/core/dataset.py index b09d0fdf..905373c4 100644 --- a/exetera/core/dataset.py +++ b/exetera/core/dataset.py @@ -48,11 +48,20 @@ def __init__(self, session, dataset_path, mode, name): self._file = h5py.File(dataset_path, mode) self._dataframes = dict() + #initilize the dataframe and fields for group in self._file.keys(): if group not in ('trash',): h5group = self._file[group] dataframe = edf.HDF5DataFrame(self, group, h5group=h5group) self._dataframes[group] = dataframe + # bind the views + for df in self._dataframes.values(): + for field in df.columns.values(): + if field.is_view(): + source_name = field._field.attrs['source_field'] + idx = source_name.rfind('/') + source_field = self._dataframes[source_name[1:idx]][source_name[idx+1:]] + df._bind_view(field, source_field) @property def session(self): diff --git a/exetera/core/fields.py b/exetera/core/fields.py index da46ec1c..3a94192c 100644 --- a/exetera/core/fields.py +++ b/exetera/core/fields.py @@ -2160,6 +2160,10 @@ def timestamp_field_constructor(session, group, name, timestamp=None, chunksize= def base_view_contructor(session, group, source): """ Constructor are for setup the hdf5 group that going to be a container for a view (rather than a field). + :param session: The ExeTera session. + :param group: The dataframe to locate this view. + :param source: The source field to copy the attributes. + :return: The h5group where this view is created. """ if source.name in group: msg = "Field '{}' already exists in group '{}'" diff --git a/tests/test_dataframe.py b/tests/test_dataframe.py index 677b1ce4..2fbfe54e 100644 --- a/tests/test_dataframe.py +++ b/tests/test_dataframe.py @@ -1426,3 +1426,25 @@ def test_concrete_field(self, creator, name, kwargs, data): self.assertListEqual([], np.asarray(f.data[:]).tolist()) self.assertListEqual(data.tolist(), np.asarray(view[name].data[:]).tolist()) # notify and update self.assertFalse(view[name] in f._view_refs) # detached + + @parameterized.expand(DEFAULT_FIELD_DATA) + def test_view_persistence(self, creator, name, kwargs, data): + """ + The view should be persistent over sessions. + """ + bio = BytesIO() + src = self.s.open_dataset(bio, 'w', 'src') + df = src.create_dataframe('df') + f = self.setup_field(df, creator, name, (), kwargs, data) + df2 = src.create_dataframe('df2') + d_filter = np.array([np.random.random()>=0.5 for i in range(len(data))]) + df.apply_filter(d_filter, df2) + self.assertTrue(df2[name].is_view()) + self.s.close() + + src = self.s.open_dataset(bio, 'r+', 'src') + df = src['df'] + df2 = src['df2'] + self.assertTrue(df2[name].is_view()) + self.assertTrue(df2[name] in df[name]._view_refs) + From 135260e58b6be9a7fda15640bc6d5e2858a59cd1 Mon Sep 17 00:00:00 2001 From: deng113jie Date: Thu, 26 May 2022 15:47:50 +0100 Subject: [PATCH 181/181] documents on future work --- docs/view.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/view.md b/docs/view.md index 4ec04c00..6d8fe2bb 100644 --- a/docs/view.md +++ b/docs/view.md @@ -1,5 +1,5 @@ # View -## what is a view +## What is a view A view is a special field that has a ‘source_field’ in it’s hdf5 attributes. In this case, the view will initialize the dataset from the destination specified by the ‘source_field’ rather than in it’s own hdf5 storage. @@ -38,3 +38,11 @@ Step4: At the moment, the view.update() will copy the original data to it’s ow ### An existing view As the view is stored in the hdf5, the view relationship can be presistenced over sessions. Upon loading a dataset (in dataset.__init__), the dataset will check if there is a view and call dataframe._bind_view() to attach the view to the field during initialization of dataset/dataframe/fields. This is why the view can only be created from a field that co-exist in the same dataset (hdf5 file). + + +## Future works +### Data fetching performance +Different ways of getting data out from the HDF5 can vary the performance a lot. For example, it's generally better to get the data out of HDF5 by chunk rather than indexes. In the current implementation (fieldarray.__getitem__), we mask the index filter with item first, then fetch the data out from hdf5. As hdf5 doesn't support un-ordered data access, we sort the mask and convert them back when return the data. Further work can be done on how to arrange the order of index filter and item (specified by the user through __getitem__). For example, with large volume of data and small set of index filter, it might make sense to mask the filter first. However in the case of large filters, it will be faster to load the data into memory first. Where is the boundary worth investigating. + +### Dependency between views +In the current implementation, the views all dependent on the source field. In the case of changing the data in the field, all the attached views will copy the data over and write it's own copy. This is not efficient with a number of views attached. One better way could be only one of the view to copy the data over and become the source field of the rest views. This needs a detailed design and implementation in fields.update().