[{"data":1,"prerenderedAt":2127},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-energy-consumption-report-from-meter-readings":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-energy-consumption-report-from-meter-readings":2092,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-energy-consumption-report-from-meter-readings":2101,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-energy-consumption-report-from-meter-readings":2106,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-energy-consumption-report-from-meter-readings":2107},{"id":4,"title":5,"body":6,"dateModified":2058,"datePublished":2058,"description":2059,"extension":2060,"faq":2061,"keyword":2070,"kind":2071,"meta":2072,"navigation":105,"path":2081,"seo":2082,"slug":2085,"stem":2086,"topics":2087,"type":2090,"__hash__":2091},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-energy-consumption-report-from-meter-readings\u002Findex.md","Build an Energy Consumption Report from Meter Readings",{"type":7,"value":8,"toc":2047},"minimark",[9,28,33,62,73,77,676,679,885,889,1160,1163,1167,1361,1364,1367,1494,1567,1571,1930,1933,1937,1972,1976,1979,1983,1992,1998,2004,2010,2014,2043],[10,11,12,13,17,18,21,22,27],"p",{},"Energy is one of the few costs most organisations still track from readings someone takes by hand: a facilities assistant reads the electricity and gas meters at each site, roughly once a month, and types the numbers into a spreadsheet. Those readings are ",[14,15,16],"em",{},"cumulative"," — the meter shows the total since it was installed — and they are taken on whatever day someone gets round to it, so they never line up with calendar months. Turning them into ",[14,19,20],{},"monthly consumption per site",", which is what budgets, invoices and sustainability reports need, takes a few steps that are easy to get wrong in Excel: subtracting consecutive readings, coping with a meter that was replaced or rolled over to zero, and spreading each interval's usage across the months it spans. This page does those steps with pandas, flags readings that look wrong, adds cost and carbon emissions, and writes a site-by-month report. It belongs to ",[23,24,26],"a",{"href":25},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[41,42,43],"code",{"__ignoreMap":39},[44,45,48,52,56,59],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," pandas",[44,60,61],{"class":54}," openpyxl\n",[10,63,64,65,68,69,72],{},"Two sheets. ",[14,66,67],{},"Meter readings"," has one row per reading: site, fuel, date and the cumulative reading in kWh. ",[14,70,71],{},"Meter changes"," records that the Hull electricity meter was replaced on 20 March 2026, with the old meter's final reading and the new meter's starting reading. The York meter is a five-digit meter that rolled over from 99,999 to zero during February. One reading — Leeds gas in early June — is suspicious.",[29,74,76],{"id":75},"step-1-from-cumulative-readings-to-consumption-per-interval","Step 1 — From cumulative readings to consumption per interval",[34,78,82],{"className":79,"code":80,"language":81,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nr = pd.read_excel(\"meter_readings.xlsx\", parse_dates=[\"Date\"]).sort_values([\"Site\", \"Fuel\", \"Date\"])\nchanges = pd.read_excel(\"meter_changes.xlsx\", parse_dates=[\"Date\"])\nROLLOVER = {\"York\": 100_000}                                  # five-digit meters wrap at 100,000\n\nr[\"Prev date\"] = r.groupby([\"Site\", \"Fuel\"])[\"Date\"].shift()\nr[\"Prev reading\"] = r.groupby([\"Site\", \"Fuel\"])[\"Reading\"].shift()\nr[\"kWh\"] = r[\"Reading\"] - r[\"Prev reading\"]\n\ndef fix(row):\n    if pd.isna(row[\"kWh\"]) or row[\"kWh\"] >= 0:\n        return row[\"kWh\"], \"\"\n    swap = changes[(changes[\"Site\"] == row[\"Site\"]) & (changes[\"Fuel\"] == row[\"Fuel\"]) &\n                   changes[\"Date\"].between(row[\"Prev date\"], row[\"Date\"])]\n    if len(swap):\n        s = swap.iloc[0]\n        return (s[\"Old meter final\"] - row[\"Prev reading\"]) + (row[\"Reading\"] - s[\"New meter start\"]), \"meter replaced\"\n    if row[\"Site\"] in ROLLOVER:\n        return row[\"kWh\"] + ROLLOVER[row[\"Site\"]], \"meter rolled over\"\n    return None, \"negative — check reading\"\n\nr[[\"kWh\", \"Note\"]] = r.apply(lambda x: pd.Series(fix(x)), axis=1)\nintervals = r.dropna(subset=[\"Prev date\"]).copy()\nprint(intervals[[\"Site\", \"Fuel\", \"Prev date\", \"Date\", \"kWh\", \"Note\"]].to_string(index=False))\n","python",[41,83,84,100,107,155,180,209,214,245,272,301,306,319,352,368,413,434,445,461,506,525,551,565,570,608,631],{"__ignoreMap":39},[44,85,86,90,94,97],{"class":46,"line":47},[44,87,89],{"class":88},"s-kum","import",[44,91,93],{"class":92},"skGVy"," pandas ",[44,95,96],{"class":88},"as",[44,98,99],{"class":92}," pd\n",[44,101,103],{"class":46,"line":102},2,[44,104,106],{"emptyLinePlaceholder":105},true,"\n",[44,108,110,113,116,119,122,125,129,131,134,137,140,143,145,148,150,152],{"class":46,"line":109},3,[44,111,112],{"class":92},"r ",[44,114,115],{"class":88},"=",[44,117,118],{"class":92}," pd.read_excel(",[44,120,121],{"class":54},"\"meter_readings.xlsx\"",[44,123,124],{"class":92},", ",[44,126,128],{"class":127},"sa561","parse_dates",[44,130,115],{"class":88},[44,132,133],{"class":92},"[",[44,135,136],{"class":54},"\"Date\"",[44,138,139],{"class":92},"]).sort_values([",[44,141,142],{"class":54},"\"Site\"",[44,144,124],{"class":92},[44,146,147],{"class":54},"\"Fuel\"",[44,149,124],{"class":92},[44,151,136],{"class":54},[44,153,154],{"class":92},"])\n",[44,156,158,161,163,165,168,170,172,174,176,178],{"class":46,"line":157},4,[44,159,160],{"class":92},"changes ",[44,162,115],{"class":88},[44,164,118],{"class":92},[44,166,167],{"class":54},"\"meter_changes.xlsx\"",[44,169,124],{"class":92},[44,171,128],{"class":127},[44,173,115],{"class":88},[44,175,133],{"class":92},[44,177,136],{"class":54},[44,179,154],{"class":92},[44,181,183,187,190,193,196,199,202,205],{"class":46,"line":182},5,[44,184,186],{"class":185},"sP0c6","ROLLOVER",[44,188,189],{"class":88}," =",[44,191,192],{"class":92}," {",[44,194,195],{"class":54},"\"York\"",[44,197,198],{"class":92},": ",[44,200,201],{"class":185},"100_000",[44,203,204],{"class":92},"}                                  ",[44,206,208],{"class":207},"s-wDw","# five-digit meters wrap at 100,000\n",[44,210,212],{"class":46,"line":211},6,[44,213,106],{"emptyLinePlaceholder":105},[44,215,217,220,223,226,228,231,233,235,237,240,242],{"class":46,"line":216},7,[44,218,219],{"class":92},"r[",[44,221,222],{"class":54},"\"Prev date\"",[44,224,225],{"class":92},"] ",[44,227,115],{"class":88},[44,229,230],{"class":92}," r.groupby([",[44,232,142],{"class":54},[44,234,124],{"class":92},[44,236,147],{"class":54},[44,238,239],{"class":92},"])[",[44,241,136],{"class":54},[44,243,244],{"class":92},"].shift()\n",[44,246,248,250,253,255,257,259,261,263,265,267,270],{"class":46,"line":247},8,[44,249,219],{"class":92},[44,251,252],{"class":54},"\"Prev reading\"",[44,254,225],{"class":92},[44,256,115],{"class":88},[44,258,230],{"class":92},[44,260,142],{"class":54},[44,262,124],{"class":92},[44,264,147],{"class":54},[44,266,239],{"class":92},[44,268,269],{"class":54},"\"Reading\"",[44,271,244],{"class":92},[44,273,275,277,280,282,284,287,289,291,294,296,298],{"class":46,"line":274},9,[44,276,219],{"class":92},[44,278,279],{"class":54},"\"kWh\"",[44,281,225],{"class":92},[44,283,115],{"class":88},[44,285,286],{"class":92}," r[",[44,288,269],{"class":54},[44,290,225],{"class":92},[44,292,293],{"class":88},"-",[44,295,286],{"class":92},[44,297,252],{"class":54},[44,299,300],{"class":92},"]\n",[44,302,304],{"class":46,"line":303},10,[44,305,106],{"emptyLinePlaceholder":105},[44,307,309,312,316],{"class":46,"line":308},11,[44,310,311],{"class":88},"def",[44,313,315],{"class":314},"s_Opv"," fix",[44,317,318],{"class":92},"(row):\n",[44,320,322,325,328,330,333,336,339,341,343,346,349],{"class":46,"line":321},12,[44,323,324],{"class":88},"    if",[44,326,327],{"class":92}," pd.isna(row[",[44,329,279],{"class":54},[44,331,332],{"class":92},"]) ",[44,334,335],{"class":88},"or",[44,337,338],{"class":92}," row[",[44,340,279],{"class":54},[44,342,225],{"class":92},[44,344,345],{"class":88},">=",[44,347,348],{"class":185}," 0",[44,350,351],{"class":92},":\n",[44,353,355,358,360,362,365],{"class":46,"line":354},13,[44,356,357],{"class":88},"        return",[44,359,338],{"class":92},[44,361,279],{"class":54},[44,363,364],{"class":92},"], ",[44,366,367],{"class":54},"\"\"\n",[44,369,371,374,376,379,381,383,386,388,390,392,395,398,400,402,404,406,408,410],{"class":46,"line":370},14,[44,372,373],{"class":92},"    swap ",[44,375,115],{"class":88},[44,377,378],{"class":92}," changes[(changes[",[44,380,142],{"class":54},[44,382,225],{"class":92},[44,384,385],{"class":88},"==",[44,387,338],{"class":92},[44,389,142],{"class":54},[44,391,332],{"class":92},[44,393,394],{"class":88},"&",[44,396,397],{"class":92}," (changes[",[44,399,147],{"class":54},[44,401,225],{"class":92},[44,403,385],{"class":88},[44,405,338],{"class":92},[44,407,147],{"class":54},[44,409,332],{"class":92},[44,411,412],{"class":88},"&\n",[44,414,416,419,421,424,426,429,431],{"class":46,"line":415},15,[44,417,418],{"class":92},"                   changes[",[44,420,136],{"class":54},[44,422,423],{"class":92},"].between(row[",[44,425,222],{"class":54},[44,427,428],{"class":92},"], row[",[44,430,136],{"class":54},[44,432,433],{"class":92},"])]\n",[44,435,437,439,442],{"class":46,"line":436},16,[44,438,324],{"class":88},[44,440,441],{"class":185}," len",[44,443,444],{"class":92},"(swap):\n",[44,446,448,451,453,456,459],{"class":46,"line":447},17,[44,449,450],{"class":92},"        s ",[44,452,115],{"class":88},[44,454,455],{"class":92}," swap.iloc[",[44,457,458],{"class":185},"0",[44,460,300],{"class":92},[44,462,464,466,469,472,474,476,478,480,482,485,488,490,492,494,497,500,503],{"class":46,"line":463},18,[44,465,357],{"class":88},[44,467,468],{"class":92}," (s[",[44,470,471],{"class":54},"\"Old meter final\"",[44,473,225],{"class":92},[44,475,293],{"class":88},[44,477,338],{"class":92},[44,479,252],{"class":54},[44,481,332],{"class":92},[44,483,484],{"class":88},"+",[44,486,487],{"class":92}," (row[",[44,489,269],{"class":54},[44,491,225],{"class":92},[44,493,293],{"class":88},[44,495,496],{"class":92}," s[",[44,498,499],{"class":54},"\"New meter start\"",[44,501,502],{"class":92},"]), ",[44,504,505],{"class":54},"\"meter replaced\"\n",[44,507,509,511,513,515,517,520,523],{"class":46,"line":508},19,[44,510,324],{"class":88},[44,512,338],{"class":92},[44,514,142],{"class":54},[44,516,225],{"class":92},[44,518,519],{"class":88},"in",[44,521,522],{"class":185}," ROLLOVER",[44,524,351],{"class":92},[44,526,528,530,532,534,536,538,540,543,545,548],{"class":46,"line":527},20,[44,529,357],{"class":88},[44,531,338],{"class":92},[44,533,279],{"class":54},[44,535,225],{"class":92},[44,537,484],{"class":88},[44,539,522],{"class":185},[44,541,542],{"class":92},"[row[",[44,544,142],{"class":54},[44,546,547],{"class":92},"]], ",[44,549,550],{"class":54},"\"meter rolled over\"\n",[44,552,554,557,560,562],{"class":46,"line":553},21,[44,555,556],{"class":88},"    return",[44,558,559],{"class":185}," None",[44,561,124],{"class":92},[44,563,564],{"class":54},"\"negative — check reading\"\n",[44,566,568],{"class":46,"line":567},22,[44,569,106],{"emptyLinePlaceholder":105},[44,571,573,576,578,580,583,586,588,591,594,597,600,602,605],{"class":46,"line":572},23,[44,574,575],{"class":92},"r[[",[44,577,279],{"class":54},[44,579,124],{"class":92},[44,581,582],{"class":54},"\"Note\"",[44,584,585],{"class":92},"]] ",[44,587,115],{"class":88},[44,589,590],{"class":92}," r.apply(",[44,592,593],{"class":88},"lambda",[44,595,596],{"class":92}," x: pd.Series(fix(x)), ",[44,598,599],{"class":127},"axis",[44,601,115],{"class":88},[44,603,604],{"class":185},"1",[44,606,607],{"class":92},")\n",[44,609,611,614,616,619,622,624,626,628],{"class":46,"line":610},24,[44,612,613],{"class":92},"intervals ",[44,615,115],{"class":88},[44,617,618],{"class":92}," r.dropna(",[44,620,621],{"class":127},"subset",[44,623,115],{"class":88},[44,625,133],{"class":92},[44,627,222],{"class":54},[44,629,630],{"class":92},"]).copy()\n",[44,632,634,637,640,642,644,646,648,650,652,654,656,658,660,662,665,668,670,673],{"class":46,"line":633},25,[44,635,636],{"class":185},"print",[44,638,639],{"class":92},"(intervals[[",[44,641,142],{"class":54},[44,643,124],{"class":92},[44,645,147],{"class":54},[44,647,124],{"class":92},[44,649,222],{"class":54},[44,651,124],{"class":92},[44,653,136],{"class":54},[44,655,124],{"class":92},[44,657,279],{"class":54},[44,659,124],{"class":92},[44,661,582],{"class":54},[44,663,664],{"class":92},"]].to_string(",[44,666,667],{"class":127},"index",[44,669,115],{"class":88},[44,671,672],{"class":185},"False",[44,674,675],{"class":92},"))\n",[10,677,678],{},"Each reading minus the previous reading for the same meter gives the energy used between the two dates. A negative difference means something happened to the meter, and there are two common explanations. If a meter change is recorded within the interval, the usage is the old meter's final reading minus the previous reading, plus the new meter's reading minus its starting value — the Hull interval spanning the replacement comes to 680 kWh — 560 on the old meter before it was removed and 120 on the new one — not minus 18,930. If the meter is known to roll over, adding the rollover value gives the true usage — York's February-to-March interval is 520 kWh, not minus 99,480. Anything else negative is left blank and flagged, because it is a misread or mistyped reading that needs checking, not something to guess at.",[680,681,689,690,689,694,689,698,689,705,689,714,689,718,689,725,689,729,689,733,689,737,689,741,689,745,689,749,689,753,689,757,689,761,689,765,689,769,689,773,689,778,689,782,689,784,689,787,689,789,689,792,689,794,689,797,689,799,689,802,689,804,689,807,689,810,689,814,689,816,689,820,689,822,689,825,689,827,689,830,689,832,689,835,689,839,689,843,689,845,689,848,689,851,689,855,689,857,689,860,689,862,689,865,689,867,689,870,689,872,689,875,689,877,689,880,689,882],"svg",{"viewBox":682,"role":683,"ariaLabelledBy":684,"xmlns":687,"style":688},"29 0 701 140","img",[685,686],"en-fixes-t","en-fixes-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:701px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[691,692,693],"title",{"id":685},"Negative differences and what they mean",[695,696,697],"desc",{"id":686},"The Hull meter was replaced on 20 March, so usage is the old meter's final reading minus the previous reading plus the new meter's reading, 560 plus 120, or 680 kWh. The York meter rolled over at 100,000, so 100,000 is added, giving 520 kWh.",[699,700],"rect",{"x":701,"y":458,"width":702,"height":703,"fill":704},"29","701","140","#ffffff",[699,706],{"x":707,"y":708,"width":709,"height":710,"rx":458,"fill":711,"stroke":712,"style":713},"46.0","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[699,715],{"x":716,"y":708,"width":717,"height":710,"rx":458,"fill":711,"stroke":712,"style":713},"78.0","118.4",[719,720,724],"text",{"x":721,"y":722,"style":723},"137.2","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[699,726],{"x":727,"y":708,"width":728,"height":710,"rx":458,"fill":711,"stroke":712,"style":713},"196.3","78.4",[719,730,732],{"x":731,"y":722,"style":723},"235.5","B",[699,734],{"x":735,"y":708,"width":736,"height":710,"rx":458,"fill":711,"stroke":712,"style":713},"274.7","71.4",[719,738,740],{"x":739,"y":722,"style":723},"310.4","C",[699,742],{"x":743,"y":708,"width":744,"height":710,"rx":458,"fill":711,"stroke":712,"style":713},"346.1","117.6",[719,746,748],{"x":747,"y":722,"style":723},"404.9","D",[699,750],{"x":751,"y":708,"width":752,"height":710,"rx":458,"fill":711,"stroke":712,"style":713},"463.7","86.3",[719,754,756],{"x":755,"y":722,"style":723},"506.9","E",[699,758],{"x":759,"y":708,"width":760,"height":710,"rx":458,"fill":711,"stroke":712,"style":713},"550.0","164.0",[719,762,764],{"x":763,"y":722,"style":723},"632.0","F",[699,766],{"x":707,"y":767,"width":709,"height":768,"rx":458,"fill":711,"stroke":712,"style":713},"40.0","28.0",[719,770,604],{"x":771,"y":772,"style":723},"62.0","59.0",[699,774],{"x":716,"y":767,"width":717,"height":768,"rx":458,"fill":775,"stroke":776,"style":777},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[719,779,781],{"x":721,"y":772,"style":780},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Meter",[699,783],{"x":727,"y":767,"width":728,"height":768,"rx":458,"fill":775,"stroke":776,"style":777},[719,785,786],{"x":731,"y":772,"style":780},"Previous",[699,788],{"x":735,"y":767,"width":736,"height":768,"rx":458,"fill":775,"stroke":776,"style":777},[719,790,791],{"x":739,"y":772,"style":780},"Current",[699,793],{"x":743,"y":767,"width":744,"height":768,"rx":458,"fill":775,"stroke":776,"style":777},[719,795,796],{"x":747,"y":772,"style":780},"Raw difference",[699,798],{"x":751,"y":767,"width":752,"height":768,"rx":458,"fill":775,"stroke":776,"style":777},[719,800,801],{"x":755,"y":772,"style":780},"Corrected",[699,803],{"x":759,"y":767,"width":760,"height":768,"rx":458,"fill":775,"stroke":776,"style":777},[719,805,806],{"x":763,"y":772,"style":780},"Why",[699,808],{"x":707,"y":809,"width":709,"height":768,"rx":458,"fill":711,"stroke":712,"style":713},"68.0",[719,811,813],{"x":771,"y":812,"style":723},"87.0","2",[699,815],{"x":716,"y":809,"width":717,"height":768,"rx":458,"fill":704,"stroke":712,"style":713},[719,817,819],{"x":721,"y":812,"style":818},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Hull electricity",[699,821],{"x":727,"y":809,"width":728,"height":768,"rx":458,"fill":704,"stroke":712,"style":713},[719,823,824],{"x":731,"y":812,"style":818},"19,050",[699,826],{"x":735,"y":809,"width":736,"height":768,"rx":458,"fill":704,"stroke":712,"style":713},[719,828,829],{"x":739,"y":812,"style":818},"120",[699,831],{"x":743,"y":809,"width":744,"height":768,"rx":458,"fill":704,"stroke":712,"style":713},[719,833,834],{"x":747,"y":812,"style":818},"−18,930",[699,836],{"x":751,"y":809,"width":752,"height":768,"rx":458,"fill":837,"stroke":838,"style":777},"#d9f4f1","var(--teal,#0f9488)",[719,840,842],{"x":755,"y":812,"style":841},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","680",[699,844],{"x":759,"y":809,"width":760,"height":768,"rx":458,"fill":704,"stroke":712,"style":713},[719,846,847],{"x":763,"y":812,"style":818},"meter replaced 20 Mar",[699,849],{"x":707,"y":850,"width":709,"height":768,"rx":458,"fill":711,"stroke":712,"style":713},"96.0",[719,852,854],{"x":771,"y":853,"style":723},"115.0","3",[699,856],{"x":716,"y":850,"width":717,"height":768,"rx":458,"fill":704,"stroke":712,"style":713},[719,858,859],{"x":721,"y":853,"style":818},"York electricity",[699,861],{"x":727,"y":850,"width":728,"height":768,"rx":458,"fill":704,"stroke":712,"style":713},[719,863,864],{"x":731,"y":853,"style":818},"99,840",[699,866],{"x":735,"y":850,"width":736,"height":768,"rx":458,"fill":704,"stroke":712,"style":713},[719,868,869],{"x":739,"y":853,"style":818},"360",[699,871],{"x":743,"y":850,"width":744,"height":768,"rx":458,"fill":704,"stroke":712,"style":713},[719,873,874],{"x":747,"y":853,"style":818},"−99,480",[699,876],{"x":751,"y":850,"width":752,"height":768,"rx":458,"fill":837,"stroke":838,"style":777},[719,878,879],{"x":755,"y":853,"style":841},"520",[699,881],{"x":759,"y":850,"width":760,"height":768,"rx":458,"fill":704,"stroke":712,"style":713},[719,883,884],{"x":763,"y":853,"style":818},"meter rolled over",[29,886,888],{"id":887},"step-2-spread-each-interval-across-calendar-months","Step 2 — Spread each interval across calendar months",[34,890,892],{"className":79,"code":891,"language":81,"meta":39,"style":39},"def by_month(row):\n    days = pd.date_range(row[\"Prev date\"], row[\"Date\"] - pd.Timedelta(days=1), freq=\"D\")\n    per_day = row[\"kWh\"] \u002F len(days)\n    return pd.Series(per_day, index=days).groupby(days.to_period(\"M\")).sum()\n\nparts = []\nfor _, row in intervals.dropna(subset=[\"kWh\"]).iterrows():\n    m = by_month(row).rename(\"kWh\").reset_index().rename(columns={\"index\": \"Month\"})\n    parts.append(m.assign(Site=row[\"Site\"], Fuel=row[\"Fuel\"]))\nmonthly = pd.concat(parts).groupby([\"Site\", \"Fuel\", \"Month\"])[\"kWh\"].sum().round(0).unstack(\"Month\")\nmonthly = monthly.loc[:, pd.Period(\"2026-01\", \"M\"):pd.Period(\"2026-05\", \"M\")]\nprint(monthly.to_string())\n",[41,893,894,903,946,967,987,991,1001,1025,1059,1088,1124,1153],{"__ignoreMap":39},[44,895,896,898,901],{"class":46,"line":47},[44,897,311],{"class":88},[44,899,900],{"class":314}," by_month",[44,902,318],{"class":92},[44,904,905,908,910,913,915,917,919,921,923,926,929,931,933,936,939,941,944],{"class":46,"line":102},[44,906,907],{"class":92},"    days ",[44,909,115],{"class":88},[44,911,912],{"class":92}," pd.date_range(row[",[44,914,222],{"class":54},[44,916,428],{"class":92},[44,918,136],{"class":54},[44,920,225],{"class":92},[44,922,293],{"class":88},[44,924,925],{"class":92}," pd.Timedelta(",[44,927,928],{"class":127},"days",[44,930,115],{"class":88},[44,932,604],{"class":185},[44,934,935],{"class":92},"), ",[44,937,938],{"class":127},"freq",[44,940,115],{"class":88},[44,942,943],{"class":54},"\"D\"",[44,945,607],{"class":92},[44,947,948,951,953,955,957,959,962,964],{"class":46,"line":109},[44,949,950],{"class":92},"    per_day ",[44,952,115],{"class":88},[44,954,338],{"class":92},[44,956,279],{"class":54},[44,958,225],{"class":92},[44,960,961],{"class":88},"\u002F",[44,963,441],{"class":185},[44,965,966],{"class":92},"(days)\n",[44,968,969,971,974,976,978,981,984],{"class":46,"line":157},[44,970,556],{"class":88},[44,972,973],{"class":92}," pd.Series(per_day, ",[44,975,667],{"class":127},[44,977,115],{"class":88},[44,979,980],{"class":92},"days).groupby(days.to_period(",[44,982,983],{"class":54},"\"M\"",[44,985,986],{"class":92},")).sum()\n",[44,988,989],{"class":46,"line":182},[44,990,106],{"emptyLinePlaceholder":105},[44,992,993,996,998],{"class":46,"line":211},[44,994,995],{"class":92},"parts ",[44,997,115],{"class":88},[44,999,1000],{"class":92}," []\n",[44,1002,1003,1006,1009,1011,1014,1016,1018,1020,1022],{"class":46,"line":216},[44,1004,1005],{"class":88},"for",[44,1007,1008],{"class":92}," _, row ",[44,1010,519],{"class":88},[44,1012,1013],{"class":92}," intervals.dropna(",[44,1015,621],{"class":127},[44,1017,115],{"class":88},[44,1019,133],{"class":92},[44,1021,279],{"class":54},[44,1023,1024],{"class":92},"]).iterrows():\n",[44,1026,1027,1030,1032,1035,1037,1040,1043,1045,1048,1051,1053,1056],{"class":46,"line":247},[44,1028,1029],{"class":92},"    m ",[44,1031,115],{"class":88},[44,1033,1034],{"class":92}," by_month(row).rename(",[44,1036,279],{"class":54},[44,1038,1039],{"class":92},").reset_index().rename(",[44,1041,1042],{"class":127},"columns",[44,1044,115],{"class":88},[44,1046,1047],{"class":92},"{",[44,1049,1050],{"class":54},"\"index\"",[44,1052,198],{"class":92},[44,1054,1055],{"class":54},"\"Month\"",[44,1057,1058],{"class":92},"})\n",[44,1060,1061,1064,1067,1069,1072,1074,1076,1079,1081,1083,1085],{"class":46,"line":274},[44,1062,1063],{"class":92},"    parts.append(m.assign(",[44,1065,1066],{"class":127},"Site",[44,1068,115],{"class":88},[44,1070,1071],{"class":92},"row[",[44,1073,142],{"class":54},[44,1075,364],{"class":92},[44,1077,1078],{"class":127},"Fuel",[44,1080,115],{"class":88},[44,1082,1071],{"class":92},[44,1084,147],{"class":54},[44,1086,1087],{"class":92},"]))\n",[44,1089,1090,1093,1095,1098,1100,1102,1104,1106,1108,1110,1112,1115,1117,1120,1122],{"class":46,"line":303},[44,1091,1092],{"class":92},"monthly ",[44,1094,115],{"class":88},[44,1096,1097],{"class":92}," pd.concat(parts).groupby([",[44,1099,142],{"class":54},[44,1101,124],{"class":92},[44,1103,147],{"class":54},[44,1105,124],{"class":92},[44,1107,1055],{"class":54},[44,1109,239],{"class":92},[44,1111,279],{"class":54},[44,1113,1114],{"class":92},"].sum().round(",[44,1116,458],{"class":185},[44,1118,1119],{"class":92},").unstack(",[44,1121,1055],{"class":54},[44,1123,607],{"class":92},[44,1125,1126,1128,1130,1133,1136,1138,1140,1143,1146,1148,1150],{"class":46,"line":308},[44,1127,1092],{"class":92},[44,1129,115],{"class":88},[44,1131,1132],{"class":92}," monthly.loc[:, pd.Period(",[44,1134,1135],{"class":54},"\"2026-01\"",[44,1137,124],{"class":92},[44,1139,983],{"class":54},[44,1141,1142],{"class":92},"):pd.Period(",[44,1144,1145],{"class":54},"\"2026-05\"",[44,1147,124],{"class":92},[44,1149,983],{"class":54},[44,1151,1152],{"class":92},")]\n",[44,1154,1155,1157],{"class":46,"line":321},[44,1156,636],{"class":185},[44,1158,1159],{"class":92},"(monthly.to_string())\n",[10,1161,1162],{},"Readings taken on the 3rd of one month and the 4th of the next cover parts of two calendar months. Spreading each interval's usage evenly over its days and summing by month apportions it fairly: an interval from 6 April to 1 May puts 25 days into April and none into May, and one from 4 February to 2 March splits 25 days in February and 1 in March. This assumes usage is even within an interval, which is reasonable for monthly readings and is the method suppliers use when they estimate. The first and last months are only partly covered by readings, so the report shows the complete months, January to May; January's figure includes only the days after the first reading, which is why sites that were read on the 2nd or 3rd look slightly low for January.",[29,1164,1166],{"id":1165},"step-3-flag-readings-that-look-wrong","Step 3 — Flag readings that look wrong",[34,1168,1170],{"className":79,"code":1169,"language":81,"meta":39,"style":39},"intervals[\"kWh per day\"] = intervals[\"kWh\"] \u002F (intervals[\"Date\"] - intervals[\"Prev date\"]).dt.days\ntypical = intervals.groupby([\"Site\", \"Fuel\"])[\"kWh per day\"].transform(\"median\")\nintervals[\"Ratio to typical\"] = (intervals[\"kWh per day\"] \u002F typical).round(2)\nsuspicious = intervals[(intervals[\"Ratio to typical\"] > 2) | (intervals[\"Ratio to typical\"] \u003C 0.4) | intervals[\"kWh\"].isna()]\nprint(suspicious[[\"Site\", \"Fuel\", \"Prev date\", \"Date\", \"kWh\", \"kWh per day\", \"Ratio to typical\"]].round(1).to_string(index=False))\n",[41,1171,1172,1209,1237,1263,1312],{"__ignoreMap":39},[44,1173,1174,1177,1180,1182,1184,1187,1189,1191,1193,1196,1198,1200,1202,1204,1206],{"class":46,"line":47},[44,1175,1176],{"class":92},"intervals[",[44,1178,1179],{"class":54},"\"kWh per day\"",[44,1181,225],{"class":92},[44,1183,115],{"class":88},[44,1185,1186],{"class":92}," intervals[",[44,1188,279],{"class":54},[44,1190,225],{"class":92},[44,1192,961],{"class":88},[44,1194,1195],{"class":92}," (intervals[",[44,1197,136],{"class":54},[44,1199,225],{"class":92},[44,1201,293],{"class":88},[44,1203,1186],{"class":92},[44,1205,222],{"class":54},[44,1207,1208],{"class":92},"]).dt.days\n",[44,1210,1211,1214,1216,1219,1221,1223,1225,1227,1229,1232,1235],{"class":46,"line":102},[44,1212,1213],{"class":92},"typical ",[44,1215,115],{"class":88},[44,1217,1218],{"class":92}," intervals.groupby([",[44,1220,142],{"class":54},[44,1222,124],{"class":92},[44,1224,147],{"class":54},[44,1226,239],{"class":92},[44,1228,1179],{"class":54},[44,1230,1231],{"class":92},"].transform(",[44,1233,1234],{"class":54},"\"median\"",[44,1236,607],{"class":92},[44,1238,1239,1241,1244,1246,1248,1250,1252,1254,1256,1259,1261],{"class":46,"line":109},[44,1240,1176],{"class":92},[44,1242,1243],{"class":54},"\"Ratio to typical\"",[44,1245,225],{"class":92},[44,1247,115],{"class":88},[44,1249,1195],{"class":92},[44,1251,1179],{"class":54},[44,1253,225],{"class":92},[44,1255,961],{"class":88},[44,1257,1258],{"class":92}," typical).round(",[44,1260,813],{"class":185},[44,1262,607],{"class":92},[44,1264,1265,1268,1270,1273,1275,1277,1280,1283,1286,1289,1291,1293,1295,1298,1301,1303,1305,1307,1309],{"class":46,"line":157},[44,1266,1267],{"class":92},"suspicious ",[44,1269,115],{"class":88},[44,1271,1272],{"class":92}," intervals[(intervals[",[44,1274,1243],{"class":54},[44,1276,225],{"class":92},[44,1278,1279],{"class":88},">",[44,1281,1282],{"class":185}," 2",[44,1284,1285],{"class":92},") ",[44,1287,1288],{"class":88},"|",[44,1290,1195],{"class":92},[44,1292,1243],{"class":54},[44,1294,225],{"class":92},[44,1296,1297],{"class":88},"\u003C",[44,1299,1300],{"class":185}," 0.4",[44,1302,1285],{"class":92},[44,1304,1288],{"class":88},[44,1306,1186],{"class":92},[44,1308,279],{"class":54},[44,1310,1311],{"class":92},"].isna()]\n",[44,1313,1314,1316,1319,1321,1323,1325,1327,1329,1331,1333,1335,1337,1339,1341,1343,1345,1348,1350,1353,1355,1357,1359],{"class":46,"line":182},[44,1315,636],{"class":185},[44,1317,1318],{"class":92},"(suspicious[[",[44,1320,142],{"class":54},[44,1322,124],{"class":92},[44,1324,147],{"class":54},[44,1326,124],{"class":92},[44,1328,222],{"class":54},[44,1330,124],{"class":92},[44,1332,136],{"class":54},[44,1334,124],{"class":92},[44,1336,279],{"class":54},[44,1338,124],{"class":92},[44,1340,1179],{"class":54},[44,1342,124],{"class":92},[44,1344,1243],{"class":54},[44,1346,1347],{"class":92},"]].round(",[44,1349,604],{"class":185},[44,1351,1352],{"class":92},").to_string(",[44,1354,667],{"class":127},[44,1356,115],{"class":88},[44,1358,672],{"class":185},[44,1360,675],{"class":92},[10,1362,1363],{},"Hand-typed readings go wrong in predictable ways — a digit transposed, a reading from the wrong meter, the gas meter read in cubic metres one month and kWh the next. Comparing each interval's daily usage with that meter's median daily usage catches most of them: anything more than twice or less than 40% of typical is listed for checking. Two Leeds gas intervals are listed. The May-to-June interval shows 327 kWh a day, two and a half times the meter's typical rate, in a month when heating demand should be falling — far more likely a misread than a real change, and worth a photograph of the meter before the figure goes into a report. The April interval is flagged for the opposite reason, at 47 kWh a day, but that one is plausible: the heating was turned down in spring. The check produces a short list for a person to judge, not a verdict, which is the right division of labour for readings typed by hand.",[10,1365,1366],{},"Notice also what the Hull replacement does to March: the 680 kWh interval covers 29 days, so March shows about half Hull's usual consumption. That is worth a second look too — a meter replaced mid-month sometimes misses a few days of readings, and the old meter's final reading is the figure to confirm with whoever swapped it.",[680,1368,689,1374,689,1377,689,1380,689,1385,689,1392,689,1398,689,1403,689,1408,689,1413,689,1416,689,1422,689,1426,689,1430,689,1434,689,1437,689,1440,689,1444,689,1447,689,1450,689,1454,689,1457,689,1462,689,1467,689,1470,689,1473,689,1477,689,1480,689,1483,689,1488,689,1491],{"viewBox":1369,"role":683,"ariaLabelledBy":1370,"xmlns":687,"style":1373},"4 0 752 108",[1371,1372],"en-pipeline-t","en-pipeline-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[691,1375,1376],{"id":1371},"From readings to the monthly report",[695,1378,1379],{"id":1372},"Consecutive readings give usage per interval, corrected for meter replacements and rollovers. Each interval is spread over its days and summed by calendar month. Intervals far from the meter's typical daily usage are flagged, and cost and emissions are added per site and month.",[699,1381],{"x":1382,"y":458,"width":1383,"height":1384,"fill":704},"4","752","108",[699,1386],{"x":1387,"y":708,"width":1388,"height":1389,"rx":1390,"fill":711,"stroke":712,"style":1391},"20.0","115.2","76.5","12","stroke-width:2px",[719,1393,1397],{"x":1394,"y":1395,"style":1396},"77.6","50.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Readings",[719,1399,1402],{"x":1394,"y":1400,"style":1401},"69.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","cumulative kWh",[46,1404],{"x1":1405,"y1":1406,"x2":1407,"y2":1406,"stroke":776,"style":1391},"140.2","54.2","162.2",[1409,1410],"polygon",{"points":1411,"fill":1412},"166.2,54.2 157.2,59.2 157.2,49.2","#5b5cf0",[699,1414],{"x":1415,"y":708,"width":1388,"height":1389,"rx":1390,"fill":775,"stroke":776,"style":1391},"171.2",[719,1417,1421],{"x":1418,"y":1419,"style":1420},"228.8","41.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Differences",[719,1423,1425],{"x":1418,"y":1424,"style":1401},"61.0","fix swaps,",[719,1427,1429],{"x":1418,"y":1428,"style":1401},"78.5","rollovers",[46,1431],{"x1":1432,"y1":1406,"x2":1433,"y2":1406,"stroke":776,"style":1391},"291.4","313.4",[1409,1435],{"points":1436,"fill":1412},"317.4,54.2 308.4,59.2 308.4,49.2",[699,1438],{"x":1439,"y":708,"width":1388,"height":1389,"rx":1390,"fill":775,"stroke":776,"style":1391},"322.4",[719,1441,1443],{"x":1442,"y":1419,"style":1420},"380.0","Spread by",[719,1445,1446],{"x":1442,"y":772,"style":1420},"day",[719,1448,1449],{"x":1442,"y":1428,"style":1401},"sum per month",[46,1451],{"x1":1452,"y1":1406,"x2":1453,"y2":1406,"stroke":776,"style":1391},"442.6","464.6",[1409,1455],{"points":1456,"fill":1412},"468.6,54.2 459.6,59.2 459.6,49.2",[699,1458],{"x":1459,"y":708,"width":1388,"height":1389,"rx":1390,"fill":1460,"stroke":1461,"style":1391},"473.6","#fdefd8","var(--gold,#b4740a)",[719,1463,1466],{"x":1464,"y":1419,"style":1465},"531.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Flag outliers",[719,1468,1469],{"x":1464,"y":1424,"style":1401},"vs typical",[719,1471,1472],{"x":1464,"y":1428,"style":1401},"kWh\u002Fday",[46,1474],{"x1":1475,"y1":1406,"x2":1476,"y2":1406,"stroke":776,"style":1391},"593.8","615.8",[1409,1478],{"points":1479,"fill":1412},"619.8,54.2 610.8,59.2 610.8,49.2",[699,1481],{"x":1482,"y":708,"width":1388,"height":1389,"rx":1390,"fill":837,"stroke":838,"style":1391},"624.8",[719,1484,1487],{"x":1485,"y":1419,"style":1486},"682.4","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Cost + CO2e",[719,1489,1490],{"x":1485,"y":1424,"style":1401},"per site and",[719,1492,1493],{"x":1485,"y":1428,"style":1401},"month",[680,1495,689,1500,689,1503,689,1506,689,1509,689,1516,689,1522,689,1527,689,1531,689,1535,689,1539,689,1543,689,1546,689,1551,689,1555,689,1558,689,1561,689,1564],{"viewBox":1496,"role":683,"ariaLabelledBy":1497,"xmlns":687,"style":1373},"4 0 752 199",[1498,1499],"en-flags-t","en-flags-d",[691,1501,1502],{"id":1498},"Flagged readings need judgement",[695,1504,1505],{"id":1499},"The Leeds gas interval from May to June runs at two and a half times the typical daily rate when heating should be falling, which points to a misread. The April interval runs at 0.4 times typical, which is plausible because the heating was turned down.",[699,1507],{"x":1382,"y":458,"width":1383,"height":1508,"fill":704},"199",[699,1510],{"x":1387,"y":708,"width":1511,"height":1512,"rx":1513,"fill":1514,"stroke":1515,"style":1391},"351.0","167.0","14","#fce9e9","var(--danger,#dc2626)",[719,1517,1521],{"x":1518,"y":1519,"style":1520},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Likely a misread",[46,1523],{"x1":1524,"y1":1525,"x2":1526,"y2":1525,"stroke":1515,"style":713},"36.0","57.0","355.0",[719,1528,1530],{"x":1518,"y":1529,"style":818},"77.0","Leeds gas, May–June",[719,1532,1534],{"x":1518,"y":1533,"style":818},"103.0","327 kWh\u002Fday, 2.5× typical",[719,1536,1538],{"x":1518,"y":1537,"style":818},"129.0","heating season ending",[719,1540,1542],{"x":1518,"y":1541,"style":818},"155.0","check the meter",[699,1544],{"x":1545,"y":708,"width":1511,"height":1512,"rx":1513,"fill":837,"stroke":838,"style":1391},"389.0",[719,1547,1550],{"x":1548,"y":1519,"style":1549},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Plausible",[46,1552],{"x1":1553,"y1":1525,"x2":1554,"y2":1525,"stroke":838,"style":713},"405.0","724.0",[719,1556,1557],{"x":1548,"y":1529,"style":818},"Leeds gas, April",[719,1559,1560],{"x":1548,"y":1533,"style":818},"47 kWh\u002Fday, 0.4× typical",[719,1562,1563],{"x":1548,"y":1537,"style":818},"heating turned down",[719,1565,1566],{"x":1548,"y":1541,"style":818},"accept with a note",[29,1568,1570],{"id":1569},"step-4-cost-emissions-and-the-report","Step 4 — Cost, emissions and the report",[34,1572,1574],{"className":79,"code":1573,"language":81,"meta":39,"style":39},"TARIFF = {\"electricity\": 0.245, \"gas\": 0.062}                 # £ per kWh\nCO2E = {\"electricity\": 0.207, \"gas\": 0.183}                   # kg CO2e per kWh — use the current official factors\nlong = monthly.stack().rename(\"kWh\").reset_index()\nlong[\"Cost £\"] = long.apply(lambda x: x[\"kWh\"] * TARIFF[x[\"Fuel\"]], axis=1).round(2)\nlong[\"kg CO2e\"] = long.apply(lambda x: x[\"kWh\"] * CO2E[x[\"Fuel\"]], axis=1).round(0)\nsummary = long.groupby([\"Site\", \"Fuel\"])[[\"kWh\", \"Cost £\", \"kg CO2e\"]].sum()\n\nwith pd.ExcelWriter(\"energy_report_2026-H1.xlsx\") as xw:\n    monthly.rename(columns=str).to_excel(xw, sheet_name=\"kWh by month\")\n    long.assign(Month=long[\"Month\"].astype(str)).to_excel(xw, sheet_name=\"Detail\", index=False)\n    summary.to_excel(xw, sheet_name=\"Totals\")\n    suspicious.to_excel(xw, sheet_name=\"Check these readings\", index=False)\nprint(summary.round(0).to_string())\n",[41,1575,1576,1609,1640,1655,1709,1757,1791,1795,1813,1838,1882,1896,1918],{"__ignoreMap":39},[44,1577,1578,1581,1583,1585,1588,1590,1593,1595,1598,1600,1603,1606],{"class":46,"line":47},[44,1579,1580],{"class":185},"TARIFF",[44,1582,189],{"class":88},[44,1584,192],{"class":92},[44,1586,1587],{"class":54},"\"electricity\"",[44,1589,198],{"class":92},[44,1591,1592],{"class":185},"0.245",[44,1594,124],{"class":92},[44,1596,1597],{"class":54},"\"gas\"",[44,1599,198],{"class":92},[44,1601,1602],{"class":185},"0.062",[44,1604,1605],{"class":92},"}                 ",[44,1607,1608],{"class":207},"# £ per kWh\n",[44,1610,1611,1614,1616,1618,1620,1622,1625,1627,1629,1631,1634,1637],{"class":46,"line":102},[44,1612,1613],{"class":185},"CO2E",[44,1615,189],{"class":88},[44,1617,192],{"class":92},[44,1619,1587],{"class":54},[44,1621,198],{"class":92},[44,1623,1624],{"class":185},"0.207",[44,1626,124],{"class":92},[44,1628,1597],{"class":54},[44,1630,198],{"class":92},[44,1632,1633],{"class":185},"0.183",[44,1635,1636],{"class":92},"}                   ",[44,1638,1639],{"class":207},"# kg CO2e per kWh — use the current official factors\n",[44,1641,1642,1645,1647,1650,1652],{"class":46,"line":109},[44,1643,1644],{"class":127},"long",[44,1646,189],{"class":88},[44,1648,1649],{"class":92}," monthly.stack().rename(",[44,1651,279],{"class":54},[44,1653,1654],{"class":92},").reset_index()\n",[44,1656,1657,1659,1661,1664,1666,1668,1671,1674,1676,1679,1681,1683,1686,1689,1692,1694,1696,1698,1700,1702,1705,1707],{"class":46,"line":157},[44,1658,1644],{"class":127},[44,1660,133],{"class":92},[44,1662,1663],{"class":54},"\"Cost £\"",[44,1665,225],{"class":92},[44,1667,115],{"class":88},[44,1669,1670],{"class":127}," long",[44,1672,1673],{"class":92},".apply(",[44,1675,593],{"class":88},[44,1677,1678],{"class":92}," x: x[",[44,1680,279],{"class":54},[44,1682,225],{"class":92},[44,1684,1685],{"class":88},"*",[44,1687,1688],{"class":185}," TARIFF",[44,1690,1691],{"class":92},"[x[",[44,1693,147],{"class":54},[44,1695,547],{"class":92},[44,1697,599],{"class":127},[44,1699,115],{"class":88},[44,1701,604],{"class":185},[44,1703,1704],{"class":92},").round(",[44,1706,813],{"class":185},[44,1708,607],{"class":92},[44,1710,1711,1713,1715,1718,1720,1722,1724,1726,1728,1730,1732,1734,1736,1739,1741,1743,1745,1747,1749,1751,1753,1755],{"class":46,"line":182},[44,1712,1644],{"class":127},[44,1714,133],{"class":92},[44,1716,1717],{"class":54},"\"kg CO2e\"",[44,1719,225],{"class":92},[44,1721,115],{"class":88},[44,1723,1670],{"class":127},[44,1725,1673],{"class":92},[44,1727,593],{"class":88},[44,1729,1678],{"class":92},[44,1731,279],{"class":54},[44,1733,225],{"class":92},[44,1735,1685],{"class":88},[44,1737,1738],{"class":185}," CO2E",[44,1740,1691],{"class":92},[44,1742,147],{"class":54},[44,1744,547],{"class":92},[44,1746,599],{"class":127},[44,1748,115],{"class":88},[44,1750,604],{"class":185},[44,1752,1704],{"class":92},[44,1754,458],{"class":185},[44,1756,607],{"class":92},[44,1758,1759,1762,1764,1766,1769,1771,1773,1775,1778,1780,1782,1784,1786,1788],{"class":46,"line":211},[44,1760,1761],{"class":92},"summary ",[44,1763,115],{"class":88},[44,1765,1670],{"class":127},[44,1767,1768],{"class":92},".groupby([",[44,1770,142],{"class":54},[44,1772,124],{"class":92},[44,1774,147],{"class":54},[44,1776,1777],{"class":92},"])[[",[44,1779,279],{"class":54},[44,1781,124],{"class":92},[44,1783,1663],{"class":54},[44,1785,124],{"class":92},[44,1787,1717],{"class":54},[44,1789,1790],{"class":92},"]].sum()\n",[44,1792,1793],{"class":46,"line":216},[44,1794,106],{"emptyLinePlaceholder":105},[44,1796,1797,1800,1803,1806,1808,1810],{"class":46,"line":247},[44,1798,1799],{"class":88},"with",[44,1801,1802],{"class":92}," pd.ExcelWriter(",[44,1804,1805],{"class":54},"\"energy_report_2026-H1.xlsx\"",[44,1807,1285],{"class":92},[44,1809,96],{"class":88},[44,1811,1812],{"class":92}," xw:\n",[44,1814,1815,1818,1820,1822,1825,1828,1831,1833,1836],{"class":46,"line":274},[44,1816,1817],{"class":92},"    monthly.rename(",[44,1819,1042],{"class":127},[44,1821,115],{"class":88},[44,1823,1824],{"class":185},"str",[44,1826,1827],{"class":92},").to_excel(xw, ",[44,1829,1830],{"class":127},"sheet_name",[44,1832,115],{"class":88},[44,1834,1835],{"class":54},"\"kWh by month\"",[44,1837,607],{"class":92},[44,1839,1840,1843,1846,1849,1851,1853,1855,1857,1860,1862,1865,1867,1869,1872,1874,1876,1878,1880],{"class":46,"line":303},[44,1841,1842],{"class":127},"    long",[44,1844,1845],{"class":92},".assign(",[44,1847,1848],{"class":127},"Month",[44,1850,115],{"class":88},[44,1852,1644],{"class":127},[44,1854,133],{"class":92},[44,1856,1055],{"class":54},[44,1858,1859],{"class":92},"].astype(",[44,1861,1824],{"class":185},[44,1863,1864],{"class":92},")).to_excel(xw, ",[44,1866,1830],{"class":127},[44,1868,115],{"class":88},[44,1870,1871],{"class":54},"\"Detail\"",[44,1873,124],{"class":92},[44,1875,667],{"class":127},[44,1877,115],{"class":88},[44,1879,672],{"class":185},[44,1881,607],{"class":92},[44,1883,1884,1887,1889,1891,1894],{"class":46,"line":308},[44,1885,1886],{"class":92},"    summary.to_excel(xw, ",[44,1888,1830],{"class":127},[44,1890,115],{"class":88},[44,1892,1893],{"class":54},"\"Totals\"",[44,1895,607],{"class":92},[44,1897,1898,1901,1903,1905,1908,1910,1912,1914,1916],{"class":46,"line":321},[44,1899,1900],{"class":92},"    suspicious.to_excel(xw, ",[44,1902,1830],{"class":127},[44,1904,115],{"class":88},[44,1906,1907],{"class":54},"\"Check these readings\"",[44,1909,124],{"class":92},[44,1911,667],{"class":127},[44,1913,115],{"class":88},[44,1915,672],{"class":185},[44,1917,607],{"class":92},[44,1919,1920,1922,1925,1927],{"class":46,"line":354},[44,1921,636],{"class":185},[44,1923,1924],{"class":92},"(summary.round(",[44,1926,458],{"class":185},[44,1928,1929],{"class":92},").to_string())\n",[10,1931,1932],{},"Consumption becomes cost by multiplying by the unit rate for each fuel, and emissions by multiplying by a conversion factor. The factors here are illustrative; for a real report use the current official factors for your country, which governments publish each year, and record which year's factors were used. Over the first five months Leeds, with both electricity and gas, accounts for most of the cost and emissions — though its gas total includes the suspicious June reading, another reason to resolve the check list first. The workbook gives the monthly kWh grid, a long-format detail table that pivots easily, totals by site and fuel, and the list of readings to check — which should be resolved before the figures are used in a budget review or a sustainability disclosure.",[29,1934,1936],{"id":1935},"common-pitfalls","Common pitfalls",[1938,1939,1940,1948,1954,1960,1966],"ul",{},[1941,1942,1943,1947],"li",{},[1944,1945,1946],"strong",{},"Treating readings as consumption."," They are cumulative; subtract the previous reading.",[1941,1949,1950,1953],{},[1944,1951,1952],{},"Negative differences used as they are."," Look for a meter change or rollover, otherwise flag the reading.",[1941,1955,1956,1959],{},[1944,1957,1958],{},"Assigning each interval to one month."," Spread usage by day across the months it covers.",[1941,1961,1962,1965],{},[1944,1963,1964],{},"Comparing months with different coverage."," Report complete months and note partial ones.",[1941,1967,1968,1971],{},[1944,1969,1970],{},"Unsourced emission factors."," Use and record the current official conversion factors.",[29,1973,1975],{"id":1974},"conclusion","Conclusion",[10,1977,1978],{},"To build an energy report from manual meter readings, subtract consecutive cumulative readings per meter, correct negative differences using recorded meter replacements or known rollover values and flag any others, spread each interval's usage evenly over its days to get calendar-month consumption, and flag intervals whose daily usage is far from the meter's typical rate. Multiplying by tariffs and emission factors gives cost and carbon per site and month, ready for a budget or sustainability report.",[29,1980,1982],{"id":1981},"frequently-asked-questions","Frequently asked questions",[10,1984,1985,1988,1989,27],{},[1944,1986,1987],{},"How do I calculate consumption from cumulative meter readings in pandas?","\nSort by meter and date and subtract the previous reading with ",[41,1990,1991],{},"groupby(...).shift()",[10,1993,1994,1997],{},[1944,1995,1996],{},"How do I handle a meter that rolled over to zero?","\nAdd the meter's rollover value, such as 100,000 for a five-digit meter, to the negative difference.",[10,1999,2000,2003],{},[1944,2001,2002],{},"How do I convert irregular meter readings to monthly usage?","\nSpread each interval's usage evenly over its days and sum the daily amounts by calendar month.",[10,2005,2006,2009],{},[1944,2007,2008],{},"How do I spot a wrong meter reading?","\nCompare each interval's usage per day with the meter's median usage per day and check intervals far above or below it.",[29,2011,2013],{"id":2012},"related","Related",[1938,2015,2016,2022,2029,2036],{},[1941,2017,2018,2019,2021],{},"Up one level: ",[23,2020,26],{"href":25}," — finance and operations reports.",[1941,2023,2024,2028],{},[23,2025,2027],{"href":2026},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-accruals-and-prepayments-schedule\u002F","Build an Accruals and Prepayments Schedule"," — the same day-based spreading.",[1941,2030,2031,2035],{},[23,2032,2034],{"href":2033},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fdetect-outliers-in-excel-data-with-pandas\u002F","Detect Outliers in Excel Data with pandas"," — outlier methods.",[1941,2037,2038,2042],{},[23,2039,2041],{"href":2040},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-fleet-mileage-and-fuel-report\u002F","Build a Fleet Mileage and Fuel Report"," — odometer readings work the same way.",[2044,2045,2046],"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}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":39,"searchDepth":102,"depth":102,"links":2048},[2049,2050,2051,2052,2053,2054,2055,2056,2057],{"id":31,"depth":102,"text":32},{"id":75,"depth":102,"text":76},{"id":887,"depth":102,"text":888},{"id":1165,"depth":102,"text":1166},{"id":1569,"depth":102,"text":1570},{"id":1935,"depth":102,"text":1936},{"id":1974,"depth":102,"text":1975},{"id":1981,"depth":102,"text":1982},{"id":2012,"depth":102,"text":2013},"2026-10-02","Turn irregular meter readings typed into a spreadsheet into monthly energy use per site with pandas: convert cumulative readings into consumption, handle a meter that was replaced and one that rolled over, spread each reading interval across calendar months by days, flag suspicious jumps, add cost from tariffs and emissions from a conversion factor, and write a site-by-month report to Excel.","md",[2062,2064,2066,2068],{"q":1987,"a":2063},"Sort by meter and date and subtract the previous reading with groupby(...).shift().",{"q":1996,"a":2065},"Add the meter's rollover value, such as 100,000 for a five-digit meter, to the negative difference.",{"q":2002,"a":2067},"Spread each interval's usage evenly over its days and sum the daily amounts by calendar month.",{"q":2008,"a":2069},"Compare each interval's usage per day with the meter's median usage per day and check intervals far above or below it.","energy consumption report meter readings python",null,{"breadcrumb":2073},[2074,2076,2079],{"name":2075,"item":961},"Home",{"name":2077,"item":2078},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2080,"item":25},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-energy-consumption-report-from-meter-readings",{"title":2083,"description":2084},"Build an Energy Consumption Report from Meter Readings in Python","Monthly energy use from meter readings with pandas: cumulative readings to consumption, meter replacements and rollovers, apportioning intervals to months by days, spike detection, cost and emissions per site.","build-an-energy-consumption-report-from-meter-readings","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-energy-consumption-report-from-meter-readings\u002Findex",[2088,2089],"report-layouts","pandas","how-to","ESCmVyZb8ulnP9dyY2ngEUxLtOSQ0oik0dXgcyKmHyk",[2093,2097],{"title":2094,"path":2095,"stem":2096,"children":-1},"Build an Employee Leave Balance Report","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-employee-leave-balance-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-employee-leave-balance-report\u002Findex",{"title":2098,"path":2099,"stem":2100,"children":-1},"Build an Intercompany Reconciliation","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-intercompany-reconciliation","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-intercompany-reconciliation\u002Findex",[2102,2104],{"path":2103,"title":2080},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2105,"title":2077},"\u002Fautomating-reporting-workflows",[],[2108,2112,2115,2118,2121,2124],{"path":2109,"title":2110,"kind":2111},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2113,"title":2114,"kind":2090},"\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":2116,"title":2117,"kind":2090},"\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":2119,"title":2120,"kind":2090},"\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":2122,"title":2123,"kind":2090},"\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":2125,"title":2126,"kind":2090},"\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",1790972127691]