[{"data":1,"prerenderedAt":2184},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fcombine-sheets-with-different-layouts-into-one-table":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fcombine-sheets-with-different-layouts-into-one-table":2154,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fcombine-sheets-with-different-layouts-into-one-table":2162,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fcombine-sheets-with-different-layouts-into-one-table":2166,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fcombine-sheets-with-different-layouts-into-one-table":2167},{"id":4,"title":5,"body":6,"dateModified":2120,"datePublished":2120,"description":2121,"extension":2122,"faq":2123,"keyword":2132,"kind":2133,"meta":2134,"navigation":117,"path":2143,"seo":2144,"slug":2147,"stem":2148,"topics":2149,"type":2152,"__hash__":2153},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fcombine-sheets-with-different-layouts-into-one-table\u002Findex.md","Combine Sheets with Different Layouts into One Table",{"type":7,"value":8,"toc":2108},"minimark",[9,45,50,78,85,89,338,352,356,856,871,1138,1142,1546,1556,1721,1725,1880,1901,1972,1976,1993,1997,2036,2040,2043,2047,2053,2059,2065,2071,2075,2104],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,38,39,44],"p",{},"Consolidating \"the same\" data from several sheets is easy when the sheets really are the same. More often each was built by a different person: the North branch puts a title and a date above its table and calls the column ",[14,15,16],"em",{},"Qty",", the South branch starts in row 1 with ",[14,19,20],{},"Units"," and lists the product before the date, the West branch adds a ",[14,23,24],{},"Comments"," column and a total row at the bottom, and one sheet says ",[14,27,28],{},"Net Sales"," where the others say ",[14,31,32],{},"Revenue",". ",[35,36,37],"code",{},"pd.concat"," on such sheets produces a wide, mostly empty table with a column for every spelling, total rows counted as sales and title text in the date column. This page combines inconsistent sheets properly: it finds each sheet's header row from known column names, maps the different spellings to one standard set, keeps the standard columns in a fixed order, drops total and note rows, records which sheet each row came from, and reports any column it could not map — so a new spelling shows up in the log rather than as silently lost data. It belongs to ",[40,41,43],"a",{"href":42},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002F","Extracting Data from Messy Excel Sheets",".",[46,47,49],"h2",{"id":48},"prerequisites","Prerequisites",[51,52,57],"pre",{"className":53,"code":54,"language":55,"meta":56,"style":56},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[35,58,59],{"__ignoreMap":56},[60,61,64,68,72,75],"span",{"class":62,"line":63},"line",1,[60,65,67],{"class":66},"sMTad","pip",[60,69,71],{"class":70},"srMev"," install",[60,73,74],{"class":70}," pandas",[60,76,77],{"class":70}," openpyxl\n",[10,79,80,81,84],{},"The workbook has four branch sheets with the same kind of data — date, product, quantity, revenue — in four layouts: a title block above the header, a different column order, a total row and a note, and a table starting in B3 with lower-case and upper-case headers and an extra ",[14,82,83],{},"Channel"," column.",[46,86,88],{"id":87},"step-1-define-the-standard-columns-and-their-synonyms","Step 1 — Define the standard columns and their synonyms",[51,90,94],{"className":91,"code":92,"language":93,"meta":56,"style":56},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nSTANDARD = [\"date\", \"product\", \"qty\", \"revenue\"]\nSYNONYMS = {\n    \"date\": {\"date\", \"order date\", \"sale date\"},\n    \"product\": {\"product\", \"item\", \"sku\"},\n    \"qty\": {\"qty\", \"units\", \"quantity\"},\n    \"revenue\": {\"revenue\", \"net sales\", \"sales £\", \"sales\"},\n}\nLOOKUP = {alias: std for std, aliases in SYNONYMS.items() for alias in aliases}\n\ndef standard_name(header):\n    return LOOKUP.get(str(header).strip().lower())\n","python",[35,95,96,112,119,154,165,189,211,233,260,266,302,307,320],{"__ignoreMap":56},[60,97,98,102,106,109],{"class":62,"line":63},[60,99,101],{"class":100},"s-kum","import",[60,103,105],{"class":104},"skGVy"," pandas ",[60,107,108],{"class":100},"as",[60,110,111],{"class":104}," pd\n",[60,113,115],{"class":62,"line":114},2,[60,116,118],{"emptyLinePlaceholder":117},true,"\n",[60,120,122,126,129,132,135,138,141,143,146,148,151],{"class":62,"line":121},3,[60,123,125],{"class":124},"sP0c6","STANDARD",[60,127,128],{"class":100}," =",[60,130,131],{"class":104}," [",[60,133,134],{"class":70},"\"date\"",[60,136,137],{"class":104},", ",[60,139,140],{"class":70},"\"product\"",[60,142,137],{"class":104},[60,144,145],{"class":70},"\"qty\"",[60,147,137],{"class":104},[60,149,150],{"class":70},"\"revenue\"",[60,152,153],{"class":104},"]\n",[60,155,157,160,162],{"class":62,"line":156},4,[60,158,159],{"class":124},"SYNONYMS",[60,161,128],{"class":100},[60,163,164],{"class":104}," {\n",[60,166,168,171,174,176,178,181,183,186],{"class":62,"line":167},5,[60,169,170],{"class":70},"    \"date\"",[60,172,173],{"class":104},": {",[60,175,134],{"class":70},[60,177,137],{"class":104},[60,179,180],{"class":70},"\"order date\"",[60,182,137],{"class":104},[60,184,185],{"class":70},"\"sale date\"",[60,187,188],{"class":104},"},\n",[60,190,192,195,197,199,201,204,206,209],{"class":62,"line":191},6,[60,193,194],{"class":70},"    \"product\"",[60,196,173],{"class":104},[60,198,140],{"class":70},[60,200,137],{"class":104},[60,202,203],{"class":70},"\"item\"",[60,205,137],{"class":104},[60,207,208],{"class":70},"\"sku\"",[60,210,188],{"class":104},[60,212,214,217,219,221,223,226,228,231],{"class":62,"line":213},7,[60,215,216],{"class":70},"    \"qty\"",[60,218,173],{"class":104},[60,220,145],{"class":70},[60,222,137],{"class":104},[60,224,225],{"class":70},"\"units\"",[60,227,137],{"class":104},[60,229,230],{"class":70},"\"quantity\"",[60,232,188],{"class":104},[60,234,236,239,241,243,245,248,250,253,255,258],{"class":62,"line":235},8,[60,237,238],{"class":70},"    \"revenue\"",[60,240,173],{"class":104},[60,242,150],{"class":70},[60,244,137],{"class":104},[60,246,247],{"class":70},"\"net sales\"",[60,249,137],{"class":104},[60,251,252],{"class":70},"\"sales £\"",[60,254,137],{"class":104},[60,256,257],{"class":70},"\"sales\"",[60,259,188],{"class":104},[60,261,263],{"class":62,"line":262},9,[60,264,265],{"class":104},"}\n",[60,267,269,272,274,277,280,283,286,289,292,294,297,299],{"class":62,"line":268},10,[60,270,271],{"class":124},"LOOKUP",[60,273,128],{"class":100},[60,275,276],{"class":104}," {alias: std ",[60,278,279],{"class":100},"for",[60,281,282],{"class":104}," std, aliases ",[60,284,285],{"class":100},"in",[60,287,288],{"class":124}," SYNONYMS",[60,290,291],{"class":104},".items() ",[60,293,279],{"class":100},[60,295,296],{"class":104}," alias ",[60,298,285],{"class":100},[60,300,301],{"class":104}," aliases}\n",[60,303,305],{"class":62,"line":304},11,[60,306,118],{"emptyLinePlaceholder":117},[60,308,310,313,317],{"class":62,"line":309},12,[60,311,312],{"class":100},"def",[60,314,316],{"class":315},"s_Opv"," standard_name",[60,318,319],{"class":104},"(header):\n",[60,321,323,326,329,332,335],{"class":62,"line":322},13,[60,324,325],{"class":100},"    return",[60,327,328],{"class":124}," LOOKUP",[60,330,331],{"class":104},".get(",[60,333,334],{"class":124},"str",[60,336,337],{"class":104},"(header).strip().lower())\n",[10,339,340,341,137,344,347,348,351],{},"The synonym table is the single place where knowledge about the sheets' layouts lives: four standard names, each with the spellings seen so far. Matching is done on the trimmed, lower-cased header, so ",[35,342,343],{},"PRODUCT",[35,345,346],{},"Product"," and ",[35,349,350],{},"product"," are the same. When a branch invents a new spelling, adding it here is the whole fix.",[46,353,355],{"id":354},"step-2-find-each-sheets-header-row-and-map-its-columns","Step 2 — Find each sheet's header row and map its columns",[51,357,359],{"className":91,"code":358,"language":93,"meta":56,"style":56},"def read_sheet(path, sheet, min_matches=3):\n    raw = pd.read_excel(path, sheet_name=sheet, header=None)\n    for i, row in raw.iterrows():\n        mapped = [standard_name(v) for v in row]\n        if sum(m is not None for m in mapped) >= min_matches:\n            break\n    else:\n        raise ValueError(f\"{sheet}: no header row with {min_matches} known columns\")\n    body = raw.iloc[i + 1:].copy()\n    body.columns = [standard_name(v) or (str(v) if pd.notna(v) else f\"_blank{j}\") for j, v in enumerate(row)]\n    body = body.loc[:, [not c.startswith(\"_blank\") for c in body.columns]]\n    unmapped = [str(v) for v, m in zip(row, mapped) if m is None and pd.notna(v)]\n    missing = [c for c in STANDARD if c not in body.columns]\n    return body, i, unmapped, missing\n\nwith pd.ExcelFile(\"branches.xlsx\") as xf:\n    sheets = xf.sheet_names\nfor sheet in sheets:\n    body, header_row, unmapped, missing = read_sheet(\"branches.xlsx\", sheet)\n    print(f\"{sheet:\u003C6} header at row {header_row + 1}, unmapped: {unmapped or '-'}, missing: {missing or '-'}\")\n",[35,360,361,380,410,423,443,480,485,493,535,554,614,644,684,716,724,729,748,759,772,788],{"__ignoreMap":56},[60,362,363,365,368,371,374,377],{"class":62,"line":63},[60,364,312],{"class":100},[60,366,367],{"class":315}," read_sheet",[60,369,370],{"class":104},"(path, sheet, min_matches",[60,372,373],{"class":100},"=",[60,375,376],{"class":124},"3",[60,378,379],{"class":104},"):\n",[60,381,382,385,387,390,394,396,399,402,404,407],{"class":62,"line":114},[60,383,384],{"class":104},"    raw ",[60,386,373],{"class":100},[60,388,389],{"class":104}," pd.read_excel(path, ",[60,391,393],{"class":392},"sa561","sheet_name",[60,395,373],{"class":100},[60,397,398],{"class":104},"sheet, ",[60,400,401],{"class":392},"header",[60,403,373],{"class":100},[60,405,406],{"class":124},"None",[60,408,409],{"class":104},")\n",[60,411,412,415,418,420],{"class":62,"line":121},[60,413,414],{"class":100},"    for",[60,416,417],{"class":104}," i, row ",[60,419,285],{"class":100},[60,421,422],{"class":104}," raw.iterrows():\n",[60,424,425,428,430,433,435,438,440],{"class":62,"line":156},[60,426,427],{"class":104},"        mapped ",[60,429,373],{"class":100},[60,431,432],{"class":104}," [standard_name(v) ",[60,434,279],{"class":100},[60,436,437],{"class":104}," v ",[60,439,285],{"class":100},[60,441,442],{"class":104}," row]\n",[60,444,445,448,451,454,457,460,463,466,469,471,474,477],{"class":62,"line":167},[60,446,447],{"class":100},"        if",[60,449,450],{"class":124}," sum",[60,452,453],{"class":104},"(m ",[60,455,456],{"class":100},"is",[60,458,459],{"class":100}," not",[60,461,462],{"class":124}," None",[60,464,465],{"class":100}," for",[60,467,468],{"class":104}," m ",[60,470,285],{"class":100},[60,472,473],{"class":104}," mapped) ",[60,475,476],{"class":100},">=",[60,478,479],{"class":104}," min_matches:\n",[60,481,482],{"class":62,"line":191},[60,483,484],{"class":100},"            break\n",[60,486,487,490],{"class":62,"line":213},[60,488,489],{"class":100},"    else",[60,491,492],{"class":104},":\n",[60,494,495,498,501,504,507,510,514,517,520,523,525,528,530,533],{"class":62,"line":235},[60,496,497],{"class":100},"        raise",[60,499,500],{"class":124}," ValueError",[60,502,503],{"class":104},"(",[60,505,506],{"class":100},"f",[60,508,509],{"class":70},"\"",[60,511,513],{"class":512},"sSjpA","{",[60,515,516],{"class":104},"sheet",[60,518,519],{"class":512},"}",[60,521,522],{"class":70},": no header row with ",[60,524,513],{"class":512},[60,526,527],{"class":104},"min_matches",[60,529,519],{"class":512},[60,531,532],{"class":70}," known columns\"",[60,534,409],{"class":104},[60,536,537,540,542,545,548,551],{"class":62,"line":262},[60,538,539],{"class":104},"    body ",[60,541,373],{"class":100},[60,543,544],{"class":104}," raw.iloc[i ",[60,546,547],{"class":100},"+",[60,549,550],{"class":124}," 1",[60,552,553],{"class":104},":].copy()\n",[60,555,556,559,561,563,566,569,571,574,577,580,583,586,589,591,594,596,598,601,603,606,608,611],{"class":62,"line":268},[60,557,558],{"class":104},"    body.columns ",[60,560,373],{"class":100},[60,562,432],{"class":104},[60,564,565],{"class":100},"or",[60,567,568],{"class":104}," (",[60,570,334],{"class":124},[60,572,573],{"class":104},"(v) ",[60,575,576],{"class":100},"if",[60,578,579],{"class":104}," pd.notna(v) ",[60,581,582],{"class":100},"else",[60,584,585],{"class":100}," f",[60,587,588],{"class":70},"\"_blank",[60,590,513],{"class":512},[60,592,593],{"class":104},"j",[60,595,519],{"class":512},[60,597,509],{"class":70},[60,599,600],{"class":104},") ",[60,602,279],{"class":100},[60,604,605],{"class":104}," j, v ",[60,607,285],{"class":100},[60,609,610],{"class":124}," enumerate",[60,612,613],{"class":104},"(row)]\n",[60,615,616,618,620,623,626,629,632,634,636,639,641],{"class":62,"line":304},[60,617,539],{"class":104},[60,619,373],{"class":100},[60,621,622],{"class":104}," body.loc[:, [",[60,624,625],{"class":100},"not",[60,627,628],{"class":104}," c.startswith(",[60,630,631],{"class":70},"\"_blank\"",[60,633,600],{"class":104},[60,635,279],{"class":100},[60,637,638],{"class":104}," c ",[60,640,285],{"class":100},[60,642,643],{"class":104}," body.columns]]\n",[60,645,646,649,651,653,655,657,659,662,664,667,670,672,674,676,678,681],{"class":62,"line":309},[60,647,648],{"class":104},"    unmapped ",[60,650,373],{"class":100},[60,652,131],{"class":104},[60,654,334],{"class":124},[60,656,573],{"class":104},[60,658,279],{"class":100},[60,660,661],{"class":104}," v, m ",[60,663,285],{"class":100},[60,665,666],{"class":124}," zip",[60,668,669],{"class":104},"(row, mapped) ",[60,671,576],{"class":100},[60,673,468],{"class":104},[60,675,456],{"class":100},[60,677,462],{"class":124},[60,679,680],{"class":100}," and",[60,682,683],{"class":104}," pd.notna(v)]\n",[60,685,686,689,691,694,696,698,700,703,706,708,710,713],{"class":62,"line":322},[60,687,688],{"class":104},"    missing ",[60,690,373],{"class":100},[60,692,693],{"class":104}," [c ",[60,695,279],{"class":100},[60,697,638],{"class":104},[60,699,285],{"class":100},[60,701,702],{"class":124}," STANDARD",[60,704,705],{"class":100}," if",[60,707,638],{"class":104},[60,709,625],{"class":100},[60,711,712],{"class":100}," in",[60,714,715],{"class":104}," body.columns]\n",[60,717,719,721],{"class":62,"line":718},14,[60,720,325],{"class":100},[60,722,723],{"class":104}," body, i, unmapped, missing\n",[60,725,727],{"class":62,"line":726},15,[60,728,118],{"emptyLinePlaceholder":117},[60,730,732,735,738,741,743,745],{"class":62,"line":731},16,[60,733,734],{"class":100},"with",[60,736,737],{"class":104}," pd.ExcelFile(",[60,739,740],{"class":70},"\"branches.xlsx\"",[60,742,600],{"class":104},[60,744,108],{"class":100},[60,746,747],{"class":104}," xf:\n",[60,749,751,754,756],{"class":62,"line":750},17,[60,752,753],{"class":104},"    sheets ",[60,755,373],{"class":100},[60,757,758],{"class":104}," xf.sheet_names\n",[60,760,762,764,767,769],{"class":62,"line":761},18,[60,763,279],{"class":100},[60,765,766],{"class":104}," sheet ",[60,768,285],{"class":100},[60,770,771],{"class":104}," sheets:\n",[60,773,775,778,780,783,785],{"class":62,"line":774},19,[60,776,777],{"class":104},"    body, header_row, unmapped, missing ",[60,779,373],{"class":100},[60,781,782],{"class":104}," read_sheet(",[60,784,740],{"class":70},[60,786,787],{"class":104},", sheet)\n",[60,789,791,794,796,798,800,802,804,807,809,812,814,817,819,821,823,826,828,831,833,836,838,841,843,846,848,850,852,854],{"class":62,"line":790},20,[60,792,793],{"class":124},"    print",[60,795,503],{"class":104},[60,797,506],{"class":100},[60,799,509],{"class":70},[60,801,513],{"class":512},[60,803,516],{"class":104},[60,805,806],{"class":100},":\u003C6",[60,808,519],{"class":512},[60,810,811],{"class":70}," header at row ",[60,813,513],{"class":512},[60,815,816],{"class":104},"header_row ",[60,818,547],{"class":100},[60,820,550],{"class":124},[60,822,519],{"class":512},[60,824,825],{"class":70},", unmapped: ",[60,827,513],{"class":512},[60,829,830],{"class":104},"unmapped ",[60,832,565],{"class":100},[60,834,835],{"class":70}," '-'",[60,837,519],{"class":512},[60,839,840],{"class":70},", missing: ",[60,842,513],{"class":512},[60,844,845],{"class":104},"missing ",[60,847,565],{"class":100},[60,849,835],{"class":70},[60,851,519],{"class":512},[60,853,509],{"class":70},[60,855,409],{"class":104},[10,857,858,859,862,863,137,866,347,868,870],{},"Reading with ",[35,860,861],{},"header=None"," gives the raw grid, so the code can look for the header itself: the first row in which at least three cells are known column names. That skips the North sheet's title block and finds the East table at row 3 wherever it starts. Each column is renamed to its standard name; columns with no header at all, such as the empty column A on the East sheet, are dropped; columns with an unknown header keep their original name and are reported — ",[14,864,865],{},"Rep",[14,867,24],{},[14,869,83],{}," here — so nothing disappears without a trace. A sheet missing a standard column is reported too.",[872,873,881,882,881,886,881,890,881,898,881,907,881,911,881,918,881,922,881,926,881,930,881,934,881,938,881,943,881,945,881,949,881,951,881,954,881,956,881,959,881,962,881,966,881,968,881,972,881,974,881,976,881,978,881,981,881,984,881,987,881,989,881,992,881,994,881,996,881,998,881,1001,881,1004,881,1007,881,1009,881,1012,881,1014,881,1016,881,1018,881,1021,881,1024,881,1028,881,1030,881,1033,881,1035,881,1037,881,1039,881,1042,881,1045,881,1049,881,1052,881,1054,881,1057,881,1062,881,1066,881,1068,881,1070,881,1072,881,1075,881,1077,881,1079,881,1081,881,1084,881,1086,881,1088,881,1090,881,1093,881,1095,881,1097,881,1099,881,1102,881,1108,881,1112,881,1119,881,1124,881,1132],"svg",{"viewBox":874,"role":875,"ariaLabelledBy":876,"xmlns":879,"style":880},"4 0 752 220","img",[877,878],"cs-map-t","cs-map-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  ",[883,884,885],"title",{"id":877},"Four layouts, one standard set of columns",[887,888,889],"desc",{"id":878},"The branch sheets use different header rows, column orders and names — Qty, Units, Quantity; Revenue, Net Sales, Sales £; Product, Item, PRODUCT. A synonym map renames them to date, product, qty and revenue in a fixed order, and extra columns are reported.",[891,892],"rect",{"x":893,"y":894,"width":895,"height":896,"fill":897},"4","0","752","220","#ffffff",[891,899],{"x":900,"y":901,"width":902,"height":903,"rx":894,"fill":904,"stroke":905,"style":906},"20.0","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[891,908],{"x":909,"y":901,"width":910,"height":903,"rx":894,"fill":904,"stroke":905,"style":906},"52.0","59.6",[912,913,917],"text",{"x":914,"y":915,"style":916},"81.8","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[891,919],{"x":920,"y":901,"width":921,"height":903,"rx":894,"fill":904,"stroke":905,"style":906},"111.6","94.3",[912,923,925],{"x":924,"y":915,"style":916},"158.7","B",[891,927],{"x":928,"y":901,"width":929,"height":903,"rx":894,"fill":904,"stroke":905,"style":906},"205.9","146.9",[912,931,933],{"x":932,"y":915,"style":916},"279.3","C",[891,935],{"x":900,"y":936,"width":902,"height":937,"rx":894,"fill":904,"stroke":905,"style":906},"64.0","28.0",[912,939,942],{"x":940,"y":941,"style":916},"36.0","83.0","1",[891,944],{"x":909,"y":936,"width":910,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,946,948],{"x":914,"y":941,"style":947},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Sheet",[891,950],{"x":920,"y":936,"width":921,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,952,953],{"x":924,"y":941,"style":947},"Header row",[891,955],{"x":928,"y":936,"width":929,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,957,958],{"x":932,"y":941,"style":947},"Columns",[891,960],{"x":900,"y":961,"width":902,"height":937,"rx":894,"fill":904,"stroke":905,"style":906},"92.0",[912,963,965],{"x":940,"y":964,"style":916},"111.0","2",[891,967],{"x":909,"y":961,"width":910,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,969,971],{"x":914,"y":964,"style":970},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","North",[891,973],{"x":920,"y":961,"width":921,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,975,893],{"x":924,"y":964,"style":970},[891,977],{"x":928,"y":961,"width":929,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,979,980],{"x":932,"y":964,"style":970},"Date, Qty, …",[891,982],{"x":900,"y":983,"width":902,"height":937,"rx":894,"fill":904,"stroke":905,"style":906},"120.0",[912,985,376],{"x":940,"y":986,"style":916},"139.0",[891,988],{"x":909,"y":983,"width":910,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,990,991],{"x":914,"y":986,"style":970},"South",[891,993],{"x":920,"y":983,"width":921,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,995,942],{"x":924,"y":986,"style":970},[891,997],{"x":928,"y":983,"width":929,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,999,1000],{"x":932,"y":986,"style":970},"Units, Net Sales, …",[891,1002],{"x":900,"y":1003,"width":902,"height":937,"rx":894,"fill":904,"stroke":905,"style":906},"148.0",[912,1005,893],{"x":940,"y":1006,"style":916},"167.0",[891,1008],{"x":909,"y":1003,"width":910,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,1010,1011],{"x":914,"y":1006,"style":970},"West",[891,1013],{"x":920,"y":1003,"width":921,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,1015,942],{"x":924,"y":1006,"style":970},[891,1017],{"x":928,"y":1003,"width":929,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,1019,1020],{"x":932,"y":1006,"style":970},"Quantity, Sales £, …",[891,1022],{"x":900,"y":1023,"width":902,"height":937,"rx":894,"fill":904,"stroke":905,"style":906},"176.0",[912,1025,1027],{"x":940,"y":1026,"style":916},"195.0","5",[891,1029],{"x":909,"y":1023,"width":910,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,1031,1032],{"x":914,"y":1026,"style":970},"East",[891,1034],{"x":920,"y":1023,"width":921,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,1036,376],{"x":924,"y":1026,"style":970},[891,1038],{"x":928,"y":1023,"width":929,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,1040,1041],{"x":932,"y":1026,"style":970},"qty, revenue, …",[891,1043],{"x":1044,"y":901,"width":902,"height":903,"rx":894,"fill":904,"stroke":905,"style":906},"517.7",[891,1046],{"x":1047,"y":901,"width":1048,"height":903,"rx":894,"fill":904,"stroke":905,"style":906},"549.7","190.3",[912,1050,917],{"x":1051,"y":915,"style":916},"644.9",[891,1053],{"x":1044,"y":936,"width":902,"height":937,"rx":894,"fill":904,"stroke":905,"style":906},[912,1055,942],{"x":1056,"y":941,"style":916},"533.7",[891,1058],{"x":1047,"y":936,"width":1048,"height":937,"rx":894,"fill":1059,"stroke":1060,"style":1061},"#d9f4f1","var(--teal,#0f9488)","stroke-width:1.5px",[912,1063,1065],{"x":1051,"y":941,"style":1064},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Standard columns",[891,1067],{"x":1044,"y":961,"width":902,"height":937,"rx":894,"fill":904,"stroke":905,"style":906},[912,1069,965],{"x":1056,"y":964,"style":916},[891,1071],{"x":1047,"y":961,"width":1048,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,1073,1074],{"x":1051,"y":964,"style":970},"date, product, qty, revenue",[891,1076],{"x":1044,"y":983,"width":902,"height":937,"rx":894,"fill":904,"stroke":905,"style":906},[912,1078,376],{"x":1056,"y":986,"style":916},[891,1080],{"x":1047,"y":983,"width":1048,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,1082,1083],{"x":1051,"y":986,"style":970},"+ unmapped: Rep",[891,1085],{"x":1044,"y":1003,"width":902,"height":937,"rx":894,"fill":904,"stroke":905,"style":906},[912,1087,893],{"x":1056,"y":1006,"style":916},[891,1089],{"x":1047,"y":1003,"width":1048,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,1091,1092],{"x":1051,"y":1006,"style":970},"+ unmapped: Comments",[891,1094],{"x":1044,"y":1023,"width":902,"height":937,"rx":894,"fill":904,"stroke":905,"style":906},[912,1096,1027],{"x":1056,"y":1026,"style":916},[891,1098],{"x":1047,"y":1023,"width":1048,"height":937,"rx":894,"fill":897,"stroke":905,"style":906},[912,1100,1101],{"x":1051,"y":1026,"style":970},"+ unmapped: Channel",[912,1103,1107],{"x":1104,"y":1105,"style":1106},"186.4","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[912,1109,1111],{"x":1110,"y":1105,"style":1106},"628.9","after",[62,1113],{"x1":1114,"y1":1115,"x2":1116,"y2":1115,"stroke":1117,"style":1118},"362.8","122.0","503.7","var(--brand,#5b5cf0)","stroke-width:2px",[1120,1121],"polygon",{"points":1122,"fill":1123},"507.7,122.0 498.7,127.0 498.7,117.0","#5b5cf0",[891,1125],{"x":1126,"y":1127,"width":1128,"height":1129,"rx":1130,"fill":897,"stroke":905,"style":1131},"385.3","89.0","100.0","22.0","11","stroke-width:1.2px",[912,1133,1137],{"x":1134,"y":1135,"style":1136},"435.3","104.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","synonym map",[46,1139,1141],{"id":1140},"step-3-clean-stack-and-keep-the-source","Step 3 — Clean, stack and keep the source",[51,1143,1145],{"className":91,"code":1144,"language":93,"meta":56,"style":56},"def clean(body, sheet):\n    df = body[[c for c in STANDARD if c in body.columns]].copy()\n    df = df.dropna(how=\"all\")\n    is_total = df[\"product\"].astype(str).str.strip().str.lower().isin({\"total\", \"grand total\", \"subtotal\"})\n    df = df[~is_total]\n    df[\"date\"] = pd.to_datetime(df[\"date\"], errors=\"coerce\")\n    df[[\"qty\", \"revenue\"]] = df[[\"qty\", \"revenue\"]].apply(pd.to_numeric, errors=\"coerce\")\n    df = df.dropna(subset=[\"qty\", \"revenue\"], how=\"all\")          # notes and other text-only rows\n    return df.assign(source=sheet)\n\nparts, report = [], []\nfor sheet in sheets:\n    body, header_row, unmapped, missing = read_sheet(\"branches.xlsx\", sheet)\n    part = clean(body, sheet)\n    parts.append(part)\n    report.append({\"sheet\": sheet, \"header row\": header_row + 1, \"rows\": len(part), \"unmapped\": \", \".join(unmapped), \"missing\": \", \".join(missing)})\ncombined = pd.concat(parts, ignore_index=True)[STANDARD + [\"source\"]]\nprint(combined.to_string(index=False))\nprint(pd.DataFrame(report).to_string(index=False))\n",[35,1146,1147,1157,1184,1203,1239,1253,1283,1319,1356,1371,1375,1385,1395,1407,1417,1422,1478,1512,1531],{"__ignoreMap":56},[60,1148,1149,1151,1154],{"class":62,"line":63},[60,1150,312],{"class":100},[60,1152,1153],{"class":315}," clean",[60,1155,1156],{"class":104},"(body, sheet):\n",[60,1158,1159,1162,1164,1167,1169,1171,1173,1175,1177,1179,1181],{"class":62,"line":114},[60,1160,1161],{"class":104},"    df ",[60,1163,373],{"class":100},[60,1165,1166],{"class":104}," body[[c ",[60,1168,279],{"class":100},[60,1170,638],{"class":104},[60,1172,285],{"class":100},[60,1174,702],{"class":124},[60,1176,705],{"class":100},[60,1178,638],{"class":104},[60,1180,285],{"class":100},[60,1182,1183],{"class":104}," body.columns]].copy()\n",[60,1185,1186,1188,1190,1193,1196,1198,1201],{"class":62,"line":121},[60,1187,1161],{"class":104},[60,1189,373],{"class":100},[60,1191,1192],{"class":104}," df.dropna(",[60,1194,1195],{"class":392},"how",[60,1197,373],{"class":100},[60,1199,1200],{"class":70},"\"all\"",[60,1202,409],{"class":104},[60,1204,1205,1208,1210,1213,1215,1218,1220,1223,1226,1228,1231,1233,1236],{"class":62,"line":156},[60,1206,1207],{"class":104},"    is_total ",[60,1209,373],{"class":100},[60,1211,1212],{"class":104}," df[",[60,1214,140],{"class":70},[60,1216,1217],{"class":104},"].astype(",[60,1219,334],{"class":124},[60,1221,1222],{"class":104},").str.strip().str.lower().isin({",[60,1224,1225],{"class":70},"\"total\"",[60,1227,137],{"class":104},[60,1229,1230],{"class":70},"\"grand total\"",[60,1232,137],{"class":104},[60,1234,1235],{"class":70},"\"subtotal\"",[60,1237,1238],{"class":104},"})\n",[60,1240,1241,1243,1245,1247,1250],{"class":62,"line":167},[60,1242,1161],{"class":104},[60,1244,373],{"class":100},[60,1246,1212],{"class":104},[60,1248,1249],{"class":100},"~",[60,1251,1252],{"class":104},"is_total]\n",[60,1254,1255,1258,1260,1263,1265,1268,1270,1273,1276,1278,1281],{"class":62,"line":191},[60,1256,1257],{"class":104},"    df[",[60,1259,134],{"class":70},[60,1261,1262],{"class":104},"] ",[60,1264,373],{"class":100},[60,1266,1267],{"class":104}," pd.to_datetime(df[",[60,1269,134],{"class":70},[60,1271,1272],{"class":104},"], ",[60,1274,1275],{"class":392},"errors",[60,1277,373],{"class":100},[60,1279,1280],{"class":70},"\"coerce\"",[60,1282,409],{"class":104},[60,1284,1285,1288,1290,1292,1294,1297,1299,1302,1304,1306,1308,1311,1313,1315,1317],{"class":62,"line":213},[60,1286,1287],{"class":104},"    df[[",[60,1289,145],{"class":70},[60,1291,137],{"class":104},[60,1293,150],{"class":70},[60,1295,1296],{"class":104},"]] ",[60,1298,373],{"class":100},[60,1300,1301],{"class":104}," df[[",[60,1303,145],{"class":70},[60,1305,137],{"class":104},[60,1307,150],{"class":70},[60,1309,1310],{"class":104},"]].apply(pd.to_numeric, ",[60,1312,1275],{"class":392},[60,1314,373],{"class":100},[60,1316,1280],{"class":70},[60,1318,409],{"class":104},[60,1320,1321,1323,1325,1327,1330,1332,1335,1337,1339,1341,1343,1345,1347,1349,1352],{"class":62,"line":235},[60,1322,1161],{"class":104},[60,1324,373],{"class":100},[60,1326,1192],{"class":104},[60,1328,1329],{"class":392},"subset",[60,1331,373],{"class":100},[60,1333,1334],{"class":104},"[",[60,1336,145],{"class":70},[60,1338,137],{"class":104},[60,1340,150],{"class":70},[60,1342,1272],{"class":104},[60,1344,1195],{"class":392},[60,1346,373],{"class":100},[60,1348,1200],{"class":70},[60,1350,1351],{"class":104},")          ",[60,1353,1355],{"class":1354},"s-wDw","# notes and other text-only rows\n",[60,1357,1358,1360,1363,1366,1368],{"class":62,"line":262},[60,1359,325],{"class":100},[60,1361,1362],{"class":104}," df.assign(",[60,1364,1365],{"class":392},"source",[60,1367,373],{"class":100},[60,1369,1370],{"class":104},"sheet)\n",[60,1372,1373],{"class":62,"line":268},[60,1374,118],{"emptyLinePlaceholder":117},[60,1376,1377,1380,1382],{"class":62,"line":304},[60,1378,1379],{"class":104},"parts, report ",[60,1381,373],{"class":100},[60,1383,1384],{"class":104}," [], []\n",[60,1386,1387,1389,1391,1393],{"class":62,"line":309},[60,1388,279],{"class":100},[60,1390,766],{"class":104},[60,1392,285],{"class":100},[60,1394,771],{"class":104},[60,1396,1397,1399,1401,1403,1405],{"class":62,"line":322},[60,1398,777],{"class":104},[60,1400,373],{"class":100},[60,1402,782],{"class":104},[60,1404,740],{"class":70},[60,1406,787],{"class":104},[60,1408,1409,1412,1414],{"class":62,"line":718},[60,1410,1411],{"class":104},"    part ",[60,1413,373],{"class":100},[60,1415,1416],{"class":104}," clean(body, sheet)\n",[60,1418,1419],{"class":62,"line":726},[60,1420,1421],{"class":104},"    parts.append(part)\n",[60,1423,1424,1427,1430,1433,1436,1439,1441,1443,1445,1448,1451,1454,1457,1460,1462,1465,1468,1471,1473,1475],{"class":62,"line":731},[60,1425,1426],{"class":104},"    report.append({",[60,1428,1429],{"class":70},"\"sheet\"",[60,1431,1432],{"class":104},": sheet, ",[60,1434,1435],{"class":70},"\"header row\"",[60,1437,1438],{"class":104},": header_row ",[60,1440,547],{"class":100},[60,1442,550],{"class":124},[60,1444,137],{"class":104},[60,1446,1447],{"class":70},"\"rows\"",[60,1449,1450],{"class":104},": ",[60,1452,1453],{"class":124},"len",[60,1455,1456],{"class":104},"(part), ",[60,1458,1459],{"class":70},"\"unmapped\"",[60,1461,1450],{"class":104},[60,1463,1464],{"class":70},"\", \"",[60,1466,1467],{"class":104},".join(unmapped), ",[60,1469,1470],{"class":70},"\"missing\"",[60,1472,1450],{"class":104},[60,1474,1464],{"class":70},[60,1476,1477],{"class":104},".join(missing)})\n",[60,1479,1480,1483,1485,1488,1491,1493,1496,1499,1501,1504,1506,1509],{"class":62,"line":750},[60,1481,1482],{"class":104},"combined ",[60,1484,373],{"class":100},[60,1486,1487],{"class":104}," pd.concat(parts, ",[60,1489,1490],{"class":392},"ignore_index",[60,1492,373],{"class":100},[60,1494,1495],{"class":124},"True",[60,1497,1498],{"class":104},")[",[60,1500,125],{"class":124},[60,1502,1503],{"class":100}," +",[60,1505,131],{"class":104},[60,1507,1508],{"class":70},"\"source\"",[60,1510,1511],{"class":104},"]]\n",[60,1513,1514,1517,1520,1523,1525,1528],{"class":62,"line":761},[60,1515,1516],{"class":124},"print",[60,1518,1519],{"class":104},"(combined.to_string(",[60,1521,1522],{"class":392},"index",[60,1524,373],{"class":100},[60,1526,1527],{"class":124},"False",[60,1529,1530],{"class":104},"))\n",[60,1532,1533,1535,1538,1540,1542,1544],{"class":62,"line":774},[60,1534,1516],{"class":124},[60,1536,1537],{"class":104},"(pd.DataFrame(report).to_string(",[60,1539,1522],{"class":392},[60,1541,373],{"class":100},[60,1543,1527],{"class":124},[60,1545,1530],{"class":104},[10,1547,1548,1549,1551,1552,1555],{},"Only the standard columns are kept, in the standard order, so the stacked table has exactly four data columns plus ",[14,1550,1365],{},". Total rows — recognised by \"Total\" in the product column — are removed, and after the numeric columns are converted, rows with neither a quantity nor a revenue are dropped, which removes notes such as \"Note: excludes returns\" wherever they were typed; counting the West branch's total row as a sale would add 350 units that were already counted. Dates and numbers are converted with ",[35,1553,1554],{},"errors=\"coerce\"",", so anything unparseable becomes missing rather than breaking the load. The source column means every row in the combined table can be traced to its sheet. Totals per branch can now be checked against what each branch reported — the West branch's own total row said 350 units and £108, which is exactly what its two remaining rows add up to.",[872,1557,881,1562,881,1565,881,1568,881,1571,881,1577,881,1588,881,1594,881,1598,881,1603,881,1606,881,1610,881,1617,881,1623,881,1627,881,1631,881,1635,881,1638,881,1643,881,1650,881,1655,881,1659,881,1662,881,1666,881,1669,881,1672,881,1679,881,1684,881,1687,881,1690,881,1694,881,1697,881,1700,881,1706,881,1711,881,1714,881,1717],{"viewBox":1558,"role":875,"ariaLabelledBy":1559,"xmlns":879,"style":880},"4 0 752 160",[1560,1561],"cs-flow-t","cs-flow-d",[883,1563,1564],{"id":1560},"Combining sheets with different layouts",[887,1566,1567],{"id":1561},"For each sheet, read the raw grid, find the header row by counting known column names, rename columns with the synonym map, keep the standard columns, remove total and note rows, convert types, add the source sheet, stack all sheets and write a report of unmapped and missing columns.",[891,1569],{"x":893,"y":894,"width":895,"height":1570,"fill":897},"160",[891,1572],{"x":900,"y":1573,"width":1574,"height":1575,"rx":1576,"fill":904,"stroke":905,"style":1118},"16.0","115.2","128.0","12",[1578,1579,1581],"g",{"transform":1580},"translate(65.6,46.5) scale(1.000)",[1582,1583],"path",{"d":1584,"fill":1585,"stroke":1586,"style":1587},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[912,1589,1593],{"x":1590,"y":1591,"style":1592},"77.6","93.0","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Raw grid",[912,1595,861],{"x":1590,"y":1596,"style":1597},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[62,1599],{"x1":1600,"y1":1601,"x2":1602,"y2":1601,"stroke":1117,"style":1118},"140.2","80.0","162.2",[1120,1604],{"points":1605,"fill":1123},"166.2,80.0 157.2,85.0 157.2,75.0",[891,1607],{"x":1608,"y":1573,"width":1574,"height":1575,"rx":1576,"fill":1609,"stroke":1117,"style":1118},"171.2","#f0f4ff",[1578,1611,1613],{"transform":1612},"translate(216.8,37.8) scale(1.000)",[1582,1614],{"d":1615,"fill":1585,"stroke":1616,"style":1587},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","var(--brand-strong,#4338ca)",[912,1618,1622],{"x":1619,"y":1620,"style":1621},"228.8","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Find header",[912,1624,1626],{"x":1619,"y":1625,"style":1597},"103.8",">= 3 known",[912,1628,1630],{"x":1619,"y":1629,"style":1597},"121.2","names",[62,1632],{"x1":1633,"y1":1601,"x2":1634,"y2":1601,"stroke":1117,"style":1118},"291.4","313.4",[1120,1636],{"points":1637,"fill":1123},"317.4,80.0 308.4,85.0 308.4,75.0",[891,1639],{"x":1640,"y":1573,"width":1574,"height":1575,"rx":1576,"fill":1641,"stroke":1642,"style":1118},"322.4","#fdefd8","var(--gold,#b4740a)",[1578,1644,1646],{"transform":1645},"translate(368.0,37.8) scale(1.000)",[1582,1647],{"d":1648,"fill":1585,"stroke":1649,"style":1587},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--gold-ink,#7a4e06)",[912,1651,1654],{"x":1652,"y":1620,"style":1653},"380.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Map",[912,1656,1658],{"x":1652,"y":1657,"style":1653},"101.8","synonyms",[912,1660,1661],{"x":1652,"y":1629,"style":1597},"standard names",[62,1663],{"x1":1664,"y1":1601,"x2":1665,"y2":1601,"stroke":1117,"style":1118},"442.6","464.6",[1120,1667],{"points":1668,"fill":1123},"468.6,80.0 459.6,85.0 459.6,75.0",[891,1670],{"x":1671,"y":1573,"width":1574,"height":1575,"rx":1576,"fill":1059,"stroke":1060,"style":1118},"473.6",[1578,1673,1675],{"transform":1674},"translate(519.2,37.8) scale(1.000)",[1582,1676],{"d":1677,"fill":1585,"stroke":1678,"style":1587},"M3 5h18l-7 8v6l-4 2v-8z","var(--teal-ink,#0b6157)",[912,1680,1683],{"x":1681,"y":1620,"style":1682},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Clean rows",[912,1685,1686],{"x":1681,"y":1625,"style":1597},"totals, notes,",[912,1688,1689],{"x":1681,"y":1629,"style":1597},"types",[62,1691],{"x1":1692,"y1":1601,"x2":1693,"y2":1601,"stroke":1117,"style":1118},"593.8","615.8",[1120,1695],{"points":1696,"fill":1123},"619.8,80.0 610.8,85.0 610.8,75.0",[891,1698],{"x":1699,"y":1573,"width":1574,"height":1575,"rx":1576,"fill":1609,"stroke":1117,"style":1118},"624.8",[1578,1701,1703],{"transform":1702},"translate(670.4,29.0) scale(1.000)",[1582,1704],{"d":1705,"fill":1585,"stroke":1616,"style":1587},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5",[912,1707,1710],{"x":1708,"y":1709,"style":1621},"682.4","75.5","Stack +",[912,1712,1713],{"x":1708,"y":1591,"style":1621},"report",[912,1715,1716],{"x":1708,"y":1596,"style":1597},"source,",[912,1718,1720],{"x":1708,"y":1719,"style":1597},"130.0","unmapped",[46,1722,1724],{"id":1723},"step-4-write-the-table-and-the-report-together","Step 4 — Write the table and the report together",[51,1726,1728],{"className":91,"code":1727,"language":93,"meta":56,"style":56},"with pd.ExcelWriter(\"branches_combined.xlsx\", engine=\"openpyxl\", date_format=\"yyyy-mm-dd\") as xw:\n    combined.to_excel(xw, sheet_name=\"Combined\", index=False)\n    pd.DataFrame(report).to_excel(xw, sheet_name=\"Load report\", index=False)\n    combined.pivot_table(index=\"source\", values=[\"qty\", \"revenue\"], aggfunc=\"sum\").to_excel(xw, sheet_name=\"By branch\")\nprint(combined.groupby(\"source\")[[\"qty\", \"revenue\"]].sum().to_string())\n",[35,1729,1730,1767,1789,1811,1859],{"__ignoreMap":56},[60,1731,1732,1734,1737,1740,1742,1745,1747,1750,1752,1755,1757,1760,1762,1764],{"class":62,"line":63},[60,1733,734],{"class":100},[60,1735,1736],{"class":104}," pd.ExcelWriter(",[60,1738,1739],{"class":70},"\"branches_combined.xlsx\"",[60,1741,137],{"class":104},[60,1743,1744],{"class":392},"engine",[60,1746,373],{"class":100},[60,1748,1749],{"class":70},"\"openpyxl\"",[60,1751,137],{"class":104},[60,1753,1754],{"class":392},"date_format",[60,1756,373],{"class":100},[60,1758,1759],{"class":70},"\"yyyy-mm-dd\"",[60,1761,600],{"class":104},[60,1763,108],{"class":100},[60,1765,1766],{"class":104}," xw:\n",[60,1768,1769,1772,1774,1776,1779,1781,1783,1785,1787],{"class":62,"line":114},[60,1770,1771],{"class":104},"    combined.to_excel(xw, ",[60,1773,393],{"class":392},[60,1775,373],{"class":100},[60,1777,1778],{"class":70},"\"Combined\"",[60,1780,137],{"class":104},[60,1782,1522],{"class":392},[60,1784,373],{"class":100},[60,1786,1527],{"class":124},[60,1788,409],{"class":104},[60,1790,1791,1794,1796,1798,1801,1803,1805,1807,1809],{"class":62,"line":121},[60,1792,1793],{"class":104},"    pd.DataFrame(report).to_excel(xw, ",[60,1795,393],{"class":392},[60,1797,373],{"class":100},[60,1799,1800],{"class":70},"\"Load report\"",[60,1802,137],{"class":104},[60,1804,1522],{"class":392},[60,1806,373],{"class":100},[60,1808,1527],{"class":124},[60,1810,409],{"class":104},[60,1812,1813,1816,1818,1820,1822,1824,1827,1829,1831,1833,1835,1837,1839,1842,1844,1847,1850,1852,1854,1857],{"class":62,"line":156},[60,1814,1815],{"class":104},"    combined.pivot_table(",[60,1817,1522],{"class":392},[60,1819,373],{"class":100},[60,1821,1508],{"class":70},[60,1823,137],{"class":104},[60,1825,1826],{"class":392},"values",[60,1828,373],{"class":100},[60,1830,1334],{"class":104},[60,1832,145],{"class":70},[60,1834,137],{"class":104},[60,1836,150],{"class":70},[60,1838,1272],{"class":104},[60,1840,1841],{"class":392},"aggfunc",[60,1843,373],{"class":100},[60,1845,1846],{"class":70},"\"sum\"",[60,1848,1849],{"class":104},").to_excel(xw, ",[60,1851,393],{"class":392},[60,1853,373],{"class":100},[60,1855,1856],{"class":70},"\"By branch\"",[60,1858,409],{"class":104},[60,1860,1861,1863,1866,1868,1871,1873,1875,1877],{"class":62,"line":167},[60,1862,1516],{"class":124},[60,1864,1865],{"class":104},"(combined.groupby(",[60,1867,1508],{"class":70},[60,1869,1870],{"class":104},")[[",[60,1872,145],{"class":70},[60,1874,137],{"class":104},[60,1876,150],{"class":70},[60,1878,1879],{"class":104},"]].sum().to_string())\n",[10,1881,1882,1883,1885,1886,1889,1890,1893,1894,1896,1897,44],{},"The output workbook has the combined data, the load report — which header row was used on each sheet, how many rows were loaded, which columns were unmapped or missing — and a per-branch summary that each branch manager can check against their own figures. The load report is what keeps the process honest over time: if a branch renames ",[14,1884,16],{}," to ",[14,1887,1888],{},"Pieces"," next month, the sheet still loads, but ",[14,1891,1892],{},"qty"," appears under missing and ",[14,1895,1888],{}," under unmapped, and the fix is one line in the synonym table. Loading many workbooks rather than many sheets works the same way, as in ",[40,1898,1900],{"href":1899},"\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 in Python",[872,1902,881,1908,881,1911,881,1914,881,1918,881,1922,881,1927,881,1931,881,1936,881,1939,881,1942,881,1945,881,1950,881,1953,881,1956,881,1958,881,1962,881,1965,881,1968,881,1970],{"viewBox":1903,"role":875,"ariaLabelledBy":1904,"xmlns":879,"style":1907},"4 0 600 180",[1905,1906],"cs-branch-t","cs-branch-d","width:100%;max-width:600px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[883,1909,1910],{"id":1905},"Rows loaded per branch",[887,1912,1913],{"id":1906},"After removing title rows, a total row and a note, North contributes three sales rows, South two, West two and East two, nine rows in all.",[891,1915],{"x":893,"y":894,"width":1916,"height":1917,"fill":897},"600","180",[891,1919],{"x":900,"y":1573,"width":1920,"height":937,"rx":1921,"fill":904,"stroke":905,"style":906},"65.8","7",[912,1923,971],{"x":1924,"y":1925,"style":1926},"52.9","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[891,1928],{"x":1929,"y":1573,"width":1930,"height":937,"rx":1921,"fill":1609,"stroke":1117,"style":1061},"95.8","431.9",[912,1932,1935],{"x":1933,"y":1925,"style":1934},"535.7","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","3 rows",[891,1937],{"x":900,"y":1938,"width":1920,"height":937,"rx":1921,"fill":904,"stroke":905,"style":906},"56.0",[912,1940,991],{"x":1924,"y":1941,"style":1926},"74.5",[891,1943],{"x":1929,"y":1938,"width":1944,"height":937,"rx":1921,"fill":1059,"stroke":1060,"style":1061},"287.9",[912,1946,1949],{"x":1947,"y":1941,"style":1948},"391.7","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","2 rows",[891,1951],{"x":900,"y":1952,"width":1920,"height":937,"rx":1921,"fill":904,"stroke":905,"style":906},"96.0",[912,1954,1011],{"x":1924,"y":1955,"style":1926},"114.5",[891,1957],{"x":1929,"y":1952,"width":1944,"height":937,"rx":1921,"fill":1641,"stroke":1642,"style":1061},[912,1959,1961],{"x":1947,"y":1955,"style":1960},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","2 rows (total and note removed)",[891,1963],{"x":900,"y":1964,"width":1920,"height":937,"rx":1921,"fill":904,"stroke":905,"style":906},"136.0",[912,1966,1032],{"x":1924,"y":1967,"style":1926},"154.5",[891,1969],{"x":1929,"y":1964,"width":1944,"height":937,"rx":1921,"fill":1059,"stroke":1060,"style":1061},[912,1971,1949],{"x":1947,"y":1967,"style":1948},[46,1973,1975],{"id":1974},"when-to-keep-the-extra-columns","When to keep the extra columns",[10,1977,1978,1979,1981,1982,1984,1985,137,1987,347,1989,1992],{},"The approach above keeps only the standard columns, which is right when the combined table feeds a report with a fixed shape. Sometimes the extras matter — the ",[14,1980,83],{}," column from the East branch might be exactly what someone wants to analyse. Two options keep them without making the table ragged. Add the column to the standard set, with synonyms, and accept that it will be empty for branches that do not record it; the load report then shows which branches are missing it, which is useful information in itself. Or write the unmapped columns into a separate long table — sheet, row, column name, value — so nothing is lost and the main table stays clean. What to avoid is the default of ",[35,1983,37],{},", which keeps every spelling as its own column and leaves readers to work out that ",[14,1986,16],{},[14,1988,20],{},[14,1990,1991],{},"Quantity"," mean the same thing.",[46,1994,1996],{"id":1995},"common-pitfalls","Common pitfalls",[1998,1999,2000,2008,2018,2024,2030],"ul",{},[2001,2002,2003,2007],"li",{},[2004,2005,2006],"strong",{},"Concatenating without renaming."," Every spelling becomes its own mostly-empty column.",[2001,2009,2010,2017],{},[2004,2011,2012,2013,2016],{},"Fixed ",[35,2014,2015],{},"header="," per sheet."," Find the header row from known names instead.",[2001,2019,2020,2023],{},[2004,2021,2022],{},"Counting total rows as data."," Remove rows labelled Total or Subtotal.",[2001,2025,2026,2029],{},[2004,2027,2028],{},"Dropping unknown columns silently."," Report them so new spellings are noticed.",[2001,2031,2032,2035],{},[2004,2033,2034],{},"No source column."," Without it, combined rows cannot be traced back.",[46,2037,2039],{"id":2038},"conclusion","Conclusion",[10,2041,2042],{},"To combine sheets that differ in layout, read each as a raw grid, find the header row as the first row with enough known column names, rename columns through a synonym map to a standard set, keep those columns in a fixed order, remove total and note rows, convert types and add the source sheet. Stack the results and write a load report of header rows, row counts and unmapped or missing columns, so changes in the sheets are visible and fixed in the synonym map.",[46,2044,2046],{"id":2045},"frequently-asked-questions","Frequently asked questions",[10,2048,2049,2052],{},[2004,2050,2051],{},"How do I combine Excel sheets with different column names in pandas?","\nRename each sheet's columns to a standard set with a dictionary of synonyms, keep only the standard columns in a fixed order, then concatenate.",[10,2054,2055,2058],{},[2004,2056,2057],{},"How do I find the header row when it differs between sheets?","\nRead the sheet with header=None and take the first row in which several cells match known column names.",[10,2060,2061,2064],{},[2004,2062,2063],{},"How do I stop total rows being added to the combined data?","\nFilter out rows whose label column says Total, Subtotal or Grand Total before concatenating.",[10,2066,2067,2070],{},[2004,2068,2069],{},"What should happen to columns that only some sheets have?","\nEither add them to the standard set and accept empty values for other sheets, or report them in a load log, but do not let them become extra ragged columns by accident.",[46,2072,2074],{"id":2073},"related","Related",[1998,2076,2077,2083,2090,2097],{},[2001,2078,2079,2080,2082],{},"Up one level: ",[40,2081,43],{"href":42}," — getting data out of untidy sheets.",[2001,2084,2085,2089],{},[40,2086,2088],{"href":2087},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-multiple-tables-from-one-excel-sheet\u002F","Read Multiple Tables from One Excel Sheet"," — several tables on one sheet.",[2001,2091,2092,2096],{},[40,2093,2095],{"href":2094},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fremove-subtotal-and-total-rows-from-an-excel-export\u002F","Remove Subtotal and Total Rows from an Excel Export"," — totals in more detail.",[2001,2098,2099,2103],{},[40,2100,2102],{"href":2101},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fstandardise-excel-column-names-with-pandas\u002F","Standardise Excel Column Names with pandas"," — cleaning header text.",[2105,2106,2107],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":56,"searchDepth":114,"depth":114,"links":2109},[2110,2111,2112,2113,2114,2115,2116,2117,2118,2119],{"id":48,"depth":114,"text":49},{"id":87,"depth":114,"text":88},{"id":354,"depth":114,"text":355},{"id":1140,"depth":114,"text":1141},{"id":1723,"depth":114,"text":1724},{"id":1974,"depth":114,"text":1975},{"id":1995,"depth":114,"text":1996},{"id":2038,"depth":114,"text":2039},{"id":2045,"depth":114,"text":2046},{"id":2073,"depth":114,"text":2074},"2026-10-02","Stack sheets that hold the same kind of data in different layouts — headers on different rows, columns in a different order and under different names, extra columns, total rows — into one clean table with pandas: find each sheet's header row, map column synonyms to standard names, drop totals and notes, record the source, and report columns that could not be mapped instead of losing them.","md",[2124,2126,2128,2130],{"q":2051,"a":2125},"Rename each sheet's columns to a standard set with a dictionary of synonyms, keep only the standard columns in a fixed order, then concatenate.",{"q":2057,"a":2127},"Read the sheet with header=None and take the first row in which several cells match known column names.",{"q":2063,"a":2129},"Filter out rows whose label column says Total, Subtotal or Grand Total before concatenating.",{"q":2069,"a":2131},"Either add them to the standard set and accept empty values for other sheets, or report them in a load log, but do not let them become extra ragged columns by accident.","combine excel sheets with different layouts into one table",null,{"breadcrumb":2135},[2136,2139,2142],{"name":2137,"item":2138},"Home","\u002F",{"name":2140,"item":2141},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":43,"item":42},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fcombine-sheets-with-different-layouts-into-one-table",{"title":2145,"description":2146},"Combine Excel Sheets with Different Layouts into One Table","Consolidate inconsistent sheets with pandas: detect the header row per sheet, a synonym map for column names, reorder to a standard schema, remove totals and notes, add a source column, and report unmapped and missing columns.","combine-sheets-with-different-layouts-into-one-table","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fcombine-sheets-with-different-layouts-into-one-table\u002Findex",[2150,2151],"messy-sheets","sheets-and-files","how-to","vwvkYBxhDhhtr2rjsB5plSMMfGuUM86vjVZ--4wWkXo",[2155,2158],{"title":43,"path":2156,"stem":2157,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Findex",{"title":2159,"path":2160,"stem":2161,"children":-1},"Extract Hyperlink URLs from Excel Cells with openpyxl","\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-hyperlink-urls-from-excel-cells","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-hyperlink-urls-from-excel-cells\u002Findex",[2163,2164],{"path":2156,"title":43},{"path":2165,"title":2140},"\u002Fadvanced-data-transformation-and-cleaning",[],[2168,2171,2174,2177,2178,2181],{"path":2169,"title":2170,"kind":2152},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Ffreeze-header-row-in-excel-with-openpyxl","Freeze the Header Row in Excel with openpyxl",{"path":2172,"title":2173,"kind":2152},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhandle-merged-cells-when-reading-excel-with-pandas","Handle Merged Cells When Reading Excel with pandas",{"path":2175,"title":2176,"kind":2152},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Funmerge-cells-and-fill-values-with-openpyxl","Unmerge Cells and Fill the Values with openpyxl",{"path":2160,"title":2159,"kind":2152},{"path":2179,"title":2180,"kind":2152},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet","Extract Report Header Fields from an Excel Sheet with Python",{"path":2182,"title":2183,"kind":2152},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout","Find the Header Row in an Excel Sheet with a Shifting Layout",1790972090809]