[{"data":1,"prerenderedAt":1879},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data":1844,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data":1853,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data":1858,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data":1859},{"id":4,"title":5,"body":6,"dateModified":1810,"datePublished":1810,"description":1811,"extension":1812,"faq":1813,"keyword":1822,"kind":1823,"meta":1824,"navigation":96,"path":1833,"seo":1834,"slug":1837,"stem":1838,"topics":1839,"type":1842,"__hash__":1843},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data\u002Findex.md","Build an Overtime Report from Shift Data",{"type":7,"value":8,"toc":1799},"minimark",[9,24,29,61,64,68,455,462,605,609,972,979,983,1228,1231,1456,1459,1463,1619,1626,1690,1694,1729,1733,1736,1740,1746,1752,1758,1764,1768,1795],[10,11,12,13,17,18,23],"p",{},"Overtime is one of the larger costs a manager can actually influence, and one of the hardest to see clearly. The data comes from a clocking system or a timesheet export — clock in, clock out, sometimes a break — and turning it into paid overtime involves rules that a spreadsheet handles awkwardly: a night shift that starts on Friday and ends on Saturday, a thirty-minute unpaid break on shifts over six hours, time rounded to the nearest quarter hour, overtime counted against contracted hours for the ",[14,15,16],"em",{},"week"," rather than the day, and different rates for Sundays and bank holidays. On top of the cost, there is a compliance question: in the UK, the Working Time Regulations limit average working time to 48 hours a week over a reference period unless an employee has opted out. This page builds the overtime report with pandas from clock records: paid hours per shift, weekly overtime above contract, premium hours, cost by department and a check of average weekly hours. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[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],{},"Two inputs: staff with department, contracted weekly hours, hourly rate and whether they have opted out of the 48-hour limit; and clock records for twelve weeks from July to September 2026, one row per shift with clock-in and clock-out timestamps. Some transport shifts run overnight. The pay rules used here are typical rather than universal: a 30-minute unpaid break on shifts longer than six hours, time rounded to the nearest 15 minutes, overtime at time-and-a-half on hours above contract in a week, and double time for hours worked on Sundays and bank holidays.",[25,65,67],{"id":66},"step-1-paid-hours-per-shift","Step 1 — Paid hours per shift",[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\nstaff = pd.read_excel(\"staff.xlsx\").set_index(\"Employee\")\nclock = pd.read_excel(\"clock_records.xlsx\", parse_dates=[\"Clock in\", \"Clock out\"])\n\nclock[\"Worked h\"] = (clock[\"Clock out\"] - clock[\"Clock in\"]).dt.total_seconds() \u002F 3600\nassert (clock[\"Worked h\"] > 0).all() and (clock[\"Worked h\"] \u003C 16).all(), \"implausible shift length\"\nclock[\"Break h\"] = (clock[\"Worked h\"] > 6) * 0.5\nclock[\"Paid h\"] = ((clock[\"Worked h\"] - clock[\"Break h\"]) * 4).round() \u002F 4      # nearest quarter hour\nclock[\"Shift date\"] = clock[\"Clock in\"].dt.normalize()\nclock[\"Week\"] = clock[\"Shift date\"].dt.to_period(\"W-SUN\")\nprint(clock[[\"Employee\", \"Clock in\", \"Clock out\", \"Worked h\", \"Paid h\"]].head(4).round({\"Worked h\": 2}).to_string(index=False))\nprint(\"overnight shifts:\", int((clock[\"Clock out\"].dt.date != clock[\"Clock in\"].dt.date).sum()))\n","python",[37,74,75,91,98,122,158,163,202,244,276,320,339,363,421],{"__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,104,107,110,113,116,119],{"class":42,"line":100},3,[40,102,103],{"class":83},"staff ",[40,105,106],{"class":79},"=",[40,108,109],{"class":83}," pd.read_excel(",[40,111,112],{"class":50},"\"staff.xlsx\"",[40,114,115],{"class":83},").set_index(",[40,117,118],{"class":50},"\"Employee\"",[40,120,121],{"class":83},")\n",[40,123,125,128,130,132,135,138,142,144,147,150,152,155],{"class":42,"line":124},4,[40,126,127],{"class":83},"clock ",[40,129,106],{"class":79},[40,131,109],{"class":83},[40,133,134],{"class":50},"\"clock_records.xlsx\"",[40,136,137],{"class":83},", ",[40,139,141],{"class":140},"sa561","parse_dates",[40,143,106],{"class":79},[40,145,146],{"class":83},"[",[40,148,149],{"class":50},"\"Clock in\"",[40,151,137],{"class":83},[40,153,154],{"class":50},"\"Clock out\"",[40,156,157],{"class":83},"])\n",[40,159,161],{"class":42,"line":160},5,[40,162,97],{"emptyLinePlaceholder":96},[40,164,166,169,172,175,177,180,182,184,187,190,192,195,198],{"class":42,"line":165},6,[40,167,168],{"class":83},"clock[",[40,170,171],{"class":50},"\"Worked h\"",[40,173,174],{"class":83},"] ",[40,176,106],{"class":79},[40,178,179],{"class":83}," (clock[",[40,181,154],{"class":50},[40,183,174],{"class":83},[40,185,186],{"class":79},"-",[40,188,189],{"class":83}," clock[",[40,191,149],{"class":50},[40,193,194],{"class":83},"]).dt.total_seconds() ",[40,196,197],{"class":79},"\u002F",[40,199,201],{"class":200},"sP0c6"," 3600\n",[40,203,205,208,210,212,214,217,220,223,226,228,230,232,235,238,241],{"class":42,"line":204},7,[40,206,207],{"class":79},"assert",[40,209,179],{"class":83},[40,211,171],{"class":50},[40,213,174],{"class":83},[40,215,216],{"class":79},">",[40,218,219],{"class":200}," 0",[40,221,222],{"class":83},").all() ",[40,224,225],{"class":79},"and",[40,227,179],{"class":83},[40,229,171],{"class":50},[40,231,174],{"class":83},[40,233,234],{"class":79},"\u003C",[40,236,237],{"class":200}," 16",[40,239,240],{"class":83},").all(), ",[40,242,243],{"class":50},"\"implausible shift length\"\n",[40,245,247,249,252,254,256,258,260,262,264,267,270,273],{"class":42,"line":246},8,[40,248,168],{"class":83},[40,250,251],{"class":50},"\"Break h\"",[40,253,174],{"class":83},[40,255,106],{"class":79},[40,257,179],{"class":83},[40,259,171],{"class":50},[40,261,174],{"class":83},[40,263,216],{"class":79},[40,265,266],{"class":200}," 6",[40,268,269],{"class":83},") ",[40,271,272],{"class":79},"*",[40,274,275],{"class":200}," 0.5\n",[40,277,279,281,284,286,288,291,293,295,297,299,301,304,306,309,312,314,316],{"class":42,"line":278},9,[40,280,168],{"class":83},[40,282,283],{"class":50},"\"Paid h\"",[40,285,174],{"class":83},[40,287,106],{"class":79},[40,289,290],{"class":83}," ((clock[",[40,292,171],{"class":50},[40,294,174],{"class":83},[40,296,186],{"class":79},[40,298,189],{"class":83},[40,300,251],{"class":50},[40,302,303],{"class":83},"]) ",[40,305,272],{"class":79},[40,307,308],{"class":200}," 4",[40,310,311],{"class":83},").round() ",[40,313,197],{"class":79},[40,315,308],{"class":200},[40,317,319],{"class":318},"s-wDw","      # nearest quarter hour\n",[40,321,323,325,328,330,332,334,336],{"class":42,"line":322},10,[40,324,168],{"class":83},[40,326,327],{"class":50},"\"Shift date\"",[40,329,174],{"class":83},[40,331,106],{"class":79},[40,333,189],{"class":83},[40,335,149],{"class":50},[40,337,338],{"class":83},"].dt.normalize()\n",[40,340,342,344,347,349,351,353,355,358,361],{"class":42,"line":341},11,[40,343,168],{"class":83},[40,345,346],{"class":50},"\"Week\"",[40,348,174],{"class":83},[40,350,106],{"class":79},[40,352,189],{"class":83},[40,354,327],{"class":50},[40,356,357],{"class":83},"].dt.to_period(",[40,359,360],{"class":50},"\"W-SUN\"",[40,362,121],{"class":83},[40,364,366,369,372,374,376,378,380,382,384,386,388,390,393,396,399,401,404,407,410,413,415,418],{"class":42,"line":365},12,[40,367,368],{"class":200},"print",[40,370,371],{"class":83},"(clock[[",[40,373,118],{"class":50},[40,375,137],{"class":83},[40,377,149],{"class":50},[40,379,137],{"class":83},[40,381,154],{"class":50},[40,383,137],{"class":83},[40,385,171],{"class":50},[40,387,137],{"class":83},[40,389,283],{"class":50},[40,391,392],{"class":83},"]].head(",[40,394,395],{"class":200},"4",[40,397,398],{"class":83},").round({",[40,400,171],{"class":50},[40,402,403],{"class":83},": ",[40,405,406],{"class":200},"2",[40,408,409],{"class":83},"}).to_string(",[40,411,412],{"class":140},"index",[40,414,106],{"class":79},[40,416,417],{"class":200},"False",[40,419,420],{"class":83},"))\n",[40,422,424,426,429,432,434,437,440,442,445,448,450,452],{"class":42,"line":423},13,[40,425,368],{"class":200},[40,427,428],{"class":83},"(",[40,430,431],{"class":50},"\"overnight shifts:\"",[40,433,137],{"class":83},[40,435,436],{"class":200},"int",[40,438,439],{"class":83},"((clock[",[40,441,154],{"class":50},[40,443,444],{"class":83},"].dt.date ",[40,446,447],{"class":79},"!=",[40,449,189],{"class":83},[40,451,149],{"class":50},[40,453,454],{"class":83},"].dt.date).sum()))\n",[10,456,457,458,461],{},"Working time is clock-out minus clock-in, computed from full timestamps so that a shift from 22:07 on Friday to 06:40 on Saturday is simply eight and a half hours — the subtraction does not care that the date changed, which is exactly where formulas comparing times of day in Excel go wrong. A sanity check rejects zero, negative or implausibly long shifts, which almost always mean a missed clock-out. The unpaid break is deducted from shifts over six hours, and the result is rounded to the nearest quarter hour, as many payroll systems do. Each shift is assigned to the date and the Monday-to-Sunday week it ",[14,459,460],{},"started"," in, a common convention for overnight work; some agreements split the hours at midnight instead, which is a small change to this step.",[463,464,472,473,472,477,472,481,472,488,472,498,472,505,472,510,472,516,472,521,472,525,472,531,472,535,472,539,472,543,472,546,472,549,472,553,472,556,472,559,472,563,472,566,472,571,472,576,472,579,472,582,472,586,472,589,472,594,472,599,472,602],"svg",{"viewBox":465,"role":466,"ariaLabelledBy":467,"xmlns":470,"style":471},"4 0 752 108","img",[468,469],"ot-steps-t","ot-steps-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[474,475,476],"title",{"id":468},"From clock records to overtime cost",[478,479,480],"desc",{"id":469},"Each shift's worked time is clock out minus clock in, so overnight shifts need no special handling. The unpaid break is deducted and the result rounded to a quarter hour. Paid hours are summed per employee and week, hours above contract are overtime, Sunday and bank holiday hours get a double-time premium, and the cost is totalled by department.",[482,483],"rect",{"x":395,"y":484,"width":485,"height":486,"fill":487},"0","752","108","#ffffff",[482,489],{"x":490,"y":491,"width":492,"height":493,"rx":494,"fill":495,"stroke":496,"style":497},"20.0","16.0","115.2","76.5","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[499,500,504],"text",{"x":501,"y":502,"style":503},"77.6","50.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Clock in \u002F out",[499,506,509],{"x":501,"y":507,"style":508},"69.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","full timestamps",[42,511],{"x1":512,"y1":513,"x2":514,"y2":513,"stroke":515,"style":497},"140.2","54.2","162.2","var(--brand,#5b5cf0)",[517,518],"polygon",{"points":519,"fill":520},"166.2,54.2 157.2,59.2 157.2,49.2","#5b5cf0",[482,522],{"x":523,"y":491,"width":492,"height":493,"rx":494,"fill":524,"stroke":515,"style":497},"171.2","#f0f4ff",[499,526,530],{"x":527,"y":528,"style":529},"228.8","41.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","− unpaid",[499,532,534],{"x":527,"y":533,"style":529},"59.0","break",[499,536,538],{"x":527,"y":537,"style":508},"78.5","shifts over 6 h",[42,540],{"x1":541,"y1":513,"x2":542,"y2":513,"stroke":515,"style":497},"291.4","313.4",[517,544],{"points":545,"fill":520},"317.4,54.2 308.4,59.2 308.4,49.2",[482,547],{"x":548,"y":491,"width":492,"height":493,"rx":494,"fill":524,"stroke":515,"style":497},"322.4",[499,550,552],{"x":551,"y":528,"style":529},"380.0","Round to 15",[499,554,555],{"x":551,"y":533,"style":529},"min",[499,557,558],{"x":551,"y":537,"style":508},"paid hours",[42,560],{"x1":561,"y1":513,"x2":562,"y2":513,"stroke":515,"style":497},"442.6","464.6",[517,564],{"points":565,"fill":520},"468.6,54.2 459.6,59.2 459.6,49.2",[482,567],{"x":568,"y":491,"width":492,"height":493,"rx":494,"fill":569,"stroke":570,"style":497},"473.6","#fdefd8","var(--gold,#b4740a)",[499,572,575],{"x":573,"y":528,"style":574},"531.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Weekly total",[499,577,578],{"x":573,"y":533,"style":574},"− contract",[499,580,581],{"x":573,"y":537,"style":508},"overtime hours",[42,583],{"x1":584,"y1":513,"x2":585,"y2":513,"stroke":515,"style":497},"593.8","615.8",[517,587],{"points":588,"fill":520},"619.8,54.2 610.8,59.2 610.8,49.2",[482,590],{"x":591,"y":491,"width":492,"height":493,"rx":494,"fill":592,"stroke":593,"style":497},"624.8","#d9f4f1","var(--teal,#0f9488)",[499,595,598],{"x":596,"y":528,"style":597},"682.4","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Premiums +",[499,600,601],{"x":596,"y":533,"style":597},"cost",[499,603,604],{"x":596,"y":537,"style":508},"by department",[25,606,608],{"id":607},"step-2-weekly-overtime-and-premium-hours","Step 2 — Weekly overtime and premium hours",[30,610,612],{"className":70,"code":611,"language":72,"meta":35,"style":35},"import holidays\n\nbank = holidays.country_holidays(\"GB\", subdiv=\"ENG\", years=[2026])\nclock[\"Premium day\"] = (clock[\"Shift date\"].dt.weekday == 6) | clock[\"Shift date\"].dt.date.isin(bank)\n\nweekly = clock.groupby([\"Employee\", \"Week\"]).agg(Paid=(\"Paid h\", \"sum\"),\n                                                  Premium=(\"Paid h\", lambda s: s[clock.loc[s.index, \"Premium day\"]].sum()))\nweekly = weekly.join(staff[[\"Contract hours\", \"Hourly rate\"]], on=\"Employee\")\nweekly[\"Overtime h\"] = (weekly[\"Paid\"] - weekly[\"Contract hours\"]).clip(lower=0)\nweekly[\"Double-time h\"] = weekly[[\"Premium\", \"Overtime h\"]].min(axis=1)       # premium-day hours within overtime\nweekly[\"Time-and-a-half h\"] = weekly[\"Overtime h\"] - weekly[\"Double-time h\"]\nweekly[\"Overtime cost\"] = (weekly[\"Time-and-a-half h\"] * 1.5 + weekly[\"Double-time h\"] * 2.0) * weekly[\"Hourly rate\"]\nprint(weekly.groupby(\"Employee\")[[\"Paid\", \"Overtime h\", \"Double-time h\", \"Overtime cost\"]].sum().round(1).to_string())\n",[37,613,614,620,624,661,696,700,736,760,789,828,866,892,938],{"__ignoreMap":35},[40,615,616,618],{"class":42,"line":43},[40,617,80],{"class":79},[40,619,60],{"class":83},[40,621,622],{"class":42,"line":93},[40,623,97],{"emptyLinePlaceholder":96},[40,625,626,629,631,634,637,639,642,644,647,649,652,654,656,659],{"class":42,"line":100},[40,627,628],{"class":83},"bank ",[40,630,106],{"class":79},[40,632,633],{"class":83}," holidays.country_holidays(",[40,635,636],{"class":50},"\"GB\"",[40,638,137],{"class":83},[40,640,641],{"class":140},"subdiv",[40,643,106],{"class":79},[40,645,646],{"class":50},"\"ENG\"",[40,648,137],{"class":83},[40,650,651],{"class":140},"years",[40,653,106],{"class":79},[40,655,146],{"class":83},[40,657,658],{"class":200},"2026",[40,660,157],{"class":83},[40,662,663,665,668,670,672,674,676,679,682,684,686,689,691,693],{"class":42,"line":124},[40,664,168],{"class":83},[40,666,667],{"class":50},"\"Premium day\"",[40,669,174],{"class":83},[40,671,106],{"class":79},[40,673,179],{"class":83},[40,675,327],{"class":50},[40,677,678],{"class":83},"].dt.weekday ",[40,680,681],{"class":79},"==",[40,683,266],{"class":200},[40,685,269],{"class":83},[40,687,688],{"class":79},"|",[40,690,189],{"class":83},[40,692,327],{"class":50},[40,694,695],{"class":83},"].dt.date.isin(bank)\n",[40,697,698],{"class":42,"line":160},[40,699,97],{"emptyLinePlaceholder":96},[40,701,702,705,707,710,712,714,716,719,722,724,726,728,730,733],{"class":42,"line":165},[40,703,704],{"class":83},"weekly ",[40,706,106],{"class":79},[40,708,709],{"class":83}," clock.groupby([",[40,711,118],{"class":50},[40,713,137],{"class":83},[40,715,346],{"class":50},[40,717,718],{"class":83},"]).agg(",[40,720,721],{"class":140},"Paid",[40,723,106],{"class":79},[40,725,428],{"class":83},[40,727,283],{"class":50},[40,729,137],{"class":83},[40,731,732],{"class":50},"\"sum\"",[40,734,735],{"class":83},"),\n",[40,737,738,741,743,745,747,749,752,755,757],{"class":42,"line":204},[40,739,740],{"class":140},"                                                  Premium",[40,742,106],{"class":79},[40,744,428],{"class":83},[40,746,283],{"class":50},[40,748,137],{"class":83},[40,750,751],{"class":79},"lambda",[40,753,754],{"class":83}," s: s[clock.loc[s.index, ",[40,756,667],{"class":50},[40,758,759],{"class":83},"]].sum()))\n",[40,761,762,764,766,769,772,774,777,780,783,785,787],{"class":42,"line":246},[40,763,704],{"class":83},[40,765,106],{"class":79},[40,767,768],{"class":83}," weekly.join(staff[[",[40,770,771],{"class":50},"\"Contract hours\"",[40,773,137],{"class":83},[40,775,776],{"class":50},"\"Hourly rate\"",[40,778,779],{"class":83},"]], ",[40,781,782],{"class":140},"on",[40,784,106],{"class":79},[40,786,118],{"class":50},[40,788,121],{"class":83},[40,790,791,794,797,799,801,804,807,809,811,814,816,819,822,824,826],{"class":42,"line":278},[40,792,793],{"class":83},"weekly[",[40,795,796],{"class":50},"\"Overtime h\"",[40,798,174],{"class":83},[40,800,106],{"class":79},[40,802,803],{"class":83}," (weekly[",[40,805,806],{"class":50},"\"Paid\"",[40,808,174],{"class":83},[40,810,186],{"class":79},[40,812,813],{"class":83}," weekly[",[40,815,771],{"class":50},[40,817,818],{"class":83},"]).clip(",[40,820,821],{"class":140},"lower",[40,823,106],{"class":79},[40,825,484],{"class":200},[40,827,121],{"class":83},[40,829,830,832,835,837,839,842,845,847,849,852,855,857,860,863],{"class":42,"line":322},[40,831,793],{"class":83},[40,833,834],{"class":50},"\"Double-time h\"",[40,836,174],{"class":83},[40,838,106],{"class":79},[40,840,841],{"class":83}," weekly[[",[40,843,844],{"class":50},"\"Premium\"",[40,846,137],{"class":83},[40,848,796],{"class":50},[40,850,851],{"class":83},"]].min(",[40,853,854],{"class":140},"axis",[40,856,106],{"class":79},[40,858,859],{"class":200},"1",[40,861,862],{"class":83},")       ",[40,864,865],{"class":318},"# premium-day hours within overtime\n",[40,867,868,870,873,875,877,879,881,883,885,887,889],{"class":42,"line":341},[40,869,793],{"class":83},[40,871,872],{"class":50},"\"Time-and-a-half h\"",[40,874,174],{"class":83},[40,876,106],{"class":79},[40,878,813],{"class":83},[40,880,796],{"class":50},[40,882,174],{"class":83},[40,884,186],{"class":79},[40,886,813],{"class":83},[40,888,834],{"class":50},[40,890,891],{"class":83},"]\n",[40,893,894,896,899,901,903,905,907,909,911,914,917,919,921,923,925,928,930,932,934,936],{"class":42,"line":365},[40,895,793],{"class":83},[40,897,898],{"class":50},"\"Overtime cost\"",[40,900,174],{"class":83},[40,902,106],{"class":79},[40,904,803],{"class":83},[40,906,872],{"class":50},[40,908,174],{"class":83},[40,910,272],{"class":79},[40,912,913],{"class":200}," 1.5",[40,915,916],{"class":79}," +",[40,918,813],{"class":83},[40,920,834],{"class":50},[40,922,174],{"class":83},[40,924,272],{"class":79},[40,926,927],{"class":200}," 2.0",[40,929,269],{"class":83},[40,931,272],{"class":79},[40,933,813],{"class":83},[40,935,776],{"class":50},[40,937,891],{"class":83},[40,939,940,942,945,947,950,952,954,956,958,960,962,964,967,969],{"class":42,"line":423},[40,941,368],{"class":200},[40,943,944],{"class":83},"(weekly.groupby(",[40,946,118],{"class":50},[40,948,949],{"class":83},")[[",[40,951,806],{"class":50},[40,953,137],{"class":83},[40,955,796],{"class":50},[40,957,137],{"class":83},[40,959,834],{"class":50},[40,961,137],{"class":83},[40,963,898],{"class":50},[40,965,966],{"class":83},"]].sum().round(",[40,968,859],{"class":200},[40,970,971],{"class":83},").to_string())\n",[10,973,974,975,978],{},"Overtime here is measured per week: paid hours above the employee's contracted weekly hours, so a long Tuesday balanced by a short Thursday is not overtime. Within the overtime, hours worked on a Sunday or a bank holiday — the 31 August bank holiday falls in this period — are paid at double time and the rest at time and a half; counting premium-day hours ",[14,976,977],{},"within"," the overtime avoids paying the same hour twice. The summary per employee already shows the pattern managers need to see: two people, one in the warehouse and one in transport, account for most of the overtime, regularly working long weeks including weekends. Cai in the warehouse worked 211.5 overtime hours over the twelve weeks and Dee in transport 247, between them about 80% of the £14,159 total overtime cost, while the office worked almost none.",[25,980,982],{"id":981},"step-3-cost-by-department-and-the-48-hour-check","Step 3 — Cost by department and the 48-hour check",[30,984,986],{"className":70,"code":985,"language":72,"meta":35,"style":35},"dept = weekly.join(staff[\"Department\"], on=\"Employee\").groupby(\"Department\").agg(\n    paid=(\"Paid\", \"sum\"), overtime=(\"Overtime h\", \"sum\"), cost=(\"Overtime cost\", \"sum\"))\ndept[\"Overtime share\"] = (dept[\"overtime\"] \u002F dept[\"paid\"] * 100).round(1)\nprint(dept.round(0).to_string())\n\navg = weekly.groupby(\"Employee\")[\"Paid\"].sum() \u002F clock[\"Week\"].nunique()\nlimit = pd.DataFrame({\"Average weekly hours\": avg.round(1), \"Opted out\": staff[\"48h opt-out\"]})\nlimit[\"Over 48h average\"] = (limit[\"Average weekly hours\"] > 48) & ~limit[\"Opted out\"]\nprint(limit.to_string())\n",[37,987,988,1018,1065,1107,1118,1122,1151,1183,1221],{"__ignoreMap":35},[40,989,990,993,995,998,1001,1004,1006,1008,1010,1013,1015],{"class":42,"line":43},[40,991,992],{"class":83},"dept ",[40,994,106],{"class":79},[40,996,997],{"class":83}," weekly.join(staff[",[40,999,1000],{"class":50},"\"Department\"",[40,1002,1003],{"class":83},"], ",[40,1005,782],{"class":140},[40,1007,106],{"class":79},[40,1009,118],{"class":50},[40,1011,1012],{"class":83},").groupby(",[40,1014,1000],{"class":50},[40,1016,1017],{"class":83},").agg(\n",[40,1019,1020,1023,1025,1027,1029,1031,1033,1036,1039,1041,1043,1045,1047,1049,1051,1053,1055,1057,1059,1061,1063],{"class":42,"line":93},[40,1021,1022],{"class":140},"    paid",[40,1024,106],{"class":79},[40,1026,428],{"class":83},[40,1028,806],{"class":50},[40,1030,137],{"class":83},[40,1032,732],{"class":50},[40,1034,1035],{"class":83},"), ",[40,1037,1038],{"class":140},"overtime",[40,1040,106],{"class":79},[40,1042,428],{"class":83},[40,1044,796],{"class":50},[40,1046,137],{"class":83},[40,1048,732],{"class":50},[40,1050,1035],{"class":83},[40,1052,601],{"class":140},[40,1054,106],{"class":79},[40,1056,428],{"class":83},[40,1058,898],{"class":50},[40,1060,137],{"class":83},[40,1062,732],{"class":50},[40,1064,420],{"class":83},[40,1066,1067,1070,1073,1075,1077,1080,1083,1085,1087,1090,1093,1095,1097,1100,1103,1105],{"class":42,"line":100},[40,1068,1069],{"class":83},"dept[",[40,1071,1072],{"class":50},"\"Overtime share\"",[40,1074,174],{"class":83},[40,1076,106],{"class":79},[40,1078,1079],{"class":83}," (dept[",[40,1081,1082],{"class":50},"\"overtime\"",[40,1084,174],{"class":83},[40,1086,197],{"class":79},[40,1088,1089],{"class":83}," dept[",[40,1091,1092],{"class":50},"\"paid\"",[40,1094,174],{"class":83},[40,1096,272],{"class":79},[40,1098,1099],{"class":200}," 100",[40,1101,1102],{"class":83},").round(",[40,1104,859],{"class":200},[40,1106,121],{"class":83},[40,1108,1109,1111,1114,1116],{"class":42,"line":124},[40,1110,368],{"class":200},[40,1112,1113],{"class":83},"(dept.round(",[40,1115,484],{"class":200},[40,1117,971],{"class":83},[40,1119,1120],{"class":42,"line":160},[40,1121,97],{"emptyLinePlaceholder":96},[40,1123,1124,1127,1129,1132,1134,1137,1139,1142,1144,1146,1148],{"class":42,"line":165},[40,1125,1126],{"class":83},"avg ",[40,1128,106],{"class":79},[40,1130,1131],{"class":83}," weekly.groupby(",[40,1133,118],{"class":50},[40,1135,1136],{"class":83},")[",[40,1138,806],{"class":50},[40,1140,1141],{"class":83},"].sum() ",[40,1143,197],{"class":79},[40,1145,189],{"class":83},[40,1147,346],{"class":50},[40,1149,1150],{"class":83},"].nunique()\n",[40,1152,1153,1156,1158,1161,1164,1167,1169,1171,1174,1177,1180],{"class":42,"line":204},[40,1154,1155],{"class":83},"limit ",[40,1157,106],{"class":79},[40,1159,1160],{"class":83}," pd.DataFrame({",[40,1162,1163],{"class":50},"\"Average weekly hours\"",[40,1165,1166],{"class":83},": avg.round(",[40,1168,859],{"class":200},[40,1170,1035],{"class":83},[40,1172,1173],{"class":50},"\"Opted out\"",[40,1175,1176],{"class":83},": staff[",[40,1178,1179],{"class":50},"\"48h opt-out\"",[40,1181,1182],{"class":83},"]})\n",[40,1184,1185,1188,1191,1193,1195,1198,1200,1202,1204,1207,1209,1212,1215,1217,1219],{"class":42,"line":246},[40,1186,1187],{"class":83},"limit[",[40,1189,1190],{"class":50},"\"Over 48h average\"",[40,1192,174],{"class":83},[40,1194,106],{"class":79},[40,1196,1197],{"class":83}," (limit[",[40,1199,1163],{"class":50},[40,1201,174],{"class":83},[40,1203,216],{"class":79},[40,1205,1206],{"class":200}," 48",[40,1208,269],{"class":83},[40,1210,1211],{"class":79},"&",[40,1213,1214],{"class":79}," ~",[40,1216,1187],{"class":83},[40,1218,1173],{"class":50},[40,1220,891],{"class":83},[40,1222,1223,1225],{"class":42,"line":278},[40,1224,368],{"class":200},[40,1226,1227],{"class":83},"(limit.to_string())\n",[10,1229,1230],{},"Rolling the weekly figures up by department gives overtime hours as a share of all paid hours and the overtime cost — the figures a budget review uses. The compliance check averages each person's weekly hours over the period, counting weeks without shifts as zero, and flags anyone above 48 hours a week who has not opted out. The UK regulations average over a 17-week reference period by default, so a twelve-week average is an early warning rather than a determination; extending the export to 17 weeks makes it the real test. Here the transport driver working the most overtime averages over 48 hours without an opt-out, which is a conversation for the manager now, before the reference period closes and while there is still time to rebalance the rota. The warehouse employee who has opted out is shown but not flagged, since the opt-out is recorded in the staff sheet.",[463,1232,472,1238,472,1241,472,1244,472,1249,472,1255,472,1259,472,1265,472,1269,472,1273,472,1277,472,1281,472,1285,472,1289,472,1293,472,1297,472,1301,472,1304,472,1307,472,1311,472,1313,472,1316,472,1318,472,1321,472,1323,472,1326,472,1328,472,1331,472,1334,472,1337,472,1339,472,1343,472,1345,472,1348,472,1350,472,1353,472,1355,472,1358,472,1360,472,1363,472,1366,472,1370,472,1372,472,1375,472,1377,472,1380,472,1382,472,1385,472,1387,472,1390,472,1394,472,1398,472,1401,472,1404,472,1406,472,1409,472,1411,472,1413,472,1415,472,1418,472,1420,472,1422,472,1424,472,1426,472,1429,472,1433,472,1435,472,1438,472,1440,472,1443,472,1445,472,1448,472,1450,472,1452,472,1454],{"viewBox":1233,"role":466,"ariaLabelledBy":1234,"xmlns":470,"style":1237},"55 0 650 196",[1235,1236],"ot-people-t","ot-people-d","width:100%;max-width:650px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[474,1239,1240],{"id":1235},"Who works the overtime",[478,1242,1243],{"id":1236},"Cai in the warehouse averages 57.6 hours a week and has opted out of the 48-hour limit. Dee in transport averages 60.6 hours without an opt-out and is flagged. Everyone else averages between about 35 and 43 hours.",[482,1245],{"x":1246,"y":484,"width":1247,"height":1248,"fill":487},"55","650","196",[482,1250],{"x":1251,"y":491,"width":1252,"height":1253,"rx":484,"fill":495,"stroke":496,"style":1254},"71.3","32.0","24.0","stroke-width:1px",[482,1256],{"x":1257,"y":491,"width":1258,"height":1253,"rx":484,"fill":495,"stroke":496,"style":1254},"103.3","108.7",[499,1260,1264],{"x":1261,"y":1262,"style":1263},"157.6","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[482,1266],{"x":1267,"y":491,"width":1268,"height":1253,"rx":484,"fill":495,"stroke":496,"style":1254},"212.0","97.1",[499,1270,1272],{"x":1271,"y":1262,"style":1263},"260.5","B",[482,1274],{"x":1275,"y":491,"width":1276,"height":1253,"rx":484,"fill":495,"stroke":496,"style":1254},"309.0","162.0",[499,1278,1280],{"x":1279,"y":1262,"style":1263},"390.1","C",[482,1282],{"x":1283,"y":491,"width":1284,"height":1253,"rx":484,"fill":495,"stroke":496,"style":1254},"471.1","84.9",[499,1286,1288],{"x":1287,"y":1262,"style":1263},"513.5","D",[482,1290],{"x":1291,"y":491,"width":1292,"height":1253,"rx":484,"fill":495,"stroke":496,"style":1254},"555.9","132.8",[499,1294,1296],{"x":1295,"y":1262,"style":1263},"622.3","E",[482,1298],{"x":1251,"y":1299,"width":1252,"height":1300,"rx":484,"fill":495,"stroke":496,"style":1254},"40.0","28.0",[499,1302,859],{"x":1303,"y":533,"style":1263},"87.3",[482,1305],{"x":1257,"y":1299,"width":1258,"height":1300,"rx":484,"fill":524,"stroke":515,"style":1306},"stroke-width:1.5px",[499,1308,1310],{"x":1261,"y":533,"style":1309},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Employee",[482,1312],{"x":1267,"y":1299,"width":1268,"height":1300,"rx":484,"fill":524,"stroke":515,"style":1306},[499,1314,1315],{"x":1271,"y":533,"style":1309},"Department",[482,1317],{"x":1275,"y":1299,"width":1276,"height":1300,"rx":484,"fill":524,"stroke":515,"style":1306},[499,1319,1320],{"x":1279,"y":533,"style":1309},"Average weekly hours",[482,1322],{"x":1283,"y":1299,"width":1284,"height":1300,"rx":484,"fill":524,"stroke":515,"style":1306},[499,1324,1325],{"x":1287,"y":533,"style":1309},"Opted out",[482,1327],{"x":1291,"y":1299,"width":1292,"height":1300,"rx":484,"fill":524,"stroke":515,"style":1306},[499,1329,1330],{"x":1295,"y":533,"style":1309},"Flag",[482,1332],{"x":1251,"y":1333,"width":1252,"height":1300,"rx":484,"fill":495,"stroke":496,"style":1254},"68.0",[499,1335,406],{"x":1303,"y":1336,"style":1263},"87.0",[482,1338],{"x":1257,"y":1333,"width":1258,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1340,1342],{"x":1261,"y":1336,"style":1341},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Cai",[482,1344],{"x":1267,"y":1333,"width":1268,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1346,1347],{"x":1271,"y":1336,"style":1341},"Warehouse",[482,1349],{"x":1275,"y":1333,"width":1276,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1351,1352],{"x":1279,"y":1336,"style":1341},"57.6",[482,1354],{"x":1283,"y":1333,"width":1284,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1356,1357],{"x":1287,"y":1336,"style":1341},"yes",[482,1359],{"x":1291,"y":1333,"width":1292,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1361,1362],{"x":1295,"y":1336,"style":1341},"—",[482,1364],{"x":1251,"y":1365,"width":1252,"height":1300,"rx":484,"fill":495,"stroke":496,"style":1254},"96.0",[499,1367,1369],{"x":1303,"y":1368,"style":1263},"115.0","3",[482,1371],{"x":1257,"y":1365,"width":1258,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1373,1374],{"x":1261,"y":1368,"style":1341},"Dee",[482,1376],{"x":1267,"y":1365,"width":1268,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1378,1379],{"x":1271,"y":1368,"style":1341},"Transport",[482,1381],{"x":1275,"y":1365,"width":1276,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1383,1384],{"x":1279,"y":1368,"style":1341},"60.6",[482,1386],{"x":1283,"y":1365,"width":1284,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1388,1389],{"x":1287,"y":1368,"style":1341},"no",[482,1391],{"x":1291,"y":1365,"width":1292,"height":1300,"rx":484,"fill":1392,"stroke":1393,"style":1306},"#fce9e9","var(--danger,#dc2626)",[499,1395,1397],{"x":1295,"y":1368,"style":1396},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","over 48h average",[482,1399],{"x":1251,"y":1400,"width":1252,"height":1300,"rx":484,"fill":495,"stroke":496,"style":1254},"124.0",[499,1402,395],{"x":1303,"y":1403,"style":1263},"143.0",[482,1405],{"x":1257,"y":1400,"width":1258,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1407,1408],{"x":1261,"y":1403,"style":1341},"Eli",[482,1410],{"x":1267,"y":1400,"width":1268,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1412,1379],{"x":1271,"y":1403,"style":1341},[482,1414],{"x":1275,"y":1400,"width":1276,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1416,1417],{"x":1279,"y":1403,"style":1341},"43.4",[482,1419],{"x":1283,"y":1400,"width":1284,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1421,1389],{"x":1287,"y":1403,"style":1341},[482,1423],{"x":1291,"y":1400,"width":1292,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1425,1362],{"x":1295,"y":1403,"style":1341},[482,1427],{"x":1251,"y":1428,"width":1252,"height":1300,"rx":484,"fill":495,"stroke":496,"style":1254},"152.0",[499,1430,1432],{"x":1303,"y":1431,"style":1263},"171.0","5",[482,1434],{"x":1257,"y":1428,"width":1258,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1436,1437],{"x":1261,"y":1431,"style":1341},"Ava, Ben, Fay",[482,1439],{"x":1267,"y":1428,"width":1268,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1441,1442],{"x":1271,"y":1431,"style":1341},"various",[482,1444],{"x":1275,"y":1428,"width":1276,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1446,1447],{"x":1279,"y":1431,"style":1341},"35–40",[482,1449],{"x":1283,"y":1428,"width":1284,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1451,1389],{"x":1287,"y":1431,"style":1341},[482,1453],{"x":1291,"y":1428,"width":1292,"height":1300,"rx":484,"fill":487,"stroke":496,"style":1254},[499,1455,1362],{"x":1295,"y":1431,"style":1341},[10,1457,1458],{},"Overtime is often a symptom rather than a choice. Two people working sixty-hour weeks for a quarter usually means a vacancy that has not been filled, a rota with a gap at weekends, or a single person who holds knowledge nobody else has — and each of those has a better fix than premium pay. The overtime share by department, 23% of paid hours in transport against 17% in the warehouse and almost nothing in the office, shows where to look first. Long hours sustained for months are also a fatigue risk, particularly for drivers, which is a further reason the 48-hour check belongs in the same report as the cost rather than in a separate HR process.",[25,1460,1462],{"id":1461},"step-4-the-report","Step 4 — The report",[30,1464,1466],{"className":70,"code":1465,"language":72,"meta":35,"style":35},"with pd.ExcelWriter(\"overtime_2026-Q3.xlsx\") as xw:\n    dept.to_excel(xw, sheet_name=\"By department\")\n    weekly.reset_index().assign(Week=lambda d: d[\"Week\"].astype(str)).to_excel(xw, sheet_name=\"By person and week\", index=False)\n    limit.to_excel(xw, sheet_name=\"48-hour check\")\n    clock.assign(Week=clock[\"Week\"].astype(str)).to_excel(xw, sheet_name=\"Shifts\", index=False)\nprint(\"total overtime cost:\", round(weekly[\"Overtime cost\"].sum(), 2))\n",[37,1467,1468,1486,1501,1543,1557,1593],{"__ignoreMap":35},[40,1469,1470,1473,1476,1479,1481,1483],{"class":42,"line":43},[40,1471,1472],{"class":79},"with",[40,1474,1475],{"class":83}," pd.ExcelWriter(",[40,1477,1478],{"class":50},"\"overtime_2026-Q3.xlsx\"",[40,1480,269],{"class":83},[40,1482,87],{"class":79},[40,1484,1485],{"class":83}," xw:\n",[40,1487,1488,1491,1494,1496,1499],{"class":42,"line":93},[40,1489,1490],{"class":83},"    dept.to_excel(xw, ",[40,1492,1493],{"class":140},"sheet_name",[40,1495,106],{"class":79},[40,1497,1498],{"class":50},"\"By department\"",[40,1500,121],{"class":83},[40,1502,1503,1506,1509,1512,1515,1517,1520,1523,1526,1528,1530,1533,1535,1537,1539,1541],{"class":42,"line":100},[40,1504,1505],{"class":83},"    weekly.reset_index().assign(",[40,1507,1508],{"class":140},"Week",[40,1510,1511],{"class":79},"=lambda",[40,1513,1514],{"class":83}," d: d[",[40,1516,346],{"class":50},[40,1518,1519],{"class":83},"].astype(",[40,1521,1522],{"class":200},"str",[40,1524,1525],{"class":83},")).to_excel(xw, ",[40,1527,1493],{"class":140},[40,1529,106],{"class":79},[40,1531,1532],{"class":50},"\"By person and week\"",[40,1534,137],{"class":83},[40,1536,412],{"class":140},[40,1538,106],{"class":79},[40,1540,417],{"class":200},[40,1542,121],{"class":83},[40,1544,1545,1548,1550,1552,1555],{"class":42,"line":124},[40,1546,1547],{"class":83},"    limit.to_excel(xw, ",[40,1549,1493],{"class":140},[40,1551,106],{"class":79},[40,1553,1554],{"class":50},"\"48-hour check\"",[40,1556,121],{"class":83},[40,1558,1559,1562,1564,1566,1568,1570,1572,1574,1576,1578,1580,1583,1585,1587,1589,1591],{"class":42,"line":160},[40,1560,1561],{"class":83},"    clock.assign(",[40,1563,1508],{"class":140},[40,1565,106],{"class":79},[40,1567,168],{"class":83},[40,1569,346],{"class":50},[40,1571,1519],{"class":83},[40,1573,1522],{"class":200},[40,1575,1525],{"class":83},[40,1577,1493],{"class":140},[40,1579,106],{"class":79},[40,1581,1582],{"class":50},"\"Shifts\"",[40,1584,137],{"class":83},[40,1586,412],{"class":140},[40,1588,106],{"class":79},[40,1590,417],{"class":200},[40,1592,121],{"class":83},[40,1594,1595,1597,1599,1602,1604,1607,1610,1612,1615,1617],{"class":42,"line":165},[40,1596,368],{"class":200},[40,1598,428],{"class":83},[40,1600,1601],{"class":50},"\"total overtime cost:\"",[40,1603,137],{"class":83},[40,1605,1606],{"class":200},"round",[40,1608,1609],{"class":83},"(weekly[",[40,1611,898],{"class":50},[40,1613,1614],{"class":83},"].sum(), ",[40,1616,406],{"class":200},[40,1618,420],{"class":83},[10,1620,1621,1622,23],{},"The workbook gives payroll the weekly overtime and premium hours per person, managers the departmental view and the 48-hour check, and everyone the underlying shifts with the paid-hours calculation visible. When overtime is concentrated in a few people week after week, it is usually cheaper to add capacity — a part-time role, a shift change — than to keep paying premium rates, and the report gives the numbers for that decision. The weekly hours also feed the rota planning in ",[19,1623,1625],{"href":1624},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcreate-a-shift-calendar-with-rotating-patterns\u002F","Create a Shift Calendar with Rotating Patterns",[463,1627,472,1632,472,1635,472,1638,472,1641,472,1646,472,1652,472,1657,472,1661,472,1665,472,1669,472,1672,472,1677,472,1681,472,1684,472,1687],{"viewBox":1628,"role":466,"ariaLabelledBy":1629,"xmlns":470,"style":471},"4 0 752 173",[1630,1631],"ot-rules-t","ot-rules-d",[474,1633,1634],{"id":1630},"Time-and-a-half or double time",[478,1636,1637],{"id":1631},"Overtime hours on ordinary days are paid at time-and-a-half. Overtime hours on Sundays and bank holidays are paid at double time. Premium-day hours are counted within the week's overtime so the same hour is not paid twice.",[482,1639],{"x":395,"y":484,"width":485,"height":1640,"fill":487},"173",[482,1642],{"x":490,"y":491,"width":1643,"height":1644,"rx":1645,"fill":524,"stroke":515,"style":497},"351.0","141.0","14",[499,1647,1651],{"x":1648,"y":1649,"style":1650},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Time-and-a-half",[42,1653],{"x1":1654,"y1":1655,"x2":1656,"y2":1655,"stroke":515,"style":1254},"36.0","57.0","355.0",[499,1658,1660],{"x":1648,"y":1659,"style":1341},"77.0","overtime on Monday to Saturday",[499,1662,1664],{"x":1648,"y":1663,"style":1341},"103.0","rate × 1.5",[499,1666,1668],{"x":1648,"y":1667,"style":1341},"129.0","most overtime hours",[482,1670],{"x":1671,"y":491,"width":1643,"height":1644,"rx":1645,"fill":569,"stroke":570,"style":497},"389.0",[499,1673,1676],{"x":1674,"y":1649,"style":1675},"564.5","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Double time",[42,1678],{"x1":1679,"y1":1655,"x2":1680,"y2":1655,"stroke":570,"style":1254},"405.0","724.0",[499,1682,1683],{"x":1674,"y":1659,"style":1341},"overtime on Sunday or bank holiday",[499,1685,1686],{"x":1674,"y":1663,"style":1341},"rate × 2.0",[499,1688,1689],{"x":1674,"y":1667,"style":1341},"counted within weekly overtime",[25,1691,1693],{"id":1692},"common-pitfalls","Common pitfalls",[1695,1696,1697,1705,1711,1717,1723],"ul",{},[1698,1699,1700,1704],"li",{},[1701,1702,1703],"strong",{},"Comparing times of day."," Use full timestamps so overnight shifts compute correctly.",[1698,1706,1707,1710],{},[1701,1708,1709],{},"Overtime per day when the rule is per week."," Sum the week first, then compare with contract.",[1698,1712,1713,1716],{},[1701,1714,1715],{},"Paying premium hours twice."," Count premium-day hours within the week's overtime.",[1698,1718,1719,1722],{},[1701,1720,1721],{},"Ignoring missed clock-outs."," Reject implausible shift lengths before calculating.",[1698,1724,1725,1728],{},[1701,1726,1727],{},"Treating a short window as the legal test."," Use the full reference period for the 48-hour average.",[25,1730,1732],{"id":1731},"conclusion","Conclusion",[10,1734,1735],{},"An overtime report computes each shift's paid hours from full clock-in and clock-out timestamps, deducts unpaid breaks and rounds to the quarter hour, then sums by employee and week, treats hours above contract as overtime and pays Sunday and bank holiday overtime at the higher rate. Rolled up by department it gives overtime share and cost; averaged per person it flags anyone above 48 hours a week without an opt-out, early enough to act.",[25,1737,1739],{"id":1738},"frequently-asked-questions","Frequently asked questions",[10,1741,1742,1745],{},[1701,1743,1744],{},"How do I calculate hours for a shift that crosses midnight in pandas?","\nSubtract full timestamps — clock out minus clock in — rather than times of day; the date change is handled automatically.",[10,1747,1748,1751],{},[1701,1749,1750],{},"How do I calculate weekly overtime from shifts?","\nSum paid hours per employee per week and subtract the contracted weekly hours, keeping only positive values.",[10,1753,1754,1757],{},[1701,1755,1756],{},"How do I round worked time to the nearest quarter hour?","\nMultiply hours by four, round, and divide by four.",[10,1759,1760,1763],{},[1701,1761,1762],{},"What is the 48-hour average check?","\nUnder the UK Working Time Regulations, average weekly working time over the reference period, normally 17 weeks, should not exceed 48 hours unless the worker has opted out.",[25,1765,1767],{"id":1766},"related","Related",[1695,1769,1770,1776,1781,1788],{},[1698,1771,1772,1773,1775],{},"Up one level: ",[19,1774,22],{"href":21}," — finance and operations reports.",[1698,1777,1778,1780],{},[19,1779,1625],{"href":1624}," — planning the shifts.",[1698,1782,1783,1787],{},[19,1784,1786],{"href":1785},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Freconcile-payroll-to-the-general-ledger\u002F","Reconcile Payroll to the General Ledger"," — where overtime is paid.",[1698,1789,1790,1794],{},[19,1791,1793],{"href":1792},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-staff-utilisation-report-from-timesheets\u002F","Build a Staff Utilisation Report from Timesheets"," — hours from another angle.",[1796,1797,1798],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":35,"searchDepth":93,"depth":93,"links":1800},[1801,1802,1803,1804,1805,1806,1807,1808,1809],{"id":27,"depth":93,"text":28},{"id":66,"depth":93,"text":67},{"id":607,"depth":93,"text":608},{"id":981,"depth":93,"text":982},{"id":1461,"depth":93,"text":1462},{"id":1692,"depth":93,"text":1693},{"id":1731,"depth":93,"text":1732},{"id":1738,"depth":93,"text":1739},{"id":1766,"depth":93,"text":1767},"2026-10-02","Calculate overtime from clock-in and clock-out records with pandas: shifts that cross midnight, unpaid breaks, hours rounded to the quarter hour, overtime above contracted weekly hours, premium rates for Sundays and bank holidays, overtime cost by department, and a check of average weekly hours against a 48-hour limit — written to an Excel report for payroll and managers.","md",[1814,1816,1818,1820],{"q":1744,"a":1815},"Subtract full timestamps — clock out minus clock in — rather than times of day; the date change is handled automatically.",{"q":1750,"a":1817},"Sum paid hours per employee per week and subtract the contracted weekly hours, keeping only positive values.",{"q":1756,"a":1819},"Multiply hours by four, round, and divide by four.",{"q":1762,"a":1821},"Under the UK Working Time Regulations, average weekly working time over the reference period, normally 17 weeks, should not exceed 48 hours unless the worker has opted out.","overtime report shift data python",null,{"breadcrumb":1825},[1826,1828,1831],{"name":1827,"item":197},"Home",{"name":1829,"item":1830},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":1832,"item":21},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data",{"title":1835,"description":1836},"Build an Overtime Report from Shift Data with Python","Overtime from clock records with pandas: overnight shifts, unpaid breaks, quarter-hour rounding, weekly overtime above contract, Sunday and bank holiday premiums, cost by department and a 48-hour average check.","build-an-overtime-report-from-shift-data","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data\u002Findex",[1840,1841],"report-layouts","pandas","how-to","nAHj9nhtssDooICZEoUCylEmq7WOiSn0V5AoL_ouAHI",[1845,1849],{"title":1846,"path":1847,"stem":1848,"children":-1},"Build an On-Time Delivery (OTIF) Report with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-on-time-delivery-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-on-time-delivery-report\u002Findex",{"title":1850,"path":1851,"stem":1852,"children":-1},"Calculate FIFO Inventory Valuation with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-fifo-inventory-valuation-with-python","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-fifo-inventory-valuation-with-python\u002Findex",[1854,1856],{"path":1855,"title":1832},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":1857,"title":1829},"\u002Fautomating-reporting-workflows",[],[1860,1864,1867,1870,1873,1876],{"path":1861,"title":1862,"kind":1863},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":1865,"title":1866,"kind":1842},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-data-dictionary-sheet-to-an-excel-report","Add a Data Dictionary Sheet to an Excel Report",{"path":1868,"title":1869,"kind":1842},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard","Add a KPI Summary Block to an Excel Dashboard",{"path":1871,"title":1872,"kind":1842},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel","Add a Table of Contents Sheet with Hyperlinks in Excel",{"path":1874,"title":1875,"kind":1842},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-slicer-style-filters-to-an-excel-dashboard","Add a Dropdown Filter That Drives an Excel Dashboard",{"path":1877,"title":1878,"kind":1842},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python","Add a Summary Sheet to an Excel Report with Python",1790972127819]