csrlite 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- csrlite/__init__.py +50 -0
- csrlite/ae/__init__.py +1 -0
- csrlite/ae/ae_listing.py +492 -0
- csrlite/ae/ae_specific.py +478 -0
- csrlite/ae/ae_summary.py +399 -0
- csrlite/ae/ae_utils.py +132 -0
- csrlite/common/count.py +199 -0
- csrlite/common/parse.py +308 -0
- csrlite/common/plan.py +353 -0
- csrlite/common/utils.py +33 -0
- csrlite/common/yaml_loader.py +71 -0
- csrlite/disposition/__init__.py +2 -0
- csrlite/disposition/disposition.py +301 -0
- csrlite-0.1.0.dist-info/METADATA +68 -0
- csrlite-0.1.0.dist-info/RECORD +17 -0
- csrlite-0.1.0.dist-info/WHEEL +5 -0
- csrlite-0.1.0.dist-info/top_level.txt +1 -0
csrlite/common/count.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# pyre-strict
|
|
2
|
+
import polars as pl
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _to_pop(
|
|
6
|
+
population: pl.DataFrame,
|
|
7
|
+
id: str,
|
|
8
|
+
group: str,
|
|
9
|
+
total: bool = True,
|
|
10
|
+
missing_group: str = "error",
|
|
11
|
+
) -> pl.DataFrame:
|
|
12
|
+
# prepare data
|
|
13
|
+
pop = population.select(id, group)
|
|
14
|
+
|
|
15
|
+
# validate data
|
|
16
|
+
if pop[id].is_duplicated().any():
|
|
17
|
+
raise ValueError(f"The '{id}' column in the population DataFrame is not unique.")
|
|
18
|
+
|
|
19
|
+
if missing_group == "error" and pop[group].is_null().any():
|
|
20
|
+
raise ValueError(
|
|
21
|
+
f"Missing values found in the '{group}' column of the population DataFrame, "
|
|
22
|
+
"and 'missing_group' is set to 'error'."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Convert group to Enum for consistent categorical ordering
|
|
26
|
+
u_pop = pop[group].unique().sort().to_list()
|
|
27
|
+
|
|
28
|
+
# handle total column
|
|
29
|
+
if total:
|
|
30
|
+
pop_total = pop.with_columns(pl.lit("Total").alias(group))
|
|
31
|
+
pop = pl.concat([pop, pop_total]).with_columns(
|
|
32
|
+
pl.col(group).cast(pl.Enum(u_pop + ["Total"]))
|
|
33
|
+
)
|
|
34
|
+
else:
|
|
35
|
+
pop = pop.with_columns(pl.col(group).cast(pl.Enum(u_pop)))
|
|
36
|
+
|
|
37
|
+
return pop
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def count_subject(
|
|
41
|
+
population: pl.DataFrame,
|
|
42
|
+
id: str,
|
|
43
|
+
group: str,
|
|
44
|
+
total: bool = True,
|
|
45
|
+
missing_group: str = "error",
|
|
46
|
+
) -> pl.DataFrame:
|
|
47
|
+
"""
|
|
48
|
+
Counts subjects by group and optionally includes a 'Total' column.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
population (pl.DataFrame): DataFrame containing subject population data,
|
|
52
|
+
must include 'id' and 'group' columns.
|
|
53
|
+
id (str): The name of the subject ID column (e.g., "USUBJID").
|
|
54
|
+
group (str): The name of the treatment group column (e.g., "TRT01A").
|
|
55
|
+
total (bool, optional): If True, adds a 'Total' group with counts across all groups.
|
|
56
|
+
Defaults to True.
|
|
57
|
+
missing_group (str, optional): How to handle missing values in the group column.
|
|
58
|
+
"error" will raise a ValueError. Defaults to "error".
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
pl.DataFrame: A DataFrame with subject counts ('n_subj_pop') for each group.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
pop = _to_pop(
|
|
65
|
+
population=population,
|
|
66
|
+
id=id,
|
|
67
|
+
group=group,
|
|
68
|
+
total=total,
|
|
69
|
+
missing_group=missing_group,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return pop.group_by(group).agg(pl.len().alias("n_subj_pop")).sort(group)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def count_subject_with_observation(
|
|
76
|
+
population: pl.DataFrame,
|
|
77
|
+
observation: pl.DataFrame,
|
|
78
|
+
id: str,
|
|
79
|
+
group: str,
|
|
80
|
+
variable: str,
|
|
81
|
+
total: bool = True,
|
|
82
|
+
missing_group: str = "error",
|
|
83
|
+
pct_digit: int = 1,
|
|
84
|
+
max_n_width: int | None = None,
|
|
85
|
+
) -> pl.DataFrame:
|
|
86
|
+
"""
|
|
87
|
+
Counts subjects and observations by group and a specified variable,
|
|
88
|
+
calculating percentages based on population denominators.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
population (pl.DataFrame): DataFrame containing subject population data,
|
|
92
|
+
must include 'id' and 'group' columns.
|
|
93
|
+
observation (pl.DataFrame): DataFrame containing observation data,
|
|
94
|
+
must include 'id' and 'variable' columns.
|
|
95
|
+
id (str): The name of the subject ID column (e.g., "USUBJID").
|
|
96
|
+
group (str): The name of the treatment group column (e.g., "TRT01A").
|
|
97
|
+
variable (str): The name of the variable to count observations for (e.g., "AESOC").
|
|
98
|
+
total (bool, optional): Not yet implemented. Defaults to True.
|
|
99
|
+
missing_group (str, optional): How to handle missing values in the group column.
|
|
100
|
+
"error" will raise a ValueError. Defaults to "error".
|
|
101
|
+
pct_digit (int, optional): Number of decimal places for percentage formatting.
|
|
102
|
+
Defaults to 1.
|
|
103
|
+
max_n_width (int, optional): Fixed width for subject count formatting. If None, inferred
|
|
104
|
+
from data. Defaults to None.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
pl.DataFrame: A DataFrame with counts and percentages of subjects and observations
|
|
108
|
+
grouped by 'group' and 'variable'.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
# prepare data
|
|
112
|
+
pop = _to_pop(
|
|
113
|
+
population=population,
|
|
114
|
+
id=id,
|
|
115
|
+
group=group,
|
|
116
|
+
total=total,
|
|
117
|
+
missing_group=missing_group,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
obs = observation.select(id, variable).join(pop, on=id, how="left")
|
|
121
|
+
|
|
122
|
+
if not obs[id].is_in(pop[id].to_list()).all():
|
|
123
|
+
# Get IDs that are in obs but not in pop
|
|
124
|
+
missing_ids = (
|
|
125
|
+
obs.filter(~pl.col(id).is_in(pop[id].to_list()))
|
|
126
|
+
.select(id)
|
|
127
|
+
.unique()
|
|
128
|
+
.to_series()
|
|
129
|
+
.to_list()
|
|
130
|
+
)
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"Some '{id}' values in the observation DataFrame are not present in the population "
|
|
133
|
+
f"DataFrame: {missing_ids}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
df_pop = count_subject(
|
|
137
|
+
population=population,
|
|
138
|
+
id=id,
|
|
139
|
+
group=group,
|
|
140
|
+
total=total,
|
|
141
|
+
missing_group=missing_group,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Count observations and subjects by group and variable
|
|
145
|
+
df_obs_counts = obs.group_by(group, variable).agg(
|
|
146
|
+
pl.len().alias("n_obs"), pl.n_unique(id).alias("n_subj")
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Create all combinations of groups and variables to ensure no missing groups
|
|
150
|
+
unique_groups = df_pop.select(group)
|
|
151
|
+
unique_variables = obs.select(variable).unique()
|
|
152
|
+
|
|
153
|
+
# Cross join to get all combinations
|
|
154
|
+
all_combinations = unique_groups.join(unique_variables, how="cross")
|
|
155
|
+
|
|
156
|
+
# Left join to preserve all combinations, filling missing counts with 0
|
|
157
|
+
df_obs = (
|
|
158
|
+
all_combinations.join(df_obs_counts, on=[group, variable], how="left")
|
|
159
|
+
.join(df_pop, on=group, how="left")
|
|
160
|
+
.with_columns([pl.col("n_obs").fill_null(0), pl.col("n_subj").fill_null(0)])
|
|
161
|
+
.with_columns(pct_subj=(pl.col("n_subj") / pl.col("n_subj_pop") * 100))
|
|
162
|
+
.with_columns(
|
|
163
|
+
pct_subj_fmt=(
|
|
164
|
+
pl.when(pl.col("pct_subj").is_null() | pl.col("pct_subj").is_nan())
|
|
165
|
+
.then(0.0)
|
|
166
|
+
.otherwise(pl.col("pct_subj"))
|
|
167
|
+
.round(pct_digit, mode="half_away_from_zero")
|
|
168
|
+
.cast(pl.String)
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Calculate max widths for proper alignment
|
|
174
|
+
if max_n_width is None:
|
|
175
|
+
max_n_width = df_obs.select(pl.col("n_subj").cast(pl.String).str.len_chars().max()).item()
|
|
176
|
+
|
|
177
|
+
# Infer max percentage width from pct_digit
|
|
178
|
+
max_pct_width = 3 if pct_digit == 0 else 4 + pct_digit
|
|
179
|
+
|
|
180
|
+
# Format with padding for alignment
|
|
181
|
+
df_obs = (
|
|
182
|
+
df_obs.with_columns(
|
|
183
|
+
[
|
|
184
|
+
pl.col("pct_subj_fmt").str.pad_start(max_pct_width, " "),
|
|
185
|
+
pl.col("n_subj")
|
|
186
|
+
.cast(pl.String)
|
|
187
|
+
.str.pad_start(max_n_width, " ")
|
|
188
|
+
.alias("n_subj_fmt"),
|
|
189
|
+
]
|
|
190
|
+
)
|
|
191
|
+
.with_columns(
|
|
192
|
+
n_pct_subj_fmt=pl.concat_str(
|
|
193
|
+
[pl.col("n_subj_fmt"), pl.lit(" ("), pl.col("pct_subj_fmt"), pl.lit(")")]
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
.sort(group, variable)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return df_obs
|
csrlite/common/parse.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# pyre-strict
|
|
2
|
+
"""
|
|
3
|
+
StudyPlan Parsing Utilities
|
|
4
|
+
|
|
5
|
+
This module provides utilities for parsing and extracting information from StudyPlan objects,
|
|
6
|
+
including filter conversion, parameter parsing, and keyword resolution.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import polars as pl
|
|
13
|
+
|
|
14
|
+
from .plan import StudyPlan
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_filter_to_sql(filter_str: str) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Parse custom filter syntax to SQL WHERE clause.
|
|
20
|
+
|
|
21
|
+
Converts:
|
|
22
|
+
- "adsl:saffl == 'Y'" -> "SAFFL = 'Y'"
|
|
23
|
+
- "adae:trtemfl == 'Y' and adae:aeser == 'Y'" -> "TRTEMFL = 'Y' AND AESER = 'Y'"
|
|
24
|
+
- "adae:aerel in ['A', 'B']" -> "AEREL IN ('A', 'B')"
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
filter_str: Custom filter string with dataset:column format
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
SQL WHERE clause string
|
|
31
|
+
"""
|
|
32
|
+
if not filter_str or filter_str.strip() == "":
|
|
33
|
+
return "1=1" # Always true
|
|
34
|
+
|
|
35
|
+
# Remove dataset prefixes (adsl:, adae:)
|
|
36
|
+
sql = re.sub(r"\w+:", "", filter_str)
|
|
37
|
+
|
|
38
|
+
# Convert Python syntax to SQL
|
|
39
|
+
sql = sql.replace("==", "=") # Python equality to SQL
|
|
40
|
+
sql = sql.replace(" and ", " AND ") # Python to SQL
|
|
41
|
+
sql = sql.replace(" and ", " AND ") # Python to SQL
|
|
42
|
+
sql = sql.replace(" or ", " OR ") # Python to SQL
|
|
43
|
+
sql = sql.replace(" in ", " IN ") # Python to SQL
|
|
44
|
+
|
|
45
|
+
# Convert Python list syntax to SQL IN: ['A', 'B'] -> ('A', 'B')
|
|
46
|
+
sql = sql.replace("[", "(").replace("]", ")")
|
|
47
|
+
|
|
48
|
+
# Uppercase column names (assuming ADaM standard)
|
|
49
|
+
# Match word boundaries before operators
|
|
50
|
+
sql = re.sub(
|
|
51
|
+
r"\b([a-z]\w*)\b(?=\s*[=<>!]|\s+IN)", lambda m: m.group(1).upper(), sql, flags=re.IGNORECASE
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return sql
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def apply_filter_sql(df: pl.DataFrame, filter_str: str) -> pl.DataFrame:
|
|
58
|
+
"""
|
|
59
|
+
Apply filter using pl.sql_expr() - simpler and faster than SQLContext.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
df: DataFrame to filter
|
|
63
|
+
filter_str: Custom filter string
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Filtered DataFrame
|
|
67
|
+
"""
|
|
68
|
+
if not filter_str or filter_str.strip() == "":
|
|
69
|
+
return df
|
|
70
|
+
|
|
71
|
+
where_clause = parse_filter_to_sql(filter_str)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
# Use pl.sql_expr() - much simpler and faster!
|
|
75
|
+
return df.filter(pl.sql_expr(where_clause))
|
|
76
|
+
except Exception as e:
|
|
77
|
+
# Fallback to manual parsing if SQL fails
|
|
78
|
+
print(f"Warning: SQL filter failed ({e}), using fallback method")
|
|
79
|
+
return df.filter(_parse_filter_expr(filter_str))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _parse_filter_expr(filter_str: str) -> Any:
|
|
83
|
+
"""
|
|
84
|
+
Fallback filter parser using Polars expressions.
|
|
85
|
+
Used if SQL parsing fails.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
filter_str: Filter string
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Polars expression
|
|
92
|
+
"""
|
|
93
|
+
if not filter_str or filter_str.strip() == "":
|
|
94
|
+
return pl.lit(True)
|
|
95
|
+
|
|
96
|
+
# Remove dataset prefixes
|
|
97
|
+
filter_str = re.sub(r"\w+:", "", filter_str)
|
|
98
|
+
|
|
99
|
+
# Handle 'in' operator: column in ['A', 'B'] -> pl.col(column).is_in(['A', 'B'])
|
|
100
|
+
in_pattern = r"(\w+)\s+in\s+\[([^\]]+)\]"
|
|
101
|
+
|
|
102
|
+
def _parse_between(match: re.Match[str]) -> str:
|
|
103
|
+
col = match.group(1).upper()
|
|
104
|
+
values = match.group(2)
|
|
105
|
+
return f"(pl.col('{col}').is_in([{values}]))"
|
|
106
|
+
|
|
107
|
+
filter_str = re.sub(in_pattern, _parse_between, filter_str)
|
|
108
|
+
|
|
109
|
+
# Handle equality/inequality
|
|
110
|
+
eq_pattern = r"(\w+)\s*(==|!=|>|<|>=|<=)\s*'([^']+)'"
|
|
111
|
+
|
|
112
|
+
def _parse_like(match: re.Match[str]) -> str:
|
|
113
|
+
col = match.group(1).upper()
|
|
114
|
+
op = match.group(2)
|
|
115
|
+
val = match.group(3)
|
|
116
|
+
return f"(pl.col('{col}') {op} '{val}')"
|
|
117
|
+
|
|
118
|
+
filter_str = re.sub(eq_pattern, _parse_like, filter_str)
|
|
119
|
+
|
|
120
|
+
# Replace 'and'/'or'
|
|
121
|
+
filter_str = filter_str.replace(" and ", " & ")
|
|
122
|
+
filter_str = filter_str.replace(" or ", " | ")
|
|
123
|
+
|
|
124
|
+
return eval(filter_str)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def parse_parameter(parameter_str: str) -> list[str]:
|
|
128
|
+
"""
|
|
129
|
+
Parse semicolon-separated parameter string.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
parameter_str: Single parameter or semicolon-separated (e.g., "any;rel;ser")
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of parameter names
|
|
136
|
+
"""
|
|
137
|
+
if not parameter_str:
|
|
138
|
+
return []
|
|
139
|
+
if ";" in parameter_str:
|
|
140
|
+
return [p.strip() for p in parameter_str.split(";")]
|
|
141
|
+
return [parameter_str]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class StudyPlanParser:
|
|
145
|
+
"""
|
|
146
|
+
Parser class for extracting and resolving information from StudyPlan objects.
|
|
147
|
+
|
|
148
|
+
This class provides methods to extract filters, labels, and other configuration
|
|
149
|
+
from StudyPlan keywords and convert them to analysis-ready formats.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, study_plan: StudyPlan) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Initialize parser with a StudyPlan object.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
study_plan: StudyPlan object with loaded datasets and keywords
|
|
158
|
+
"""
|
|
159
|
+
self.study_plan = study_plan
|
|
160
|
+
|
|
161
|
+
def get_population_filter(self, population: str) -> str:
|
|
162
|
+
"""
|
|
163
|
+
Get population filter as SQL WHERE clause.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
population: Population keyword name
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
SQL WHERE clause string
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
ValueError: If population keyword not found
|
|
173
|
+
"""
|
|
174
|
+
pop = self.study_plan.keywords.get_population(population)
|
|
175
|
+
if pop is None:
|
|
176
|
+
raise ValueError(f"Population '{population}' not found")
|
|
177
|
+
return parse_filter_to_sql(pop.filter)
|
|
178
|
+
|
|
179
|
+
def get_observation_filter(self, observation: str | None) -> str | None:
|
|
180
|
+
"""
|
|
181
|
+
Get observation filter as SQL WHERE clause.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
observation: Optional observation keyword name
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
SQL WHERE clause string or None if observation not specified
|
|
188
|
+
"""
|
|
189
|
+
if not observation:
|
|
190
|
+
return None
|
|
191
|
+
obs = self.study_plan.keywords.get_observation(observation)
|
|
192
|
+
if obs:
|
|
193
|
+
return parse_filter_to_sql(obs.filter)
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def get_parameter_info(
|
|
197
|
+
self, parameter: str
|
|
198
|
+
) -> tuple[list[str], list[str], list[str], list[int]]:
|
|
199
|
+
"""
|
|
200
|
+
Get parameter names, filters, labels, and indent levels.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
parameter: Parameter keyword, can be semicolon-separated (e.g., "any;rel;ser")
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Tuple of (parameter_names, parameter_filters, parameter_labels, parameter_indents)
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
ValueError: If any parameter keyword not found
|
|
210
|
+
"""
|
|
211
|
+
param_names = parse_parameter(parameter)
|
|
212
|
+
param_labels = []
|
|
213
|
+
param_filters = []
|
|
214
|
+
param_indents = []
|
|
215
|
+
|
|
216
|
+
for param_name in param_names:
|
|
217
|
+
param = self.study_plan.keywords.get_parameter(param_name)
|
|
218
|
+
if param is None:
|
|
219
|
+
raise ValueError(f"Parameter '{param_name}' not found")
|
|
220
|
+
param_filters.append(parse_filter_to_sql(param.filter))
|
|
221
|
+
param_labels.append(param.label or param_name)
|
|
222
|
+
param_indents.append(param.indent)
|
|
223
|
+
|
|
224
|
+
return param_names, param_filters, param_labels, param_indents
|
|
225
|
+
|
|
226
|
+
def get_single_parameter_info(self, parameter: str) -> tuple[str, str]:
|
|
227
|
+
"""
|
|
228
|
+
Get single parameter filter and label (NOT semicolon-separated).
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
parameter: Single parameter keyword name
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Tuple of (parameter_filter, parameter_label)
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
ValueError: If parameter keyword not found
|
|
238
|
+
"""
|
|
239
|
+
param = self.study_plan.keywords.get_parameter(parameter)
|
|
240
|
+
if param is None:
|
|
241
|
+
raise ValueError(f"Parameter '{parameter}' not found")
|
|
242
|
+
return parse_filter_to_sql(param.filter), param.label or parameter
|
|
243
|
+
|
|
244
|
+
def get_group_info(self, group: str) -> tuple[str, list[str]]:
|
|
245
|
+
"""
|
|
246
|
+
Get group variable name and labels.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
group: Group keyword name
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Tuple of (group_variable, group_labels)
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
ValueError: If group keyword not found
|
|
256
|
+
"""
|
|
257
|
+
grp = self.study_plan.keywords.get_group(group)
|
|
258
|
+
if grp is None:
|
|
259
|
+
raise ValueError(f"Group '{group}' not found")
|
|
260
|
+
|
|
261
|
+
group_var = grp.variable.split(":")[-1].upper()
|
|
262
|
+
group_labels = grp.group_label if grp.group_label else []
|
|
263
|
+
|
|
264
|
+
return group_var, group_labels
|
|
265
|
+
|
|
266
|
+
def get_datasets(self, *dataset_names: str) -> tuple[pl.DataFrame, ...]:
|
|
267
|
+
"""
|
|
268
|
+
Get multiple datasets from StudyPlan.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
*dataset_names: Names of datasets to retrieve (e.g., "adsl", "adae")
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Tuple of DataFrames in the order requested
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
ValueError: If any dataset not found
|
|
278
|
+
"""
|
|
279
|
+
datasets = []
|
|
280
|
+
for name in dataset_names:
|
|
281
|
+
ds = self.study_plan.datasets.get(name)
|
|
282
|
+
if ds is None:
|
|
283
|
+
raise ValueError(f"Dataset '{name}' not found in study plan")
|
|
284
|
+
datasets.append(ds)
|
|
285
|
+
return tuple(datasets)
|
|
286
|
+
|
|
287
|
+
def get_population_data(self, population: str, group: str) -> tuple[pl.DataFrame, str]:
|
|
288
|
+
"""
|
|
289
|
+
Get filtered population dataset and group variable.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
population: Population keyword name
|
|
293
|
+
group: Group keyword name
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Tuple of (filtered_adsl, group_variable)
|
|
297
|
+
"""
|
|
298
|
+
# Get ADSL dataset
|
|
299
|
+
(adsl,) = self.get_datasets("adsl")
|
|
300
|
+
|
|
301
|
+
# Apply population filter
|
|
302
|
+
pop_filter = self.get_population_filter(population)
|
|
303
|
+
adsl_pop = apply_filter_sql(adsl, pop_filter)
|
|
304
|
+
|
|
305
|
+
# Get group variable
|
|
306
|
+
group_var, _ = self.get_group_info(group)
|
|
307
|
+
|
|
308
|
+
return adsl_pop, group_var
|