Skip to content

Commit 1c345b2

Browse files
update to trigger dask (#1184)
* update to trigger dask * update maxdate, NaN filtering * update special case check * update deepcopy * update inequality * added series to class check * restore deleted operators * fixed equal_to indexing * present_on_multiple_rows_within fix * rework empty * updated empty check * empty * equality * restored USDM test suite * restore missing relationship_data * Remove cartesian products, fix ContentsDefineVLMDatasetBuilder, fix test_present_on_multiple_rows_within * Fix get_codelist_attributes dask series * fixes for record_count and unique * Fix indexes for dask match datasets * reenumerate index instead of preserving old index * compute before boolean masking * better index reset * try range instead of rangeindex * revert to set_index * Revert most of distinct and record_count test changes * better record_count example * Remove comments --------- Co-authored-by: gerrycampion <85252124+gerrycampion@users.noreply.github.com> Co-authored-by: Gerry Campion <gcampion@cdisc.org>
1 parent a11a99e commit 1c345b2

15 files changed

Lines changed: 132 additions & 90 deletions

File tree

.github/workflows/test_suite.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ jobs:
8383
- name: Run validation with Dask
8484
id: dask_run
8585
continue-on-error: true
86+
env:
87+
DATASET_SIZE_THRESHOLD: 0
8688
run: |
87-
echo "DATASET_SIZE_THRESHOLD=0" > .env
8889
python core.py validate -s sdtmig -v 3-3 ${{ env.RULE_LIST }} -d CORE_Test_Suite/data -ct sdtmct-2020-03-27 -of json -o CORE_Test_Suite/dask-results -l info || true
8990
9091
if [ -f "CORE_Test_Suite/dask-results.json" ]; then

cdisc_rules_engine/check_operators/dataframe_operators.py

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,10 @@ def _check_equality(
163163
comparison_data = (
164164
comparator if comparator not in row or value_is_literal else row[comparator]
165165
)
166-
target_values = row[target]
167-
target_is_empty = pd.isna(target_values)
168-
if not target_is_empty and isinstance(row[target], str):
169-
target_is_empty = row[target] == ""
170-
comp_is_empty = pd.isna(comparison_data)
171-
if not comp_is_empty and isinstance(comparison_data, str):
172-
comp_is_empty = comparison_data == ""
173-
if target_is_empty or comp_is_empty:
166+
both_null = (comparison_data == "" or comparison_data is None) & (
167+
row[target] == "" or row[target] is None
168+
)
169+
if both_null:
174170
return False
175171
if case_insensitive:
176172
target_val = row[target].lower() if row[target] else None
@@ -198,32 +194,11 @@ def _check_inequality(
198194
comparison_data = (
199195
comparator if comparator not in row or value_is_literal else row[comparator]
200196
)
201-
target_is_empty = pd.isna(row[target])
202-
if isinstance(row[target], str) and (
203-
not target_is_empty.any()
204-
if hasattr(target_is_empty, "any")
205-
else not target_is_empty
206-
):
207-
target_is_empty = row[target] == ""
208-
comp_is_empty = pd.isna(comparison_data)
209-
if isinstance(comparison_data, str) and (
210-
not comp_is_empty.any()
211-
if hasattr(comp_is_empty, "any")
212-
else not comp_is_empty
213-
):
214-
comp_is_empty = comparison_data == ""
215-
target_is_empty_scalar = (
216-
target_is_empty.any()
217-
if hasattr(target_is_empty, "any")
218-
else target_is_empty
197+
both_null = (comparison_data == "" or comparison_data is None) & (
198+
row[target] == "" or row[target] is None
219199
)
220-
comp_is_empty_scalar = (
221-
comp_is_empty.any() if hasattr(comp_is_empty, "any") else comp_is_empty
222-
)
223-
if target_is_empty_scalar and comp_is_empty_scalar:
200+
if both_null:
224201
return False
225-
if target_is_empty_scalar or comp_is_empty_scalar:
226-
return True
227202
if case_insensitive:
228203
target_val = row[target].lower() if row[target] else None
229204
comparison_val = comparison_data.lower() if comparison_data else None
@@ -243,7 +218,8 @@ def equal_to(self, other_value):
243218
return self.value.apply(
244219
lambda row: self._check_equality(row, target, comparator, value_is_literal),
245220
axis=1,
246-
).astype(bool)
221+
meta=(None, "bool"),
222+
).reset_index(drop=True)
247223

248224
@log_operator_execution
249225
@type_operator(FIELD_DATAFRAME)
@@ -294,7 +270,8 @@ def not_equal_to(self, other_value):
294270
row, target, comparator, value_is_literal
295271
),
296272
axis=1,
297-
)
273+
meta=(None, "bool"),
274+
).reset_index(drop=True)
298275

299276
@log_operator_execution
300277
@type_operator(FIELD_DATAFRAME)
@@ -1290,7 +1267,9 @@ def present_on_multiple_rows_within(self, other_value: dict):
12901267
lambda x: self.validate_series_length(x, target, min_count), meta=meta
12911268
)
12921269
uuid = str(uuid4())
1293-
return self.value.merge(results.rename(uuid), on=target)[uuid]
1270+
return self.value.merge(
1271+
results.rename(uuid).reset_index(), on=[group_by_column, target]
1272+
)[uuid]
12941273

12951274
def validate_series_length(
12961275
self, data: DatasetInterface, target: str, min_length: int

cdisc_rules_engine/dataset_builders/contents_define_vlm_dataset_builder.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from pandas import Series
12
from cdisc_rules_engine.dataset_builders.values_dataset_builder import (
23
ValuesDatasetBuilder,
34
)
@@ -74,16 +75,14 @@ def build(self):
7475

7576
@staticmethod
7677
def apply_filters(
77-
vlm_row: dict, data_contents_df: DatasetInterface
78+
vlm_row: Series, data_contents_df: DatasetInterface
7879
) -> DatasetInterface:
7980
filter_results = data_contents_df.apply(
8081
lambda data_contents_row: vlm_row["filter"](data_contents_row), axis=1
8182
)
8283
row_numbers = data_contents_df.data["row_number"].copy()
8384
rows = row_numbers.where(filter_results)
84-
filtered_df = rows.dropna().to_frame()
85-
lut_subset = data_contents_df.__class__.cartesian_product(
86-
filtered_df,
87-
vlm_row.to_frame().T[["define_variable_name", "define_vlm_name"]],
88-
)
85+
filtered_df = data_contents_df.__class__(rows.dropna().to_frame())
86+
vlm_row_df = vlm_row.to_frame().T[["define_variable_name", "define_vlm_name"]]
87+
lut_subset = filtered_df.assign(**vlm_row_df.iloc[0])
8988
return lut_subset

cdisc_rules_engine/models/dataset/dask_dataset.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def data(self, data):
4444

4545
def __getitem__(self, item):
4646
try:
47+
if hasattr(item, "dtype") and pd.api.types.is_bool_dtype(item.dtype):
48+
# Handle boolean indexing
49+
return self._data.compute()[item]
4750
return self._data[item].compute().reset_index(drop=True)
4851
except ValueError as e:
4952
# Handle boolean indexing length mismatch which occurs when filtering
@@ -93,6 +96,14 @@ def __len__(self):
9396

9497
return self.length
9598

99+
def __deepcopy__(self, memo):
100+
pandas_df = self._data.compute()
101+
fresh_dask_df = dd.from_pandas(pandas_df, npartitions=DEFAULT_NUM_PARTITIONS)
102+
new_instance = self.__class__(fresh_dask_df)
103+
new_instance.length = self.length
104+
memo[id(self)] = new_instance
105+
return new_instance
106+
96107
@classmethod
97108
def from_dict(cls, data: dict, **kwargs):
98109
dataframe = dd.from_dict(data, npartitions=DEFAULT_NUM_PARTITIONS, **kwargs)
@@ -232,7 +243,7 @@ def melt(
232243
return self.__class__(new_data)
233244

234245
def assign(self, **kwargs):
235-
return self.data.assign(**kwargs)
246+
return self.__class__(self.data.assign(**kwargs))
236247

237248
def copy(self):
238249
new_data = self._data.copy()
@@ -265,18 +276,6 @@ def get_error_rows(self, results) -> "pd.Dataframe":
265276
1000, npartitions=-1
266277
)
267278

268-
@classmethod
269-
def cartesian_product(cls, left, right):
270-
"""
271-
Return the cartesian product of two dataframes
272-
"""
273-
return cls(
274-
dd.from_pandas(
275-
left.compute().merge(right, how="cross"),
276-
npartitions=DEFAULT_NUM_PARTITIONS,
277-
)
278-
)
279-
280279
def dropna(self, inplace=False, **kwargs):
281280
result = self._data.dropna(**kwargs)
282281
if inplace:
@@ -371,7 +370,9 @@ def partition_isin(partition):
371370
return result
372371

373372
def filter_by_value(self, column, values):
374-
computed_data = self._data.compute()
375-
mask = computed_data[column].isin(values)
376-
filtered_df = computed_data[mask]
377-
return filtered_df
373+
mask = self._data[column].isin(values)
374+
return self.__class__(self._data[mask])
375+
376+
def max(self, *args, **kwargs):
377+
result = self._data.max(*args, **kwargs)
378+
return self.__class__(result)

cdisc_rules_engine/models/dataset/dataset_interface.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ def len(self) -> int:
170170
Return the length of the dataset
171171
"""
172172

173+
@abstractmethod
174+
def assign(self, **kwargs):
175+
"""
176+
Assign new columns to the dataset.
177+
This method should return a new instance of the dataset with the new columns added.
178+
"""
179+
173180
@abstractmethod
174181
def copy(self) -> "DatasetInterface":
175182
"""
@@ -194,12 +201,6 @@ def where(cond, other, **kwargs):
194201
Wrapper for dataframe where function
195202
"""
196203

197-
@abstractmethod
198-
def cartesian_product(cls, left, right):
199-
"""
200-
Return the cartesian product of two dataframes
201-
"""
202-
203204
@abstractmethod
204205
def sort_values(self, by, **kwargs):
205206
"""

cdisc_rules_engine/models/dataset/pandas_dataset.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ def len(self) -> int:
187187
def size(self) -> int:
188188
return self._data.memory_usage().sum()
189189

190+
def assign(self, **kwargs):
191+
return self.__class__(self.data.assign(**kwargs))
192+
190193
def copy(self):
191194
new_data = self._data.copy()
192195
return self.__class__(new_data)
@@ -206,13 +209,6 @@ def where(self, cond, other, **kwargs):
206209
new_data = self._data.where(cond, other, **kwargs)
207210
return self.__class__(new_data)
208211

209-
@classmethod
210-
def cartesian_product(cls, left, right):
211-
"""
212-
Return the cartesian product of two dataframes
213-
"""
214-
return cls(left.merge(right, how="cross"))
215-
216212
def sort_values(self, by: Union[str, list[str]], **kwargs) -> "pd.Dataframe":
217213
"""
218214
Sort the underlying dataframe and return a raw dataframe.
@@ -247,6 +243,9 @@ def astype(self, dtype, **kwargs):
247243
def min(self, *args, **kwargs):
248244
return self.__class__(self._data.min(*args, **kwargs))
249245

246+
def max(self, *args, **kwargs):
247+
return self.__class__(self._data.max(*args, **kwargs))
248+
250249
def reset_index(self, drop=False, **kwargs):
251250
"""
252251
Reset the index of the dataset.

cdisc_rules_engine/operations/get_codelist_attributes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def _get_codelist_attributes(self):
5555

5656
# 4.0 merge the two datasets by CC
5757
# -------------------------------------------------------------------
58-
cc_key = ct_data[ct_name].to_list()
58+
cc_key = ct_data[ct_name]
5959
ct_list = ct_cache[(ct_cache[ct_name].isin(cc_key))]
6060
ds_len = self.params.dataframe.len()
6161
result = pd.Series([ct_list[ct_attribute].values[0] for _ in range(ds_len)])

cdisc_rules_engine/operations/max_date.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ def _execute_operation(self):
1212
else:
1313
result = max_date.isoformat()
1414
else:
15-
result = self.params.dataframe.groupby(
16-
self.params.grouping, as_index=False
17-
).data.max()
15+
result = self.params.dataframe.groupby(self.params.grouping).max()
16+
if isinstance(result, pd.Series):
17+
result = result.apply(lambda x: x.isoformat() if pd.notna(x) else "")
18+
elif isinstance(result, pd.DataFrame):
19+
for col in result.columns:
20+
if pd.api.types.is_datetime64_any_dtype(result[col]):
21+
result[col] = result[col].apply(
22+
lambda x: x.isoformat() if pd.notna(x) else ""
23+
)
1824
return result

cdisc_rules_engine/operations/record_count.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pandas as pd
2+
from numpy import int64
23

34
from cdisc_rules_engine.operations.base_operation import BaseOperation
45

@@ -33,6 +34,7 @@ def _execute_operation(self) -> pd.Series:
3334
how="left",
3435
)
3536
.fillna(0)
37+
.astype({self.params.target: int64})
3638
)
3739
return group_df
3840
return result

cdisc_rules_engine/services/data_services/base_data_service.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from concurrent.futures import ThreadPoolExecutor
66
import os
77
import numpy as np
8+
import dask.dataframe as dd
89

910
from cdisc_rules_engine.interfaces import (
1011
CacheServiceInterface,
@@ -29,6 +30,7 @@
2930
get_directory_path,
3031
search_in_list_of_dicts,
3132
tag_source,
33+
replace_nan_values_in_df,
3234
)
3335
from cdisc_rules_engine.utilities.sdtm_utilities import get_class_and_domain_metadata
3436
from cdisc_rules_engine.models.dataset.dataset_interface import DatasetInterface
@@ -266,8 +268,22 @@ def _contains_topic_variable(
266268
"""
267269

268270
def check_presence(key):
269-
in_dataset = key in dataset
270-
in_values = key in self.dataset_implementation.get_series_values(dataset)
271+
if hasattr(dataset, "columns"):
272+
columns = dataset.columns
273+
if hasattr(columns, "tolist"):
274+
columns = columns.tolist()
275+
in_dataset = key in columns
276+
in_values = key in self.dataset_implementation.get_series_values(
277+
dataset
278+
)
279+
else:
280+
series_values = dataset.values
281+
if hasattr(series_values, "tolist"):
282+
series_values = series_values.tolist()
283+
in_dataset = key in series_values
284+
in_values = key in self.dataset_implementation.get_series_values(
285+
dataset
286+
)
271287
return in_dataset or in_values
272288

273289
if not check_presence("DOMAIN") and not check_presence("RDOMAIN"):
@@ -300,9 +316,16 @@ def _replace_nans_in_specified_cols_with_none(
300316
"""
301317
Replaces NaN in specified columns with None.
302318
"""
303-
if isinstance(column_names, List):
304-
column_names = dataset.data[column_names].columns
305-
dataset.data = dataset.data.replace(np.nan, {col: None for col in column_names})
319+
valid_columns = [col for col in column_names if col in dataset.data.columns]
320+
if not valid_columns:
321+
return dataset
322+
if isinstance(dataset.data, dd.DataFrame):
323+
dataset.data = dataset.data.map_partitions(
324+
replace_nan_values_in_df, valid_columns
325+
)
326+
else:
327+
dataset.data = replace_nan_values_in_df(dataset.data, valid_columns)
328+
return dataset
306329

307330
async def _async_get_dataset(
308331
self, function_to_call: Callable, dataset_name: str, **kwargs

0 commit comments

Comments
 (0)