csrlite 0.2.0__tar.gz → 0.3.0__tar.gz
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-0.2.0/src/csrlite.egg-info → csrlite-0.3.0}/PKG-INFO +2 -2
- {csrlite-0.2.0 → csrlite-0.3.0}/pyproject.toml +10 -2
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/__init__.py +52 -0
- csrlite-0.3.0/src/csrlite/cm/cm_listing.py +497 -0
- csrlite-0.3.0/src/csrlite/cm/cm_summary.py +327 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/common/rtf.py +52 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/disposition/disposition.py +1 -1
- csrlite-0.3.0/src/csrlite/ie/ie_listing.py +109 -0
- csrlite-0.3.0/src/csrlite/ie/ie_summary.py +292 -0
- csrlite-0.3.0/src/csrlite/mh/mh_listing.py +209 -0
- csrlite-0.3.0/src/csrlite/mh/mh_summary.py +333 -0
- csrlite-0.3.0/src/csrlite/pd/pd_listing.py +461 -0
- {csrlite-0.2.0 → csrlite-0.3.0/src/csrlite.egg-info}/PKG-INFO +2 -2
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite.egg-info/SOURCES.txt +8 -1
- {csrlite-0.2.0 → csrlite-0.3.0}/MANIFEST.in +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/README.md +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/setup.cfg +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/ae/__init__.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/ae/ae_listing.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/ae/ae_specific.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/ae/ae_summary.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/ae/ae_utils.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/common/config.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/common/count.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/common/parse.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/common/plan.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/common/utils.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/common/yaml_loader.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite/disposition/__init__.py +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite.egg-info/dependency_links.txt +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite.egg-info/requires.txt +0 -0
- {csrlite-0.2.0 → csrlite-0.3.0}/src/csrlite.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csrlite
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A hierarchical YAML-based framework for generating Tables, Listings, and Figures in clinical trials
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: Yilong Zhang <elong0527@gmail.com>, Ming-Chun Chen <hellomingchun@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/elong0527/csrlite
|
|
8
8
|
Project-URL: Documentation, https://elong0527.github.io/csrlite
|
|
@@ -4,9 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "csrlite"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "A hierarchical YAML-based framework for generating Tables, Listings, and Figures in clinical trials"
|
|
9
|
-
authors = [
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Yilong Zhang", email = "elong0527@gmail.com"},
|
|
11
|
+
{name = "Ming-Chun Chen", email = "hellomingchun@gmail.com"}
|
|
12
|
+
]
|
|
10
13
|
license = {text = "MIT"}
|
|
11
14
|
readme = "README.md"
|
|
12
15
|
requires-python = ">=3.10"
|
|
@@ -150,6 +153,11 @@ ignore = []
|
|
|
150
153
|
dev = [
|
|
151
154
|
"jupyter>=1.1.1",
|
|
152
155
|
"jupyter-cache>=1.0.1",
|
|
156
|
+
"mypy>=1.19.0",
|
|
153
157
|
"nbclient>=0.10.2",
|
|
154
158
|
"notebook>=7.5.0",
|
|
159
|
+
"pyre-check>=0.9.25",
|
|
160
|
+
"pytest>=9.0.2",
|
|
161
|
+
"pytest-cov>=7.0.0",
|
|
162
|
+
"ruff>=0.14.8",
|
|
155
163
|
]
|
|
@@ -13,6 +13,14 @@ from .ae.ae_summary import ( # AE summary functions
|
|
|
13
13
|
ae_summary,
|
|
14
14
|
study_plan_to_ae_summary,
|
|
15
15
|
)
|
|
16
|
+
from .cm.cm_listing import ( # CM listing functions
|
|
17
|
+
cm_listing,
|
|
18
|
+
study_plan_to_cm_listing,
|
|
19
|
+
)
|
|
20
|
+
from .cm.cm_summary import (
|
|
21
|
+
cm_summary,
|
|
22
|
+
study_plan_to_cm_summary,
|
|
23
|
+
)
|
|
16
24
|
from .common.config import config
|
|
17
25
|
from .common.count import (
|
|
18
26
|
count_subject,
|
|
@@ -26,6 +34,29 @@ from .common.plan import ( # Core classes
|
|
|
26
34
|
load_plan,
|
|
27
35
|
)
|
|
28
36
|
from .disposition.disposition import study_plan_to_disposition_summary
|
|
37
|
+
from .ie.ie_listing import (
|
|
38
|
+
ie_listing_df,
|
|
39
|
+
ie_listing_rtf,
|
|
40
|
+
study_plan_to_ie_listing,
|
|
41
|
+
)
|
|
42
|
+
from .ie.ie_summary import (
|
|
43
|
+
ie_ard,
|
|
44
|
+
ie_df,
|
|
45
|
+
ie_rtf,
|
|
46
|
+
study_plan_to_ie_summary,
|
|
47
|
+
)
|
|
48
|
+
from .mh.mh_listing import (
|
|
49
|
+
mh_listing,
|
|
50
|
+
study_plan_to_mh_listing,
|
|
51
|
+
)
|
|
52
|
+
from .mh.mh_summary import (
|
|
53
|
+
mh_summary,
|
|
54
|
+
study_plan_to_mh_summary,
|
|
55
|
+
)
|
|
56
|
+
from .pd.pd_listing import (
|
|
57
|
+
pd_listing,
|
|
58
|
+
study_plan_to_pd_listing,
|
|
59
|
+
)
|
|
29
60
|
|
|
30
61
|
# Configure logging
|
|
31
62
|
logging.basicConfig(
|
|
@@ -47,6 +78,11 @@ __all__ = [
|
|
|
47
78
|
"study_plan_to_ae_summary",
|
|
48
79
|
"study_plan_to_ae_specific",
|
|
49
80
|
"study_plan_to_ae_listing",
|
|
81
|
+
# CM analysis
|
|
82
|
+
"cm_listing",
|
|
83
|
+
"study_plan_to_cm_listing",
|
|
84
|
+
"cm_summary",
|
|
85
|
+
"study_plan_to_cm_summary",
|
|
50
86
|
# Disposition analysis
|
|
51
87
|
"study_plan_to_disposition_summary",
|
|
52
88
|
# Count functions
|
|
@@ -55,4 +91,20 @@ __all__ = [
|
|
|
55
91
|
# Parse utilities
|
|
56
92
|
"StudyPlanParser",
|
|
57
93
|
"parse_filter_to_sql",
|
|
94
|
+
# IE analysis
|
|
95
|
+
"ie_ard",
|
|
96
|
+
"ie_df",
|
|
97
|
+
"ie_rtf",
|
|
98
|
+
"study_plan_to_ie_summary",
|
|
99
|
+
"ie_listing_df",
|
|
100
|
+
"ie_listing_rtf",
|
|
101
|
+
"study_plan_to_ie_listing",
|
|
102
|
+
# PD analysis
|
|
103
|
+
"pd_listing",
|
|
104
|
+
"study_plan_to_pd_listing",
|
|
105
|
+
# MH analysis
|
|
106
|
+
"mh_listing",
|
|
107
|
+
"study_plan_to_mh_listing",
|
|
108
|
+
"mh_summary",
|
|
109
|
+
"study_plan_to_mh_summary",
|
|
58
110
|
]
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
# pyre-strict
|
|
2
|
+
"""
|
|
3
|
+
Concomitant Medications (CM) Listing Functions
|
|
4
|
+
|
|
5
|
+
This module provides functions for generating detailed CM listings showing individual
|
|
6
|
+
medication records with key details like medication name, dose, route, etc.
|
|
7
|
+
|
|
8
|
+
The two-step pipeline:
|
|
9
|
+
- cm_listing_ard: Filter, select, sort, and rename columns (returns display-ready data)
|
|
10
|
+
- cm_listing_rtf: Generate formatted RTF output
|
|
11
|
+
- cm_listing: Complete pipeline wrapper
|
|
12
|
+
- study_plan_to_cm_listing: Batch generation from StudyPlan
|
|
13
|
+
|
|
14
|
+
Uses Polars native SQL capabilities for data manipulation and parse.py utilities
|
|
15
|
+
for StudyPlan parsing.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import polars as pl
|
|
22
|
+
from rtflite import RTFBody, RTFColumnHeader, RTFDocument, RTFFootnote, RTFPage, RTFSource, RTFTitle
|
|
23
|
+
|
|
24
|
+
from ..common.parse import StudyPlanParser
|
|
25
|
+
from ..common.plan import StudyPlan
|
|
26
|
+
from ..common.utils import apply_common_filters
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cm_listing_ard(
|
|
30
|
+
population: pl.DataFrame,
|
|
31
|
+
observation: pl.DataFrame,
|
|
32
|
+
population_filter: str | None,
|
|
33
|
+
observation_filter: str | None,
|
|
34
|
+
parameter_filter: str | None,
|
|
35
|
+
id: tuple[str, str],
|
|
36
|
+
population_columns: list[tuple[str, str]] | None = None,
|
|
37
|
+
observation_columns: list[tuple[str, str]] | None = None,
|
|
38
|
+
sort_columns: list[str] | None = None,
|
|
39
|
+
page_by: list[str] | None = None,
|
|
40
|
+
) -> pl.DataFrame:
|
|
41
|
+
"""
|
|
42
|
+
Generate Analysis Results Data (ARD) for CM listing.
|
|
43
|
+
|
|
44
|
+
Filters and joins population and observation data, then selects relevant columns.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
population: Population DataFrame (subject-level data, e.g., ADSL)
|
|
48
|
+
observation: Observation DataFrame (event data, e.g., ADCM)
|
|
49
|
+
population_filter: SQL WHERE clause for population (can be None)
|
|
50
|
+
observation_filter: SQL WHERE clause for observation (can be None)
|
|
51
|
+
parameter_filter: SQL WHERE clause for parameter filtering (can be None)
|
|
52
|
+
id: Tuple (variable_name, label) for ID column
|
|
53
|
+
population_columns: List of tuples (variable_name, label) from population
|
|
54
|
+
observation_columns: List of tuples (variable_name, label) from observation
|
|
55
|
+
sort_columns: List of column names to sort by. If None, sorts by id column.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
pl.DataFrame: Filtered and joined records with selected columns
|
|
59
|
+
"""
|
|
60
|
+
id_var_name, id_var_label = id
|
|
61
|
+
|
|
62
|
+
# Apply common filters
|
|
63
|
+
population_filtered, observation_to_filter = apply_common_filters(
|
|
64
|
+
population=population,
|
|
65
|
+
observation=observation,
|
|
66
|
+
population_filter=population_filter,
|
|
67
|
+
observation_filter=observation_filter,
|
|
68
|
+
parameter_filter=parameter_filter,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
assert observation_to_filter is not None
|
|
72
|
+
|
|
73
|
+
# Filter observation to include only subjects in filtered population
|
|
74
|
+
observation_filtered = observation_to_filter.filter(
|
|
75
|
+
pl.col(id_var_name).is_in(population_filtered[id_var_name].to_list())
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Determine which observation columns to select
|
|
79
|
+
if observation_columns is None:
|
|
80
|
+
# Default: select id column only
|
|
81
|
+
obs_cols = [id_var_name]
|
|
82
|
+
else:
|
|
83
|
+
# Extract variable names from tuples
|
|
84
|
+
obs_col_names = [var_name for var_name, _ in observation_columns]
|
|
85
|
+
# Ensure id is included
|
|
86
|
+
obs_cols = [id_var_name] + [col for col in obs_col_names if col != id_var_name]
|
|
87
|
+
|
|
88
|
+
# Select available observation columns
|
|
89
|
+
obs_cols_available = [col for col in obs_cols if col in observation_filtered.columns]
|
|
90
|
+
result = observation_filtered.select(obs_cols_available)
|
|
91
|
+
|
|
92
|
+
# Join with population to add population columns
|
|
93
|
+
if population_columns is not None:
|
|
94
|
+
# Extract variable names from tuples
|
|
95
|
+
pop_col_names = [var_name for var_name, _ in population_columns]
|
|
96
|
+
# Select id + requested population columns
|
|
97
|
+
pop_cols = [id_var_name] + [col for col in pop_col_names if col != id_var_name]
|
|
98
|
+
pop_cols_available = [col for col in pop_cols if col in population_filtered.columns]
|
|
99
|
+
population_subset = population_filtered.select(pop_cols_available)
|
|
100
|
+
|
|
101
|
+
# Left join to preserve all observation records
|
|
102
|
+
result = result.join(population_subset, on=id_var_name, how="left")
|
|
103
|
+
|
|
104
|
+
# Create __index__ column for pagination
|
|
105
|
+
# Default to using the id column as the index
|
|
106
|
+
if id_var_name in result.columns:
|
|
107
|
+
result = result.with_columns(
|
|
108
|
+
(pl.lit(f"{id_var_label} = ") + pl.col(id_var_name).cast(pl.Utf8)).alias("__index__")
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Use page_by columns if provided and they exist
|
|
112
|
+
existing_page_by_cols = [col for col in page_by if col in result.columns] if page_by else []
|
|
113
|
+
|
|
114
|
+
if existing_page_by_cols:
|
|
115
|
+
# Create a mapping from column name to label
|
|
116
|
+
column_labels = {id_var_name: id_var_label}
|
|
117
|
+
if population_columns:
|
|
118
|
+
for var_name, var_label in population_columns:
|
|
119
|
+
column_labels[var_name] = var_label
|
|
120
|
+
|
|
121
|
+
# Ensure the order of labels matches the order of columns in page_by
|
|
122
|
+
index_expressions = []
|
|
123
|
+
for col_name in existing_page_by_cols:
|
|
124
|
+
label = column_labels.get(col_name, col_name)
|
|
125
|
+
index_expressions.append(pl.lit(f"{label} = ") + pl.col(col_name).cast(pl.Utf8))
|
|
126
|
+
|
|
127
|
+
result = result.with_columns(
|
|
128
|
+
pl.concat_str(index_expressions, separator=", ").alias("__index__")
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
page_by_remove = [col for col in (page_by or []) if col != id_var_name]
|
|
132
|
+
result = result.drop(page_by_remove)
|
|
133
|
+
|
|
134
|
+
if "__index__" in result.columns:
|
|
135
|
+
# Get all columns except __index__
|
|
136
|
+
other_columns = [col for col in result.columns if col != "__index__"]
|
|
137
|
+
# Reorder to have __index__ first
|
|
138
|
+
result = result.select(["__index__"] + other_columns)
|
|
139
|
+
|
|
140
|
+
# Sort by specified columns or default to id column
|
|
141
|
+
if sort_columns is None:
|
|
142
|
+
# Default: sort by id column if it exists in result
|
|
143
|
+
if id_var_name in result.columns:
|
|
144
|
+
result = result.sort(id_var_name)
|
|
145
|
+
else:
|
|
146
|
+
# Sort by specified columns that exist in result
|
|
147
|
+
cols_to_sort = [col for col in sort_columns if col in result.columns]
|
|
148
|
+
if cols_to_sort:
|
|
149
|
+
result = result.sort(cols_to_sort)
|
|
150
|
+
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def cm_listing_rtf(
|
|
155
|
+
df: pl.DataFrame,
|
|
156
|
+
column_labels: dict[str, str],
|
|
157
|
+
title: list[str],
|
|
158
|
+
footnote: list[str] | None,
|
|
159
|
+
source: list[str] | None,
|
|
160
|
+
col_rel_width: list[float] | None = None,
|
|
161
|
+
group_by: list[str] | None = None,
|
|
162
|
+
page_by: list[str] | None = None,
|
|
163
|
+
orientation: str = "landscape",
|
|
164
|
+
) -> RTFDocument:
|
|
165
|
+
"""
|
|
166
|
+
Generate RTF table from CM listing display DataFrame.
|
|
167
|
+
|
|
168
|
+
Creates a formatted RTF table with column headers and optional section grouping/pagination.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
df: Display DataFrame from cm_listing_ard
|
|
172
|
+
column_labels: Dictionary mapping column names to display labels
|
|
173
|
+
title: Title(s) for the table as list of strings
|
|
174
|
+
footnote: Optional footnote(s) as list of strings
|
|
175
|
+
source: Optional source note(s) as list of strings
|
|
176
|
+
col_rel_width: Optional list of relative column widths. If None, auto-calculated
|
|
177
|
+
as equal widths for all columns
|
|
178
|
+
group_by: Optional list of column names to group by for section headers within pages.
|
|
179
|
+
Should only contain population columns (e.g., ["TRT01A", "USUBJID"])
|
|
180
|
+
page_by: Optional list of column names to trigger new pages when values change.
|
|
181
|
+
Should only contain population columns (e.g., ["TRT01A"])
|
|
182
|
+
orientation: Page orientation ("portrait" or "landscape"), default is "landscape"
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
RTFDocument: RTF document object that can be written to file
|
|
186
|
+
"""
|
|
187
|
+
# Calculate number of columns
|
|
188
|
+
n_cols = len(df.columns)
|
|
189
|
+
|
|
190
|
+
# Build column headers using labels
|
|
191
|
+
col_header = [column_labels.get(col, col) for col in df.columns]
|
|
192
|
+
|
|
193
|
+
# Calculate column widths
|
|
194
|
+
if col_rel_width is None:
|
|
195
|
+
col_widths = [1.0] * n_cols
|
|
196
|
+
else:
|
|
197
|
+
col_widths = col_rel_width
|
|
198
|
+
|
|
199
|
+
# Normalize title, footnote, source to lists
|
|
200
|
+
title_list = title
|
|
201
|
+
footnote_list: list[str] = footnote or []
|
|
202
|
+
source_list: list[str] = source or []
|
|
203
|
+
|
|
204
|
+
# Build RTF document
|
|
205
|
+
rtf_components: dict[str, Any] = {
|
|
206
|
+
"df": df,
|
|
207
|
+
"rtf_page": RTFPage(orientation=orientation),
|
|
208
|
+
"rtf_title": RTFTitle(text=title_list),
|
|
209
|
+
"rtf_column_header": [
|
|
210
|
+
RTFColumnHeader(
|
|
211
|
+
text=col_header[1:],
|
|
212
|
+
col_rel_width=col_widths[1:],
|
|
213
|
+
text_justification=["l"] + ["c"] * (n_cols - 1),
|
|
214
|
+
),
|
|
215
|
+
],
|
|
216
|
+
"rtf_body": RTFBody(
|
|
217
|
+
col_rel_width=col_widths,
|
|
218
|
+
text_justification=["l"] * n_cols,
|
|
219
|
+
border_left=["single"],
|
|
220
|
+
border_top=["single"] + [""] * (n_cols - 1),
|
|
221
|
+
border_bottom=["single"] + [""] * (n_cols - 1),
|
|
222
|
+
group_by=group_by,
|
|
223
|
+
page_by=page_by,
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# Add optional footnote
|
|
228
|
+
if footnote_list:
|
|
229
|
+
rtf_components["rtf_footnote"] = RTFFootnote(text=footnote_list)
|
|
230
|
+
|
|
231
|
+
# Add optional source
|
|
232
|
+
if source_list:
|
|
233
|
+
rtf_components["rtf_source"] = RTFSource(text=source_list)
|
|
234
|
+
|
|
235
|
+
# Create RTF document
|
|
236
|
+
doc = RTFDocument(**rtf_components)
|
|
237
|
+
|
|
238
|
+
return doc
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def cm_listing(
|
|
242
|
+
population: pl.DataFrame,
|
|
243
|
+
observation: pl.DataFrame,
|
|
244
|
+
population_filter: str | None,
|
|
245
|
+
observation_filter: str | None,
|
|
246
|
+
parameter_filter: str | None,
|
|
247
|
+
id: tuple[str, str],
|
|
248
|
+
title: list[str],
|
|
249
|
+
footnote: list[str] | None,
|
|
250
|
+
source: list[str] | None,
|
|
251
|
+
output_file: str,
|
|
252
|
+
population_columns: list[tuple[str, str]] | None = None,
|
|
253
|
+
observation_columns: list[tuple[str, str]] | None = None,
|
|
254
|
+
sort_columns: list[str] | None = None,
|
|
255
|
+
group_by: list[str] | None = None,
|
|
256
|
+
page_by: list[str] | None = None,
|
|
257
|
+
col_rel_width: list[float] | None = None,
|
|
258
|
+
orientation: str = "landscape",
|
|
259
|
+
) -> str:
|
|
260
|
+
"""
|
|
261
|
+
Complete CM listing pipeline wrapper.
|
|
262
|
+
|
|
263
|
+
This function orchestrates the two-step pipeline:
|
|
264
|
+
1. cm_listing_ard: Filter, join, select, and sort columns
|
|
265
|
+
2. cm_listing_rtf: Generate RTF output with optional grouping/pagination
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
population: Population DataFrame (subject-level data, e.g., ADSL)
|
|
269
|
+
observation: Observation DataFrame (event data, e.g., ADCM)
|
|
270
|
+
population_filter: SQL WHERE clause for population (can be None)
|
|
271
|
+
observation_filter: SQL WHERE clause for observation (can be None)
|
|
272
|
+
parameter_filter: SQL WHERE clause for parameter filtering (can be None)
|
|
273
|
+
id: Tuple (variable_name, label) for ID column
|
|
274
|
+
title: Title for RTF output as list of strings
|
|
275
|
+
footnote: Optional footnote for RTF output as list of strings
|
|
276
|
+
source: Optional source for RTF output as list of strings
|
|
277
|
+
output_file: File path to write RTF output
|
|
278
|
+
population_columns: Optional list of tuples (variable_name, label) from population
|
|
279
|
+
observation_columns: Optional list of tuples (variable_name, label) from observation
|
|
280
|
+
sort_columns: Optional list of column names to sort by. If None, sorts by id column.
|
|
281
|
+
group_by: Optional list of column names to group by for section headers
|
|
282
|
+
(population columns only)
|
|
283
|
+
page_by: Optional list of column names to trigger new pages (population columns only)
|
|
284
|
+
col_rel_width: Optional column widths for RTF output
|
|
285
|
+
orientation: Page orientation ("portrait" or "landscape"), default is "landscape"
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
str: Path to the generated RTF file
|
|
289
|
+
"""
|
|
290
|
+
# Step 1: Generate ARD (includes filtering, joining, and selecting)
|
|
291
|
+
df = cm_listing_ard(
|
|
292
|
+
population=population,
|
|
293
|
+
observation=observation,
|
|
294
|
+
population_filter=population_filter,
|
|
295
|
+
observation_filter=observation_filter,
|
|
296
|
+
parameter_filter=parameter_filter,
|
|
297
|
+
id=id,
|
|
298
|
+
population_columns=population_columns,
|
|
299
|
+
observation_columns=observation_columns,
|
|
300
|
+
sort_columns=sort_columns,
|
|
301
|
+
page_by=page_by,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Build column labels from tuples
|
|
305
|
+
id_var_name, id_var_label = id
|
|
306
|
+
column_labels = {id_var_name: id_var_label}
|
|
307
|
+
|
|
308
|
+
# Add observation column labels
|
|
309
|
+
if observation_columns is not None:
|
|
310
|
+
for var_name, var_label in observation_columns:
|
|
311
|
+
column_labels[var_name] = var_label
|
|
312
|
+
|
|
313
|
+
# Add population column labels
|
|
314
|
+
if population_columns is not None:
|
|
315
|
+
for var_name, var_label in population_columns:
|
|
316
|
+
column_labels[var_name] = var_label
|
|
317
|
+
|
|
318
|
+
# Set __index__ header to empty string
|
|
319
|
+
column_labels["__index__"] = ""
|
|
320
|
+
|
|
321
|
+
# Step 2: Generate RTF and write to file
|
|
322
|
+
rtf_doc = cm_listing_rtf(
|
|
323
|
+
df=df,
|
|
324
|
+
column_labels=column_labels,
|
|
325
|
+
title=title,
|
|
326
|
+
footnote=footnote,
|
|
327
|
+
source=source,
|
|
328
|
+
col_rel_width=col_rel_width,
|
|
329
|
+
group_by=group_by,
|
|
330
|
+
page_by=["__index__"],
|
|
331
|
+
orientation=orientation,
|
|
332
|
+
)
|
|
333
|
+
rtf_doc.write_rtf(output_file)
|
|
334
|
+
|
|
335
|
+
return output_file
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def study_plan_to_cm_listing(
|
|
339
|
+
study_plan: StudyPlan,
|
|
340
|
+
) -> list[str]:
|
|
341
|
+
"""
|
|
342
|
+
Generate CM listing RTF outputs for all analyses defined in StudyPlan.
|
|
343
|
+
|
|
344
|
+
This function reads the expanded plan from StudyPlan and generates
|
|
345
|
+
an RTF listing for each cm_listing analysis specification automatically.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
study_plan: StudyPlan object with loaded datasets and analysis specifications
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
list[str]: List of paths to generated RTF files
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
# Meta data
|
|
355
|
+
analysis = "cm_listing"
|
|
356
|
+
output_dir = study_plan.output_dir
|
|
357
|
+
# Auto-adjust column widths based on typical content
|
|
358
|
+
col_rel_width = [1.0] * 12 # Estimate based on number of columns
|
|
359
|
+
footnote = None
|
|
360
|
+
source = None
|
|
361
|
+
|
|
362
|
+
population_df_name = "adsl"
|
|
363
|
+
observation_df_name = "adcm"
|
|
364
|
+
|
|
365
|
+
id = ("USUBJID", "Subject ID")
|
|
366
|
+
# Column configuration with labels - easy to customize
|
|
367
|
+
# Population columns (demographics) - group variable will be added dynamically
|
|
368
|
+
population_columns_base = [
|
|
369
|
+
("AGE", "Age"),
|
|
370
|
+
("SEX", "Sex"),
|
|
371
|
+
("RACE", "Race"),
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
# Observation columns (event details)
|
|
375
|
+
# Adjusted based on available columns in adcm.parquet
|
|
376
|
+
observation_columns_base = [
|
|
377
|
+
("CMSEQ", "Sequence"),
|
|
378
|
+
("CMCAT", "Category"),
|
|
379
|
+
("ASTDT", "Start Date"),
|
|
380
|
+
("AENDT", "End Date"),
|
|
381
|
+
("CMINDC", "Indication"),
|
|
382
|
+
("CMTRT", "Medication"),
|
|
383
|
+
("CMDECOD", "Standardized Name"),
|
|
384
|
+
("CMDOSE", "Dose"),
|
|
385
|
+
("CMDOSU", "Unit"),
|
|
386
|
+
("CMROUTE", "Route"),
|
|
387
|
+
("CMFREQ", "Frequency"),
|
|
388
|
+
("CMENDPT", "End Point"),
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
# Sorting configuration
|
|
392
|
+
sort_columns = ["TRT01A", "USUBJID", "ASTDT", "CMSEQ"]
|
|
393
|
+
page_by = ["USUBJID", "SEX", "RACE", "AGE", "TRT01A"]
|
|
394
|
+
group_by = ["USUBJID"]
|
|
395
|
+
|
|
396
|
+
# Create output directory if it doesn't exist
|
|
397
|
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
398
|
+
|
|
399
|
+
# Initialize parser
|
|
400
|
+
parser = StudyPlanParser(study_plan)
|
|
401
|
+
|
|
402
|
+
# Get expanded plan DataFrame
|
|
403
|
+
plan_df = study_plan.get_plan_df()
|
|
404
|
+
|
|
405
|
+
# Filter for CM listing analyses
|
|
406
|
+
cm_plans = plan_df.filter(pl.col("analysis") == analysis)
|
|
407
|
+
|
|
408
|
+
rtf_files = []
|
|
409
|
+
|
|
410
|
+
# Generate RTF for each analysis
|
|
411
|
+
for row in cm_plans.iter_rows(named=True):
|
|
412
|
+
population = row["population"]
|
|
413
|
+
observation = row.get("observation")
|
|
414
|
+
parameter = row.get("parameter")
|
|
415
|
+
group = row.get("group")
|
|
416
|
+
|
|
417
|
+
# Validate group is specified
|
|
418
|
+
if group is None:
|
|
419
|
+
raise ValueError(
|
|
420
|
+
f"Group not specified in YAML for analysis: population={population}, "
|
|
421
|
+
f"observation={observation}, parameter={parameter}. "
|
|
422
|
+
"Please add group to your YAML plan."
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Get datasets using parser
|
|
426
|
+
population_df, observation_df = parser.get_datasets(population_df_name, observation_df_name)
|
|
427
|
+
|
|
428
|
+
# Get filters using parser
|
|
429
|
+
population_filter = parser.get_population_filter(population)
|
|
430
|
+
obs_filter = parser.get_observation_filter(observation)
|
|
431
|
+
|
|
432
|
+
# Get group variable name from YAML
|
|
433
|
+
group_var_name, group_labels = parser.get_group_info(group)
|
|
434
|
+
|
|
435
|
+
# Determine group variable label
|
|
436
|
+
group_var_label = group_labels[0] if group_labels else "Treatment"
|
|
437
|
+
|
|
438
|
+
# Build columns dynamically from base configuration with labels
|
|
439
|
+
population_columns = population_columns_base + [(group_var_name, group_var_label)]
|
|
440
|
+
observation_columns = observation_columns_base
|
|
441
|
+
|
|
442
|
+
# Dynamic sort and page configuration
|
|
443
|
+
sort_columns = [group_var_name, "USUBJID", "ASTDT", "CMSEQ"]
|
|
444
|
+
page_by = ["USUBJID", "SEX", "RACE", "AGE", group_var_name]
|
|
445
|
+
|
|
446
|
+
# Get parameter filter if parameter is specified
|
|
447
|
+
parameter_filter = None
|
|
448
|
+
if parameter:
|
|
449
|
+
# Assuming similar parameter structure, though CM might just use subsets
|
|
450
|
+
param_names, param_filters, param_labels, _ = parser.get_parameter_info(parameter)
|
|
451
|
+
# For cm_listing, use the first filter
|
|
452
|
+
parameter_filter = param_filters[0] if param_filters else None
|
|
453
|
+
|
|
454
|
+
# Build title
|
|
455
|
+
title_parts = ["Listing of Concomitant Medications"]
|
|
456
|
+
|
|
457
|
+
if observation:
|
|
458
|
+
obs_kw = study_plan.keywords.observations.get(observation)
|
|
459
|
+
if obs_kw and obs_kw.label:
|
|
460
|
+
title_parts.append(obs_kw.label)
|
|
461
|
+
|
|
462
|
+
pop_kw = study_plan.keywords.populations.get(population)
|
|
463
|
+
if pop_kw and pop_kw.label:
|
|
464
|
+
title_parts.append(pop_kw.label)
|
|
465
|
+
|
|
466
|
+
# Build output filename
|
|
467
|
+
filename = f"{analysis}_{population}"
|
|
468
|
+
if observation:
|
|
469
|
+
filename += f"_{observation}"
|
|
470
|
+
if parameter:
|
|
471
|
+
filename += f"_{parameter.replace(';', '_')}"
|
|
472
|
+
filename += ".rtf"
|
|
473
|
+
output_file = str(Path(output_dir) / filename)
|
|
474
|
+
|
|
475
|
+
# Generate RTF
|
|
476
|
+
rtf_path = cm_listing(
|
|
477
|
+
population=population_df,
|
|
478
|
+
observation=observation_df,
|
|
479
|
+
population_filter=population_filter,
|
|
480
|
+
observation_filter=obs_filter,
|
|
481
|
+
parameter_filter=parameter_filter,
|
|
482
|
+
id=id,
|
|
483
|
+
title=title_parts,
|
|
484
|
+
footnote=footnote,
|
|
485
|
+
source=source,
|
|
486
|
+
output_file=output_file,
|
|
487
|
+
population_columns=population_columns,
|
|
488
|
+
observation_columns=observation_columns,
|
|
489
|
+
sort_columns=sort_columns,
|
|
490
|
+
col_rel_width=col_rel_width,
|
|
491
|
+
group_by=group_by,
|
|
492
|
+
page_by=page_by,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
rtf_files.append(rtf_path)
|
|
496
|
+
|
|
497
|
+
return rtf_files
|