csrlite 0.3.0__py3-none-any.whl → 0.3.2__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 +91 -110
- csrlite/ae/__init__.py +1 -1
- csrlite/ae/ae_listing.py +494 -494
- csrlite/ae/ae_specific.py +483 -483
- csrlite/ae/ae_summary.py +401 -401
- csrlite/ae/ae_utils.py +62 -62
- csrlite/cm/cm_listing.py +497 -497
- csrlite/cm/cm_summary.py +327 -327
- csrlite/common/config.py +34 -34
- csrlite/common/count.py +293 -293
- csrlite/common/parse.py +308 -308
- csrlite/common/plan.py +365 -365
- csrlite/common/rtf.py +166 -137
- csrlite/common/utils.py +33 -33
- csrlite/common/yaml_loader.py +71 -71
- csrlite/disposition/__init__.py +2 -2
- csrlite/disposition/disposition.py +332 -332
- csrlite/ie/{ie_summary.py → ie.py} +405 -292
- csrlite/pd/pd_listing.py +461 -461
- {csrlite-0.3.0.dist-info → csrlite-0.3.2.dist-info}/METADATA +68 -68
- csrlite-0.3.2.dist-info/RECORD +23 -0
- {csrlite-0.3.0.dist-info → csrlite-0.3.2.dist-info}/WHEEL +1 -1
- csrlite/ie/ie_listing.py +0 -109
- csrlite/mh/mh_listing.py +0 -209
- csrlite/mh/mh_summary.py +0 -333
- csrlite-0.3.0.dist-info/RECORD +0 -26
- {csrlite-0.3.0.dist-info → csrlite-0.3.2.dist-info}/top_level.txt +0 -0
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: csrlite
|
|
3
|
-
Version: 0.3.
|
|
4
|
-
Summary: A hierarchical YAML-based framework for generating Tables, Listings, and Figures in clinical trials
|
|
5
|
-
Author-email: Yilong Zhang <elong0527@gmail.com>, Ming-Chun Chen <hellomingchun@gmail.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/elong0527/csrlite
|
|
8
|
-
Project-URL: Documentation, https://elong0527.github.io/csrlite
|
|
9
|
-
Project-URL: Repository, https://github.com/elong0527/csrlite.git
|
|
10
|
-
Project-URL: Bug Tracker, https://github.com/elong0527/csrlite/issues
|
|
11
|
-
Keywords: clinical-trials,biostatistics,yaml,tlf,tables,listings,figures
|
|
12
|
-
Classifier: Development Status :: 3 - Alpha
|
|
13
|
-
Classifier: Intended Audience :: Science/Research
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
-
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
19
|
-
Requires-Python: >=3.10
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: pydantic>=2.0.0
|
|
22
|
-
Requires-Dist: pyyaml>=6.0
|
|
23
|
-
Requires-Dist: polars>=0.20.0
|
|
24
|
-
Requires-Dist: rtflite>=2.1.1
|
|
25
|
-
Provides-Extra: rtf
|
|
26
|
-
Requires-Dist: rtflite; extra == "rtf"
|
|
27
|
-
Provides-Extra: plotting
|
|
28
|
-
Requires-Dist: matplotlib>=3.5.0; extra == "plotting"
|
|
29
|
-
Requires-Dist: plotly>=5.0.0; extra == "plotting"
|
|
30
|
-
Provides-Extra: dev
|
|
31
|
-
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
32
|
-
Requires-Dist: pytest>=9.0.1; extra == "dev"
|
|
33
|
-
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
34
|
-
Requires-Dist: isort>=7.0.0; extra == "dev"
|
|
35
|
-
Requires-Dist: ruff>=0.14.8; extra == "dev"
|
|
36
|
-
Requires-Dist: mypy>=1.19.0; extra == "dev"
|
|
37
|
-
Requires-Dist: quarto>=0.1.0; extra == "dev"
|
|
38
|
-
Requires-Dist: pyre-check>=0.9.18; extra == "dev"
|
|
39
|
-
Requires-Dist: jupyter>=1.1.1; extra == "dev"
|
|
40
|
-
Requires-Dist: jupyter-cache>=1.0.1; extra == "dev"
|
|
41
|
-
Requires-Dist: nbformat>=5.10.4; extra == "dev"
|
|
42
|
-
Provides-Extra: all
|
|
43
|
-
Requires-Dist: rtflite; extra == "all"
|
|
44
|
-
Requires-Dist: matplotlib>=3.5.0; extra == "all"
|
|
45
|
-
Requires-Dist: plotly>=5.0.0; extra == "all"
|
|
46
|
-
|
|
47
|
-
# csrlite
|
|
48
|
-
|
|
49
|
-
[](https://github.com/elong0527/csrlite/actions/workflows/ci.yml)
|
|
50
|
-
[](https://codecov.io/gh/elong0527/csrlite)
|
|
51
|
-
[](https://badge.fury.io/py/csrlite)
|
|
52
|
-
[](https://www.python.org/downloads/)
|
|
53
|
-
|
|
54
|
-
A hierarchical YAML-based framework for generating Tables, Listings, and Figures in clinical trials.
|
|
55
|
-
|
|
56
|
-
## Installation
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
pip install csrlite
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Documentation
|
|
63
|
-
|
|
64
|
-
Visit [https://elong0527.github.io/csrlite](https://elong0527.github.io/csrlite) for full documentation.
|
|
65
|
-
|
|
66
|
-
## License
|
|
67
|
-
|
|
68
|
-
MIT License - see [LICENSE](LICENSE) file for details.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csrlite
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: A hierarchical YAML-based framework for generating Tables, Listings, and Figures in clinical trials
|
|
5
|
+
Author-email: Yilong Zhang <elong0527@gmail.com>, Ming-Chun Chen <hellomingchun@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/elong0527/csrlite
|
|
8
|
+
Project-URL: Documentation, https://elong0527.github.io/csrlite
|
|
9
|
+
Project-URL: Repository, https://github.com/elong0527/csrlite.git
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/elong0527/csrlite/issues
|
|
11
|
+
Keywords: clinical-trials,biostatistics,yaml,tlf,tables,listings,figures
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: pydantic>=2.0.0
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: polars>=0.20.0
|
|
24
|
+
Requires-Dist: rtflite>=2.1.1
|
|
25
|
+
Provides-Extra: rtf
|
|
26
|
+
Requires-Dist: rtflite; extra == "rtf"
|
|
27
|
+
Provides-Extra: plotting
|
|
28
|
+
Requires-Dist: matplotlib>=3.5.0; extra == "plotting"
|
|
29
|
+
Requires-Dist: plotly>=5.0.0; extra == "plotting"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest>=9.0.1; extra == "dev"
|
|
33
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: isort>=7.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.14.8; extra == "dev"
|
|
36
|
+
Requires-Dist: mypy>=1.19.0; extra == "dev"
|
|
37
|
+
Requires-Dist: quarto>=0.1.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pyre-check>=0.9.18; extra == "dev"
|
|
39
|
+
Requires-Dist: jupyter>=1.1.1; extra == "dev"
|
|
40
|
+
Requires-Dist: jupyter-cache>=1.0.1; extra == "dev"
|
|
41
|
+
Requires-Dist: nbformat>=5.10.4; extra == "dev"
|
|
42
|
+
Provides-Extra: all
|
|
43
|
+
Requires-Dist: rtflite; extra == "all"
|
|
44
|
+
Requires-Dist: matplotlib>=3.5.0; extra == "all"
|
|
45
|
+
Requires-Dist: plotly>=5.0.0; extra == "all"
|
|
46
|
+
|
|
47
|
+
# csrlite
|
|
48
|
+
|
|
49
|
+
[](https://github.com/elong0527/csrlite/actions/workflows/ci.yml)
|
|
50
|
+
[](https://codecov.io/gh/elong0527/csrlite)
|
|
51
|
+
[](https://badge.fury.io/py/csrlite)
|
|
52
|
+
[](https://www.python.org/downloads/)
|
|
53
|
+
|
|
54
|
+
A hierarchical YAML-based framework for generating Tables, Listings, and Figures in clinical trials.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install csrlite
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Documentation
|
|
63
|
+
|
|
64
|
+
Visit [https://elong0527.github.io/csrlite](https://elong0527.github.io/csrlite) for full documentation.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
csrlite/__init__.py,sha256=TotlI9TWnCkOxP8OOG5AfTMKjWLhM2-AJOEA6YF5ZLQ,2190
|
|
2
|
+
csrlite/ae/__init__.py,sha256=Pp661SGbsoYoXLmd24dUQB_lx1jYKWEsCEPjgVg5UUU,15
|
|
3
|
+
csrlite/ae/ae_listing.py,sha256=fZWhYmi_Vzp974z-3o7EQ9joizEjF0mio2eB5_I8ghU,18883
|
|
4
|
+
csrlite/ae/ae_specific.py,sha256=P1glCd5K_ur_KexxbVaC79BmowgQSMI9K8xdzL6OmIM,17505
|
|
5
|
+
csrlite/ae/ae_summary.py,sha256=Vk4bUonnjAji2zeJkiPHW7nwgpwUG1UfbuzH4M0w1MM,14093
|
|
6
|
+
csrlite/ae/ae_utils.py,sha256=tOVOdsj-TRTw1w5T4JqFTYUwTfOppZWGlRHi6nkNWRA,2073
|
|
7
|
+
csrlite/cm/cm_listing.py,sha256=VRgEdwWNlgjqU5W_-ObAxUwqapDf2G44xffwDiybIa4,18904
|
|
8
|
+
csrlite/cm/cm_summary.py,sha256=DDWvubtENtf_Hr537xyGjYP2MOxS-meLs-yInFRynXM,10479
|
|
9
|
+
csrlite/common/config.py,sha256=qWFTNSaeWnqAagXM6FwACdQafXLG_nFWxFHEk5dAo-I,912
|
|
10
|
+
csrlite/common/count.py,sha256=T9nB9hYdc2jIIuV9HISK2ZS0wXNV_FUBDCKW_VVM1fQ,9078
|
|
11
|
+
csrlite/common/parse.py,sha256=J4UkmNlG10mrJ-O68-po1LlfLchK_kSvfdU1GOaAYgQ,9627
|
|
12
|
+
csrlite/common/plan.py,sha256=tYnK-5TVCoGWobxkqmasYYYkc9DJlHmOiTIcrP3MFRo,13293
|
|
13
|
+
csrlite/common/rtf.py,sha256=fbube-5QoWYE8u6GCjE5qCMS6BzmXqJhoZI8GpOYXJM,5610
|
|
14
|
+
csrlite/common/utils.py,sha256=XLUQqYtEm4SLNImvNDxAuj9OOu6Eogm3_uIp0vyfpfQ,1143
|
|
15
|
+
csrlite/common/yaml_loader.py,sha256=ZPgXw5X9eitMHCPEJBHzeYtcjesEQ8sP0hOFSYnzrgk,3142
|
|
16
|
+
csrlite/disposition/__init__.py,sha256=AiLW7Os7SBFrvvkaUMrxYEntK15_XXT0HYpZQxBx7ro,87
|
|
17
|
+
csrlite/disposition/disposition.py,sha256=W1Oik5hqDfalQ69NXullH7XYWP3oRw2x2nFuFWlE3Qo,10679
|
|
18
|
+
csrlite/ie/ie.py,sha256=9kEAe4aY3xfHe7O-o2ZD3LNfL5eTha2ZPkAflopjPYU,13544
|
|
19
|
+
csrlite/pd/pd_listing.py,sha256=xPqpJFuxx_7vzjADGDSBo7RK2GpGWpR6SIMnCjKNirI,17632
|
|
20
|
+
csrlite-0.3.2.dist-info/METADATA,sha256=RDlBzyKYUN2AGe_k-ZOOB0USa6g-qdl4OZPa-FwJFGk,2896
|
|
21
|
+
csrlite-0.3.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
22
|
+
csrlite-0.3.2.dist-info/top_level.txt,sha256=59zJTvGH5zx2FY4vCl4kgnH8awT0cZrg21Mace7IFlU,8
|
|
23
|
+
csrlite-0.3.2.dist-info/RECORD,,
|
csrlite/ie/ie_listing.py
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# pyre-strict
|
|
2
|
-
"""
|
|
3
|
-
Inclusion/Exclusion (IE) Listing Analysis Functions
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
import polars as pl
|
|
9
|
-
|
|
10
|
-
from ..common.parse import StudyPlanParser
|
|
11
|
-
from ..common.plan import StudyPlan
|
|
12
|
-
from ..common.rtf import create_rtf_listing
|
|
13
|
-
from ..common.utils import apply_common_filters
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def study_plan_to_ie_listing(
|
|
17
|
-
study_plan: StudyPlan,
|
|
18
|
-
) -> list[str]:
|
|
19
|
-
"""
|
|
20
|
-
Generate IE Listing outputs.
|
|
21
|
-
"""
|
|
22
|
-
# Meta data
|
|
23
|
-
analysis_type = "ie_listing"
|
|
24
|
-
output_dir = study_plan.output_dir
|
|
25
|
-
title = "Listing of Protocol Deviations"
|
|
26
|
-
|
|
27
|
-
# Ensure output directory exists
|
|
28
|
-
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
29
|
-
|
|
30
|
-
# Initialize parser
|
|
31
|
-
parser = StudyPlanParser(study_plan)
|
|
32
|
-
|
|
33
|
-
# Get expanded plan (Manually expansion to avoid AttributeError)
|
|
34
|
-
plans = study_plan.study_data.get("plans", [])
|
|
35
|
-
all_specs = []
|
|
36
|
-
for plan_data in plans:
|
|
37
|
-
expanded = study_plan.expander.expand_plan(plan_data)
|
|
38
|
-
for p in expanded:
|
|
39
|
-
all_specs.append(study_plan.expander.create_analysis_spec(p))
|
|
40
|
-
|
|
41
|
-
plan_df = pl.DataFrame(all_specs)
|
|
42
|
-
|
|
43
|
-
if "analysis" in plan_df.columns:
|
|
44
|
-
listing_plans = plan_df.filter(pl.col("analysis") == analysis_type)
|
|
45
|
-
else:
|
|
46
|
-
listing_plans = pl.DataFrame()
|
|
47
|
-
|
|
48
|
-
generated_files = []
|
|
49
|
-
|
|
50
|
-
# If listing_plans is empty, create a dummy row to force generation
|
|
51
|
-
if listing_plans.height == 0:
|
|
52
|
-
listing_plans = pl.DataFrame([{"population": "enrolled", "analysis": analysis_type}])
|
|
53
|
-
|
|
54
|
-
for analysis in listing_plans.iter_rows(named=True):
|
|
55
|
-
# Load ADSL
|
|
56
|
-
pop_name = analysis.get("population", "enrolled")
|
|
57
|
-
|
|
58
|
-
try:
|
|
59
|
-
(adsl_raw,) = parser.get_datasets("adsl")
|
|
60
|
-
pop_filter = parser.get_population_filter(pop_name)
|
|
61
|
-
|
|
62
|
-
adsl, _ = apply_common_filters(
|
|
63
|
-
population=adsl_raw,
|
|
64
|
-
observation=None,
|
|
65
|
-
population_filter=pop_filter,
|
|
66
|
-
observation_filter=None,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
except ValueError as e:
|
|
70
|
-
print(f"Error loading population: {e}")
|
|
71
|
-
continue
|
|
72
|
-
|
|
73
|
-
# Output filename
|
|
74
|
-
filename = f"{analysis_type}_{pop_name}.rtf".lower()
|
|
75
|
-
output_path = f"{output_dir}/{filename}"
|
|
76
|
-
|
|
77
|
-
# Generate DF
|
|
78
|
-
df = ie_listing_df(adsl)
|
|
79
|
-
|
|
80
|
-
# Generate RTF
|
|
81
|
-
ie_listing_rtf(df, output_path, title=title)
|
|
82
|
-
|
|
83
|
-
generated_files.append(output_path)
|
|
84
|
-
|
|
85
|
-
return generated_files
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def ie_listing_df(adsl: pl.DataFrame) -> pl.DataFrame:
|
|
89
|
-
"""Select columns for Listing."""
|
|
90
|
-
# Check if DCSREAS exists
|
|
91
|
-
cols = ["USUBJID", "DCSREAS"]
|
|
92
|
-
available = [c for c in cols if c in adsl.columns]
|
|
93
|
-
return adsl.select(available)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def ie_listing_rtf(df: pl.DataFrame, output_path: str, title: str | list[str] = "") -> None:
|
|
97
|
-
"""Generate RTF Listing."""
|
|
98
|
-
col_widths = [1.5, 3.5] # Approximate ratio
|
|
99
|
-
|
|
100
|
-
rtf_doc = create_rtf_listing(
|
|
101
|
-
df=df,
|
|
102
|
-
col_header=list(df.columns),
|
|
103
|
-
col_widths=col_widths,
|
|
104
|
-
title=title,
|
|
105
|
-
footnote=[],
|
|
106
|
-
source=[],
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
rtf_doc.write_rtf(output_path)
|
csrlite/mh/mh_listing.py
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
# pyre-strict
|
|
2
|
-
"""
|
|
3
|
-
Medical History (MH) Listing Analysis Functions
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
import polars as pl
|
|
9
|
-
|
|
10
|
-
from ..common.parse import StudyPlanParser
|
|
11
|
-
from ..common.plan import StudyPlan
|
|
12
|
-
from ..common.rtf import create_rtf_listing
|
|
13
|
-
from ..common.utils import apply_common_filters
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def mh_listing(
|
|
17
|
-
population: pl.DataFrame,
|
|
18
|
-
observation: pl.DataFrame,
|
|
19
|
-
population_filter: str | None = "SAFFL = 'Y'",
|
|
20
|
-
observation_filter: str | None = "MHOCCUR = 'Y'",
|
|
21
|
-
id: tuple[str, str] = ("USUBJID", "Subject ID"),
|
|
22
|
-
title: list[str] | None = None,
|
|
23
|
-
footnote: list[str] | None = None,
|
|
24
|
-
source: list[str] | None = None,
|
|
25
|
-
output_file: str = "mh_listing.rtf",
|
|
26
|
-
population_columns: list[tuple[str, str]] | None = None,
|
|
27
|
-
observation_columns: list[tuple[str, str]] | None = None,
|
|
28
|
-
sort_columns: list[str] | None = None,
|
|
29
|
-
) -> str:
|
|
30
|
-
"""
|
|
31
|
-
Generate Medical History Listing.
|
|
32
|
-
"""
|
|
33
|
-
if title is None:
|
|
34
|
-
title = ["Listing of Medical History"]
|
|
35
|
-
|
|
36
|
-
# Generate DF
|
|
37
|
-
df = mh_listing_df(
|
|
38
|
-
population=population,
|
|
39
|
-
observation=observation,
|
|
40
|
-
population_filter=population_filter,
|
|
41
|
-
observation_filter=observation_filter,
|
|
42
|
-
id_col=id[0],
|
|
43
|
-
pop_cols=population_columns,
|
|
44
|
-
obs_cols=observation_columns,
|
|
45
|
-
sort_cols=sort_columns,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
# Generate RTF
|
|
49
|
-
mh_listing_rtf(df=df, output_path=output_file, title=title, footnote=footnote, source=source)
|
|
50
|
-
|
|
51
|
-
return output_file
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def mh_listing_df(
|
|
55
|
-
population: pl.DataFrame,
|
|
56
|
-
observation: pl.DataFrame,
|
|
57
|
-
population_filter: str | None,
|
|
58
|
-
observation_filter: str | None,
|
|
59
|
-
id_col: str,
|
|
60
|
-
pop_cols: list[tuple[str, str]] | None,
|
|
61
|
-
obs_cols: list[tuple[str, str]] | None,
|
|
62
|
-
sort_cols: list[str] | None,
|
|
63
|
-
) -> pl.DataFrame:
|
|
64
|
-
# Defaults
|
|
65
|
-
if pop_cols is None:
|
|
66
|
-
# Default interesting cols from ADSL
|
|
67
|
-
pop_cols = [("TRT01A", "Treatment"), ("AGE", "Age"), ("SEX", "Sex")]
|
|
68
|
-
|
|
69
|
-
if obs_cols is None:
|
|
70
|
-
# Default from ADMH
|
|
71
|
-
obs_cols = [
|
|
72
|
-
("MHSEQ", "Seq"),
|
|
73
|
-
("MHBODSYS", "System Organ Class"),
|
|
74
|
-
("MHDECOD", "Preferred Term"),
|
|
75
|
-
("MHSTDTC", "Start Date"),
|
|
76
|
-
("MHENRTPT", "Status"),
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
# Apply filters
|
|
80
|
-
adsl, adq = apply_common_filters(
|
|
81
|
-
population=population,
|
|
82
|
-
observation=observation,
|
|
83
|
-
population_filter=population_filter,
|
|
84
|
-
observation_filter=observation_filter,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
if adq is None:
|
|
88
|
-
raise ValueError("Observation data is missing")
|
|
89
|
-
|
|
90
|
-
# Join
|
|
91
|
-
# Select specific columns from ADSL
|
|
92
|
-
pop_col_names = [c[0] for c in pop_cols]
|
|
93
|
-
# Ensure ID is there
|
|
94
|
-
if id_col not in pop_col_names:
|
|
95
|
-
pop_col_names = [id_col] + pop_col_names
|
|
96
|
-
|
|
97
|
-
adsl_sub = adsl.select(pop_col_names)
|
|
98
|
-
|
|
99
|
-
joined = adq.join(adsl_sub, on=id_col, how="inner")
|
|
100
|
-
|
|
101
|
-
# Sort
|
|
102
|
-
if sort_cols:
|
|
103
|
-
# Check if cols exist
|
|
104
|
-
valid_sorts = [c for c in sort_cols if c in joined.columns]
|
|
105
|
-
if valid_sorts:
|
|
106
|
-
joined = joined.sort(valid_sorts)
|
|
107
|
-
|
|
108
|
-
# Select display columns (id + pop + obs)
|
|
109
|
-
display_cols = [id_col] + [c[0] for c in pop_cols if c[0] != id_col] + [c[0] for c in obs_cols]
|
|
110
|
-
final_df = joined.select([c for c in display_cols if c in joined.columns])
|
|
111
|
-
|
|
112
|
-
# Rename for display?
|
|
113
|
-
# Usually listing keeps raw names or we Map them.
|
|
114
|
-
# The create_rtf_listing function takes col_header list.
|
|
115
|
-
|
|
116
|
-
return final_df
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def mh_listing_rtf(
|
|
120
|
-
df: pl.DataFrame,
|
|
121
|
-
output_path: str,
|
|
122
|
-
title: list[str] | str,
|
|
123
|
-
footnote: list[str] | None,
|
|
124
|
-
source: list[str] | None,
|
|
125
|
-
) -> None:
|
|
126
|
-
if df.is_empty():
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
# Generate headers from predefined mapping or current logic?
|
|
130
|
-
# Here we just use column names for simplicity or we could pass headers.
|
|
131
|
-
# We didn't output headers from mh_listing_df.
|
|
132
|
-
# Let's assume the order is maintained.
|
|
133
|
-
|
|
134
|
-
headers = list(df.columns)
|
|
135
|
-
|
|
136
|
-
# Approximate widths
|
|
137
|
-
# ID: 1, TRT: 1.5, AGE: 0.5, SEX: 0.5, SEQ: 0.5, SOC: 2, PT: 2, DATE: 1, STATUS: 1
|
|
138
|
-
# Total ~ 10 units?
|
|
139
|
-
# Simple uniform distribution or weighted?
|
|
140
|
-
n_cols = len(headers)
|
|
141
|
-
col_widths = [1.0] * n_cols
|
|
142
|
-
|
|
143
|
-
rtf_doc = create_rtf_listing(
|
|
144
|
-
df=df,
|
|
145
|
-
col_header=headers,
|
|
146
|
-
col_widths=col_widths,
|
|
147
|
-
title=title,
|
|
148
|
-
footnote=footnote,
|
|
149
|
-
source=source,
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
rtf_doc.write_rtf(output_path)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def study_plan_to_mh_listing(study_plan: StudyPlan) -> list[str]:
|
|
156
|
-
"""
|
|
157
|
-
Batch generate MH listings.
|
|
158
|
-
"""
|
|
159
|
-
analysis_type = "mh_listing"
|
|
160
|
-
output_dir = study_plan.output_dir
|
|
161
|
-
|
|
162
|
-
parser = StudyPlanParser(study_plan)
|
|
163
|
-
|
|
164
|
-
plans = study_plan.study_data.get("plans", [])
|
|
165
|
-
all_specs = []
|
|
166
|
-
for plan_data in plans:
|
|
167
|
-
expanded = study_plan.expander.expand_plan(plan_data)
|
|
168
|
-
for p in expanded:
|
|
169
|
-
all_specs.append(study_plan.expander.create_analysis_spec(p))
|
|
170
|
-
|
|
171
|
-
plan_df = pl.DataFrame(all_specs)
|
|
172
|
-
|
|
173
|
-
if "analysis" in plan_df.columns:
|
|
174
|
-
mh_plans = plan_df.filter(pl.col("analysis") == analysis_type)
|
|
175
|
-
else:
|
|
176
|
-
mh_plans = pl.DataFrame()
|
|
177
|
-
|
|
178
|
-
generated_files = []
|
|
179
|
-
|
|
180
|
-
for analysis in mh_plans.iter_rows(named=True):
|
|
181
|
-
pop_name = analysis.get("population", "enrolled")
|
|
182
|
-
|
|
183
|
-
try:
|
|
184
|
-
# Load Population
|
|
185
|
-
adsl, _ = parser.get_population_data(pop_name, "trt01a") # dummy group
|
|
186
|
-
|
|
187
|
-
(admh,) = parser.get_datasets("admh")
|
|
188
|
-
|
|
189
|
-
filename = f"{analysis_type}_{pop_name}.rtf".lower()
|
|
190
|
-
output_path = f"{output_dir}/{filename}"
|
|
191
|
-
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
|
192
|
-
|
|
193
|
-
mh_listing(
|
|
194
|
-
population=adsl,
|
|
195
|
-
observation=admh,
|
|
196
|
-
population_filter=None,
|
|
197
|
-
observation_filter=None, # Show all?
|
|
198
|
-
output_file=output_path,
|
|
199
|
-
title=["Listing of Medical History", f"({pop_name} Population)"],
|
|
200
|
-
source=["Source: ADSL, ADMH"],
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
generated_files.append(output_path)
|
|
204
|
-
|
|
205
|
-
except Exception as e:
|
|
206
|
-
print(f"Error generating MH listing: {e}")
|
|
207
|
-
continue
|
|
208
|
-
|
|
209
|
-
return generated_files
|