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