File size: 33,095 Bytes
2887d2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2543663
 
 
 
 
 
 
 
 
2887d2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88f716d
 
 
 
2887d2e
88f716d
2887d2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650

import datetime 
import gradio as gr
import itertools
import numpy as np
import os
import pandas as pd
import re

from collections import defaultdict


def convert_date(d):
    return datetime.datetime.strptime(str(d), "%B %d, %Y").date()


def main(files):
    available_files = []
    fname_dict = {os.path.basename(f).split(".")[0]: f for f in files}
    for fname, fpath in fname_dict.items():
        available_files.append(fname)
        if fname == "qgenda_schedule":
            schedule = pd.read_excel(fpath, header=None)
        elif fname == "info":
            core_rotations = pd.read_excel(fpath, sheet_name="Core Rotations", header=None)
            call_rotations = pd.read_excel(fpath, sheet_name="Call Rotations", header=None)
            holidays = pd.read_excel(fpath, sheet_name="Holidays", header=None)
            pto = pd.read_excel(fpath, sheet_name="PTO", header=None)
            academic_years = pd.read_excel(fpath, sheet_name="AYs")
            call_shifts = pd.read_excel(fpath, sheet_name="Weekend Call Shifts", header=None)
            roster = pd.read_excel(fpath, sheet_name="Resident Roster") 
    assert all(x in ["qgenda_schedule", "info"] for x in available_files)

    # -- for debugging, load files from disk --   
    # Load in all files
    # schedule = pd.read_excel("key_files/qgenda_schedule.xlsx", header=None) # remove first 3 rows
    # core_rotations = pd.read_excel("key_files/rotations.xlsx", sheet_name="core", header=None)
    # call_rotations = pd.read_excel("key_files/rotations.xlsx", sheet_name="call", header=None)
    # holidays = pd.read_excel("key_files/holidays.xlsx", header=None)
    # pto = pd.read_excel("key_files/pto.xlsx", header=None)
    # academic_years = pd.read_excel("key_files/academic_years.xlsx")
    # call_shifts = pd.read_excel("key_files/call_shifts.xlsx", header=None)
    # roster = pd.read_excel("key_files/resident_roster.xlsx")
    # -- 

    schedule = schedule.iloc[3:].reset_index(drop=True) # remove first 3 rows
    core_rotations = core_rotations.iloc[:, 0].tolist()
    call_rotations = call_rotations.iloc[:, 0].tolist()
    holidays = [_.date() for _ in holidays.iloc[:, 0].tolist()]
    pto = pto.iloc[:, 0].tolist()
    call_shifts = call_shifts.iloc[:, 0].tolist()

    # remove stuff at the bottom
    try:
        try:
            last_row = schedule.iloc[:, 0].tolist().index("Schedule Notes:")
        except ValueError:
            last_row = schedule.iloc[:, 0].tolist().index("Phone Numbers")
    except ValueError:
        last_row = len(schedule) + 1

    schedule = schedule.iloc[:last_row].reset_index(drop=True)

    # remove days of week rows
    remove_rows = []
    for idx, i in enumerate(schedule.iloc[:, 0].tolist()):
        if i == "Sunday":
            remove_rows.append(idx)

    schedule = schedule.drop(remove_rows).reset_index(drop=True)

    # split the schedule by weeks
    sundays, week_rows = [], []
    for idx, i in enumerate(schedule.iloc[:, 0].tolist()):
        try:
            tmp_date = convert_date(i)
            sundays.append(tmp_date)
            week_rows.append(idx)
        except ValueError:
            continue


    weekly_schedule = {}
    for idx, each_sunday in enumerate(sundays):
        if idx == len(sundays) - 1:
            weekly_schedule[each_sunday] = schedule.iloc[week_rows[idx]:].reset_index(drop=True)
        else:
            weekly_schedule[each_sunday] = schedule.iloc[week_rows[idx]:week_rows[idx+1]].reset_index(drop=True)


    daily_schedule = {}
    for each_week, each_schedule in weekly_schedule.items():
        tmp_daily_schedule = {}
        list_by_day = [each_schedule.iloc[:, idx:idx+2] for idx in range(0, each_schedule.shape[1], 2)]
        for each_day in list_by_day:
            tmp_date = convert_date(each_day.iloc[0, 0])
            tmp_daily_schedule[tmp_date] = defaultdict(list)
            tmp_residents, tmp_tasks = each_day.iloc[1:, 0].tolist(), each_day.iloc[1:, 1].tolist()
            tmp_tasks = [re.sub(r" \([A-Z]+\)", "", str(_)) for _ in tmp_tasks]
            # PTO supersedes everything else, meaning if you have a PTO task then all other tasks are void
            pto_residents = []
            for resident, task in zip(tmp_residents, tmp_tasks):
                if task in pto:
                    pto_residents.append(resident)
            for resident, task in zip(tmp_residents, tmp_tasks):
                if str(resident) == "nan" or resident in pto_residents:
                    if task not in pto:
                        continue
                if tmp_date.weekday() in [5, 6] and task in core_rotations + holidays:
                    continue
                tmp_daily_schedule[tmp_date][resident].append(task)
        daily_schedule.update(tmp_daily_schedule)


    weekly_schedule_with_tasks = {}
    for each_week, each_schedule in weekly_schedule.items():
        tmp_weekly_schedule = {}
        tmp_weekly_schedule[each_week] = defaultdict(list)
        list_by_day = [each_schedule.iloc[:, idx:idx+2] for idx in range(0, each_schedule.shape[1], 2)]
        for each_day in list_by_day:
            tmp_date = convert_date(each_day.iloc[0, 0])
            tmp_residents, tmp_tasks = each_day.iloc[1:, 0].tolist(), each_day.iloc[1:, 1].tolist()
            tmp_tasks = [re.sub(r" \([A-Z]+\)", "", str(_)) for _ in tmp_tasks]
            pto_residents = []
            for resident, task in zip(tmp_residents, tmp_tasks):
                if task in pto:
                    pto_residents.append(resident)
            for resident, task in zip(tmp_residents, tmp_tasks):
                if str(resident) == "nan" or resident in pto_residents:
                    continue
                task = re.sub(r" \([A-Z]+\)", "", task)
                if tmp_date.weekday() in [5, 6] and task in core_rotations:
                    continue
                tmp_weekly_schedule[each_week][resident].append(task)
        weekly_schedule_with_tasks.update(tmp_weekly_schedule)


    # create a calendar for each resident
    resident_calendar = defaultdict(dict)
    for tmp_date, resident_dict in daily_schedule.items():
        for resident_name, tasks in resident_dict.items():
            resident_calendar[resident_name][tmp_date] = tasks

    resident_weekly_calendar = defaultdict(dict)
    for tmp_week, resident_dict in weekly_schedule_with_tasks.items():
        for resident_name, tasks in resident_dict.items():
            resident_weekly_calendar[resident_name][tmp_week] = tasks

    # count number of weeks for each core rotation
    resident_core_rotation_tallies = {}
    for row_idx, row in academic_years.iterrows():
        # need to subtract 1 day from start since it is a Monday and ED/NF/Consult start on Sunday so will affect
        # those who start at the beginning of the year
        start, stop = row["Start"].date() - datetime.timedelta(days=1), row["Stop"].date()
        ay = row["Year"]
        resident_core_rotation_tallies[ay] = defaultdict(dict)
        for resident_name, calendar in resident_weekly_calendar.items():
            tmp_rotation_counts = defaultdict(list)
            tmp_calendar = {k: v for k, v in calendar.items() if k >= start and k < stop}
            for each_week, weekly_tasks in tmp_calendar.items():
                tmp_tasks, tmp_counts = np.unique(weekly_tasks, return_counts=True)
                tmp_task_counts = {k: v for k, v in zip(tmp_tasks, tmp_counts)}
                for k, v in tmp_task_counts.items():
                    if k in core_rotations and v >= 3:
                        tmp_rotation_counts[k].append(1) 
            tmp_rotation_counts = {k: np.sum(v) for k, v in tmp_rotation_counts.items()} 
            for each_core_rotation in core_rotations:
                if each_core_rotation not in tmp_rotation_counts:
                    tmp_rotation_counts[each_core_rotation] = 0
            resident_core_rotation_tallies[ay][resident_name] = tmp_rotation_counts

    for each_ay, each_resident_dict in resident_core_rotation_tallies.items():
        for each_resident, resident_rotation_counts in each_resident_dict.items():
            resident_rotation_counts["Total Nucs"] = resident_rotation_counts["Nucs"] + resident_rotation_counts["CV Nucs"]

    # it's easier to keep track of NF, consult by days rather than weeks due to potential random swaps, sick call, and split weeks
    # also keep track of nucs days since for nucs time requirements days may be more important than weeks
    resident_rotations_by_days = {}
    for row_idx, row in academic_years.iterrows():
        # need to subtract 1 day from start since it is a Monday and ED/NF/Consult start on Sunday so will affect
        # those who start at the beginning of the year
        start, stop = row["Start"].date() - datetime.timedelta(days=1), row["Stop"].date()
        ay = row["Year"]
        resident_rotations_by_days[ay] = defaultdict(dict)
        for resident_name, calendar in resident_weekly_calendar.items():
            tmp_rotation_counts = defaultdict(list)
            tmp_calendar = {k: v for k, v in calendar.items() if k >= start and k < stop}
            for each_week, weekly_tasks in tmp_calendar.items():
                tmp_tasks, tmp_counts = np.unique(weekly_tasks, return_counts=True)
                tmp_task_counts = {k: v for k, v in zip(tmp_tasks, tmp_counts)}
                for k, v in tmp_task_counts.items():
                    if k in core_rotations + call_rotations:
                        tmp_rotation_counts[k].append(v) 
            tmp_rotation_counts = {k: np.sum(v) for k, v in tmp_rotation_counts.items()} 
            for each_core_rotation in core_rotations + call_rotations:
                if each_core_rotation not in tmp_rotation_counts:
                    tmp_rotation_counts[each_core_rotation] = 0
            resident_rotations_by_days[ay][resident_name] = tmp_rotation_counts

    for each_ay, each_resident_dict in resident_rotations_by_days.items():
        for each_resident, resident_rotation_counts in each_resident_dict.items():
            resident_rotation_counts["Total Nucs"] = resident_rotation_counts["Nucs"] + resident_rotation_counts["CV Nucs"]

    for each_ay, each_resident_dict in resident_rotations_by_days.items():
        for each_resident, resident_rotation_counts in each_resident_dict.items():
            resident_core_rotation_tallies[each_ay][each_resident]["ED"] = int(np.round(resident_rotation_counts["ED"] / 6))
            resident_core_rotation_tallies[each_ay][each_resident]["Consult"] = int(np.round(resident_rotation_counts["Consult"] / 6))
            resident_core_rotation_tallies[each_ay][each_resident]["NF"] = int(np.round(resident_rotation_counts["NF"] / 6))

    # count weekend call shifts (Sat Consult, Sat NF, In-House, Sat EDD)
    # does not include Sundays from NF, consult, ED rotation or Angio/IR call)
    call_shifts_worked = {}
    for row_idx, row in academic_years.iterrows():
        # note here we do NOT subtract 1 day from start because if you are assigned to Sunday in-house shift 
        # prior to AY start it counts for the that AY
        start, stop = row["Start"].date(), row["Stop"].date()
        ay = row["Year"]
        call_shifts_worked[ay] = defaultdict(dict)
        for resident_name, calendar in resident_calendar.items(): # use daily calendar
            tmp_call_shifts = defaultdict(list)
            tmp_calendar = {k: v for k, v in calendar.items() if k >= start and k < stop}
            for each_day, daily_tasks in tmp_calendar.items():
                for each_task in daily_tasks:
                    if each_task in call_shifts:
                        tmp_call_shifts[each_task].append(1)
            tmp_call_shifts = {k: np.sum(v) for k, v in tmp_call_shifts.items()} 
            for each_call_shift in call_shifts:
                if each_call_shift not in tmp_call_shifts:
                    tmp_call_shifts[each_call_shift] = 0
            call_shifts_worked[ay][resident_name] = tmp_call_shifts


    # count holidays worked (call shifts only)
    # differentiates between working on the actual holiday and holiday weekend
    def count_holidays_worked(which_holidays):
        holidays_worked = {}
        for row_idx, row in academic_years.iterrows():
            start, stop = row["Start"].date(), row["Stop"].date()
            ay = row["Year"]
            holidays_worked[ay] = defaultdict(dict)
            for resident_name, calendar in resident_calendar.items():
                tmp_holidays = []
                tmp_calendar = {k: v for k, v in calendar.items() if k >= start and k < stop and k in which_holidays}
                for each_holiday, daily_tasks in tmp_calendar.items():
                    for each_task in daily_tasks:
                        if each_task in call_rotations + call_shifts:
                            tmp_holidays.append(1)
                            break # if stack shifts, break so it doesn't double count
                holidays_worked[ay][resident_name] = int(np.sum(tmp_holidays))
        return holidays_worked


    holidays_worked = count_holidays_worked(holidays)

    # identify which holidays are Christmas, New Years, and Thanksgiving
    # Thanksgiving is the only holiday in November 
    thanksgiving = [_ for _ in holidays if _.month == 11]
    # consider observed dates as well 
    christmas = [_ for _ in holidays if _.month == 12 and abs(int((datetime.date(_.year, 12, 25) - _).days)) < 2]
    new_years_candidates = [datetime.date(2000, 1, 1), datetime.date(2000, 12, 31), datetime.date(2000, 1, 2)]
    new_years = []
    for _ in holidays:
        if _.month == 1 and _.day in [1, 2] or _.month == 12 and _.day == 31:
            new_years.append(_)
    thanksgiving_worked = count_holidays_worked(thanksgiving)
    christmas_worked = count_holidays_worked(christmas)
    new_years_worked = count_holidays_worked(new_years)

    # also count weekends for Thanksgiving, Christmas, New Years
    # if you work more than one day for a weekend, it will count for more than 1
    # since thanksgiving is always Thurs, it's easy...
    thanksgiving_weekend = []

    for each_thanksgiving in thanksgiving:
        tmp_weekend = []
        for each_day in [*daily_schedule]:
            if int((each_day - each_thanksgiving).days) in [2, 3]:
                assert each_day.weekday() in [5, 6]
                tmp_weekend.append(each_day)
        thanksgiving_weekend.append(tmp_weekend)

    # Christmas/NY a bit trickier
    # first identify the Christmas and New Year's holiday weeks (AKA which weeks contain the observed holiday)
    # remember- the week start always precedes the holiday
    # make dict where key is week and value is all days in that week
    weeks_dict = {}
    for each_week in [*weekly_schedule]:
        # week starts on Sunday
        assert each_week.weekday() == 6
        weeks_dict[each_week] = [each_week + datetime.timedelta(days=_) for _ in range(7)]
        assert len(weeks_dict[each_week]) == 7

    # there are 3 weekends here- weekend before Xmas holiday week, weekend in between, weekend after New Year's holiday week
    # technically the middle one is the worst
    # also for some people they may have Xmas holiday week off but then work weekend after New Year's holiday week
    #   which is really not that bad, but this is annoying to take into account so will just treat them all the same 
    christmas_weeks, new_years_weeks = [], []
    for each_week, days_in_week in weeks_dict.items():
        if len(list(set(days_in_week) & set(christmas))) > 0:
            christmas_weeks.append(each_week)
        if len(list(set(days_in_week) & set(new_years))) > 0:
            new_years_weeks.append(each_week)
        
    christmas_new_years_weekend = []
    for each_week in christmas_weeks:
        christmas_new_years_weekend.append([each_week, each_week - datetime.timedelta(days=1)]) # Sunday
    for each_week in new_years_weeks:
        christmas_new_years_weekend.append([each_week, each_week - datetime.timedelta(days=1)])
        christmas_new_years_weekend.append([each_week + datetime.timedelta(days=7), each_week + datetime.timedelta(days=6)])


    def count_holiday_weekends_worked(which_holidays):
        holidays_worked = {}
        for row_idx, row in academic_years.iterrows():
            start, stop = row["Start"].date(), row["Stop"].date()
            ay = row["Year"]
            holidays_worked[ay] = defaultdict(dict)
            for resident_name, calendar in resident_calendar.items():
                tmp_holidays = []
                tmp_calendar = {k: v for k, v in calendar.items() if k >= start and k < stop and k in list(itertools.chain(*which_holidays))}
                tmp_weekend_task_dict = defaultdict(list)
                for idx, each_weekend in enumerate(which_holidays):
                    for each_day, daily_tasks in tmp_calendar.items():
                        if each_day in each_weekend:
                            tmp_weekend_task_dict[idx].extend(daily_tasks)
                for each_weekend, weekend_tasks in tmp_weekend_task_dict.items():
                    for each_task in weekend_tasks:
                        if each_task in call_rotations + call_shifts: 
                            tmp_holidays.append(1)
                            break
                holidays_worked[ay][resident_name] = int(np.sum(tmp_holidays))
        return holidays_worked


    thanksgiving_weekends_worked = count_holiday_weekends_worked(thanksgiving_weekend)
    christmas_new_years_weekends_worked = count_holiday_weekends_worked(christmas_new_years_weekend)

    # Count PTO days
    pto_days = {}
    for row_idx, row in academic_years.iterrows():
        start, stop = row["Start"].date(), row["Stop"].date()
        ay = row["Year"]
        pto_days[ay] = defaultdict(dict)
        for resident_name, calendar in resident_calendar.items(): # use daily calendar
            tmp_pto_days = defaultdict(list)
            tmp_calendar = {k: v for k, v in calendar.items() if k >= start and k < stop}
            in_house_days = 0
            for each_day, daily_tasks in tmp_calendar.items():
                # doesn't count if weekend or holiday
                for each_task in daily_tasks:
                    if each_task in pto and each_day.weekday() not in [5, 6]:
                        if each_task == "LOA":
                        # Easier to see how many LOA weeks if we just include holidays
                            tmp_pto_days[each_task].append(1)
                        elif each_task != "LOA" and each_day not in holidays:
                            tmp_pto_days[each_task].append(1)
                    if each_task == "In-House":
                        in_house_days += 1
            tmp_pto_days = {k: np.sum(v) for k, v in tmp_pto_days.items()} 
            # treat VACATION and HOLIDAY the same then subtract in house days 
            for pto_day_type in pto:
                if pto_day_type not in tmp_pto_days:
                    tmp_pto_days[pto_day_type] = 0
            tmp_pto_days["VACATION"] = tmp_pto_days["VACATION"] + tmp_pto_days["HOLIDAY"] - in_house_days 
            _ = tmp_pto_days.pop("HOLIDAY")
            pto_days[ay][resident_name] = tmp_pto_days


    # Count sick call
    sick_call_weekdays = {}
    for row_idx, row in academic_years.iterrows():
        start, stop = row["Start"].date(), row["Stop"].date() 
        ay = row["Year"]
        sick_call_weekdays[ay] = defaultdict(dict)
        for resident_name, calendar in resident_calendar.items():
            tmp_sick_call_weekdays = []
            tmp_calendar = {k: v for k, v in calendar.items() if k >= start and k < stop}
            for each_day, daily_tasks in tmp_calendar.items():
                if each_day.weekday() not in [0, 1, 2, 3]: # Mon-Thurs only
                    continue
                if "Sick Call" in daily_tasks:
                    tmp_sick_call_weekdays.append(1)
            sick_call_weekdays[ay][resident_name] = int(np.round(np.sum(tmp_sick_call_weekdays) / 4))

    sick_call_weekends = {}
    for row_idx, row in academic_years.iterrows():
        start, stop = row["Start"].date(), row["Stop"].date() 
        ay = row["Year"]
        sick_call_weekends[ay] = defaultdict(dict)
        for resident_name, calendar in resident_calendar.items():
            tmp_sick_call_weekends = []
            tmp_calendar = {k: v for k, v in calendar.items() if k >= start and k < stop}
            for each_day, daily_tasks in tmp_calendar.items():
                if each_day.weekday() not in [4, 5, 6]: # Fri-Sun only
                    continue
                if "Sick Call" in daily_tasks:
                    tmp_sick_call_weekends.append(1)
            sick_call_weekends[ay][resident_name] = int(np.round(np.sum(tmp_sick_call_weekends) / 3))

    sick_call_holidays = {}
    for row_idx, row in academic_years.iterrows():
        start, stop = row["Start"].date(), row["Stop"].date() 
        ay = row["Year"]
        sick_call_holidays[ay] = defaultdict(dict)
        for resident_name, calendar in resident_calendar.items():
            tmp_sick_call_holidays = []
            tmp_calendar = {k: v for k, v in calendar.items() if k >= start and k < stop}
            for each_day, daily_tasks in tmp_calendar.items():
                if each_day not in holidays:
                    continue
                if "Sick Call" in daily_tasks:
                    tmp_sick_call_holidays.append(1)
            sick_call_holidays[ay][resident_name] = int(np.sum(tmp_sick_call_holidays))

    sick_call_major_holidays = {}
    for row_idx, row in academic_years.iterrows():
        start, stop = row["Start"].date(), row["Stop"].date() 
        ay = row["Year"]
        sick_call_major_holidays[ay] = defaultdict(dict)
        for resident_name, calendar in resident_calendar.items():
            tmp_sick_call_major_holidays = defaultdict(list)
            tmp_calendar = {k: v for k, v in calendar.items() if k >= start and k < stop}
            for each_day, daily_tasks in tmp_calendar.items():
                if each_day not in thanksgiving + christmas + new_years:
                    continue
                if "Sick Call" in daily_tasks:
                    if each_day in thanksgiving:
                        tmp_sick_call_major_holidays["Thanksgiving"].append(1)
                    elif each_day in christmas:
                        tmp_sick_call_major_holidays["Xmas"].append(1)
                    elif each_day in new_years:
                        tmp_sick_call_major_holidays["NY"].append(1)
            for each_major_holiday in ["Thanksgiving", "Xmas", "NY"]:
                if each_major_holiday not in tmp_sick_call_major_holidays:
                    tmp_sick_call_major_holidays[each_major_holiday] = 0
            for k, v in tmp_sick_call_major_holidays.items():
                tmp_sick_call_major_holidays[k] = int(np.sum(v))
            sick_call_major_holidays[ay][resident_name] = tmp_sick_call_major_holidays

    sick_call_combined = sick_call_major_holidays.copy()
    for ay, residents in sick_call_combined.items():
        for resident_name, resident_dict in residents.items():
            sick_call_combined[ay][resident_name]["Weekdays"] = sick_call_weekdays[ay][resident_name]
            sick_call_combined[ay][resident_name]["Weekends"] = sick_call_weekends[ay][resident_name]
            sick_call_combined[ay][resident_name]["Holidays"] = sick_call_holidays[ay][resident_name]

    # Eliminate residents from years they were not a resident yet
    for each_ay, each_resident_dict in resident_core_rotation_tallies.copy().items():
        for each_resident, resident_rotation_dict in each_resident_dict.copy().items():
            if np.sum(list(resident_rotation_dict.values())) == 0:
                _ = resident_core_rotation_tallies[each_ay].pop(each_resident)

    classes = defaultdict(list)
    for row_idx, row in roster.iterrows():
        classes[row.Year].append(row.Resident)

    ######################################################
    # CREATE EXCEL SPREADSHEET FOR CORE ROTATION TALLIES #
    ######################################################
    core_rotation_tallies_by_class = {}
    for each_class, residents in classes.items():
        core_rotation_tallies_by_class[each_class] = {}
        for each_resident in residents:
            tmp_df_list = []
            for each_ay, ay_tallies in resident_core_rotation_tallies.items():
                if each_resident not in ay_tallies:
                    continue
                tmp_df = pd.DataFrame(ay_tallies[each_resident], index=[0])
                tmp_df["Year"] = each_ay
                tmp_df_list.append(tmp_df)
            tmp_df = pd.concat(tmp_df_list)
            tmp_total = dict(tmp_df.sum())
            tmp_total["Year"] = "Total"
            filler = {k: "" for k in [*tmp_total]}
            filler = pd.DataFrame(filler, index=[0])
            tmp_df = pd.concat([tmp_df, pd.DataFrame(tmp_total, index=[0]), filler])
            tmp_df["Resident"] = [each_resident] + [""] * (len(tmp_df) - 1)
            tmp_df = tmp_df[["Resident", "Year"] + list(tmp_df.columns[:-2])]
            core_rotation_tallies_by_class[each_class][each_resident] = tmp_df

    core_rotation_tallies_df_by_class = {}
    for each_class in [*core_rotation_tallies_by_class]:
        core_rotation_tallies_df_by_class[each_class] = pd.concat(list(core_rotation_tallies_by_class[each_class].values()))


    ####################################################
    # CREATE EXCEL SPREADSHEET FOR WEEKEND CALL SHIFTS #
    ####################################################
    weekend_call_shifts_by_class = {}
    for each_class, residents in classes.items():
        weekend_call_shifts_by_class[each_class] = {}
        for each_resident in residents:
            tmp_df_list = []
            for each_ay, ay_tallies in call_shifts_worked.items():
                if each_resident not in ay_tallies:
                    continue
                tmp_df = pd.DataFrame(ay_tallies[each_resident], index=[0])
                tmp_df["Year"] = each_ay
                tmp_df_list.append(tmp_df)
            tmp_df = pd.concat(tmp_df_list)
            tmp_total = dict(tmp_df.sum())
            tmp_total["Year"] = "Total"
            filler = {k: "" for k in [*tmp_total]}
            filler = pd.DataFrame(filler, index=[0])
            tmp_df = pd.concat([tmp_df, pd.DataFrame(tmp_total, index=[0]), filler])
            tmp_df["Resident"] = [each_resident] + [""] * (len(tmp_df) - 1)
            tmp_df = tmp_df[["Resident", "Year"] + list(tmp_df.columns[:-2])]
            weekend_call_shifts_by_class[each_class][each_resident] = tmp_df

    weekend_call_shifts_df_by_class = {}
    for each_class in [*weekend_call_shifts_by_class]:
        weekend_call_shifts_df_by_class[each_class] = pd.concat(list(weekend_call_shifts_by_class[each_class].values()))

    weekend_call_shifts_df = pd.concat([v for k, v in weekend_call_shifts_df_by_class.items()])


    #################################################
    # CREATE EXCEL SPREADSHEET FOR SICK CALL SHIFTS #
    #################################################
    sick_call_by_class = {}
    for each_class, residents in classes.items():
        sick_call_by_class[each_class] = {}
        for each_resident in residents:
            tmp_df_list = []
            for each_ay, ay_tallies in sick_call_combined.items():
                if each_resident not in ay_tallies:
                    continue
                tmp_df = pd.DataFrame(ay_tallies[each_resident], index=[0])
                tmp_df = tmp_df[["Weekdays", "Weekends", "Holidays", "Thanksgiving", "Xmas", "NY"]]
                tmp_df["Year"] = each_ay
                tmp_df_list.append(tmp_df)
            tmp_df = pd.concat(tmp_df_list)
            tmp_total = dict(tmp_df.sum())
            tmp_total["Year"] = "Total"
            filler = {k: "" for k in [*tmp_total]}
            filler = pd.DataFrame(filler, index=[0])
            tmp_df = pd.concat([tmp_df, pd.DataFrame(tmp_total, index=[0]), filler])
            tmp_df["Resident"] = [each_resident] + [""] * (len(tmp_df) - 1)
            tmp_df = tmp_df[["Resident", "Year"] + list(tmp_df.columns[:-2])]
            sick_call_by_class[each_class][each_resident] = tmp_df

    sick_call_df_by_class = {}
    for each_class in [*sick_call_by_class]:
        sick_call_df_by_class[each_class] = pd.concat(list(sick_call_by_class[each_class].values()))

    sick_call_df = pd.concat([v for k, v in sick_call_df_by_class.items()])


    #########################################
    # CREATE EXCEL SPREADSHEET FOR PTO DAYS #
    #########################################
    pto_by_class = {}
    for each_class, residents in classes.items():
        pto_by_class[each_class] = {}
        for each_resident in residents:
            tmp_df_list = []
            for each_ay, ay_pto in pto_days.items():
                if each_resident not in ay_tallies:
                    continue
                tmp_df = pd.DataFrame(ay_pto[each_resident], index=[0])
                tmp_df["Total (Days)"] = tmp_df.sum(1)
                tmp_df["Total (Weeks)"] = np.round(tmp_df["Total (Days)"] / 5).astype("int")
                tmp_df["Year"] = each_ay
                tmp_df_list.append(tmp_df)
            tmp_df = pd.concat(tmp_df_list)
            tmp_total = dict(tmp_df.sum())
            tmp_total["Year"] = "Total"
            filler = {k: "" for k in [*tmp_total]}
            filler = pd.DataFrame(filler, index=[0])
            tmp_df = pd.concat([tmp_df, pd.DataFrame(tmp_total, index=[0]), filler])
            tmp_df["Resident"] = [each_resident] + [""] * (len(tmp_df) - 1)
            tmp_df = tmp_df[["Resident", "Year"] + list(tmp_df.columns[:-2])]
            pto_by_class[each_class][each_resident] = tmp_df

    pto_df_by_class = {}
    for each_class in [*pto_by_class]:
        pto_df_by_class[each_class] = pd.concat(list(pto_by_class[each_class].values()))

    pto_df = pd.concat([v for k, v in pto_df_by_class.items()])


    #########################################
    # CREATE EXCEL SPREADSHEET FOR HOLIDAYS #
    #########################################
    # first, combine the separate holiday dicts into 1...
    combined_holidays = {}
    for each_ay, residents in holidays_worked.items():
        combined_holidays[each_ay] = defaultdict(dict)
        for resident_name, resident_dict in residents.items():
            combined_holidays[each_ay][resident_name] = {
                "All Holidays": holidays_worked[each_ay][resident_name],
                "Thanksgiving": thanksgiving_worked[each_ay][resident_name],
                "Xmas": christmas_worked[each_ay][resident_name],
                "NY": new_years_worked[each_ay][resident_name],
                "Thanksgiving Weekend": thanksgiving_weekends_worked[each_ay][resident_name],
                "Xmas/NY Weekend": christmas_new_years_weekends_worked[each_ay][resident_name]
            }
        
    holidays_by_class = {}
    for each_class, residents in classes.items():
        holidays_by_class[each_class] = {}
        for each_resident in residents:
            tmp_df_list = []
            for each_ay, ay_holidays in combined_holidays.items():
                if each_resident not in ay_holidays:
                    continue
                tmp_df = pd.DataFrame(ay_holidays[each_resident], index=[0])
                tmp_df["Year"] = each_ay
                tmp_df_list.append(tmp_df)
            tmp_df = pd.concat(tmp_df_list)
            tmp_total = dict(tmp_df.sum())
            tmp_total["Year"] = "Total"
            filler = {k: "" for k in [*tmp_total]}
            filler = pd.DataFrame(filler, index=[0])
            tmp_df = pd.concat([tmp_df, pd.DataFrame(tmp_total, index=[0]), filler])
            tmp_df["Resident"] = [each_resident] + [""] * (len(tmp_df) - 1)
            tmp_df = tmp_df[["Resident", "Year"] + list(tmp_df.columns[:-2])]
            holidays_by_class[each_class][each_resident] = tmp_df

    holidays_df_by_class = {}
    for each_class in [*holidays_by_class]:
        holidays_df_by_class[each_class] = pd.concat(list(holidays_by_class[each_class].values()))

    holidays_df = pd.concat([v for k, v in holidays_df_by_class.items()])

    with pd.ExcelWriter("all_tallies.xlsx") as writer:
        for each_class, each_df in core_rotation_tallies_df_by_class.items():
            each_df.to_excel(writer, sheet_name=f"R{each_class}", index=False)
        pto_df.to_excel(writer, sheet_name="PTO", index=False)
        weekend_call_shifts_df.to_excel(writer, sheet_name=f"Call Shifts", index=False)
        sick_call_df.to_excel(writer, sheet_name=f"Sick Call", index=False)
        holidays_df.to_excel(writer, sheet_name="Holidays", index=False)

    return "all_tallies.xlsx"


demo = gr.Interface(
    fn=main,
    inputs=gr.File(file_count="multiple"),
    outputs="file")


if __name__ == "__main__":
    demo.launch()