[{"data":1,"prerenderedAt":1928},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-that-totals-in-a-report-add-up":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-that-totals-in-a-report-add-up":1894,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-that-totals-in-a-report-add-up":1903,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-that-totals-in-a-report-add-up":1908,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-that-totals-in-a-report-add-up":1909},{"id":4,"title":5,"body":6,"dateModified":1860,"datePublished":1860,"description":1861,"extension":1862,"faq":1863,"keyword":1872,"kind":1873,"meta":1874,"navigation":126,"path":1883,"seo":1884,"slug":1887,"stem":1888,"topics":1889,"type":1892,"__hash__":1893},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-that-totals-in-a-report-add-up\u002Findex.md","Check That Totals in a Report Add Up",{"type":7,"value":8,"toc":1848},"minimark",[9,24,29,57,80,84,330,344,348,877,884,887,1027,1031,1314,1321,1521,1592,1596,1706,1720,1724,1736,1740,1778,1782,1785,1789,1795,1801,1807,1813,1817,1844],[10,11,12,13,17,18,23],"p",{},"The most embarrassing error in a report is a total that does not add up. A reader runs a calculator down a column, gets a different number, and from then on trusts nothing in the document. It happens more often than it should: a total typed over a formula during a late correction, a row added below a ",[14,15,16],"code",{},"SUM"," range, a summary sheet copied from last month's file, a rounding difference that grew. Checking every total by hand is exactly the kind of job people skip under deadline pressure, and exactly the kind a script does well. This page builds a checker that reads a finished report workbook, recomputes every total row, total column and grand total, compares the summary sheet with the detail it summarises, accepts differences within a rounding tolerance, and lists each failure with its sheet and cell so it can be found in seconds. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","Validating Excel Data with Python",".",[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\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51,54],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," pandas",[39,55,56],{"class":49}," openpyxl\n",[10,58,59,60,64,65,68,69,71,72,75,76,79],{},"The report under test has a detail sheet, ",[61,62,63],"em",{},"By region",", with three quarters per region, a ",[61,66,67],{},"Total"," column on the right and a ",[61,70,67],{}," row at the bottom, and a ",[61,73,74],{},"Summary"," sheet quoting a few figures from it. The workbook holds values, as a finished report sent to readers usually does; for a workbook with live formulas, read it with ",[14,77,78],{},"data_only=True"," after it has been calculated, or recalculate it with LibreOffice first.",[25,81,83],{"id":82},"step-1-read-the-table-with-cell-addresses-kept","Step 1 — Read the table with cell addresses kept",[30,85,89],{"className":86,"code":87,"language":88,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\nfrom openpyxl import load_workbook\n\nwb = load_workbook(\"quarterly_report.xlsx\", data_only=True)\nws = wb[\"By region\"]\nheader = [c.value for c in ws[1]]\nrows = [[c for c in r] for r in ws.iter_rows(min_row=2)]\ntable = pd.DataFrame([[c.value for c in r] for r in rows], columns=header).set_index(\"Region\")\naddr = pd.DataFrame([[c.coordinate for c in r] for r in rows], columns=header).set_index(pd.Index(table.index))\nprint(table.to_string())\n","python",[14,90,91,107,121,128,159,176,205,246,287,321],{"__ignoreMap":35},[39,92,93,97,101,104],{"class":41,"line":42},[39,94,96],{"class":95},"s-kum","import",[39,98,100],{"class":99},"skGVy"," pandas ",[39,102,103],{"class":95},"as",[39,105,106],{"class":99}," pd\n",[39,108,110,113,116,118],{"class":41,"line":109},2,[39,111,112],{"class":95},"from",[39,114,115],{"class":99}," openpyxl ",[39,117,96],{"class":95},[39,119,120],{"class":99}," load_workbook\n",[39,122,124],{"class":41,"line":123},3,[39,125,127],{"emptyLinePlaceholder":126},true,"\n",[39,129,131,134,137,140,143,146,150,152,156],{"class":41,"line":130},4,[39,132,133],{"class":99},"wb ",[39,135,136],{"class":95},"=",[39,138,139],{"class":99}," load_workbook(",[39,141,142],{"class":49},"\"quarterly_report.xlsx\"",[39,144,145],{"class":99},", ",[39,147,149],{"class":148},"sa561","data_only",[39,151,136],{"class":95},[39,153,155],{"class":154},"sP0c6","True",[39,157,158],{"class":99},")\n",[39,160,162,165,167,170,173],{"class":41,"line":161},5,[39,163,164],{"class":99},"ws ",[39,166,136],{"class":95},[39,168,169],{"class":99}," wb[",[39,171,172],{"class":49},"\"By region\"",[39,174,175],{"class":99},"]\n",[39,177,179,182,184,187,190,193,196,199,202],{"class":41,"line":178},6,[39,180,181],{"class":99},"header ",[39,183,136],{"class":95},[39,185,186],{"class":99}," [c.value ",[39,188,189],{"class":95},"for",[39,191,192],{"class":99}," c ",[39,194,195],{"class":95},"in",[39,197,198],{"class":99}," ws[",[39,200,201],{"class":154},"1",[39,203,204],{"class":99},"]]\n",[39,206,208,211,213,216,218,220,222,225,227,230,232,235,238,240,243],{"class":41,"line":207},7,[39,209,210],{"class":99},"rows ",[39,212,136],{"class":95},[39,214,215],{"class":99}," [[c ",[39,217,189],{"class":95},[39,219,192],{"class":99},[39,221,195],{"class":95},[39,223,224],{"class":99}," r] ",[39,226,189],{"class":95},[39,228,229],{"class":99}," r ",[39,231,195],{"class":95},[39,233,234],{"class":99}," ws.iter_rows(",[39,236,237],{"class":148},"min_row",[39,239,136],{"class":95},[39,241,242],{"class":154},"2",[39,244,245],{"class":99},")]\n",[39,247,249,252,254,257,259,261,263,265,267,269,271,274,277,279,282,285],{"class":41,"line":248},8,[39,250,251],{"class":99},"table ",[39,253,136],{"class":95},[39,255,256],{"class":99}," pd.DataFrame([[c.value ",[39,258,189],{"class":95},[39,260,192],{"class":99},[39,262,195],{"class":95},[39,264,224],{"class":99},[39,266,189],{"class":95},[39,268,229],{"class":99},[39,270,195],{"class":95},[39,272,273],{"class":99}," rows], ",[39,275,276],{"class":148},"columns",[39,278,136],{"class":95},[39,280,281],{"class":99},"header).set_index(",[39,283,284],{"class":49},"\"Region\"",[39,286,158],{"class":99},[39,288,290,293,295,298,300,302,304,306,308,310,312,314,316,318],{"class":41,"line":289},9,[39,291,292],{"class":99},"addr ",[39,294,136],{"class":95},[39,296,297],{"class":99}," pd.DataFrame([[c.coordinate ",[39,299,189],{"class":95},[39,301,192],{"class":99},[39,303,195],{"class":95},[39,305,224],{"class":99},[39,307,189],{"class":95},[39,309,229],{"class":99},[39,311,195],{"class":95},[39,313,273],{"class":99},[39,315,276],{"class":148},[39,317,136],{"class":95},[39,319,320],{"class":99},"header).set_index(pd.Index(table.index))\n",[39,322,324,327],{"class":41,"line":323},10,[39,325,326],{"class":154},"print",[39,328,329],{"class":99},"(table.to_string())\n",[10,331,332,333,336,337,340,341,343],{},"The checker needs both the values and where they live, so a failure can be reported as ",[61,334,335],{},"E3"," rather than ",[61,338,339],{},"row 2, column 4",". Reading the sheet with openpyxl gives cell objects that carry both; two parallel DataFrames — one of values, one of cell addresses, with the same index and columns — keep them together through every calculation that follows. ",[14,342,78],{}," returns calculated results rather than formula text, which is what a check of a finished report needs.",[25,345,347],{"id":346},"step-2-total-rows-total-columns-and-the-grand-total","Step 2 — Total rows, total columns and the grand total",[30,349,351],{"className":86,"code":350,"language":88,"meta":35,"style":35},"TOL = 0.015                                         # accept differences up to 1.5p from rounding\ndetail = table.drop(index=\"Total\")\nquarters = [\"Q1\", \"Q2\", \"Q3\"]\nfailures = []\n\ndef check(where, expected, shown, label):\n    if shown is None or pd.isna(shown):\n        failures.append({\"Cell\": where, \"Check\": label, \"Shown\": None, \"Expected\": round(expected, 2), \"Difference\": None})\n    elif abs(shown - expected) > TOL:\n        failures.append({\"Cell\": where, \"Check\": label, \"Shown\": shown, \"Expected\": round(expected, 2),\n                         \"Difference\": round(shown - expected, 2)})\n\nfor region, row in detail.iterrows():                       # each region's Total column\n    check(f\"'By region'!{addr.loc[region, 'Total']}\", row[quarters].sum(), row[\"Total\"], f\"{region}: row total\")\nfor q in quarters:                                          # each quarter's Total row\n    check(f\"'By region'!{addr.loc['Total', q]}\", detail[q].sum(), table.loc[\"Total\", q], f\"{q}: column total\")\ngrand = table.loc[\"Total\", \"Total\"]\ncheck(f\"'By region'!{addr.loc['Total', 'Total']}\", detail[quarters].to_numpy().sum(), grand, \"grand total vs all cells\")\ncheck(f\"'By region'!{addr.loc['Total', 'Total']}\", table.loc[\"Total\", quarters].sum(), grand, \"grand total vs total row\")\ncheck(f\"'By region'!{addr.loc['Total', 'Total']}\", detail[\"Total\"].sum(), grand, \"grand total vs total column\")\nprint(pd.DataFrame(failures).to_string(index=False))\n",[14,352,353,368,388,413,423,427,439,459,513,539,569,591,596,612,667,683,730,749,783,821,859],{"__ignoreMap":35},[39,354,355,358,361,364],{"class":41,"line":42},[39,356,357],{"class":154},"TOL",[39,359,360],{"class":95}," =",[39,362,363],{"class":154}," 0.015",[39,365,367],{"class":366},"s-wDw","                                         # accept differences up to 1.5p from rounding\n",[39,369,370,373,375,378,381,383,386],{"class":41,"line":109},[39,371,372],{"class":99},"detail ",[39,374,136],{"class":95},[39,376,377],{"class":99}," table.drop(",[39,379,380],{"class":148},"index",[39,382,136],{"class":95},[39,384,385],{"class":49},"\"Total\"",[39,387,158],{"class":99},[39,389,390,393,395,398,401,403,406,408,411],{"class":41,"line":123},[39,391,392],{"class":99},"quarters ",[39,394,136],{"class":95},[39,396,397],{"class":99}," [",[39,399,400],{"class":49},"\"Q1\"",[39,402,145],{"class":99},[39,404,405],{"class":49},"\"Q2\"",[39,407,145],{"class":99},[39,409,410],{"class":49},"\"Q3\"",[39,412,175],{"class":99},[39,414,415,418,420],{"class":41,"line":130},[39,416,417],{"class":99},"failures ",[39,419,136],{"class":95},[39,421,422],{"class":99}," []\n",[39,424,425],{"class":41,"line":161},[39,426,127],{"emptyLinePlaceholder":126},[39,428,429,432,436],{"class":41,"line":178},[39,430,431],{"class":95},"def",[39,433,435],{"class":434},"s_Opv"," check",[39,437,438],{"class":99},"(where, expected, shown, label):\n",[39,440,441,444,447,450,453,456],{"class":41,"line":207},[39,442,443],{"class":95},"    if",[39,445,446],{"class":99}," shown ",[39,448,449],{"class":95},"is",[39,451,452],{"class":154}," None",[39,454,455],{"class":95}," or",[39,457,458],{"class":99}," pd.isna(shown):\n",[39,460,461,464,467,470,473,476,479,482,485,487,490,492,495,498,500,503,506,508,510],{"class":41,"line":248},[39,462,463],{"class":99},"        failures.append({",[39,465,466],{"class":49},"\"Cell\"",[39,468,469],{"class":99},": where, ",[39,471,472],{"class":49},"\"Check\"",[39,474,475],{"class":99},": label, ",[39,477,478],{"class":49},"\"Shown\"",[39,480,481],{"class":99},": ",[39,483,484],{"class":154},"None",[39,486,145],{"class":99},[39,488,489],{"class":49},"\"Expected\"",[39,491,481],{"class":99},[39,493,494],{"class":154},"round",[39,496,497],{"class":99},"(expected, ",[39,499,242],{"class":154},[39,501,502],{"class":99},"), ",[39,504,505],{"class":49},"\"Difference\"",[39,507,481],{"class":99},[39,509,484],{"class":154},[39,511,512],{"class":99},"})\n",[39,514,515,518,521,524,527,530,533,536],{"class":41,"line":289},[39,516,517],{"class":95},"    elif",[39,519,520],{"class":154}," abs",[39,522,523],{"class":99},"(shown ",[39,525,526],{"class":95},"-",[39,528,529],{"class":99}," expected) ",[39,531,532],{"class":95},">",[39,534,535],{"class":154}," TOL",[39,537,538],{"class":99},":\n",[39,540,541,543,545,547,549,551,553,556,558,560,562,564,566],{"class":41,"line":323},[39,542,463],{"class":99},[39,544,466],{"class":49},[39,546,469],{"class":99},[39,548,472],{"class":49},[39,550,475],{"class":99},[39,552,478],{"class":49},[39,554,555],{"class":99},": shown, ",[39,557,489],{"class":49},[39,559,481],{"class":99},[39,561,494],{"class":154},[39,563,497],{"class":99},[39,565,242],{"class":154},[39,567,568],{"class":99},"),\n",[39,570,572,575,577,579,581,583,586,588],{"class":41,"line":571},11,[39,573,574],{"class":49},"                         \"Difference\"",[39,576,481],{"class":99},[39,578,494],{"class":154},[39,580,523],{"class":99},[39,582,526],{"class":95},[39,584,585],{"class":99}," expected, ",[39,587,242],{"class":154},[39,589,590],{"class":99},")})\n",[39,592,594],{"class":41,"line":593},12,[39,595,127],{"emptyLinePlaceholder":126},[39,597,599,601,604,606,609],{"class":41,"line":598},13,[39,600,189],{"class":95},[39,602,603],{"class":99}," region, row ",[39,605,195],{"class":95},[39,607,608],{"class":99}," detail.iterrows():                       ",[39,610,611],{"class":366},"# each region's Total column\n",[39,613,615,618,621,624,628,631,634,637,640,643,646,648,651,653,655,657,660,662,665],{"class":41,"line":614},14,[39,616,617],{"class":99},"    check(",[39,619,620],{"class":95},"f",[39,622,623],{"class":49},"\"'By region'!",[39,625,627],{"class":626},"sSjpA","{",[39,629,630],{"class":99},"addr.loc[region, ",[39,632,633],{"class":49},"'Total'",[39,635,636],{"class":99},"]",[39,638,639],{"class":626},"}",[39,641,642],{"class":49},"\"",[39,644,645],{"class":99},", row[quarters].sum(), row[",[39,647,385],{"class":49},[39,649,650],{"class":99},"], ",[39,652,620],{"class":95},[39,654,642],{"class":49},[39,656,627],{"class":626},[39,658,659],{"class":99},"region",[39,661,639],{"class":626},[39,663,664],{"class":49},": row total\"",[39,666,158],{"class":99},[39,668,670,672,675,677,680],{"class":41,"line":669},15,[39,671,189],{"class":95},[39,673,674],{"class":99}," q ",[39,676,195],{"class":95},[39,678,679],{"class":99}," quarters:                                          ",[39,681,682],{"class":366},"# each quarter's Total row\n",[39,684,686,688,690,692,694,697,699,702,704,706,709,711,714,716,718,720,723,725,728],{"class":41,"line":685},16,[39,687,617],{"class":99},[39,689,620],{"class":95},[39,691,623],{"class":49},[39,693,627],{"class":626},[39,695,696],{"class":99},"addr.loc[",[39,698,633],{"class":49},[39,700,701],{"class":99},", q]",[39,703,639],{"class":626},[39,705,642],{"class":49},[39,707,708],{"class":99},", detail[q].sum(), table.loc[",[39,710,385],{"class":49},[39,712,713],{"class":99},", q], ",[39,715,620],{"class":95},[39,717,642],{"class":49},[39,719,627],{"class":626},[39,721,722],{"class":99},"q",[39,724,639],{"class":626},[39,726,727],{"class":49},": column total\"",[39,729,158],{"class":99},[39,731,733,736,738,741,743,745,747],{"class":41,"line":732},17,[39,734,735],{"class":99},"grand ",[39,737,136],{"class":95},[39,739,740],{"class":99}," table.loc[",[39,742,385],{"class":49},[39,744,145],{"class":99},[39,746,385],{"class":49},[39,748,175],{"class":99},[39,750,752,755,757,759,761,763,765,767,769,771,773,775,778,781],{"class":41,"line":751},18,[39,753,754],{"class":99},"check(",[39,756,620],{"class":95},[39,758,623],{"class":49},[39,760,627],{"class":626},[39,762,696],{"class":99},[39,764,633],{"class":49},[39,766,145],{"class":99},[39,768,633],{"class":49},[39,770,636],{"class":99},[39,772,639],{"class":626},[39,774,642],{"class":49},[39,776,777],{"class":99},", detail[quarters].to_numpy().sum(), grand, ",[39,779,780],{"class":49},"\"grand total vs all cells\"",[39,782,158],{"class":99},[39,784,786,788,790,792,794,796,798,800,802,804,806,808,811,813,816,819],{"class":41,"line":785},19,[39,787,754],{"class":99},[39,789,620],{"class":95},[39,791,623],{"class":49},[39,793,627],{"class":626},[39,795,696],{"class":99},[39,797,633],{"class":49},[39,799,145],{"class":99},[39,801,633],{"class":49},[39,803,636],{"class":99},[39,805,639],{"class":626},[39,807,642],{"class":49},[39,809,810],{"class":99},", table.loc[",[39,812,385],{"class":49},[39,814,815],{"class":99},", quarters].sum(), grand, ",[39,817,818],{"class":49},"\"grand total vs total row\"",[39,820,158],{"class":99},[39,822,824,826,828,830,832,834,836,838,840,842,844,846,849,851,854,857],{"class":41,"line":823},20,[39,825,754],{"class":99},[39,827,620],{"class":95},[39,829,623],{"class":49},[39,831,627],{"class":626},[39,833,696],{"class":99},[39,835,633],{"class":49},[39,837,145],{"class":99},[39,839,633],{"class":49},[39,841,636],{"class":99},[39,843,639],{"class":626},[39,845,642],{"class":49},[39,847,848],{"class":99},", detail[",[39,850,385],{"class":49},[39,852,853],{"class":99},"].sum(), grand, ",[39,855,856],{"class":49},"\"grand total vs total column\"",[39,858,158],{"class":99},[39,860,862,864,867,869,871,874],{"class":41,"line":861},21,[39,863,326],{"class":154},[39,865,866],{"class":99},"(pd.DataFrame(failures).to_string(",[39,868,380],{"class":148},[39,870,136],{"class":95},[39,872,873],{"class":154},"False",[39,875,876],{"class":99},"))\n",[10,878,879,880,883],{},"Each check recomputes a figure from the cells it summarises and compares it with the figure the report shows. Row totals are the sum across the quarters, column totals the sum down the regions. The grand total gets three checks — against the sum of every detail cell, against the sum of the total row and against the sum of the total column — because a report can be internally inconsistent in ways that only one of them catches. Accountants call this ",[61,881,882],{},"cross-footing",". Differences up to a penny and a half pass, because figures rounded to the penny legitimately disagree by a penny when summed; anything larger is a failure. A missing total is reported too.",[10,885,886],{},"The run finds two problems. South's row total shows 311.50 against a computed 311.55 — the kind of five-pence slip left by a total typed over a formula. The Q2 column total is a penny out, which the tolerance accepts. And because South's row total is wrong, the grand total no longer agrees with the sum of the total column, even though it agrees with the detail cells — the cross-footing check pinpoints that it is a row total, not the grand total, that is wrong.",[888,889,897,898,897,902,897,906,897,914,897,924,897,931,897,936,897,941,897,946,897,949,897,954,897,958,897,962,897,966,897,969,897,974,897,979,897,982,897,985,897,989,897,992,897,995,897,998,897,1001,897,1004,897,1008,897,1011,897,1016,897,1021,897,1024],"svg",{"viewBox":890,"role":891,"ariaLabelledBy":892,"xmlns":895,"style":896},"4 0 752 108","img",[893,894],"tot-crossfoot-t","tot-crossfoot-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  ",[899,900,901],"title",{"id":893},"Cross-footing a table",[903,904,905],"desc",{"id":894},"Each row total is checked against the sum across its row, and each column total against the sum down its column. The grand total is then checked three ways, against all detail cells, against the total row and against the total column, so an error in any one total shows up.",[907,908],"rect",{"x":909,"y":910,"width":911,"height":912,"fill":913},"4","0","752","108","#ffffff",[907,915],{"x":916,"y":917,"width":918,"height":919,"rx":920,"fill":921,"stroke":922,"style":923},"20.0","16.0","115.2","76.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[925,926,930],"text",{"x":927,"y":928,"style":929},"77.6","50.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Row totals",[925,932,935],{"x":927,"y":933,"style":934},"69.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","sum across",[41,937],{"x1":938,"y1":939,"x2":940,"y2":939,"stroke":922,"style":923},"140.2","54.2","162.2",[942,943],"polygon",{"points":944,"fill":945},"166.2,54.2 157.2,59.2 157.2,49.2","#5b5cf0",[907,947],{"x":948,"y":917,"width":918,"height":919,"rx":920,"fill":921,"stroke":922,"style":923},"171.2",[925,950,953],{"x":951,"y":952,"style":929},"228.8","41.5","Column",[925,955,957],{"x":951,"y":956,"style":929},"59.0","totals",[925,959,961],{"x":951,"y":960,"style":934},"78.5","sum down",[41,963],{"x1":964,"y1":939,"x2":965,"y2":939,"stroke":922,"style":923},"291.4","313.4",[942,967],{"points":968,"fill":945},"317.4,54.2 308.4,59.2 308.4,49.2",[907,970],{"x":971,"y":917,"width":918,"height":919,"rx":920,"fill":972,"stroke":973,"style":923},"322.4","#fdefd8","var(--gold,#b4740a)",[925,975,978],{"x":976,"y":952,"style":977},"380.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Grand total",[925,980,981],{"x":976,"y":956,"style":977},"vs detail",[925,983,984],{"x":976,"y":960,"style":934},"all cells",[41,986],{"x1":987,"y1":939,"x2":988,"y2":939,"stroke":922,"style":923},"442.6","464.6",[942,990],{"points":991,"fill":945},"468.6,54.2 459.6,59.2 459.6,49.2",[907,993],{"x":994,"y":917,"width":918,"height":919,"rx":920,"fill":972,"stroke":973,"style":923},"473.6",[925,996,978],{"x":997,"y":952,"style":977},"531.2",[925,999,1000],{"x":997,"y":956,"style":977},"vs totals",[925,1002,1003],{"x":997,"y":960,"style":934},"row and column",[41,1005],{"x1":1006,"y1":939,"x2":1007,"y2":939,"stroke":922,"style":923},"593.8","615.8",[942,1009],{"points":1010,"fill":945},"619.8,54.2 610.8,59.2 610.8,49.2",[907,1012],{"x":1013,"y":917,"width":918,"height":919,"rx":920,"fill":1014,"stroke":1015,"style":923},"624.8","#d9f4f1","var(--teal,#0f9488)",[925,1017,1020],{"x":1018,"y":952,"style":1019},"682.4","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Within",[925,1022,1023],{"x":1018,"y":956,"style":1019},"tolerance?",[925,1025,1026],{"x":1018,"y":960,"style":934},"≤ 1.5p passes",[25,1028,1030],{"id":1029},"step-3-the-summary-sheet-against-the-detail","Step 3 — The summary sheet against the detail",[30,1032,1034],{"className":86,"code":1033,"language":88,"meta":35,"style":35},"summary = {r[0].value: (r[1].value, r[1].coordinate) for r in wb[\"Summary\"].iter_rows(min_row=2)}\nexpected = {\"Revenue Q1–Q3\": detail[quarters].to_numpy().sum(),\n            \"North Q3\": detail.loc[\"North\", \"Q3\"]}\nfor measure, value in expected.items():\n    shown, cell = summary[measure]\n    check(f\"'Summary'!{cell}\", value, shown, f\"summary: {measure}\")\nbest = detail[\"Total\"].idxmax()\nif summary[\"Best region\"][0] != best:\n    failures.append({\"Cell\": f\"'Summary'!{summary['Best region'][1]}\", \"Check\": \"summary: best region\",\n                     \"Shown\": summary[\"Best region\"][0], \"Expected\": best, \"Difference\": None})\nreport = pd.DataFrame(failures)\nprint(report.to_string(index=False))\n",[14,1035,1036,1084,1100,1118,1130,1140,1177,1192,1217,1260,1289,1299],{"__ignoreMap":35},[39,1037,1038,1041,1043,1046,1048,1051,1053,1056,1058,1061,1063,1065,1067,1069,1072,1075,1077,1079,1081],{"class":41,"line":42},[39,1039,1040],{"class":99},"summary ",[39,1042,136],{"class":95},[39,1044,1045],{"class":99}," {r[",[39,1047,910],{"class":154},[39,1049,1050],{"class":99},"].value: (r[",[39,1052,201],{"class":154},[39,1054,1055],{"class":99},"].value, r[",[39,1057,201],{"class":154},[39,1059,1060],{"class":99},"].coordinate) ",[39,1062,189],{"class":95},[39,1064,229],{"class":99},[39,1066,195],{"class":95},[39,1068,169],{"class":99},[39,1070,1071],{"class":49},"\"Summary\"",[39,1073,1074],{"class":99},"].iter_rows(",[39,1076,237],{"class":148},[39,1078,136],{"class":95},[39,1080,242],{"class":154},[39,1082,1083],{"class":99},")}\n",[39,1085,1086,1089,1091,1094,1097],{"class":41,"line":109},[39,1087,1088],{"class":99},"expected ",[39,1090,136],{"class":95},[39,1092,1093],{"class":99}," {",[39,1095,1096],{"class":49},"\"Revenue Q1–Q3\"",[39,1098,1099],{"class":99},": detail[quarters].to_numpy().sum(),\n",[39,1101,1102,1105,1108,1111,1113,1115],{"class":41,"line":123},[39,1103,1104],{"class":49},"            \"North Q3\"",[39,1106,1107],{"class":99},": detail.loc[",[39,1109,1110],{"class":49},"\"North\"",[39,1112,145],{"class":99},[39,1114,410],{"class":49},[39,1116,1117],{"class":99},"]}\n",[39,1119,1120,1122,1125,1127],{"class":41,"line":130},[39,1121,189],{"class":95},[39,1123,1124],{"class":99}," measure, value ",[39,1126,195],{"class":95},[39,1128,1129],{"class":99}," expected.items():\n",[39,1131,1132,1135,1137],{"class":41,"line":161},[39,1133,1134],{"class":99},"    shown, cell ",[39,1136,136],{"class":95},[39,1138,1139],{"class":99}," summary[measure]\n",[39,1141,1142,1144,1146,1149,1151,1154,1156,1158,1161,1163,1166,1168,1171,1173,1175],{"class":41,"line":178},[39,1143,617],{"class":99},[39,1145,620],{"class":95},[39,1147,1148],{"class":49},"\"'Summary'!",[39,1150,627],{"class":626},[39,1152,1153],{"class":99},"cell",[39,1155,639],{"class":626},[39,1157,642],{"class":49},[39,1159,1160],{"class":99},", value, shown, ",[39,1162,620],{"class":95},[39,1164,1165],{"class":49},"\"summary: ",[39,1167,627],{"class":626},[39,1169,1170],{"class":99},"measure",[39,1172,639],{"class":626},[39,1174,642],{"class":49},[39,1176,158],{"class":99},[39,1178,1179,1182,1184,1187,1189],{"class":41,"line":207},[39,1180,1181],{"class":99},"best ",[39,1183,136],{"class":95},[39,1185,1186],{"class":99}," detail[",[39,1188,385],{"class":49},[39,1190,1191],{"class":99},"].idxmax()\n",[39,1193,1194,1197,1200,1203,1206,1208,1211,1214],{"class":41,"line":248},[39,1195,1196],{"class":95},"if",[39,1198,1199],{"class":99}," summary[",[39,1201,1202],{"class":49},"\"Best region\"",[39,1204,1205],{"class":99},"][",[39,1207,910],{"class":154},[39,1209,1210],{"class":99},"] ",[39,1212,1213],{"class":95},"!=",[39,1215,1216],{"class":99}," best:\n",[39,1218,1219,1222,1224,1226,1228,1230,1232,1235,1238,1240,1242,1244,1246,1248,1250,1252,1254,1257],{"class":41,"line":289},[39,1220,1221],{"class":99},"    failures.append({",[39,1223,466],{"class":49},[39,1225,481],{"class":99},[39,1227,620],{"class":95},[39,1229,1148],{"class":49},[39,1231,627],{"class":626},[39,1233,1234],{"class":99},"summary[",[39,1236,1237],{"class":49},"'Best region'",[39,1239,1205],{"class":99},[39,1241,201],{"class":154},[39,1243,636],{"class":99},[39,1245,639],{"class":626},[39,1247,642],{"class":49},[39,1249,145],{"class":99},[39,1251,472],{"class":49},[39,1253,481],{"class":99},[39,1255,1256],{"class":49},"\"summary: best region\"",[39,1258,1259],{"class":99},",\n",[39,1261,1262,1265,1268,1270,1272,1274,1276,1278,1281,1283,1285,1287],{"class":41,"line":323},[39,1263,1264],{"class":49},"                     \"Shown\"",[39,1266,1267],{"class":99},": summary[",[39,1269,1202],{"class":49},[39,1271,1205],{"class":99},[39,1273,910],{"class":154},[39,1275,650],{"class":99},[39,1277,489],{"class":49},[39,1279,1280],{"class":99},": best, ",[39,1282,505],{"class":49},[39,1284,481],{"class":99},[39,1286,484],{"class":154},[39,1288,512],{"class":99},[39,1290,1291,1294,1296],{"class":41,"line":571},[39,1292,1293],{"class":99},"report ",[39,1295,136],{"class":95},[39,1297,1298],{"class":99}," pd.DataFrame(failures)\n",[39,1300,1301,1303,1306,1308,1310,1312],{"class":41,"line":593},[39,1302,326],{"class":154},[39,1304,1305],{"class":99},"(report.to_string(",[39,1307,380],{"class":148},[39,1309,136],{"class":95},[39,1311,873],{"class":154},[39,1313,876],{"class":99},[10,1315,1316,1317,1320],{},"Summary sheets are where stale numbers hide: a figure copied by hand from the detail, or left over from last month's file. Each summary figure is recomputed from the detail and compared in the same way, including non-numeric facts such as which region was best. The summary's revenue figure is the one that would have embarrassed someone: it shows 1,146.08 against 1,147.08 in the detail — exactly a pound short, the signature of a figure retyped by hand with one digit wrong. ",[61,1318,1319],{},"North Q3"," matches exactly. The best region is checked against the row totals — which also means that if a row total were wrong enough, this check would flag it too.",[888,1322,897,1328,897,1331,897,1334,897,1339,897,1347,897,1351,897,1357,897,1361,897,1365,897,1369,897,1373,897,1377,897,1381,897,1385,897,1389,897,1393,897,1396,897,1399,897,1403,897,1405,897,1408,897,1410,897,1413,897,1415,897,1418,897,1420,897,1423,897,1426,897,1429,897,1431,897,1435,897,1437,897,1440,897,1442,897,1445,897,1447,897,1450,897,1454,897,1458,897,1461,897,1465,897,1467,897,1470,897,1472,897,1475,897,1477,897,1480,897,1482,897,1485,897,1487,897,1491,897,1494,897,1497,897,1499,897,1502,897,1504,897,1507,897,1509,897,1512,897,1514,897,1516,897,1518],{"viewBox":1323,"role":891,"ariaLabelledBy":1324,"xmlns":895,"style":1327},"73 0 613 168",[1325,1326],"tot-failures-t","tot-failures-d","width:100%;max-width:613px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[899,1329,1330],{"id":1325},"What the checker reports",[903,1332,1333],{"id":1326},"South's row total in E3 shows 311.50 against 311.55 calculated, a difference of minus 0.05. The grand total in E6 differs from the sum of the Total column by 0.05 for the same reason. The summary's revenue figure is exactly one pound short of the detail. The one-penny difference in the Q2 total is within tolerance and is not reported.",[907,1335],{"x":1336,"y":910,"width":1337,"height":1338,"fill":913},"73","613","168",[907,1340],{"x":1341,"y":917,"width":1342,"height":1343,"rx":910,"fill":1344,"stroke":1345,"style":1346},"90.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[907,1348],{"x":1349,"y":917,"width":1350,"height":1343,"rx":910,"fill":1344,"stroke":1345,"style":1346},"122.0","112.2",[925,1352,1356],{"x":1353,"y":1354,"style":1355},"178.1","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[907,1358],{"x":1359,"y":917,"width":1360,"height":1343,"rx":910,"fill":1344,"stroke":1345,"style":1346},"234.2","187.9",[925,1362,1364],{"x":1363,"y":1354,"style":1355},"328.1","B",[907,1366],{"x":1367,"y":917,"width":1368,"height":1343,"rx":910,"fill":1344,"stroke":1345,"style":1346},"422.1","76.7",[925,1370,1372],{"x":1371,"y":1354,"style":1355},"460.4","C",[907,1374],{"x":1375,"y":917,"width":1376,"height":1343,"rx":910,"fill":1344,"stroke":1345,"style":1346},"498.7","81.9",[925,1378,1380],{"x":1379,"y":1354,"style":1355},"539.7","D",[907,1382],{"x":1383,"y":917,"width":1384,"height":1343,"rx":910,"fill":1344,"stroke":1345,"style":1346},"580.6","89.5",[925,1386,1388],{"x":1387,"y":1354,"style":1355},"625.3","E",[907,1390],{"x":1341,"y":1391,"width":1342,"height":1392,"rx":910,"fill":1344,"stroke":1345,"style":1346},"40.0","28.0",[925,1394,201],{"x":1395,"y":956,"style":1355},"106.0",[907,1397],{"x":1349,"y":1391,"width":1350,"height":1392,"rx":910,"fill":921,"stroke":922,"style":1398},"stroke-width:1.5px",[925,1400,1402],{"x":1353,"y":956,"style":1401},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Cell",[907,1404],{"x":1359,"y":1391,"width":1360,"height":1392,"rx":910,"fill":921,"stroke":922,"style":1398},[925,1406,1407],{"x":1363,"y":956,"style":1401},"Check",[907,1409],{"x":1367,"y":1391,"width":1368,"height":1392,"rx":910,"fill":921,"stroke":922,"style":1398},[925,1411,1412],{"x":1371,"y":956,"style":1401},"Shown",[907,1414],{"x":1375,"y":1391,"width":1376,"height":1392,"rx":910,"fill":921,"stroke":922,"style":1398},[925,1416,1417],{"x":1379,"y":956,"style":1401},"Expected",[907,1419],{"x":1383,"y":1391,"width":1384,"height":1392,"rx":910,"fill":921,"stroke":922,"style":1398},[925,1421,1422],{"x":1387,"y":956,"style":1401},"Difference",[907,1424],{"x":1341,"y":1425,"width":1342,"height":1392,"rx":910,"fill":1344,"stroke":1345,"style":1346},"68.0",[925,1427,242],{"x":1395,"y":1428,"style":1355},"87.0",[907,1430],{"x":1349,"y":1425,"width":1350,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1432,1434],{"x":1353,"y":1428,"style":1433},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","'By region'!E3",[907,1436],{"x":1359,"y":1425,"width":1360,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1438,1439],{"x":1363,"y":1428,"style":1433},"South: row total",[907,1441],{"x":1367,"y":1425,"width":1368,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1443,1444],{"x":1371,"y":1428,"style":1433},"311.50",[907,1446],{"x":1375,"y":1425,"width":1376,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1448,1449],{"x":1379,"y":1428,"style":1433},"311.55",[907,1451],{"x":1383,"y":1425,"width":1384,"height":1392,"rx":910,"fill":1452,"stroke":1453,"style":1398},"#fce9e9","var(--danger,#dc2626)",[925,1455,1457],{"x":1387,"y":1428,"style":1456},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","−0.05",[907,1459],{"x":1341,"y":1460,"width":1342,"height":1392,"rx":910,"fill":1344,"stroke":1345,"style":1346},"96.0",[925,1462,1464],{"x":1395,"y":1463,"style":1355},"115.0","3",[907,1466],{"x":1349,"y":1460,"width":1350,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1468,1469],{"x":1353,"y":1463,"style":1433},"'By region'!E6",[907,1471],{"x":1359,"y":1460,"width":1360,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1473,1474],{"x":1363,"y":1463,"style":1433},"grand total vs total column",[907,1476],{"x":1367,"y":1460,"width":1368,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1478,1479],{"x":1371,"y":1463,"style":1433},"1,147.08",[907,1481],{"x":1375,"y":1460,"width":1376,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1483,1484],{"x":1379,"y":1463,"style":1433},"1,147.03",[907,1486],{"x":1383,"y":1460,"width":1384,"height":1392,"rx":910,"fill":972,"stroke":973,"style":1398},[925,1488,1490],{"x":1387,"y":1463,"style":1489},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","+0.05",[907,1492],{"x":1341,"y":1493,"width":1342,"height":1392,"rx":910,"fill":1344,"stroke":1345,"style":1346},"124.0",[925,1495,909],{"x":1395,"y":1496,"style":1355},"143.0",[907,1498],{"x":1349,"y":1493,"width":1350,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1500,1501],{"x":1353,"y":1496,"style":1433},"'Summary'!B2",[907,1503],{"x":1359,"y":1493,"width":1360,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1505,1506],{"x":1363,"y":1496,"style":1433},"summary: Revenue Q1–Q3",[907,1508],{"x":1367,"y":1493,"width":1368,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1510,1511],{"x":1371,"y":1496,"style":1433},"1,146.08",[907,1513],{"x":1375,"y":1493,"width":1376,"height":1392,"rx":910,"fill":913,"stroke":1345,"style":1346},[925,1515,1479],{"x":1379,"y":1496,"style":1433},[907,1517],{"x":1383,"y":1493,"width":1384,"height":1392,"rx":910,"fill":1452,"stroke":1453,"style":1398},[925,1519,1520],{"x":1387,"y":1496,"style":1456},"−1.00",[888,1522,897,1527,897,1530,897,1533,897,1536,897,1541,897,1547,897,1552,897,1556,897,1560,897,1564,897,1568,897,1571,897,1576,897,1580,897,1583,897,1586,897,1589],{"viewBox":1523,"role":891,"ariaLabelledBy":1524,"xmlns":895,"style":896},"4 0 752 199",[1525,1526],"tot-kinds-t","tot-kinds-d",[899,1528,1529],{"id":1525},"Two kinds of failure, two kinds of fix",[903,1531,1532],{"id":1526},"A row or column total that disagrees with its cells usually means a total was typed over a formula or a range missed a row; restore the formula. A summary figure that disagrees with the detail usually means it was copied or retyped by hand; replace it with a reference to the detail.",[907,1534],{"x":909,"y":910,"width":911,"height":1535,"fill":913},"199",[907,1537],{"x":916,"y":917,"width":1538,"height":1539,"rx":1540,"fill":972,"stroke":973,"style":923},"351.0","167.0","14",[925,1542,1546],{"x":1543,"y":1544,"style":1545},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Total disagrees with its cells",[41,1548],{"x1":1549,"y1":1550,"x2":1551,"y2":1550,"stroke":973,"style":1346},"36.0","57.0","355.0",[925,1553,1555],{"x":1543,"y":1554,"style":1433},"77.0","typed over a formula",[925,1557,1559],{"x":1543,"y":1558,"style":1433},"103.0","SUM range missed a row",[925,1561,1563],{"x":1543,"y":1562,"style":1433},"129.0","fix: restore the formula",[925,1565,1567],{"x":1543,"y":1566,"style":1433},"155.0","e.g. South E3",[907,1569],{"x":1570,"y":917,"width":1538,"height":1539,"rx":1540,"fill":1452,"stroke":1453,"style":923},"389.0",[925,1572,1575],{"x":1573,"y":1544,"style":1574},"564.5","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Summary disagrees with detail",[41,1577],{"x1":1578,"y1":1550,"x2":1579,"y2":1550,"stroke":1453,"style":1346},"405.0","724.0",[925,1581,1582],{"x":1573,"y":1554,"style":1433},"copied or retyped by hand",[925,1584,1585],{"x":1573,"y":1558,"style":1433},"left from last month",[925,1587,1588],{"x":1573,"y":1562,"style":1433},"fix: link to the detail",[925,1590,1591],{"x":1573,"y":1566,"style":1433},"e.g. Summary B2",[25,1593,1595],{"id":1594},"step-4-make-it-a-gate-before-sending","Step 4 — Make it a gate before sending",[30,1597,1599],{"className":86,"code":1598,"language":88,"meta":35,"style":35},"def check_report(path):\n    \"\"\"Return a DataFrame of failed checks; empty means the report adds up.\"\"\"\n    # (the checks above, wrapped in a function)\n    return report\n\nif len(report):\n    report.to_excel(\"quarterly_report_CHECKS_FAILED.xlsx\", index=False)\n    print(f\"{len(report)} check(s) failed — report not sent\")\nelse:\n    print(\"all totals agree — OK to send\")\n",[14,1600,1601,1611,1616,1621,1629,1633,1643,1661,1688,1695],{"__ignoreMap":35},[39,1602,1603,1605,1608],{"class":41,"line":42},[39,1604,431],{"class":95},[39,1606,1607],{"class":434}," check_report",[39,1609,1610],{"class":99},"(path):\n",[39,1612,1613],{"class":41,"line":109},[39,1614,1615],{"class":49},"    \"\"\"Return a DataFrame of failed checks; empty means the report adds up.\"\"\"\n",[39,1617,1618],{"class":41,"line":123},[39,1619,1620],{"class":366},"    # (the checks above, wrapped in a function)\n",[39,1622,1623,1626],{"class":41,"line":130},[39,1624,1625],{"class":95},"    return",[39,1627,1628],{"class":99}," report\n",[39,1630,1631],{"class":41,"line":161},[39,1632,127],{"emptyLinePlaceholder":126},[39,1634,1635,1637,1640],{"class":41,"line":178},[39,1636,1196],{"class":95},[39,1638,1639],{"class":154}," len",[39,1641,1642],{"class":99},"(report):\n",[39,1644,1645,1648,1651,1653,1655,1657,1659],{"class":41,"line":207},[39,1646,1647],{"class":99},"    report.to_excel(",[39,1649,1650],{"class":49},"\"quarterly_report_CHECKS_FAILED.xlsx\"",[39,1652,145],{"class":99},[39,1654,380],{"class":148},[39,1656,136],{"class":95},[39,1658,873],{"class":154},[39,1660,158],{"class":99},[39,1662,1663,1666,1669,1671,1673,1675,1678,1681,1683,1686],{"class":41,"line":248},[39,1664,1665],{"class":154},"    print",[39,1667,1668],{"class":99},"(",[39,1670,620],{"class":95},[39,1672,642],{"class":49},[39,1674,627],{"class":626},[39,1676,1677],{"class":154},"len",[39,1679,1680],{"class":99},"(report)",[39,1682,639],{"class":626},[39,1684,1685],{"class":49}," check(s) failed — report not sent\"",[39,1687,158],{"class":99},[39,1689,1690,1693],{"class":41,"line":289},[39,1691,1692],{"class":95},"else",[39,1694,538],{"class":99},[39,1696,1697,1699,1701,1704],{"class":41,"line":323},[39,1698,1665],{"class":154},[39,1700,1668],{"class":99},[39,1702,1703],{"class":49},"\"all totals agree — OK to send\"",[39,1705,158],{"class":99},[10,1707,1708,1709,1711,1712,1715,1716,23],{},"The checks are most useful as a gate: the report job runs them after building the workbook and before emailing it, and refuses to send if anything fails, writing the failures to a file the person on duty can open. A failure list with sheet-qualified cell addresses — ",[61,1710,1434],{}," — can be pasted into Excel's ",[61,1713,1714],{},"Go To"," box, which takes the reader straight to the cell. The same gate fits the workflow in ",[19,1717,1719],{"href":1718},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel Report Before Sending It",[25,1721,1723],{"id":1722},"beyond-simple-sums","Beyond simple sums",[10,1725,1726,1727,1731,1732,1735],{},"Not every total in a report is a sum, and the same approach extends to the others. Averages and percentages should be recomputed from their parts rather than averaged again — an average of regional margins is not the overall margin unless the regions are the same size. Balances should follow their movements: an opening balance plus the month's movements equals the closing balance, the check used for bank statements in ",[19,1728,1730],{"href":1729},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-position-report-from-bank-statements\u002F","Build a Cash Position Report from Bank Statements",". And figures that appear in two places — a revenue number in the summary, the chart title and the email text — should all be compared with the one source. Each of these is one more call to the same ",[14,1733,1734],{},"check"," function with a different expected value, so the list of checks grows with the report without the code becoming complicated.",[25,1737,1739],{"id":1738},"common-pitfalls","Common pitfalls",[1741,1742,1743,1754,1760,1766,1772],"ul",{},[1744,1745,1746,1750,1751,1753],"li",{},[1747,1748,1749],"strong",{},"Reading formulas instead of values."," Use ",[14,1752,78],{}," on a calculated workbook.",[1744,1755,1756,1759],{},[1747,1757,1758],{},"No tolerance."," Rounded figures legitimately differ by a penny; allow for it, but no more.",[1744,1761,1762,1765],{},[1747,1763,1764],{},"Checking the grand total one way."," Cross-foot it against detail, total row and total column.",[1744,1767,1768,1771],{},[1747,1769,1770],{},"Ignoring the summary sheet."," Recompute every summary figure from the detail.",[1744,1773,1774,1777],{},[1747,1775,1776],{},"Failures without addresses."," Report sheet and cell so the error can be found at once.",[25,1779,1781],{"id":1780},"conclusion","Conclusion",[10,1783,1784],{},"To check that a report's totals add up, read the sheet with openpyxl keeping cell addresses alongside the values, recompute every row total, column total and the grand total — cross-footed three ways — and every figure on the summary sheet, and compare each with what the report shows within a small rounding tolerance. List the failures by sheet and cell, and run the checks as a gate that stops the report being sent until they pass.",[25,1786,1788],{"id":1787},"frequently-asked-questions","Frequently asked questions",[10,1790,1791,1794],{},[1747,1792,1793],{},"How do I check that Excel totals are correct with Python?","\nRead the values, recompute each total from the cells it covers, and compare with the stored total within a small tolerance.",[10,1796,1797,1800],{},[1747,1798,1799],{},"What is cross-footing?","\nChecking that the sum of the row totals and the sum of the column totals both equal the grand total, and that it equals the sum of all detail cells.",[10,1802,1803,1806],{},[1747,1804,1805],{},"Why do my totals differ by a penny?","\nEach figure was rounded before summing; allow a tolerance of about a penny for such differences.",[10,1808,1809,1812],{},[1747,1810,1811],{},"How do I report which cell is wrong?","\nKeep each cell's coordinate alongside its value when reading the sheet and include it in the failure list.",[25,1814,1816],{"id":1815},"related","Related",[1741,1818,1819,1825,1830,1837],{},[1744,1820,1821,1822,1824],{},"Up one level: ",[19,1823,22],{"href":21}," — checking data quality.",[1744,1826,1827,1829],{},[19,1828,1719],{"href":1718}," — the sending gate.",[1744,1831,1832,1836],{},[19,1833,1835],{"href":1834},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-grouped-data-with-blank-rows-between-groups\u002F","Write Grouped Data with Blank Rows Between Groups"," — totals written as formulas.",[1744,1838,1839,1843],{},[19,1840,1842],{"href":1841},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fmatch-excel-rounding-in-pandas\u002F","Match Excel Rounding in pandas"," — why rounded figures disagree.",[1845,1846,1847],"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}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":109,"depth":109,"links":1849},[1850,1851,1852,1853,1854,1855,1856,1857,1858,1859],{"id":27,"depth":109,"text":28},{"id":82,"depth":109,"text":83},{"id":346,"depth":109,"text":347},{"id":1029,"depth":109,"text":1030},{"id":1594,"depth":109,"text":1595},{"id":1722,"depth":109,"text":1723},{"id":1738,"depth":109,"text":1739},{"id":1780,"depth":109,"text":1781},{"id":1787,"depth":109,"text":1788},{"id":1815,"depth":109,"text":1816},"2026-10-02","Automatically check a finished Excel report before it is sent: every total row equals the sum of the rows above it, every total column equals the sum across, the grand total cross-foots both ways, the summary sheet agrees with the detail sheet, and differences within a rounding tolerance are accepted — with each failure reported by sheet and cell so it can be fixed.","md",[1864,1866,1868,1870],{"q":1793,"a":1865},"Read the values, recompute each total from the cells it covers, and compare with the stored total within a small tolerance.",{"q":1799,"a":1867},"Checking that the sum of the row totals and the sum of the column totals both equal the grand total, and that it equals the sum of all detail cells.",{"q":1805,"a":1869},"Each figure was rounded before summing; allow a tolerance of about a penny for such differences.",{"q":1811,"a":1871},"Keep each cell's coordinate alongside its value when reading the sheet and include it in the failure list.","check report totals add up python",null,{"breadcrumb":1875},[1876,1879,1882],{"name":1877,"item":1878},"Home","\u002F",{"name":1880,"item":1881},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":22,"item":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-that-totals-in-a-report-add-up",{"title":1885,"description":1886},"Check That Totals in an Excel Report Add Up with Python","Validate Excel report totals with pandas and openpyxl: total rows and columns, cross-footing, summary versus detail, rounding tolerance and failures listed by cell address.","check-that-totals-in-a-report-add-up","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-that-totals-in-a-report-add-up\u002Findex",[1890,1891],"data-validation","excel-formulas","how-to","HP6E6lFEmA8DzX4olvR7uuCbrsypxbdHAYeIDYbKaw0",[1895,1899],{"title":1896,"path":1897,"stem":1898,"children":-1},"Check References Between Excel Sheets with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-references-between-excel-sheets","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-references-between-excel-sheets\u002Findex",{"title":1900,"path":1901,"stem":1902,"children":-1},"Compare Two Excel Files for Differences with Python","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python\u002Findex",[1904,1906],{"path":1905,"title":1880},"\u002Fadvanced-data-transformation-and-cleaning",{"path":1907,"title":22},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python",[],[1910,1913,1916,1919,1922,1925],{"path":1911,"title":1912,"kind":1892},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fdetect-outliers-in-excel-data-with-pandas","Detect Outliers in Excel Data with pandas",{"path":1914,"title":1915,"kind":1892},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl","Highlight Cells Containing Specific Text with openpyxl",{"path":1917,"title":1918,"kind":1892},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Ffind-near-duplicate-names-within-one-excel-column","Find Near-Duplicate Names Within One Excel Column",{"path":1920,"title":1921,"kind":1892},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook","Read Dropdown Lists and Validation Rules from an Existing Workbook",{"path":1923,"title":1924,"kind":1892},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ffind-and-report-missing-values-in-an-excel-file","Find and Report Missing Values in an Excel File",{"path":1926,"title":1927,"kind":1892},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Fflag-rows-with-too-many-missing-values","Flag Rows with Too Many Missing Values",1790972098531]