[{"data":1,"prerenderedAt":2682},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout":2649,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout":2658,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout":2664,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout":2665},{"id":4,"title":5,"body":6,"dateModified":2613,"datePublished":2613,"description":2614,"extension":2615,"faq":2616,"keyword":2627,"kind":2628,"meta":2629,"navigation":200,"path":2638,"seo":2639,"slug":2641,"stem":2642,"topics":2643,"type":2647,"__hash__":2648},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout\u002Findex.md","Find the Header Row in an Excel Sheet with a Shifting Layout",{"type":7,"value":8,"toc":2599},"minimark",[9,40,137,142,169,172,460,464,479,549,556,560,563,845,851,957,961,979,1274,1281,1285,1288,1522,1529,1723,1727,1730,1971,1982,1986,1989,2270,2281,2285,2399,2403,2420,2504,2508,2516,2520,2527,2533,2543,2549,2555,2559,2595],[10,11,12,16,17,20,21,24,25,30,31,35,36,39],"p",{},[13,14,15],"code",{},"skiprows=2"," is a promise that the table always starts on row 3. Exports from finance systems, CRMs and\nshared drives break that promise constantly: one month the banner has a company name and a date, the\nnext it gains a filter description and two blank lines, and the header drifts down to row 6. The script\ndoes not crash. It reads the banner as the header, produces columns called ",[13,18,19],{},"Unnamed: 1",", and fails three\nsteps later with a ",[13,22,23],{},"KeyError"," that says nothing about the real cause. This guide from\n",[26,27,29],"a",{"href":28},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002F","Extracting Data from Messy Excel Sheets","\nfinds the header by what it ",[32,33,34],"em",{},"says",", not where it ",[32,37,38],{},"was",".",[41,42,50,51,50,55,50,59,50,66,50,75,50,82,50,89,50,94,50,98,50,102,50,106,50,111,50,116,50,120,50,122,50,125,50,128,50,131],"svg",{"viewBox":43,"role":44,"ariaLabelledBy":45,"xmlns":48,"style":49},"0 0 760 214","img",[46,47],"hdr-drift-t","hdr-drift-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[52,53,54],"title",{"id":46},"The same export with its header on different rows",[56,57,58],"desc",{"id":47},"In March the banner is two lines and the header is on row 3. In April the banner has grown with blank lines and the header has moved to row 6, so a fixed skiprows value reads the wrong row.",[60,61],"rect",{"x":62,"y":62,"width":63,"height":64,"fill":65},"0","760","214","#ffffff",[60,67],{"x":68,"y":68,"width":69,"height":70,"rx":71,"fill":72,"stroke":73,"style":74},"20","350","150","14","#d9f4f1","var(--teal,#0f9488)","stroke-width:2px",[76,77,81],"text",{"x":78,"y":79,"style":80},"195","46","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","march.xlsx",[83,84],"line",{"x1":85,"y1":86,"x2":87,"y2":86,"stroke":73,"style":88},"36","56","354","stroke-width:1px",[76,90,93],{"x":78,"y":91,"style":92},"78","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","row 1: ACME Corp — export",[76,95,97],{"x":78,"y":96,"style":92},"101","row 2: Generated 2026-03-01",[76,99,101],{"x":78,"y":100,"style":92},"124","row 3: Order ID | Customer | Amount",[76,103,105],{"x":78,"y":104,"style":92},"147","row 4: 1 | Ana | 10.5",[60,107],{"x":108,"y":68,"width":69,"height":70,"rx":71,"fill":109,"stroke":110,"style":74},"390","#fee8f2","var(--accent,#f43f8f)",[76,112,115],{"x":113,"y":79,"style":114},"565","font-size:13px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","april.xlsx",[83,117],{"x1":118,"y1":86,"x2":119,"y2":86,"stroke":110,"style":88},"406","724",[76,121,93],{"x":113,"y":91,"style":92},[76,123,124],{"x":113,"y":96,"style":92},"row 2: Generated 2026-04-01",[76,126,127],{"x":113,"y":100,"style":92},"rows 3–5: blank",[76,129,130],{"x":113,"y":104,"style":92},"row 6: Order ID | Customer | Amount",[76,132,136],{"x":133,"y":134,"style":135},"380","200","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","skiprows=2 is right for March and wrong for April",[138,139,141],"h2",{"id":140},"prerequisites","Prerequisites",[143,144,149],"pre",{"className":145,"code":146,"language":147,"meta":148,"style":148},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[13,150,151],{"__ignoreMap":148},[152,153,155,159,163,166],"span",{"class":83,"line":154},1,[152,156,158],{"class":157},"sMTad","pip",[152,160,162],{"class":161},"srMev"," install",[152,164,165],{"class":161}," pandas",[152,167,168],{"class":161}," openpyxl\n",[10,170,171],{},"This creates two exports with different banner heights, an unused column A, a blank line and a total\nrow at the bottom — the usual features of a system-generated sheet:",[143,173,177],{"className":174,"code":175,"language":176,"meta":148,"style":148},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\n\nfor name, header_row in ((\"march.xlsx\", 3), (\"april.xlsx\", 6)):\n    wb = Workbook()\n    ws = wb.active\n    ws.title = \"Export\"\n    ws.append([\"ACME Corp — Sales export\"])\n    ws.append([f\"Generated {name[:-5]} run\"])\n    for _ in range(header_row - 3):\n        ws.append([])\n    ws.append([None, \"Order ID\", \"Customer\", \"Amount\"])\n    ws.append([None, 1, \"Ana\", 10.5])\n    ws.append([None, 2, \"Ben\", 20])\n    ws.append([])\n    ws.append([None, \"Total\", None, 30.5])\n    wb.save(name)\n","python",[13,178,179,195,202,241,253,264,275,287,322,347,353,378,402,425,431,454],{"__ignoreMap":148},[152,180,181,185,189,192],{"class":83,"line":154},[152,182,184],{"class":183},"s-kum","from",[152,186,188],{"class":187},"skGVy"," openpyxl ",[152,190,191],{"class":183},"import",[152,193,194],{"class":187}," Workbook\n",[152,196,198],{"class":83,"line":197},2,[152,199,201],{"emptyLinePlaceholder":200},true,"\n",[152,203,205,208,211,214,217,220,223,227,230,233,235,238],{"class":83,"line":204},3,[152,206,207],{"class":183},"for",[152,209,210],{"class":187}," name, header_row ",[152,212,213],{"class":183},"in",[152,215,216],{"class":187}," ((",[152,218,219],{"class":161},"\"march.xlsx\"",[152,221,222],{"class":187},", ",[152,224,226],{"class":225},"sP0c6","3",[152,228,229],{"class":187},"), (",[152,231,232],{"class":161},"\"april.xlsx\"",[152,234,222],{"class":187},[152,236,237],{"class":225},"6",[152,239,240],{"class":187},")):\n",[152,242,244,247,250],{"class":83,"line":243},4,[152,245,246],{"class":187},"    wb ",[152,248,249],{"class":183},"=",[152,251,252],{"class":187}," Workbook()\n",[152,254,256,259,261],{"class":83,"line":255},5,[152,257,258],{"class":187},"    ws ",[152,260,249],{"class":183},[152,262,263],{"class":187}," wb.active\n",[152,265,267,270,272],{"class":83,"line":266},6,[152,268,269],{"class":187},"    ws.title ",[152,271,249],{"class":183},[152,273,274],{"class":161}," \"Export\"\n",[152,276,278,281,284],{"class":83,"line":277},7,[152,279,280],{"class":187},"    ws.append([",[152,282,283],{"class":161},"\"ACME Corp — Sales export\"",[152,285,286],{"class":187},"])\n",[152,288,290,292,295,298,302,305,308,311,314,317,320],{"class":83,"line":289},8,[152,291,280],{"class":187},[152,293,294],{"class":183},"f",[152,296,297],{"class":161},"\"Generated ",[152,299,301],{"class":300},"sSjpA","{",[152,303,304],{"class":187},"name[:",[152,306,307],{"class":183},"-",[152,309,310],{"class":225},"5",[152,312,313],{"class":187},"]",[152,315,316],{"class":300},"}",[152,318,319],{"class":161}," run\"",[152,321,286],{"class":187},[152,323,325,328,331,333,336,339,341,344],{"class":83,"line":324},9,[152,326,327],{"class":183},"    for",[152,329,330],{"class":187}," _ ",[152,332,213],{"class":183},[152,334,335],{"class":225}," range",[152,337,338],{"class":187},"(header_row ",[152,340,307],{"class":183},[152,342,343],{"class":225}," 3",[152,345,346],{"class":187},"):\n",[152,348,350],{"class":83,"line":349},10,[152,351,352],{"class":187},"        ws.append([])\n",[152,354,356,358,361,363,366,368,371,373,376],{"class":83,"line":355},11,[152,357,280],{"class":187},[152,359,360],{"class":225},"None",[152,362,222],{"class":187},[152,364,365],{"class":161},"\"Order ID\"",[152,367,222],{"class":187},[152,369,370],{"class":161},"\"Customer\"",[152,372,222],{"class":187},[152,374,375],{"class":161},"\"Amount\"",[152,377,286],{"class":187},[152,379,381,383,385,387,390,392,395,397,400],{"class":83,"line":380},12,[152,382,280],{"class":187},[152,384,360],{"class":225},[152,386,222],{"class":187},[152,388,389],{"class":225},"1",[152,391,222],{"class":187},[152,393,394],{"class":161},"\"Ana\"",[152,396,222],{"class":187},[152,398,399],{"class":225},"10.5",[152,401,286],{"class":187},[152,403,405,407,409,411,414,416,419,421,423],{"class":83,"line":404},13,[152,406,280],{"class":187},[152,408,360],{"class":225},[152,410,222],{"class":187},[152,412,413],{"class":225},"2",[152,415,222],{"class":187},[152,417,418],{"class":161},"\"Ben\"",[152,420,222],{"class":187},[152,422,68],{"class":225},[152,424,286],{"class":187},[152,426,428],{"class":83,"line":427},14,[152,429,430],{"class":187},"    ws.append([])\n",[152,432,434,436,438,440,443,445,447,449,452],{"class":83,"line":433},15,[152,435,280],{"class":187},[152,437,360],{"class":225},[152,439,222],{"class":187},[152,441,442],{"class":161},"\"Total\"",[152,444,222],{"class":187},[152,446,360],{"class":225},[152,448,222],{"class":187},[152,450,451],{"class":225},"30.5",[152,453,286],{"class":187},[152,455,457],{"class":83,"line":456},16,[152,458,459],{"class":187},"    wb.save(name)\n",[138,461,463],{"id":462},"step-1-preview-the-top-of-the-sheet-without-a-header","Step 1 — Preview the top of the sheet without a header",[10,465,466,467,470,471,474,475,478],{},"Read only the first few dozen rows with ",[13,468,469],{},"header=None",". This is cheap even for a large file because\n",[13,472,473],{},"nrows"," stops the parse early, and the result's integer index maps directly onto the value that\n",[13,476,477],{},"read_excel(header=...)"," expects in the next step.",[143,480,482],{"className":174,"code":481,"language":176,"meta":148,"style":148},"import pandas as pd\n\npreview = pd.read_excel(\"april.xlsx\", header=None, nrows=30)\nprint(preview.head(8))\n",[13,483,484,497,501,535],{"__ignoreMap":148},[152,485,486,488,491,494],{"class":83,"line":154},[152,487,191],{"class":183},[152,489,490],{"class":187}," pandas ",[152,492,493],{"class":183},"as",[152,495,496],{"class":187}," pd\n",[152,498,499],{"class":83,"line":197},[152,500,201],{"emptyLinePlaceholder":200},[152,502,503,506,508,511,513,515,519,521,523,525,527,529,532],{"class":83,"line":204},[152,504,505],{"class":187},"preview ",[152,507,249],{"class":183},[152,509,510],{"class":187}," pd.read_excel(",[152,512,232],{"class":161},[152,514,222],{"class":187},[152,516,518],{"class":517},"sa561","header",[152,520,249],{"class":183},[152,522,360],{"class":225},[152,524,222],{"class":187},[152,526,473],{"class":517},[152,528,249],{"class":183},[152,530,531],{"class":225},"30",[152,533,534],{"class":187},")\n",[152,536,537,540,543,546],{"class":83,"line":243},[152,538,539],{"class":225},"print",[152,541,542],{"class":187},"(preview.head(",[152,544,545],{"class":225},"8",[152,547,548],{"class":187},"))\n",[10,550,551,552,555],{},"Blank rows in the banner are kept as rows of ",[13,553,554],{},"NaN",", so position 5 in the preview really is the sixth row\nof the sheet. That one-to-one mapping is what makes the scan safe.",[138,557,559],{"id":558},"step-2-score-each-row-against-the-labels-you-require","Step 2 — Score each row against the labels you require",[10,561,562],{},"Normalise every non-empty cell in a row — strip it, lower-case it — and check whether the set of labels\nyou need is a subset of what the row contains. Subset, not equality, so an extra column added to the export\nnext quarter does not break detection.",[143,564,566],{"className":174,"code":565,"language":176,"meta":148,"style":148},"REQUIRED = {\"order id\", \"customer\", \"amount\"}\n\ndef normalise(value) -> str:\n    return \" \".join(str(value).split()).lower()\n\ndef find_header_row(path, sheet_name=0, required=REQUIRED, scan_rows=30) -> int:\n    preview = pd.read_excel(path, sheet_name=sheet_name, header=None, nrows=scan_rows)\n    for position, row in preview.iterrows():\n        labels = {normalise(v) for v in row.dropna()}\n        if required \u003C= labels:\n            return position\n    raise ValueError(f\"no row in the first {scan_rows} contains all of {sorted(required)}\")\n\nfor path in (\"march.xlsx\", \"april.xlsx\"):\n    print(path, find_header_row(path))\n# march.xlsx 2\n# april.xlsx 5\n",[13,567,568,595,599,617,633,637,673,706,718,738,752,760,801,805,825,833,839],{"__ignoreMap":148},[152,569,570,573,576,579,582,584,587,589,592],{"class":83,"line":154},[152,571,572],{"class":225},"REQUIRED",[152,574,575],{"class":183}," =",[152,577,578],{"class":187}," {",[152,580,581],{"class":161},"\"order id\"",[152,583,222],{"class":187},[152,585,586],{"class":161},"\"customer\"",[152,588,222],{"class":187},[152,590,591],{"class":161},"\"amount\"",[152,593,594],{"class":187},"}\n",[152,596,597],{"class":83,"line":197},[152,598,201],{"emptyLinePlaceholder":200},[152,600,601,604,608,611,614],{"class":83,"line":204},[152,602,603],{"class":183},"def",[152,605,607],{"class":606},"s_Opv"," normalise",[152,609,610],{"class":187},"(value) -> ",[152,612,613],{"class":225},"str",[152,615,616],{"class":187},":\n",[152,618,619,622,625,628,630],{"class":83,"line":243},[152,620,621],{"class":183},"    return",[152,623,624],{"class":161}," \" \"",[152,626,627],{"class":187},".join(",[152,629,613],{"class":225},[152,631,632],{"class":187},"(value).split()).lower()\n",[152,634,635],{"class":83,"line":255},[152,636,201],{"emptyLinePlaceholder":200},[152,638,639,641,644,647,649,651,654,656,658,661,663,665,668,671],{"class":83,"line":266},[152,640,603],{"class":183},[152,642,643],{"class":606}," find_header_row",[152,645,646],{"class":187},"(path, sheet_name",[152,648,249],{"class":183},[152,650,62],{"class":225},[152,652,653],{"class":187},", required",[152,655,249],{"class":183},[152,657,572],{"class":225},[152,659,660],{"class":187},", scan_rows",[152,662,249],{"class":183},[152,664,531],{"class":225},[152,666,667],{"class":187},") -> ",[152,669,670],{"class":225},"int",[152,672,616],{"class":187},[152,674,675,678,680,683,686,688,691,693,695,697,699,701,703],{"class":83,"line":277},[152,676,677],{"class":187},"    preview ",[152,679,249],{"class":183},[152,681,682],{"class":187}," pd.read_excel(path, ",[152,684,685],{"class":517},"sheet_name",[152,687,249],{"class":183},[152,689,690],{"class":187},"sheet_name, ",[152,692,518],{"class":517},[152,694,249],{"class":183},[152,696,360],{"class":225},[152,698,222],{"class":187},[152,700,473],{"class":517},[152,702,249],{"class":183},[152,704,705],{"class":187},"scan_rows)\n",[152,707,708,710,713,715],{"class":83,"line":289},[152,709,327],{"class":183},[152,711,712],{"class":187}," position, row ",[152,714,213],{"class":183},[152,716,717],{"class":187}," preview.iterrows():\n",[152,719,720,723,725,728,730,733,735],{"class":83,"line":324},[152,721,722],{"class":187},"        labels ",[152,724,249],{"class":183},[152,726,727],{"class":187}," {normalise(v) ",[152,729,207],{"class":183},[152,731,732],{"class":187}," v ",[152,734,213],{"class":183},[152,736,737],{"class":187}," row.dropna()}\n",[152,739,740,743,746,749],{"class":83,"line":349},[152,741,742],{"class":183},"        if",[152,744,745],{"class":187}," required ",[152,747,748],{"class":183},"\u003C=",[152,750,751],{"class":187}," labels:\n",[152,753,754,757],{"class":83,"line":355},[152,755,756],{"class":183},"            return",[152,758,759],{"class":187}," position\n",[152,761,762,765,768,771,773,776,778,781,783,786,788,791,794,796,799],{"class":83,"line":380},[152,763,764],{"class":183},"    raise",[152,766,767],{"class":225}," ValueError",[152,769,770],{"class":187},"(",[152,772,294],{"class":183},[152,774,775],{"class":161},"\"no row in the first ",[152,777,301],{"class":300},[152,779,780],{"class":187},"scan_rows",[152,782,316],{"class":300},[152,784,785],{"class":161}," contains all of ",[152,787,301],{"class":300},[152,789,790],{"class":225},"sorted",[152,792,793],{"class":187},"(required)",[152,795,316],{"class":300},[152,797,798],{"class":161},"\"",[152,800,534],{"class":187},[152,802,803],{"class":83,"line":404},[152,804,201],{"emptyLinePlaceholder":200},[152,806,807,809,812,814,817,819,821,823],{"class":83,"line":427},[152,808,207],{"class":183},[152,810,811],{"class":187}," path ",[152,813,213],{"class":183},[152,815,816],{"class":187}," (",[152,818,219],{"class":161},[152,820,222],{"class":187},[152,822,232],{"class":161},[152,824,346],{"class":187},[152,826,827,830],{"class":83,"line":433},[152,828,829],{"class":225},"    print",[152,831,832],{"class":187},"(path, find_header_row(path))\n",[152,834,835],{"class":83,"line":456},[152,836,838],{"class":837},"s-wDw","# march.xlsx 2\n",[152,840,842],{"class":83,"line":841},17,[152,843,844],{"class":837},"# april.xlsx 5\n",[10,846,847,850],{},[13,848,849],{},"\" \".join(str(value).split())"," collapses runs of whitespace, including line breaks and the double spaces\npeople type by accident, before comparing. The error message lists the labels that were expected, which\nis precisely what somebody needs to know when a column has been renamed upstream.",[41,852,50,857,50,860,50,863,50,866,50,872,50,878,50,882,50,886,50,892,50,897,50,901,50,906,50,909,50,912,50,916,50,919,50,922,50,926,50,929,50,932,50,936,50,939,50,942,50,947,50,950,50,953],{"viewBox":853,"role":44,"ariaLabelledBy":854,"xmlns":48,"style":49},"0 0 760 156",[855,856],"hdr-scan-t","hdr-scan-d",[52,858,859],{"id":855},"Scanning for the header row by its labels",[56,861,862],{"id":856},"Read the first thirty rows raw, normalise each row cells to lower-case labels, test whether the required labels are a subset, and return the first matching row position.",[60,864],{"x":62,"y":62,"width":63,"height":865,"fill":65},"156",[60,867],{"x":68,"y":68,"width":70,"height":868,"rx":869,"fill":870,"stroke":871,"style":74},"86","12","#e7ebef","var(--line,#cdd5e6)",[76,873,877],{"x":874,"y":875,"style":876},"95","50","font-size:14px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","preview",[76,879,469],{"x":874,"y":880,"style":881},"72","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[76,883,885],{"x":874,"y":884,"style":881},"92","nrows=30",[83,887],{"x1":888,"y1":889,"x2":890,"y2":889,"stroke":891,"style":74},"175","63","204","var(--brand,#5b5cf0)",[893,894],"polygon",{"points":895,"fill":896},"207,63 198,58 198,68","#5b5cf0",[60,898],{"x":899,"y":68,"width":70,"height":868,"rx":869,"fill":900,"stroke":891,"style":74},"210","#f0f4ff",[76,902,905],{"x":903,"y":875,"style":904},"285","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","normalise",[76,907,908],{"x":903,"y":880,"style":92},"strip, collapse,",[76,910,911],{"x":903,"y":884,"style":92},"lower-case",[83,913],{"x1":914,"y1":889,"x2":915,"y2":889,"stroke":891,"style":74},"365","394",[893,917],{"points":918,"fill":896},"397,63 388,58 388,68",[60,920],{"x":921,"y":68,"width":70,"height":868,"rx":869,"fill":900,"stroke":891,"style":74},"400",[76,923,925],{"x":924,"y":875,"style":904},"475","required ⊆ row?",[76,927,928],{"x":924,"y":880,"style":92},"subset test",[76,930,931],{"x":924,"y":884,"style":92},"per row",[83,933],{"x1":934,"y1":889,"x2":935,"y2":889,"stroke":891,"style":74},"555","584",[893,937],{"points":938,"fill":896},"587,63 578,58 578,68",[60,940],{"x":941,"y":68,"width":70,"height":868,"rx":869,"fill":72,"stroke":73,"style":74},"590",[76,943,946],{"x":944,"y":875,"style":945},"665","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","header=position",[76,948,949],{"x":944,"y":880,"style":92},"read the table",[76,951,952],{"x":944,"y":884,"style":92},"from that row",[76,954,956],{"x":133,"y":955,"style":135},"140","extra columns still match; renamed ones fail loudly",[138,958,960],{"id":959},"step-3-accept-known-aliases","Step 3 — Accept known aliases",[10,962,963,964,967,968,222,971,974,975,978],{},"Label drift is the other half of the problem: ",[13,965,966],{},"Order ID"," becomes ",[13,969,970],{},"Order #",[13,972,973],{},"Amount"," becomes\n",[13,976,977],{},"Amount (EUR)",". Map every accepted spelling to a canonical name, match on canonical names, and rename the\ncolumns once the table is read. The mapping doubles as documentation of every variant you have met.",[143,980,982],{"className":174,"code":981,"language":176,"meta":148,"style":148},"ALIASES = {\n    \"order id\": \"order_id\", \"order #\": \"order_id\", \"order no\": \"order_id\",\n    \"customer\": \"customer\", \"client\": \"customer\",\n    \"amount\": \"amount\", \"amount (eur)\": \"amount\", \"net amount\": \"amount\",\n}\nCANONICAL = set(ALIASES.values())\n\ndef canonical(value) -> str | None:\n    return ALIASES.get(normalise(value))\n\ndef find_header_row_aliased(path, sheet_name=0, scan_rows=30) -> int:\n    preview = pd.read_excel(path, sheet_name=sheet_name, header=None, nrows=scan_rows)\n    for position, row in preview.iterrows():\n        if CANONICAL \u003C= {canonical(v) for v in row.dropna()}:\n            return position\n    raise ValueError(f\"no header row with columns {sorted(CANONICAL)}\")\n\nprint(find_header_row_aliased(\"april.xlsx\"))   # 5\n",[13,983,984,994,1026,1046,1075,1079,1096,1100,1119,1129,1133,1158,1186,1196,1218,1224,1254,1258],{"__ignoreMap":148},[152,985,986,989,991],{"class":83,"line":154},[152,987,988],{"class":225},"ALIASES",[152,990,575],{"class":183},[152,992,993],{"class":187}," {\n",[152,995,996,999,1002,1005,1007,1010,1012,1014,1016,1019,1021,1023],{"class":83,"line":197},[152,997,998],{"class":161},"    \"order id\"",[152,1000,1001],{"class":187},": ",[152,1003,1004],{"class":161},"\"order_id\"",[152,1006,222],{"class":187},[152,1008,1009],{"class":161},"\"order #\"",[152,1011,1001],{"class":187},[152,1013,1004],{"class":161},[152,1015,222],{"class":187},[152,1017,1018],{"class":161},"\"order no\"",[152,1020,1001],{"class":187},[152,1022,1004],{"class":161},[152,1024,1025],{"class":187},",\n",[152,1027,1028,1031,1033,1035,1037,1040,1042,1044],{"class":83,"line":204},[152,1029,1030],{"class":161},"    \"customer\"",[152,1032,1001],{"class":187},[152,1034,586],{"class":161},[152,1036,222],{"class":187},[152,1038,1039],{"class":161},"\"client\"",[152,1041,1001],{"class":187},[152,1043,586],{"class":161},[152,1045,1025],{"class":187},[152,1047,1048,1051,1053,1055,1057,1060,1062,1064,1066,1069,1071,1073],{"class":83,"line":243},[152,1049,1050],{"class":161},"    \"amount\"",[152,1052,1001],{"class":187},[152,1054,591],{"class":161},[152,1056,222],{"class":187},[152,1058,1059],{"class":161},"\"amount (eur)\"",[152,1061,1001],{"class":187},[152,1063,591],{"class":161},[152,1065,222],{"class":187},[152,1067,1068],{"class":161},"\"net amount\"",[152,1070,1001],{"class":187},[152,1072,591],{"class":161},[152,1074,1025],{"class":187},[152,1076,1077],{"class":83,"line":255},[152,1078,594],{"class":187},[152,1080,1081,1084,1086,1089,1091,1093],{"class":83,"line":266},[152,1082,1083],{"class":225},"CANONICAL",[152,1085,575],{"class":183},[152,1087,1088],{"class":225}," set",[152,1090,770],{"class":187},[152,1092,988],{"class":225},[152,1094,1095],{"class":187},".values())\n",[152,1097,1098],{"class":83,"line":277},[152,1099,201],{"emptyLinePlaceholder":200},[152,1101,1102,1104,1107,1109,1111,1114,1117],{"class":83,"line":289},[152,1103,603],{"class":183},[152,1105,1106],{"class":606}," canonical",[152,1108,610],{"class":187},[152,1110,613],{"class":225},[152,1112,1113],{"class":183}," |",[152,1115,1116],{"class":225}," None",[152,1118,616],{"class":187},[152,1120,1121,1123,1126],{"class":83,"line":324},[152,1122,621],{"class":183},[152,1124,1125],{"class":225}," ALIASES",[152,1127,1128],{"class":187},".get(normalise(value))\n",[152,1130,1131],{"class":83,"line":349},[152,1132,201],{"emptyLinePlaceholder":200},[152,1134,1135,1137,1140,1142,1144,1146,1148,1150,1152,1154,1156],{"class":83,"line":355},[152,1136,603],{"class":183},[152,1138,1139],{"class":606}," find_header_row_aliased",[152,1141,646],{"class":187},[152,1143,249],{"class":183},[152,1145,62],{"class":225},[152,1147,660],{"class":187},[152,1149,249],{"class":183},[152,1151,531],{"class":225},[152,1153,667],{"class":187},[152,1155,670],{"class":225},[152,1157,616],{"class":187},[152,1159,1160,1162,1164,1166,1168,1170,1172,1174,1176,1178,1180,1182,1184],{"class":83,"line":380},[152,1161,677],{"class":187},[152,1163,249],{"class":183},[152,1165,682],{"class":187},[152,1167,685],{"class":517},[152,1169,249],{"class":183},[152,1171,690],{"class":187},[152,1173,518],{"class":517},[152,1175,249],{"class":183},[152,1177,360],{"class":225},[152,1179,222],{"class":187},[152,1181,473],{"class":517},[152,1183,249],{"class":183},[152,1185,705],{"class":187},[152,1187,1188,1190,1192,1194],{"class":83,"line":404},[152,1189,327],{"class":183},[152,1191,712],{"class":187},[152,1193,213],{"class":183},[152,1195,717],{"class":187},[152,1197,1198,1200,1203,1206,1209,1211,1213,1215],{"class":83,"line":427},[152,1199,742],{"class":183},[152,1201,1202],{"class":225}," CANONICAL",[152,1204,1205],{"class":183}," \u003C=",[152,1207,1208],{"class":187}," {canonical(v) ",[152,1210,207],{"class":183},[152,1212,732],{"class":187},[152,1214,213],{"class":183},[152,1216,1217],{"class":187}," row.dropna()}:\n",[152,1219,1220,1222],{"class":83,"line":433},[152,1221,756],{"class":183},[152,1223,759],{"class":187},[152,1225,1226,1228,1230,1232,1234,1237,1239,1241,1243,1245,1248,1250,1252],{"class":83,"line":456},[152,1227,764],{"class":183},[152,1229,767],{"class":225},[152,1231,770],{"class":187},[152,1233,294],{"class":183},[152,1235,1236],{"class":161},"\"no header row with columns ",[152,1238,301],{"class":300},[152,1240,790],{"class":225},[152,1242,770],{"class":187},[152,1244,1083],{"class":225},[152,1246,1247],{"class":187},")",[152,1249,316],{"class":300},[152,1251,798],{"class":161},[152,1253,534],{"class":187},[152,1255,1256],{"class":83,"line":841},[152,1257,201],{"emptyLinePlaceholder":200},[152,1259,1261,1263,1266,1268,1271],{"class":83,"line":1260},18,[152,1262,539],{"class":225},[152,1264,1265],{"class":187},"(find_header_row_aliased(",[152,1267,232],{"class":161},[152,1269,1270],{"class":187},"))   ",[152,1272,1273],{"class":837},"# 5\n",[10,1275,1276,1277,39],{},"Keep the alias table in configuration rather than code if the files come from several sources, as in\n",[26,1278,1280],{"href":1279},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002F","Keep Excel Report Settings in a Config File",[138,1282,1284],{"id":1283},"step-4-read-the-table-and-trim-what-surrounds-it","Step 4 — Read the table and trim what surrounds it",[10,1286,1287],{},"With the header position known, read the sheet again from that row. Then remove the three things that\ntypically surround an exported table: an empty leading column, the blank row before the footer, and the\nfooter itself.",[143,1289,1291],{"className":174,"code":1290,"language":176,"meta":148,"style":148},"def read_export(path, sheet_name=0) -> pd.DataFrame:\n    header = find_header_row_aliased(path, sheet_name)\n    frame = pd.read_excel(path, sheet_name=sheet_name, header=header)\n\n    frame = frame.dropna(axis=1, how=\"all\")                    # unused column A\n    frame = frame.rename(columns=lambda c: canonical(c) or c)\n\n    blank = frame.isna().all(axis=1).to_numpy()\n    if blank.any():\n        frame = frame.iloc[: blank.argmax()]                   # stop at the first blank row\n    return frame.reset_index(drop=True)\n\nfor path in (\"march.xlsx\", \"april.xlsx\"):\n    table = read_export(path)\n    print(path, table.columns.tolist(), len(table), table[\"amount\"].sum())\n# march.xlsx ['order_id', 'customer', 'amount'] 2 30.5\n# april.xlsx ['order_id', 'customer', 'amount'] 2 30.5\n",[13,1292,1293,1309,1319,1341,1345,1377,1401,1405,1424,1432,1445,1462,1466,1484,1494,1512,1517],{"__ignoreMap":148},[152,1294,1295,1297,1300,1302,1304,1306],{"class":83,"line":154},[152,1296,603],{"class":183},[152,1298,1299],{"class":606}," read_export",[152,1301,646],{"class":187},[152,1303,249],{"class":183},[152,1305,62],{"class":225},[152,1307,1308],{"class":187},") -> pd.DataFrame:\n",[152,1310,1311,1314,1316],{"class":83,"line":197},[152,1312,1313],{"class":187},"    header ",[152,1315,249],{"class":183},[152,1317,1318],{"class":187}," find_header_row_aliased(path, sheet_name)\n",[152,1320,1321,1324,1326,1328,1330,1332,1334,1336,1338],{"class":83,"line":204},[152,1322,1323],{"class":187},"    frame ",[152,1325,249],{"class":183},[152,1327,682],{"class":187},[152,1329,685],{"class":517},[152,1331,249],{"class":183},[152,1333,690],{"class":187},[152,1335,518],{"class":517},[152,1337,249],{"class":183},[152,1339,1340],{"class":187},"header)\n",[152,1342,1343],{"class":83,"line":243},[152,1344,201],{"emptyLinePlaceholder":200},[152,1346,1347,1349,1351,1354,1357,1359,1361,1363,1366,1368,1371,1374],{"class":83,"line":255},[152,1348,1323],{"class":187},[152,1350,249],{"class":183},[152,1352,1353],{"class":187}," frame.dropna(",[152,1355,1356],{"class":517},"axis",[152,1358,249],{"class":183},[152,1360,389],{"class":225},[152,1362,222],{"class":187},[152,1364,1365],{"class":517},"how",[152,1367,249],{"class":183},[152,1369,1370],{"class":161},"\"all\"",[152,1372,1373],{"class":187},")                    ",[152,1375,1376],{"class":837},"# unused column A\n",[152,1378,1379,1381,1383,1386,1389,1392,1395,1398],{"class":83,"line":266},[152,1380,1323],{"class":187},[152,1382,249],{"class":183},[152,1384,1385],{"class":187}," frame.rename(",[152,1387,1388],{"class":517},"columns",[152,1390,1391],{"class":183},"=lambda",[152,1393,1394],{"class":187}," c: canonical(c) ",[152,1396,1397],{"class":183},"or",[152,1399,1400],{"class":187}," c)\n",[152,1402,1403],{"class":83,"line":277},[152,1404,201],{"emptyLinePlaceholder":200},[152,1406,1407,1410,1412,1415,1417,1419,1421],{"class":83,"line":289},[152,1408,1409],{"class":187},"    blank ",[152,1411,249],{"class":183},[152,1413,1414],{"class":187}," frame.isna().all(",[152,1416,1356],{"class":517},[152,1418,249],{"class":183},[152,1420,389],{"class":225},[152,1422,1423],{"class":187},").to_numpy()\n",[152,1425,1426,1429],{"class":83,"line":324},[152,1427,1428],{"class":183},"    if",[152,1430,1431],{"class":187}," blank.any():\n",[152,1433,1434,1437,1439,1442],{"class":83,"line":349},[152,1435,1436],{"class":187},"        frame ",[152,1438,249],{"class":183},[152,1440,1441],{"class":187}," frame.iloc[: blank.argmax()]                   ",[152,1443,1444],{"class":837},"# stop at the first blank row\n",[152,1446,1447,1449,1452,1455,1457,1460],{"class":83,"line":355},[152,1448,621],{"class":183},[152,1450,1451],{"class":187}," frame.reset_index(",[152,1453,1454],{"class":517},"drop",[152,1456,249],{"class":183},[152,1458,1459],{"class":225},"True",[152,1461,534],{"class":187},[152,1463,1464],{"class":83,"line":380},[152,1465,201],{"emptyLinePlaceholder":200},[152,1467,1468,1470,1472,1474,1476,1478,1480,1482],{"class":83,"line":404},[152,1469,207],{"class":183},[152,1471,811],{"class":187},[152,1473,213],{"class":183},[152,1475,816],{"class":187},[152,1477,219],{"class":161},[152,1479,222],{"class":187},[152,1481,232],{"class":161},[152,1483,346],{"class":187},[152,1485,1486,1489,1491],{"class":83,"line":427},[152,1487,1488],{"class":187},"    table ",[152,1490,249],{"class":183},[152,1492,1493],{"class":187}," read_export(path)\n",[152,1495,1496,1498,1501,1504,1507,1509],{"class":83,"line":433},[152,1497,829],{"class":225},[152,1499,1500],{"class":187},"(path, table.columns.tolist(), ",[152,1502,1503],{"class":225},"len",[152,1505,1506],{"class":187},"(table), table[",[152,1508,591],{"class":161},[152,1510,1511],{"class":187},"].sum())\n",[152,1513,1514],{"class":83,"line":456},[152,1515,1516],{"class":837},"# march.xlsx ['order_id', 'customer', 'amount'] 2 30.5\n",[152,1518,1519],{"class":83,"line":841},[152,1520,1521],{"class":837},"# april.xlsx ['order_id', 'customer', 'amount'] 2 30.5\n",[10,1523,1524,1525,39],{},"Stopping at the first fully blank row removes the footer without having to recognise it. When a footer\nsits directly under the data with no gap, filter it by content instead, which is the subject of\n",[26,1526,1528],{"href":1527},"\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",[41,1530,50,1536,50,1539,50,1542,50,1546,50,1548,50,1551,50,1556,50,1560,50,1564,50,1567,50,1571,50,1575,50,1579,50,1581,50,1585,50,1587,50,1589,50,1591,50,1593,50,1595,50,1598,50,1600,50,1602,50,1605,50,1607,50,1610,50,1612,50,1614,50,1619,50,1624,50,1627,50,1631,50,1633,50,1635,50,1637,50,1639,50,1642,50,1644,50,1646,50,1648,50,1651,50,1653,50,1655,50,1657,50,1659,50,1662,50,1664,50,1666,50,1669,50,1673,50,1675,50,1677,50,1679,50,1681,50,1686,50,1691,50,1693,50,1697,50,1699,50,1701,50,1705,50,1707,50,1709,50,1711,50,1714,50,1718],{"viewBox":1531,"role":44,"ariaLabelledBy":1532,"xmlns":48,"style":1535},"0 0 566 284",[1533,1534],"hdr-trim-t","hdr-trim-d","width:100%;max-width:566px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[52,1537,1538],{"id":1533},"What gets trimmed around a detected table",[56,1540,1541],{"id":1534},"The banner rows above the header are skipped by the header position, the empty column A is dropped, and the blank row and total row below the data are cut off at the first blank row.",[60,1543],{"x":62,"y":62,"width":1544,"height":1545,"fill":65},"566","284",[60,1547],{"x":68,"y":68,"width":85,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},[60,1549],{"x":86,"y":68,"width":1550,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},"60",[76,1552,1555],{"x":868,"y":1553,"style":1554},"40","font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[60,1557],{"x":1558,"y":68,"width":1559,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},"116","90",[76,1561,1563],{"x":1562,"y":1553,"style":1554},"161","B",[60,1565],{"x":1566,"y":68,"width":1559,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},"206",[76,1568,1570],{"x":1569,"y":1553,"style":1554},"251","C",[60,1572],{"x":1573,"y":68,"width":1574,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},"296","80",[76,1576,1578],{"x":1577,"y":1553,"style":1554},"336","D",[60,1580],{"x":68,"y":875,"width":85,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},[76,1582,310],{"x":1583,"y":1584,"style":1554},"38","70",[60,1586],{"x":86,"y":875,"width":1550,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[60,1588],{"x":1558,"y":875,"width":1559,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[60,1590],{"x":1566,"y":875,"width":1559,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[60,1592],{"x":1573,"y":875,"width":1574,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[60,1594],{"x":68,"y":1574,"width":85,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},[76,1596,237],{"x":1583,"y":1597,"style":1554},"100",[60,1599],{"x":86,"y":1574,"width":1550,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},[60,1601],{"x":1558,"y":1574,"width":1559,"height":531,"rx":62,"fill":900,"stroke":891,"style":88},[76,1603,966],{"x":1562,"y":1597,"style":1604},"font-size:11.5px;font-weight:600;fill:var(--brand-strong,#4338ca);text-anchor:middle",[60,1606],{"x":1566,"y":1574,"width":1559,"height":531,"rx":62,"fill":900,"stroke":891,"style":88},[76,1608,1609],{"x":1569,"y":1597,"style":1604},"Customer",[60,1611],{"x":1573,"y":1574,"width":1574,"height":531,"rx":62,"fill":900,"stroke":891,"style":88},[76,1613,973],{"x":1577,"y":1597,"style":1604},[60,1615],{"x":1616,"y":1617,"width":70,"height":1618,"rx":545,"fill":900,"stroke":891,"style":88},"396","82","26",[76,1620,1623],{"x":1621,"y":1622,"style":1604},"471","99","detected header",[60,1625],{"x":68,"y":1626,"width":85,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},"110",[76,1628,1630],{"x":1583,"y":1629,"style":1554},"130","7",[60,1632],{"x":86,"y":1626,"width":1550,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},[60,1634],{"x":1558,"y":1626,"width":1559,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[76,1636,389],{"x":1562,"y":1629,"style":92},[60,1638],{"x":1566,"y":1626,"width":1559,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[76,1640,1641],{"x":1569,"y":1629,"style":92},"Ana",[60,1643],{"x":1573,"y":1626,"width":1574,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[76,1645,399],{"x":1577,"y":1629,"style":92},[60,1647],{"x":68,"y":955,"width":85,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},[76,1649,545],{"x":1583,"y":1650,"style":1554},"160",[60,1652],{"x":86,"y":955,"width":1550,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},[60,1654],{"x":1558,"y":955,"width":1559,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[76,1656,413],{"x":1562,"y":1650,"style":92},[60,1658],{"x":1566,"y":955,"width":1559,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[76,1660,1661],{"x":1569,"y":1650,"style":92},"Ben",[60,1663],{"x":1573,"y":955,"width":1574,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[76,1665,68],{"x":1577,"y":1650,"style":92},[60,1667],{"x":68,"y":1668,"width":85,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},"170",[76,1670,1672],{"x":1583,"y":1671,"style":1554},"190","9",[60,1674],{"x":86,"y":1668,"width":1550,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[60,1676],{"x":1558,"y":1668,"width":1559,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[60,1678],{"x":1566,"y":1668,"width":1559,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[60,1680],{"x":1573,"y":1668,"width":1574,"height":531,"rx":62,"fill":65,"stroke":871,"style":88},[60,1682],{"x":1616,"y":1683,"width":70,"height":1618,"rx":545,"fill":1684,"stroke":1685,"style":88},"172","#fdefd8","var(--gold,#b4740a)",[76,1687,1690],{"x":1621,"y":1688,"style":1689},"189","font-size:11.5px;font-weight:600;fill:var(--gold-ink,#7a4e06);text-anchor:middle","first blank: cut",[60,1692],{"x":68,"y":134,"width":85,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},[76,1694,1696],{"x":1583,"y":1695,"style":1554},"220","10",[60,1698],{"x":86,"y":134,"width":1550,"height":531,"rx":62,"fill":870,"stroke":871,"style":88},[60,1700],{"x":1558,"y":134,"width":1559,"height":531,"rx":62,"fill":109,"stroke":110,"style":88},[76,1702,1704],{"x":1562,"y":1695,"style":1703},"font-size:11.5px;font-weight:600;fill:var(--accent-ink,#be185d);text-anchor:middle","Total",[60,1706],{"x":1566,"y":134,"width":1559,"height":531,"rx":62,"fill":109,"stroke":110,"style":88},[60,1708],{"x":1573,"y":134,"width":1574,"height":531,"rx":62,"fill":109,"stroke":110,"style":88},[76,1710,451],{"x":1577,"y":1695,"style":1703},[60,1712],{"x":1616,"y":1713,"width":70,"height":1618,"rx":545,"fill":109,"stroke":110,"style":88},"202",[76,1715,1717],{"x":1621,"y":1716,"style":1703},"219","footer: dropped",[76,1719,1722],{"x":1720,"y":1721,"style":135},"283","262","column A is empty in every row, so dropna(axis=1) removes it",[138,1724,1726],{"id":1725},"step-5-make-detection-failures-obvious","Step 5 — Make detection failures obvious",[10,1728,1729],{},"The value of this approach is that the failure mode changes from \"wrong data\" to \"clear error\". Push that\nfurther by reporting what the scan actually saw, so the person reading the log can tell a renamed column\nfrom a completely different file.",[143,1731,1733],{"className":174,"code":1732,"language":176,"meta":148,"style":148},"def describe_top(path, sheet_name=0, rows=10) -> str:\n    preview = pd.read_excel(path, sheet_name=sheet_name, header=None, nrows=rows)\n    lines = []\n    for position, row in preview.iterrows():\n        cells = [str(v) for v in row.dropna()]\n        if cells:\n            lines.append(f\"  row {position + 1}: {' | '.join(cells)[:90]}\")\n    return \"\\n\".join(lines)\n\ntry:\n    find_header_row(\"april.xlsx\", required={\"order id\", \"customer\", \"region\"})\nexcept ValueError as exc:\n    print(exc)\n    print(describe_top(\"april.xlsx\"))\n",[13,1734,1735,1761,1790,1800,1810,1834,1841,1884,1899,1903,1910,1940,1953,1960],{"__ignoreMap":148},[152,1736,1737,1739,1742,1744,1746,1748,1751,1753,1755,1757,1759],{"class":83,"line":154},[152,1738,603],{"class":183},[152,1740,1741],{"class":606}," describe_top",[152,1743,646],{"class":187},[152,1745,249],{"class":183},[152,1747,62],{"class":225},[152,1749,1750],{"class":187},", rows",[152,1752,249],{"class":183},[152,1754,1696],{"class":225},[152,1756,667],{"class":187},[152,1758,613],{"class":225},[152,1760,616],{"class":187},[152,1762,1763,1765,1767,1769,1771,1773,1775,1777,1779,1781,1783,1785,1787],{"class":83,"line":197},[152,1764,677],{"class":187},[152,1766,249],{"class":183},[152,1768,682],{"class":187},[152,1770,685],{"class":517},[152,1772,249],{"class":183},[152,1774,690],{"class":187},[152,1776,518],{"class":517},[152,1778,249],{"class":183},[152,1780,360],{"class":225},[152,1782,222],{"class":187},[152,1784,473],{"class":517},[152,1786,249],{"class":183},[152,1788,1789],{"class":187},"rows)\n",[152,1791,1792,1795,1797],{"class":83,"line":204},[152,1793,1794],{"class":187},"    lines ",[152,1796,249],{"class":183},[152,1798,1799],{"class":187}," []\n",[152,1801,1802,1804,1806,1808],{"class":83,"line":243},[152,1803,327],{"class":183},[152,1805,712],{"class":187},[152,1807,213],{"class":183},[152,1809,717],{"class":187},[152,1811,1812,1815,1817,1820,1822,1825,1827,1829,1831],{"class":83,"line":255},[152,1813,1814],{"class":187},"        cells ",[152,1816,249],{"class":183},[152,1818,1819],{"class":187}," [",[152,1821,613],{"class":225},[152,1823,1824],{"class":187},"(v) ",[152,1826,207],{"class":183},[152,1828,732],{"class":187},[152,1830,213],{"class":183},[152,1832,1833],{"class":187}," row.dropna()]\n",[152,1835,1836,1838],{"class":83,"line":266},[152,1837,742],{"class":183},[152,1839,1840],{"class":187}," cells:\n",[152,1842,1843,1846,1848,1851,1853,1856,1859,1862,1864,1866,1868,1871,1874,1876,1878,1880,1882],{"class":83,"line":277},[152,1844,1845],{"class":187},"            lines.append(",[152,1847,294],{"class":183},[152,1849,1850],{"class":161},"\"  row ",[152,1852,301],{"class":300},[152,1854,1855],{"class":187},"position ",[152,1857,1858],{"class":183},"+",[152,1860,1861],{"class":225}," 1",[152,1863,316],{"class":300},[152,1865,1001],{"class":161},[152,1867,301],{"class":300},[152,1869,1870],{"class":161},"' | '",[152,1872,1873],{"class":187},".join(cells)[:",[152,1875,1559],{"class":225},[152,1877,313],{"class":187},[152,1879,316],{"class":300},[152,1881,798],{"class":161},[152,1883,534],{"class":187},[152,1885,1886,1888,1891,1894,1896],{"class":83,"line":289},[152,1887,621],{"class":183},[152,1889,1890],{"class":161}," \"",[152,1892,1893],{"class":300},"\\n",[152,1895,798],{"class":161},[152,1897,1898],{"class":187},".join(lines)\n",[152,1900,1901],{"class":83,"line":324},[152,1902,201],{"emptyLinePlaceholder":200},[152,1904,1905,1908],{"class":83,"line":349},[152,1906,1907],{"class":183},"try",[152,1909,616],{"class":187},[152,1911,1912,1915,1917,1919,1922,1924,1926,1928,1930,1932,1934,1937],{"class":83,"line":355},[152,1913,1914],{"class":187},"    find_header_row(",[152,1916,232],{"class":161},[152,1918,222],{"class":187},[152,1920,1921],{"class":517},"required",[152,1923,249],{"class":183},[152,1925,301],{"class":187},[152,1927,581],{"class":161},[152,1929,222],{"class":187},[152,1931,586],{"class":161},[152,1933,222],{"class":187},[152,1935,1936],{"class":161},"\"region\"",[152,1938,1939],{"class":187},"})\n",[152,1941,1942,1945,1947,1950],{"class":83,"line":380},[152,1943,1944],{"class":183},"except",[152,1946,767],{"class":225},[152,1948,1949],{"class":183}," as",[152,1951,1952],{"class":187}," exc:\n",[152,1954,1955,1957],{"class":83,"line":404},[152,1956,829],{"class":225},[152,1958,1959],{"class":187},"(exc)\n",[152,1961,1962,1964,1967,1969],{"class":83,"line":427},[152,1963,829],{"class":225},[152,1965,1966],{"class":187},"(describe_top(",[152,1968,232],{"class":161},[152,1970,548],{"class":187},[10,1972,1973,1974,1977,1978,1981],{},"That printout shows ",[13,1975,1976],{},"Order ID | Customer | Amount"," on row 6 and no ",[13,1979,1980],{},"Region"," anywhere — enough for a human\nto see at a glance that the export no longer contains a column the report depends on.",[138,1983,1985],{"id":1984},"step-6-process-a-folder-of-exports-with-mixed-layouts","Step 6 — Process a folder of exports with mixed layouts",[10,1987,1988],{},"Header detection earns its keep when a job consumes many files at once: twelve monthly exports, or one file\nper branch, each saved by a different person with a slightly different banner. Run the reader over the whole\nfolder, record the detected header position and row count per file, and keep going when one file fails so a\nsingle broken export does not block the other eleven.",[143,1990,1992],{"className":174,"code":1991,"language":176,"meta":148,"style":148},"from pathlib import Path\n\ndef load_folder(folder: str) -> tuple[pd.DataFrame, pd.DataFrame]:\n    frames, audit = [], []\n    for path in sorted(Path(folder).glob(\"*.xlsx\")):\n        try:\n            header = find_header_row_aliased(path)\n            table = read_export(path).assign(source=path.name)\n        except ValueError as exc:\n            audit.append({\"file\": path.name, \"header_row\": None, \"rows\": 0, \"error\": str(exc)})\n            continue\n        frames.append(table)\n        audit.append({\"file\": path.name, \"header_row\": header + 1, \"rows\": len(table), \"error\": \"\"})\n    combined = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()\n    return combined, pd.DataFrame(audit)\n\ncombined, audit = load_folder(\".\")\nprint(audit)\nprint(combined.groupby(\"source\")[\"amount\"].sum())\n",[13,1993,1994,2006,2010,2025,2035,2054,2061,2071,2089,2100,2139,2144,2149,2187,2219,2226,2230,2245,2252],{"__ignoreMap":148},[152,1995,1996,1998,2001,2003],{"class":83,"line":154},[152,1997,184],{"class":183},[152,1999,2000],{"class":187}," pathlib ",[152,2002,191],{"class":183},[152,2004,2005],{"class":187}," Path\n",[152,2007,2008],{"class":83,"line":197},[152,2009,201],{"emptyLinePlaceholder":200},[152,2011,2012,2014,2017,2020,2022],{"class":83,"line":204},[152,2013,603],{"class":183},[152,2015,2016],{"class":606}," load_folder",[152,2018,2019],{"class":187},"(folder: ",[152,2021,613],{"class":225},[152,2023,2024],{"class":187},") -> tuple[pd.DataFrame, pd.DataFrame]:\n",[152,2026,2027,2030,2032],{"class":83,"line":243},[152,2028,2029],{"class":187},"    frames, audit ",[152,2031,249],{"class":183},[152,2033,2034],{"class":187}," [], []\n",[152,2036,2037,2039,2041,2043,2046,2049,2052],{"class":83,"line":255},[152,2038,327],{"class":183},[152,2040,811],{"class":187},[152,2042,213],{"class":183},[152,2044,2045],{"class":225}," sorted",[152,2047,2048],{"class":187},"(Path(folder).glob(",[152,2050,2051],{"class":161},"\"*.xlsx\"",[152,2053,240],{"class":187},[152,2055,2056,2059],{"class":83,"line":266},[152,2057,2058],{"class":183},"        try",[152,2060,616],{"class":187},[152,2062,2063,2066,2068],{"class":83,"line":277},[152,2064,2065],{"class":187},"            header ",[152,2067,249],{"class":183},[152,2069,2070],{"class":187}," find_header_row_aliased(path)\n",[152,2072,2073,2076,2078,2081,2084,2086],{"class":83,"line":289},[152,2074,2075],{"class":187},"            table ",[152,2077,249],{"class":183},[152,2079,2080],{"class":187}," read_export(path).assign(",[152,2082,2083],{"class":517},"source",[152,2085,249],{"class":183},[152,2087,2088],{"class":187},"path.name)\n",[152,2090,2091,2094,2096,2098],{"class":83,"line":324},[152,2092,2093],{"class":183},"        except",[152,2095,767],{"class":225},[152,2097,1949],{"class":183},[152,2099,1952],{"class":187},[152,2101,2102,2105,2108,2111,2114,2116,2118,2120,2123,2125,2127,2129,2132,2134,2136],{"class":83,"line":349},[152,2103,2104],{"class":187},"            audit.append({",[152,2106,2107],{"class":161},"\"file\"",[152,2109,2110],{"class":187},": path.name, ",[152,2112,2113],{"class":161},"\"header_row\"",[152,2115,1001],{"class":187},[152,2117,360],{"class":225},[152,2119,222],{"class":187},[152,2121,2122],{"class":161},"\"rows\"",[152,2124,1001],{"class":187},[152,2126,62],{"class":225},[152,2128,222],{"class":187},[152,2130,2131],{"class":161},"\"error\"",[152,2133,1001],{"class":187},[152,2135,613],{"class":225},[152,2137,2138],{"class":187},"(exc)})\n",[152,2140,2141],{"class":83,"line":355},[152,2142,2143],{"class":183},"            continue\n",[152,2145,2146],{"class":83,"line":380},[152,2147,2148],{"class":187},"        frames.append(table)\n",[152,2150,2151,2154,2156,2158,2160,2163,2165,2167,2169,2171,2173,2175,2178,2180,2182,2185],{"class":83,"line":404},[152,2152,2153],{"class":187},"        audit.append({",[152,2155,2107],{"class":161},[152,2157,2110],{"class":187},[152,2159,2113],{"class":161},[152,2161,2162],{"class":187},": header ",[152,2164,1858],{"class":183},[152,2166,1861],{"class":225},[152,2168,222],{"class":187},[152,2170,2122],{"class":161},[152,2172,1001],{"class":187},[152,2174,1503],{"class":225},[152,2176,2177],{"class":187},"(table), ",[152,2179,2131],{"class":161},[152,2181,1001],{"class":187},[152,2183,2184],{"class":161},"\"\"",[152,2186,1939],{"class":187},[152,2188,2189,2192,2194,2197,2200,2202,2204,2207,2210,2213,2216],{"class":83,"line":427},[152,2190,2191],{"class":187},"    combined ",[152,2193,249],{"class":183},[152,2195,2196],{"class":187}," pd.concat(frames, ",[152,2198,2199],{"class":517},"ignore_index",[152,2201,249],{"class":183},[152,2203,1459],{"class":225},[152,2205,2206],{"class":187},") ",[152,2208,2209],{"class":183},"if",[152,2211,2212],{"class":187}," frames ",[152,2214,2215],{"class":183},"else",[152,2217,2218],{"class":187}," pd.DataFrame()\n",[152,2220,2221,2223],{"class":83,"line":433},[152,2222,621],{"class":183},[152,2224,2225],{"class":187}," combined, pd.DataFrame(audit)\n",[152,2227,2228],{"class":83,"line":456},[152,2229,201],{"emptyLinePlaceholder":200},[152,2231,2232,2235,2237,2240,2243],{"class":83,"line":841},[152,2233,2234],{"class":187},"combined, audit ",[152,2236,249],{"class":183},[152,2238,2239],{"class":187}," load_folder(",[152,2241,2242],{"class":161},"\".\"",[152,2244,534],{"class":187},[152,2246,2247,2249],{"class":83,"line":1260},[152,2248,539],{"class":225},[152,2250,2251],{"class":187},"(audit)\n",[152,2253,2255,2257,2260,2263,2266,2268],{"class":83,"line":2254},19,[152,2256,539],{"class":225},[152,2258,2259],{"class":187},"(combined.groupby(",[152,2261,2262],{"class":161},"\"source\"",[152,2264,2265],{"class":187},")[",[152,2267,591],{"class":161},[152,2269,1511],{"class":187},[10,2271,2272,2273,2276,2277,39],{},"The audit frame is the part to keep. Writing it to a second sheet of the output workbook, or to the job log,\nrecords which row each file's table started on. When a total looks wrong next month, a header position that\nsuddenly changed from 3 to 9 is often the first clue that the export itself changed — a new filter block in\nthe banner, or a different report template chosen by mistake. ",[13,2274,2275],{},"header + 1"," converts the zero-based pandas\nposition back into the row number a person sees in Excel, which avoids a confusing off-by-one conversation.\nCombining the files afterwards follows the patterns in\n",[26,2278,2280],{"href":2279},"\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",[138,2282,2284],{"id":2283},"common-pitfalls","Common pitfalls",[2286,2287,2288,2304],"table",{},[2289,2290,2291],"thead",{},[2292,2293,2294,2298,2301],"tr",{},[2295,2296,2297],"th",{},"Symptom",[2295,2299,2300],{},"Cause",[2295,2302,2303],{},"Fix",[2305,2306,2307,2319,2338,2355,2369,2388],"tbody",{},[2292,2308,2309,2313,2316],{},[2310,2311,2312],"td",{},"Header found in the banner",[2310,2314,2315],{},"A title contains one of the required words",[2310,2317,2318],{},"Require several labels, never one",[2292,2320,2321,2324,2327],{},[2310,2322,2323],{},"Header not found though it is visible",[2310,2325,2326],{},"Non-breaking spaces or line breaks in labels",[2310,2328,2329,2330,2333,2334,2337],{},"Normalise with ",[13,2331,2332],{},"split()"," and ",[13,2335,2336],{},"join"," as in Step 2",[2292,2339,2340,2343,2349],{},[2310,2341,2342],{},"Row position off by one",[2310,2344,2345,2348],{},[13,2346,2347],{},"skiprows"," passed to the preview",[2310,2350,2351,2352,2354],{},"Never combine ",[13,2353,2347],{}," with the scan",[2292,2356,2357,2360,2363],{},[2310,2358,2359],{},"Detection slow on huge files",[2310,2361,2362],{},"Preview read the whole sheet",[2310,2364,2365,2366,2368],{},"Always pass ",[13,2367,473],{}," to the preview",[2292,2370,2371,2374,2381],{},[2310,2372,2373],{},"Numeric header labels do not match",[2310,2375,2376,2377,2380],{},"Year headers such as ",[13,2378,2379],{},"2026"," read as numbers",[2310,2382,2383,2384,2387],{},"Compare ",[13,2385,2386],{},"normalise(v)"," strings, which converts them",[2292,2389,2390,2393,2396],{},[2310,2391,2392],{},"Table cut short",[2310,2394,2395],{},"A blank row inside the data",[2310,2397,2398],{},"Stop at two consecutive blank rows, or filter blank rows instead of cutting",[138,2400,2402],{"id":2401},"performance-and-scale-notes","Performance and scale notes",[10,2404,2405,2406,2408,2409,2411,2412,2415,2416,39],{},"The preview is the only extra cost, and with ",[13,2407,885],{}," it is a small fraction of the full read. The\nsecond read parses the table as normal. If a file is large enough that two reads hurt, read once with\n",[13,2410,469],{},", find the header position in memory, and promote that row with ",[13,2413,2414],{},"set_axis"," — the same\ntechnique used in ",[26,2417,2419],{"href":2418},"\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",[41,2421,50,2427,50,2430,50,2433,50,2437,50,2441,50,2444,50,2450,50,2454,50,2460,50,2465,50,2467,50,2471,50,2474,50,2477,50,2481,50,2484,50,2488,50,2491,50,2495,50,2499],{"viewBox":2422,"role":44,"ariaLabelledBy":2423,"xmlns":48,"style":2426},"0 0 720 212",[2424,2425],"hdr-cost-t","hdr-cost-d","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[52,2428,2429],{"id":2424},"Cost of header detection relative to the table read",[56,2431,2432],{"id":2425},"A thirty-row preview costs a few percent of a full read. Reading the sheet once raw and promoting the header in memory avoids the second parse entirely.",[60,2434],{"x":62,"y":62,"width":2435,"height":2436,"fill":65},"720","212",[76,2438,2440],{"x":68,"y":531,"style":2439},"font-size:12px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","relative time on a 200,000-row export",[60,2442],{"x":68,"y":79,"width":1668,"height":2443,"rx":237,"fill":65,"stroke":871,"style":88},"28",[76,2445,2449],{"x":2446,"y":2447,"style":2448},"105","65","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:middle","30-row preview",[60,2451],{"x":890,"y":2452,"width":2453,"height":1618,"rx":237,"fill":870,"stroke":871,"style":88},"47","330",[60,2455],{"x":2456,"y":2457,"width":2458,"height":2459,"rx":310,"fill":72,"stroke":73,"style":88},"205","48","11.2","24",[76,2461,2464],{"x":2462,"y":2447,"style":2463},"546","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","a few percent",[60,2466],{"x":68,"y":1559,"width":1668,"height":2443,"rx":237,"fill":65,"stroke":871,"style":88},[76,2468,2470],{"x":2446,"y":2469,"style":2448},"109","table read",[60,2472],{"x":890,"y":2473,"width":2453,"height":1618,"rx":237,"fill":870,"stroke":871,"style":88},"91",[60,2475],{"x":2456,"y":884,"width":2476,"height":2459,"rx":310,"fill":1684,"stroke":1685,"style":88},"295",[76,2478,2480],{"x":2462,"y":2469,"style":2479},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","the real cost",[60,2482],{"x":68,"y":2483,"width":1668,"height":2443,"rx":237,"fill":65,"stroke":871,"style":88},"134",[76,2485,2487],{"x":2446,"y":2486,"style":2448},"153","single raw read",[60,2489],{"x":890,"y":2490,"width":2453,"height":1618,"rx":237,"fill":870,"stroke":871,"style":88},"135",[60,2492],{"x":2456,"y":2493,"width":2494,"height":2459,"rx":310,"fill":900,"stroke":891,"style":88},"136","301.6",[76,2496,2498],{"x":2462,"y":2486,"style":2497},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","no second parse",[76,2500,2503],{"x":2501,"y":2502,"style":135},"360","194","two reads are fine until the file is large",[138,2505,2507],{"id":2506},"conclusion","Conclusion",[10,2509,2510,2511,2333,2513,2515],{},"Find the header by its content. Preview the top of the sheet with ",[13,2512,469],{},[13,2514,473],{},", normalise each\nrow's labels, and pick the first row that contains every label you need — through an alias table when the\nnames drift. Read from that position, drop empty columns, stop at the first blank row, and make detection\nfailures print what the scan saw. The script then tolerates banners that grow and columns that are added,\nand fails loudly on the one change that genuinely needs a human.",[138,2517,2519],{"id":2518},"frequently-asked-questions","Frequently asked questions",[10,2521,2522,2526],{},[2523,2524,2525],"strong",{},"Why not just use skiprows with a larger number?","\nBecause a fixed number is only right for one layout. Too small and the banner becomes the header; too large and you skip real rows. Detecting the header adapts to each file.",[10,2528,2529,2532],{},[2523,2530,2531],{},"How many rows should the preview scan?","\nEnough to cover the tallest banner you have seen, with room to spare. Thirty is a good default; a report with a long parameter block may need fifty. The cost of scanning more is small.",[10,2534,2535,2538,2539,2542],{},[2523,2536,2537],{},"What if the header spans two rows?","\nFind the lower row by its labels, then pass header=",[152,2540,2541],{},"position - 1, position"," to read_excel. The approach in the multi-row headers guide explains how the two levels combine.",[10,2544,2545,2548],{},[2523,2546,2547],{},"Can I detect the header column as well as the row?","\nYes. Once the row is found, the first non-empty cell in it gives the starting column, and usecols can restrict the read to the table's width.",[10,2550,2551,2554],{},[2523,2552,2553],{},"Does this work with CSV exports too?","\nYes. Replace read_excel with read_csv in the preview and the final read; header=None and nrows behave the same way. CSV banners are often shorter, but they drift just as much.",[138,2556,2558],{"id":2557},"related","Related",[2560,2561,2562,2569,2576,2583,2590],"ul",{},[2563,2564,2565,2566,2568],"li",{},"Up one level: ",[26,2567,29],{"href":28}," — techniques for sheets built for people.",[2563,2570,2571,2575],{},[26,2572,2574],{"href":2573},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fskip-rows-and-set-header-when-reading-excel-with-pandas\u002F","Skip Rows and Set the Header When Reading Excel with pandas"," — the fixed-layout options this page replaces.",[2563,2577,2578,2582],{},[26,2579,2581],{"href":2580},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas\u002F","Read Excel with Multi-Row Headers in pandas"," — when the detected header has a second storey.",[2563,2584,2585,2589],{},[26,2586,2588],{"href":2587},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-columns-before-import-with-pandas\u002F","Validate Excel Columns Before Import with pandas"," — checking the columns after they are found.",[2563,2591,2592,2594],{},[26,2593,1528],{"href":1527}," — cleaning up the bottom of the same export.",[2596,2597,2598],"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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":148,"searchDepth":197,"depth":197,"links":2600},[2601,2602,2603,2604,2605,2606,2607,2608,2609,2610,2611,2612],{"id":140,"depth":197,"text":141},{"id":462,"depth":197,"text":463},{"id":558,"depth":197,"text":559},{"id":959,"depth":197,"text":960},{"id":1283,"depth":197,"text":1284},{"id":1725,"depth":197,"text":1726},{"id":1984,"depth":197,"text":1985},{"id":2283,"depth":197,"text":2284},{"id":2401,"depth":197,"text":2402},{"id":2506,"depth":197,"text":2507},{"id":2518,"depth":197,"text":2519},{"id":2557,"depth":197,"text":2558},"2026-09-17","Exports that start their table on row 3 one month and row 6 the next break skiprows. Scan for the row that holds your required labels, then read from there — with aliases and column offsets handled.","md",[2617,2619,2621,2623,2625],{"q":2525,"a":2618},"Because a fixed number is only right for one layout. Too small and the banner becomes the header; too large and you skip real rows. Detecting the header adapts to each file.",{"q":2531,"a":2620},"Enough to cover the tallest banner you have seen, with room to spare. Thirty is a good default; a report with a long parameter block may need fifty. The cost of scanning more is small.",{"q":2537,"a":2622},"Find the lower row by its labels, then pass header=[position - 1, position] to read_excel. The approach in the multi-row headers guide explains how the two levels combine.",{"q":2547,"a":2624},"Yes. Once the row is found, the first non-empty cell in it gives the starting column, and usecols can restrict the read to the table's width.",{"q":2553,"a":2626},"Yes. Replace read_excel with read_csv in the preview and the final read; header=None and nrows behave the same way. CSV banners are often shorter, but they drift just as much.","find the header row in an excel sheet with a shifting layout",null,{"breadcrumb":2630},[2631,2634,2637],{"name":2632,"item":2633},"Home","\u002F",{"name":2635,"item":2636},"Advanced Data Transformation and Cleaning","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":29,"item":28},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout",{"title":5,"description":2640},"Detect the header row of an Excel export in pandas when banner rows vary: scan with header=None, match required labels and aliases, trim offset columns and stop at the first blank row.","find-the-header-row-in-an-excel-sheet-with-a-shifting-layout","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout\u002Findex",[2644,2645,2646],"messy-sheets","reading-excel","pandas","how-to","1KAk-niYGcL6IGM-CHcVsmKnMbR8mqWhYVQYf6NZhww",[2650,2654],{"title":2651,"path":2652,"stem":2653,"children":-1},"Extract Report Header Fields from an Excel Sheet with Python","\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet\u002Findex",{"title":2655,"path":2656,"stem":2657,"children":-1},"Parse Numbers with Units in Excel Cells","\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fparse-numbers-with-units-in-excel-cells","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fparse-numbers-with-units-in-excel-cells\u002Findex",[2659,2661],{"path":2660,"title":29},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets",{"path":2662,"title":2663},"\u002Fadvanced-data-transformation-and-cleaning","Advanced Data Transformation and Cleaning for Excel Automation",[],[2666,2669,2672,2675,2678,2681],{"path":2667,"title":2668,"kind":2647},"\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":2670,"title":2671,"kind":2647},"\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":2673,"title":2674,"kind":2647},"\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":2676,"title":2677,"kind":2647},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fcombine-sheets-with-different-layouts-into-one-table","Combine Sheets with Different Layouts into One Table",{"path":2679,"title":2680,"kind":2647},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-hyperlink-urls-from-excel-cells","Extract Hyperlink URLs from Excel Cells with openpyxl",{"path":2652,"title":2651,"kind":2647},1790972090885]