Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cdisc_rules_engine/models/operation_params.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import List
from typing import List, Any
from cdisc_rules_engine.models.external_dictionaries_container import (
ExternalDictionariesContainer,
)
Expand Down Expand Up @@ -57,6 +57,7 @@ class OperationParams:
regex: str = None
returntype: str = None
source: str = None
value: Any = None
target: str = None
subtract: str = None
order_insensitive: bool = True
Expand Down
2 changes: 2 additions & 0 deletions cdisc_rules_engine/operations/operations_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
from cdisc_rules_engine.operations.valid_external_dictionary_code_term_pair import (
ValidExternalDictionaryCodeTermPair,
)
from cdisc_rules_engine.operations.value_equals import ValueEquals
from cdisc_rules_engine.operations.variable_exists import VariableExists
from cdisc_rules_engine.operations.variable_names import VariableNames
from cdisc_rules_engine.operations.variable_value_count import VariableValueCount
Expand Down Expand Up @@ -144,6 +145,7 @@ class OperationsFactory(FactoryInterface):
"valid_external_dictionary_code": ValidExternalDictionaryCode,
"valid_external_dictionary_code_term_pair": ValidExternalDictionaryCodeTermPair,
"valid_define_external_dictionary_version": DefineDictionaryVersionValidator,
"value_equals": ValueEquals,
"get_dataset_filtered_variables": GetDatasetFilteredVariables,
"get_xhtml_errors": GetXhtmlErrors,
}
Expand Down
60 changes: 60 additions & 0 deletions cdisc_rules_engine/operations/value_equals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import pandas as pd
from cdisc_rules_engine.models.dataset.dask_dataset import DaskDataset
from cdisc_rules_engine.operations.base_operation import BaseOperation


class ValueEquals(BaseOperation):
def _execute_operation(self):
dataframe = self.params.dataframe
target = self.params.target
expected = self.params.value

if target not in dataframe.columns:
raise ValueError(f"Target column '{target}' not found in dataset")

target_series = dataframe[target]

if self._is_variable_list_column(target_series):
# Avoid dask row-wise apply tokenization issues by using pandas for this path.
if isinstance(dataframe, DaskDataset):
pandas_df = dataframe.data.compute()
return pandas_df.apply(
self._get_matching_variable_names_from_row,
axis=1,
args=(target, expected),
)
return dataframe.apply(
self._get_matching_variable_names_from_row,
axis=1,
args=(target, expected),
)

return target_series.apply(
lambda value: [target] if self._values_equal(value, expected) else []
)

def _is_variable_list_column(self, series) -> bool:
non_null_values = series[series.notna()]
return len(non_null_values) > 0 and all(
isinstance(value, (list, tuple, set)) for value in non_null_values
)

def _get_matching_variable_names_from_row(
self, row, list_column_name: str, expected
):
variable_names = row[list_column_name]
if not isinstance(variable_names, (list, tuple, set)):
return []

matches = []
for variable_name in variable_names:
if variable_name in row.index and self._values_equal(
row[variable_name], expected
):
matches.append(variable_name)
return matches

def _values_equal(self, actual, expected) -> bool:
if expected is None:
return pd.isna(actual)
return actual == expected
1 change: 1 addition & 0 deletions cdisc_rules_engine/utilities/rule_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ def perform_rule_operations(
regex=operation.get("regex"),
returntype=operation.get("returntype"),
source=operation.get("source"),
value=operation.get("value"),
standard=standard,
standard_substandard=standard_substandard,
standard_version=standard_version,
Expand Down
15 changes: 14 additions & 1 deletion resources/schema/rule-merged/Operations.json
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@
"properties": {
"operator": {
"const": "record_count",
"markdownDescription": "\nIf no filter or group is provided, returns the number of records in the dataset. If filter is provided, returns the number of records in the dataset that contain the value(s) in the corresponding column(s) provided in the filter. Filter can have a wildcard `&` that when added to the end of the filter value will look for all instances of that prefix (see 4th example below). If group is provided, returns the number of rows matching each unique set of the grouping variables. These can be static column name(s) or can be derived from other operations like get_dataset_filtered_variables.\n\nIf both filter and group are provided, returns the number of records in the dataset that contain the value(s) in the corresponding column(s) provided in the filter that also match each unique set of the grouping variables.\n\n**Wildcard Filtering:** Filter values ending with % will match any records where the column value starts with the specified prefix. For example, RACE% will match RACE1, RACE2, RACE3, etc. This is useful for matching related variables with numeric or alphabetic suffixes.\n\n**Regex Transformation:** If regex is provided along with group, the regex pattern will be applied to transform grouping column values before grouping. The regex is only applied to columns where the pattern matches the data type. For example, using regex `^\\d{4}-\\d{2}-\\d{2}` on a column containing `2022-01-14T08:00` will extract `2022-01-14` for grouping purposes.\n\nIf group is provided, group_aliases may also be provided to assign new grouping variable names so that results grouped by the values in one set of grouping variables can be merged onto a dataset according to the same grouping value(s) stored in different set of grouping variables. When both group and group_aliases are provided, columns are renamed according to corresponding list position (i.e., the 1st column in group is renamed to the 1st column in group_aliases, etc.). If there are more columns listed in group than in group_aliases, only the group columns with corresponding group_aliases columns will be renamed. If there are more columns listed in group_aliases than in group, the extra column names in group_aliases will be ignored.\n\nExample: return the number of records in a dataset.\n\n```yaml\n- operator: record_count\n id: $records_in_dataset\n```\n\nExample: return the number of records where STUDYID = \"CDISC01\" and FLAGVAR = \"Y\".\n\n```yaml\n- operator: record_count\n id: $flagged_cdisc01_records_in_dataset\n filter:\n STUDYID: \"CDISC01\"\n FLAGVAR: \"Y\"\n```\n\nExample: return the number of records grouped by USUBJID and timing variables, extracting only the date portion from datetime values.\n\n```yaml\n- operator: record_count\n id: $records_per_usubjid_date\n group:\n - USUBJID\n - --TESTCD\n - $TIMING_VARIABLES\n regex: \"^\\d{4}-\\d{2}-\\d{2}\"\n```\n\nExample: return the number of records where QNAM starts with \"RACE\" (matches RACE1, RACE2, RACE3, etc.) per USUBJID.\n\n```yaml\n- operator: record_count\n id: $race_records_in_dataset\n filter:\n QNAM: \"RACE&\"\n group:\n - \"USUBJID\"\n```\n\nExample: return the number of records grouped by USUBJID.\n\n```yaml\n- operator: record_count\n id: $records_per_usubjid\n group:\n - USUBJID\n```\n\nExample: return the number of records grouped by USUBJID where FLAGVAR = \"Y\".\n\n```yaml\n- operator: record_count\n id: $flagged_records_per_usubjid\n group:\n - USUBJID\n filter:\n FLAGVAR: \"Y\"\n```\n\nExample: return the number of records grouped by USUBJID and IDVARVAL where QNAM = \"TEST1\" and IDVAR = \"GROUPID\", renaming the IDVARVAL column to GROUPID for subsequent merging.\n\n```yaml\n- operator: record_count\n id: $test1_records_per_usubjid_groupid\n group:\n - USUBJID\n - IDVARVAL\n filter:\n QNAM: \"TEST1\"\n IDVAR: \"GROUPID\"\n group_aliases:\n - USUBJID\n - GROUPID\n```\n\nExample: Group the StudyIdentifier dataset by parent_id and merge the result back to the context dataset StudyVersion using StudyVersion.id == StudyIdentifier.parent_id\n\n```yaml\nScope:\n Entities:\n Include:\n - StudyVersion\nOperations:\n - domain: StudyIdentifier\n filter:\n parent_entity: \"StudyVersion\"\n parent_rel: \"studyIdentifiers\"\n rel_type: \"definition\"\n studyIdentifierScope.organizationType.code: \"C70793\"\n studyIdentifierScope.organizationType.codeSystem: \"http://www.cdisc.org\"\n group:\n - parent_id\n group_aliases:\n - id\n id: $num_sponsor_ids\n operator: record_count\n```\n"
"markdownDescription": "\nIf no filter or group is provided, returns the number of records in the dataset. If filter is provided, returns the number of records in the dataset that contain the value(s) in the corresponding column(s) provided in the filter. Filter can have a wildcard `&` that when added to the end of the filter value will look for all instances of that prefix (see 4th example below). If group is provided, returns the number of rows matching each unique set of the grouping variables. These can be static column name(s) or can be derived from other operations like get_dataset_filtered_variables.\n\nIf both filter and group are provided, returns the number of records in the dataset that contain the value(s) in the corresponding column(s) provided in the filter that also match each unique set of the grouping variables.\n\n**Wildcard Filtering:** Filter values ending with % will match any records where the column value starts with the specified prefix. For example, RACE% will match RACE1, RACE2, RACE3, etc. This is useful for matching related variables with numeric or alphabetic suffixes.\n\n**Regex Transformation:** If regex is provided along with group, the regex pattern will be applied to transform grouping column values before grouping. The regex is only applied to columns where the pattern matches the data type. For example, using regex `^\\d{4}-\\d{2}-\\d{2}` on a column containing `2022-01-14T08:00` will extract `2022-01-14` for grouping purposes.\n\nIf group is provided, group_aliases may also be provided to assign new grouping variable names so that results grouped by the values in one set of grouping variables can be merged onto a dataset according to the same grouping value(s) stored in different set of grouping variables. When both group and group_aliases are provided, columns are renamed according to corresponding list position (i.e., the 1st column in group is renamed to the 1st column in group_aliases, etc.). If there are more columns listed in group than in group_aliases, only the group columns with corresponding group_aliases columns will be renamed. If there are more columns listed in group_aliases than in group, the extra column names in group_aliases will be ignored.\n\nExample: return the number of records in a dataset.\n\n```yaml\n- operator: record_count\n id: $records_in_dataset\n```\n\nExample: return the number of records where STUDYID = \"CDISC01\" and FLAGVAR = \"Y\".\n\n```yaml\n- operator: record_count\n id: $flagged_cdisc01_records_in_dataset\n filter:\n STUDYID: \"CDISC01\"\n FLAGVAR: \"Y\"\n```\n\nExample: return the number of records grouped by USUBJID and timing variables, extracting only the date portion from datetime values.\n\n```yaml\n- operator: record_count\n id: $records_per_usubjid_date\n group:\n - USUBJID\n - --TESTCD\n - $TIMING_VARIABLES\n regex: \"^\\d{4}-\\d{2}-\\d{2}\"\n```\n\nExample: return the number of records where QNAM starts with \"RACE\" (matches RACE1, RACE2, RACE3, etc.) per USUBJID.\n\n```yaml\n- operator: record_count\n id: $race_records_in_dataset\n filter:\n QNAM: \"RACE&\"\n group:\n - \"USUBJID\"\n```\n\nExample: return the number of records grouped by USUBJID.\n\n```yaml\n- operator: record_count\n id: $records_per_usubjid\n group:\n - USUBJID\n```\n\nExample: return the number of records grouped by USUBJID where FLAGVAR = \"Y\".\n\n```yaml\n- operator: record_count\n id: $flagged_records_per_usubjid\n group:\n - USUBJID\n filter:\n FLAGVAR: \"Y\"\n```\n\nExample: return the number of records grouped by USUBJID and IDVARVAL where QNAM = \"TEST1\" and IDVAR = \"GROUPID\", renaming the IDVARVAL column to GROUPID for subsequent merging.\n\n```yaml\n- operator: record_count\n id: $test1_records_per_usubjid_groupid\n group:\n - USUBJID\n - IDVARVAL\n filter:\n QNAM: \"TEST1\"\n IDVAR: \"GROUPID\"\n group_aliases:\n - USUBJID\n - GROUPID\n```\n\nExample: Group the StudyIdentifier dataset by parent_id and merge the result back to the context dataset StudyVersion using StudyVersion.id == StudyIdentifier.parent_id\n\n```yaml\nScope:\n Entities:\n Include:\n - StudyVersion\nOperations:\n - domain: StudyIdentifier\n filter:\n parent_entity: \"StudyVersion\"\n parent_rel: \"studyIdentifiers\"\n rel_type: \"definition\"\n studyIdentifierScope.organizationType.code: \"C70793\"\n studyIdentifierScope.organizationType.codeSystem: \"http://www.cdisc.org\"\n group:\n - parent_id\n group_aliases:\n - id\n id: $num_sponsor_ids\n operator: record_count\n```"
}
},
"required": ["id", "operator"],
Expand Down Expand Up @@ -530,6 +530,16 @@
},
"required": ["id", "operator", "name", "namespace"],
"type": "object"
},
{
"properties": {
"operator": {
"const": "value_equals",
"markdownDescription": "\nCompares a target column value against a specified value, returning matching variable names. When the target is a single variable column, returns the variable name if the value matches, or an empty list if it does not. When the target is a variable-list column (containing lists of variable names per row), evaluates each listed variable against the specified value and returns only those variable names where the value matched.\n\n**Required Parameters:**\n\n- `name`: Target column name to check\n- `value`: Value to compare against (can be scalar, list, or null)\n\n**Null Semantics:**\n\nWhen `value` is null, only true null values (as determined by `pd.isna()`) are matched. Empty strings, zero, or other \"falsy\" values are not considered null.\n\n**Single Column Example:**\n\nGiven a dataset:\n\n```yaml\nPPSTRESU\n3\n5\n3\n```\n\nand the following operation:\n\n```yaml\nOperations:\n - id: $ppstresu_equals_3\n operator: value_equals\n name: PPSTRESU\n value: 3\n```\n\nThis will result in:\n\n```\nPPSTRESU $ppstresu_equals_3\n3 [PPSTRESU]\n5 []\n3 [PPSTRESU]\n```\n\n**Variable-List Column Example:**\n\nGiven a dataset where one column contains lists of variable names to check:\n\n```\n$var_list A B C\n[A, B] 1 null 5\n[B, C] 2 3 null\n```\n\nand the following operation:\n\n```yaml\nOperations:\n - id: $vars_equal_null\n operator: value_equals\n name: $var_list\n value: null\n```\n\nThis will result in:\n\n```\n$var_list A B C $vars_equal_null\n[A, B] 1 null 5 [B]\n[B, C] 2 3 null [C]\n```\n\n**Null Value Example:**\n\nTo detect rows where a specific variable is null:\n\n```yaml\nOperations:\n - id: $status_is_null\n operator: value_equals\n name: STATUS\n value: null\n```\n\nwill return `[STATUS]` for rows where STATUS is null (via `pd.isna()`), and `[]` for rows where STATUS has any other value (including empty string).\n"
}
},
"required": ["id", "operator", "name", "value"],
"type": "object"
}
],
"properties": {
Expand Down Expand Up @@ -697,6 +707,9 @@
"term_pref_term": {
"type": "string"
},
"value": {
"type": ["string", "number", "boolean", "null", "array"]
},
"value_is_reference": {
"type": "boolean"
},
Expand Down
12 changes: 12 additions & 0 deletions resources/schema/rule/Operations.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,15 @@
},
"required": ["id", "operator", "name", "namespace"],
"type": "object"
},
{
"properties": {
"operator": {
"const": "value_equals"
}
},
"required": ["id", "operator", "name", "value"],
"type": "object"
}
],
"properties": {
Expand Down Expand Up @@ -644,6 +653,9 @@
"term_pref_term": {
"type": "string"
},
"value": {
"type": ["string", "number", "boolean", "null", "array"]
},
"value_is_reference": {
"type": "boolean"
},
Expand Down
84 changes: 84 additions & 0 deletions resources/schema/rule/Operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,90 @@ Operations:
id: $num_sponsor_ids
operator: record_count
```
### value_equals

Compares a target column value against a specified value, returning matching variable names. When the target is a single variable column, returns the variable name if the value matches, or an empty list if it does not. When the target is a variable-list column (containing lists of variable names per row), evaluates each listed variable against the specified value and returns only those variable names where the value matched.

**Required Parameters:**

- `name`: Target column name to check
- `value`: Value to compare against (can be scalar, list, or null)

**Null Semantics:**

When `value` is null, only true null values (as determined by `pd.isna()`) are matched. Empty strings, zero, or other "falsy" values are not considered null.

**Single Column Example:**

Given a dataset:

```yaml
PPSTRESU
3
5
3
```

and the following operation:

```yaml
Operations:
- id: $ppstresu_equals_3
operator: value_equals
name: PPSTRESU
value: 3
```

This will result in:

```
PPSTRESU $ppstresu_equals_3
3 [PPSTRESU]
5 []
3 [PPSTRESU]
```

**Variable-List Column Example:**

Given a dataset where one column contains lists of variable names to check:

```
$var_list A B C
[A, B] 1 null 5
[B, C] 2 3 null
```

and the following operation:

```yaml
Operations:
- id: $vars_equal_null
operator: value_equals
name: $var_list
value: null
```

This will result in:

```
$var_list A B C $vars_equal_null
[A, B] 1 null 5 [B]
[B, C] 2 3 null [C]
```

**Null Value Example:**

To detect rows where a specific variable is null:

```yaml
Operations:
- id: $status_is_null
operator: value_equals
name: STATUS
value: null
```

will return `[STATUS]` for rows where STATUS is null (via `pd.isna()`), and `[]` for rows where STATUS has any other value (including empty string).

### variable_count

Expand Down
Loading
Loading