meta-edc 1.0.6__py3-none-any.whl → 1.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.
Files changed (79) hide show
  1. meta_ae/action_items.py +10 -2
  2. meta_ae/baker_recipes.py +1 -2
  3. meta_ae/tests/tests/test_actions.py +1 -2
  4. meta_analytics/dataframes/__init__.py +3 -0
  5. meta_analytics/dataframes/constants.py +1 -1
  6. meta_analytics/dataframes/get_eos_df.py +15 -2
  7. meta_analytics/dataframes/get_glucose_df.py +149 -0
  8. meta_analytics/dataframes/get_glucose_fbg_df.py +27 -0
  9. meta_analytics/dataframes/get_glucose_fbg_ogtt_df.py +22 -0
  10. meta_analytics/dataframes/glucose_endpoints/endpoint_by_date.py +106 -120
  11. meta_analytics/dataframes/glucose_endpoints/glucose_endpoints_by_date.py +36 -227
  12. meta_analytics/dataframes/utils.py +18 -4
  13. meta_analytics/notebooks/anu.ipynb +95 -0
  14. meta_analytics/notebooks/appointment_planning.ipynb +329 -0
  15. meta_analytics/notebooks/arvs.ipynb +103 -0
  16. meta_analytics/notebooks/cleaning/consent_v1_ext.ipynb +227 -0
  17. meta_analytics/notebooks/cleaning/offschedule_eos.ipynb +353 -0
  18. meta_analytics/notebooks/dsmc/renal_dysfunction.ipynb +435 -0
  19. meta_analytics/notebooks/endpoints/meta_endpoints_by_date.ipynb +664 -0
  20. meta_analytics/notebooks/followup_examination.ipynb +141 -0
  21. meta_analytics/notebooks/hba1c.ipynb +136 -0
  22. meta_analytics/notebooks/hiv_regimens.ipynb +429 -0
  23. meta_analytics/notebooks/incidence.ipynb +232 -0
  24. meta_analytics/notebooks/liver.ipynb +389 -0
  25. meta_analytics/notebooks/magreth.ipynb +645 -0
  26. meta_analytics/notebooks/monitoring_report.ipynb +1751 -0
  27. meta_analytics/notebooks/pharmacy.ipynb +1070 -0
  28. meta_analytics/notebooks/pharmacy_stock_202410.ipynb +306 -0
  29. meta_analytics/notebooks/steering.ipynb +61 -0
  30. meta_analytics/notebooks/undiagnosed/meta3_screening_consort_chart.ipynb +1176 -0
  31. meta_analytics/notebooks/undiagnosed/meta3_screening_undiagnosed.ipynb +519 -0
  32. meta_analytics/notebooks/undiagnosed/meta_screening_table2.ipynb +964 -0
  33. meta_analytics/notebooks/undiagnosed/screen_undiagnosed_or.ipynb +296 -0
  34. meta_analytics/notebooks/undiagnosed/screening.ipynb +273 -0
  35. meta_analytics/notebooks/undiagnosed/screening2.ipynb +958 -0
  36. meta_analytics/notebooks/undiagnosed/screening_undiagnosed_20241002.ipynb +958 -0
  37. meta_analytics/notebooks/ven.ipynb +191 -0
  38. meta_analytics/notebooks/vitals.ipynb +263 -0
  39. meta_analytics/utils.py +81 -0
  40. meta_edc/settings/debug.py +3 -2
  41. meta_edc/urls.py +1 -0
  42. {meta_edc-1.0.6.dist-info → meta_edc-1.1.0.dist-info}/METADATA +6 -5
  43. {meta_edc-1.0.6.dist-info → meta_edc-1.1.0.dist-info}/RECORD +77 -36
  44. {meta_edc-1.0.6.dist-info → meta_edc-1.1.0.dist-info}/WHEEL +1 -1
  45. meta_edc-1.1.0.dist-info/licenses/AUTHORS.rst +8 -0
  46. meta_labs/reportables.py +14 -11
  47. meta_labs/tests/test_reportables.py +33 -12
  48. meta_pharmacy/notebooks/pharmacy.ipynb +41 -0
  49. meta_prn/migrations/0063_historicaloffstudymedication_singleton_field_and_more.py +37 -0
  50. meta_prn/migrations/0064_auto_20250602_2143.py +18 -0
  51. meta_prn/models/end_of_study.py +2 -0
  52. meta_prn/models/off_study_medication.py +2 -0
  53. meta_reports/migrations/0054_auto_20250422_2003.py +81 -0
  54. meta_reports/migrations/0055_alter_glucosesummary_table.py +17 -0
  55. meta_reports/migrations/0056_auto_20250422_2214.py +54 -0
  56. meta_reports/migrations/0057_auto_20250422_2224.py +54 -0
  57. meta_reports/migrations/0058_auto_20250422_2232.py +54 -0
  58. meta_reports/models/dbviews/glucose_summary/unmanaged_model.py +13 -1
  59. meta_reports/models/dbviews/glucose_summary/view_definition.py +8 -5
  60. meta_screening/eligibility/eligibility_part_three/base_eligibility_part_three.py +59 -47
  61. meta_screening/form_validators/screening_part_three.py +6 -1
  62. meta_screening/tests/meta_test_case_mixin.py +3 -0
  63. meta_screening/tests/tests/test_forms.py +9 -2
  64. meta_screening/tests/tests/test_screening_part_three.py +11 -14
  65. meta_subject/action_items.py +1 -2
  66. meta_subject/choices.py +2 -1
  67. meta_subject/form_validators/glucose_form_validator.py +16 -1
  68. meta_subject/forms/blood_results/blood_results_rft_form.py +60 -3
  69. meta_subject/forms/study_medication_form.py +5 -3
  70. meta_subject/migrations/0221_auto_20250402_1913.py +42 -0
  71. meta_subject/migrations/0222_alter_historicalstudymedication_stock_codes_and_more.py +46 -0
  72. meta_subject/migrations/0223_bloodresultsfbc_errors_bloodresultsgludummy_errors_and_more.py +83 -0
  73. meta_subject/migrations/0224_bloodresultsfbc_abnormal_summary_and_more.py +153 -0
  74. meta_subject/tests/tests/test_egfr.py +5 -5
  75. meta_analytics/dataframes/enrolled/__init__.py +0 -1
  76. meta_analytics/dataframes/enrolled/get_glucose_df.py +0 -122
  77. /meta_edc-1.0.6.dist-info/AUTHORS → /meta_analytics/dataframes/glucose_endpoints/utils.py +0 -0
  78. {meta_edc-1.0.6.dist-info → meta_edc-1.1.0.dist-info/licenses}/LICENSE +0 -0
  79. {meta_edc-1.0.6.dist-info → meta_edc-1.1.0.dist-info}/top_level.txt +0 -0
meta_ae/action_items.py CHANGED
@@ -12,7 +12,16 @@ from edc_adverse_event.constants import (
12
12
  DEATH_REPORT_ACTION,
13
13
  DEATH_REPORT_TMG_ACTION,
14
14
  )
15
- from edc_constants.constants import CLOSED, DEAD, HIGH_PRIORITY, NO, YES
15
+ from edc_constants.constants import (
16
+ CLOSED,
17
+ DEAD,
18
+ GRADE3,
19
+ GRADE4,
20
+ GRADE5,
21
+ HIGH_PRIORITY,
22
+ NO,
23
+ YES,
24
+ )
16
25
  from edc_lab_results.constants import (
17
26
  BLOOD_RESULTS_FBC_ACTION,
18
27
  BLOOD_RESULTS_GLU_ACTION,
@@ -22,7 +31,6 @@ from edc_lab_results.constants import (
22
31
  )
23
32
  from edc_ltfu.constants import LOST_TO_FOLLOWUP
24
33
  from edc_notification.utils import get_email_contacts
25
- from edc_reportable import GRADE3, GRADE4, GRADE5
26
34
  from edc_visit_schedule.utils import get_offschedule_models
27
35
 
28
36
  from meta_prn.constants import OFFSTUDY_MEDICATION_ACTION
meta_ae/baker_recipes.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from edc_adverse_event.constants import NOT_RELATED
2
- from edc_constants.constants import NO, NOT_APPLICABLE, YES
3
- from edc_reportable.constants import GRADE4
2
+ from edc_constants.constants import GRADE4, NO, NOT_APPLICABLE, YES
4
3
  from edc_utils.date import get_utcnow
5
4
  from model_bakery.recipe import Recipe
6
5
 
@@ -7,8 +7,7 @@ from edc_adverse_event.constants import (
7
7
  DEATH_REPORT_ACTION,
8
8
  DEATH_REPORT_TMG_ACTION,
9
9
  )
10
- from edc_constants.constants import CLOSED, NEW
11
- from edc_reportable.constants import GRADE4, GRADE5
10
+ from edc_constants.constants import CLOSED, GRADE4, GRADE5, NEW
12
11
  from model_bakery import baker
13
12
 
14
13
  from meta_screening.tests.meta_test_case_mixin import MetaTestCaseMixin
@@ -8,6 +8,9 @@ from .constants import (
8
8
  endpoint_columns,
9
9
  )
10
10
  from .get_eos_df import get_eos_df
11
+ from .get_glucose_df import get_glucose_df
12
+ from .get_glucose_fbg_df import get_glucose_fbg_df
13
+ from .get_glucose_fbg_ogtt_df import get_glucose_fbg_ogtt_df
11
14
  from .get_last_imp_visits_df import get_last_imp_visits_df
12
15
  from .glucose_endpoints import EndpointByDate, GlucoseEndpointsByDate
13
16
  from .screening import get_glucose_tested_only_df, get_screening_df
@@ -16,7 +16,7 @@ endpoint_columns = [
16
16
  "fbg_value",
17
17
  "ogtt_value",
18
18
  "fbg_datetime",
19
- "fasting",
19
+ "fasted",
20
20
  "endpoint_label",
21
21
  "endpoint_type",
22
22
  "endpoint",
@@ -13,14 +13,27 @@ def get_eos_df() -> pd.DataFrame:
13
13
  df_eos = get_eos("meta_prn.endofstudy")
14
14
  df_visit = get_subject_visit("meta_subject.subjectvisit")
15
15
  df_last_visit = (
16
- df_visit.groupby(["subject_identifier", "site"])
16
+ df_visit.groupby(["subject_identifier", "site_id"])
17
17
  .agg({"endline_visit_code": "max", "endline_visit_datetime": "max"})
18
18
  .reset_index()
19
19
  )
20
- df_last_visit = df_last_visit.rename(columns={"site": "site_id"})
20
+ # df_last_visit = df_last_visit.rename(columns={"site": "site_id"})
21
21
 
22
22
  df_eos = df_eos.merge(
23
23
  df_last_visit, on="subject_identifier", how="left", suffixes=("", "_y")
24
24
  )
25
25
  df_eos = df_eos.drop(columns=["site_id_y"])
26
+ df_visit_grp = (
27
+ df_visit.groupby(by=["subject_identifier"])[["baseline_datetime", "visit_datetime"]]
28
+ .max()
29
+ .reset_index()
30
+ )
31
+ df_visit_grp["followup_days"] = (
32
+ df_visit_grp["visit_datetime"] - df_visit_grp["baseline_datetime"]
33
+ ).dt.days
34
+ df_eos = df_eos.merge(
35
+ df_visit_grp[["subject_identifier", "followup_days"]],
36
+ on="subject_identifier",
37
+ how="left",
38
+ ).reset_index(drop=True)
26
39
  return df_eos
@@ -0,0 +1,149 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from django_pandas.io import read_frame
4
+ from edc_appointment.constants import MISSED_APPT # noqa
5
+ from edc_pdutils.dataframes import get_eos, get_subject_consent, get_subject_visit
6
+
7
+ from meta_subject.models import Glucose, GlucoseFbg
8
+
9
+
10
+ def get_glucose_df() -> pd.DataFrame:
11
+ subject_visit_df = (
12
+ get_subject_visit("meta_subject.subjectvisit")
13
+ .rename(columns={"id": "subject_visit_id"})
14
+ .query("appt_timing!=@MISSED_APPT")
15
+ )
16
+ df_glucose_fbg = read_frame(GlucoseFbg.objects.all(), verbose=False).rename(
17
+ columns={"fasting": "fasted", "subject_visit": "subject_visit_id"},
18
+ )
19
+ df_glucose_fbg["fasting_hrs"] = np.nan
20
+ df_glucose_fbg["fasting_hrs"] = df_glucose_fbg["fasting_duration_delta"].apply(
21
+ lambda x: x.total_seconds() / 3600
22
+ )
23
+ df_glucose_fbg.loc[
24
+ :,
25
+ ["ogtt_value", "ogtt_units", "ogtt_datetime"],
26
+ ] = [np.nan, None, pd.NaT]
27
+ df_glucose_fbg["source"] = "meta_subject.glucosefbg"
28
+ df_glucose_fbg = pd.merge(
29
+ subject_visit_df[
30
+ [
31
+ "subject_identifier",
32
+ "site_id",
33
+ "visit_code",
34
+ "visit_datetime",
35
+ "baseline_datetime",
36
+ "subject_visit_id",
37
+ ]
38
+ ],
39
+ df_glucose_fbg[[col for col in df_glucose_fbg.columns if "site_id" not in col]],
40
+ on="subject_visit_id",
41
+ how="left",
42
+ )
43
+
44
+ df_glucose = read_frame(Glucose.objects.all(), verbose=False).rename(
45
+ columns={"subject_visit": "subject_visit_id", "fasting": "fasted"}
46
+ )
47
+ df_glucose["fasting_hrs"] = np.nan
48
+ df_glucose["fasting_hrs"] = df_glucose["fasting_duration_delta"].apply(
49
+ lambda x: x.total_seconds() / 3600
50
+ )
51
+ df_glucose["source"] = "meta_subject.glucose"
52
+
53
+ df_glucose = pd.merge(
54
+ subject_visit_df[
55
+ [
56
+ "subject_identifier",
57
+ "site_id",
58
+ "visit_code",
59
+ "visit_datetime",
60
+ "baseline_datetime",
61
+ "subject_visit_id",
62
+ ]
63
+ ],
64
+ df_glucose[[col for col in df_glucose.columns if "site_id" not in col]],
65
+ on="subject_visit_id",
66
+ how="left",
67
+ )
68
+
69
+ keep_cols = [
70
+ "subject_identifier",
71
+ "site_id",
72
+ "visit_code",
73
+ "visit_datetime",
74
+ "baseline_datetime",
75
+ "subject_visit_id",
76
+ "fasted",
77
+ "fasting_hrs",
78
+ "fbg_value",
79
+ "fbg_units",
80
+ "fbg_datetime",
81
+ "ogtt_value",
82
+ "ogtt_units",
83
+ "ogtt_datetime",
84
+ "source",
85
+ "revision",
86
+ "report_datetime",
87
+ ]
88
+ df = pd.merge(
89
+ df_glucose[keep_cols],
90
+ df_glucose_fbg[keep_cols],
91
+ on="subject_visit_id",
92
+ how="outer",
93
+ # indicator=True,
94
+ suffixes=("", "_2"),
95
+ )
96
+
97
+ for suffix in ["", "_2"]:
98
+ df[[f"fasting_hrs{suffix}", f"fbg_value{suffix}", f"ogtt_value{suffix}"]] = df[
99
+ [f"fasting_hrs{suffix}", f"fbg_value{suffix}", f"ogtt_value{suffix}"]
100
+ ].apply(pd.to_numeric)
101
+ df.loc[
102
+ (df[f"fbg_units{suffix}"] != "mmol/L (millimoles/L)")
103
+ & (df[f"fbg_value{suffix}"] >= 0),
104
+ f"fbg_units{suffix}",
105
+ ] = "mmol/L (millimoles/L)"
106
+ df.loc[
107
+ (df[f"ogtt_units{suffix}"] != "mmol/L (millimoles/L)")
108
+ & (df[f"ogtt_value{suffix}"] >= 0),
109
+ f"ogtt_units{suffix}",
110
+ ] = "mmol/L (millimoles/L)"
111
+
112
+ # reconcile all to single column
113
+ for col in ["fasted", "fbg_value", "ogtt_value", "fbg_datetime", "ogtt_datetime"]:
114
+ df.loc[(df[col].isna()) & (df[f"{col}_2"].notna()), col] = df[f"{col}_2"]
115
+
116
+ df_consent = get_subject_consent("meta_consent.subjectconsent")
117
+ df_eos = get_eos("meta_prn.endofstudy")
118
+ df = df.merge(
119
+ df_consent[["subject_identifier", "gender", "consent_datetime", "dob"]],
120
+ on="subject_identifier",
121
+ how="left",
122
+ ).merge(
123
+ df_eos[["subject_identifier", "offstudy_datetime", "offstudy_reason"]],
124
+ on="subject_identifier",
125
+ how="left",
126
+ )
127
+
128
+ df[[col for col in df.columns if "datetime" in col]] = df[
129
+ [col for col in df.columns if "datetime" in col]
130
+ ].apply(lambda x: x.dt.tz_localize(None) if x.dtype == "datetime64[ns, UTC]" else x)
131
+
132
+ df["visit_days"] = df["baseline_datetime"].rsub(df["visit_datetime"]).dt.days
133
+ df["fgb_days"] = df["baseline_datetime"].rsub(df["fbg_datetime"]).dt.days
134
+ df["ogtt_days"] = df["baseline_datetime"].rsub(df["ogtt_datetime"]).dt.days
135
+ df["visit_days"] = pd.to_numeric(df["visit_days"], downcast="integer")
136
+ df["fgb_days"] = pd.to_numeric(df["fgb_days"], downcast="integer")
137
+ df["ogtt_days"] = pd.to_numeric(df["ogtt_days"], downcast="integer")
138
+
139
+ df = (
140
+ df.query(
141
+ "offstudy_reason != 'Patient fulfilled late exclusion criteria "
142
+ "(due to abnormal blood values or raised blood pressure at enrolment'"
143
+ )
144
+ .copy()
145
+ .drop(columns=[col for col in df.columns if "_2" in col])
146
+ .sort_values(by=["subject_identifier", "visit_code"])
147
+ .reset_index(drop=True)
148
+ )
149
+ return df
@@ -0,0 +1,27 @@
1
+ import pandas as pd
2
+ from edc_constants.constants import NO, YES
3
+ from edc_pdutils.dataframes import get_crf
4
+
5
+ from meta_analytics.dataframes.utils import calculate_fasting_hrs
6
+
7
+ __all__ = ["get_glucose_fbg_df"]
8
+
9
+
10
+ def get_glucose_fbg_df(subject_identifiers: list[str] | None = None) -> pd.DataFrame:
11
+ """Returns a prepared Dataframe of CRF
12
+ meta_subject.glucosefbg.
13
+
14
+ Note: meta_subject.glucosefbg has only FBG measures.
15
+ """
16
+ df = get_crf(
17
+ model="meta_subject.glucosefbg",
18
+ subject_identifiers=subject_identifiers or [],
19
+ subject_visit_model="meta_subject.subjectvisit",
20
+ )
21
+ df["source"] = "meta_subject.glucosefbg"
22
+ df.rename(columns={"fbg_fasting": "fasting"}, inplace=True)
23
+ df.loc[(df["fasting"] == "fasting"), "fasting"] = YES
24
+ df.loc[(df["fasting"] == "non_fasting"), "fasting"] = NO
25
+ df = calculate_fasting_hrs(df)
26
+ df = df.reset_index(drop=True)
27
+ return df
@@ -0,0 +1,22 @@
1
+ import pandas as pd
2
+ from edc_pdutils.dataframes import get_crf
3
+
4
+ from .utils import calculate_fasting_hrs
5
+
6
+ __all__ = ["get_glucose_fbg_ogtt_df"]
7
+
8
+
9
+ def get_glucose_fbg_ogtt_df(subject_identifiers: list[str] | None = None) -> pd.DataFrame:
10
+ """Returns a prepared Dataframe of CRF meta_subject.glucose.
11
+
12
+ Note: meta_subject.glucose has FBG and OGTT measures.
13
+ """
14
+ df = get_crf(
15
+ model="meta_subject.glucose",
16
+ subject_identifiers=subject_identifiers or [],
17
+ subject_visit_model="meta_subject.subjectvisit",
18
+ )
19
+ df["source"] = "meta_subject.glucose"
20
+ df = calculate_fasting_hrs(df)
21
+ df = df.reset_index(drop=True)
22
+ return df
@@ -1,3 +1,5 @@
1
+ from dataclasses import dataclass, field
2
+
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
  from edc_constants.constants import YES
@@ -13,6 +15,97 @@ class InvalidCaseList(Exception):
13
15
  pass
14
16
 
15
17
 
18
+ @dataclass(kw_only=True)
19
+ class CaseData:
20
+ df: pd.DataFrame
21
+ index: int
22
+ fbg_value: float | None = field(default=None, init=False)
23
+ fbg_datetime: pd.Timestamp | None = field(default=None, init=False)
24
+ fasted: str | None = field(default=None, init=False)
25
+ ogtt_value: float | None = field(default=None, init=False)
26
+ next_fbg_value: float | None = field(default=None, init=False)
27
+ next_fbg_datetime: pd.Timestamp | None = field(default=None, init=False)
28
+ next_fasted: str | None = field(default=None, init=False)
29
+ next_ogtt_value: float | None = field(default=None, init=False)
30
+
31
+ previous_fbg_value: float | None = field(default=None, init=False)
32
+ previous_fbg_datetime: pd.Timestamp | None = field(default=None, init=False)
33
+ previous_fasted: str | None = field(default=None, init=False)
34
+ previous_ogtt_value: float | None = field(default=None, init=False)
35
+
36
+ fbg_threshold: float = field(default=7.0, init=False)
37
+ ogtt_threshold: float = field(default=11.1, init=False)
38
+
39
+ def __post_init__(self):
40
+ self.fbg_value = self.df.loc[self.index, "fbg_value"]
41
+ self.fbg_datetime = self.df.loc[self.index, "fbg_datetime"]
42
+ self.ogtt_value = self.df.loc[self.index, "ogtt_value"]
43
+ self.fasted = self.df.loc[self.index, "fasted"]
44
+
45
+ try:
46
+ self.next_fbg_value = self.df.loc[self.index + 1, "fbg_value"]
47
+ except KeyError:
48
+ self.next_fbg_value = np.nan
49
+ self.next_fbg_datetime = pd.NaT
50
+ self.next_ogtt_value = np.nan
51
+ self.next_fasted = np.nan
52
+ else:
53
+ self.next_fbg_datetime = self.df.loc[self.index + 1, "fbg_datetime"]
54
+ self.next_ogtt_value = self.df.loc[self.index + 1, "ogtt_value"]
55
+ self.next_fasted = self.df.loc[self.index + 1, "fasted"]
56
+
57
+ try:
58
+ self.previous_fbg_value = self.df.loc[self.index - 1, "fbg_value"]
59
+ except KeyError:
60
+ self.previous_fbg_value = np.nan
61
+ self.previous_fbg_datetime = pd.NaT
62
+ self.previous_ogtt_value = np.nan
63
+ self.previous_fasted = np.nan
64
+ else:
65
+ self.previous_fbg_datetime = self.df.loc[self.index - 1, "fbg_datetime"]
66
+ self.previous_ogtt_value = self.df.loc[self.index - 1, "ogtt_value"]
67
+ self.previous_fasted = self.df.loc[self.index - 1, "fasted"]
68
+
69
+ def case_two(self) -> bool:
70
+ """ "FBG >= 7 x 2, first OGTT<=11.1"""
71
+ if (
72
+ self.fbg_value >= self.fbg_threshold
73
+ and self.next_fbg_value >= self.fbg_threshold
74
+ and 0.0 < self.ogtt_value < self.ogtt_threshold
75
+ and self.fasted == YES
76
+ and self.next_fasted == YES
77
+ and (self.next_fbg_datetime.date() - self.fbg_datetime.date()).days > 6
78
+ ):
79
+ return True
80
+ return False
81
+
82
+ def case_three(self) -> bool:
83
+ """ "FBG >= 7 x 2, second OGTT<=11.1"""
84
+ if (
85
+ self.fbg_value >= self.fbg_threshold
86
+ and self.next_fbg_value >= self.fbg_threshold
87
+ and 0.0 < self.next_ogtt_value < self.ogtt_threshold
88
+ and self.fasted == YES
89
+ and self.next_fasted == YES
90
+ and (self.next_fbg_datetime.date() - self.fbg_datetime.date()).days > 6
91
+ ):
92
+ return True
93
+ return False
94
+
95
+ def case_two_reversed(self) -> bool:
96
+ """Same as case 2, but with the previous FBG reading."""
97
+ if (
98
+ self.fbg_value >= self.fbg_threshold
99
+ and self.previous_fbg_value >= self.fbg_threshold
100
+ and 0.0 < self.previous_ogtt_value < self.ogtt_threshold
101
+ and self.fasted == YES
102
+ and self.previous_fasted == YES
103
+ and (self.fbg_datetime.date() - self.previous_fbg_datetime.date()).days > 6
104
+ ):
105
+ return True
106
+ return False
107
+
108
+
16
109
  class EndpointByDate:
17
110
  """Given all timepoints for a subject, flag the first timepoint
18
111
  where the protocol endpoint is reached.
@@ -27,157 +120,50 @@ class EndpointByDate:
27
120
  * case 3. FBG >= 7 x 2, second OGTT<11.1
28
121
 
29
122
  Additional criteria considered:
30
- 1. any threshhold FBG must be taken while fasted (fasting=YES)
123
+ 1. any threshhold FBG must be taken while fasted (fasted=YES)
31
124
  2. threshhold FBG readings must be consecutive (no
32
125
  readings below threshold in the sequence regardless
33
126
  of fasting)
34
127
  3. at least 7 days between threshhold FBG readings.
35
128
  4. at least one of the two threshold FBG readings must be taken
36
129
  with an OGTT at the same timepoint.
37
-
38
- Note:
39
- case 4 is not a protocol endpoint. It considers only FBG and fasting.
40
- It looks for two consecutive fasted threshold FBG readings.
41
130
  """
42
131
 
43
- valid_case_list = [2, 3, 4]
44
-
45
132
  def __init__(
46
133
  self,
47
134
  subject_df: pd.DataFrame = None,
48
135
  fbg_threshhold: float = None,
49
136
  ogtt_threshhold: float = None,
50
- case_list: list[int] | None = None,
51
137
  ):
52
138
  self.row = None
53
139
  self.index = None
54
- self.subject_df = subject_df[subject_df["fbg_value"].notna()]
55
- self.subject_df = self.subject_df.reset_index(drop=True)
140
+ self.subject_df = subject_df.sort_values(by=["visit_code"]).reset_index(drop=True)
56
141
  self.fbg_threshhold = fbg_threshhold
57
142
  self.ogtt_threshhold = ogtt_threshhold
58
- self.case_list = case_list or [2, 3]
59
- if [x for x in self.case_list if x not in self.valid_case_list]:
60
- raise InvalidCaseList(f"Expected any of {self.valid_case_list}. Got {case_list}.")
61
- self.endpoint_cases = {k: v for k, v in endpoint_cases.items() if k in self.case_list}
62
143
  self.evaluate()
63
144
 
64
145
  def evaluate(self):
65
146
  for index, _ in self.subject_df.iterrows():
66
- if 2 in self.case_list and self.case_two(index):
147
+ case_data = CaseData(df=self.subject_df, index=index)
148
+ if case_data.case_two():
149
+ self.endpoint_reached(index, case=2, fbg_datetime=case_data.next_fbg_datetime)
67
150
  break
68
- elif 3 in self.case_list and self.case_three(index):
151
+ elif case_data.case_three():
152
+ self.endpoint_reached(index, case=3, fbg_datetime=case_data.next_fbg_datetime)
69
153
  break
70
- elif 4 in self.case_list and self.case_four(index):
154
+ elif case_data.case_two_reversed():
155
+ self.endpoint_reached(index, case=2, fbg_datetime=case_data.fbg_datetime)
71
156
  break
157
+ else:
158
+ pass
72
159
 
73
- def endpoint_reached(self, index: int, case: int, next_is_endpoint: bool | None = None):
160
+ def endpoint_reached(self, index: int, case: int, fbg_datetime: pd.Timestamp):
74
161
  """Update the subject_df"""
75
- fbg_datetime = (
76
- self.get_next("fbg_datetime", index)
77
- if next_is_endpoint
78
- else self.get("fbg_datetime", index)
79
- )
80
162
  self.subject_df.loc[self.subject_df["fbg_datetime"] == fbg_datetime, "endpoint"] = 1
81
163
  self.subject_df["interval_in_days"] = np.nan
82
- try:
83
- self.subject_df.loc[
84
- self.subject_df["fbg_datetime"] == fbg_datetime, "interval_in_days"
85
- ] = self.sequential_assessments_in_days(index)
86
- except EndpointTdeltaError:
87
- pass
88
- self.subject_df["interval_in_days"] = pd.to_numeric(
89
- self.subject_df["interval_in_days"]
90
- )
91
164
  self.subject_df.loc[
92
165
  self.subject_df["fbg_datetime"] == fbg_datetime, "endpoint_type"
93
166
  ] = case
94
167
  self.subject_df.loc[
95
168
  self.subject_df["fbg_datetime"] == fbg_datetime, "endpoint_label"
96
- ] = self.endpoint_cases[case]
97
-
98
- def case_two(self, index: int):
99
- """FBG >= 7 x 2, first OGTT<11.1.
100
-
101
- First FBG must be done with corresponding OGTT.
102
- """
103
- reached = (
104
- self.get_next("fbg_datetime", index)
105
- and self.get("fbg_value", index)
106
- and self.get("ogtt_value", index)
107
- and self.get("fasting", index)
108
- and self.get_next("fbg_value", index)
109
- and self.get_next("fasting", index)
110
- and self.get("fbg_value", index) >= self.fbg_threshhold
111
- and self.get("ogtt_value", index) < self.ogtt_threshhold
112
- and self.get("fasting", index) == YES
113
- and self.get_next("fbg_value", index) >= self.fbg_threshhold
114
- and self.get_next("fasting", index) == YES
115
- and (self.get_next("fbg_datetime", index) - self.get("fbg_datetime", index)).days
116
- >= 7
117
- )
118
- if reached:
119
- self.endpoint_reached(index, case=2, next_is_endpoint=True)
120
- return reached
121
-
122
- def case_three(self, index: int):
123
- """FBG >= 7 x 2, second OGTT<11.1.
124
-
125
- Second FBG must be done with corresponding OGTT.
126
- """
127
- reached = (
128
- self.get_next("fbg_datetime", index)
129
- and self.get("fbg_value", index)
130
- and self.get("fasting", index)
131
- and self.get_next("fbg_value", index)
132
- and self.get_next("ogtt_value", index)
133
- and self.get_next("fasting", index)
134
- and self.get("fbg_value", index) >= self.fbg_threshhold
135
- and self.get("fasting", index) == YES
136
- and self.get_next("fbg_value", index) >= self.fbg_threshhold
137
- and self.get_next("ogtt_value", index) < self.ogtt_threshhold
138
- and self.get_next("fasting", index) == YES
139
- and (self.get_next("fbg_datetime", index) - self.get("fbg_datetime", index)).days
140
- >= 7
141
- )
142
- if reached:
143
- self.endpoint_reached(index, case=3, next_is_endpoint=True)
144
- return reached
145
-
146
- def case_four(self, index: int):
147
- """FBG >= 7 x 2, OGTT not considered
148
-
149
- This is not a protocol endpoint.
150
- """
151
- reached = (
152
- self.get("fbg_value", index)
153
- and self.get("fbg_datetime", index)
154
- and self.get("fasting", index)
155
- and self.get_next("fbg_value", index)
156
- and self.get_next("ogtt_value", index)
157
- and self.get_next("fbg_datetime", index)
158
- and self.get_next("fasting", index)
159
- and self.get("fbg_value", index) >= self.fbg_threshhold
160
- and self.get("fasting", index) == YES
161
- and self.get_next("fbg_value", index) >= self.fbg_threshhold
162
- and self.get_next("fasting", index) == YES
163
- and (self.get_next("fbg_datetime", index) - self.get("fbg_datetime", index)).days
164
- >= 7
165
- )
166
- if reached:
167
- self.endpoint_reached(index, case=4, next_is_endpoint=True)
168
- return reached
169
-
170
- def sequential_assessments_in_days(self, index) -> int:
171
- if not self.get_next("fbg_value", index):
172
- raise EndpointTdeltaError
173
- return (self.get_next("fbg_datetime", index) - self.get("visit_datetime", index)).days
174
-
175
- def get(self, col: str, index: int) -> float | None:
176
- try:
177
- next_value = self.subject_df.iloc[index : index + 1][col].item()
178
- except ValueError:
179
- next_value = None
180
- return next_value
181
-
182
- def get_next(self, col: str, index: int) -> float | None:
183
- return self.get(col, index + 1)
169
+ ] = endpoint_cases[case]