[{"data":1,"prerenderedAt":2451},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-business-days-between-excel-dates-with-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-business-days-between-excel-dates-with-pandas":2416,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-business-days-between-excel-dates-with-pandas":2425,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-business-days-between-excel-dates-with-pandas":2431,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-business-days-between-excel-dates-with-pandas":2432},{"id":4,"title":5,"body":6,"dateModified":2382,"datePublished":2382,"description":2383,"extension":2384,"faq":2385,"keyword":2394,"kind":2395,"meta":2396,"navigation":268,"path":2405,"seo":2406,"slug":2409,"stem":2410,"topics":2411,"type":2414,"__hash__":2415},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-business-days-between-excel-dates-with-pandas\u002Findex.md","Calculate Business Days Between Excel Dates with pandas",{"type":7,"value":8,"toc":2367},"minimark",[9,28,201,206,237,240,499,503,526,679,697,701,715,945,952,1046,1050,1060,1171,1177,1181,1192,1320,1323,1327,1349,1552,1566,1652,1656,1659,1828,1839,1843,1854,2116,2123,2127,2238,2242,2265,2269,2291,2295,2305,2311,2317,2323,2327,2363],[10,11,12,13,17,18,21,22,27],"p",{},"Service-level reports live and die by working days: how many business days a ticket stayed open, whether an\ninvoice was paid within thirty working days, when a request is due if it arrived on a Friday. In Excel those are\n",[14,15,16],"code",{},"NETWORKDAYS"," and ",[14,19,20],{},"WORKDAY",", filled down a column with a holiday range on another sheet. Moving the report to\nPython means reproducing them exactly — including the off-by-one rules that make a careless translation disagree\nwith the spreadsheet it replaces. This guide, part of\n",[23,24,26],"a",{"href":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002F","Working with Dates and Times in Excel Data",",\nuses NumPy's business-day functions, which are vectorised and fast.",[29,30,38,39,38,43,38,47,38,54,38,62,38,66,38,73,38,76,38,80,38,83,38,87,38,90,38,94,38,97,38,101,38,104,38,108,38,112,38,116,38,120,38,124,38,126,38,130,38,132,38,135,38,137,38,140,38,143,38,147,38,149,38,152,38,154,38,157,38,159,38,162,38,164,38,167,38,169,38,172,38,175,38,179,38,181,38,184,38,186,38,189,38,191,38,193,38,195],"svg",{"viewBox":31,"role":32,"ariaLabelledBy":33,"xmlns":36,"style":37},"0 0 426 194","img",[34,35],"bd-week-t","bd-week-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:426px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[40,41,42],"title",{"id":34},"Counting working days across a weekend and a holiday",[44,45,46],"desc",{"id":35},"From Thursday 24 December to Monday 4 January there are twelve calendar days. Weekends and the holidays on 25 December, 28 December and 1 January are excluded, leaving five working days when both ends are counted.",[48,49],"rect",{"x":50,"y":50,"width":51,"height":52,"fill":53},"0","426","194","#ffffff",[48,55],{"x":56,"y":56,"width":57,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"20","36","30","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[48,63],{"x":64,"y":56,"width":65,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"56","70",[67,68,72],"text",{"x":69,"y":70,"style":71},"91","40","font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[48,74],{"x":75,"y":56,"width":65,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"126",[67,77,79],{"x":78,"y":70,"style":71},"161","B",[48,81],{"x":82,"y":56,"width":65,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"196",[67,84,86],{"x":85,"y":70,"style":71},"231","C",[48,88],{"x":89,"y":56,"width":65,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"266",[67,91,93],{"x":92,"y":70,"style":71},"301","D",[48,95],{"x":96,"y":56,"width":65,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"336",[67,98,100],{"x":99,"y":70,"style":71},"371","E",[48,102],{"x":56,"y":103,"width":57,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"50",[67,105,107],{"x":106,"y":65,"style":71},"38","wk 1",[48,109],{"x":64,"y":103,"width":65,"height":58,"rx":50,"fill":110,"stroke":111,"style":61},"#d9f4f1","var(--teal,#0f9488)",[67,113,115],{"x":69,"y":65,"style":114},"font-size:11.5px;font-weight:600;fill:var(--teal-ink,#0b6157);text-anchor:middle","Thu 24",[48,117],{"x":75,"y":103,"width":65,"height":58,"rx":50,"fill":118,"stroke":119,"style":61},"#fee8f2","var(--accent,#f43f8f)",[67,121,123],{"x":78,"y":65,"style":122},"font-size:11.5px;font-weight:600;fill:var(--accent-ink,#be185d);text-anchor:middle","Fri 25",[48,125],{"x":82,"y":103,"width":65,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},[67,127,129],{"x":85,"y":65,"style":128},"font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","Sat 26",[48,131],{"x":89,"y":103,"width":65,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},[67,133,134],{"x":92,"y":65,"style":128},"Sun 27",[48,136],{"x":96,"y":103,"width":65,"height":58,"rx":50,"fill":118,"stroke":119,"style":61},[67,138,139],{"x":99,"y":65,"style":122},"Mon 28",[48,141],{"x":56,"y":142,"width":57,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"80",[67,144,146],{"x":106,"y":145,"style":71},"100","wk 2",[48,148],{"x":64,"y":142,"width":65,"height":58,"rx":50,"fill":110,"stroke":111,"style":61},[67,150,151],{"x":69,"y":145,"style":114},"Tue 29",[48,153],{"x":75,"y":142,"width":65,"height":58,"rx":50,"fill":110,"stroke":111,"style":61},[67,155,156],{"x":78,"y":145,"style":114},"Wed 30",[48,158],{"x":82,"y":142,"width":65,"height":58,"rx":50,"fill":110,"stroke":111,"style":61},[67,160,161],{"x":85,"y":145,"style":114},"Thu 31",[48,163],{"x":89,"y":142,"width":65,"height":58,"rx":50,"fill":118,"stroke":119,"style":61},[67,165,166],{"x":92,"y":145,"style":122},"Fri 1",[48,168],{"x":96,"y":142,"width":65,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},[67,170,171],{"x":99,"y":145,"style":128},"Sat 2",[48,173],{"x":56,"y":174,"width":57,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"110",[67,176,178],{"x":106,"y":177,"style":71},"130","wk 3",[48,180],{"x":64,"y":174,"width":65,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},[67,182,183],{"x":69,"y":177,"style":128},"Sun 3",[48,185],{"x":75,"y":174,"width":65,"height":58,"rx":50,"fill":110,"stroke":111,"style":61},[67,187,188],{"x":78,"y":177,"style":114},"Mon 4",[48,190],{"x":82,"y":174,"width":65,"height":58,"rx":50,"fill":53,"stroke":60,"style":61},[48,192],{"x":89,"y":174,"width":65,"height":58,"rx":50,"fill":53,"stroke":60,"style":61},[48,194],{"x":96,"y":174,"width":65,"height":58,"rx":50,"fill":53,"stroke":60,"style":61},[67,196,200],{"x":197,"y":198,"style":199},"213","172","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","teal = working day (5), pink = holiday, grey = weekend",[202,203,205],"h2",{"id":204},"prerequisites","Prerequisites",[207,208,213],"pre",{"className":209,"code":210,"language":211,"meta":212,"style":212},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas numpy openpyxl\n","bash","",[14,214,215],{"__ignoreMap":212},[216,217,220,224,228,231,234],"span",{"class":218,"line":219},"line",1,[216,221,223],{"class":222},"sMTad","pip",[216,225,227],{"class":226},"srMev"," install",[216,229,230],{"class":226}," pandas",[216,232,233],{"class":226}," numpy",[216,235,236],{"class":226}," openpyxl\n",[10,238,239],{},"A ticket log and a holiday list, written as a workbook with two sheets the way these reports are usually organised:",[207,241,245],{"className":242,"code":243,"language":244,"meta":212,"style":212},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ntickets = pd.DataFrame({\n    \"Ticket\": [\"T-101\", \"T-102\", \"T-103\", \"T-104\", \"T-105\"],\n    \"Opened\": pd.to_datetime([\"2026-09-07\", \"2026-09-11\", \"2026-12-24\", \"2026-09-12\", None]),\n    \"Closed\": pd.to_datetime([\"2026-09-11\", \"2026-09-14\", \"2027-01-04\", \"2026-09-12\", \"2026-10-01\"]),\n})\nholidays = pd.DataFrame({\"Holiday\": pd.to_datetime([\"2026-12-25\", \"2026-12-28\", \"2027-01-01\"])})\n\nwith pd.ExcelWriter(\"service_desk.xlsx\") as writer:\n    tickets.to_excel(writer, sheet_name=\"Tickets\", index=False)\n    holidays.to_excel(writer, sheet_name=\"Holidays\", index=False)\n","python",[14,246,247,263,270,282,318,354,385,391,423,428,448,476],{"__ignoreMap":212},[216,248,249,253,257,260],{"class":218,"line":219},[216,250,252],{"class":251},"s-kum","import",[216,254,256],{"class":255},"skGVy"," pandas ",[216,258,259],{"class":251},"as",[216,261,262],{"class":255}," pd\n",[216,264,266],{"class":218,"line":265},2,[216,267,269],{"emptyLinePlaceholder":268},true,"\n",[216,271,273,276,279],{"class":218,"line":272},3,[216,274,275],{"class":255},"tickets ",[216,277,278],{"class":251},"=",[216,280,281],{"class":255}," pd.DataFrame({\n",[216,283,285,288,291,294,297,300,302,305,307,310,312,315],{"class":218,"line":284},4,[216,286,287],{"class":226},"    \"Ticket\"",[216,289,290],{"class":255},": [",[216,292,293],{"class":226},"\"T-101\"",[216,295,296],{"class":255},", ",[216,298,299],{"class":226},"\"T-102\"",[216,301,296],{"class":255},[216,303,304],{"class":226},"\"T-103\"",[216,306,296],{"class":255},[216,308,309],{"class":226},"\"T-104\"",[216,311,296],{"class":255},[216,313,314],{"class":226},"\"T-105\"",[216,316,317],{"class":255},"],\n",[216,319,321,324,327,330,332,335,337,340,342,345,347,351],{"class":218,"line":320},5,[216,322,323],{"class":226},"    \"Opened\"",[216,325,326],{"class":255},": pd.to_datetime([",[216,328,329],{"class":226},"\"2026-09-07\"",[216,331,296],{"class":255},[216,333,334],{"class":226},"\"2026-09-11\"",[216,336,296],{"class":255},[216,338,339],{"class":226},"\"2026-12-24\"",[216,341,296],{"class":255},[216,343,344],{"class":226},"\"2026-09-12\"",[216,346,296],{"class":255},[216,348,350],{"class":349},"sP0c6","None",[216,352,353],{"class":255},"]),\n",[216,355,357,360,362,364,366,369,371,374,376,378,380,383],{"class":218,"line":356},6,[216,358,359],{"class":226},"    \"Closed\"",[216,361,326],{"class":255},[216,363,334],{"class":226},[216,365,296],{"class":255},[216,367,368],{"class":226},"\"2026-09-14\"",[216,370,296],{"class":255},[216,372,373],{"class":226},"\"2027-01-04\"",[216,375,296],{"class":255},[216,377,344],{"class":226},[216,379,296],{"class":255},[216,381,382],{"class":226},"\"2026-10-01\"",[216,384,353],{"class":255},[216,386,388],{"class":218,"line":387},7,[216,389,390],{"class":255},"})\n",[216,392,394,397,399,402,405,407,410,412,415,417,420],{"class":218,"line":393},8,[216,395,396],{"class":255},"holidays ",[216,398,278],{"class":251},[216,400,401],{"class":255}," pd.DataFrame({",[216,403,404],{"class":226},"\"Holiday\"",[216,406,326],{"class":255},[216,408,409],{"class":226},"\"2026-12-25\"",[216,411,296],{"class":255},[216,413,414],{"class":226},"\"2026-12-28\"",[216,416,296],{"class":255},[216,418,419],{"class":226},"\"2027-01-01\"",[216,421,422],{"class":255},"])})\n",[216,424,426],{"class":218,"line":425},9,[216,427,269],{"emptyLinePlaceholder":268},[216,429,431,434,437,440,443,445],{"class":218,"line":430},10,[216,432,433],{"class":251},"with",[216,435,436],{"class":255}," pd.ExcelWriter(",[216,438,439],{"class":226},"\"service_desk.xlsx\"",[216,441,442],{"class":255},") ",[216,444,259],{"class":251},[216,446,447],{"class":255}," writer:\n",[216,449,451,454,458,460,463,465,468,470,473],{"class":218,"line":450},11,[216,452,453],{"class":255},"    tickets.to_excel(writer, ",[216,455,457],{"class":456},"sa561","sheet_name",[216,459,278],{"class":251},[216,461,462],{"class":226},"\"Tickets\"",[216,464,296],{"class":255},[216,466,467],{"class":456},"index",[216,469,278],{"class":251},[216,471,472],{"class":349},"False",[216,474,475],{"class":255},")\n",[216,477,479,482,484,486,489,491,493,495,497],{"class":218,"line":478},12,[216,480,481],{"class":255},"    holidays.to_excel(writer, ",[216,483,457],{"class":456},[216,485,278],{"class":251},[216,487,488],{"class":226},"\"Holidays\"",[216,490,296],{"class":255},[216,492,467],{"class":456},[216,494,278],{"class":251},[216,496,472],{"class":349},[216,498,475],{"class":255},[202,500,502],{"id":501},"step-1-count-business-days-with-busday_count","Step 1 — Count business days with busday_count",[10,504,505,508,509,512,513,517,518,521,522,525],{},[14,506,507],{},"np.busday_count(begin, end)"," counts Monday-to-Friday days in the half-open interval from ",[14,510,511],{},"begin"," up to but ",[514,515,516],"em",{},"not including"," ",[14,519,520],{},"end",". It works on arrays of\n",[14,523,524],{},"datetime64[D]",", so a whole column is one call.",[207,527,529],{"className":242,"code":528,"language":244,"meta":212,"style":212},"import numpy as np\n\nframe = pd.read_excel(\"service_desk.xlsx\", sheet_name=\"Tickets\", parse_dates=[\"Opened\", \"Closed\"])\nvalid = frame[[\"Opened\", \"Closed\"]].notna().all(axis=1)\n\nbegin = frame.loc[valid, \"Opened\"].to_numpy(dtype=\"datetime64[D]\")\nend = frame.loc[valid, \"Closed\"].to_numpy(dtype=\"datetime64[D]\")\nprint(np.busday_count(begin, end))          # [4 1 7 0]\n",[14,530,531,543,547,588,617,621,646,667],{"__ignoreMap":212},[216,532,533,535,538,540],{"class":218,"line":219},[216,534,252],{"class":251},[216,536,537],{"class":255}," numpy ",[216,539,259],{"class":251},[216,541,542],{"class":255}," np\n",[216,544,545],{"class":218,"line":265},[216,546,269],{"emptyLinePlaceholder":268},[216,548,549,552,554,557,559,561,563,565,567,569,572,574,577,580,582,585],{"class":218,"line":272},[216,550,551],{"class":255},"frame ",[216,553,278],{"class":251},[216,555,556],{"class":255}," pd.read_excel(",[216,558,439],{"class":226},[216,560,296],{"class":255},[216,562,457],{"class":456},[216,564,278],{"class":251},[216,566,462],{"class":226},[216,568,296],{"class":255},[216,570,571],{"class":456},"parse_dates",[216,573,278],{"class":251},[216,575,576],{"class":255},"[",[216,578,579],{"class":226},"\"Opened\"",[216,581,296],{"class":255},[216,583,584],{"class":226},"\"Closed\"",[216,586,587],{"class":255},"])\n",[216,589,590,593,595,598,600,602,604,607,610,612,615],{"class":218,"line":284},[216,591,592],{"class":255},"valid ",[216,594,278],{"class":251},[216,596,597],{"class":255}," frame[[",[216,599,579],{"class":226},[216,601,296],{"class":255},[216,603,584],{"class":226},[216,605,606],{"class":255},"]].notna().all(",[216,608,609],{"class":456},"axis",[216,611,278],{"class":251},[216,613,614],{"class":349},"1",[216,616,475],{"class":255},[216,618,619],{"class":218,"line":320},[216,620,269],{"emptyLinePlaceholder":268},[216,622,623,626,628,631,633,636,639,641,644],{"class":218,"line":356},[216,624,625],{"class":255},"begin ",[216,627,278],{"class":251},[216,629,630],{"class":255}," frame.loc[valid, ",[216,632,579],{"class":226},[216,634,635],{"class":255},"].to_numpy(",[216,637,638],{"class":456},"dtype",[216,640,278],{"class":251},[216,642,643],{"class":226},"\"datetime64[D]\"",[216,645,475],{"class":255},[216,647,648,651,653,655,657,659,661,663,665],{"class":218,"line":387},[216,649,650],{"class":255},"end ",[216,652,278],{"class":251},[216,654,630],{"class":255},[216,656,584],{"class":226},[216,658,635],{"class":255},[216,660,638],{"class":456},[216,662,278],{"class":251},[216,664,643],{"class":226},[216,666,475],{"class":255},[216,668,669,672,675],{"class":218,"line":393},[216,670,671],{"class":349},"print",[216,673,674],{"class":255},"(np.busday_count(begin, end))          ",[216,676,678],{"class":677},"s-wDw","# [4 1 7 0]\n",[10,680,681,682,684,685,688,689,692,693,696],{},"Converting to ",[14,683,524],{}," drops the time of day, which is also what Excel does when a date cell carries a time. The ",[14,686,687],{},"valid"," mask matters because ",[14,690,691],{},"busday_count","\ncannot handle ",[14,694,695],{},"NaT",": ticket T-105 has no opening date and must be kept out of the call.",[202,698,700],{"id":699},"step-2-match-networkdays-by-counting-both-ends","Step 2 — Match NETWORKDAYS by counting both ends",[10,702,703,704,707,708,711,712,714],{},"Excel's ",[14,705,706],{},"NETWORKDAYS(start, end)"," counts ",[514,709,710],{},"both"," the start and end dates when they are working days: Monday to Friday of the same week is 5, not 4. To match it, move the\nend one day later before calling ",[14,713,691],{},".",[207,716,718],{"className":242,"code":717,"language":244,"meta":212,"style":212},"def networkdays(start: pd.Series, end: pd.Series, holidays=None) -> pd.Series:\n    result = pd.Series(pd.NA, index=start.index, dtype=\"Int64\")\n    ok = start.notna() & end.notna()\n    counts = np.busday_count(\n        start[ok].to_numpy(dtype=\"datetime64[D]\"),\n        (end[ok] + pd.Timedelta(days=1)).to_numpy(dtype=\"datetime64[D]\"),\n        holidays=[] if holidays is None else holidays,\n    )\n    result[ok] = counts\n    return result\n\nframe[\"WorkingDays\"] = networkdays(frame[\"Opened\"], frame[\"Closed\"])\nprint(frame[[\"Ticket\", \"Opened\", \"Closed\", \"WorkingDays\"]])\n",[14,719,720,739,770,786,796,810,839,867,872,882,890,894,919],{"__ignoreMap":212},[216,721,722,725,729,732,734,736],{"class":218,"line":219},[216,723,724],{"class":251},"def",[216,726,728],{"class":727},"s_Opv"," networkdays",[216,730,731],{"class":255},"(start: pd.Series, end: pd.Series, holidays",[216,733,278],{"class":251},[216,735,350],{"class":349},[216,737,738],{"class":255},") -> pd.Series:\n",[216,740,741,744,746,749,752,754,756,758,761,763,765,768],{"class":218,"line":265},[216,742,743],{"class":255},"    result ",[216,745,278],{"class":251},[216,747,748],{"class":255}," pd.Series(pd.",[216,750,751],{"class":349},"NA",[216,753,296],{"class":255},[216,755,467],{"class":456},[216,757,278],{"class":251},[216,759,760],{"class":255},"start.index, ",[216,762,638],{"class":456},[216,764,278],{"class":251},[216,766,767],{"class":226},"\"Int64\"",[216,769,475],{"class":255},[216,771,772,775,777,780,783],{"class":218,"line":272},[216,773,774],{"class":255},"    ok ",[216,776,278],{"class":251},[216,778,779],{"class":255}," start.notna() ",[216,781,782],{"class":251},"&",[216,784,785],{"class":255}," end.notna()\n",[216,787,788,791,793],{"class":218,"line":284},[216,789,790],{"class":255},"    counts ",[216,792,278],{"class":251},[216,794,795],{"class":255}," np.busday_count(\n",[216,797,798,801,803,805,807],{"class":218,"line":320},[216,799,800],{"class":255},"        start[ok].to_numpy(",[216,802,638],{"class":456},[216,804,278],{"class":251},[216,806,643],{"class":226},[216,808,809],{"class":255},"),\n",[216,811,812,815,818,821,824,826,828,831,833,835,837],{"class":218,"line":356},[216,813,814],{"class":255},"        (end[ok] ",[216,816,817],{"class":251},"+",[216,819,820],{"class":255}," pd.Timedelta(",[216,822,823],{"class":456},"days",[216,825,278],{"class":251},[216,827,614],{"class":349},[216,829,830],{"class":255},")).to_numpy(",[216,832,638],{"class":456},[216,834,278],{"class":251},[216,836,643],{"class":226},[216,838,809],{"class":255},[216,840,841,844,846,849,852,855,858,861,864],{"class":218,"line":387},[216,842,843],{"class":456},"        holidays",[216,845,278],{"class":251},[216,847,848],{"class":255},"[] ",[216,850,851],{"class":251},"if",[216,853,854],{"class":255}," holidays ",[216,856,857],{"class":251},"is",[216,859,860],{"class":349}," None",[216,862,863],{"class":251}," else",[216,865,866],{"class":255}," holidays,\n",[216,868,869],{"class":218,"line":393},[216,870,871],{"class":255},"    )\n",[216,873,874,877,879],{"class":218,"line":425},[216,875,876],{"class":255},"    result[ok] ",[216,878,278],{"class":251},[216,880,881],{"class":255}," counts\n",[216,883,884,887],{"class":218,"line":430},[216,885,886],{"class":251},"    return",[216,888,889],{"class":255}," result\n",[216,891,892],{"class":218,"line":450},[216,893,269],{"emptyLinePlaceholder":268},[216,895,896,899,902,905,907,910,912,915,917],{"class":218,"line":478},[216,897,898],{"class":255},"frame[",[216,900,901],{"class":226},"\"WorkingDays\"",[216,903,904],{"class":255},"] ",[216,906,278],{"class":251},[216,908,909],{"class":255}," networkdays(frame[",[216,911,579],{"class":226},[216,913,914],{"class":255},"], frame[",[216,916,584],{"class":226},[216,918,587],{"class":255},[216,920,922,924,927,930,932,934,936,938,940,942],{"class":218,"line":921},13,[216,923,671],{"class":349},[216,925,926],{"class":255},"(frame[[",[216,928,929],{"class":226},"\"Ticket\"",[216,931,296],{"class":255},[216,933,579],{"class":226},[216,935,296],{"class":255},[216,937,584],{"class":226},[216,939,296],{"class":255},[216,941,901],{"class":226},[216,943,944],{"class":255},"]])\n",[10,946,947,948,951],{},"Ticket T-101 now reads 5, as it would in the spreadsheet. T-104 opened and closed on a Saturday and correctly reads 0. T-105 stays ",[14,949,950],{},"\u003CNA>"," instead of raising, which lets the\nreport show it as \"not measurable\" rather than crashing the job.",[29,953,38,959,38,962,38,965,38,969,38,977,38,983,38,986,38,991,38,995,38,999,38,1004,38,1009,38,1013,38,1016,38,1019,38,1022,38,1025,38,1030,38,1034,38,1037,38,1039,38,1042],{"viewBox":954,"role":32,"ariaLabelledBy":955,"xmlns":36,"style":958},"0 0 760 191",[956,957],"bd-ends-t","bd-ends-d","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,960,961],{"id":956},"Why busday_count and NETWORKDAYS differ by one",[44,963,964],{"id":957},"numpy busday_count excludes the end date, so Monday to Friday gives four. Excel NETWORKDAYS includes both ends, giving five. Adding one day to the end before counting makes them agree.",[48,966],{"x":50,"y":50,"width":967,"height":968,"fill":53},"760","191",[48,970],{"x":56,"y":56,"width":971,"height":972,"rx":973,"fill":974,"stroke":975,"style":976},"226.7","127","14","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[67,978,982],{"x":979,"y":980,"style":981},"133.3","46","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","np.busday_count",[218,984],{"x1":57,"y1":64,"x2":985,"y2":64,"stroke":975,"style":61},"230.7",[67,987,990],{"x":979,"y":988,"style":989},"78","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","[begin, end) half-open",[67,992,994],{"x":979,"y":993,"style":989},"101","Mon → Fri = 4",[67,996,998],{"x":979,"y":997,"style":989},"124","end date not counted",[48,1000],{"x":1001,"y":56,"width":971,"height":972,"rx":973,"fill":1002,"stroke":1003,"style":976},"266.7","#f0f4ff","var(--brand,#5b5cf0)",[67,1005,1008],{"x":1006,"y":980,"style":1007},"380","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Excel NETWORKDAYS",[218,1010],{"x1":1011,"y1":64,"x2":1012,"y2":64,"stroke":1003,"style":61},"282.7","477.3",[67,1014,1015],{"x":1006,"y":988,"style":989},"[start, end] inclusive",[67,1017,1018],{"x":1006,"y":993,"style":989},"Mon → Fri = 5",[67,1020,1021],{"x":1006,"y":997,"style":989},"both dates counted",[48,1023],{"x":1024,"y":56,"width":971,"height":972,"rx":973,"fill":110,"stroke":111,"style":976},"513.3",[67,1026,1029],{"x":1027,"y":980,"style":1028},"626.7","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","end + 1 day",[218,1031],{"x1":1032,"y1":64,"x2":1033,"y2":64,"stroke":111,"style":61},"529.3","724",[67,1035,1036],{"x":1027,"y":988,"style":989},"busday_count(start, end+1)",[67,1038,1018],{"x":1027,"y":993,"style":989},[67,1040,1041],{"x":1027,"y":997,"style":989},"matches Excel",[67,1043,1045],{"x":1006,"y":1044,"style":199},"177","check the convention before comparing with an existing spreadsheet",[202,1047,1049],{"id":1048},"step-3-exclude-holidays-from-a-sheet","Step 3 — Exclude holidays from a sheet",[10,1051,1052,1053,1056,1057,1059],{},"Both functions take a ",[14,1054,1055],{},"holidays"," array. Read it from the workbook's holiday sheet so the finance team can maintain it without touching code, and convert it to\n",[14,1058,524],{}," once.",[207,1061,1063],{"className":242,"code":1062,"language":244,"meta":212,"style":212},"holiday_days = (\n    pd.read_excel(\"service_desk.xlsx\", sheet_name=\"Holidays\")[\"Holiday\"]\n      .dropna()\n      .to_numpy(dtype=\"datetime64[D]\")\n)\n\nframe[\"WorkingDays\"] = networkdays(frame[\"Opened\"], frame[\"Closed\"], holidays=holiday_days)\nprint(frame[[\"Ticket\", \"WorkingDays\"]])\n# T-103, open over Christmas and New Year, counts 5 instead of 8\n",[14,1064,1065,1075,1098,1103,1116,1120,1124,1152,1166],{"__ignoreMap":212},[216,1066,1067,1070,1072],{"class":218,"line":219},[216,1068,1069],{"class":255},"holiday_days ",[216,1071,278],{"class":251},[216,1073,1074],{"class":255}," (\n",[216,1076,1077,1080,1082,1084,1086,1088,1090,1093,1095],{"class":218,"line":265},[216,1078,1079],{"class":255},"    pd.read_excel(",[216,1081,439],{"class":226},[216,1083,296],{"class":255},[216,1085,457],{"class":456},[216,1087,278],{"class":251},[216,1089,488],{"class":226},[216,1091,1092],{"class":255},")[",[216,1094,404],{"class":226},[216,1096,1097],{"class":255},"]\n",[216,1099,1100],{"class":218,"line":272},[216,1101,1102],{"class":255},"      .dropna()\n",[216,1104,1105,1108,1110,1112,1114],{"class":218,"line":284},[216,1106,1107],{"class":255},"      .to_numpy(",[216,1109,638],{"class":456},[216,1111,278],{"class":251},[216,1113,643],{"class":226},[216,1115,475],{"class":255},[216,1117,1118],{"class":218,"line":320},[216,1119,475],{"class":255},[216,1121,1122],{"class":218,"line":356},[216,1123,269],{"emptyLinePlaceholder":268},[216,1125,1126,1128,1130,1132,1134,1136,1138,1140,1142,1145,1147,1149],{"class":218,"line":387},[216,1127,898],{"class":255},[216,1129,901],{"class":226},[216,1131,904],{"class":255},[216,1133,278],{"class":251},[216,1135,909],{"class":255},[216,1137,579],{"class":226},[216,1139,914],{"class":255},[216,1141,584],{"class":226},[216,1143,1144],{"class":255},"], ",[216,1146,1055],{"class":456},[216,1148,278],{"class":251},[216,1150,1151],{"class":255},"holiday_days)\n",[216,1153,1154,1156,1158,1160,1162,1164],{"class":218,"line":393},[216,1155,671],{"class":349},[216,1157,926],{"class":255},[216,1159,929],{"class":226},[216,1161,296],{"class":255},[216,1163,901],{"class":226},[216,1165,944],{"class":255},[216,1167,1168],{"class":218,"line":425},[216,1169,1170],{"class":677},"# T-103, open over Christmas and New Year, counts 5 instead of 8\n",[10,1172,1173,1174,1176],{},"Holidays that fall on a weekend are ignored automatically, so there is no need to filter the list. For a report spanning many years and countries, a maintained source\nis less error-prone than a typed list; the ",[14,1175,1055],{}," Python package provides public holidays by country and year, and its dates convert the same way.",[202,1178,1180],{"id":1179},"step-4-use-a-custom-weekend-like-networkdaysintl","Step 4 — Use a custom weekend like NETWORKDAYS.INTL",[10,1182,1183,1184,1187,1188,1191],{},"Not every organisation works Monday to Friday. ",[14,1185,1186],{},"NETWORKDAYS.INTL"," takes a weekend code; NumPy takes a ",[14,1189,1190],{},"weekmask",", either seven 0\u002F1 characters starting on Monday or\na list of day names.",[207,1193,1195],{"className":242,"code":1194,"language":244,"meta":212,"style":212},"sunday_to_thursday = \"Sun Mon Tue Wed Thu\"\nsix_day_week = \"1111110\"                    # Monday to Saturday\n\nstart = np.datetime64(\"2026-09-06\")         # a Sunday\nend = np.datetime64(\"2026-09-12\")           # the following Saturday\none_day = np.timedelta64(1, \"D\")\nprint(np.busday_count(start, end + one_day, weekmask=sunday_to_thursday))   # 5\nprint(np.busday_count(start, end + one_day, weekmask=six_day_week))         # 6\n",[14,1196,1197,1207,1220,1224,1243,1259,1278,1300],{"__ignoreMap":212},[216,1198,1199,1202,1204],{"class":218,"line":219},[216,1200,1201],{"class":255},"sunday_to_thursday ",[216,1203,278],{"class":251},[216,1205,1206],{"class":226}," \"Sun Mon Tue Wed Thu\"\n",[216,1208,1209,1212,1214,1217],{"class":218,"line":265},[216,1210,1211],{"class":255},"six_day_week ",[216,1213,278],{"class":251},[216,1215,1216],{"class":226}," \"1111110\"",[216,1218,1219],{"class":677},"                    # Monday to Saturday\n",[216,1221,1222],{"class":218,"line":272},[216,1223,269],{"emptyLinePlaceholder":268},[216,1225,1226,1229,1231,1234,1237,1240],{"class":218,"line":284},[216,1227,1228],{"class":255},"start ",[216,1230,278],{"class":251},[216,1232,1233],{"class":255}," np.datetime64(",[216,1235,1236],{"class":226},"\"2026-09-06\"",[216,1238,1239],{"class":255},")         ",[216,1241,1242],{"class":677},"# a Sunday\n",[216,1244,1245,1247,1249,1251,1253,1256],{"class":218,"line":320},[216,1246,650],{"class":255},[216,1248,278],{"class":251},[216,1250,1233],{"class":255},[216,1252,344],{"class":226},[216,1254,1255],{"class":255},")           ",[216,1257,1258],{"class":677},"# the following Saturday\n",[216,1260,1261,1264,1266,1269,1271,1273,1276],{"class":218,"line":356},[216,1262,1263],{"class":255},"one_day ",[216,1265,278],{"class":251},[216,1267,1268],{"class":255}," np.timedelta64(",[216,1270,614],{"class":349},[216,1272,296],{"class":255},[216,1274,1275],{"class":226},"\"D\"",[216,1277,475],{"class":255},[216,1279,1280,1282,1285,1287,1290,1292,1294,1297],{"class":218,"line":387},[216,1281,671],{"class":349},[216,1283,1284],{"class":255},"(np.busday_count(start, end ",[216,1286,817],{"class":251},[216,1288,1289],{"class":255}," one_day, ",[216,1291,1190],{"class":456},[216,1293,278],{"class":251},[216,1295,1296],{"class":255},"sunday_to_thursday))   ",[216,1298,1299],{"class":677},"# 5\n",[216,1301,1302,1304,1306,1308,1310,1312,1314,1317],{"class":218,"line":393},[216,1303,671],{"class":349},[216,1305,1284],{"class":255},[216,1307,817],{"class":251},[216,1309,1289],{"class":255},[216,1311,1190],{"class":456},[216,1313,278],{"class":251},[216,1315,1316],{"class":255},"six_day_week))         ",[216,1318,1319],{"class":677},"# 6\n",[10,1321,1322],{},"A different weekmask per row — offices in several countries in one report — is easiest handled by grouping on the country column and calling the function once per group\nwith that country's mask and holiday list.",[202,1324,1326],{"id":1325},"step-5-add-working-days-to-a-date-like-workday","Step 5 — Add working days to a date like WORKDAY",[10,1328,1329,1330,1333,1334,1337,1338,1341,1342,1344,1345,1348],{},"Deadlines go the other way: a request received on a date is due a number of working days later. ",[14,1331,1332],{},"np.busday_offset(date, n)"," moves by ",[14,1335,1336],{},"n"," business days. The ",[14,1339,1340],{},"roll"," argument\ndecides what happens when the start date itself is not a working day, and matching Excel's ",[14,1343,20],{}," needs ",[14,1346,1347],{},"roll=\"backward\""," for positive offsets.",[207,1350,1352],{"className":242,"code":1351,"language":244,"meta":212,"style":212},"def workday(start: pd.Series, days: int, holidays=None) -> pd.Series:\n    result = pd.Series(pd.NaT, index=start.index, dtype=\"datetime64[ns]\")\n    ok = start.notna()\n    shifted = np.busday_offset(\n        start[ok].to_numpy(dtype=\"datetime64[D]\"), days,\n        roll=\"backward\" if days >= 0 else \"forward\",\n        holidays=[] if holidays is None else holidays,\n    )\n    result[ok] = pd.to_datetime(shifted)\n    return result\n\nframe[\"DueBy\"] = workday(frame[\"Opened\"], 3, holidays=holiday_days)\nprint(frame[[\"Ticket\", \"Opened\", \"DueBy\"]])\n",[14,1353,1354,1376,1400,1409,1419,1432,1462,1482,1486,1495,1501,1505,1534],{"__ignoreMap":212},[216,1355,1356,1358,1361,1364,1367,1370,1372,1374],{"class":218,"line":219},[216,1357,724],{"class":251},[216,1359,1360],{"class":727}," workday",[216,1362,1363],{"class":255},"(start: pd.Series, days: ",[216,1365,1366],{"class":349},"int",[216,1368,1369],{"class":255},", holidays",[216,1371,278],{"class":251},[216,1373,350],{"class":349},[216,1375,738],{"class":255},[216,1377,1378,1380,1382,1385,1387,1389,1391,1393,1395,1398],{"class":218,"line":265},[216,1379,743],{"class":255},[216,1381,278],{"class":251},[216,1383,1384],{"class":255}," pd.Series(pd.NaT, ",[216,1386,467],{"class":456},[216,1388,278],{"class":251},[216,1390,760],{"class":255},[216,1392,638],{"class":456},[216,1394,278],{"class":251},[216,1396,1397],{"class":226},"\"datetime64[ns]\"",[216,1399,475],{"class":255},[216,1401,1402,1404,1406],{"class":218,"line":272},[216,1403,774],{"class":255},[216,1405,278],{"class":251},[216,1407,1408],{"class":255}," start.notna()\n",[216,1410,1411,1414,1416],{"class":218,"line":284},[216,1412,1413],{"class":255},"    shifted ",[216,1415,278],{"class":251},[216,1417,1418],{"class":255}," np.busday_offset(\n",[216,1420,1421,1423,1425,1427,1429],{"class":218,"line":320},[216,1422,800],{"class":255},[216,1424,638],{"class":456},[216,1426,278],{"class":251},[216,1428,643],{"class":226},[216,1430,1431],{"class":255},"), days,\n",[216,1433,1434,1437,1439,1442,1445,1448,1451,1454,1456,1459],{"class":218,"line":356},[216,1435,1436],{"class":456},"        roll",[216,1438,278],{"class":251},[216,1440,1441],{"class":226},"\"backward\"",[216,1443,1444],{"class":251}," if",[216,1446,1447],{"class":255}," days ",[216,1449,1450],{"class":251},">=",[216,1452,1453],{"class":349}," 0",[216,1455,863],{"class":251},[216,1457,1458],{"class":226}," \"forward\"",[216,1460,1461],{"class":255},",\n",[216,1463,1464,1466,1468,1470,1472,1474,1476,1478,1480],{"class":218,"line":387},[216,1465,843],{"class":456},[216,1467,278],{"class":251},[216,1469,848],{"class":255},[216,1471,851],{"class":251},[216,1473,854],{"class":255},[216,1475,857],{"class":251},[216,1477,860],{"class":349},[216,1479,863],{"class":251},[216,1481,866],{"class":255},[216,1483,1484],{"class":218,"line":393},[216,1485,871],{"class":255},[216,1487,1488,1490,1492],{"class":218,"line":425},[216,1489,876],{"class":255},[216,1491,278],{"class":251},[216,1493,1494],{"class":255}," pd.to_datetime(shifted)\n",[216,1496,1497,1499],{"class":218,"line":430},[216,1498,886],{"class":251},[216,1500,889],{"class":255},[216,1502,1503],{"class":218,"line":450},[216,1504,269],{"emptyLinePlaceholder":268},[216,1506,1507,1509,1512,1514,1516,1519,1521,1523,1526,1528,1530,1532],{"class":218,"line":478},[216,1508,898],{"class":255},[216,1510,1511],{"class":226},"\"DueBy\"",[216,1513,904],{"class":255},[216,1515,278],{"class":251},[216,1517,1518],{"class":255}," workday(frame[",[216,1520,579],{"class":226},[216,1522,1144],{"class":255},[216,1524,1525],{"class":349},"3",[216,1527,296],{"class":255},[216,1529,1055],{"class":456},[216,1531,278],{"class":251},[216,1533,1151],{"class":255},[216,1535,1536,1538,1540,1542,1544,1546,1548,1550],{"class":218,"line":921},[216,1537,671],{"class":349},[216,1539,926],{"class":255},[216,1541,929],{"class":226},[216,1543,296],{"class":255},[216,1545,579],{"class":226},[216,1547,296],{"class":255},[216,1549,1511],{"class":226},[216,1551,944],{"class":255},[10,1553,1554,1555,1557,1558,1561,1562,1565],{},"For T-104, opened on Saturday 12 September, the due date is Wednesday 16 September — three working days counted from the weekend, the same answer ",[14,1556,20],{}," gives. With the\ndefault ",[14,1559,1560],{},"roll=\"raise\""," NumPy would refuse the Saturday date entirely, and with ",[14,1563,1564],{},"roll=\"forward\""," it would first move to Monday and then add three days, landing a day late.",[29,1567,38,1573,38,1576,38,1579,38,1583,38,1587,38,1592,38,1597,38,1603,38,1610,38,1615,38,1618,38,1621,38,1623,38,1627,38,1631,38,1634,38,1637,38,1640,38,1644,38,1648],{"viewBox":1568,"role":32,"ariaLabelledBy":1569,"xmlns":36,"style":1572},"0 0 720 212",[1570,1571],"bd-roll-t","bd-roll-d","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,1574,1575],{"id":1570},"Due date for a ticket opened on a Saturday plus three working days",[44,1577,1578],{"id":1571},"With roll backward the Saturday is treated as the preceding Friday and three working days later is Wednesday, matching Excel WORKDAY. With roll forward it is treated as Monday and the result is Thursday, one day late. The default raise refuses the date.",[48,1580],{"x":50,"y":50,"width":1581,"height":1582,"fill":53},"720","212",[67,1584,1586],{"x":56,"y":58,"style":1585},"font-size:12px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","busday_offset(\"2026-09-12\", 3) by roll setting",[48,1588],{"x":56,"y":980,"width":1589,"height":1590,"rx":1591,"fill":53,"stroke":60,"style":61},"170","28","6",[67,1593,1347],{"x":1594,"y":1595,"style":1596},"105","65","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:middle",[48,1598],{"x":1599,"y":1600,"width":1601,"height":1602,"rx":1591,"fill":59,"stroke":60,"style":61},"204","47","330","26",[48,1604],{"x":1605,"y":1606,"width":1607,"height":1608,"rx":1609,"fill":110,"stroke":111,"style":61},"205","48","179.5","24","5",[67,1611,1614],{"x":1612,"y":1595,"style":1613},"546","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","Wed 16 Sep, as WORKDAY",[48,1616],{"x":56,"y":1617,"width":1589,"height":1590,"rx":1591,"fill":53,"stroke":60,"style":61},"90",[67,1619,1564],{"x":1594,"y":1620,"style":1596},"109",[48,1622],{"x":1599,"y":69,"width":1601,"height":1602,"rx":1591,"fill":59,"stroke":60,"style":61},[48,1624],{"x":1605,"y":1625,"width":1626,"height":1608,"rx":1609,"fill":118,"stroke":119,"style":61},"92","229",[67,1628,1630],{"x":1612,"y":1620,"style":1629},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","Thu 17 Sep, a day late",[48,1632],{"x":56,"y":1633,"width":1589,"height":1590,"rx":1591,"fill":53,"stroke":60,"style":61},"134",[67,1635,1560],{"x":1594,"y":1636,"style":1596},"153",[48,1638],{"x":1599,"y":1639,"width":1601,"height":1602,"rx":1591,"fill":59,"stroke":60,"style":61},"135",[48,1641],{"x":1605,"y":1642,"width":1643,"height":1608,"rx":1609,"fill":59,"stroke":60,"style":61},"136","31",[67,1645,1647],{"x":1612,"y":1636,"style":1646},"font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:start","ValueError",[67,1649,1651],{"x":1650,"y":52,"style":199},"360","weekend start dates are where translations go wrong",[202,1653,1655],{"id":1654},"step-6-flag-sla-breaches-and-write-the-result","Step 6 — Flag SLA breaches and write the result",[10,1657,1658],{},"With counts and due dates as columns, the service report is a comparison and a write. Keep the working-day count as a number and let Excel format the dates.",[207,1660,1662],{"className":242,"code":1661,"language":244,"meta":212,"style":212},"SLA_DAYS = 3\nframe[\"Breached\"] = frame[\"WorkingDays\"].map(\n    lambda days: pd.NA if pd.isna(days) else (\"Yes\" if days > SLA_DAYS else \"No\")\n)\n\nwith pd.ExcelWriter(\"sla_report.xlsx\", engine=\"openpyxl\", datetime_format=\"yyyy-mm-dd\") as writer:\n    frame.to_excel(writer, sheet_name=\"SLA\", index=False)\n\nprint(pd.read_excel(\"sla_report.xlsx\")[[\"Ticket\", \"WorkingDays\", \"Breached\"]])\n",[14,1663,1664,1675,1694,1735,1739,1743,1778,1800,1804],{"__ignoreMap":212},[216,1665,1666,1669,1672],{"class":218,"line":219},[216,1667,1668],{"class":349},"SLA_DAYS",[216,1670,1671],{"class":251}," =",[216,1673,1674],{"class":349}," 3\n",[216,1676,1677,1679,1682,1684,1686,1689,1691],{"class":218,"line":265},[216,1678,898],{"class":255},[216,1680,1681],{"class":226},"\"Breached\"",[216,1683,904],{"class":255},[216,1685,278],{"class":251},[216,1687,1688],{"class":255}," frame[",[216,1690,901],{"class":226},[216,1692,1693],{"class":255},"].map(\n",[216,1695,1696,1699,1702,1704,1706,1709,1712,1715,1718,1720,1722,1725,1728,1730,1733],{"class":218,"line":272},[216,1697,1698],{"class":251},"    lambda",[216,1700,1701],{"class":255}," days: pd.",[216,1703,751],{"class":349},[216,1705,1444],{"class":251},[216,1707,1708],{"class":255}," pd.isna(days) ",[216,1710,1711],{"class":251},"else",[216,1713,1714],{"class":255}," (",[216,1716,1717],{"class":226},"\"Yes\"",[216,1719,1444],{"class":251},[216,1721,1447],{"class":255},[216,1723,1724],{"class":251},">",[216,1726,1727],{"class":349}," SLA_DAYS",[216,1729,863],{"class":251},[216,1731,1732],{"class":226}," \"No\"",[216,1734,475],{"class":255},[216,1736,1737],{"class":218,"line":284},[216,1738,475],{"class":255},[216,1740,1741],{"class":218,"line":320},[216,1742,269],{"emptyLinePlaceholder":268},[216,1744,1745,1747,1749,1752,1754,1757,1759,1762,1764,1767,1769,1772,1774,1776],{"class":218,"line":356},[216,1746,433],{"class":251},[216,1748,436],{"class":255},[216,1750,1751],{"class":226},"\"sla_report.xlsx\"",[216,1753,296],{"class":255},[216,1755,1756],{"class":456},"engine",[216,1758,278],{"class":251},[216,1760,1761],{"class":226},"\"openpyxl\"",[216,1763,296],{"class":255},[216,1765,1766],{"class":456},"datetime_format",[216,1768,278],{"class":251},[216,1770,1771],{"class":226},"\"yyyy-mm-dd\"",[216,1773,442],{"class":255},[216,1775,259],{"class":251},[216,1777,447],{"class":255},[216,1779,1780,1783,1785,1787,1790,1792,1794,1796,1798],{"class":218,"line":387},[216,1781,1782],{"class":255},"    frame.to_excel(writer, ",[216,1784,457],{"class":456},[216,1786,278],{"class":251},[216,1788,1789],{"class":226},"\"SLA\"",[216,1791,296],{"class":255},[216,1793,467],{"class":456},[216,1795,278],{"class":251},[216,1797,472],{"class":349},[216,1799,475],{"class":255},[216,1801,1802],{"class":218,"line":393},[216,1803,269],{"emptyLinePlaceholder":268},[216,1805,1806,1808,1811,1813,1816,1818,1820,1822,1824,1826],{"class":218,"line":425},[216,1807,671],{"class":349},[216,1809,1810],{"class":255},"(pd.read_excel(",[216,1812,1751],{"class":226},[216,1814,1815],{"class":255},")[[",[216,1817,929],{"class":226},[216,1819,296],{"class":255},[216,1821,901],{"class":226},[216,1823,296],{"class":255},[216,1825,1681],{"class":226},[216,1827,944],{"class":255},[10,1829,1830,1831,1834,1835,714],{},"Leaving ",[14,1832,1833],{},"Breached"," as missing for tickets without both dates keeps them out of breach counts and makes the data gap visible in the output. Formatting the date columns further is\ncovered in ",[23,1836,1838],{"href":1837},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fapplying-number-and-date-formats-in-excel\u002Fformat-dates-in-excel-cells-with-python\u002F","Format Dates in Excel Cells with Python",[202,1840,1842],{"id":1841},"step-7-build-one-business-calendar-and-reuse-it","Step 7 — Build one business calendar and reuse it",[10,1844,1845,1846,1849,1850,1853],{},"Real reports rarely need a single count. They need working days open, a due date, working days overdue, and perhaps working days per month for capacity figures — all\nunder the same weekend and holiday rules. A ",[14,1847,1848],{},"np.busdaycalendar"," bundles those rules into one object, validated once, that every business-day function accepts through\n",[14,1851,1852],{},"busdaycal=",". Changing the rules then means changing one line.",[207,1855,1857],{"className":242,"code":1856,"language":244,"meta":212,"style":212},"calendar = np.busdaycalendar(weekmask=\"Mon Tue Wed Thu Fri\", holidays=holiday_days)\n\nopened = frame.loc[valid, \"Opened\"].to_numpy(dtype=\"datetime64[D]\")\nclosed = frame.loc[valid, \"Closed\"].to_numpy(dtype=\"datetime64[D]\")\ndue = np.busday_offset(opened, SLA_DAYS, roll=\"backward\", busdaycal=calendar)\n\noverdue = np.maximum(np.busday_count(due, closed, busdaycal=calendar), 0)\nprint(pd.DataFrame({\"Ticket\": frame.loc[valid, \"Ticket\"], \"Due\": due, \"DaysOverdue\": overdue}))\n\nmonths = pd.period_range(\"2026-09\", \"2027-01\", freq=\"M\")\nper_month = np.busday_count(months.start_time.to_numpy(\"datetime64[D]\"),\n                            (months.end_time + pd.Timedelta(days=1)).to_numpy(\"datetime64[D]\"),\n                            busdaycal=calendar)\nprint(dict(zip(months.astype(str), per_month)))\n",[14,1858,1859,1884,1888,1909,1930,1960,1964,1985,2013,2017,2047,2061,2082,2091],{"__ignoreMap":212},[216,1860,1861,1864,1866,1869,1871,1873,1876,1878,1880,1882],{"class":218,"line":219},[216,1862,1863],{"class":255},"calendar ",[216,1865,278],{"class":251},[216,1867,1868],{"class":255}," np.busdaycalendar(",[216,1870,1190],{"class":456},[216,1872,278],{"class":251},[216,1874,1875],{"class":226},"\"Mon Tue Wed Thu Fri\"",[216,1877,296],{"class":255},[216,1879,1055],{"class":456},[216,1881,278],{"class":251},[216,1883,1151],{"class":255},[216,1885,1886],{"class":218,"line":265},[216,1887,269],{"emptyLinePlaceholder":268},[216,1889,1890,1893,1895,1897,1899,1901,1903,1905,1907],{"class":218,"line":272},[216,1891,1892],{"class":255},"opened ",[216,1894,278],{"class":251},[216,1896,630],{"class":255},[216,1898,579],{"class":226},[216,1900,635],{"class":255},[216,1902,638],{"class":456},[216,1904,278],{"class":251},[216,1906,643],{"class":226},[216,1908,475],{"class":255},[216,1910,1911,1914,1916,1918,1920,1922,1924,1926,1928],{"class":218,"line":284},[216,1912,1913],{"class":255},"closed ",[216,1915,278],{"class":251},[216,1917,630],{"class":255},[216,1919,584],{"class":226},[216,1921,635],{"class":255},[216,1923,638],{"class":456},[216,1925,278],{"class":251},[216,1927,643],{"class":226},[216,1929,475],{"class":255},[216,1931,1932,1935,1937,1940,1942,1944,1946,1948,1950,1952,1955,1957],{"class":218,"line":320},[216,1933,1934],{"class":255},"due ",[216,1936,278],{"class":251},[216,1938,1939],{"class":255}," np.busday_offset(opened, ",[216,1941,1668],{"class":349},[216,1943,296],{"class":255},[216,1945,1340],{"class":456},[216,1947,278],{"class":251},[216,1949,1441],{"class":226},[216,1951,296],{"class":255},[216,1953,1954],{"class":456},"busdaycal",[216,1956,278],{"class":251},[216,1958,1959],{"class":255},"calendar)\n",[216,1961,1962],{"class":218,"line":356},[216,1963,269],{"emptyLinePlaceholder":268},[216,1965,1966,1969,1971,1974,1976,1978,1981,1983],{"class":218,"line":387},[216,1967,1968],{"class":255},"overdue ",[216,1970,278],{"class":251},[216,1972,1973],{"class":255}," np.maximum(np.busday_count(due, closed, ",[216,1975,1954],{"class":456},[216,1977,278],{"class":251},[216,1979,1980],{"class":255},"calendar), ",[216,1982,50],{"class":349},[216,1984,475],{"class":255},[216,1986,1987,1989,1992,1994,1997,1999,2001,2004,2007,2010],{"class":218,"line":393},[216,1988,671],{"class":349},[216,1990,1991],{"class":255},"(pd.DataFrame({",[216,1993,929],{"class":226},[216,1995,1996],{"class":255},": frame.loc[valid, ",[216,1998,929],{"class":226},[216,2000,1144],{"class":255},[216,2002,2003],{"class":226},"\"Due\"",[216,2005,2006],{"class":255},": due, ",[216,2008,2009],{"class":226},"\"DaysOverdue\"",[216,2011,2012],{"class":255},": overdue}))\n",[216,2014,2015],{"class":218,"line":425},[216,2016,269],{"emptyLinePlaceholder":268},[216,2018,2019,2022,2024,2027,2030,2032,2035,2037,2040,2042,2045],{"class":218,"line":430},[216,2020,2021],{"class":255},"months ",[216,2023,278],{"class":251},[216,2025,2026],{"class":255}," pd.period_range(",[216,2028,2029],{"class":226},"\"2026-09\"",[216,2031,296],{"class":255},[216,2033,2034],{"class":226},"\"2027-01\"",[216,2036,296],{"class":255},[216,2038,2039],{"class":456},"freq",[216,2041,278],{"class":251},[216,2043,2044],{"class":226},"\"M\"",[216,2046,475],{"class":255},[216,2048,2049,2052,2054,2057,2059],{"class":218,"line":450},[216,2050,2051],{"class":255},"per_month ",[216,2053,278],{"class":251},[216,2055,2056],{"class":255}," np.busday_count(months.start_time.to_numpy(",[216,2058,643],{"class":226},[216,2060,809],{"class":255},[216,2062,2063,2066,2068,2070,2072,2074,2076,2078,2080],{"class":218,"line":478},[216,2064,2065],{"class":255},"                            (months.end_time ",[216,2067,817],{"class":251},[216,2069,820],{"class":255},[216,2071,823],{"class":456},[216,2073,278],{"class":251},[216,2075,614],{"class":349},[216,2077,830],{"class":255},[216,2079,643],{"class":226},[216,2081,809],{"class":255},[216,2083,2084,2087,2089],{"class":218,"line":921},[216,2085,2086],{"class":456},"                            busdaycal",[216,2088,278],{"class":251},[216,2090,1959],{"class":255},[216,2092,2094,2096,2099,2102,2104,2107,2110,2113],{"class":218,"line":2093},14,[216,2095,671],{"class":349},[216,2097,2098],{"class":255},"(",[216,2100,2101],{"class":349},"dict",[216,2103,2098],{"class":255},[216,2105,2106],{"class":349},"zip",[216,2108,2109],{"class":255},"(months.astype(",[216,2111,2112],{"class":349},"str",[216,2114,2115],{"class":255},"), per_month)))\n",[10,2117,2118,2119,714],{},"The overdue count uses the half-open convention on purpose: a ticket closed on its due date is zero days overdue, and one closed the next working day is one. The working\ndays per month are a useful denominator for productivity figures, since a December with two holidays should not be compared raw with a September that has none. Per-month\ngrouping of the tickets themselves is covered in\n",[23,2120,2122],{"href":2121},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas\u002F","Group Excel Rows by Month and Quarter with pandas",[202,2124,2126],{"id":2125},"common-pitfalls","Common pitfalls",[2128,2129,2130,2146],"table",{},[2131,2132,2133],"thead",{},[2134,2135,2136,2140,2143],"tr",{},[2137,2138,2139],"th",{},"Symptom",[2137,2141,2142],{},"Cause",[2137,2144,2145],{},"Fix",[2147,2148,2149,2163,2181,2203,2216,2227],"tbody",{},[2134,2150,2151,2155,2160],{},[2152,2153,2154],"td",{},"Every count one lower than Excel",[2152,2156,2157,2159],{},[14,2158,691],{}," excludes the end date",[2152,2161,2162],{},"Add one day to the end",[2134,2164,2165,2170,2175],{},[2152,2166,2167],{},[14,2168,2169],{},"ValueError: Could not convert object to NumPy datetime",[2152,2171,2172,2174],{},[14,2173,695],{}," or text dates in the column",[2152,2176,2177,2178],{},"Mask missing values; parse with ",[14,2179,2180],{},"pd.to_datetime",[2134,2182,2183,2188,2193],{},[2152,2184,2185],{},[14,2186,2187],{},"ValueError: Non-business day date in busday_offset",[2152,2189,2190,2191],{},"Start on a weekend with default ",[14,2192,1560],{},[2152,2194,2195,2196,2198,2199,2202],{},"Pass ",[14,2197,1347],{}," (positive) or ",[14,2200,2201],{},"\"forward\""," (negative)",[2134,2204,2205,2208,2211],{},[2152,2206,2207],{},"Holidays have no effect",[2152,2209,2210],{},"Holiday dates are strings or timestamps with times",[2152,2212,2213,2214],{},"Convert the list to ",[14,2215,524],{},[2134,2217,2218,2221,2224],{},[2152,2219,2220],{},"Negative counts",[2152,2222,2223],{},"End date before start date",[2152,2225,2226],{},"Validate order, or take the absolute value if intended",[2134,2228,2229,2232,2235],{},[2152,2230,2231],{},"Dates shifted by timezone",[2152,2233,2234],{},"Timezone-aware timestamps",[2152,2236,2237],{},"Convert to local time and drop the zone first",[202,2239,2241],{"id":2240},"performance-and-scale-notes","Performance and scale notes",[10,2243,2244,17,2246,2249,2250,2253,2254,2257,2258,2261,2262,2264],{},[14,2245,691],{},[14,2247,2248],{},"busday_offset"," are vectorised in C, so a million rows take a fraction of a second — far faster than ",[14,2251,2252],{},"apply"," with a Python function, and faster than generating\n",[14,2255,2256],{},"bdate_range"," per row. Building a ",[14,2259,2260],{},"np.busdaycalendar(weekmask=..., holidays=...)"," once and passing it as ",[14,2263,1852],{}," avoids re-validating the holiday list on every call when the\nsame calendar is used repeatedly.",[202,2266,2268],{"id":2267},"conclusion","Conclusion",[10,2270,2271,17,2273,2276,2277,17,2279,2281,2282,2284,2285,2287,2288,2290],{},[14,2272,982],{},[14,2274,2275],{},"np.busday_offset"," are the vectorised equivalents of ",[14,2278,16],{},[14,2280,20],{},". Convert columns to ",[14,2283,524],{},", mask missing dates, and account for the\nconventions: add a day to the end to count both ends like ",[14,2286,16],{},", and roll backward for positive offsets to match ",[14,2289,20],{}," on weekend start dates. Load holidays from the\nworkbook, use a weekmask for non-standard weekends, and keep missing results as missing so they never count as breaches.",[202,2292,2294],{"id":2293},"frequently-asked-questions","Frequently asked questions",[10,2296,2297,2301,2302,2304],{},[2298,2299,2300],"strong",{},"What is the pandas equivalent of NETWORKDAYS?","\nnumpy.busday_count on the start and end columns converted to datetime64",[216,2303,93],{},", with one day added to the end so both dates are counted as Excel does.",[10,2306,2307,2310],{},[2298,2308,2309],{},"How do I exclude public holidays?","\nPass an array of holiday dates as holidays to busday_count or busday_offset. Reading them from a sheet in the same workbook lets non-developers maintain the list.",[10,2312,2313,2316],{},[2298,2314,2315],{},"Can pandas CustomBusinessDay do the same thing?","\nYes, for adding working days: a CustomBusinessDay offset with holidays shifts dates correctly. For counting days between two columns, busday_count is simpler and much faster.",[10,2318,2319,2322],{},[2298,2320,2321],{},"How do I count working hours instead of days?","\nCount full working days between the dates, then add the partial hours of the first and last day from the timestamps. Business-hour offsets in pandas can help, but explicit arithmetic is easier to verify.",[202,2324,2326],{"id":2325},"related","Related",[2328,2329,2330,2337,2344,2349,2356],"ul",{},[2331,2332,2333,2334,2336],"li",{},"Up one level: ",[23,2335,26],{"href":25}," — parsing, grouping and timezones.",[2331,2338,2339,2343],{},[23,2340,2342],{"href":2341},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fparse-excel-dates-into-python-datetimes-with-pandas\u002F","Parse Excel Dates into Python datetimes with pandas"," — getting clean date columns first.",[2331,2345,2346,2348],{},[23,2347,2122],{"href":2121}," — summarising SLA results by period.",[2331,2350,2351,2355],{},[23,2352,2354],{"href":2353},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","Excel Formula Equivalents in pandas"," — more spreadsheet functions translated.",[2331,2357,2358,2362],{},[23,2359,2361],{"href":2360},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexcel-if-formulas-as-pandas-conditional-columns\u002F","Excel IF Formulas as pandas Conditional Columns"," — building breach flags and categories.",[2364,2365,2366],"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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":212,"searchDepth":265,"depth":265,"links":2368},[2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379,2380,2381],{"id":204,"depth":265,"text":205},{"id":501,"depth":265,"text":502},{"id":699,"depth":265,"text":700},{"id":1048,"depth":265,"text":1049},{"id":1179,"depth":265,"text":1180},{"id":1325,"depth":265,"text":1326},{"id":1654,"depth":265,"text":1655},{"id":1841,"depth":265,"text":1842},{"id":2125,"depth":265,"text":2126},{"id":2240,"depth":265,"text":2241},{"id":2267,"depth":265,"text":2268},{"id":2293,"depth":265,"text":2294},{"id":2325,"depth":265,"text":2326},"2026-09-17","Reproduce NETWORKDAYS, NETWORKDAYS.INTL and WORKDAY in Python: count working days between two date columns with numpy busday_count, include both ends like Excel, exclude holidays from a sheet, and add working days to a date.","md",[2386,2388,2390,2392],{"q":2300,"a":2387},"numpy.busday_count on the start and end columns converted to datetime64[D], with one day added to the end so both dates are counted as Excel does.",{"q":2309,"a":2389},"Pass an array of holiday dates as holidays to busday_count or busday_offset. Reading them from a sheet in the same workbook lets non-developers maintain the list.",{"q":2315,"a":2391},"Yes, for adding working days: a CustomBusinessDay offset with holidays shifts dates correctly. For counting days between two columns, busday_count is simpler and much faster.",{"q":2321,"a":2393},"Count full working days between the dates, then add the partial hours of the first and last day from the timestamps. Business-hour offsets in pandas can help, but explicit arithmetic is easier to verify.","calculate business days between excel dates with pandas",null,{"breadcrumb":2397},[2398,2401,2404],{"name":2399,"item":2400},"Home","\u002F",{"name":2402,"item":2403},"Advanced Data Transformation and Cleaning","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":26,"item":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-business-days-between-excel-dates-with-pandas",{"title":2407,"description":2408},"NETWORKDAYS and WORKDAY in pandas: Business Days Between Dates","Calculate working days between Excel dates in pandas with numpy busday_count and busday_offset: inclusive counts matching NETWORKDAYS, holiday lists, custom weekends, missing dates and SLA deadlines.","calculate-business-days-between-excel-dates-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-business-days-between-excel-dates-with-pandas\u002Findex",[2412,2413],"dates-and-times","pandas","how-to","-gSs0tZMKs3tQCNCoOnLxR3fIjsvcLDWmAY4gs4S8OY",[2417,2421],{"title":2418,"path":2419,"stem":2420,"children":-1},"Calculate Age and Tenure from Dates with pandas (DATEDIF Equivalent)","\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-age-and-tenure-from-dates-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-age-and-tenure-from-dates-with-pandas\u002Findex",{"title":2422,"path":2423,"stem":2424,"children":-1},"Calculate Month-to-Date and Year-to-Date Totals with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-month-to-date-and-year-to-date-totals-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-month-to-date-and-year-to-date-totals-with-pandas\u002Findex",[2426,2429],{"path":2427,"title":2428},"\u002Fadvanced-data-transformation-and-cleaning","Advanced Data Transformation and Cleaning for Excel Automation",{"path":2430,"title":26},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data",[],[2433,2436,2439,2442,2445,2448],{"path":2434,"title":2435,"kind":2414},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-overdue-dates-with-openpyxl","Highlight Overdue Dates in Excel with openpyxl",{"path":2437,"title":2438,"kind":2414},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-weekends-and-holidays-in-a-date-column","Highlight Weekends and Holidays in a Date Column",{"path":2440,"title":2441,"kind":2414},"\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":2443,"title":2444,"kind":2414},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling","Calculate Moving Averages with pandas rolling",{"path":2446,"title":2447,"kind":2414},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas","EOMONTH and EDATE Equivalents in pandas",{"path":2449,"title":2450,"kind":2414},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fyearfrac-and-days360-equivalents-in-python","YEARFRAC and DAYS360 Equivalents in Python",1790972101099]