[{"data":1,"prerenderedAt":2244},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcreate-a-shift-calendar-with-rotating-patterns":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcreate-a-shift-calendar-with-rotating-patterns":2211,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcreate-a-shift-calendar-with-rotating-patterns":2220,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcreate-a-shift-calendar-with-rotating-patterns":2225,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcreate-a-shift-calendar-with-rotating-patterns":2226},{"id":4,"title":5,"body":6,"dateModified":2177,"datePublished":2177,"description":2178,"extension":2179,"faq":2180,"keyword":2189,"kind":2190,"meta":2191,"navigation":96,"path":2200,"seo":2201,"slug":2204,"stem":2205,"topics":2206,"type":2209,"__hash__":2210},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcreate-a-shift-calendar-with-rotating-patterns\u002Findex.md","Create a Shift Calendar with Rotating Patterns",{"type":7,"value":8,"toc":2166},"minimark",[9,24,29,61,64,68,313,320,815,938,942,1142,1148,1152,1340,1343,1412,1416,2040,2047,2051,2086,2090,2097,2101,2111,2117,2123,2129,2133,2162],[10,11,12,13,17,18,23],"p",{},"Round-the-clock operations — a warehouse, a control room, a care home, a production line — run on rotating shift patterns. Four teams cover days and nights between them, each working a fixed sequence that repeats every few weeks, and the rota for any future date follows from the pattern and one starting date. Rotas are nevertheless often kept in spreadsheets filled in by hand, month by month, with the familiar results: a day where two teams are on nights and nobody is on days, or a team that silently works more hours than the others for a quarter. Generating the rota from the pattern removes both problems. This page stores a common pattern — the 2-2-3 or ",[14,15,16],"em",{},"Panama"," rotation, on 12-hour shifts — as a sequence, expands it to a quarter with pandas, checks coverage for every day and shift, totals the hours per team, marks public holidays, and writes a colour-coded calendar to Excel. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002F","Working with Dates and Times in Excel Data",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl holidays\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55,58],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," pandas",[40,56,57],{"class":50}," openpyxl",[40,59,60],{"class":50}," holidays\n",[10,62,63],{},"The pattern used here has four teams, A to D, working 12-hour day shifts (D) or night shifts (N) or resting (–). Each team follows the same 28-day sequence, offset by a week from the next team, so that every day exactly one team is on days and one on nights.",[25,65,67],{"id":66},"step-1-the-pattern-as-data","Step 1 — The pattern as data",[30,69,73],{"className":70,"code":71,"language":72,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\n# 2-2-3 \"Panama\" rotation over 28 days: two on, two off, three on, then the same on nights\nDAYS_HALF   = \"DD--DDD--DD---\"\nNIGHTS_HALF = \"NN--NNN--NN---\"\nCYCLE = DAYS_HALF + NIGHTS_HALF                       # 28 characters, one per day\nassert len(CYCLE) == 28\n\n# each team starts the cycle on a different day; the offsets are chosen so the teams interlock\nOFFSETS = {\"A\": 0, \"B\": 14, \"C\": 7, \"D\": 21}\nANCHOR = pd.Timestamp(\"2026-01-05\")                  # day 0 of the cycle for team A\nprint(\"cycle:\", CYCLE)\nprint({team: CYCLE[off:] + CYCLE[:off] for team, off in OFFSETS.items()})\n","python",[37,74,75,91,98,105,118,130,150,173,178,184,238,258,276],{"__ignoreMap":35},[40,76,77,81,85,88],{"class":42,"line":43},[40,78,80],{"class":79},"s-kum","import",[40,82,84],{"class":83},"skGVy"," pandas ",[40,86,87],{"class":79},"as",[40,89,90],{"class":83}," pd\n",[40,92,94],{"class":42,"line":93},2,[40,95,97],{"emptyLinePlaceholder":96},true,"\n",[40,99,101],{"class":42,"line":100},3,[40,102,104],{"class":103},"s-wDw","# 2-2-3 \"Panama\" rotation over 28 days: two on, two off, three on, then the same on nights\n",[40,106,108,112,115],{"class":42,"line":107},4,[40,109,111],{"class":110},"sP0c6","DAYS_HALF",[40,113,114],{"class":79},"   =",[40,116,117],{"class":50}," \"DD--DDD--DD---\"\n",[40,119,121,124,127],{"class":42,"line":120},5,[40,122,123],{"class":110},"NIGHTS_HALF",[40,125,126],{"class":79}," =",[40,128,129],{"class":50}," \"NN--NNN--NN---\"\n",[40,131,133,136,138,141,144,147],{"class":42,"line":132},6,[40,134,135],{"class":110},"CYCLE",[40,137,126],{"class":79},[40,139,140],{"class":110}," DAYS_HALF",[40,142,143],{"class":79}," +",[40,145,146],{"class":110}," NIGHTS_HALF",[40,148,149],{"class":103},"                       # 28 characters, one per day\n",[40,151,153,156,159,162,164,167,170],{"class":42,"line":152},7,[40,154,155],{"class":79},"assert",[40,157,158],{"class":110}," len",[40,160,161],{"class":83},"(",[40,163,135],{"class":110},[40,165,166],{"class":83},") ",[40,168,169],{"class":79},"==",[40,171,172],{"class":110}," 28\n",[40,174,176],{"class":42,"line":175},8,[40,177,97],{"emptyLinePlaceholder":96},[40,179,181],{"class":42,"line":180},9,[40,182,183],{"class":103},"# each team starts the cycle on a different day; the offsets are chosen so the teams interlock\n",[40,185,187,190,192,195,198,201,204,207,210,212,215,217,220,222,225,227,230,232,235],{"class":42,"line":186},10,[40,188,189],{"class":110},"OFFSETS",[40,191,126],{"class":79},[40,193,194],{"class":83}," {",[40,196,197],{"class":50},"\"A\"",[40,199,200],{"class":83},": ",[40,202,203],{"class":110},"0",[40,205,206],{"class":83},", ",[40,208,209],{"class":50},"\"B\"",[40,211,200],{"class":83},[40,213,214],{"class":110},"14",[40,216,206],{"class":83},[40,218,219],{"class":50},"\"C\"",[40,221,200],{"class":83},[40,223,224],{"class":110},"7",[40,226,206],{"class":83},[40,228,229],{"class":50},"\"D\"",[40,231,200],{"class":83},[40,233,234],{"class":110},"21",[40,236,237],{"class":83},"}\n",[40,239,241,244,246,249,252,255],{"class":42,"line":240},11,[40,242,243],{"class":110},"ANCHOR",[40,245,126],{"class":79},[40,247,248],{"class":83}," pd.Timestamp(",[40,250,251],{"class":50},"\"2026-01-05\"",[40,253,254],{"class":83},")                  ",[40,256,257],{"class":103},"# day 0 of the cycle for team A\n",[40,259,261,264,266,269,271,273],{"class":42,"line":260},12,[40,262,263],{"class":110},"print",[40,265,161],{"class":83},[40,267,268],{"class":50},"\"cycle:\"",[40,270,206],{"class":83},[40,272,135],{"class":110},[40,274,275],{"class":83},")\n",[40,277,279,281,284,286,289,292,295,298,301,304,307,310],{"class":42,"line":278},13,[40,280,263],{"class":110},[40,282,283],{"class":83},"({team: ",[40,285,135],{"class":110},[40,287,288],{"class":83},"[off:] ",[40,290,291],{"class":79},"+",[40,293,294],{"class":110}," CYCLE",[40,296,297],{"class":83},"[:off] ",[40,299,300],{"class":79},"for",[40,302,303],{"class":83}," team, off ",[40,305,306],{"class":79},"in",[40,308,309],{"class":110}," OFFSETS",[40,311,312],{"class":83},".items()})\n",[10,314,315,316,319],{},"The whole rota is defined by three things: the cycle of shift codes, one character per day; each team's offset into that cycle; and an anchor date on which team A is at position zero. The 2-2-3 pattern works two days, rests two, works three, rests two, works two, rests three — fourteen days covering seven shifts — first on days and then on nights. Shifting the four teams by 0, 7, 14 and 21 days interlocks them so the gaps in one team's pattern are filled by another. Keeping the pattern as a short string means it can be read, reviewed and changed by anyone, and a different pattern — ",[14,317,318],{},"four on, four off",", or a three-team rotation — is just a different string and set of offsets.",[321,322,330,331,330,335,330,339,330,346,330,355,330,359,330,366,330,370,330,374,330,377,330,381,330,384,330,388,330,391,330,395,330,398,330,402,330,405,330,409,330,412,330,416,330,419,330,423,330,426,330,430,330,433,330,437,330,440,330,444,330,447,330,451,330,454,330,458,330,461,330,465,330,469,330,474,330,479,330,483,330,485,330,487,330,489,330,491,330,493,330,496,330,498,330,501,330,503,330,506,330,508,330,511,330,513,330,516,330,518,330,520,330,522,330,525,330,527,330,530,330,532,330,534,330,536,330,539,330,541,330,544,330,546,330,549,330,552,330,555,330,557,330,560,330,562,330,564,330,566,330,568,330,570,330,573,330,575,330,577,330,579,330,581,330,583,330,585,330,587,330,589,330,591,330,593,330,595,330,597,330,599,330,601,330,603,330,605,330,607,330,609,330,611,330,613,330,615,330,617,330,620,330,623,330,625,330,627,330,629,330,631,330,633,330,635,330,637,330,639,330,641,330,643,330,645,330,647,330,649,330,651,330,653,330,655,330,657,330,659,330,661,330,663,330,665,330,667,330,669,330,671,330,673,330,675,330,677,330,679,330,681,330,683,330,686,330,689,330,691,330,693,330,695,330,697,330,699,330,701,330,703,330,705,330,707,330,709,330,711,330,713,330,715,330,717,330,719,330,721,330,723,330,725,330,727,330,729,330,731,330,733,330,735,330,737,330,739,330,741,330,743,330,745,330,747,330,749,330,752,330,755,330,757,330,759,330,761,330,763,330,765,330,767,330,769,330,771,330,773,330,775,330,777,330,779,330,781,330,783,330,785,330,787,330,789,330,791,330,793,330,795,330,797,330,799,330,801,330,803,330,805,330,807,330,809,330,811,330,813],"svg",{"viewBox":323,"role":324,"ariaLabelledBy":325,"xmlns":328,"style":329},"10 0 739 196","img",[326,327],"shift-pattern-t","shift-pattern-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:739px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[332,333,334],"title",{"id":326},"The first two weeks of the cycle for each team",[336,337,338],"desc",{"id":327},"In the first two weeks teams A and B work days 0, 1, 4, 5, 6, 9 and 10, A on days and B on nights. Teams C and D work the other seven days, swapping between days and nights, so every day has one team on days and one on nights.",[340,341],"rect",{"x":342,"y":203,"width":343,"height":344,"fill":345},"10","739","196","#ffffff",[340,347],{"x":348,"y":349,"width":350,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"26.6","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[340,356],{"x":357,"y":349,"width":358,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"58.6","58.7",[360,361,365],"text",{"x":362,"y":363,"style":364},"88.0","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[340,367],{"x":368,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"117.4","44.0",[360,371,373],{"x":372,"y":363,"style":364},"139.4","B",[340,375],{"x":376,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"161.4",[360,378,380],{"x":379,"y":363,"style":364},"183.4","C",[340,382],{"x":383,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"205.4",[360,385,387],{"x":386,"y":363,"style":364},"227.4","D",[340,389],{"x":390,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"249.4",[360,392,394],{"x":393,"y":363,"style":364},"271.4","E",[340,396],{"x":397,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"293.4",[360,399,401],{"x":400,"y":363,"style":364},"315.4","F",[340,403],{"x":404,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"337.4",[360,406,408],{"x":407,"y":363,"style":364},"359.4","G",[340,410],{"x":411,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"381.4",[360,413,415],{"x":414,"y":363,"style":364},"403.4","H",[340,417],{"x":418,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"425.4",[360,420,422],{"x":421,"y":363,"style":364},"447.4","I",[340,424],{"x":425,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"469.4",[360,427,429],{"x":428,"y":363,"style":364},"491.4","J",[340,431],{"x":432,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"513.4",[360,434,436],{"x":435,"y":363,"style":364},"535.4","K",[340,438],{"x":439,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"557.4",[360,441,443],{"x":442,"y":363,"style":364},"579.4","L",[340,445],{"x":446,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"601.4",[360,448,450],{"x":449,"y":363,"style":364},"623.4","M",[340,452],{"x":453,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"645.4",[360,455,457],{"x":456,"y":363,"style":364},"667.4","N",[340,459],{"x":460,"y":349,"width":369,"height":351,"rx":203,"fill":352,"stroke":353,"style":354},"689.4",[360,462,464],{"x":463,"y":363,"style":364},"711.4","O",[340,466],{"x":348,"y":467,"width":350,"height":468,"rx":203,"fill":352,"stroke":353,"style":354},"40.0","28.0",[360,470,473],{"x":471,"y":472,"style":364},"42.6","59.0","1",[340,475],{"x":357,"y":467,"width":358,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[360,480,482],{"x":362,"y":472,"style":481},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Team",[340,484],{"x":368,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,486,203],{"x":372,"y":472,"style":481},[340,488],{"x":376,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,490,473],{"x":379,"y":472,"style":481},[340,492],{"x":383,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,494,495],{"x":386,"y":472,"style":481},"2",[340,497],{"x":390,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,499,500],{"x":393,"y":472,"style":481},"3",[340,502],{"x":397,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,504,505],{"x":400,"y":472,"style":481},"4",[340,507],{"x":404,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,509,510],{"x":407,"y":472,"style":481},"5",[340,512],{"x":411,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,514,515],{"x":414,"y":472,"style":481},"6",[340,517],{"x":418,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,519,224],{"x":421,"y":472,"style":481},[340,521],{"x":425,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,523,524],{"x":428,"y":472,"style":481},"8",[340,526],{"x":432,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,528,529],{"x":435,"y":472,"style":481},"9",[340,531],{"x":439,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,533,342],{"x":442,"y":472,"style":481},[340,535],{"x":446,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,537,538],{"x":449,"y":472,"style":481},"11",[340,540],{"x":453,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,542,543],{"x":456,"y":472,"style":481},"12",[340,545],{"x":460,"y":467,"width":369,"height":468,"rx":203,"fill":476,"stroke":477,"style":478},[360,547,548],{"x":463,"y":472,"style":481},"13",[340,550],{"x":348,"y":551,"width":350,"height":468,"rx":203,"fill":352,"stroke":353,"style":354},"68.0",[360,553,495],{"x":471,"y":554,"style":364},"87.0",[340,556],{"x":357,"y":551,"width":358,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,558,365],{"x":362,"y":554,"style":559},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[340,561],{"x":368,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,563,387],{"x":372,"y":554,"style":559},[340,565],{"x":376,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,567,387],{"x":379,"y":554,"style":559},[340,569],{"x":383,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,571,572],{"x":386,"y":554,"style":559},"–",[340,574],{"x":390,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,576,572],{"x":393,"y":554,"style":559},[340,578],{"x":397,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,580,387],{"x":400,"y":554,"style":559},[340,582],{"x":404,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,584,387],{"x":407,"y":554,"style":559},[340,586],{"x":411,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,588,387],{"x":414,"y":554,"style":559},[340,590],{"x":418,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,592,572],{"x":421,"y":554,"style":559},[340,594],{"x":425,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,596,572],{"x":428,"y":554,"style":559},[340,598],{"x":432,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,600,387],{"x":435,"y":554,"style":559},[340,602],{"x":439,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,604,387],{"x":442,"y":554,"style":559},[340,606],{"x":446,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,608,572],{"x":449,"y":554,"style":559},[340,610],{"x":453,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,612,572],{"x":456,"y":554,"style":559},[340,614],{"x":460,"y":551,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,616,572],{"x":463,"y":554,"style":559},[340,618],{"x":348,"y":619,"width":350,"height":468,"rx":203,"fill":352,"stroke":353,"style":354},"96.0",[360,621,500],{"x":471,"y":622,"style":364},"115.0",[340,624],{"x":357,"y":619,"width":358,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,626,373],{"x":362,"y":622,"style":559},[340,628],{"x":368,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,630,457],{"x":372,"y":622,"style":559},[340,632],{"x":376,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,634,457],{"x":379,"y":622,"style":559},[340,636],{"x":383,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,638,572],{"x":386,"y":622,"style":559},[340,640],{"x":390,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,642,572],{"x":393,"y":622,"style":559},[340,644],{"x":397,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,646,457],{"x":400,"y":622,"style":559},[340,648],{"x":404,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,650,457],{"x":407,"y":622,"style":559},[340,652],{"x":411,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,654,457],{"x":414,"y":622,"style":559},[340,656],{"x":418,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,658,572],{"x":421,"y":622,"style":559},[340,660],{"x":425,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,662,572],{"x":428,"y":622,"style":559},[340,664],{"x":432,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,666,457],{"x":435,"y":622,"style":559},[340,668],{"x":439,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,670,457],{"x":442,"y":622,"style":559},[340,672],{"x":446,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,674,572],{"x":449,"y":622,"style":559},[340,676],{"x":453,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,678,572],{"x":456,"y":622,"style":559},[340,680],{"x":460,"y":619,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,682,572],{"x":463,"y":622,"style":559},[340,684],{"x":348,"y":685,"width":350,"height":468,"rx":203,"fill":352,"stroke":353,"style":354},"124.0",[360,687,505],{"x":471,"y":688,"style":364},"143.0",[340,690],{"x":357,"y":685,"width":358,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,692,380],{"x":362,"y":688,"style":559},[340,694],{"x":368,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,696,572],{"x":372,"y":688,"style":559},[340,698],{"x":376,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,700,572],{"x":379,"y":688,"style":559},[340,702],{"x":383,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,704,387],{"x":386,"y":688,"style":559},[340,706],{"x":390,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,708,387],{"x":393,"y":688,"style":559},[340,710],{"x":397,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,712,572],{"x":400,"y":688,"style":559},[340,714],{"x":404,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,716,572],{"x":407,"y":688,"style":559},[340,718],{"x":411,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,720,572],{"x":414,"y":688,"style":559},[340,722],{"x":418,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,724,457],{"x":421,"y":688,"style":559},[340,726],{"x":425,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,728,457],{"x":428,"y":688,"style":559},[340,730],{"x":432,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,732,572],{"x":435,"y":688,"style":559},[340,734],{"x":439,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,736,572],{"x":442,"y":688,"style":559},[340,738],{"x":446,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,740,457],{"x":449,"y":688,"style":559},[340,742],{"x":453,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,744,457],{"x":456,"y":688,"style":559},[340,746],{"x":460,"y":685,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,748,457],{"x":463,"y":688,"style":559},[340,750],{"x":348,"y":751,"width":350,"height":468,"rx":203,"fill":352,"stroke":353,"style":354},"152.0",[360,753,510],{"x":471,"y":754,"style":364},"171.0",[340,756],{"x":357,"y":751,"width":358,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,758,387],{"x":362,"y":754,"style":559},[340,760],{"x":368,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,762,572],{"x":372,"y":754,"style":559},[340,764],{"x":376,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,766,572],{"x":379,"y":754,"style":559},[340,768],{"x":383,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,770,457],{"x":386,"y":754,"style":559},[340,772],{"x":390,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,774,457],{"x":393,"y":754,"style":559},[340,776],{"x":397,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,778,572],{"x":400,"y":754,"style":559},[340,780],{"x":404,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,782,572],{"x":407,"y":754,"style":559},[340,784],{"x":411,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,786,572],{"x":414,"y":754,"style":559},[340,788],{"x":418,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,790,387],{"x":421,"y":754,"style":559},[340,792],{"x":425,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,794,387],{"x":428,"y":754,"style":559},[340,796],{"x":432,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,798,572],{"x":435,"y":754,"style":559},[340,800],{"x":439,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,802,572],{"x":442,"y":754,"style":559},[340,804],{"x":446,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,806,387],{"x":449,"y":754,"style":559},[340,808],{"x":453,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,810,387],{"x":456,"y":754,"style":559},[340,812],{"x":460,"y":751,"width":369,"height":468,"rx":203,"fill":345,"stroke":353,"style":354},[360,814,387],{"x":463,"y":754,"style":559},[321,816,330,822,330,825,330,828,330,832,330,838,330,844,330,849,330,854,330,859,330,862,330,868,330,872,330,876,330,880,330,883,330,886,330,890,330,893,330,895,330,899,330,902,330,907,330,912,330,915,330,918,330,922,330,925,330,930,330,935],{"viewBox":817,"role":324,"ariaLabelledBy":818,"xmlns":328,"style":821},"4 0 752 108",[819,820],"shift-formula-t","shift-formula-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[332,823,824],{"id":819},"A team's shift on any date",[336,826,827],{"id":820},"Count the days from the anchor date to the date, add the team's offset, take the remainder after dividing by the 28-day cycle length, and look up that position in the cycle string to get D, N or a rest day.",[340,829],{"x":505,"y":203,"width":830,"height":831,"fill":345},"752","108",[340,833],{"x":834,"y":349,"width":835,"height":836,"rx":543,"fill":352,"stroke":353,"style":837},"20.0","115.2","76.5","stroke-width:2px",[360,839,843],{"x":840,"y":841,"style":842},"77.6","50.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Date",[360,845,848],{"x":840,"y":846,"style":847},"69.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","e.g. 1 Oct 2026",[42,850],{"x1":851,"y1":852,"x2":853,"y2":852,"stroke":477,"style":837},"140.2","54.2","162.2",[855,856],"polygon",{"points":857,"fill":858},"166.2,54.2 157.2,59.2 157.2,49.2","#5b5cf0",[340,860],{"x":861,"y":349,"width":835,"height":836,"rx":543,"fill":476,"stroke":477,"style":837},"171.2",[360,863,867],{"x":864,"y":865,"style":866},"228.8","41.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","− anchor",[360,869,871],{"x":864,"y":870,"style":847},"61.0","days since 5",[360,873,875],{"x":864,"y":874,"style":847},"78.5","Jan 2026",[42,877],{"x1":878,"y1":852,"x2":879,"y2":852,"stroke":477,"style":837},"291.4","313.4",[855,881],{"points":882,"fill":858},"317.4,54.2 308.4,59.2 308.4,49.2",[340,884],{"x":885,"y":349,"width":835,"height":836,"rx":543,"fill":476,"stroke":477,"style":837},"322.4",[360,887,889],{"x":888,"y":865,"style":866},"380.0","+ team offset",[360,891,892],{"x":888,"y":870,"style":847},"A 0, B 14, C 7, D",[360,894,234],{"x":888,"y":874,"style":847},[42,896],{"x1":897,"y1":852,"x2":898,"y2":852,"stroke":477,"style":837},"442.6","464.6",[855,900],{"points":901,"fill":858},"468.6,54.2 459.6,59.2 459.6,49.2",[340,903],{"x":904,"y":349,"width":835,"height":836,"rx":543,"fill":905,"stroke":906,"style":837},"473.6","#fdefd8","var(--gold,#b4740a)",[360,908,911],{"x":909,"y":865,"style":910},"531.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","mod 28",[360,913,914],{"x":909,"y":870,"style":847},"position in the",[360,916,917],{"x":909,"y":874,"style":847},"cycle",[42,919],{"x1":920,"y1":852,"x2":921,"y2":852,"stroke":477,"style":837},"593.8","615.8",[855,923],{"points":924,"fill":858},"619.8,54.2 610.8,59.2 610.8,49.2",[340,926],{"x":927,"y":349,"width":835,"height":836,"rx":543,"fill":928,"stroke":929,"style":837},"624.8","#d9f4f1","var(--teal,#0f9488)",[360,931,934],{"x":932,"y":841,"style":933},"682.4","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Cycle[position]",[360,936,937],{"x":932,"y":846,"style":847},"D, N or rest",[25,939,941],{"id":940},"step-2-expand-it-to-any-date-range","Step 2 — Expand it to any date range",[30,943,945],{"className":70,"code":944,"language":72,"meta":35,"style":35},"def rota(start, end):\n    dates = pd.date_range(start, end, freq=\"D\")\n    rows = {}\n    for team, off in OFFSETS.items():\n        pos = ((dates - ANCHOR).days + off) % len(CYCLE)\n        rows[team] = [CYCLE[p] for p in pos]\n    return pd.DataFrame(rows, index=dates)\n\nq4 = rota(\"2026-10-01\", \"2026-12-31\")\nprint(q4.head(7).rename(index=lambda d: d.strftime(\"%a %d %b\")).T.to_string())\n",[37,946,947,959,980,990,1004,1039,1064,1080,1084,1104],{"__ignoreMap":35},[40,948,949,952,956],{"class":42,"line":43},[40,950,951],{"class":79},"def",[40,953,955],{"class":954},"s_Opv"," rota",[40,957,958],{"class":83},"(start, end):\n",[40,960,961,964,967,970,974,976,978],{"class":42,"line":93},[40,962,963],{"class":83},"    dates ",[40,965,966],{"class":79},"=",[40,968,969],{"class":83}," pd.date_range(start, end, ",[40,971,973],{"class":972},"sa561","freq",[40,975,966],{"class":79},[40,977,229],{"class":50},[40,979,275],{"class":83},[40,981,982,985,987],{"class":42,"line":100},[40,983,984],{"class":83},"    rows ",[40,986,966],{"class":79},[40,988,989],{"class":83}," {}\n",[40,991,992,995,997,999,1001],{"class":42,"line":107},[40,993,994],{"class":79},"    for",[40,996,303],{"class":83},[40,998,306],{"class":79},[40,1000,309],{"class":110},[40,1002,1003],{"class":83},".items():\n",[40,1005,1006,1009,1011,1014,1017,1020,1023,1025,1028,1031,1033,1035,1037],{"class":42,"line":120},[40,1007,1008],{"class":83},"        pos ",[40,1010,966],{"class":79},[40,1012,1013],{"class":83}," ((dates ",[40,1015,1016],{"class":79},"-",[40,1018,1019],{"class":110}," ANCHOR",[40,1021,1022],{"class":83},").days ",[40,1024,291],{"class":79},[40,1026,1027],{"class":83}," off) ",[40,1029,1030],{"class":79},"%",[40,1032,158],{"class":110},[40,1034,161],{"class":83},[40,1036,135],{"class":110},[40,1038,275],{"class":83},[40,1040,1041,1044,1046,1049,1051,1054,1056,1059,1061],{"class":42,"line":132},[40,1042,1043],{"class":83},"        rows[team] ",[40,1045,966],{"class":79},[40,1047,1048],{"class":83}," [",[40,1050,135],{"class":110},[40,1052,1053],{"class":83},"[p] ",[40,1055,300],{"class":79},[40,1057,1058],{"class":83}," p ",[40,1060,306],{"class":79},[40,1062,1063],{"class":83}," pos]\n",[40,1065,1066,1069,1072,1075,1077],{"class":42,"line":152},[40,1067,1068],{"class":79},"    return",[40,1070,1071],{"class":83}," pd.DataFrame(rows, ",[40,1073,1074],{"class":972},"index",[40,1076,966],{"class":79},[40,1078,1079],{"class":83},"dates)\n",[40,1081,1082],{"class":42,"line":175},[40,1083,97],{"emptyLinePlaceholder":96},[40,1085,1086,1089,1091,1094,1097,1099,1102],{"class":42,"line":180},[40,1087,1088],{"class":83},"q4 ",[40,1090,966],{"class":79},[40,1092,1093],{"class":83}," rota(",[40,1095,1096],{"class":50},"\"2026-10-01\"",[40,1098,206],{"class":83},[40,1100,1101],{"class":50},"\"2026-12-31\"",[40,1103,275],{"class":83},[40,1105,1106,1108,1111,1113,1116,1118,1121,1124,1127,1131,1134,1137,1139],{"class":42,"line":186},[40,1107,263],{"class":110},[40,1109,1110],{"class":83},"(q4.head(",[40,1112,224],{"class":110},[40,1114,1115],{"class":83},").rename(",[40,1117,1074],{"class":972},[40,1119,1120],{"class":79},"=lambda",[40,1122,1123],{"class":83}," d: d.strftime(",[40,1125,1126],{"class":50},"\"",[40,1128,1130],{"class":1129},"sSjpA","%a",[40,1132,1133],{"class":1129}," %d",[40,1135,1136],{"class":1129}," %b",[40,1138,1126],{"class":50},[40,1140,1141],{"class":83},")).T.to_string())\n",[10,1143,1144,1145,1147],{},"For any date, a team's position in the cycle is the number of days since the anchor, plus the team's offset, modulo the cycle length. That one line of arithmetic gives the rota for any period, past or future, with no state carried from one month to the next. The modulo also handles dates before the anchor correctly, because Python's ",[37,1146,1030],{}," always returns a non-negative result. Producing the fourth quarter of 2026 is a call with two dates; producing all of next year is the same call with different dates. The first week of October shows the pattern at work: on Thursday 1 October team D is on days and C on nights, and from Friday the 2nd teams B and A take over for the weekend.",[25,1149,1151],{"id":1150},"step-3-prove-the-coverage-and-compare-the-hours","Step 3 — Prove the coverage and compare the hours",[30,1153,1155],{"className":70,"code":1154,"language":72,"meta":35,"style":35},"coverage = pd.DataFrame({\"Day teams\": (q4 == \"D\").sum(axis=1), \"Night teams\": (q4 == \"N\").sum(axis=1)})\nproblems = coverage[(coverage[\"Day teams\"] != 1) | (coverage[\"Night teams\"] != 1)]\nprint(\"days checked:\", len(coverage), \"| days with a coverage problem:\", len(problems))\n\nhours = (q4 != \"-\").astype(int).mul(12).groupby(q4.index.to_period(\"M\")).sum()\nhours.loc[\"Total\"] = hours.sum()\nprint(hours.to_string())\n",[37,1156,1157,1212,1252,1279,1283,1318,1333],{"__ignoreMap":35},[40,1158,1159,1162,1164,1167,1170,1173,1175,1178,1181,1184,1186,1188,1191,1194,1196,1198,1201,1203,1205,1207,1209],{"class":42,"line":43},[40,1160,1161],{"class":83},"coverage ",[40,1163,966],{"class":79},[40,1165,1166],{"class":83}," pd.DataFrame({",[40,1168,1169],{"class":50},"\"Day teams\"",[40,1171,1172],{"class":83},": (q4 ",[40,1174,169],{"class":79},[40,1176,1177],{"class":50}," \"D\"",[40,1179,1180],{"class":83},").sum(",[40,1182,1183],{"class":972},"axis",[40,1185,966],{"class":79},[40,1187,473],{"class":110},[40,1189,1190],{"class":83},"), ",[40,1192,1193],{"class":50},"\"Night teams\"",[40,1195,1172],{"class":83},[40,1197,169],{"class":79},[40,1199,1200],{"class":50}," \"N\"",[40,1202,1180],{"class":83},[40,1204,1183],{"class":972},[40,1206,966],{"class":79},[40,1208,473],{"class":110},[40,1210,1211],{"class":83},")})\n",[40,1213,1214,1217,1219,1222,1224,1227,1230,1233,1235,1238,1241,1243,1245,1247,1249],{"class":42,"line":93},[40,1215,1216],{"class":83},"problems ",[40,1218,966],{"class":79},[40,1220,1221],{"class":83}," coverage[(coverage[",[40,1223,1169],{"class":50},[40,1225,1226],{"class":83},"] ",[40,1228,1229],{"class":79},"!=",[40,1231,1232],{"class":110}," 1",[40,1234,166],{"class":83},[40,1236,1237],{"class":79},"|",[40,1239,1240],{"class":83}," (coverage[",[40,1242,1193],{"class":50},[40,1244,1226],{"class":83},[40,1246,1229],{"class":79},[40,1248,1232],{"class":110},[40,1250,1251],{"class":83},")]\n",[40,1253,1254,1256,1258,1261,1263,1266,1269,1272,1274,1276],{"class":42,"line":100},[40,1255,263],{"class":110},[40,1257,161],{"class":83},[40,1259,1260],{"class":50},"\"days checked:\"",[40,1262,206],{"class":83},[40,1264,1265],{"class":110},"len",[40,1267,1268],{"class":83},"(coverage), ",[40,1270,1271],{"class":50},"\"| days with a coverage problem:\"",[40,1273,206],{"class":83},[40,1275,1265],{"class":110},[40,1277,1278],{"class":83},"(problems))\n",[40,1280,1281],{"class":42,"line":107},[40,1282,97],{"emptyLinePlaceholder":96},[40,1284,1285,1288,1290,1293,1295,1298,1301,1304,1307,1309,1312,1315],{"class":42,"line":120},[40,1286,1287],{"class":83},"hours ",[40,1289,966],{"class":79},[40,1291,1292],{"class":83}," (q4 ",[40,1294,1229],{"class":79},[40,1296,1297],{"class":50}," \"-\"",[40,1299,1300],{"class":83},").astype(",[40,1302,1303],{"class":110},"int",[40,1305,1306],{"class":83},").mul(",[40,1308,543],{"class":110},[40,1310,1311],{"class":83},").groupby(q4.index.to_period(",[40,1313,1314],{"class":50},"\"M\"",[40,1316,1317],{"class":83},")).sum()\n",[40,1319,1320,1323,1326,1328,1330],{"class":42,"line":132},[40,1321,1322],{"class":83},"hours.loc[",[40,1324,1325],{"class":50},"\"Total\"",[40,1327,1226],{"class":83},[40,1329,966],{"class":79},[40,1331,1332],{"class":83}," hours.sum()\n",[40,1334,1335,1337],{"class":42,"line":152},[40,1336,263],{"class":110},[40,1338,1339],{"class":83},"(hours.to_string())\n",[10,1341,1342],{},"The check that matters most is coverage: for every date, exactly one team on days and exactly one on nights. Counting the codes across each row and listing any day where either count is not one gives a definite answer for the whole quarter — zero problem days here. Run the same check after any manual swap is entered into the rota, and a swap that leaves a night uncovered is caught before the night arrives. Hours per team per month, at twelve hours a shift, show whether the pattern is fair over the period. Over a full 28-day cycle every team works the same fourteen shifts, but calendar months do not line up with the cycle. In October and December teams A and B work sixteen shifts (192 hours) against fifteen for C and D, and over the quarter A and B end up two shifts ahead — 564 hours against 540. The difference evens out over the following cycles, but it is exactly the kind of imbalance that causes complaints if nobody can show where it comes from; the hours table shows it, and a long-run total over a year shows it disappear.",[321,1344,330,1349,330,1352,330,1355,330,1358,330,1362,330,1367,330,1372,330,1376,330,1380,330,1384,330,1388,330,1391,330,1396,330,1400,330,1403,330,1406,330,1409],{"viewBox":1345,"role":324,"ariaLabelledBy":1346,"xmlns":328,"style":821},"4 0 752 199",[1347,1348],"shift-checks-t","shift-checks-d",[332,1350,1351],{"id":1347},"Two checks on every rota",[336,1353,1354],{"id":1348},"The coverage check counts, for each date, the teams on days and the teams on nights and requires exactly one of each. The hours check totals twelve hours per worked shift by team and month to show whether the load is shared fairly.",[340,1356],{"x":505,"y":203,"width":830,"height":1357,"fill":345},"199",[340,1359],{"x":834,"y":349,"width":1360,"height":1361,"rx":214,"fill":928,"stroke":929,"style":837},"351.0","167.0",[360,1363,1366],{"x":1364,"y":369,"style":1365},"195.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Coverage, per day",[42,1368],{"x1":1369,"y1":1370,"x2":1371,"y2":1370,"stroke":929,"style":354},"36.0","57.0","355.0",[360,1373,1375],{"x":1364,"y":1374,"style":559},"77.0","exactly one team on days",[360,1377,1379],{"x":1364,"y":1378,"style":559},"103.0","exactly one team on nights",[360,1381,1383],{"x":1364,"y":1382,"style":559},"129.0","list any day that fails",[360,1385,1387],{"x":1364,"y":1386,"style":559},"155.0","rerun after every swap",[340,1389],{"x":1390,"y":349,"width":1360,"height":1361,"rx":214,"fill":476,"stroke":477,"style":837},"389.0",[360,1392,1395],{"x":1393,"y":369,"style":1394},"564.5","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Hours, per team and month",[42,1397],{"x1":1398,"y1":1370,"x2":1399,"y2":1370,"stroke":477,"style":354},"405.0","724.0",[360,1401,1402],{"x":1393,"y":1374,"style":559},"12 h per worked shift",[360,1404,1405],{"x":1393,"y":1378,"style":559},"months do not match the cycle",[360,1407,1408],{"x":1393,"y":1382,"style":559},"equal over a full cycle",[360,1410,1411],{"x":1393,"y":1386,"style":559},"compare quarterly totals",[25,1413,1415],{"id":1414},"step-4-the-calendar-in-excel-with-holidays","Step 4 — The calendar in Excel, with holidays",[30,1417,1419],{"className":70,"code":1418,"language":72,"meta":35,"style":35},"import holidays\nfrom openpyxl import load_workbook\nfrom openpyxl.formatting.rule import CellIsRule\nfrom openpyxl.styles import Alignment, Font, PatternFill\n\nbank = holidays.country_holidays(\"GB\", subdiv=\"ENG\", years=2026)\ngrid = q4.T\ngrid.columns = [d.strftime(\"%d %b\") for d in q4.index]\npath = \"shift_rota_2026-Q4.xlsx\"\nwith pd.ExcelWriter(path) as xw:\n    grid.to_excel(xw, sheet_name=\"Rota\", startrow=1)\n    hours.to_excel(xw, sheet_name=\"Hours\")\n    coverage.rename(index=lambda d: d.date()).to_excel(xw, sheet_name=\"Coverage check\")\n\nwb = load_workbook(path)\nws = wb[\"Rota\"]\nfor j, d in enumerate(q4.index, start=2):\n    ws.cell(1, j, d.strftime(\"%a\")).alignment = Alignment(horizontal=\"center\")\n    if d.date() in bank:\n        ws.cell(1, j, \"BH\").font = Font(bold=True, color=\"C00000\")\n    ws.column_dimensions[ws.cell(2, j).column_letter].width = 6.5\narea = f\"B3:{ws.cell(2, ws.max_column).column_letter}6\"\nfor code, colour in [(\"D\", \"FFF2CC\"), (\"N\", \"D9E1F2\")]:\n    ws.conditional_formatting.add(area, CellIsRule(operator=\"equal\", formula=[f'\"{code}\"'], fill=PatternFill(\"solid\", bgColor=colour)))\nfor row in ws.iter_rows(min_row=3, min_col=2):\n    for cell in row:\n        cell.alignment = Alignment(horizontal=\"center\")\nws.freeze_panes = \"B3\"\nwb.save(path)\nprint(\"bank holidays in the quarter:\", [(d, n) for d, n in sorted(bank.items()) if d >= q4.index[0].date()])\n",[37,1420,1421,1427,1440,1452,1464,1468,1503,1513,1544,1554,1567,1591,1605,1626,1631,1642,1658,1684,1719,1733,1775,1791,1822,1856,1919,1950,1963,1981,1992,1998],{"__ignoreMap":35},[40,1422,1423,1425],{"class":42,"line":43},[40,1424,80],{"class":79},[40,1426,60],{"class":83},[40,1428,1429,1432,1435,1437],{"class":42,"line":93},[40,1430,1431],{"class":79},"from",[40,1433,1434],{"class":83}," openpyxl ",[40,1436,80],{"class":79},[40,1438,1439],{"class":83}," load_workbook\n",[40,1441,1442,1444,1447,1449],{"class":42,"line":100},[40,1443,1431],{"class":79},[40,1445,1446],{"class":83}," openpyxl.formatting.rule ",[40,1448,80],{"class":79},[40,1450,1451],{"class":83}," CellIsRule\n",[40,1453,1454,1456,1459,1461],{"class":42,"line":107},[40,1455,1431],{"class":79},[40,1457,1458],{"class":83}," openpyxl.styles ",[40,1460,80],{"class":79},[40,1462,1463],{"class":83}," Alignment, Font, PatternFill\n",[40,1465,1466],{"class":42,"line":120},[40,1467,97],{"emptyLinePlaceholder":96},[40,1469,1470,1473,1475,1478,1481,1483,1486,1488,1491,1493,1496,1498,1501],{"class":42,"line":132},[40,1471,1472],{"class":83},"bank ",[40,1474,966],{"class":79},[40,1476,1477],{"class":83}," holidays.country_holidays(",[40,1479,1480],{"class":50},"\"GB\"",[40,1482,206],{"class":83},[40,1484,1485],{"class":972},"subdiv",[40,1487,966],{"class":79},[40,1489,1490],{"class":50},"\"ENG\"",[40,1492,206],{"class":83},[40,1494,1495],{"class":972},"years",[40,1497,966],{"class":79},[40,1499,1500],{"class":110},"2026",[40,1502,275],{"class":83},[40,1504,1505,1508,1510],{"class":42,"line":152},[40,1506,1507],{"class":83},"grid ",[40,1509,966],{"class":79},[40,1511,1512],{"class":83}," q4.T\n",[40,1514,1515,1518,1520,1523,1525,1528,1530,1532,1534,1536,1539,1541],{"class":42,"line":175},[40,1516,1517],{"class":83},"grid.columns ",[40,1519,966],{"class":79},[40,1521,1522],{"class":83}," [d.strftime(",[40,1524,1126],{"class":50},[40,1526,1527],{"class":1129},"%d",[40,1529,1136],{"class":1129},[40,1531,1126],{"class":50},[40,1533,166],{"class":83},[40,1535,300],{"class":79},[40,1537,1538],{"class":83}," d ",[40,1540,306],{"class":79},[40,1542,1543],{"class":83}," q4.index]\n",[40,1545,1546,1549,1551],{"class":42,"line":180},[40,1547,1548],{"class":83},"path ",[40,1550,966],{"class":79},[40,1552,1553],{"class":50}," \"shift_rota_2026-Q4.xlsx\"\n",[40,1555,1556,1559,1562,1564],{"class":42,"line":186},[40,1557,1558],{"class":79},"with",[40,1560,1561],{"class":83}," pd.ExcelWriter(path) ",[40,1563,87],{"class":79},[40,1565,1566],{"class":83}," xw:\n",[40,1568,1569,1572,1575,1577,1580,1582,1585,1587,1589],{"class":42,"line":240},[40,1570,1571],{"class":83},"    grid.to_excel(xw, ",[40,1573,1574],{"class":972},"sheet_name",[40,1576,966],{"class":79},[40,1578,1579],{"class":50},"\"Rota\"",[40,1581,206],{"class":83},[40,1583,1584],{"class":972},"startrow",[40,1586,966],{"class":79},[40,1588,473],{"class":110},[40,1590,275],{"class":83},[40,1592,1593,1596,1598,1600,1603],{"class":42,"line":260},[40,1594,1595],{"class":83},"    hours.to_excel(xw, ",[40,1597,1574],{"class":972},[40,1599,966],{"class":79},[40,1601,1602],{"class":50},"\"Hours\"",[40,1604,275],{"class":83},[40,1606,1607,1610,1612,1614,1617,1619,1621,1624],{"class":42,"line":278},[40,1608,1609],{"class":83},"    coverage.rename(",[40,1611,1074],{"class":972},[40,1613,1120],{"class":79},[40,1615,1616],{"class":83}," d: d.date()).to_excel(xw, ",[40,1618,1574],{"class":972},[40,1620,966],{"class":79},[40,1622,1623],{"class":50},"\"Coverage check\"",[40,1625,275],{"class":83},[40,1627,1629],{"class":42,"line":1628},14,[40,1630,97],{"emptyLinePlaceholder":96},[40,1632,1634,1637,1639],{"class":42,"line":1633},15,[40,1635,1636],{"class":83},"wb ",[40,1638,966],{"class":79},[40,1640,1641],{"class":83}," load_workbook(path)\n",[40,1643,1645,1648,1650,1653,1655],{"class":42,"line":1644},16,[40,1646,1647],{"class":83},"ws ",[40,1649,966],{"class":79},[40,1651,1652],{"class":83}," wb[",[40,1654,1579],{"class":50},[40,1656,1657],{"class":83},"]\n",[40,1659,1661,1663,1666,1668,1671,1674,1677,1679,1681],{"class":42,"line":1660},17,[40,1662,300],{"class":79},[40,1664,1665],{"class":83}," j, d ",[40,1667,306],{"class":79},[40,1669,1670],{"class":110}," enumerate",[40,1672,1673],{"class":83},"(q4.index, ",[40,1675,1676],{"class":972},"start",[40,1678,966],{"class":79},[40,1680,495],{"class":110},[40,1682,1683],{"class":83},"):\n",[40,1685,1687,1690,1692,1695,1697,1699,1701,1704,1706,1709,1712,1714,1717],{"class":42,"line":1686},18,[40,1688,1689],{"class":83},"    ws.cell(",[40,1691,473],{"class":110},[40,1693,1694],{"class":83},", j, d.strftime(",[40,1696,1126],{"class":50},[40,1698,1130],{"class":1129},[40,1700,1126],{"class":50},[40,1702,1703],{"class":83},")).alignment ",[40,1705,966],{"class":79},[40,1707,1708],{"class":83}," Alignment(",[40,1710,1711],{"class":972},"horizontal",[40,1713,966],{"class":79},[40,1715,1716],{"class":50},"\"center\"",[40,1718,275],{"class":83},[40,1720,1722,1725,1728,1730],{"class":42,"line":1721},19,[40,1723,1724],{"class":79},"    if",[40,1726,1727],{"class":83}," d.date() ",[40,1729,306],{"class":79},[40,1731,1732],{"class":83}," bank:\n",[40,1734,1736,1739,1741,1744,1747,1750,1752,1755,1758,1760,1763,1765,1768,1770,1773],{"class":42,"line":1735},20,[40,1737,1738],{"class":83},"        ws.cell(",[40,1740,473],{"class":110},[40,1742,1743],{"class":83},", j, ",[40,1745,1746],{"class":50},"\"BH\"",[40,1748,1749],{"class":83},").font ",[40,1751,966],{"class":79},[40,1753,1754],{"class":83}," Font(",[40,1756,1757],{"class":972},"bold",[40,1759,966],{"class":79},[40,1761,1762],{"class":110},"True",[40,1764,206],{"class":83},[40,1766,1767],{"class":972},"color",[40,1769,966],{"class":79},[40,1771,1772],{"class":50},"\"C00000\"",[40,1774,275],{"class":83},[40,1776,1778,1781,1783,1786,1788],{"class":42,"line":1777},21,[40,1779,1780],{"class":83},"    ws.column_dimensions[ws.cell(",[40,1782,495],{"class":110},[40,1784,1785],{"class":83},", j).column_letter].width ",[40,1787,966],{"class":79},[40,1789,1790],{"class":110}," 6.5\n",[40,1792,1794,1797,1799,1802,1805,1808,1811,1813,1816,1819],{"class":42,"line":1793},22,[40,1795,1796],{"class":83},"area ",[40,1798,966],{"class":79},[40,1800,1801],{"class":79}," f",[40,1803,1804],{"class":50},"\"B3:",[40,1806,1807],{"class":1129},"{",[40,1809,1810],{"class":83},"ws.cell(",[40,1812,495],{"class":110},[40,1814,1815],{"class":83},", ws.max_column).column_letter",[40,1817,1818],{"class":1129},"}",[40,1820,1821],{"class":50},"6\"\n",[40,1823,1825,1827,1830,1832,1835,1837,1839,1842,1845,1848,1850,1853],{"class":42,"line":1824},23,[40,1826,300],{"class":79},[40,1828,1829],{"class":83}," code, colour ",[40,1831,306],{"class":79},[40,1833,1834],{"class":83}," [(",[40,1836,229],{"class":50},[40,1838,206],{"class":83},[40,1840,1841],{"class":50},"\"FFF2CC\"",[40,1843,1844],{"class":83},"), (",[40,1846,1847],{"class":50},"\"N\"",[40,1849,206],{"class":83},[40,1851,1852],{"class":50},"\"D9E1F2\"",[40,1854,1855],{"class":83},")]:\n",[40,1857,1859,1862,1865,1867,1870,1872,1875,1877,1880,1883,1886,1888,1890,1892,1895,1898,1901,1903,1906,1909,1911,1914,1916],{"class":42,"line":1858},24,[40,1860,1861],{"class":83},"    ws.conditional_formatting.add(area, CellIsRule(",[40,1863,1864],{"class":972},"operator",[40,1866,966],{"class":79},[40,1868,1869],{"class":50},"\"equal\"",[40,1871,206],{"class":83},[40,1873,1874],{"class":972},"formula",[40,1876,966],{"class":79},[40,1878,1879],{"class":83},"[",[40,1881,1882],{"class":79},"f",[40,1884,1885],{"class":50},"'\"",[40,1887,1807],{"class":1129},[40,1889,37],{"class":83},[40,1891,1818],{"class":1129},[40,1893,1894],{"class":50},"\"'",[40,1896,1897],{"class":83},"], ",[40,1899,1900],{"class":972},"fill",[40,1902,966],{"class":79},[40,1904,1905],{"class":83},"PatternFill(",[40,1907,1908],{"class":50},"\"solid\"",[40,1910,206],{"class":83},[40,1912,1913],{"class":972},"bgColor",[40,1915,966],{"class":79},[40,1917,1918],{"class":83},"colour)))\n",[40,1920,1922,1924,1927,1929,1932,1935,1937,1939,1941,1944,1946,1948],{"class":42,"line":1921},25,[40,1923,300],{"class":79},[40,1925,1926],{"class":83}," row ",[40,1928,306],{"class":79},[40,1930,1931],{"class":83}," ws.iter_rows(",[40,1933,1934],{"class":972},"min_row",[40,1936,966],{"class":79},[40,1938,500],{"class":110},[40,1940,206],{"class":83},[40,1942,1943],{"class":972},"min_col",[40,1945,966],{"class":79},[40,1947,495],{"class":110},[40,1949,1683],{"class":83},[40,1951,1953,1955,1958,1960],{"class":42,"line":1952},26,[40,1954,994],{"class":79},[40,1956,1957],{"class":83}," cell ",[40,1959,306],{"class":79},[40,1961,1962],{"class":83}," row:\n",[40,1964,1966,1969,1971,1973,1975,1977,1979],{"class":42,"line":1965},27,[40,1967,1968],{"class":83},"        cell.alignment ",[40,1970,966],{"class":79},[40,1972,1708],{"class":83},[40,1974,1711],{"class":972},[40,1976,966],{"class":79},[40,1978,1716],{"class":50},[40,1980,275],{"class":83},[40,1982,1984,1987,1989],{"class":42,"line":1983},28,[40,1985,1986],{"class":83},"ws.freeze_panes ",[40,1988,966],{"class":79},[40,1990,1991],{"class":50}," \"B3\"\n",[40,1993,1995],{"class":42,"line":1994},29,[40,1996,1997],{"class":83},"wb.save(path)\n",[40,1999,2001,2003,2005,2008,2011,2013,2016,2018,2021,2024,2027,2029,2032,2035,2037],{"class":42,"line":2000},30,[40,2002,263],{"class":110},[40,2004,161],{"class":83},[40,2006,2007],{"class":50},"\"bank holidays in the quarter:\"",[40,2009,2010],{"class":83},", [(d, n) ",[40,2012,300],{"class":79},[40,2014,2015],{"class":83}," d, n ",[40,2017,306],{"class":79},[40,2019,2020],{"class":110}," sorted",[40,2022,2023],{"class":83},"(bank.items()) ",[40,2025,2026],{"class":79},"if",[40,2028,1538],{"class":83},[40,2030,2031],{"class":79},">=",[40,2033,2034],{"class":83}," q4.index[",[40,2036,203],{"class":110},[40,2038,2039],{"class":83},"].date()])\n",[10,2041,2042,2043,2046],{},"The calendar puts teams down the side and dates across, with the day of the week above each date, so a team leader can read a whole quarter on one sheet. Day shifts are pale yellow and night shifts pale blue, applied with conditional formatting so that a manual swap typed into the sheet changes colour automatically. Public holidays are marked ",[14,2044,2045],{},"BH"," in red above the date, which matters because holiday shifts are usually paid at a higher rate and need to be counted. The hours and coverage tables go on their own sheets, giving the manager the evidence behind the rota. The quarter includes Christmas Day on Friday 25 December, Boxing Day on the Saturday and the substitute bank holiday on Monday 28 December, and the rota shows at a glance which teams work them — something worth agreeing well in advance.",[25,2048,2050],{"id":2049},"common-pitfalls","Common pitfalls",[2052,2053,2054,2062,2068,2074,2080],"ul",{},[2055,2056,2057,2061],"li",{},[2058,2059,2060],"strong",{},"Rotas typed by hand."," Generate them from the pattern and an anchor date.",[2055,2063,2064,2067],{},[2058,2065,2066],{},"No coverage check."," Count teams per shift per day after every change.",[2055,2069,2070,2073],{},[2058,2071,2072],{},"Expecting equal monthly hours."," Months do not align with the cycle; compare over full cycles.",[2055,2075,2076,2079],{},[2058,2077,2078],{},"Losing the anchor."," Store the anchor date with the pattern; without it the rota cannot be reproduced.",[2055,2081,2082,2085],{},[2058,2083,2084],{},"Hard-coded colours."," Use conditional formatting so swapped shifts recolour themselves.",[25,2087,2089],{"id":2088},"conclusion","Conclusion",[10,2091,2092,2093,2096],{},"A rotating shift rota is a cycle of shift codes, an offset per team and an anchor date. A team's shift on any day is the cycle at position ",[14,2094,2095],{},"(days since anchor + offset) mod cycle length",", which expands to any date range in one step. Check that every day has exactly one team on each shift, total the hours per team, mark the public holidays and write a colour-coded calendar whose formatting follows any later swaps.",[25,2098,2100],{"id":2099},"frequently-asked-questions","Frequently asked questions",[10,2102,2103,2106,2107,2110],{},[2058,2104,2105],{},"How do I generate a rotating shift pattern in Python?","\nStore the pattern as a sequence of codes, give each team an offset, and look up ",[37,2108,2109],{},"(date − anchor).days + offset"," modulo the cycle length for every date.",[10,2112,2113,2116],{},[2058,2114,2115],{},"What is the 2-2-3 shift pattern?","\nA 14-day sequence of two days on, two off, three on, two off, two on and three off, usually run on 12-hour shifts and alternated between days and nights.",[10,2118,2119,2122],{},[2058,2120,2121],{},"How do I check a rota covers every shift?","\nCount the teams on each shift for each date and list any date where the count is not exactly one.",[10,2124,2125,2128],{},[2058,2126,2127],{},"Why do teams work different hours in a month on the same pattern?","\nCalendar months do not line up with the rotation cycle; over a full cycle every team works the same number of shifts.",[25,2130,2132],{"id":2131},"related","Related",[2052,2134,2135,2141,2148,2155],{},[2055,2136,2137,2138,2140],{},"Up one level: ",[19,2139,22],{"href":21}," — dates in Excel data.",[2055,2142,2143,2147],{},[19,2144,2146],{"href":2145},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data\u002F","Build an Overtime Report from Shift Data"," — paying for the shifts.",[2055,2149,2150,2154],{},[19,2151,2153],{"href":2152},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-weekends-and-holidays-in-a-date-column\u002F","Highlight Weekends and Holidays in a Date Column"," — marking special days.",[2055,2156,2157,2161],{},[19,2158,2160],{"href":2159},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-the-nth-weekday-of-a-month\u002F","Calculate the Nth Weekday of a Month"," — other repeating dates.",[2163,2164,2165],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":93,"depth":93,"links":2167},[2168,2169,2170,2171,2172,2173,2174,2175,2176],{"id":27,"depth":93,"text":28},{"id":66,"depth":93,"text":67},{"id":940,"depth":93,"text":941},{"id":1150,"depth":93,"text":1151},{"id":1414,"depth":93,"text":1415},{"id":2049,"depth":93,"text":2050},{"id":2088,"depth":93,"text":2089},{"id":2099,"depth":93,"text":2100},{"id":2131,"depth":93,"text":2132},"2026-10-02","Generate a shift rota for teams on a rotating pattern — such as four teams on a 2-2-3 day and night cycle — with pandas: store each team's pattern as a repeating sequence anchored to a start date, expand it to any date range, prove every day has exactly one team on days and one on nights, total hours per team per month, mark public holidays, and write a colour-coded calendar grid to Excel.","md",[2181,2183,2185,2187],{"q":2105,"a":2182},"Store the pattern as a sequence of codes, give each team an offset, and look up (date − anchor).days + offset modulo the cycle length for every date.",{"q":2115,"a":2184},"A 14-day sequence of two days on, two off, three on, two off, two on and three off, usually run on 12-hour shifts and alternated between days and nights.",{"q":2121,"a":2186},"Count the teams on each shift for each date and list any date where the count is not exactly one.",{"q":2127,"a":2188},"Calendar months do not line up with the rotation cycle; over a full cycle every team works the same number of shifts.","rotating shift calendar python excel",null,{"breadcrumb":2192},[2193,2196,2199],{"name":2194,"item":2195},"Home","\u002F",{"name":2197,"item":2198},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":22,"item":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcreate-a-shift-calendar-with-rotating-patterns",{"title":2202,"description":2203},"Create a Shift Calendar with Rotating Patterns in Python","Rotating shift rotas with pandas: a 2-2-3 pattern anchored to a start date, expanded to any period, daily coverage checks, hours per team, holidays and a colour-coded Excel calendar.","create-a-shift-calendar-with-rotating-patterns","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcreate-a-shift-calendar-with-rotating-patterns\u002Findex",[2207,2208],"dates-and-times","report-layouts","how-to","HyLlkuE1J2VhsSeQsYNU8gkyx078segkK4EKk9VW1wc",[2212,2216],{"title":2213,"path":2214,"stem":2215,"children":-1},"Convert Year-Week Codes to Dates with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fconvert-year-week-codes-to-dates-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fconvert-year-week-codes-to-dates-with-pandas\u002Findex",{"title":2217,"path":2218,"stem":2219,"children":-1},"Expand Date Ranges into One Row per Day with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fexpand-date-ranges-into-one-row-per-day-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fexpand-date-ranges-into-one-row-per-day-with-pandas\u002Findex",[2221,2223],{"path":2222,"title":2197},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2224,"title":22},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data",[],[2227,2230,2232,2235,2238,2241],{"path":2228,"title":2229,"kind":2209},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-overdue-dates-with-openpyxl","Highlight Overdue Dates in Excel with openpyxl",{"path":2231,"title":2153,"kind":2209},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-weekends-and-holidays-in-a-date-column",{"path":2233,"title":2234,"kind":2209},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fkeep-the-latest-record-per-key-with-pandas","Keep the Latest Record per Key When Deduplicating with pandas",{"path":2236,"title":2237,"kind":2209},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling","Calculate Moving Averages with pandas rolling",{"path":2239,"title":2240,"kind":2209},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas","EOMONTH and EDATE Equivalents in pandas",{"path":2242,"title":2243,"kind":2209},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fyearfrac-and-days360-equivalents-in-python","YEARFRAC and DAYS360 Equivalents in Python",1790972105888]