[{"data":1,"prerenderedAt":2626},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python":2618},{"id":4,"title":5,"body":6,"dateModified":2589,"datePublished":2589,"description":2590,"extension":2591,"faq":2592,"meta":2601,"navigation":203,"path":2611,"seo":2612,"slug":2614,"stem":2615,"type":2616,"__hash__":2617},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python\u002Findex.md","Compare Two Excel Files for Differences with Python",{"type":7,"value":8,"toc":2573},"minimark",[9,19,138,143,171,175,543,546,550,553,765,768,845,849,852,961,968,972,978,1238,1252,1255,1386,1390,1393,1858,1861,1865,1877,2086,2094,2098,2101,2188,2199,2203,2305,2309,2312,2463,2466,2470,2480,2484,2487,2491,2498,2504,2510,2520,2524,2527,2536,2539,2569],[10,11,12,13,18],"p",{},"\"What changed since last month?\" is a question a spreadsheet cannot answer about itself. Two files, hundreds of rows, and the differences that matter are usually a handful of cells. This guide, part of ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","Validating Excel Data with Python",", builds a diff that reports added rows, removed rows and changed cells — with the old and new values side by side.",[20,21,29,30,29,34,29,38,29,45,29,54,29,59,29,68,29,75,29,80,29,84,29,89,29,92,29,95,29,100,29,103,29,106,29,111,29,114,29,120,29,125,29,133],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 740 244","img",[25,26],"cmp-sets-t","cmp-sets-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:740px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[31,32,33],"title",{"id":25},"Aligning two files on a key splits the rows three ways",[35,36,37],"desc",{"id":26},"Keys present only in the old file are removed rows. Keys present only in the new file are added rows. Keys in both are compared cell by cell and split again into unchanged and modified rows.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","740","244","#ffffff",[39,46],{"x":47,"y":47,"width":48,"height":49,"rx":50,"fill":51,"stroke":52,"style":53},"40","250","120","60","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[39,55],{"x":56,"y":47,"width":48,"height":49,"rx":50,"fill":57,"stroke":58,"style":53},"450","#d9f4f1","var(--teal,#0f9488)",[39,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":53},"290","52","160","96","20","#ebebfd","var(--brand,#5b5cf0)",[69,70,74],"text",{"x":71,"y":72,"style":73},"150","86","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","only in old",[69,76,79],{"x":71,"y":77,"style":78},"110","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","removed rows",[69,81,83],{"x":71,"y":82,"style":78},"132","key disappeared",[69,85,88],{"x":86,"y":72,"style":87},"370","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","in both",[69,90,91],{"x":86,"y":77,"style":78},"compare cells",[69,93,94],{"x":86,"y":82,"style":78},"unchanged or modified",[69,96,99],{"x":97,"y":72,"style":98},"590","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","only in new",[69,101,102],{"x":97,"y":77,"style":78},"added rows",[69,104,105],{"x":97,"y":82,"style":78},"key appeared",[69,107,110],{"x":71,"y":108,"style":109},"30","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","last month.xlsx",[69,112,113],{"x":97,"y":108,"style":109},"this month.xlsx",[115,116],"line",{"x1":86,"y1":117,"x2":86,"y2":118,"stroke":67,"style":119},"152","186","stroke-width:1.5px",[121,122],"polygon",{"points":123,"fill":124},"370,190 364,178 376,178","#5b5cf0",[39,126],{"x":127,"y":128,"width":129,"height":47,"rx":130,"fill":131,"stroke":132},"200","192","340","10","#f0f4ff","var(--line,#cdd5e6)",[69,134,137],{"x":86,"y":135,"style":136},"217","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","cell-level diff: column, old value, new value",[139,140,142],"h2",{"id":141},"prerequisites","Prerequisites",[144,145,150],"pre",{"className":146,"code":147,"language":148,"meta":149,"style":149},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[151,152,153],"code",{"__ignoreMap":149},[154,155,157,161,165,168],"span",{"class":115,"line":156},1,[154,158,160],{"class":159},"sMTad","pip",[154,162,164],{"class":163},"srMev"," install",[154,166,167],{"class":163}," pandas",[154,169,170],{"class":163}," openpyxl\n",[139,172,174],{"id":173},"two-versions-of-the-same-report","Two versions of the same report",[144,176,180],{"className":177,"code":178,"language":179,"meta":149,"style":149},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\npd.DataFrame([\n    {\"Order_ID\": 2001, \"Region\": \"North\", \"Quantity\": 4, \"Unit_Price\": 19.99},\n    {\"Order_ID\": 2002, \"Region\": \"South\", \"Quantity\": 2, \"Unit_Price\": 49.50},\n    {\"Order_ID\": 2003, \"Region\": \"West\", \"Quantity\": 7, \"Unit_Price\": 12.25},\n]).to_excel(\"orders_january.xlsx\", index=False, sheet_name=\"Orders\")\n\npd.DataFrame([\n    {\"Order_ID\": 2002, \"Region\": \"South\", \"Quantity\": 3, \"Unit_Price\": 49.50},  # quantity changed\n    {\"Order_ID\": 2003, \"Region\": \"West\", \"Quantity\": 7, \"Unit_Price\": 11.95},   # price changed\n    {\"Order_ID\": 2004, \"Region\": \"Central\", \"Quantity\": 5, \"Unit_Price\": 8.75}, # added\n]).to_excel(\"orders_february.xlsx\", index=False, sheet_name=\"Orders\")\n","python",[151,181,182,198,205,211,261,302,343,377,382,387,430,472,517],{"__ignoreMap":149},[154,183,184,188,192,195],{"class":115,"line":156},[154,185,187],{"class":186},"s-kum","import",[154,189,191],{"class":190},"skGVy"," pandas ",[154,193,194],{"class":186},"as",[154,196,197],{"class":190}," pd\n",[154,199,201],{"class":115,"line":200},2,[154,202,204],{"emptyLinePlaceholder":203},true,"\n",[154,206,208],{"class":115,"line":207},3,[154,209,210],{"class":190},"pd.DataFrame([\n",[154,212,214,217,220,223,227,230,233,235,238,240,243,245,248,250,253,255,258],{"class":115,"line":213},4,[154,215,216],{"class":190},"    {",[154,218,219],{"class":163},"\"Order_ID\"",[154,221,222],{"class":190},": ",[154,224,226],{"class":225},"sP0c6","2001",[154,228,229],{"class":190},", ",[154,231,232],{"class":163},"\"Region\"",[154,234,222],{"class":190},[154,236,237],{"class":163},"\"North\"",[154,239,229],{"class":190},[154,241,242],{"class":163},"\"Quantity\"",[154,244,222],{"class":190},[154,246,247],{"class":225},"4",[154,249,229],{"class":190},[154,251,252],{"class":163},"\"Unit_Price\"",[154,254,222],{"class":190},[154,256,257],{"class":225},"19.99",[154,259,260],{"class":190},"},\n",[154,262,264,266,268,270,273,275,277,279,282,284,286,288,291,293,295,297,300],{"class":115,"line":263},5,[154,265,216],{"class":190},[154,267,219],{"class":163},[154,269,222],{"class":190},[154,271,272],{"class":225},"2002",[154,274,229],{"class":190},[154,276,232],{"class":163},[154,278,222],{"class":190},[154,280,281],{"class":163},"\"South\"",[154,283,229],{"class":190},[154,285,242],{"class":163},[154,287,222],{"class":190},[154,289,290],{"class":225},"2",[154,292,229],{"class":190},[154,294,252],{"class":163},[154,296,222],{"class":190},[154,298,299],{"class":225},"49.50",[154,301,260],{"class":190},[154,303,305,307,309,311,314,316,318,320,323,325,327,329,332,334,336,338,341],{"class":115,"line":304},6,[154,306,216],{"class":190},[154,308,219],{"class":163},[154,310,222],{"class":190},[154,312,313],{"class":225},"2003",[154,315,229],{"class":190},[154,317,232],{"class":163},[154,319,222],{"class":190},[154,321,322],{"class":163},"\"West\"",[154,324,229],{"class":190},[154,326,242],{"class":163},[154,328,222],{"class":190},[154,330,331],{"class":225},"7",[154,333,229],{"class":190},[154,335,252],{"class":163},[154,337,222],{"class":190},[154,339,340],{"class":225},"12.25",[154,342,260],{"class":190},[154,344,346,349,352,354,358,361,364,366,369,371,374],{"class":115,"line":345},7,[154,347,348],{"class":190},"]).to_excel(",[154,350,351],{"class":163},"\"orders_january.xlsx\"",[154,353,229],{"class":190},[154,355,357],{"class":356},"sa561","index",[154,359,360],{"class":186},"=",[154,362,363],{"class":225},"False",[154,365,229],{"class":190},[154,367,368],{"class":356},"sheet_name",[154,370,360],{"class":186},[154,372,373],{"class":163},"\"Orders\"",[154,375,376],{"class":190},")\n",[154,378,380],{"class":115,"line":379},8,[154,381,204],{"emptyLinePlaceholder":203},[154,383,385],{"class":115,"line":384},9,[154,386,210],{"class":190},[154,388,390,392,394,396,398,400,402,404,406,408,410,412,415,417,419,421,423,426],{"class":115,"line":389},10,[154,391,216],{"class":190},[154,393,219],{"class":163},[154,395,222],{"class":190},[154,397,272],{"class":225},[154,399,229],{"class":190},[154,401,232],{"class":163},[154,403,222],{"class":190},[154,405,281],{"class":163},[154,407,229],{"class":190},[154,409,242],{"class":163},[154,411,222],{"class":190},[154,413,414],{"class":225},"3",[154,416,229],{"class":190},[154,418,252],{"class":163},[154,420,222],{"class":190},[154,422,299],{"class":225},[154,424,425],{"class":190},"},  ",[154,427,429],{"class":428},"s-wDw","# quantity changed\n",[154,431,433,435,437,439,441,443,445,447,449,451,453,455,457,459,461,463,466,469],{"class":115,"line":432},11,[154,434,216],{"class":190},[154,436,219],{"class":163},[154,438,222],{"class":190},[154,440,313],{"class":225},[154,442,229],{"class":190},[154,444,232],{"class":163},[154,446,222],{"class":190},[154,448,322],{"class":163},[154,450,229],{"class":190},[154,452,242],{"class":163},[154,454,222],{"class":190},[154,456,331],{"class":225},[154,458,229],{"class":190},[154,460,252],{"class":163},[154,462,222],{"class":190},[154,464,465],{"class":225},"11.95",[154,467,468],{"class":190},"},   ",[154,470,471],{"class":428},"# price changed\n",[154,473,475,477,479,481,484,486,488,490,493,495,497,499,502,504,506,508,511,514],{"class":115,"line":474},12,[154,476,216],{"class":190},[154,478,219],{"class":163},[154,480,222],{"class":190},[154,482,483],{"class":225},"2004",[154,485,229],{"class":190},[154,487,232],{"class":163},[154,489,222],{"class":190},[154,491,492],{"class":163},"\"Central\"",[154,494,229],{"class":190},[154,496,242],{"class":163},[154,498,222],{"class":190},[154,500,501],{"class":225},"5",[154,503,229],{"class":190},[154,505,252],{"class":163},[154,507,222],{"class":190},[154,509,510],{"class":225},"8.75",[154,512,513],{"class":190},"}, ",[154,515,516],{"class":428},"# added\n",[154,518,520,522,525,527,529,531,533,535,537,539,541],{"class":115,"line":519},13,[154,521,348],{"class":190},[154,523,524],{"class":163},"\"orders_february.xlsx\"",[154,526,229],{"class":190},[154,528,357],{"class":356},[154,530,360],{"class":186},[154,532,363],{"class":225},[154,534,229],{"class":190},[154,536,368],{"class":356},[154,538,360],{"class":186},[154,540,373],{"class":163},[154,542,376],{"class":190},[10,544,545],{},"Order 2001 has gone, 2004 is new, and two cells changed. A good diff reports exactly those four facts and nothing else.",[139,547,549],{"id":548},"align-on-a-key-never-on-position","Align on a key, never on position",[10,551,552],{},"Comparing row 5 with row 5 is only valid if nothing was inserted, removed or re-sorted — which is exactly what you are trying to detect. Index both frames on the business key:",[144,554,556],{"className":177,"code":555,"language":179,"meta":149,"style":149},"import pandas as pd\n\nKEY = \"Order_ID\"\n\ndef load(path, sheet=\"Orders\", key=KEY):\n    df = pd.read_excel(path, sheet_name=sheet)\n    if df[key].duplicated().any():\n        dupes = df.loc[df[key].duplicated(keep=False), key].unique()\n        raise ValueError(f\"{path}: key {key} is not unique — {list(dupes)[:5]}\")\n    return df.set_index(key).sort_index()\n\nold = load(\"orders_january.xlsx\")\nnew = load(\"orders_february.xlsx\")\nprint(old.index.tolist(), new.index.tolist())\n",[151,557,558,568,572,583,587,613,630,638,658,717,725,729,743,756],{"__ignoreMap":149},[154,559,560,562,564,566],{"class":115,"line":156},[154,561,187],{"class":186},[154,563,191],{"class":190},[154,565,194],{"class":186},[154,567,197],{"class":190},[154,569,570],{"class":115,"line":200},[154,571,204],{"emptyLinePlaceholder":203},[154,573,574,577,580],{"class":115,"line":207},[154,575,576],{"class":225},"KEY",[154,578,579],{"class":186}," =",[154,581,582],{"class":163}," \"Order_ID\"\n",[154,584,585],{"class":115,"line":213},[154,586,204],{"emptyLinePlaceholder":203},[154,588,589,592,596,599,601,603,606,608,610],{"class":115,"line":263},[154,590,591],{"class":186},"def",[154,593,595],{"class":594},"s_Opv"," load",[154,597,598],{"class":190},"(path, sheet",[154,600,360],{"class":186},[154,602,373],{"class":163},[154,604,605],{"class":190},", key",[154,607,360],{"class":186},[154,609,576],{"class":225},[154,611,612],{"class":190},"):\n",[154,614,615,618,620,623,625,627],{"class":115,"line":304},[154,616,617],{"class":190},"    df ",[154,619,360],{"class":186},[154,621,622],{"class":190}," pd.read_excel(path, ",[154,624,368],{"class":356},[154,626,360],{"class":186},[154,628,629],{"class":190},"sheet)\n",[154,631,632,635],{"class":115,"line":345},[154,633,634],{"class":186},"    if",[154,636,637],{"class":190}," df[key].duplicated().any():\n",[154,639,640,643,645,648,651,653,655],{"class":115,"line":379},[154,641,642],{"class":190},"        dupes ",[154,644,360],{"class":186},[154,646,647],{"class":190}," df.loc[df[key].duplicated(",[154,649,650],{"class":356},"keep",[154,652,360],{"class":186},[154,654,363],{"class":225},[154,656,657],{"class":190},"), key].unique()\n",[154,659,660,663,666,669,672,675,679,682,685,688,690,693,695,698,700,703,706,708,711,713,715],{"class":115,"line":384},[154,661,662],{"class":186},"        raise",[154,664,665],{"class":225}," ValueError",[154,667,668],{"class":190},"(",[154,670,671],{"class":186},"f",[154,673,674],{"class":163},"\"",[154,676,678],{"class":677},"sSjpA","{",[154,680,681],{"class":190},"path",[154,683,684],{"class":677},"}",[154,686,687],{"class":163},": key ",[154,689,678],{"class":677},[154,691,692],{"class":190},"key",[154,694,684],{"class":677},[154,696,697],{"class":163}," is not unique — ",[154,699,678],{"class":677},[154,701,702],{"class":225},"list",[154,704,705],{"class":190},"(dupes)[:",[154,707,501],{"class":225},[154,709,710],{"class":190},"]",[154,712,684],{"class":677},[154,714,674],{"class":163},[154,716,376],{"class":190},[154,718,719,722],{"class":115,"line":389},[154,720,721],{"class":186},"    return",[154,723,724],{"class":190}," df.set_index(key).sort_index()\n",[154,726,727],{"class":115,"line":432},[154,728,204],{"emptyLinePlaceholder":203},[154,730,731,734,736,739,741],{"class":115,"line":474},[154,732,733],{"class":190},"old ",[154,735,360],{"class":186},[154,737,738],{"class":190}," load(",[154,740,351],{"class":163},[154,742,376],{"class":190},[154,744,745,748,750,752,754],{"class":115,"line":519},[154,746,747],{"class":190},"new ",[154,749,360],{"class":186},[154,751,738],{"class":190},[154,753,524],{"class":163},[154,755,376],{"class":190},[154,757,759,762],{"class":115,"line":758},14,[154,760,761],{"class":225},"print",[154,763,764],{"class":190},"(old.index.tolist(), new.index.tolist())\n",[10,766,767],{},"The uniqueness check is not optional. A duplicated key makes the comparison meaningless — pandas will align one row against several — and it is far better to stop here than to publish a diff nobody can reconcile. When no single column is unique, build a composite key first:",[144,769,771],{"className":177,"code":770,"language":179,"meta":149,"style":149},"composite = [\"Order_Date\", \"Customer\", \"SKU\"]\ndf = df.assign(_key=df[composite].astype(str).agg(\"␟\".join, axis=1)).set_index(\"_key\")\n",[151,772,773,799],{"__ignoreMap":149},[154,774,775,778,780,783,786,788,791,793,796],{"class":115,"line":156},[154,776,777],{"class":190},"composite ",[154,779,360],{"class":186},[154,781,782],{"class":190}," [",[154,784,785],{"class":163},"\"Order_Date\"",[154,787,229],{"class":190},[154,789,790],{"class":163},"\"Customer\"",[154,792,229],{"class":190},[154,794,795],{"class":163},"\"SKU\"",[154,797,798],{"class":190},"]\n",[154,800,801,804,806,809,812,814,817,820,823,826,829,832,834,837,840,843],{"class":115,"line":200},[154,802,803],{"class":190},"df ",[154,805,360],{"class":186},[154,807,808],{"class":190}," df.assign(",[154,810,811],{"class":356},"_key",[154,813,360],{"class":186},[154,815,816],{"class":190},"df[composite].astype(",[154,818,819],{"class":225},"str",[154,821,822],{"class":190},").agg(",[154,824,825],{"class":163},"\"␟\"",[154,827,828],{"class":190},".join, ",[154,830,831],{"class":356},"axis",[154,833,360],{"class":186},[154,835,836],{"class":225},"1",[154,838,839],{"class":190},")).set_index(",[154,841,842],{"class":163},"\"_key\"",[154,844,376],{"class":190},[139,846,848],{"id":847},"added-and-removed-rows","Added and removed rows",[10,850,851],{},"With both frames indexed, set arithmetic answers two of the three questions:",[144,853,855],{"className":177,"code":854,"language":179,"meta":149,"style":149},"import pandas as pd\n\nadded = new.loc[new.index.difference(old.index)]\nremoved = old.loc[old.index.difference(new.index)]\ncommon = old.index.intersection(new.index)\n\nprint(f\"{len(added)} added, {len(removed)} removed, {len(common)} in both\")\nprint(added)\n",[151,856,857,867,871,881,891,901,905,954],{"__ignoreMap":149},[154,858,859,861,863,865],{"class":115,"line":156},[154,860,187],{"class":186},[154,862,191],{"class":190},[154,864,194],{"class":186},[154,866,197],{"class":190},[154,868,869],{"class":115,"line":200},[154,870,204],{"emptyLinePlaceholder":203},[154,872,873,876,878],{"class":115,"line":207},[154,874,875],{"class":190},"added ",[154,877,360],{"class":186},[154,879,880],{"class":190}," new.loc[new.index.difference(old.index)]\n",[154,882,883,886,888],{"class":115,"line":213},[154,884,885],{"class":190},"removed ",[154,887,360],{"class":186},[154,889,890],{"class":190}," old.loc[old.index.difference(new.index)]\n",[154,892,893,896,898],{"class":115,"line":263},[154,894,895],{"class":190},"common ",[154,897,360],{"class":186},[154,899,900],{"class":190}," old.index.intersection(new.index)\n",[154,902,903],{"class":115,"line":304},[154,904,204],{"emptyLinePlaceholder":203},[154,906,907,909,911,913,915,917,920,923,925,928,930,932,935,937,940,942,944,947,949,952],{"class":115,"line":345},[154,908,761],{"class":225},[154,910,668],{"class":190},[154,912,671],{"class":186},[154,914,674],{"class":163},[154,916,678],{"class":677},[154,918,919],{"class":225},"len",[154,921,922],{"class":190},"(added)",[154,924,684],{"class":677},[154,926,927],{"class":163}," added, ",[154,929,678],{"class":677},[154,931,919],{"class":225},[154,933,934],{"class":190},"(removed)",[154,936,684],{"class":677},[154,938,939],{"class":163}," removed, ",[154,941,678],{"class":677},[154,943,919],{"class":225},[154,945,946],{"class":190},"(common)",[154,948,684],{"class":677},[154,950,951],{"class":163}," in both\"",[154,953,376],{"class":190},[154,955,956,958],{"class":115,"line":379},[154,957,761],{"class":225},[154,959,960],{"class":190},"(added)\n",[10,962,963,964,967],{},"That is already a useful report on its own. For a monthly reconciliation, ",[151,965,966],{},"removed"," is often the interesting half: rows that quietly disappeared from a source system rarely announce themselves, and the total moving by exactly one order's value is much harder to spot than a missing key.",[139,969,971],{"id":970},"changed-cells-with-old-and-new-values","Changed cells, with old and new values",[10,973,974,977],{},[151,975,976],{},"DataFrame.compare"," does the cell-level work, given two frames aligned on the same index and columns:",[144,979,981],{"className":177,"code":980,"language":179,"meta":149,"style":149},"import pandas as pd\n\nshared_columns = old.columns.intersection(new.columns)\nleft = old.loc[common, shared_columns]\nright = new.loc[common, shared_columns]\n\n# Round floats so invisible precision differences do not read as changes\nleft = left.round({c: 2 for c in left.select_dtypes(\"number\").columns})\nright = right.round({c: 2 for c in right.select_dtypes(\"number\").columns})\n\ndiff = left.compare(right, result_names=(\"old\", \"new\"))\nprint(diff)\n\nchanges = (\n    diff.stack(0, future_stack=True)\n        .rename_axis([KEY, \"Column\"])\n        .reset_index()\n        .dropna(subset=[\"old\", \"new\"], how=\"all\")\n)\nprint(changes.to_string(index=False))\n",[151,982,983,993,997,1007,1017,1027,1031,1036,1065,1089,1093,1121,1128,1132,1142,1162,1178,1184,1217,1222],{"__ignoreMap":149},[154,984,985,987,989,991],{"class":115,"line":156},[154,986,187],{"class":186},[154,988,191],{"class":190},[154,990,194],{"class":186},[154,992,197],{"class":190},[154,994,995],{"class":115,"line":200},[154,996,204],{"emptyLinePlaceholder":203},[154,998,999,1002,1004],{"class":115,"line":207},[154,1000,1001],{"class":190},"shared_columns ",[154,1003,360],{"class":186},[154,1005,1006],{"class":190}," old.columns.intersection(new.columns)\n",[154,1008,1009,1012,1014],{"class":115,"line":213},[154,1010,1011],{"class":190},"left ",[154,1013,360],{"class":186},[154,1015,1016],{"class":190}," old.loc[common, shared_columns]\n",[154,1018,1019,1022,1024],{"class":115,"line":263},[154,1020,1021],{"class":190},"right ",[154,1023,360],{"class":186},[154,1025,1026],{"class":190}," new.loc[common, shared_columns]\n",[154,1028,1029],{"class":115,"line":304},[154,1030,204],{"emptyLinePlaceholder":203},[154,1032,1033],{"class":115,"line":345},[154,1034,1035],{"class":428},"# Round floats so invisible precision differences do not read as changes\n",[154,1037,1038,1040,1042,1045,1047,1050,1053,1056,1059,1062],{"class":115,"line":379},[154,1039,1011],{"class":190},[154,1041,360],{"class":186},[154,1043,1044],{"class":190}," left.round({c: ",[154,1046,290],{"class":225},[154,1048,1049],{"class":186}," for",[154,1051,1052],{"class":190}," c ",[154,1054,1055],{"class":186},"in",[154,1057,1058],{"class":190}," left.select_dtypes(",[154,1060,1061],{"class":163},"\"number\"",[154,1063,1064],{"class":190},").columns})\n",[154,1066,1067,1069,1071,1074,1076,1078,1080,1082,1085,1087],{"class":115,"line":384},[154,1068,1021],{"class":190},[154,1070,360],{"class":186},[154,1072,1073],{"class":190}," right.round({c: ",[154,1075,290],{"class":225},[154,1077,1049],{"class":186},[154,1079,1052],{"class":190},[154,1081,1055],{"class":186},[154,1083,1084],{"class":190}," right.select_dtypes(",[154,1086,1061],{"class":163},[154,1088,1064],{"class":190},[154,1090,1091],{"class":115,"line":389},[154,1092,204],{"emptyLinePlaceholder":203},[154,1094,1095,1098,1100,1103,1106,1108,1110,1113,1115,1118],{"class":115,"line":432},[154,1096,1097],{"class":190},"diff ",[154,1099,360],{"class":186},[154,1101,1102],{"class":190}," left.compare(right, ",[154,1104,1105],{"class":356},"result_names",[154,1107,360],{"class":186},[154,1109,668],{"class":190},[154,1111,1112],{"class":163},"\"old\"",[154,1114,229],{"class":190},[154,1116,1117],{"class":163},"\"new\"",[154,1119,1120],{"class":190},"))\n",[154,1122,1123,1125],{"class":115,"line":474},[154,1124,761],{"class":225},[154,1126,1127],{"class":190},"(diff)\n",[154,1129,1130],{"class":115,"line":519},[154,1131,204],{"emptyLinePlaceholder":203},[154,1133,1134,1137,1139],{"class":115,"line":758},[154,1135,1136],{"class":190},"changes ",[154,1138,360],{"class":186},[154,1140,1141],{"class":190}," (\n",[154,1143,1145,1148,1150,1152,1155,1157,1160],{"class":115,"line":1144},15,[154,1146,1147],{"class":190},"    diff.stack(",[154,1149,41],{"class":225},[154,1151,229],{"class":190},[154,1153,1154],{"class":356},"future_stack",[154,1156,360],{"class":186},[154,1158,1159],{"class":225},"True",[154,1161,376],{"class":190},[154,1163,1165,1168,1170,1172,1175],{"class":115,"line":1164},16,[154,1166,1167],{"class":190},"        .rename_axis([",[154,1169,576],{"class":225},[154,1171,229],{"class":190},[154,1173,1174],{"class":163},"\"Column\"",[154,1176,1177],{"class":190},"])\n",[154,1179,1181],{"class":115,"line":1180},17,[154,1182,1183],{"class":190},"        .reset_index()\n",[154,1185,1187,1190,1193,1195,1198,1200,1202,1204,1207,1210,1212,1215],{"class":115,"line":1186},18,[154,1188,1189],{"class":190},"        .dropna(",[154,1191,1192],{"class":356},"subset",[154,1194,360],{"class":186},[154,1196,1197],{"class":190},"[",[154,1199,1112],{"class":163},[154,1201,229],{"class":190},[154,1203,1117],{"class":163},[154,1205,1206],{"class":190},"], ",[154,1208,1209],{"class":356},"how",[154,1211,360],{"class":186},[154,1213,1214],{"class":163},"\"all\"",[154,1216,376],{"class":190},[154,1218,1220],{"class":115,"line":1219},19,[154,1221,376],{"class":190},[154,1223,1225,1227,1230,1232,1234,1236],{"class":115,"line":1224},20,[154,1226,761],{"class":225},[154,1228,1229],{"class":190},"(changes.to_string(",[154,1231,357],{"class":356},[154,1233,360],{"class":186},[154,1235,363],{"class":225},[154,1237,1120],{"class":190},[10,1239,1240,1243,1244,1247,1248,1251],{},[151,1241,1242],{},"compare"," returns only the rows and columns that differ, with a two-level column index of ",[151,1245,1246],{},"old"," and ",[151,1249,1250],{},"new",". Stacking that into a long frame gives one row per change — key, column, old value, new value — which is the shape a person can read and a spreadsheet can filter.",[10,1253,1254],{},"Rounding before comparing is the detail that decides whether the report is trusted. A price that went through a currency conversion may differ in the fifteenth decimal place; reporting that as a change teaches readers to ignore the diff.",[20,1256,29,1261,29,1264,29,1267,29,1270,29,1275,29,1279,29,1284,29,1287,29,1290,29,1293,29,1298,29,1300,29,1304,29,1307,29,1310,29,1313,29,1316,29,1319,29,1324,29,1328,29,1334,29,1338,29,1342,29,1345,29,1349,29,1351,29,1354,29,1356,29,1358,29,1360,29,1362,29,1365,29,1368,29,1371,29,1373,29,1375,29,1377,29,1382],{"viewBox":1257,"role":23,"ariaLabelledBy":1258,"xmlns":27,"style":28},"0 0 740 236",[1259,1260],"cmp-long-t","cmp-long-d",[31,1262,1263],{"id":1259},"Reshaping a comparison into one row per change",[35,1265,1266],{"id":1260},"The compare output has a two-level column header with old and new under each changed column. Stacking it produces a long table with one row per change carrying the key, the column name, the old value and the new value.",[39,1268],{"x":41,"y":41,"width":42,"height":1269,"fill":44},"236",[69,1271,1274],{"x":1272,"y":1273,"style":109},"170","28","compare() output",[39,1276],{"x":1277,"y":47,"width":1278,"height":1273,"fill":124},"24","292",[69,1280,1283],{"x":50,"y":1281,"style":1282},"59","font-size:11px;font-weight:700;fill:#ffffff","Order_ID",[69,1285,1286],{"x":63,"y":1281,"style":1282},"Quantity",[69,1288,1289],{"x":48,"y":1281,"style":1282},"Unit_Price",[39,1291],{"x":1277,"y":1292,"width":1278,"height":1277,"fill":66,"stroke":132},"68",[69,1294,1297],{"x":63,"y":1295,"style":1296},"85","font-size:10.5px;fill:var(--muted,#5b6780)","old   new",[69,1299,1297],{"x":48,"y":1295,"style":1296},[39,1301],{"x":1277,"y":1302,"width":1278,"height":1303,"fill":44,"stroke":132},"92","26",[69,1305,272],{"x":50,"y":77,"style":1306},"font-size:11px;fill:var(--text,#172033)",[69,1308,1309],{"x":63,"y":77,"style":1306},"2      3",[39,1311],{"x":1277,"y":1312,"width":1278,"height":1303,"fill":44,"stroke":132},"118",[69,1314,313],{"x":50,"y":1315,"style":1306},"136",[69,1317,1318],{"x":48,"y":1315,"style":1306},"12.25  11.95",[115,1320],{"x1":1321,"y1":1322,"x2":1323,"y2":1322,"stroke":58,"style":53},"324","100","368",[121,1325],{"points":1326,"fill":1327},"372,100 360,94 360,106","#0f766e",[69,1329,1333],{"x":1330,"y":1331,"style":1332},"348","88","font-size:11px;fill:var(--teal,#0f9488);text-anchor:middle","stack",[69,1335,1337],{"x":1336,"y":1273,"style":109},"546","one row per change",[39,1339],{"x":1340,"y":47,"width":1341,"height":1273,"fill":1327},"378","336",[69,1343,1283],{"x":1344,"y":1281,"style":1282},"396",[69,1346,1348],{"x":1347,"y":1281,"style":1282},"480","Column",[69,1350,1246],{"x":97,"y":1281,"style":1282},[69,1352,1250],{"x":1353,"y":1281,"style":1282},"660",[39,1355],{"x":1340,"y":1292,"width":1341,"height":1303,"fill":44,"stroke":132},[69,1357,272],{"x":1344,"y":72,"style":1306},[69,1359,1286],{"x":1347,"y":72,"style":1306},[69,1361,290],{"x":97,"y":72,"style":1306},[69,1363,414],{"x":1353,"y":72,"style":1364},"font-size:11px;font-weight:700;fill:var(--teal,#0f9488)",[39,1366],{"x":1340,"y":1367,"width":1341,"height":1303,"fill":131,"stroke":132},"94",[69,1369,313],{"x":1344,"y":1370,"style":1306},"112",[69,1372,1289],{"x":1347,"y":1370,"style":1306},[69,1374,340],{"x":97,"y":1370,"style":1306},[69,1376,465],{"x":1353,"y":1370,"style":1364},[69,1378,1381],{"x":86,"y":1379,"style":1380},"180","font-size:12px;fill:var(--muted,#5b6780);text-anchor:middle","The long shape filters, sorts and pivots; the wide shape does not.",[69,1383,1385],{"x":86,"y":1384,"style":1380},"206","Send the long one to the business, keep the wide one for eyeballing.",[139,1387,1389],{"id":1388},"write-a-difference-report","Write a difference report",[10,1391,1392],{},"Four sheets, colour-coded, and the summary first:",[144,1394,1396],{"className":177,"code":1395,"language":179,"meta":149,"style":149},"import pandas as pd\nfrom openpyxl import load_workbook\nfrom openpyxl.styles import PatternFill\n\nsummary = pd.DataFrame([\n    {\"Measure\": \"Rows in old\", \"Value\": len(old)},\n    {\"Measure\": \"Rows in new\", \"Value\": len(new)},\n    {\"Measure\": \"Added\", \"Value\": len(added)},\n    {\"Measure\": \"Removed\", \"Value\": len(removed)},\n    {\"Measure\": \"Cells changed\", \"Value\": len(changes)},\n])\n\nwith pd.ExcelWriter(\"orders_diff.xlsx\", engine=\"openpyxl\") as writer:\n    summary.to_excel(writer, sheet_name=\"Summary\", index=False)\n    changes.to_excel(writer, sheet_name=\"Changed\", index=False)\n    added.reset_index().to_excel(writer, sheet_name=\"Added\", index=False)\n    removed.reset_index().to_excel(writer, sheet_name=\"Removed\", index=False)\n\nwb = load_workbook(\"orders_diff.xlsx\")\nfor sheet, colour in ((\"Added\", \"C6EFCE\"), (\"Removed\", \"FFC7CE\"), (\"Changed\", \"FFEB9C\")):\n    ws = wb[sheet]\n    fill = PatternFill(\"solid\", start_color=colour)\n    for row in ws.iter_rows(min_row=2):\n        for cell in row:\n            cell.fill = fill\nwb.active = 0\nwb.save(\"orders_diff.xlsx\")\nprint(\"wrote orders_diff.xlsx\")\n",[151,1397,1398,1408,1421,1433,1437,1447,1471,1493,1515,1537,1559,1563,1567,1596,1618,1640,1661,1682,1686,1700,1742,1753,1777,1800,1814,1825,1836,1846],{"__ignoreMap":149},[154,1399,1400,1402,1404,1406],{"class":115,"line":156},[154,1401,187],{"class":186},[154,1403,191],{"class":190},[154,1405,194],{"class":186},[154,1407,197],{"class":190},[154,1409,1410,1413,1416,1418],{"class":115,"line":200},[154,1411,1412],{"class":186},"from",[154,1414,1415],{"class":190}," openpyxl ",[154,1417,187],{"class":186},[154,1419,1420],{"class":190}," load_workbook\n",[154,1422,1423,1425,1428,1430],{"class":115,"line":207},[154,1424,1412],{"class":186},[154,1426,1427],{"class":190}," openpyxl.styles ",[154,1429,187],{"class":186},[154,1431,1432],{"class":190}," PatternFill\n",[154,1434,1435],{"class":115,"line":213},[154,1436,204],{"emptyLinePlaceholder":203},[154,1438,1439,1442,1444],{"class":115,"line":263},[154,1440,1441],{"class":190},"summary ",[154,1443,360],{"class":186},[154,1445,1446],{"class":190}," pd.DataFrame([\n",[154,1448,1449,1451,1454,1456,1459,1461,1464,1466,1468],{"class":115,"line":304},[154,1450,216],{"class":190},[154,1452,1453],{"class":163},"\"Measure\"",[154,1455,222],{"class":190},[154,1457,1458],{"class":163},"\"Rows in old\"",[154,1460,229],{"class":190},[154,1462,1463],{"class":163},"\"Value\"",[154,1465,222],{"class":190},[154,1467,919],{"class":225},[154,1469,1470],{"class":190},"(old)},\n",[154,1472,1473,1475,1477,1479,1482,1484,1486,1488,1490],{"class":115,"line":345},[154,1474,216],{"class":190},[154,1476,1453],{"class":163},[154,1478,222],{"class":190},[154,1480,1481],{"class":163},"\"Rows in new\"",[154,1483,229],{"class":190},[154,1485,1463],{"class":163},[154,1487,222],{"class":190},[154,1489,919],{"class":225},[154,1491,1492],{"class":190},"(new)},\n",[154,1494,1495,1497,1499,1501,1504,1506,1508,1510,1512],{"class":115,"line":379},[154,1496,216],{"class":190},[154,1498,1453],{"class":163},[154,1500,222],{"class":190},[154,1502,1503],{"class":163},"\"Added\"",[154,1505,229],{"class":190},[154,1507,1463],{"class":163},[154,1509,222],{"class":190},[154,1511,919],{"class":225},[154,1513,1514],{"class":190},"(added)},\n",[154,1516,1517,1519,1521,1523,1526,1528,1530,1532,1534],{"class":115,"line":384},[154,1518,216],{"class":190},[154,1520,1453],{"class":163},[154,1522,222],{"class":190},[154,1524,1525],{"class":163},"\"Removed\"",[154,1527,229],{"class":190},[154,1529,1463],{"class":163},[154,1531,222],{"class":190},[154,1533,919],{"class":225},[154,1535,1536],{"class":190},"(removed)},\n",[154,1538,1539,1541,1543,1545,1548,1550,1552,1554,1556],{"class":115,"line":389},[154,1540,216],{"class":190},[154,1542,1453],{"class":163},[154,1544,222],{"class":190},[154,1546,1547],{"class":163},"\"Cells changed\"",[154,1549,229],{"class":190},[154,1551,1463],{"class":163},[154,1553,222],{"class":190},[154,1555,919],{"class":225},[154,1557,1558],{"class":190},"(changes)},\n",[154,1560,1561],{"class":115,"line":432},[154,1562,1177],{"class":190},[154,1564,1565],{"class":115,"line":474},[154,1566,204],{"emptyLinePlaceholder":203},[154,1568,1569,1572,1575,1578,1580,1583,1585,1588,1591,1593],{"class":115,"line":519},[154,1570,1571],{"class":186},"with",[154,1573,1574],{"class":190}," pd.ExcelWriter(",[154,1576,1577],{"class":163},"\"orders_diff.xlsx\"",[154,1579,229],{"class":190},[154,1581,1582],{"class":356},"engine",[154,1584,360],{"class":186},[154,1586,1587],{"class":163},"\"openpyxl\"",[154,1589,1590],{"class":190},") ",[154,1592,194],{"class":186},[154,1594,1595],{"class":190}," writer:\n",[154,1597,1598,1601,1603,1605,1608,1610,1612,1614,1616],{"class":115,"line":758},[154,1599,1600],{"class":190},"    summary.to_excel(writer, ",[154,1602,368],{"class":356},[154,1604,360],{"class":186},[154,1606,1607],{"class":163},"\"Summary\"",[154,1609,229],{"class":190},[154,1611,357],{"class":356},[154,1613,360],{"class":186},[154,1615,363],{"class":225},[154,1617,376],{"class":190},[154,1619,1620,1623,1625,1627,1630,1632,1634,1636,1638],{"class":115,"line":1144},[154,1621,1622],{"class":190},"    changes.to_excel(writer, ",[154,1624,368],{"class":356},[154,1626,360],{"class":186},[154,1628,1629],{"class":163},"\"Changed\"",[154,1631,229],{"class":190},[154,1633,357],{"class":356},[154,1635,360],{"class":186},[154,1637,363],{"class":225},[154,1639,376],{"class":190},[154,1641,1642,1645,1647,1649,1651,1653,1655,1657,1659],{"class":115,"line":1164},[154,1643,1644],{"class":190},"    added.reset_index().to_excel(writer, ",[154,1646,368],{"class":356},[154,1648,360],{"class":186},[154,1650,1503],{"class":163},[154,1652,229],{"class":190},[154,1654,357],{"class":356},[154,1656,360],{"class":186},[154,1658,363],{"class":225},[154,1660,376],{"class":190},[154,1662,1663,1666,1668,1670,1672,1674,1676,1678,1680],{"class":115,"line":1180},[154,1664,1665],{"class":190},"    removed.reset_index().to_excel(writer, ",[154,1667,368],{"class":356},[154,1669,360],{"class":186},[154,1671,1525],{"class":163},[154,1673,229],{"class":190},[154,1675,357],{"class":356},[154,1677,360],{"class":186},[154,1679,363],{"class":225},[154,1681,376],{"class":190},[154,1683,1684],{"class":115,"line":1186},[154,1685,204],{"emptyLinePlaceholder":203},[154,1687,1688,1691,1693,1696,1698],{"class":115,"line":1219},[154,1689,1690],{"class":190},"wb ",[154,1692,360],{"class":186},[154,1694,1695],{"class":190}," load_workbook(",[154,1697,1577],{"class":163},[154,1699,376],{"class":190},[154,1701,1702,1705,1708,1710,1713,1715,1717,1720,1723,1725,1727,1730,1732,1734,1736,1739],{"class":115,"line":1224},[154,1703,1704],{"class":186},"for",[154,1706,1707],{"class":190}," sheet, colour ",[154,1709,1055],{"class":186},[154,1711,1712],{"class":190}," ((",[154,1714,1503],{"class":163},[154,1716,229],{"class":190},[154,1718,1719],{"class":163},"\"C6EFCE\"",[154,1721,1722],{"class":190},"), (",[154,1724,1525],{"class":163},[154,1726,229],{"class":190},[154,1728,1729],{"class":163},"\"FFC7CE\"",[154,1731,1722],{"class":190},[154,1733,1629],{"class":163},[154,1735,229],{"class":190},[154,1737,1738],{"class":163},"\"FFEB9C\"",[154,1740,1741],{"class":190},")):\n",[154,1743,1745,1748,1750],{"class":115,"line":1744},21,[154,1746,1747],{"class":190},"    ws ",[154,1749,360],{"class":186},[154,1751,1752],{"class":190}," wb[sheet]\n",[154,1754,1756,1759,1761,1764,1767,1769,1772,1774],{"class":115,"line":1755},22,[154,1757,1758],{"class":190},"    fill ",[154,1760,360],{"class":186},[154,1762,1763],{"class":190}," PatternFill(",[154,1765,1766],{"class":163},"\"solid\"",[154,1768,229],{"class":190},[154,1770,1771],{"class":356},"start_color",[154,1773,360],{"class":186},[154,1775,1776],{"class":190},"colour)\n",[154,1778,1780,1783,1786,1788,1791,1794,1796,1798],{"class":115,"line":1779},23,[154,1781,1782],{"class":186},"    for",[154,1784,1785],{"class":190}," row ",[154,1787,1055],{"class":186},[154,1789,1790],{"class":190}," ws.iter_rows(",[154,1792,1793],{"class":356},"min_row",[154,1795,360],{"class":186},[154,1797,290],{"class":225},[154,1799,612],{"class":190},[154,1801,1803,1806,1809,1811],{"class":115,"line":1802},24,[154,1804,1805],{"class":186},"        for",[154,1807,1808],{"class":190}," cell ",[154,1810,1055],{"class":186},[154,1812,1813],{"class":190}," row:\n",[154,1815,1817,1820,1822],{"class":115,"line":1816},25,[154,1818,1819],{"class":190},"            cell.fill ",[154,1821,360],{"class":186},[154,1823,1824],{"class":190}," fill\n",[154,1826,1828,1831,1833],{"class":115,"line":1827},26,[154,1829,1830],{"class":190},"wb.active ",[154,1832,360],{"class":186},[154,1834,1835],{"class":225}," 0\n",[154,1837,1839,1842,1844],{"class":115,"line":1838},27,[154,1840,1841],{"class":190},"wb.save(",[154,1843,1577],{"class":163},[154,1845,376],{"class":190},[154,1847,1849,1851,1853,1856],{"class":115,"line":1848},28,[154,1850,761],{"class":225},[154,1852,668],{"class":190},[154,1854,1855],{"class":163},"\"wrote orders_diff.xlsx\"",[154,1857,376],{"class":190},[10,1859,1860],{},"Green for added, red for removed and amber for changed are the conventions readers already know from Excel's own cell styles, so the report needs no key. The summary sheet is what makes it skimmable: most months the honest answer is \"three cells changed\", and a reader should be able to see that without scrolling.",[139,1862,1864],{"id":1863},"comparing-formulas-instead-of-values","Comparing formulas instead of values",[10,1866,1867,1868,1872,1873,1876],{},"pandas sees values. When the question is whether someone edited the ",[1869,1870,1871],"em",{},"model"," — a changed ",[151,1874,1875],{},"SUM"," range, a hardcoded number replacing a formula — read both files with openpyxl instead:",[144,1878,1880],{"className":177,"code":1879,"language":179,"meta":149,"style":149},"from openpyxl import load_workbook\n\na = load_workbook(\"model_v1.xlsx\")[\"Summary\"]\nb = load_workbook(\"model_v2.xlsx\")[\"Summary\"]\n\nrows = max(a.max_row, b.max_row)\ncols = max(a.max_column, b.max_column)\n\nfor r in range(1, rows + 1):\n    for c in range(1, cols + 1):\n        old_value, new_value = a.cell(r, c).value, b.cell(r, c).value\n        if old_value != new_value:\n            print(f\"{a.cell(r, c).coordinate}: {old_value!r} -> {new_value!r}\")\n",[151,1881,1882,1892,1896,1915,1933,1937,1950,1962,1966,1993,2016,2026,2040],{"__ignoreMap":149},[154,1883,1884,1886,1888,1890],{"class":115,"line":156},[154,1885,1412],{"class":186},[154,1887,1415],{"class":190},[154,1889,187],{"class":186},[154,1891,1420],{"class":190},[154,1893,1894],{"class":115,"line":200},[154,1895,204],{"emptyLinePlaceholder":203},[154,1897,1898,1901,1903,1905,1908,1911,1913],{"class":115,"line":207},[154,1899,1900],{"class":190},"a ",[154,1902,360],{"class":186},[154,1904,1695],{"class":190},[154,1906,1907],{"class":163},"\"model_v1.xlsx\"",[154,1909,1910],{"class":190},")[",[154,1912,1607],{"class":163},[154,1914,798],{"class":190},[154,1916,1917,1920,1922,1924,1927,1929,1931],{"class":115,"line":213},[154,1918,1919],{"class":190},"b ",[154,1921,360],{"class":186},[154,1923,1695],{"class":190},[154,1925,1926],{"class":163},"\"model_v2.xlsx\"",[154,1928,1910],{"class":190},[154,1930,1607],{"class":163},[154,1932,798],{"class":190},[154,1934,1935],{"class":115,"line":263},[154,1936,204],{"emptyLinePlaceholder":203},[154,1938,1939,1942,1944,1947],{"class":115,"line":304},[154,1940,1941],{"class":190},"rows ",[154,1943,360],{"class":186},[154,1945,1946],{"class":225}," max",[154,1948,1949],{"class":190},"(a.max_row, b.max_row)\n",[154,1951,1952,1955,1957,1959],{"class":115,"line":345},[154,1953,1954],{"class":190},"cols ",[154,1956,360],{"class":186},[154,1958,1946],{"class":225},[154,1960,1961],{"class":190},"(a.max_column, b.max_column)\n",[154,1963,1964],{"class":115,"line":379},[154,1965,204],{"emptyLinePlaceholder":203},[154,1967,1968,1970,1973,1975,1978,1980,1982,1985,1988,1991],{"class":115,"line":384},[154,1969,1704],{"class":186},[154,1971,1972],{"class":190}," r ",[154,1974,1055],{"class":186},[154,1976,1977],{"class":225}," range",[154,1979,668],{"class":190},[154,1981,836],{"class":225},[154,1983,1984],{"class":190},", rows ",[154,1986,1987],{"class":186},"+",[154,1989,1990],{"class":225}," 1",[154,1992,612],{"class":190},[154,1994,1995,1997,1999,2001,2003,2005,2007,2010,2012,2014],{"class":115,"line":389},[154,1996,1782],{"class":186},[154,1998,1052],{"class":190},[154,2000,1055],{"class":186},[154,2002,1977],{"class":225},[154,2004,668],{"class":190},[154,2006,836],{"class":225},[154,2008,2009],{"class":190},", cols ",[154,2011,1987],{"class":186},[154,2013,1990],{"class":225},[154,2015,612],{"class":190},[154,2017,2018,2021,2023],{"class":115,"line":432},[154,2019,2020],{"class":190},"        old_value, new_value ",[154,2022,360],{"class":186},[154,2024,2025],{"class":190}," a.cell(r, c).value, b.cell(r, c).value\n",[154,2027,2028,2031,2034,2037],{"class":115,"line":474},[154,2029,2030],{"class":186},"        if",[154,2032,2033],{"class":190}," old_value ",[154,2035,2036],{"class":186},"!=",[154,2038,2039],{"class":190}," new_value:\n",[154,2041,2042,2045,2047,2049,2051,2053,2056,2058,2060,2062,2065,2068,2070,2073,2075,2078,2080,2082,2084],{"class":115,"line":519},[154,2043,2044],{"class":225},"            print",[154,2046,668],{"class":190},[154,2048,671],{"class":186},[154,2050,674],{"class":163},[154,2052,678],{"class":677},[154,2054,2055],{"class":190},"a.cell(r, c).coordinate",[154,2057,684],{"class":677},[154,2059,222],{"class":163},[154,2061,678],{"class":677},[154,2063,2064],{"class":190},"old_value",[154,2066,2067],{"class":186},"!r",[154,2069,684],{"class":677},[154,2071,2072],{"class":163}," -> ",[154,2074,678],{"class":677},[154,2076,2077],{"class":190},"new_value",[154,2079,2067],{"class":186},[154,2081,684],{"class":677},[154,2083,674],{"class":163},[154,2085,376],{"class":190},[10,2087,2088,2089,2093],{},"This is the check to run on a template before it goes into production, and it pairs well with ",[14,2090,2092],{"href":2091},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fconvert-excel-formulas-to-values-with-python\u002F","freezing formulas into values"," for the copy you distribute — the model stays comparable, the distributed file cannot drift.",[139,2095,2097],{"id":2096},"decide-what-counts-as-a-change","Decide what counts as a change",[10,2099,2100],{},"Not every difference is worth reporting, and a diff that reports everything gets ignored. Agree the tolerances once, per column type:",[20,2102,29,2107,29,2110,29,2113,29,2116,29,2121,29,2127,29,2131,29,2135,29,2139,29,2144,29,2147,29,2151,29,2153,29,2157,29,2160,29,2163,29,2166,29,2169,29,2172,29,2175,29,2178,29,2182,29,2185],{"viewBox":2103,"role":23,"ariaLabelledBy":2104,"xmlns":27,"style":28},"0 0 740 214",[2105,2106],"cmp-tol-t","cmp-tol-d",[31,2108,2109],{"id":2105},"Tolerances that keep a diff meaningful",[35,2111,2112],{"id":2106},"Money is compared to two decimal places, quantities exactly, text after trimming and case folding, dates by calendar day ignoring any time component, and identifiers exactly with no normalisation at all.",[39,2114],{"x":41,"y":41,"width":42,"height":2115,"fill":44},"214",[39,2117],{"x":1277,"y":1277,"width":2118,"height":2119,"rx":2120,"fill":66},"692","34","8",[69,2122,2126],{"x":2123,"y":2124,"style":2125},"44","46","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca)","column type",[69,2128,2130],{"x":2129,"y":2124,"style":2125},"330","compare how",[69,2132,2134],{"x":2133,"y":2124,"style":2125},"560","why",[39,2136],{"x":1277,"y":2137,"width":2118,"height":108,"rx":2138,"fill":44,"stroke":132},"62","6",[69,2140,2143],{"x":2123,"y":2141,"style":2142},"82","font-size:11.5px;fill:var(--text,#172033)","money",[69,2145,2146],{"x":2129,"y":2141,"style":2142},"round to 2 dp",[69,2148,2150],{"x":2133,"y":2141,"style":2149},"font-size:11.5px;fill:var(--muted,#5b6780)","float noise is not a change",[39,2152],{"x":1277,"y":1367,"width":2118,"height":108,"rx":2138,"fill":131,"stroke":132},[69,2154,2156],{"x":2123,"y":2155,"style":2142},"114","quantities",[69,2158,2159],{"x":2129,"y":2155,"style":2142},"exact",[69,2161,2162],{"x":2133,"y":2155,"style":2149},"a unit is a unit",[39,2164],{"x":1277,"y":2165,"width":2118,"height":108,"rx":2138,"fill":44,"stroke":132},"126",[69,2167,69],{"x":2123,"y":2168,"style":2142},"146",[69,2170,2171],{"x":2129,"y":2168,"style":2142},"trim and casefold first",[69,2173,2174],{"x":2133,"y":2168,"style":2149},"retyping is not editing",[39,2176],{"x":1277,"y":2177,"width":2118,"height":108,"rx":2138,"fill":131,"stroke":132},"158",[69,2179,2181],{"x":2123,"y":2180,"style":2142},"178","dates \u002F identifiers",[69,2183,2184],{"x":2129,"y":2180,"style":2142},"calendar day \u002F exact",[69,2186,2187],{"x":2133,"y":2180,"style":2149},"times drift, ids must not",[10,2189,2190,2191,2194,2195,2198],{},"Identifiers are the one column type that should never be normalised for comparison. If ",[151,2192,2193],{},"A-100"," becomes ",[151,2196,2197],{},"a-100"," between two versions, that genuinely is a change — one that will break the next join — and quietly folding the case hides exactly the problem the diff exists to find.",[139,2200,2202],{"id":2201},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[2204,2205,2206,2222],"table",{},[2207,2208,2209],"thead",{},[2210,2211,2212,2216,2219],"tr",{},[2213,2214,2215],"th",{},"Symptom",[2213,2217,2218],{},"Cause",[2213,2220,2221],{},"Fix",[2223,2224,2225,2240,2257,2268,2283,2294],"tbody",{},[2210,2226,2227,2231,2234],{},[2228,2229,2230],"td",{},"Every row reported as changed",[2228,2232,2233],{},"Compared by position after a re-sort",[2228,2235,2236,2237],{},"Align on a key with ",[151,2238,2239],{},"set_index",[2210,2241,2242,2247,2250],{},[2228,2243,2244],{},[151,2245,2246],{},"ValueError: cannot compare",[2228,2248,2249],{},"Different columns in the two files",[2228,2251,2252,2253,2256],{},"Compare on ",[151,2254,2255],{},"columns.intersection"," and report the rest separately",[2210,2258,2259,2262,2265],{},[2228,2260,2261],{},"Numbers differ by 1e-12",[2228,2263,2264],{},"Floating-point noise",[2228,2266,2267],{},"Round both frames before comparing",[2210,2269,2270,2277,2280],{},[2228,2271,2272,2273,2276],{},"Blank versus ",[151,2274,2275],{},"NaN"," reported as a change",[2228,2278,2279],{},"Different empty representations",[2228,2281,2282],{},"Normalise empties before comparing",[2210,2284,2285,2288,2291],{},[2228,2286,2287],{},"Diff is empty but the files differ",[2228,2289,2290],{},"Compared the wrong sheet",[2228,2292,2293],{},"Name the sheet explicitly on both reads",[2210,2295,2296,2299,2302],{},[2228,2297,2298],{},"Key not unique error",[2228,2300,2301],{},"Source has genuine duplicates",[2228,2303,2304],{},"Deduplicate or extend the key before diffing",[139,2306,2308],{"id":2307},"keep-the-diff-in-the-pipeline","Keep the diff in the pipeline",[10,2310,2311],{},"Running the comparison by hand answers this month's question. Running it automatically answers the question nobody thought to ask — the one where a source system silently changed its history:",[144,2313,2315],{"className":177,"code":2314,"language":179,"meta":149,"style":149},"import sys\n\ndef diff_exit_code(added, removed, changes, tolerance=0.02):\n    \"\"\"0 = as expected, 1 = worth a look, 2 = the past changed.\"\"\"\n    if len(removed):\n        return 2                                   # historical rows disappeared\n    churn = (len(added) + len(changes)) \u002F max(len(new), 1)\n    return 1 if churn > tolerance else 0\n\ncode = diff_exit_code(added, removed, changes)\nprint(\"diff exit code:\", code)\nsys.exit(code)\n",[151,2316,2317,2324,2328,2345,2350,2360,2371,2409,2432,2436,2446,2458],{"__ignoreMap":149},[154,2318,2319,2321],{"class":115,"line":156},[154,2320,187],{"class":186},[154,2322,2323],{"class":190}," sys\n",[154,2325,2326],{"class":115,"line":200},[154,2327,204],{"emptyLinePlaceholder":203},[154,2329,2330,2332,2335,2338,2340,2343],{"class":115,"line":207},[154,2331,591],{"class":186},[154,2333,2334],{"class":594}," diff_exit_code",[154,2336,2337],{"class":190},"(added, removed, changes, tolerance",[154,2339,360],{"class":186},[154,2341,2342],{"class":225},"0.02",[154,2344,612],{"class":190},[154,2346,2347],{"class":115,"line":213},[154,2348,2349],{"class":163},"    \"\"\"0 = as expected, 1 = worth a look, 2 = the past changed.\"\"\"\n",[154,2351,2352,2354,2357],{"class":115,"line":263},[154,2353,634],{"class":186},[154,2355,2356],{"class":225}," len",[154,2358,2359],{"class":190},"(removed):\n",[154,2361,2362,2365,2368],{"class":115,"line":304},[154,2363,2364],{"class":186},"        return",[154,2366,2367],{"class":225}," 2",[154,2369,2370],{"class":428},"                                   # historical rows disappeared\n",[154,2372,2373,2376,2378,2381,2383,2386,2388,2390,2393,2396,2398,2400,2402,2405,2407],{"class":115,"line":345},[154,2374,2375],{"class":190},"    churn ",[154,2377,360],{"class":186},[154,2379,2380],{"class":190}," (",[154,2382,919],{"class":225},[154,2384,2385],{"class":190},"(added) ",[154,2387,1987],{"class":186},[154,2389,2356],{"class":225},[154,2391,2392],{"class":190},"(changes)) ",[154,2394,2395],{"class":186},"\u002F",[154,2397,1946],{"class":225},[154,2399,668],{"class":190},[154,2401,919],{"class":225},[154,2403,2404],{"class":190},"(new), ",[154,2406,836],{"class":225},[154,2408,376],{"class":190},[154,2410,2411,2413,2415,2418,2421,2424,2427,2430],{"class":115,"line":379},[154,2412,721],{"class":186},[154,2414,1990],{"class":225},[154,2416,2417],{"class":186}," if",[154,2419,2420],{"class":190}," churn ",[154,2422,2423],{"class":186},">",[154,2425,2426],{"class":190}," tolerance ",[154,2428,2429],{"class":186},"else",[154,2431,1835],{"class":225},[154,2433,2434],{"class":115,"line":384},[154,2435,204],{"emptyLinePlaceholder":203},[154,2437,2438,2441,2443],{"class":115,"line":389},[154,2439,2440],{"class":190},"code ",[154,2442,360],{"class":186},[154,2444,2445],{"class":190}," diff_exit_code(added, removed, changes)\n",[154,2447,2448,2450,2452,2455],{"class":115,"line":432},[154,2449,761],{"class":225},[154,2451,668],{"class":190},[154,2453,2454],{"class":163},"\"diff exit code:\"",[154,2456,2457],{"class":190},", code)\n",[154,2459,2460],{"class":115,"line":474},[154,2461,2462],{"class":190},"sys.exit(code)\n",[10,2464,2465],{},"Treating removals as the most serious outcome reflects how these files behave in practice: rows are appended constantly, edited occasionally, and deleted almost never. A month where three historical orders vanished is worth a phone call even if every other number matches, and a scheduler that reads exit codes turns that judgement into an alert without anyone opening the workbook.",[139,2467,2469],{"id":2468},"performance-and-scale-notes","Performance and scale notes",[10,2471,2472,2473,2476,2477,2479],{},"Indexing and set operations are hash-based and scale well; the cost is dominated by reading two workbooks. For large files, read only the columns you intend to compare with ",[151,2474,2475],{},"usecols",", and consider caching the previous month's frame as Parquet so only the new workbook has to be parsed. ",[151,2478,976],{}," materialises a frame containing every differing cell, which is small in a healthy diff — if it is large, that is itself the finding, and the summary sheet should say so rather than the report attempting to list a hundred thousand changes.",[139,2481,2483],{"id":2482},"conclusion","Conclusion",[10,2485,2486],{},"A workbook diff is three questions answered in order: which keys left, which arrived, and which shared rows changed. Align on a business key rather than on row position, verify the key is unique before trusting anything, round numerics to the precision you care about, and reshape the comparison into one row per change. Colour the output the way Excel does, put the counts on a summary sheet, and the monthly \"what changed?\" conversation takes a minute.",[139,2488,2490],{"id":2489},"frequently-asked-questions","Frequently asked questions",[10,2492,2493,2497],{},[2494,2495,2496],"strong",{},"Do the two files need the same row order?","\nNo, and they should not be compared by position. Align both frames on a business key so a re-sorted export does not read as every row having changed.",[10,2499,2500,2503],{},[2494,2501,2502],{},"What if there is no unique key?","\nBuild a composite key from the columns that identify a record — date, customer and product, say — and verify it is unique in both files before comparing.",[10,2505,2506,2509],{},[2494,2507,2508],{},"Why do identical-looking numbers show as changed?","\nFloating-point values that differ far below the visible precision still compare unequal. Round to the precision you actually care about before comparing.",[10,2511,2512,2515,2516,2519],{},[2494,2513,2514],{},"Can I compare formulas rather than values?","\nYes — read with openpyxl instead of pandas and compare ",[151,2517,2518],{},"cell.value"," on both workbooks, which gives you the formula text rather than the cached result.",[139,2521,2523],{"id":2522},"related","Related",[10,2525,2526],{},"Up to the parent guide:",[2528,2529,2530],"ul",{},[2531,2532,2533,2535],"li",{},[14,2534,17],{"href":16}," — validation across versions as well as within one file.",[10,2537,2538],{},"Related guides:",[2528,2540,2541,2548,2555,2562],{},[2531,2542,2543,2547],{},[14,2544,2546],{"href":2545},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python\u002F","Find Duplicate Rows in Excel with Python"," — the key-uniqueness check this comparison depends on.",[2531,2549,2550,2554],{},[14,2551,2553],{"href":2552},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-two-excel-files-on-common-column-python\u002F","Merge Two Excel Files on a Common Column with Python"," — joining files rather than diffing them.",[2531,2556,2557,2561],{},[14,2558,2560],{"href":2559},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python\u002F","Highlight Invalid Cells in Excel with Python"," — the same colour conventions applied to validation.",[2531,2563,2564,2568],{},[14,2565,2567],{"href":2566},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fcombine-multiple-excel-files-into-one-python\u002F","Combine Multiple Excel Files into One with Python"," — when the versions should be stacked instead.",[2570,2571,2572],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":149,"searchDepth":200,"depth":200,"links":2574},[2575,2576,2577,2578,2579,2580,2581,2582,2583,2584,2585,2586,2587,2588],{"id":141,"depth":200,"text":142},{"id":173,"depth":200,"text":174},{"id":548,"depth":200,"text":549},{"id":847,"depth":200,"text":848},{"id":970,"depth":200,"text":971},{"id":1388,"depth":200,"text":1389},{"id":1863,"depth":200,"text":1864},{"id":2096,"depth":200,"text":2097},{"id":2201,"depth":200,"text":2202},{"id":2307,"depth":200,"text":2308},{"id":2468,"depth":200,"text":2469},{"id":2482,"depth":200,"text":2483},{"id":2489,"depth":200,"text":2490},{"id":2522,"depth":200,"text":2523},"2026-08-01","Diff two versions of a workbook with pandas — added, removed and changed rows keyed on an identifier, a cell-level change list, and a colour-coded difference report in Excel.","md",[2593,2595,2597,2599],{"q":2496,"a":2594},"No, and they should not be compared by position. Align both frames on a business key so a re-sorted export does not read as every row having changed.",{"q":2502,"a":2596},"Build a composite key from the columns that identify a record — date, customer and product, say — and verify it is unique in both files before comparing.",{"q":2508,"a":2598},"Floating-point values that differ far below the visible precision still compare unequal. Round to the precision you actually care about before comparing.",{"q":2514,"a":2600},"Yes — read with openpyxl instead of pandas and compare cell.value on both workbooks, which gives you the formula text rather than the cached result.",{"breadcrumb":2602},[2603,2605,2608,2609],{"name":2604,"item":2395},"Home",{"name":2606,"item":2607},"Advanced Data Transformation and Cleaning","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":17,"item":16},{"name":5,"item":2610},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python\u002F","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python",{"title":5,"description":2613},"Compare Excel files in Python: align on a key, find added, removed and modified rows, list old and new values per cell with DataFrame.compare, and write a readable diff workbook.","compare-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","how-to","RPZ30FJ1IUZ1sS0eCHeEegezel4QN2PlSsVaW53hhsc",[2619,2623],{"title":2620,"path":2621,"stem":2622,"children":-1},"Check Excel Data Types with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-excel-data-types-with-pandas","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-excel-data-types-with-pandas\u002Findex",{"title":2546,"path":2624,"stem":2625,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python\u002Findex",1785584462739]