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
@@ -0,0 +1,1070 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "id": "0",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "%%capture\n",
11
+ "import os\n",
12
+ "from pathlib import Path\n",
13
+ "import pandas as pd\n",
14
+ "from dj_notebook import activate\n",
15
+ "import numpy as np\n",
16
+ "\n",
17
+ "env_file = os.environ[\"META_ENV\"]\n",
18
+ "reports_folder = Path(os.environ[\"META_REPORTS_FOLDER\"])\n",
19
+ "analysis_folder = Path(os.environ[\"META_ANALYSIS_FOLDER\"])\n",
20
+ "pharmacy_folder = Path(os.environ[\"META_PHARMACY_FOLDER\"])\n",
21
+ "plus = activate(dotenv_file=env_file)"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "execution_count": null,
27
+ "id": "1",
28
+ "metadata": {},
29
+ "outputs": [],
30
+ "source": [
31
+ "from edc_pharmacy.analytics.dataframes import no_stock_for_subjects_df\n",
32
+ "from datetime import datetime\n",
33
+ "from edc_registration.models import RegisteredSubject\n",
34
+ "\n",
35
+ "from edc_appointment.analytics import get_appointment_df\n",
36
+ "from edc_appointment.constants import NEW_APPT\n",
37
+ "from edc_pharmacy.models import StockRequest, Allocation, ReceiveItem, OrderItem, Lot\n",
38
+ "\n",
39
+ "from edc_pharmacy.analytics import get_next_scheduled_visit_for_subjects_df\n",
40
+ "from meta_rando.models import RandomizationList\n",
41
+ "from edc_pharmacy.models import Stock\n",
42
+ "from edc_visit_schedule.models import SubjectScheduleHistory\n",
43
+ "from django.apps import apps as django_apps\n",
44
+ "from django.db.models import Count\n",
45
+ "from django_pandas.io import read_frame\n",
46
+ "from edc_visit_schedule.site_visit_schedules import site_visit_schedules\n",
47
+ "from edc_pharmacy.models import Container\n",
48
+ "from great_tables import GT, html, loc, style\n",
49
+ "from PIL import Image\n",
50
+ "from edc_pdutils.dataframes.get_subject_visit import convert_visit_code_to_float\n"
51
+ ]
52
+ },
53
+ {
54
+ "cell_type": "code",
55
+ "execution_count": null,
56
+ "id": "2",
57
+ "metadata": {},
58
+ "outputs": [],
59
+ "source": [
60
+ "from edc_model_to_dataframe import read_frame_edc\n",
61
+ "from meta_subject.models import FollowupExamination\n",
62
+ "\n",
63
+ "df = read_frame_edc(FollowupExamination.objects.all(), drop_sys_columns=True, drop_action_item_columns=True)\n",
64
+ "df = df.replace(\"none\", pd.NA)\n",
65
+ "df = df.replace(\"none\", pd.NA)\n",
66
+ "df = df.fillna(pd.NA)\n",
67
+ "convert_visit_code_to_float(df)"
68
+ ]
69
+ },
70
+ {
71
+ "cell_type": "code",
72
+ "execution_count": null,
73
+ "id": "3",
74
+ "metadata": {},
75
+ "outputs": [],
76
+ "source": [
77
+ "from edc_analytics.stata import get_stata_labels_from_model\n",
78
+ "\n",
79
+ "df = df[[\"subject_identifier\", \"subject_visit_id\", \"report_datetime\", \"visit_code\", \"site_id\", \"site_name\", \"visit_reason\", \"symptoms\",\"symptoms_detail\", \"symptoms_sought_care\", \"symptoms_g3\", \"symptoms_g4\", \"comment\"]].copy().reset_index(drop=True)\n",
80
+ "\n",
81
+ "df = df.astype(\n",
82
+ " {col: \"Float64\" for col in df.select_dtypes(include=[\"float\", \"float64\"]).columns}\n",
83
+ ")\n",
84
+ "df_meds = df.astype(\n",
85
+ " {col: \"Int64\" for col in df.select_dtypes(include=[\"int\", \"int64\"]).columns}\n",
86
+ ")\n",
87
+ "df = df.astype(\n",
88
+ " {\n",
89
+ " col: \"datetime64[ns]\"\n",
90
+ " for col in df.select_dtypes(include=[\"datetime\", \"datetime64\"]).columns\n",
91
+ " }\n",
92
+ ")\n",
93
+ "df = df.astype(\n",
94
+ " {\n",
95
+ " col: str\n",
96
+ " for col in df.select_dtypes(include=[\"object\"]).columns\n",
97
+ " }\n",
98
+ ")\n",
99
+ "df = df.fillna(pd.NA)\n",
100
+ "\n",
101
+ "variable_labels = {}\n",
102
+ "variable_labels.update(**get_stata_labels_from_model(df, model=\"meta_subject.followupexamination\", suffix=None))\n",
103
+ "\n",
104
+ "df.to_stata(\n",
105
+ " path=analysis_folder / \"followupexamination.dta\",\n",
106
+ " variable_labels=variable_labels,\n",
107
+ " version=118,\n",
108
+ " write_index=False,\n",
109
+ ")"
110
+ ]
111
+ },
112
+ {
113
+ "cell_type": "code",
114
+ "execution_count": null,
115
+ "id": "4",
116
+ "metadata": {},
117
+ "outputs": [],
118
+ "source": [
119
+ "df"
120
+ ]
121
+ },
122
+ {
123
+ "cell_type": "code",
124
+ "execution_count": null,
125
+ "id": "5",
126
+ "metadata": {},
127
+ "outputs": [],
128
+ "source": [
129
+ "\n",
130
+ "def get_great_table(df:pd.DataFrame, title:str, footnote:str|None=None):\n",
131
+ " return (GT(df)\n",
132
+ " .tab_header(title=html(title))\n",
133
+ " .cols_align(align=\"left\", columns=[0])\n",
134
+ " .cols_align(align=\"right\", columns=list(range(1, len(df.columns))))\n",
135
+ " .opt_stylize(style=5)\n",
136
+ " .opt_row_striping(row_striping=False)\n",
137
+ " .opt_vertical_padding(scale=1.2)\n",
138
+ " .opt_horizontal_padding(scale=1.0)\n",
139
+ " .tab_options(\n",
140
+ " stub_background_color=\"white\",\n",
141
+ " row_group_border_bottom_style=\"hidden\",\n",
142
+ " row_group_padding=0.5,\n",
143
+ " row_group_background_color=\"white\",\n",
144
+ " table_background_color=\"white\",\n",
145
+ " table_font_size=12,\n",
146
+ " )\n",
147
+ " .tab_style(\n",
148
+ " style=[style.fill(color=\"white\"), style.text(color=\"black\")],\n",
149
+ " locations=loc.body(columns=list(range(len(df.columns))), rows=list(range(0, len(df)))),\n",
150
+ " )\n",
151
+ " .tab_style(\n",
152
+ " style=[style.fill(color=\"lightgrey\"), style.text(color=\"black\")],\n",
153
+ " locations=loc.body(columns=list(range(len(df.columns))), rows=[len(df)-1]),\n",
154
+ " )\n",
155
+ " .tab_source_note(source_note=html(footnote or \"\"))\n",
156
+ " .tab_style(\n",
157
+ " style=style.text(color=\"black\", size=\"small\"),\n",
158
+ " locations=loc.footer(),\n",
159
+ " )\n",
160
+ "\n",
161
+ "\n",
162
+ " )\n"
163
+ ]
164
+ },
165
+ {
166
+ "cell_type": "code",
167
+ "execution_count": null,
168
+ "id": "6",
169
+ "metadata": {},
170
+ "outputs": [],
171
+ "source": [
172
+ "start_from_appt_date = datetime(2025,5,15)"
173
+ ]
174
+ },
175
+ {
176
+ "cell_type": "code",
177
+ "execution_count": null,
178
+ "id": "7",
179
+ "metadata": {},
180
+ "outputs": [],
181
+ "source": [
182
+ "# get rando\n",
183
+ "df_rando = read_frame(RandomizationList.objects.values(\"subject_identifier\", \"assignment\").filter(subject_identifier__isnull=False))"
184
+ ]
185
+ },
186
+ {
187
+ "cell_type": "code",
188
+ "execution_count": null,
189
+ "id": "8",
190
+ "metadata": {},
191
+ "outputs": [],
192
+ "source": [
193
+ "# get appointments\n",
194
+ "df_appt = get_appointment_df()\n",
195
+ "print(f\"{len(df_appt[(df_appt.appt_status==NEW_APPT) & (df_appt.appt_datetime >= start_from_appt_date) & (df_appt.appt_datetime < datetime(2026,3,1)) & (df_appt.visit_code!=1480.0)])} appointments after filtering\")"
196
+ ]
197
+ },
198
+ {
199
+ "cell_type": "code",
200
+ "execution_count": null,
201
+ "id": "9",
202
+ "metadata": {},
203
+ "outputs": [],
204
+ "source": [
205
+ "# create a dataframe of subjects still on the 'schedule' schedule\n",
206
+ "# use SubjectScheduleHistory where offschedule_datetime is null\n",
207
+ "df_subject_schedule = read_frame(SubjectScheduleHistory.objects.values(\"subject_identifier\", \"visit_schedule_name\", \"schedule_name\", \"onschedule_datetime\", \"offschedule_datetime\").filter(offschedule_datetime__isnull=True, schedule_name=\"schedule\"))\n",
208
+ "\n",
209
+ "print(f\"{len(df_subject_schedule)} subjects currently onstudy\")"
210
+ ]
211
+ },
212
+ {
213
+ "cell_type": "code",
214
+ "execution_count": null,
215
+ "id": "10",
216
+ "metadata": {},
217
+ "outputs": [],
218
+ "source": [
219
+ "# for now merge with the unfiltered df_appt\n",
220
+ "df_main = df_subject_schedule.merge(\n",
221
+ " df_appt[[\"appointment_id\", \"subject_identifier\", \"visit_code\", \"visit_code_str\", \"appt_datetime\", \"baseline_datetime\", \"endline_visit_code\", \"visit_code_sequence\", \"appt_status\"]],\n",
222
+ " on=\"subject_identifier\",\n",
223
+ " how=\"left\")\n",
224
+ "# exclude unscheduled,\n",
225
+ "df_main = df_main[\n",
226
+ " (df_main.visit_code_sequence==0) &\n",
227
+ " (df_main.visit_schedule_name==\"visit_schedule\") &\n",
228
+ " (df_main.schedule_name==\"schedule\") &\n",
229
+ " (df_main.visit_code<2000.0) &\n",
230
+ " (df_main.appt_status==NEW_APPT)\n",
231
+ "].copy()\n",
232
+ "print(f\"{len(df_main)} new appointments for subjects on study\")\n"
233
+ ]
234
+ },
235
+ {
236
+ "cell_type": "code",
237
+ "execution_count": null,
238
+ "id": "11",
239
+ "metadata": {},
240
+ "outputs": [],
241
+ "source": [
242
+ "# number of appointments before extended all subjects out to 48m\n",
243
+ "df_grouped = df_main[\n",
244
+ " (df_main.appt_datetime >= start_from_appt_date) &\n",
245
+ " (df_main.appt_datetime < datetime(2026,3,1)) &\n",
246
+ " (df_main.visit_code!=1480.0)\n",
247
+ "].visit_code.value_counts().reset_index(name=\"appointments\").sort_values(by=\"visit_code\", ascending=True).reset_index(drop=True)\n",
248
+ "df_grouped[\"cumsum\"] = df_grouped.appointments.cumsum()\n",
249
+ "df_grouped[\"cumsum\"].max()\n"
250
+ ]
251
+ },
252
+ {
253
+ "cell_type": "code",
254
+ "execution_count": null,
255
+ "id": "12",
256
+ "metadata": {},
257
+ "outputs": [],
258
+ "source": [
259
+ "df_main"
260
+ ]
261
+ },
262
+ {
263
+ "cell_type": "code",
264
+ "execution_count": null,
265
+ "id": "13",
266
+ "metadata": {},
267
+ "outputs": [],
268
+ "source": [
269
+ "# now extend everyone to 48 months.\n",
270
+ "# Subjects are in the process of consenting for extended\n",
271
+ "# followup. Assume ALL have done so by filling in all\n",
272
+ "# subject schedules to 48m\n",
273
+ "\n",
274
+ "# pivot\n",
275
+ "df_pivot = df_main[\n",
276
+ " (df_main.visit_code_sequence==0) &\n",
277
+ " (df_main.visit_code<2000.0)\n",
278
+ "].pivot_table(index=\"subject_identifier\", columns='visit_code', values='appt_datetime', aggfunc='count')\n",
279
+ "df_pivot.fillna(0, inplace=True)\n",
280
+ "df_pivot.reset_index(inplace=True)\n",
281
+ "df_pivot.rename_axis(\"\", axis=\"columns\", inplace=True)\n",
282
+ "\n",
283
+ "# melt\n",
284
+ "df_pivot = df_pivot.melt(id_vars=\"subject_identifier\", var_name=\"visit_code\", value_name=\"exists\")\n",
285
+ "df_pivot[\"visit_code\"] = df_pivot[\"visit_code\"].astype(float)\n",
286
+ "df_pivot.sort_values([\"subject_identifier\", \"visit_code\"], ascending=True, inplace=True)\n",
287
+ "df_pivot.reset_index(drop=True, inplace=True)\n",
288
+ "\n",
289
+ "# merge in baseline_datetime\n",
290
+ "df_baseline = df_appt[df_appt.visit_code==1000.0][[\"subject_identifier\", \"baseline_datetime\"]]\n",
291
+ "df_pivot = df_pivot.merge(df_baseline, on=[\"subject_identifier\"], how=\"left\")\n",
292
+ "df_pivot.reset_index(drop=True, inplace=True)\n",
293
+ "\n",
294
+ "# merge df_main back in\n",
295
+ "df_pivot = df_pivot.merge(df_main[[\"subject_identifier\", \"visit_code\", \"appt_datetime\", \"appt_status\"]], on=[\"subject_identifier\",\"visit_code\"], how=\"left\")\n",
296
+ "df_pivot"
297
+ ]
298
+ },
299
+ {
300
+ "cell_type": "code",
301
+ "execution_count": null,
302
+ "id": "14",
303
+ "metadata": {},
304
+ "outputs": [],
305
+ "source": [
306
+ "# len(df_pivot[(df_pivot.appt_datetime>=datetime(2025,1,1)) & (df_pivot.visit_code==MONTH48)])/3"
307
+ ]
308
+ },
309
+ {
310
+ "cell_type": "code",
311
+ "execution_count": null,
312
+ "id": "15",
313
+ "metadata": {},
314
+ "outputs": [],
315
+ "source": [
316
+ "# extend no one!\n",
317
+ "# df_pivot = df_pivot[df_pivot.exists==1].copy()\n",
318
+ "# df_pivot.reset_index(drop=True, inplace=True)\n"
319
+ ]
320
+ },
321
+ {
322
+ "cell_type": "code",
323
+ "execution_count": null,
324
+ "id": "16",
325
+ "metadata": {},
326
+ "outputs": [],
327
+ "source": [
328
+ "# add appointments do not have an appt_datetime, so calculate\n",
329
+ "# using the visit schedule relative to baseline_datetime\n",
330
+ "visit_schedule = site_visit_schedules.get_visit_schedule(\"visit_schedule\")\n",
331
+ "schedule = visit_schedule.schedules.get(\"schedule\")\n",
332
+ "mapping = {k: visit.rbase for k,visit in schedule.visits.items()}\n",
333
+ "\n",
334
+ "def estimate_appt_datetime(row):\n",
335
+ " if pd.isna(row[\"appt_datetime\"]):\n",
336
+ " row[\"appt_datetime\"] = row[\"baseline_datetime\"] + mapping.get(str(int(row[\"visit_code\"])))\n",
337
+ " return row\n",
338
+ "\n",
339
+ "df_pivot = df_pivot.apply(estimate_appt_datetime, axis=1)\n",
340
+ "df_pivot.sort_values(by=[\"subject_identifier\", \"visit_code\"], ascending=True, inplace=True)\n",
341
+ "df_pivot.reset_index(drop=True, inplace=True)\n",
342
+ "\n",
343
+ "# merge in assignment\n",
344
+ "df_pivot = df_pivot.merge(df_rando, on=\"subject_identifier\", how=\"left\")\n",
345
+ "df_pivot.reset_index(drop=True, inplace=True)\n",
346
+ "\n",
347
+ "# flag added appointments as NEW\n",
348
+ "df_pivot.loc[df_pivot.exists==0.0, \"appt_status\"] = NEW_APPT\n",
349
+ "\n",
350
+ "print(f\"{len(df_pivot)} appointments\")"
351
+ ]
352
+ },
353
+ {
354
+ "cell_type": "code",
355
+ "execution_count": null,
356
+ "id": "17",
357
+ "metadata": {},
358
+ "outputs": [],
359
+ "source": [
360
+ "# df_subject_appointments is a dataframe of appointments\n",
361
+ "# - only include NEW appointments\n",
362
+ "# - only include appts between today (2025,4,4) and before (2026,3,1).\n",
363
+ "# - exclude the last visit (48m) since no meds are dispensed then.\n",
364
+ "cutoff_date = datetime(2026,3,1)\n",
365
+ "df_subject_appointments = df_pivot[\n",
366
+ " (df_pivot.appt_status==NEW_APPT) &\n",
367
+ " (df_pivot.appt_datetime >= start_from_appt_date) &\n",
368
+ " (df_pivot.appt_datetime < cutoff_date) &\n",
369
+ " (df_pivot.visit_code!=1480.0)\n",
370
+ "].copy()\n",
371
+ "print(f\"{len(df_subject_appointments)} appointments\")"
372
+ ]
373
+ },
374
+ {
375
+ "cell_type": "code",
376
+ "execution_count": null,
377
+ "id": "18",
378
+ "metadata": {},
379
+ "outputs": [],
380
+ "source": [
381
+ "n = df_subject_appointments.subject_identifier.nunique()\n",
382
+ "print(f\"{n} subjects\")\n"
383
+ ]
384
+ },
385
+ {
386
+ "cell_type": "code",
387
+ "execution_count": null,
388
+ "id": "19",
389
+ "metadata": {},
390
+ "outputs": [],
391
+ "source": [
392
+ "(len(df_subject_appointments[df_subject_appointments.appt_datetime>=datetime(2026,1,1)])/36)/5"
393
+ ]
394
+ },
395
+ {
396
+ "cell_type": "code",
397
+ "execution_count": null,
398
+ "id": "20",
399
+ "metadata": {},
400
+ "outputs": [],
401
+ "source": [
402
+ "# summarize the appointments\n",
403
+ "df_summary = df_subject_appointments.visit_code.value_counts().reset_index(name=\"appointments\").sort_values(by=[\"visit_code\"], ascending=True)\n",
404
+ "df_summary[\"cumsum\"] = df_summary.appointments.cumsum()\n",
405
+ "df_summary"
406
+ ]
407
+ },
408
+ {
409
+ "cell_type": "code",
410
+ "execution_count": null,
411
+ "id": "21",
412
+ "metadata": {},
413
+ "outputs": [],
414
+ "source": [
415
+ "df = df_subject_appointments.assignment.value_counts(dropna=False).reset_index()\n",
416
+ "df.rename(columns={\"count\":\"appointments\"}, inplace=True)\n",
417
+ "df[\"bottles\"] = df.appointments * 3\n",
418
+ "df[\"tablets\"] = df.bottles * 128\n",
419
+ "\n",
420
+ "# we need this many bottles / tablets by assignment\n",
421
+ "# filter\n",
422
+ "df.loc[len(df)] = {\"appointments\": df.appointments.sum(), \"bottles\": df.bottles.sum(), \"tablets\": df.tablets.sum()}\n",
423
+ "df"
424
+ ]
425
+ },
426
+ {
427
+ "cell_type": "code",
428
+ "execution_count": null,
429
+ "id": "22",
430
+ "metadata": {},
431
+ "outputs": [],
432
+ "source": [
433
+ "gt = get_great_table(\n",
434
+ " df,\n",
435
+ " \"Table 1: IMP Bottles of 128 needed<BR><small>as of 2025-05-15</small>\",\n",
436
+ " footnote=(\n",
437
+ " \"<ol>\"\n",
438
+ " \"<li>assume all participants consent for extended followup.\"\n",
439
+ " \"<li>Need 3 bottles every three months\"\n",
440
+ " \"<li>48m appointment is excluded\"\n",
441
+ " \"<li>Only prepare for appointments scheduled before 2026-03-01.\"\n",
442
+ " \"</ol>\"\n",
443
+ " ))\n",
444
+ "gt.show()"
445
+ ]
446
+ },
447
+ {
448
+ "cell_type": "code",
449
+ "execution_count": null,
450
+ "id": "23",
451
+ "metadata": {},
452
+ "outputs": [],
453
+ "source": [
454
+ "\n",
455
+ "# save as png\n",
456
+ "gt.save(analysis_folder / \"pharmacy_tbl1.png\")\n",
457
+ "# export to PDF\n",
458
+ "image = Image.open(analysis_folder / \"pharmacy_tbl1.png\")\n",
459
+ "image = image.resize((image.width * 6, image.height * 6), Image.LANCZOS)\n",
460
+ "image.save(analysis_folder / \"pharmacy_tbl1.pdf\", \"PDF\", resolution=800, optimize=True, quality=95)"
461
+ ]
462
+ },
463
+ {
464
+ "cell_type": "code",
465
+ "execution_count": null,
466
+ "id": "24",
467
+ "metadata": {},
468
+ "outputs": [],
469
+ "source": [
470
+ "# now lets look at the stock\n",
471
+ "df_stock = read_frame(Stock.objects.values(\"code\", \"lot_id\", \"container__name\", \"confirmed\", \"allocated\", \"dispensed\", \"qty_in\", \"qty_out\", \"unit_qty_in\", \"unit_qty_out\").all(), verbose=False)\n",
472
+ "\n",
473
+ "# merge in assignment\n",
474
+ "df_lot = read_frame(Lot.objects.values(\"id\", \"assignment__name\").all(), verbose=False)\n",
475
+ "df_lot.rename(columns={\"id\":\"lot_id\", \"assignment__name\": \"assignment\"}, inplace=True)\n",
476
+ "df_stock = df_stock.merge(df_lot[[\"lot_id\", \"assignment\"]], on=\"lot_id\", how=\"left\")\n",
477
+ "df_stock.rename(columns={\"container__name\":\"container\"}, inplace=True)\n",
478
+ "df_stock.reset_index(drop=True, inplace=True)"
479
+ ]
480
+ },
481
+ {
482
+ "cell_type": "code",
483
+ "execution_count": null,
484
+ "id": "25",
485
+ "metadata": {},
486
+ "outputs": [],
487
+ "source": [
488
+ "# merge in container columns\n",
489
+ "df_container = read_frame(Container.objects.all())\n",
490
+ "df_container.rename(columns={\"name\": \"container\", \"display_name\": \"container_display_name\", \"units\": \"container_units\", \"qty\": \"container_qty\"}, inplace=True)\n",
491
+ "df_stock = df_stock.merge(df_container[[\"container\", \"container_display_name\", \"container_type\", \"container_units\", \"container_qty\"]], on=\"container\", how=\"left\")\n",
492
+ "df_stock.reset_index(drop=True, inplace=True)\n",
493
+ "\n",
494
+ "# calculate bal\n",
495
+ "df_stock[\"bal\"] = df_stock[\"unit_qty_in\"] - df_stock[\"unit_qty_out\"]\n"
496
+ ]
497
+ },
498
+ {
499
+ "cell_type": "code",
500
+ "execution_count": null,
501
+ "id": "26",
502
+ "metadata": {},
503
+ "outputs": [],
504
+ "source": [
505
+ "# show the balance of tablets decanted to bottles by assignment (on the EDC)\n",
506
+ "df2 = df_stock[df_stock.container_display_name==\"Bottle 128\"].groupby(by=[\"assignment\"]).bal.agg(\"sum\").reset_index()\n",
507
+ "df2.loc[len(df2)] = {\"bal\": df2.bal.sum()}\n",
508
+ "df2"
509
+ ]
510
+ },
511
+ {
512
+ "cell_type": "code",
513
+ "execution_count": null,
514
+ "id": "27",
515
+ "metadata": {},
516
+ "outputs": [],
517
+ "source": [
518
+ "# some bottles, as of today, have not been captured in the system\n",
519
+ "# here is an estimate of what has been decanted into bottles but not labelled.\n",
520
+ "# in the system, these tablets would appear on the EDC as still in buckets\n",
521
+ "df3 = df2.copy()\n",
522
+ "df3 = df3.drop(len(df3) - 1)\n",
523
+ "placebo_unlabelled = 0 # 21*128*128\n",
524
+ "active_unlabelled = 0 # 25*191*128\n",
525
+ "\n",
526
+ "# adding in the estimates, this is about what we have bottled\n",
527
+ "df3.loc[df3.assignment==\"placebo\", \"bal\"] += placebo_unlabelled\n",
528
+ "df3.loc[df3.assignment==\"active\", \"bal\"] += active_unlabelled\n",
529
+ "df3.loc[len(df3)] = {\"bal\": df3.bal.sum()}\n",
530
+ "df3"
531
+ ]
532
+ },
533
+ {
534
+ "cell_type": "code",
535
+ "execution_count": null,
536
+ "id": "28",
537
+ "metadata": {},
538
+ "outputs": [],
539
+ "source": [
540
+ "gt = get_great_table(\n",
541
+ " df3,\n",
542
+ " \"Table 2: IMP tablets in stock<BR><small>as of 2025-04-04</small>\",\n",
543
+ " # footnote=\"Includes recently decanted but unlabelled bottles\"\n",
544
+ " )\n",
545
+ "gt.show()"
546
+ ]
547
+ },
548
+ {
549
+ "cell_type": "code",
550
+ "execution_count": null,
551
+ "id": "29",
552
+ "metadata": {},
553
+ "outputs": [],
554
+ "source": [
555
+ "# save as png\n",
556
+ "gt.save(analysis_folder / \"pharmacy_tbl2.png\")\n",
557
+ "# export to PDF\n",
558
+ "image = Image.open(analysis_folder / \"pharmacy_tbl2.png\")\n",
559
+ "image = image.resize((image.width * 6, image.height * 6), Image.LANCZOS)\n",
560
+ "image.save(analysis_folder / \"pharmacy_tbl2.pdf\", \"PDF\", resolution=800, optimize=True, quality=95)"
561
+ ]
562
+ },
563
+ {
564
+ "cell_type": "code",
565
+ "execution_count": null,
566
+ "id": "30",
567
+ "metadata": {},
568
+ "outputs": [],
569
+ "source": [
570
+ "# tablets: ordered\n",
571
+ "df_orderitems = read_frame(OrderItem.objects.all())\n",
572
+ "df_orderitems.qty.sum()"
573
+ ]
574
+ },
575
+ {
576
+ "cell_type": "code",
577
+ "execution_count": null,
578
+ "id": "31",
579
+ "metadata": {},
580
+ "outputs": [],
581
+ "source": [
582
+ "# tablets: received\n",
583
+ "df_received_items = read_frame(ReceiveItem.objects.all())\n",
584
+ "df_received_items.unit_qty.sum()"
585
+ ]
586
+ },
587
+ {
588
+ "cell_type": "code",
589
+ "execution_count": null,
590
+ "id": "32",
591
+ "metadata": {},
592
+ "outputs": [],
593
+ "source": [
594
+ "# tablets: received into stock\n",
595
+ "df_stock[df_stock.container_type==\"bucket\"].unit_qty_in.sum()"
596
+ ]
597
+ },
598
+ {
599
+ "cell_type": "code",
600
+ "execution_count": null,
601
+ "id": "33",
602
+ "metadata": {},
603
+ "outputs": [],
604
+ "source": [
605
+ "# tablets: decanted from buckets into bottles\n",
606
+ "df_stock[df_stock.container_type==\"bucket\"].unit_qty_out.sum()"
607
+ ]
608
+ },
609
+ {
610
+ "cell_type": "code",
611
+ "execution_count": null,
612
+ "id": "34",
613
+ "metadata": {},
614
+ "outputs": [],
615
+ "source": [
616
+ "# tablets: total in bottles\n",
617
+ "df_stock[df_stock.container_type==\"Bottle\"].unit_qty_in.sum()"
618
+ ]
619
+ },
620
+ {
621
+ "cell_type": "code",
622
+ "execution_count": null,
623
+ "id": "35",
624
+ "metadata": {},
625
+ "outputs": [],
626
+ "source": [
627
+ "# tablets: total bottles available / not yet dispensed BY ASSIGNMENT\n",
628
+ "# the total matches the total above for column \"bal\"\n",
629
+ "df4 = df_stock[(df_stock.container_type==\"Bottle\") & (df_stock.confirmed==True) & (df_stock.dispensed==True)].groupby(by=[\"assignment\"]).unit_qty_in.sum().reset_index()\n",
630
+ "df4[\"subtotal\"] = np.nan\n",
631
+ "df4.loc[len(df4)] = {\"subtotal\": df4.unit_qty_in.sum()}\n",
632
+ "df4[\"dispensed\"] = True\n",
633
+ "\n",
634
+ "df5 = df_stock[(df_stock.container_type==\"Bottle\") & (df_stock.confirmed==True) & (df_stock.dispensed==False)].groupby(by=[\"assignment\"]).unit_qty_in.sum().reset_index()\n",
635
+ "df5.loc[df5.assignment==\"placebo\", \"unit_qty_in\"] += placebo_unlabelled\n",
636
+ "df5.loc[df5.assignment==\"active\", \"unit_qty_in\"] += active_unlabelled\n",
637
+ "df5[\"subtotal\"] = np.nan\n",
638
+ "df5.loc[len(df5)] = {\"subtotal\" : df5.unit_qty_in.sum()}\n",
639
+ "df5[\"dispensed\"] = False\n",
640
+ "\n",
641
+ "df6 = pd.concat([df4, df5])\n",
642
+ "df6[\"total\"] = np.nan\n",
643
+ "df6.reset_index(drop=True, inplace=True)\n",
644
+ "df6.loc[len(df6)] = {\"total\": df6.subtotal.sum()}\n",
645
+ "df6 = df6[[\"dispensed\", \"assignment\", \"unit_qty_in\", \"subtotal\", \"total\"]]\n",
646
+ "df6"
647
+ ]
648
+ },
649
+ {
650
+ "cell_type": "code",
651
+ "execution_count": null,
652
+ "id": "36",
653
+ "metadata": {},
654
+ "outputs": [],
655
+ "source": []
656
+ },
657
+ {
658
+ "cell_type": "code",
659
+ "execution_count": null,
660
+ "id": "37",
661
+ "metadata": {},
662
+ "outputs": [],
663
+ "source": []
664
+ },
665
+ {
666
+ "cell_type": "code",
667
+ "execution_count": null,
668
+ "id": "38",
669
+ "metadata": {},
670
+ "outputs": [],
671
+ "source": []
672
+ },
673
+ {
674
+ "cell_type": "code",
675
+ "execution_count": null,
676
+ "id": "39",
677
+ "metadata": {},
678
+ "outputs": [],
679
+ "source": [
680
+ "from meta_visit_schedule.constants import MONTH36\n",
681
+ "\n",
682
+ "df_appt[(df_appt.visit_code_str==MONTH36) & (df_appt.appt_datetime >= datetime(2024,12,15)) & (df_appt.appt_status==NEW_APPT) & (df_appt.appt_datetime <= datetime(2026,2,28))]"
683
+ ]
684
+ },
685
+ {
686
+ "cell_type": "code",
687
+ "execution_count": null,
688
+ "id": "40",
689
+ "metadata": {},
690
+ "outputs": [],
691
+ "source": [
692
+ "def remove_subjects_where_stock_on_site(stock_request: StockRequest, df: pd.DataFrame):\n",
693
+ " stock_model_cls = django_apps.get_model(\"edc_pharmacy.Stock\")\n",
694
+ " qs_stock = (\n",
695
+ " stock_model_cls.objects.values(\n",
696
+ " \"allocation__registered_subject__subject_identifier\", \"code\"\n",
697
+ " )\n",
698
+ " .filter(location=stock_request.location, qty=1)\n",
699
+ " .annotate(count=Count(\"allocation__registered_subject__subject_identifier\"))\n",
700
+ " )\n",
701
+ " df_stock = read_frame(qs_stock)\n",
702
+ " df_stock = df_stock.rename(\n",
703
+ " columns={\n",
704
+ " \"allocation__registered_subject__subject_identifier\": \"subject_identifier\",\n",
705
+ " \"count\": \"stock_qty\",\n",
706
+ " }\n",
707
+ " )\n",
708
+ " if not df.empty and not df_stock.empty:\n",
709
+ " df_subject = df.copy()\n",
710
+ " df_subject[\"code\"] = None\n",
711
+ " df = df.merge(df_stock, on=\"subject_identifier\", how=\"left\")\n",
712
+ " for index, row in df.iterrows():\n",
713
+ " qty_needed = stock_request.containers_per_subject - len(df[df.subject_identifier == row.subject_identifier])\n",
714
+ " if qty_needed > 0:\n",
715
+ " for _ in range(0, qty_needed):\n",
716
+ " df = pd.concat([df, df_subject])\n",
717
+ " else:\n",
718
+ " df[\"code\"] = None\n",
719
+ " df[\"stock_qty\"] = 0.0\n",
720
+ " df = df.reset_index(drop=True)\n",
721
+ " return df\n"
722
+ ]
723
+ },
724
+ {
725
+ "cell_type": "code",
726
+ "execution_count": null,
727
+ "id": "41",
728
+ "metadata": {},
729
+ "outputs": [],
730
+ "source": [
731
+ "def pad_with_null_rows(df, qty_needed):\n",
732
+ " padded_data = []\n",
733
+ " for index, row in df.iterrows():\n",
734
+ " customer = row['subject']\n",
735
+ " products = row['product_code']\n",
736
+ " # Pad the products list with None to make its length x\n",
737
+ " products += [None] * (qty_needed - len(products))\n",
738
+ " # Create x rows for each customer\n",
739
+ " for product in products:\n",
740
+ " padded_data.append({'customer': customer, 'product_code': product})\n",
741
+ " return pd.DataFrame(padded_data)"
742
+ ]
743
+ },
744
+ {
745
+ "cell_type": "code",
746
+ "execution_count": null,
747
+ "id": "42",
748
+ "metadata": {},
749
+ "outputs": [],
750
+ "source": [
751
+ "pk = \"5455cf66-b8e5-449c-a1e8-24d3325026d7\"\n",
752
+ "stock_request = StockRequest.objects.get(pk=pk)\n"
753
+ ]
754
+ },
755
+ {
756
+ "cell_type": "code",
757
+ "execution_count": null,
758
+ "id": "43",
759
+ "metadata": {},
760
+ "outputs": [],
761
+ "source": [
762
+ "df_subjects = get_next_scheduled_visit_for_subjects_df(stock_request)\n",
763
+ "df_subjects"
764
+ ]
765
+ },
766
+ {
767
+ "cell_type": "code",
768
+ "execution_count": null,
769
+ "id": "44",
770
+ "metadata": {},
771
+ "outputs": [],
772
+ "source": []
773
+ },
774
+ {
775
+ "cell_type": "code",
776
+ "execution_count": null,
777
+ "id": "45",
778
+ "metadata": {},
779
+ "outputs": [],
780
+ "source": [
781
+ "df = df_subjects.copy()\n",
782
+ "stock_model_cls = django_apps.get_model(\"edc_pharmacy.Stock\")\n",
783
+ "qs_stock = (\n",
784
+ " stock_model_cls.objects.values(\n",
785
+ " \"allocation__registered_subject__subject_identifier\", \"code\"\n",
786
+ " )\n",
787
+ " .filter(location=stock_request.location, qty=1)\n",
788
+ " .annotate(count=Count(\"allocation__registered_subject__subject_identifier\"))\n",
789
+ ")\n",
790
+ "df_stock = read_frame(qs_stock)\n",
791
+ "df_stock = df_stock.rename(\n",
792
+ " columns={\n",
793
+ " \"allocation__registered_subject__subject_identifier\": \"subject_identifier\",\n",
794
+ " \"count\": \"stock_qty\",\n",
795
+ " }\n",
796
+ ")\n",
797
+ "df_stock"
798
+ ]
799
+ },
800
+ {
801
+ "cell_type": "code",
802
+ "execution_count": null,
803
+ "id": "46",
804
+ "metadata": {},
805
+ "outputs": [],
806
+ "source": [
807
+ "df.merge(df_stock, on=\"subject_identifier\", how=\"left\")"
808
+ ]
809
+ },
810
+ {
811
+ "cell_type": "code",
812
+ "execution_count": null,
813
+ "id": "47",
814
+ "metadata": {},
815
+ "outputs": [],
816
+ "source": [
817
+ "if not df.empty and not df_stock.empty:\n",
818
+ " df_subject = df.copy()\n",
819
+ " df_subject[\"code\"] = None\n",
820
+ " df = df.merge(df_stock, on=\"subject_identifier\", how=\"left\")\n",
821
+ " for index, row in df.iterrows():\n",
822
+ " qty_needed = stock_request.containers_per_subject - len(df[df.subject_identifier == row.subject_identifier])\n",
823
+ " if qty_needed > 0:\n",
824
+ " for _ in range(0, qty_needed):\n",
825
+ " df = pd.concat([df, df_subject])\n",
826
+ "else:\n",
827
+ " df[\"code\"] = None\n",
828
+ "df[\"stock_qty\"] = 0.0\n",
829
+ "df = df.reset_index(drop=True)\n",
830
+ "df"
831
+ ]
832
+ },
833
+ {
834
+ "cell_type": "code",
835
+ "execution_count": null,
836
+ "id": "48",
837
+ "metadata": {},
838
+ "outputs": [],
839
+ "source": [
840
+ "df.loc[df.index.repeat(3)]"
841
+ ]
842
+ },
843
+ {
844
+ "cell_type": "code",
845
+ "execution_count": null,
846
+ "id": "49",
847
+ "metadata": {},
848
+ "outputs": [],
849
+ "source": [
850
+ "if not df.empty and not df_stock.empty:\n",
851
+ " df = df.merge(df_stock, on=\"subject_identifier\", how=\"left\")\n",
852
+ "else:\n",
853
+ " df[\"code\"] = None\n",
854
+ "df[\"stock_qty\"] = 0.0\n",
855
+ "df = df.reset_index(drop=True)\n",
856
+ "df"
857
+ ]
858
+ },
859
+ {
860
+ "cell_type": "code",
861
+ "execution_count": null,
862
+ "id": "50",
863
+ "metadata": {},
864
+ "outputs": [],
865
+ "source": [
866
+ "df = remove_subjects_where_stock_on_site(stock_request, df_subjects)\n",
867
+ "df"
868
+ ]
869
+ },
870
+ {
871
+ "cell_type": "code",
872
+ "execution_count": null,
873
+ "id": "51",
874
+ "metadata": {},
875
+ "outputs": [],
876
+ "source": [
877
+ "df_instock = df[~df.code.isna()]\n",
878
+ "df_instock = df_instock.reset_index(drop=True)\n",
879
+ "df_instock = df_instock.sort_values(by=[\"subject_identifier\"])\n",
880
+ "\n",
881
+ "df_nostock = df[df.code.isna()]\n",
882
+ "df_nostock = df_nostock.reset_index(drop=True)\n",
883
+ "df_nostock = df_nostock.loc[\n",
884
+ " df_nostock.index.repeat(stock_request.containers_per_subject)\n",
885
+ "].reset_index(drop=True)\n",
886
+ "df_nostock = df_nostock.sort_values(by=[\"subject_identifier\"])\n",
887
+ "df_nostock[\"code\"] = df_nostock[\"code\"].fillna(\"---\")\n"
888
+ ]
889
+ },
890
+ {
891
+ "cell_type": "code",
892
+ "execution_count": null,
893
+ "id": "52",
894
+ "metadata": {},
895
+ "outputs": [],
896
+ "source": []
897
+ },
898
+ {
899
+ "cell_type": "code",
900
+ "execution_count": null,
901
+ "id": "53",
902
+ "metadata": {},
903
+ "outputs": [],
904
+ "source": []
905
+ },
906
+ {
907
+ "cell_type": "code",
908
+ "execution_count": null,
909
+ "id": "54",
910
+ "metadata": {},
911
+ "outputs": [],
912
+ "source": [
913
+ "no_stock_for_subjects_df()"
914
+ ]
915
+ },
916
+ {
917
+ "cell_type": "code",
918
+ "execution_count": null,
919
+ "id": "55",
920
+ "metadata": {},
921
+ "outputs": [],
922
+ "source": [
923
+ "df_schedule = read_frame(SubjectScheduleHistory.objects.values(\"subject_identifier\", \"visit_schedule_name\",\"schedule_name\", \"offschedule_datetime\").all())\n"
924
+ ]
925
+ },
926
+ {
927
+ "cell_type": "code",
928
+ "execution_count": null,
929
+ "id": "56",
930
+ "metadata": {},
931
+ "outputs": [],
932
+ "source": [
933
+ "df_schedule = df_schedule[(df_schedule.visit_schedule_name==\"visit_schedule\") & (df_schedule.schedule_name==\"schedule\") & df_schedule.offschedule_datetime.isna()]\n",
934
+ "df_schedule.reset_index(drop=True, inplace=True)"
935
+ ]
936
+ },
937
+ {
938
+ "cell_type": "code",
939
+ "execution_count": null,
940
+ "id": "57",
941
+ "metadata": {},
942
+ "outputs": [],
943
+ "source": [
944
+ "df_stock = read_frame(Stock.objects.all(), verbose=False)\n",
945
+ "df_stock_on_site = df_stock[(df_stock.confirmed_at_site==True) & (df_stock.dispensed==False)].copy()\n",
946
+ "df_stock_on_site.reset_index(drop=True, inplace=True)\n",
947
+ "df_stock_on_site = df_stock_on_site.drop(columns=[\"subject_identifier\"])\n"
948
+ ]
949
+ },
950
+ {
951
+ "cell_type": "code",
952
+ "execution_count": null,
953
+ "id": "58",
954
+ "metadata": {},
955
+ "outputs": [],
956
+ "source": [
957
+ "df_allocation = read_frame(Allocation.objects.values(\"id\", \"registered_subject\").all(), verbose=False)\n",
958
+ "df_rs = read_frame(RegisteredSubject.objects.values(\"id\", \"subject_identifier\").all(), verbose=False)\n",
959
+ "df_allocation = df_allocation.merge(df_rs[[\"id\", \"subject_identifier\"]], how=\"left\", left_on=\"registered_subject\", right_on=\"id\", suffixes=[\"_allocation\", \"_rs\"])"
960
+ ]
961
+ },
962
+ {
963
+ "cell_type": "code",
964
+ "execution_count": null,
965
+ "id": "59",
966
+ "metadata": {},
967
+ "outputs": [],
968
+ "source": [
969
+ "df_stock_on_site = df_stock_on_site.merge(df_allocation[[\"id_allocation\", \"subject_identifier\"]], how=\"left\", left_on=\"allocation\", right_on=\"id_allocation\")"
970
+ ]
971
+ },
972
+ {
973
+ "cell_type": "code",
974
+ "execution_count": null,
975
+ "id": "60",
976
+ "metadata": {},
977
+ "outputs": [],
978
+ "source": [
979
+ "df = pd.merge(df_schedule[[\"subject_identifier\", 'offschedule_datetime']], df_stock_on_site, on=\"subject_identifier\", how=\"left\")\n",
980
+ "df= df[df.code.isna()][[\"subject_identifier\", ]].sort_values(by=[\"subject_identifier\"]).reset_index(drop=True)"
981
+ ]
982
+ },
983
+ {
984
+ "cell_type": "code",
985
+ "execution_count": null,
986
+ "id": "61",
987
+ "metadata": {},
988
+ "outputs": [],
989
+ "source": [
990
+ "df_appt = get_next_scheduled_visit_for_subjects_df()\n",
991
+ "df_appt = df_appt[[\"subject_identifier\", \"site_id\", \"visit_code\", \"appt_datetime\", \"baseline_datetime\"]].copy()\n",
992
+ "df_appt.reset_index(drop=True, inplace=True)"
993
+ ]
994
+ },
995
+ {
996
+ "cell_type": "code",
997
+ "execution_count": null,
998
+ "id": "62",
999
+ "metadata": {},
1000
+ "outputs": [],
1001
+ "source": [
1002
+ "\n",
1003
+ "df = df.merge(df_appt, how=\"left\", on=\"subject_identifier\")\n",
1004
+ "df = df[(df.appt_datetime.notna())]\n",
1005
+ "df.reset_index(drop=True, inplace=True)"
1006
+ ]
1007
+ },
1008
+ {
1009
+ "cell_type": "code",
1010
+ "execution_count": null,
1011
+ "id": "63",
1012
+ "metadata": {},
1013
+ "outputs": [],
1014
+ "source": [
1015
+ "utc_now = pd.Timestamp.utcnow().tz_localize(None)\n",
1016
+ "df[\"relative_days\"] = (df.appt_datetime - utc_now).dt.days\n",
1017
+ "df_final = df[(df.relative_days >= -105)].copy()\n",
1018
+ "df_final.reset_index(drop=True, inplace=True)\n",
1019
+ "df_final"
1020
+ ]
1021
+ },
1022
+ {
1023
+ "cell_type": "code",
1024
+ "execution_count": null,
1025
+ "id": "64",
1026
+ "metadata": {},
1027
+ "outputs": [],
1028
+ "source": [
1029
+ "RegisteredSubject.objects.filter(site_id=10)"
1030
+ ]
1031
+ },
1032
+ {
1033
+ "cell_type": "code",
1034
+ "execution_count": null,
1035
+ "id": "65",
1036
+ "metadata": {},
1037
+ "outputs": [],
1038
+ "source": []
1039
+ },
1040
+ {
1041
+ "cell_type": "code",
1042
+ "execution_count": null,
1043
+ "id": "66",
1044
+ "metadata": {},
1045
+ "outputs": [],
1046
+ "source": []
1047
+ }
1048
+ ],
1049
+ "metadata": {
1050
+ "kernelspec": {
1051
+ "display_name": "Python 3",
1052
+ "language": "python",
1053
+ "name": "python3"
1054
+ },
1055
+ "language_info": {
1056
+ "codemirror_mode": {
1057
+ "name": "ipython",
1058
+ "version": 2
1059
+ },
1060
+ "file_extension": ".py",
1061
+ "mimetype": "text/x-python",
1062
+ "name": "python",
1063
+ "nbconvert_exporter": "python",
1064
+ "pygments_lexer": "ipython2",
1065
+ "version": "2.7.6"
1066
+ }
1067
+ },
1068
+ "nbformat": 4,
1069
+ "nbformat_minor": 5
1070
+ }