[{"data":1,"prerenderedAt":2278},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas":2245,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas":2253,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas":2259,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas":2260},{"id":4,"title":5,"body":6,"dateModified":2210,"datePublished":2210,"description":2211,"extension":2212,"faq":2213,"keyword":2222,"kind":2223,"meta":2224,"navigation":300,"path":2233,"seo":2234,"slug":2237,"stem":2238,"topics":2239,"type":2243,"__hash__":2244},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas\u002Findex.md","Read Excel with Multi-Row Headers in pandas",{"type":7,"value":8,"toc":2196},"minimark",[9,44,232,237,265,272,498,502,519,600,610,704,707,711,723,834,844,937,941,948,1052,1059,1063,1070,1197,1211,1311,1315,1320,1515,1540,1544,1559,1846,1857,1861,1979,2000,2004,2010,2096,2102,2106,2119,2123,2134,2140,2146,2152,2156,2192],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,37,38,43],"p",{},"Finance and operations workbooks love a two-storey header: a merged ",[14,15,16],"code",{},"Q1"," cell sitting above\n",[14,19,20],{},"Revenue"," and ",[14,23,24],{},"Units",", then ",[14,27,28],{},"Q2"," above the same pair again. It reads well on screen and badly in\npandas, where a default ",[14,31,32],{},"read_excel"," takes only the top row as the header, turns the merged gaps into\n",[14,35,36],{},"Unnamed: 2"," columns and pushes the real sub-headers down into the first data row. This guide from\n",[39,40,42],"a",{"href":41},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002F","Extracting Data from Messy Excel Sheets","\nshows how to read those headers properly and then decide what shape you actually want the data in.",[45,46,54,55,54,59,54,63,54,70,54,77,54,86,54,90,54,96,54,100,54,104,54,107,54,111,54,113,54,118,54,122,54,126,54,128,54,131,54,133,54,136,54,142,54,147,54,150,54,154,54,156,54,160,54,164,54,166,54,168,54,171,54,175,54,178,54,182,54,184,54,188,54,190,54,193,54,195,54,198,54,201,54,205,54,208,54,212,54,214,54,217,54,219,54,222,54,224,54,227],"svg",{"viewBox":47,"role":48,"ariaLabelledBy":49,"xmlns":52,"style":53},"0 0 626 254","img",[50,51],"mrh-sheet-t","mrh-sheet-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:626px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[56,57,58],"title",{"id":50},"A two-row header with merged quarter labels",[60,61,62],"desc",{"id":51},"Row 1 holds Region and the merged labels Q1 and Q2; row 2 holds Revenue and Units under each quarter; data starts in row 3.",[64,65],"rect",{"x":66,"y":66,"width":67,"height":68,"fill":69},"0","626","254","#ffffff",[71,72,76],"text",{"x":73,"y":74,"style":75},"313","32","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","two header rows, one logical column name each",[64,78],{"x":79,"y":80,"width":81,"height":82,"rx":66,"fill":83,"stroke":84,"style":85},"20","50","36","30","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[64,87],{"x":88,"y":80,"width":89,"height":82,"rx":66,"fill":83,"stroke":84,"style":85},"56","80",[71,91,95],{"x":92,"y":93,"style":94},"96","70","font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[64,97],{"x":98,"y":80,"width":99,"height":82,"rx":66,"fill":83,"stroke":84,"style":85},"136","150",[71,101,103],{"x":102,"y":93,"style":94},"211","B",[64,105],{"x":106,"y":80,"width":99,"height":82,"rx":66,"fill":83,"stroke":84,"style":85},"286",[71,108,110],{"x":109,"y":93,"style":94},"361","C",[64,112],{"x":79,"y":89,"width":81,"height":82,"rx":66,"fill":83,"stroke":84,"style":85},[71,114,117],{"x":115,"y":116,"style":94},"38","100","1",[64,119],{"x":88,"y":89,"width":89,"height":82,"rx":66,"fill":120,"stroke":121,"style":85},"#f0f4ff","var(--brand,#5b5cf0)",[71,123,125],{"x":92,"y":116,"style":124},"font-size:11.5px;font-weight:600;fill:var(--brand-strong,#4338ca);text-anchor:middle","Region",[64,127],{"x":98,"y":89,"width":99,"height":82,"rx":66,"fill":120,"stroke":121,"style":85},[71,129,130],{"x":102,"y":116,"style":124},"Q1 (merged)",[64,132],{"x":106,"y":89,"width":99,"height":82,"rx":66,"fill":120,"stroke":121,"style":85},[71,134,135],{"x":109,"y":116,"style":124},"Q2 (merged)",[64,137],{"x":138,"y":139,"width":99,"height":140,"rx":141,"fill":120,"stroke":121,"style":85},"456","82","26","8",[71,143,146],{"x":144,"y":145,"style":124},"531","99","header level 0",[64,148],{"x":79,"y":149,"width":81,"height":82,"rx":66,"fill":83,"stroke":84,"style":85},"110",[71,151,153],{"x":115,"y":152,"style":94},"130","2",[64,155],{"x":88,"y":149,"width":89,"height":82,"rx":66,"fill":69,"stroke":84,"style":85},[64,157],{"x":98,"y":149,"width":99,"height":82,"rx":66,"fill":158,"stroke":159,"style":85},"#d9f4f1","var(--teal,#0f9488)",[71,161,163],{"x":102,"y":152,"style":162},"font-size:11.5px;font-weight:600;fill:var(--teal-ink,#0b6157);text-anchor:middle","Revenue | Units",[64,165],{"x":106,"y":149,"width":99,"height":82,"rx":66,"fill":158,"stroke":159,"style":85},[71,167,163],{"x":109,"y":152,"style":162},[64,169],{"x":138,"y":170,"width":99,"height":140,"rx":141,"fill":158,"stroke":159,"style":85},"112",[71,172,174],{"x":144,"y":173,"style":162},"129","header level 1",[64,176],{"x":79,"y":177,"width":81,"height":82,"rx":66,"fill":83,"stroke":84,"style":85},"140",[71,179,181],{"x":115,"y":180,"style":94},"160","3",[64,183],{"x":88,"y":177,"width":89,"height":82,"rx":66,"fill":69,"stroke":84,"style":85},[71,185,187],{"x":92,"y":180,"style":186},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","North",[64,189],{"x":98,"y":177,"width":99,"height":82,"rx":66,"fill":69,"stroke":84,"style":85},[71,191,192],{"x":102,"y":180,"style":186},"1200 | 30",[64,194],{"x":106,"y":177,"width":99,"height":82,"rx":66,"fill":69,"stroke":84,"style":85},[71,196,197],{"x":109,"y":180,"style":186},"1500 | 34",[64,199],{"x":138,"y":200,"width":99,"height":140,"rx":141,"fill":83,"stroke":84,"style":85},"142",[71,202,204],{"x":144,"y":203,"style":94},"159","data starts here",[64,206],{"x":79,"y":207,"width":81,"height":82,"rx":66,"fill":83,"stroke":84,"style":85},"170",[71,209,211],{"x":115,"y":210,"style":94},"190","4",[64,213],{"x":88,"y":207,"width":89,"height":82,"rx":66,"fill":69,"stroke":84,"style":85},[71,215,216],{"x":92,"y":210,"style":186},"South",[64,218],{"x":98,"y":207,"width":99,"height":82,"rx":66,"fill":69,"stroke":84,"style":85},[71,220,221],{"x":102,"y":210,"style":186},"900 | 22",[64,223],{"x":106,"y":207,"width":99,"height":82,"rx":66,"fill":69,"stroke":84,"style":85},[71,225,226],{"x":109,"y":210,"style":186},"1100 | 25",[71,228,231],{"x":73,"y":229,"style":230},"232","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","each column is really a (quarter, measure) pair",[233,234,236],"h2",{"id":235},"prerequisites","Prerequisites",[238,239,244],"pre",{"className":240,"code":241,"language":242,"meta":243,"style":243},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,245,246],{"__ignoreMap":243},[247,248,251,255,259,262],"span",{"class":249,"line":250},"line",1,[247,252,254],{"class":253},"sMTad","pip",[247,256,258],{"class":257},"srMev"," install",[247,260,261],{"class":257}," pandas",[247,263,264],{"class":257}," openpyxl\n",[10,266,267,268,271],{},"This builds ",[14,269,270],{},"quarterly.xlsx"," with merged quarter cells, matching the diagram:",[238,273,277],{"className":274,"code":275,"language":276,"meta":243,"style":243},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\n\nwb = Workbook()\nws = wb.active\nws.title = \"Quarterly\"\nws.append([\"Region\", \"Q1\", None, \"Q2\", None])\nws.append([None, \"Revenue\", \"Units\", \"Revenue\", \"Units\"])\nws.append([\"North\", 1200, 30, 1500, 34])\nws.append([\"South\", 900, 22, 1100, 25])\nws.merge_cells(\"B1:C1\")\nws.merge_cells(\"D1:E1\")\nws.merge_cells(\"A1:A2\")\nwb.save(\"quarterly.xlsx\")\n","python",[14,278,279,295,302,314,325,336,369,396,425,455,467,477,487],{"__ignoreMap":243},[247,280,281,285,289,292],{"class":249,"line":250},[247,282,284],{"class":283},"s-kum","from",[247,286,288],{"class":287},"skGVy"," openpyxl ",[247,290,291],{"class":283},"import",[247,293,294],{"class":287}," Workbook\n",[247,296,298],{"class":249,"line":297},2,[247,299,301],{"emptyLinePlaceholder":300},true,"\n",[247,303,305,308,311],{"class":249,"line":304},3,[247,306,307],{"class":287},"wb ",[247,309,310],{"class":283},"=",[247,312,313],{"class":287}," Workbook()\n",[247,315,317,320,322],{"class":249,"line":316},4,[247,318,319],{"class":287},"ws ",[247,321,310],{"class":283},[247,323,324],{"class":287}," wb.active\n",[247,326,328,331,333],{"class":249,"line":327},5,[247,329,330],{"class":287},"ws.title ",[247,332,310],{"class":283},[247,334,335],{"class":257}," \"Quarterly\"\n",[247,337,339,342,345,348,351,353,357,359,362,364,366],{"class":249,"line":338},6,[247,340,341],{"class":287},"ws.append([",[247,343,344],{"class":257},"\"Region\"",[247,346,347],{"class":287},", ",[247,349,350],{"class":257},"\"Q1\"",[247,352,347],{"class":287},[247,354,356],{"class":355},"sP0c6","None",[247,358,347],{"class":287},[247,360,361],{"class":257},"\"Q2\"",[247,363,347],{"class":287},[247,365,356],{"class":355},[247,367,368],{"class":287},"])\n",[247,370,372,374,376,378,381,383,386,388,390,392,394],{"class":249,"line":371},7,[247,373,341],{"class":287},[247,375,356],{"class":355},[247,377,347],{"class":287},[247,379,380],{"class":257},"\"Revenue\"",[247,382,347],{"class":287},[247,384,385],{"class":257},"\"Units\"",[247,387,347],{"class":287},[247,389,380],{"class":257},[247,391,347],{"class":287},[247,393,385],{"class":257},[247,395,368],{"class":287},[247,397,399,401,404,406,409,411,413,415,418,420,423],{"class":249,"line":398},8,[247,400,341],{"class":287},[247,402,403],{"class":257},"\"North\"",[247,405,347],{"class":287},[247,407,408],{"class":355},"1200",[247,410,347],{"class":287},[247,412,82],{"class":355},[247,414,347],{"class":287},[247,416,417],{"class":355},"1500",[247,419,347],{"class":287},[247,421,422],{"class":355},"34",[247,424,368],{"class":287},[247,426,428,430,433,435,438,440,443,445,448,450,453],{"class":249,"line":427},9,[247,429,341],{"class":287},[247,431,432],{"class":257},"\"South\"",[247,434,347],{"class":287},[247,436,437],{"class":355},"900",[247,439,347],{"class":287},[247,441,442],{"class":355},"22",[247,444,347],{"class":287},[247,446,447],{"class":355},"1100",[247,449,347],{"class":287},[247,451,452],{"class":355},"25",[247,454,368],{"class":287},[247,456,458,461,464],{"class":249,"line":457},10,[247,459,460],{"class":287},"ws.merge_cells(",[247,462,463],{"class":257},"\"B1:C1\"",[247,465,466],{"class":287},")\n",[247,468,470,472,475],{"class":249,"line":469},11,[247,471,460],{"class":287},[247,473,474],{"class":257},"\"D1:E1\"",[247,476,466],{"class":287},[247,478,480,482,485],{"class":249,"line":479},12,[247,481,460],{"class":287},[247,483,484],{"class":257},"\"A1:A2\"",[247,486,466],{"class":287},[247,488,490,493,496],{"class":249,"line":489},13,[247,491,492],{"class":287},"wb.save(",[247,494,495],{"class":257},"\"quarterly.xlsx\"",[247,497,466],{"class":287},[233,499,501],{"id":500},"step-1-pass-both-header-rows-to-read_excel","Step 1 — Pass both header rows to read_excel",[10,503,504,507,508,511,512,515,516,518],{},[14,505,506],{},"header"," accepts a list of row positions. Give it ",[14,509,510],{},"[0, 1]"," and pandas builds a two-level\n",[14,513,514],{},"MultiIndex"," for the columns, forward-filling the blank cells that a merge leaves behind so that both\n",[14,517,24],{}," columns know which quarter they belong to.",[238,520,522],{"className":274,"code":521,"language":276,"meta":243,"style":243},"import pandas as pd\n\ndf = pd.read_excel(\"quarterly.xlsx\", sheet_name=\"Quarterly\", header=[0, 1])\nprint(df.columns.tolist())\n# [('Region', 'Unnamed: 0_level_1'), ('Q1', 'Revenue'), ('Q1', 'Units'),\n#  ('Q2', 'Revenue'), ('Q2', 'Units')]\n",[14,523,524,537,541,581,589,595],{"__ignoreMap":243},[247,525,526,528,531,534],{"class":249,"line":250},[247,527,291],{"class":283},[247,529,530],{"class":287}," pandas ",[247,532,533],{"class":283},"as",[247,535,536],{"class":287}," pd\n",[247,538,539],{"class":249,"line":297},[247,540,301],{"emptyLinePlaceholder":300},[247,542,543,546,548,551,553,555,559,561,564,566,568,570,573,575,577,579],{"class":249,"line":304},[247,544,545],{"class":287},"df ",[247,547,310],{"class":283},[247,549,550],{"class":287}," pd.read_excel(",[247,552,495],{"class":257},[247,554,347],{"class":287},[247,556,558],{"class":557},"sa561","sheet_name",[247,560,310],{"class":283},[247,562,563],{"class":257},"\"Quarterly\"",[247,565,347],{"class":287},[247,567,506],{"class":557},[247,569,310],{"class":283},[247,571,572],{"class":287},"[",[247,574,66],{"class":355},[247,576,347],{"class":287},[247,578,117],{"class":355},[247,580,368],{"class":287},[247,582,583,586],{"class":249,"line":316},[247,584,585],{"class":355},"print",[247,587,588],{"class":287},"(df.columns.tolist())\n",[247,590,591],{"class":249,"line":327},[247,592,594],{"class":593},"s-wDw","# [('Region', 'Unnamed: 0_level_1'), ('Q1', 'Revenue'), ('Q1', 'Units'),\n",[247,596,597],{"class":249,"line":338},[247,598,599],{"class":593},"#  ('Q2', 'Revenue'), ('Q2', 'Units')]\n",[10,601,602,603,605,606,609],{},"The one awkward column is ",[14,604,125],{},". Its second-level cell is empty, and pandas cannot forward-fill\nacross levels, so it invents ",[14,607,608],{},"Unnamed: 0_level_1",". The fix is to make that column the index, which\ntakes it out of the column hierarchy entirely.",[238,611,613],{"className":274,"code":612,"language":276,"meta":243,"style":243},"df = pd.read_excel(\"quarterly.xlsx\", sheet_name=\"Quarterly\", header=[0, 1], index_col=0)\ndf = df.rename_axis(\"Region\")\nprint(df)\n#        Q1            Q2\n#   Revenue Units Revenue Units\n# Region\n# North    1200    30    1500    34\n# South     900    22    1100    25\n",[14,614,615,659,672,679,684,689,694,699],{"__ignoreMap":243},[247,616,617,619,621,623,625,627,629,631,633,635,637,639,641,643,645,647,650,653,655,657],{"class":249,"line":250},[247,618,545],{"class":287},[247,620,310],{"class":283},[247,622,550],{"class":287},[247,624,495],{"class":257},[247,626,347],{"class":287},[247,628,558],{"class":557},[247,630,310],{"class":283},[247,632,563],{"class":257},[247,634,347],{"class":287},[247,636,506],{"class":557},[247,638,310],{"class":283},[247,640,572],{"class":287},[247,642,66],{"class":355},[247,644,347],{"class":287},[247,646,117],{"class":355},[247,648,649],{"class":287},"], ",[247,651,652],{"class":557},"index_col",[247,654,310],{"class":283},[247,656,66],{"class":355},[247,658,466],{"class":287},[247,660,661,663,665,668,670],{"class":249,"line":297},[247,662,545],{"class":287},[247,664,310],{"class":283},[247,666,667],{"class":287}," df.rename_axis(",[247,669,344],{"class":257},[247,671,466],{"class":287},[247,673,674,676],{"class":249,"line":304},[247,675,585],{"class":355},[247,677,678],{"class":287},"(df)\n",[247,680,681],{"class":249,"line":316},[247,682,683],{"class":593},"#        Q1            Q2\n",[247,685,686],{"class":249,"line":327},[247,687,688],{"class":593},"#   Revenue Units Revenue Units\n",[247,690,691],{"class":249,"line":338},[247,692,693],{"class":593},"# Region\n",[247,695,696],{"class":249,"line":371},[247,697,698],{"class":593},"# North    1200    30    1500    34\n",[247,700,701],{"class":249,"line":398},[247,702,703],{"class":593},"# South     900    22    1100    25\n",[10,705,706],{},"This forward fill happens whether the quarter labels are merged cells or simply typed once with blanks\nbeside them — pandas treats both the same way — so you do not need to unmerge anything first.",[233,708,710],{"id":709},"step-2-select-from-the-multiindex","Step 2 — Select from the MultiIndex",[10,712,713,714,717,718,722],{},"A MultiIndex is more convenient than it first appears. Indexing with the top-level label returns that\nquarter's sub-table, and ",[14,715,716],{},"xs"," slices across the ",[719,720,721],"em",{},"second"," level to line up one measure for every quarter.",[238,724,726],{"className":274,"code":725,"language":276,"meta":243,"style":243},"q1 = df[\"Q1\"]                                  # Revenue and Units for Q1\nrevenue = df.xs(\"Revenue\", axis=1, level=1)    # Revenue for every quarter\nnorth_q2_units = df.loc[\"North\", (\"Q2\", \"Units\")]\n\nprint(q1)\nprint(revenue)\nprint(north_q2_units)                          # 34\n",[14,727,728,746,782,806,810,817,824],{"__ignoreMap":243},[247,729,730,733,735,738,740,743],{"class":249,"line":250},[247,731,732],{"class":287},"q1 ",[247,734,310],{"class":283},[247,736,737],{"class":287}," df[",[247,739,350],{"class":257},[247,741,742],{"class":287},"]                                  ",[247,744,745],{"class":593},"# Revenue and Units for Q1\n",[247,747,748,751,753,756,758,760,763,765,767,769,772,774,776,779],{"class":249,"line":297},[247,749,750],{"class":287},"revenue ",[247,752,310],{"class":283},[247,754,755],{"class":287}," df.xs(",[247,757,380],{"class":257},[247,759,347],{"class":287},[247,761,762],{"class":557},"axis",[247,764,310],{"class":283},[247,766,117],{"class":355},[247,768,347],{"class":287},[247,770,771],{"class":557},"level",[247,773,310],{"class":283},[247,775,117],{"class":355},[247,777,778],{"class":287},")    ",[247,780,781],{"class":593},"# Revenue for every quarter\n",[247,783,784,787,789,792,794,797,799,801,803],{"class":249,"line":304},[247,785,786],{"class":287},"north_q2_units ",[247,788,310],{"class":283},[247,790,791],{"class":287}," df.loc[",[247,793,403],{"class":257},[247,795,796],{"class":287},", (",[247,798,361],{"class":257},[247,800,347],{"class":287},[247,802,385],{"class":257},[247,804,805],{"class":287},")]\n",[247,807,808],{"class":249,"line":316},[247,809,301],{"emptyLinePlaceholder":300},[247,811,812,814],{"class":249,"line":327},[247,813,585],{"class":355},[247,815,816],{"class":287},"(q1)\n",[247,818,819,821],{"class":249,"line":338},[247,820,585],{"class":355},[247,822,823],{"class":287},"(revenue)\n",[247,825,826,828,831],{"class":249,"line":371},[247,827,585],{"class":355},[247,829,830],{"class":287},"(north_q2_units)                          ",[247,832,833],{"class":593},"# 34\n",[10,835,836,839,840,843],{},[14,837,838],{},"revenue"," is often the frame you actually want for a report: one row per region, one column per\nquarter, with the quarter labels promoted to plain column names. Growth between quarters is then a\nsingle ",[14,841,842],{},"pct_change(axis=1)"," away.",[45,845,54,851,54,854,54,857,54,861,54,867,54,873,54,876,54,881,54,885,54,889,54,892,54,897,54,901,54,904,54,907,54,910,54,915,54,920,54,924,54,927,54,930,54,933],{"viewBox":846,"role":48,"ariaLabelledBy":847,"xmlns":52,"style":850},"0 0 760 191",[848,849],"mrh-select-t","mrh-select-d","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[56,852,853],{"id":848},"Selecting along each level of a two-level column index",[60,855,856],{"id":849},"Indexing by the top level returns one quarter with both measures; xs on level 1 returns one measure across all quarters; a tuple returns one exact column.",[64,858],{"x":66,"y":66,"width":859,"height":860,"fill":69},"760","191",[64,862],{"x":79,"y":79,"width":863,"height":864,"rx":865,"fill":120,"stroke":121,"style":866},"226.7","127","14","stroke-width:2px",[71,868,872],{"x":869,"y":870,"style":871},"133.3","46","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","df[\"Q1\"]",[249,874],{"x1":81,"y1":88,"x2":875,"y2":88,"stroke":121,"style":85},"230.7",[71,877,880],{"x":869,"y":878,"style":879},"78","font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Q1 → Revenue, Units",[71,882,884],{"x":869,"y":883,"style":879},"101","one quarter",[71,886,888],{"x":869,"y":887,"style":879},"124","both measures",[64,890],{"x":891,"y":79,"width":863,"height":864,"rx":865,"fill":158,"stroke":159,"style":866},"266.7",[71,893,896],{"x":894,"y":870,"style":895},"380","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","df.xs(\"Revenue\", level=1)",[249,898],{"x1":899,"y1":88,"x2":900,"y2":88,"stroke":159,"style":85},"282.7","477.3",[71,902,903],{"x":894,"y":878,"style":879},"Revenue → Q1, Q2",[71,905,906],{"x":894,"y":883,"style":879},"one measure",[71,908,909],{"x":894,"y":887,"style":879},"every quarter",[64,911],{"x":912,"y":79,"width":863,"height":864,"rx":865,"fill":913,"stroke":914,"style":866},"513.3","#fdefd8","var(--gold,#b4740a)",[71,916,919],{"x":917,"y":870,"style":918},"626.7","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","df[(\"Q2\", \"Units\")]",[249,921],{"x1":922,"y1":88,"x2":923,"y2":88,"stroke":914,"style":85},"529.3","724",[71,925,926],{"x":917,"y":878,"style":879},"a single Series",[71,928,929],{"x":917,"y":883,"style":879},"exact pair",[71,931,932],{"x":917,"y":887,"style":879},"no ambiguity",[71,934,936],{"x":894,"y":935,"style":230},"177","level 0 is the quarter, level 1 is the measure",[233,938,940],{"id":939},"step-3-flatten-the-headers-for-export","Step 3 — Flatten the headers for export",[10,942,943,944,947],{},"Most destinations cannot take a MultiIndex. A database table, a CSV consumer or a plain ",[14,945,946],{},"to_excel","\nwithout merged headers all want one string per column. Join the levels with a space — or an\nunderscore if the names will become SQL identifiers.",[238,949,951],{"className":274,"code":950,"language":276,"meta":243,"style":243},"flat = df.copy()\nflat.columns = [f\"{quarter} {measure}\" for quarter, measure in flat.columns]\nflat = flat.reset_index()\nprint(flat.columns.tolist())\n# ['Region', 'Q1 Revenue', 'Q1 Units', 'Q2 Revenue', 'Q2 Units']\nflat.to_csv(\"quarterly_flat.csv\", index=False)\n",[14,952,953,963,1011,1020,1027,1032],{"__ignoreMap":243},[247,954,955,958,960],{"class":249,"line":250},[247,956,957],{"class":287},"flat ",[247,959,310],{"class":283},[247,961,962],{"class":287}," df.copy()\n",[247,964,965,968,970,973,976,979,983,986,989,992,995,997,999,1002,1005,1008],{"class":249,"line":297},[247,966,967],{"class":287},"flat.columns ",[247,969,310],{"class":283},[247,971,972],{"class":287}," [",[247,974,975],{"class":283},"f",[247,977,978],{"class":257},"\"",[247,980,982],{"class":981},"sSjpA","{",[247,984,985],{"class":287},"quarter",[247,987,988],{"class":981},"}",[247,990,991],{"class":981}," {",[247,993,994],{"class":287},"measure",[247,996,988],{"class":981},[247,998,978],{"class":257},[247,1000,1001],{"class":283}," for",[247,1003,1004],{"class":287}," quarter, measure ",[247,1006,1007],{"class":283},"in",[247,1009,1010],{"class":287}," flat.columns]\n",[247,1012,1013,1015,1017],{"class":249,"line":304},[247,1014,957],{"class":287},[247,1016,310],{"class":283},[247,1018,1019],{"class":287}," flat.reset_index()\n",[247,1021,1022,1024],{"class":249,"line":316},[247,1023,585],{"class":355},[247,1025,1026],{"class":287},"(flat.columns.tolist())\n",[247,1028,1029],{"class":249,"line":327},[247,1030,1031],{"class":593},"# ['Region', 'Q1 Revenue', 'Q1 Units', 'Q2 Revenue', 'Q2 Units']\n",[247,1033,1034,1037,1040,1042,1045,1047,1050],{"class":249,"line":338},[247,1035,1036],{"class":287},"flat.to_csv(",[247,1038,1039],{"class":257},"\"quarterly_flat.csv\"",[247,1041,347],{"class":287},[247,1043,1044],{"class":557},"index",[247,1046,310],{"class":283},[247,1048,1049],{"class":355},"False",[247,1051,466],{"class":287},[10,1053,1054,1055,1058],{},"If some columns have only one meaningful level, filter the empty parts out when you join so you do\nnot end up with trailing spaces: ",[14,1056,1057],{},"\" \".join(p for p in col if p and not str(p).startswith(\"Unnamed\"))",".",[233,1060,1062],{"id":1061},"step-4-reshape-into-long-format","Step 4 — Reshape into long format",[10,1064,1065,1066,1069],{},"The quarter is really a ",[719,1067,1068],{},"value",", not part of a column name. Stacking the top level moves it into its\nown column, which is the shape that grouping, charting and database loads all prefer — and it keeps\nworking when next quarter's pair of columns is added.",[238,1071,1073],{"className":274,"code":1072,"language":276,"meta":243,"style":243},"long = (\n    df.stack(level=0, future_stack=True)\n      .rename_axis([\"Region\", \"Quarter\"])\n      .reset_index()\n)\nprint(long)\n#   Region Quarter  Revenue  Units\n# 0  North      Q1     1200     30\n# 1  North      Q2     1500     34\n# 2  South      Q1      900     22\n# 3  South      Q2     1100     25\n\nprint(long.groupby(\"Quarter\")[[\"Revenue\", \"Units\"]].sum())\n",[14,1074,1075,1086,1109,1123,1128,1132,1143,1148,1153,1158,1163,1168,1172],{"__ignoreMap":243},[247,1076,1077,1080,1083],{"class":249,"line":250},[247,1078,1079],{"class":557},"long",[247,1081,1082],{"class":283}," =",[247,1084,1085],{"class":287}," (\n",[247,1087,1088,1091,1093,1095,1097,1099,1102,1104,1107],{"class":249,"line":297},[247,1089,1090],{"class":287},"    df.stack(",[247,1092,771],{"class":557},[247,1094,310],{"class":283},[247,1096,66],{"class":355},[247,1098,347],{"class":287},[247,1100,1101],{"class":557},"future_stack",[247,1103,310],{"class":283},[247,1105,1106],{"class":355},"True",[247,1108,466],{"class":287},[247,1110,1111,1114,1116,1118,1121],{"class":249,"line":304},[247,1112,1113],{"class":287},"      .rename_axis([",[247,1115,344],{"class":257},[247,1117,347],{"class":287},[247,1119,1120],{"class":257},"\"Quarter\"",[247,1122,368],{"class":287},[247,1124,1125],{"class":249,"line":316},[247,1126,1127],{"class":287},"      .reset_index()\n",[247,1129,1130],{"class":249,"line":327},[247,1131,466],{"class":287},[247,1133,1134,1136,1139,1141],{"class":249,"line":338},[247,1135,585],{"class":355},[247,1137,1138],{"class":287},"(",[247,1140,1079],{"class":557},[247,1142,466],{"class":287},[247,1144,1145],{"class":249,"line":371},[247,1146,1147],{"class":593},"#   Region Quarter  Revenue  Units\n",[247,1149,1150],{"class":249,"line":398},[247,1151,1152],{"class":593},"# 0  North      Q1     1200     30\n",[247,1154,1155],{"class":249,"line":427},[247,1156,1157],{"class":593},"# 1  North      Q2     1500     34\n",[247,1159,1160],{"class":249,"line":457},[247,1161,1162],{"class":593},"# 2  South      Q1      900     22\n",[247,1164,1165],{"class":249,"line":469},[247,1166,1167],{"class":593},"# 3  South      Q2     1100     25\n",[247,1169,1170],{"class":249,"line":479},[247,1171,301],{"emptyLinePlaceholder":300},[247,1173,1174,1176,1178,1180,1183,1185,1188,1190,1192,1194],{"class":249,"line":489},[247,1175,585],{"class":355},[247,1177,1138],{"class":287},[247,1179,1079],{"class":557},[247,1181,1182],{"class":287},".groupby(",[247,1184,1120],{"class":257},[247,1186,1187],{"class":287},")[[",[247,1189,380],{"class":257},[247,1191,347],{"class":287},[247,1193,385],{"class":257},[247,1195,1196],{"class":287},"]].sum())\n",[10,1198,1199,1202,1203,1207,1208,1058],{},[14,1200,1201],{},"future_stack=True"," opts into the newer stacking implementation, which keeps missing values instead of\nsilently dropping rows. That is the behaviour you want for report data, where an empty cell usually\nmeans zero activity rather than a row to discard. The reverse operation — long back to a wide\npresentation table — is covered in ",[39,1204,1206],{"href":1205},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Funpivot-a-wide-excel-sheet-with-pandas-melt\u002F","Unpivot a Wide Excel Sheet with pandas melt","\nalongside ",[14,1209,1210],{},"pivot_table",[45,1212,54,1217,54,1220,54,1223,54,1226,54,1230,54,1235,54,1240,54,1244,54,1249,54,1254,54,1257,54,1261,54,1264,54,1267,54,1271,54,1274,54,1277,54,1281,54,1284,54,1287,54,1291,54,1294,54,1297,54,1302,54,1305,54,1308],{"viewBox":1213,"role":48,"ariaLabelledBy":1214,"xmlns":52,"style":850},"0 0 760 156",[1215,1216],"mrh-reshape-t","mrh-reshape-d",[56,1218,1219],{"id":1215},"From a two-row header to a tidy long table",[60,1221,1222],{"id":1216},"Read with header equal to zero and one, move Region into the index, stack the quarter level into its own column, and reset the index to get one row per region and quarter.",[64,1224],{"x":66,"y":66,"width":859,"height":1225,"fill":69},"156",[64,1227],{"x":79,"y":79,"width":99,"height":1228,"rx":1229,"fill":83,"stroke":84,"style":866},"86","12",[71,1231,1234],{"x":1232,"y":80,"style":1233},"95","font-size:14px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","read",[71,1236,1239],{"x":1232,"y":1237,"style":1238},"72","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","header=[0, 1]",[71,1241,1243],{"x":1232,"y":1242,"style":1238},"92","index_col=0",[249,1245],{"x1":1246,"y1":1247,"x2":1248,"y2":1247,"stroke":121,"style":866},"175","63","204",[1250,1251],"polygon",{"points":1252,"fill":1253},"207,63 198,58 198,68","#5b5cf0",[64,1255],{"x":1256,"y":79,"width":99,"height":1228,"rx":1229,"fill":120,"stroke":121,"style":866},"210",[71,1258,514],{"x":1259,"y":80,"style":1260},"285","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[71,1262,1263],{"x":1259,"y":1237,"style":186},"(quarter, measure)",[71,1265,1266],{"x":1259,"y":1242,"style":186},"column pairs",[249,1268],{"x1":1269,"y1":1247,"x2":1270,"y2":1247,"stroke":121,"style":866},"365","394",[1250,1272],{"points":1273,"fill":1253},"397,63 388,58 388,68",[64,1275],{"x":1276,"y":79,"width":99,"height":1228,"rx":1229,"fill":120,"stroke":121,"style":866},"400",[71,1278,1280],{"x":1279,"y":80,"style":1260},"475","stack level 0",[71,1282,1283],{"x":1279,"y":1237,"style":186},"quarter becomes",[71,1285,1286],{"x":1279,"y":1242,"style":186},"a column",[249,1288],{"x1":1289,"y1":1247,"x2":1290,"y2":1247,"stroke":121,"style":866},"555","584",[1250,1292],{"points":1293,"fill":1253},"587,63 578,58 578,68",[64,1295],{"x":1296,"y":79,"width":99,"height":1228,"rx":1229,"fill":158,"stroke":159,"style":866},"590",[71,1298,1301],{"x":1299,"y":80,"style":1300},"665","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","tidy table",[71,1303,1304],{"x":1299,"y":1237,"style":186},"Region, Quarter,",[71,1306,1307],{"x":1299,"y":1242,"style":186},"Revenue, Units",[71,1309,1310],{"x":894,"y":177,"style":230},"new quarters add rows, not columns",[233,1312,1314],{"id":1313},"step-5-build-the-header-by-hand-when-the-layout-is-irregular","Step 5 — Build the header by hand when the layout is irregular",[10,1316,1317,1319],{},[14,1318,1239],{}," needs the two header rows to be adjacent and to start at a known position. When a\ntitle block sits above them, or when the top row has notes in it, read the sheet raw and assemble the\nMultiIndex yourself. This also gives you a place to clean labels before they become column names.",[238,1321,1323],{"className":274,"code":1322,"language":276,"meta":243,"style":243},"raw = pd.read_excel(\"quarterly.xlsx\", sheet_name=\"Quarterly\", header=None)\n\ntop = raw.iloc[0].ffill().astype(str).str.strip()\nsub = raw.iloc[1].fillna(\"\").astype(str).str.strip()\n\ncolumns = pd.MultiIndex.from_arrays([top, sub])\nmanual = (\n    raw.iloc[2:]\n       .set_axis(columns, axis=1)\n       .set_index((\"Region\", \"\"))\n       .rename_axis(\"Region\")\n       .rename_axis([None, None], axis=1)\n)\nmanual = manual.apply(pd.to_numeric)\nprint(manual.equals(df))\n",[14,1324,1325,1354,1358,1379,1403,1407,1417,1426,1436,1449,1463,1472,1493,1497,1507],{"__ignoreMap":243},[247,1326,1327,1330,1332,1334,1336,1338,1340,1342,1344,1346,1348,1350,1352],{"class":249,"line":250},[247,1328,1329],{"class":287},"raw ",[247,1331,310],{"class":283},[247,1333,550],{"class":287},[247,1335,495],{"class":257},[247,1337,347],{"class":287},[247,1339,558],{"class":557},[247,1341,310],{"class":283},[247,1343,563],{"class":257},[247,1345,347],{"class":287},[247,1347,506],{"class":557},[247,1349,310],{"class":283},[247,1351,356],{"class":355},[247,1353,466],{"class":287},[247,1355,1356],{"class":249,"line":297},[247,1357,301],{"emptyLinePlaceholder":300},[247,1359,1360,1363,1365,1368,1370,1373,1376],{"class":249,"line":304},[247,1361,1362],{"class":287},"top ",[247,1364,310],{"class":283},[247,1366,1367],{"class":287}," raw.iloc[",[247,1369,66],{"class":355},[247,1371,1372],{"class":287},"].ffill().astype(",[247,1374,1375],{"class":355},"str",[247,1377,1378],{"class":287},").str.strip()\n",[247,1380,1381,1384,1386,1388,1390,1393,1396,1399,1401],{"class":249,"line":316},[247,1382,1383],{"class":287},"sub ",[247,1385,310],{"class":283},[247,1387,1367],{"class":287},[247,1389,117],{"class":355},[247,1391,1392],{"class":287},"].fillna(",[247,1394,1395],{"class":257},"\"\"",[247,1397,1398],{"class":287},").astype(",[247,1400,1375],{"class":355},[247,1402,1378],{"class":287},[247,1404,1405],{"class":249,"line":327},[247,1406,301],{"emptyLinePlaceholder":300},[247,1408,1409,1412,1414],{"class":249,"line":338},[247,1410,1411],{"class":287},"columns ",[247,1413,310],{"class":283},[247,1415,1416],{"class":287}," pd.MultiIndex.from_arrays([top, sub])\n",[247,1418,1419,1422,1424],{"class":249,"line":371},[247,1420,1421],{"class":287},"manual ",[247,1423,310],{"class":283},[247,1425,1085],{"class":287},[247,1427,1428,1431,1433],{"class":249,"line":398},[247,1429,1430],{"class":287},"    raw.iloc[",[247,1432,153],{"class":355},[247,1434,1435],{"class":287},":]\n",[247,1437,1438,1441,1443,1445,1447],{"class":249,"line":427},[247,1439,1440],{"class":287},"       .set_axis(columns, ",[247,1442,762],{"class":557},[247,1444,310],{"class":283},[247,1446,117],{"class":355},[247,1448,466],{"class":287},[247,1450,1451,1454,1456,1458,1460],{"class":249,"line":457},[247,1452,1453],{"class":287},"       .set_index((",[247,1455,344],{"class":257},[247,1457,347],{"class":287},[247,1459,1395],{"class":257},[247,1461,1462],{"class":287},"))\n",[247,1464,1465,1468,1470],{"class":249,"line":469},[247,1466,1467],{"class":287},"       .rename_axis(",[247,1469,344],{"class":257},[247,1471,466],{"class":287},[247,1473,1474,1477,1479,1481,1483,1485,1487,1489,1491],{"class":249,"line":479},[247,1475,1476],{"class":287},"       .rename_axis([",[247,1478,356],{"class":355},[247,1480,347],{"class":287},[247,1482,356],{"class":355},[247,1484,649],{"class":287},[247,1486,762],{"class":557},[247,1488,310],{"class":283},[247,1490,117],{"class":355},[247,1492,466],{"class":287},[247,1494,1495],{"class":249,"line":489},[247,1496,466],{"class":287},[247,1498,1500,1502,1504],{"class":249,"line":1499},14,[247,1501,1421],{"class":287},[247,1503,310],{"class":283},[247,1505,1506],{"class":287}," manual.apply(pd.to_numeric)\n",[247,1508,1510,1512],{"class":249,"line":1509},15,[247,1511,585],{"class":355},[247,1513,1514],{"class":287},"(manual.equals(df))\n",[10,1516,1517,1518,21,1520,1522,1523,1527,1528,1531,1532,1535,1536,1539],{},"The row positions ",[14,1519,66],{},[14,1521,117],{}," are the only assumptions left, and they can come from a search rather\nthan a constant — the approach in ",[39,1524,1526],{"href":1525},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout\u002F","Find the Header Row in an Excel Sheet with a Shifting Layout",".\nBecause the frame was read with ",[14,1529,1530],{},"header=None",", the numeric columns start out as ",[14,1533,1534],{},"object",", which is why\nthe final ",[14,1537,1538],{},"apply(pd.to_numeric)"," is needed.",[233,1541,1543],{"id":1542},"step-6-validate-the-header-before-trusting-it","Step 6 — Validate the header before trusting it",[10,1545,1546,1547,1550,1551,1554,1555,1558],{},"A two-row header is a contract between the person maintaining the workbook and your script, and it is\nthe kind of contract that gets broken quietly. Someone inserts a ",[14,1548,1549],{},"Margin"," measure under each quarter, or\ntypes ",[14,1552,1553],{},"Q3 "," with a trailing space, and every downstream selection either fails with a ",[14,1556,1557],{},"KeyError"," or —\nworse — succeeds on the wrong columns. A short check straight after the read turns that into a clear\nmessage at the point where the file came in.",[238,1560,1562],{"className":274,"code":1561,"language":276,"meta":243,"style":243},"EXPECTED_MEASURES = {\"Revenue\", \"Units\"}\n\ndef check_header(frame: pd.DataFrame) -> None:\n    quarters = frame.columns.get_level_values(0).unique()\n    problems = []\n    for quarter in quarters:\n        if not str(quarter).startswith(\"Q\") or str(quarter) != str(quarter).strip():\n            problems.append(f\"unexpected quarter label {quarter!r}\")\n        measures = set(frame[quarter].columns)\n        if measures != EXPECTED_MEASURES:\n            problems.append(f\"{quarter}: measures {sorted(measures)} != {sorted(EXPECTED_MEASURES)}\")\n    if problems:\n        raise ValueError(\"header check failed:\\n  \" + \"\\n  \".join(problems))\n\ncheck_header(df)\nprint(\"header OK:\", list(df.columns.get_level_values(0).unique()))\n",[14,1563,1564,1582,1586,1603,1618,1628,1641,1677,1700,1713,1727,1774,1782,1814,1818,1823],{"__ignoreMap":243},[247,1565,1566,1569,1571,1573,1575,1577,1579],{"class":249,"line":250},[247,1567,1568],{"class":355},"EXPECTED_MEASURES",[247,1570,1082],{"class":283},[247,1572,991],{"class":287},[247,1574,380],{"class":257},[247,1576,347],{"class":287},[247,1578,385],{"class":257},[247,1580,1581],{"class":287},"}\n",[247,1583,1584],{"class":249,"line":297},[247,1585,301],{"emptyLinePlaceholder":300},[247,1587,1588,1591,1595,1598,1600],{"class":249,"line":304},[247,1589,1590],{"class":283},"def",[247,1592,1594],{"class":1593},"s_Opv"," check_header",[247,1596,1597],{"class":287},"(frame: pd.DataFrame) -> ",[247,1599,356],{"class":355},[247,1601,1602],{"class":287},":\n",[247,1604,1605,1608,1610,1613,1615],{"class":249,"line":316},[247,1606,1607],{"class":287},"    quarters ",[247,1609,310],{"class":283},[247,1611,1612],{"class":287}," frame.columns.get_level_values(",[247,1614,66],{"class":355},[247,1616,1617],{"class":287},").unique()\n",[247,1619,1620,1623,1625],{"class":249,"line":327},[247,1621,1622],{"class":287},"    problems ",[247,1624,310],{"class":283},[247,1626,1627],{"class":287}," []\n",[247,1629,1630,1633,1636,1638],{"class":249,"line":338},[247,1631,1632],{"class":283},"    for",[247,1634,1635],{"class":287}," quarter ",[247,1637,1007],{"class":283},[247,1639,1640],{"class":287}," quarters:\n",[247,1642,1643,1646,1649,1652,1655,1658,1661,1664,1666,1669,1672,1674],{"class":249,"line":371},[247,1644,1645],{"class":283},"        if",[247,1647,1648],{"class":283}," not",[247,1650,1651],{"class":355}," str",[247,1653,1654],{"class":287},"(quarter).startswith(",[247,1656,1657],{"class":257},"\"Q\"",[247,1659,1660],{"class":287},") ",[247,1662,1663],{"class":283},"or",[247,1665,1651],{"class":355},[247,1667,1668],{"class":287},"(quarter) ",[247,1670,1671],{"class":283},"!=",[247,1673,1651],{"class":355},[247,1675,1676],{"class":287},"(quarter).strip():\n",[247,1678,1679,1682,1684,1687,1689,1691,1694,1696,1698],{"class":249,"line":398},[247,1680,1681],{"class":287},"            problems.append(",[247,1683,975],{"class":283},[247,1685,1686],{"class":257},"\"unexpected quarter label ",[247,1688,982],{"class":981},[247,1690,985],{"class":287},[247,1692,1693],{"class":283},"!r",[247,1695,988],{"class":981},[247,1697,978],{"class":257},[247,1699,466],{"class":287},[247,1701,1702,1705,1707,1710],{"class":249,"line":427},[247,1703,1704],{"class":287},"        measures ",[247,1706,310],{"class":283},[247,1708,1709],{"class":355}," set",[247,1711,1712],{"class":287},"(frame[quarter].columns)\n",[247,1714,1715,1717,1720,1722,1725],{"class":249,"line":457},[247,1716,1645],{"class":283},[247,1718,1719],{"class":287}," measures ",[247,1721,1671],{"class":283},[247,1723,1724],{"class":355}," EXPECTED_MEASURES",[247,1726,1602],{"class":287},[247,1728,1729,1731,1733,1735,1737,1739,1741,1744,1746,1749,1752,1754,1757,1759,1761,1763,1765,1768,1770,1772],{"class":249,"line":469},[247,1730,1681],{"class":287},[247,1732,975],{"class":283},[247,1734,978],{"class":257},[247,1736,982],{"class":981},[247,1738,985],{"class":287},[247,1740,988],{"class":981},[247,1742,1743],{"class":257},": measures ",[247,1745,982],{"class":981},[247,1747,1748],{"class":355},"sorted",[247,1750,1751],{"class":287},"(measures)",[247,1753,988],{"class":981},[247,1755,1756],{"class":257}," != ",[247,1758,982],{"class":981},[247,1760,1748],{"class":355},[247,1762,1138],{"class":287},[247,1764,1568],{"class":355},[247,1766,1767],{"class":287},")",[247,1769,988],{"class":981},[247,1771,978],{"class":257},[247,1773,466],{"class":287},[247,1775,1776,1779],{"class":249,"line":479},[247,1777,1778],{"class":283},"    if",[247,1780,1781],{"class":287}," problems:\n",[247,1783,1784,1787,1790,1792,1795,1798,1801,1804,1807,1809,1811],{"class":249,"line":489},[247,1785,1786],{"class":283},"        raise",[247,1788,1789],{"class":355}," ValueError",[247,1791,1138],{"class":287},[247,1793,1794],{"class":257},"\"header check failed:",[247,1796,1797],{"class":981},"\\n",[247,1799,1800],{"class":257},"  \"",[247,1802,1803],{"class":283}," +",[247,1805,1806],{"class":257}," \"",[247,1808,1797],{"class":981},[247,1810,1800],{"class":257},[247,1812,1813],{"class":287},".join(problems))\n",[247,1815,1816],{"class":249,"line":1499},[247,1817,301],{"emptyLinePlaceholder":300},[247,1819,1820],{"class":249,"line":1509},[247,1821,1822],{"class":287},"check_header(df)\n",[247,1824,1826,1828,1830,1833,1835,1838,1841,1843],{"class":249,"line":1825},16,[247,1827,585],{"class":355},[247,1829,1138],{"class":287},[247,1831,1832],{"class":257},"\"header OK:\"",[247,1834,347],{"class":287},[247,1836,1837],{"class":355},"list",[247,1839,1840],{"class":287},"(df.columns.get_level_values(",[247,1842,66],{"class":355},[247,1844,1845],{"class":287},").unique()))\n",[10,1847,1848,1849,1852,1853,1058],{},"Checking the ",[719,1850,1851],{},"set"," of measures under each quarter, rather than the total column count, gives an error\nthat names the exact group that changed. It also lets a new quarter through without complaint, which is\nthe one change to this layout that is expected every three months. For wider schema checks — types,\nranges, required columns — the same idea scales up into the approach in\n",[39,1854,1856],{"href":1855},"\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",[233,1858,1860],{"id":1859},"common-pitfalls","Common pitfalls",[1862,1863,1864,1880],"table",{},[1865,1866,1867],"thead",{},[1868,1869,1870,1874,1877],"tr",{},[1871,1872,1873],"th",{},"Symptom",[1871,1875,1876],{},"Cause",[1871,1878,1879],{},"Fix",[1881,1882,1883,1900,1917,1942,1953,1968],"tbody",{},[1868,1884,1885,1891,1894],{},[1886,1887,1888,1890],"td",{},[14,1889,608],{}," in the columns",[1886,1892,1893],{},"A column with only a top-level label",[1886,1895,1896,1897,1899],{},"Use ",[14,1898,1243],{},", or rename that tuple after reading",[1868,1901,1902,1905,1912],{},[1886,1903,1904],{},"Sub-header values appear as the first data row",[1886,1906,1907,1908,1911],{},"Only ",[14,1909,1910],{},"header=0"," was passed",[1886,1913,1914,1915],{},"Pass ",[14,1916,1239],{},[1868,1918,1919,1925,1935],{},[1886,1920,1921,1924],{},[14,1922,1923],{},"ValueError"," about header rows",[1886,1926,1927,1930,1931,1934],{},[14,1928,1929],{},"usecols"," or ",[14,1932,1933],{},"skiprows"," changed the row positions",[1886,1936,1937,1938,1941],{},"Count header positions ",[719,1939,1940],{},"after"," skipping rows",[1868,1943,1944,1947,1950],{},[1886,1945,1946],{},"Quarter missing from some columns",[1886,1948,1949],{},"A top-level label typed only in a later column",[1886,1951,1952],{},"Forward fill needs the label on the leftmost cell of each group",[1868,1954,1955,1961,1964],{},[1886,1956,1957,1958],{},"Rows vanish after ",[14,1959,1960],{},"stack",[1886,1962,1963],{},"The legacy stack drops all-NaN rows",[1886,1965,1914,1966],{},[14,1967,1201],{},[1868,1969,1970,1973,1976],{},[1886,1971,1972],{},"Column names end in a space",[1886,1974,1975],{},"Flattening joined an empty level",[1886,1977,1978],{},"Filter empty parts before joining",[10,1980,1981,1982,1984,1985,1988,1989,21,1992,1995,1996,1058],{},"The headers in hand-built workbooks also carry invisible passengers — trailing spaces, non-breaking\nspaces, line breaks typed with Alt+Enter. A header that prints as ",[14,1983,20],{}," but fails ",[14,1986,1987],{},"df[\"Q1\"][\"Revenue\"]","\nalmost always has one; normalise labels with ",[14,1990,1991],{},".str.strip()",[14,1993,1994],{},".str.replace(r\"\\s+\", \" \", regex=True)",",\nas in ",[39,1997,1999],{"href":1998},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fstrip-whitespace-and-normalise-text-columns-with-pandas\u002F","Strip Whitespace and Normalise Text Columns with pandas",[233,2001,2003],{"id":2002},"performance-and-scale-notes","Performance and scale notes",[10,2005,2006,2007,2009],{},"A multi-row header costs nothing extra to parse; it is the same cells read once. The difference shows\nup afterwards. MultiIndex selection is fast, but a wide sheet with dozens of quarters and many measures\nis far easier to work with after one ",[14,2008,1960],{},", because every later operation runs on a few named columns\ninstead of a few hundred tuples.",[45,2011,54,2017,54,2020,54,2023,54,2027,54,2031,54,2035,54,2041,54,2045,54,2054,54,2059,54,2062,54,2066,54,2069,54,2071,54,2074,54,2077,54,2081,54,2084,54,2087,54,2091],{"viewBox":2012,"role":48,"ariaLabelledBy":2013,"xmlns":52,"style":2016},"0 0 720 212",[2014,2015],"mrh-width-t","mrh-width-d","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[56,2018,2019],{"id":2014},"Column count as quarters are added, wide versus long",[60,2021,2022],{"id":2015},"With two measures, the wide layout gains two columns every quarter and reaches forty-one columns after five years, while the long layout stays at four columns and grows by rows instead.",[64,2024],{"x":66,"y":66,"width":2025,"height":2026,"fill":69},"720","212",[71,2028,2030],{"x":79,"y":82,"style":2029},"font-size:12px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","columns after five years of quarterly data",[64,2032],{"x":79,"y":870,"width":207,"height":2033,"rx":2034,"fill":69,"stroke":84,"style":85},"28","6",[71,2036,2040],{"x":2037,"y":2038,"style":2039},"105","65","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:middle","wide (MultiIndex)",[64,2042],{"x":1248,"y":2043,"width":2044,"height":140,"rx":2034,"fill":83,"stroke":84,"style":85},"47","330",[64,2046],{"x":2047,"y":2048,"width":2049,"height":2050,"rx":2051,"fill":2052,"stroke":2053,"style":85},"205","48","311.5","24","5","#fee8f2","var(--accent,#f43f8f)",[71,2055,2058],{"x":2056,"y":2038,"style":2057},"546","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","41 columns",[64,2060],{"x":79,"y":2061,"width":207,"height":2033,"rx":2034,"fill":69,"stroke":84,"style":85},"90",[71,2063,2065],{"x":2037,"y":2064,"style":2039},"109","flattened names",[64,2067],{"x":1248,"y":2068,"width":2044,"height":140,"rx":2034,"fill":83,"stroke":84,"style":85},"91",[64,2070],{"x":2047,"y":1242,"width":2049,"height":2050,"rx":2051,"fill":913,"stroke":914,"style":85},[71,2072,2058],{"x":2056,"y":2064,"style":2073},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start",[64,2075],{"x":79,"y":2076,"width":207,"height":2033,"rx":2034,"fill":69,"stroke":84,"style":85},"134",[71,2078,2080],{"x":2037,"y":2079,"style":2039},"153","long (stacked)",[64,2082],{"x":1248,"y":2083,"width":2044,"height":140,"rx":2034,"fill":83,"stroke":84,"style":85},"135",[64,2085],{"x":2047,"y":98,"width":2086,"height":2050,"rx":2051,"fill":158,"stroke":159,"style":85},"31",[71,2088,2090],{"x":2056,"y":2079,"style":2089},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","4 columns",[71,2092,2095],{"x":2093,"y":2094,"style":230},"360","194","stacking turns an ever-widening sheet into a fixed schema",[10,2097,2098,2099,2101],{},"If the workbook is large, restrict the read with ",[14,2100,1929],{}," so that pandas only parses the regions and\nquarters you need. The header rows are included in the slice automatically as long as the column range\ncovers them.",[233,2103,2105],{"id":2104},"conclusion","Conclusion",[10,2107,2108,2109,2111,2112,2114,2115,2118],{},"Tell ",[14,2110,32],{}," how many header rows there are with ",[14,2113,1239],{},", move the label column into the\nindex so it does not end up ",[14,2116,2117],{},"Unnamed",", and let pandas forward-fill the merged quarter labels for you.\nFrom there, choose the shape by destination: keep the MultiIndex for interactive selection, flatten the\nnames for CSV or SQL, and stack the top level whenever the data is heading into grouping, charts or a\ndatabase. When the header rows move, read the sheet raw and build the MultiIndex yourself.",[233,2120,2122],{"id":2121},"frequently-asked-questions","Frequently asked questions",[10,2124,2125,2129,2130,2133],{},[2126,2127,2128],"strong",{},"Can pandas read three header rows?","\nYes. Pass header=",[247,2131,2132],{},"0, 1, 2"," and you get a three-level MultiIndex. Selection with xs takes the level number, so level=2 slices across the bottom row of labels.",[10,2135,2136,2139],{},[2126,2137,2138],{},"Do I need to unmerge the cells first?","\nNo. Merged cells store their value only in the top-left cell, and pandas forward-fills the blanks to the right when it builds the header levels. Unmerging with openpyxl gives the same result.",[10,2141,2142,2145],{},[2126,2143,2144],{},"How do I write a MultiIndex back to Excel with merged headers?","\nCall to_excel on the DataFrame as it is. pandas writes the levels as separate header rows and merges repeated top-level labels automatically; pass merge_cells=False if you want every cell filled instead.",[10,2147,2148,2151],{},[2126,2149,2150],{},"Why does my index column show up as a header level?","\nWhen index_col is set, pandas writes the index name on the header row. Clear it with rename_axis(None) if you do not want it, or reset_index() to turn it back into a normal column.",[233,2153,2155],{"id":2154},"related","Related",[2157,2158,2159,2166,2173,2180,2185],"ul",{},[2160,2161,2162,2163,2165],"li",{},"Up one level: ",[39,2164,42],{"href":41}," — reading worksheets that were designed for people, not parsers.",[2160,2167,2168,2172],{},[39,2169,2171],{"href":2170},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhandle-merged-cells-when-reading-excel-with-pandas\u002F","Handle Merged Cells When Reading Excel with pandas"," — merged cells in the body of a sheet rather than its header.",[2160,2174,2175,2179],{},[39,2176,2178],{"href":2177},"\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"," — splitting a sheet into blocks before reading each header.",[2160,2181,2182,2184],{},[39,2183,1206],{"href":1205}," — the single-header version of going wide to long.",[2160,2186,2187,2191],{},[39,2188,2190],{"href":2189},"\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"," — header and skiprows for simpler layouts.",[2193,2194,2195],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":243,"searchDepth":297,"depth":297,"links":2197},[2198,2199,2200,2201,2202,2203,2204,2205,2206,2207,2208,2209],{"id":235,"depth":297,"text":236},{"id":500,"depth":297,"text":501},{"id":709,"depth":297,"text":710},{"id":939,"depth":297,"text":940},{"id":1061,"depth":297,"text":1062},{"id":1313,"depth":297,"text":1314},{"id":1542,"depth":297,"text":1543},{"id":1859,"depth":297,"text":1860},{"id":2002,"depth":297,"text":2003},{"id":2104,"depth":297,"text":2105},{"id":2121,"depth":297,"text":2122},{"id":2154,"depth":297,"text":2155},"2026-09-17","Load a sheet whose column headers span two rows — a merged quarter above Revenue and Units — into a MultiIndex, then select, flatten or reshape it into a tidy table.","md",[2214,2216,2218,2220],{"q":2128,"a":2215},"Yes. Pass header=[0, 1, 2] and you get a three-level MultiIndex. Selection with xs takes the level number, so level=2 slices across the bottom row of labels.",{"q":2138,"a":2217},"No. Merged cells store their value only in the top-left cell, and pandas forward-fills the blanks to the right when it builds the header levels. Unmerging with openpyxl gives the same result.",{"q":2144,"a":2219},"Call to_excel on the DataFrame as it is. pandas writes the levels as separate header rows and merges repeated top-level labels automatically; pass merge_cells=False if you want every cell filled instead.",{"q":2150,"a":2221},"When index_col is set, pandas writes the index name on the header row. Clear it with rename_axis(None) if you do not want it, or reset_index() to turn it back into a normal column.","read excel with multi-row headers in pandas",null,{"breadcrumb":2225},[2226,2229,2232],{"name":2227,"item":2228},"Home","\u002F",{"name":2230,"item":2231},"Advanced Data Transformation and Cleaning","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":42,"item":41},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas",{"title":2235,"description":2236},"Read Excel with Multi-Row Headers in pandas (MultiIndex)","Read Excel files with two-row or merged headers in pandas: header=[0, 1], index_col, selecting with xs, flattening column names, and stacking into long format.","read-excel-with-multi-row-headers-in-pandas","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas\u002Findex",[2240,2241,2242],"messy-sheets","reading-excel","pandas","how-to","YoBzfsz17Cw-Q3WZdsYTWuWjgvjm4u_CW_Vc5Gy2QO0",[2246,2250],{"title":2247,"path":2248,"stem":2249,"children":-1},"Read Dropdown Lists and Validation Rules from an Existing Workbook","\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook\u002Findex",{"title":2178,"path":2251,"stem":2252,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-multiple-tables-from-one-excel-sheet","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-multiple-tables-from-one-excel-sheet\u002Findex",[2254,2256],{"path":2255,"title":42},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets",{"path":2257,"title":2258},"\u002Fadvanced-data-transformation-and-cleaning","Advanced Data Transformation and Cleaning for Excel Automation",[],[2261,2264,2266,2269,2272,2275],{"path":2262,"title":2263,"kind":2243},"\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":2265,"title":2171,"kind":2243},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhandle-merged-cells-when-reading-excel-with-pandas",{"path":2267,"title":2268,"kind":2243},"\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":2270,"title":2271,"kind":2243},"\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":2273,"title":2274,"kind":2243},"\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":2276,"title":2277,"kind":2243},"\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",1790972091055]