[{"data":1,"prerenderedAt":2491},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhandle-merged-cells-when-reading-excel-with-pandas":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhandle-merged-cells-when-reading-excel-with-pandas":2482},{"id":4,"title":5,"body":6,"dateModified":2456,"datePublished":2456,"description":2457,"extension":2458,"faq":2459,"meta":2473,"navigation":249,"path":2474,"seo":2475,"slug":2478,"stem":2479,"type":2480,"__hash__":2481},"docs\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhandle-merged-cells-when-reading-excel-with-pandas\u002Findex.md","Handle Merged Cells When Reading Excel with pandas",{"type":7,"value":8,"toc":2444},"minimark",[9,24,186,191,218,221,462,466,472,553,556,883,903,907,914,1006,1013,1078,1084,1125,1128,1214,1218,1221,1321,1604,1616,1619,1623,1705,1712,1927,1933,1937,2078,2082,2092,2102,2158,2164,2309,2329,2333,2343,2347,2356,2365,2374,2384,2399,2403,2440],[10,11,12,13,17,18,23],"p",{},"A merged cell looks like one cell holding one value. In the file it is nothing of the sort: Excel stores the value in the top-left cell of the range and leaves every other cell genuinely empty. pandas reads exactly that, so a tidy-looking sheet with a merged Region column arrives as one label followed by three ",[14,15,16],"code",{},"NaN","s — and if you group by that column, three-quarters of the rows fall out. This guide covers detecting merges, filling them correctly, and unmerging a workbook at the source. It extends ",[19,20,22],"a",{"href":21},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002F","Reading Excel Files with pandas",".",[25,26,35,36,35,40,35,44,35,51,35,58,35,62,35,70,35,76,35,80,35,84,35,90,35,95,35,100,35,104,35,109,35,112,35,116,35,119,35,122,35,128,35,133,35,139,35,142,35,146,35,149,35,153,35,155,35,158,35,162,35,165,35,167,35,169,35,171,35,173,35,175,35,177,35,179,35,181],"svg",{"viewBox":27,"role":28,"ariaLabel":29,"ariaLabelledBy":30,"xmlns":33,"style":34},"0 0 800 258","img","A merged Region cell spanning three rows in Excel, and the same data in pandas where only the first row carries the label and the next two are NaN.",[31,32],"merge-t","merge-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[37,38,39],"title",{"id":31},"What a merged cell looks like in Excel and in a DataFrame",[41,42,43],"desc",{"id":32},"On the left, an Excel sheet where the Region cell for North spans three rows as a single merged block covering three branch rows. On the right, the same data read into pandas: the first row carries North and the next two rows carry NaN, because the file only ever stored the value once. A group-by on region would therefore see one North row rather than three.",[45,46],"rect",{"x":47,"y":47,"width":48,"height":49,"fill":50},"0","800","258","#ffffff",[52,53,57],"text",{"x":54,"y":55,"style":56},"188","30","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","in Excel",[52,59,61],{"x":60,"y":55,"style":56},"596","in pandas",[45,63],{"x":55,"y":64,"width":65,"height":55,"rx":66,"fill":67,"stroke":68,"style":69},"44","140","5","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[52,71,75],{"x":72,"y":73,"style":74},"100","64","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Region",[45,77],{"x":78,"y":64,"width":79,"height":55,"rx":66,"fill":67,"stroke":68,"style":69},"176","170",[52,81,83],{"x":82,"y":73,"style":74},"261","Branch",[45,85],{"x":55,"y":86,"width":65,"height":87,"rx":66,"fill":88,"stroke":89,"style":69},"80","108","#d9f4f1","var(--teal,#0f9488)",[52,91,94],{"x":72,"y":92,"style":93},"130","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","North",[52,96,99],{"x":72,"y":97,"style":98},"152","font-size:9.5px;fill:var(--muted,#5b6780);text-anchor:middle","merged A2:A4",[45,101],{"x":78,"y":86,"width":79,"height":102,"rx":66,"fill":50,"stroke":103},"34","var(--line,#cdd5e6)",[52,105,108],{"x":82,"y":106,"style":107},"102","font-size:11px;fill:var(--text,#172033);text-anchor:middle","Branch 1",[45,110],{"x":78,"y":111,"width":79,"height":102,"rx":66,"fill":50,"stroke":103},"117",[52,113,115],{"x":82,"y":114,"style":107},"139","Branch 2",[45,117],{"x":78,"y":118,"width":79,"height":102,"rx":66,"fill":50,"stroke":103},"154",[52,120,121],{"x":82,"y":78,"style":107},"Branch 3",[123,124],"line",{"x1":125,"y1":126,"x2":127,"y2":126,"stroke":68,"style":69},"374","120","414",[129,130],"polygon",{"points":131,"fill":132},"422,120 410,114 410,126","#5b5cf0",[52,134,138],{"x":135,"y":136,"style":137},"398","212","font-size:10px;fill:var(--muted,#5b6780);text-anchor:middle","read_excel",[45,140],{"x":141,"y":64,"width":65,"height":55,"rx":66,"fill":67,"stroke":68,"style":69},"440",[52,143,145],{"x":144,"y":73,"style":74},"510","region",[45,147],{"x":148,"y":64,"width":79,"height":55,"rx":66,"fill":67,"stroke":68,"style":69},"586",[52,150,152],{"x":151,"y":73,"style":74},"671","branch",[45,154],{"x":141,"y":86,"width":65,"height":102,"rx":66,"fill":88,"stroke":89,"style":69},[52,156,94],{"x":144,"y":106,"style":157},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[45,159],{"x":141,"y":111,"width":65,"height":102,"rx":66,"fill":160,"stroke":161,"style":69},"#fee8f2","var(--accent,#f43f8f)",[52,163,16],{"x":144,"y":114,"style":164},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[45,166],{"x":141,"y":118,"width":65,"height":102,"rx":66,"fill":160,"stroke":161,"style":69},[52,168,16],{"x":144,"y":78,"style":164},[45,170],{"x":148,"y":86,"width":79,"height":102,"rx":66,"fill":50,"stroke":103},[52,172,108],{"x":151,"y":106,"style":107},[45,174],{"x":148,"y":111,"width":79,"height":102,"rx":66,"fill":50,"stroke":103},[52,176,115],{"x":151,"y":114,"style":107},[45,178],{"x":148,"y":118,"width":79,"height":102,"rx":66,"fill":50,"stroke":103},[52,180,121],{"x":151,"y":78,"style":107},[52,182,185],{"x":183,"y":184,"style":164},"400","228","group by region and two of the three branches disappear",[187,188,190],"h2",{"id":189},"prerequisites","Prerequisites",[192,193,198],"pre",{"className":194,"code":195,"language":196,"meta":197,"style":197},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,199,200],{"__ignoreMap":197},[201,202,204,208,212,215],"span",{"class":123,"line":203},1,[201,205,207],{"class":206},"sMTad","pip",[201,209,211],{"class":210},"srMev"," install",[201,213,214],{"class":210}," pandas",[201,216,217],{"class":210}," openpyxl\n",[10,219,220],{},"A workbook with a vertical merge, so every example has something real to work on:",[192,222,226],{"className":223,"code":224,"language":225,"meta":197,"style":197},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\n\nwb = Workbook()\nws = wb.active\nws.append([\"Region\", \"Branch\", \"Revenue\"])\nfor row in [\n    [\"North\", \"Branch 1\", 5150.00], [None, \"Branch 2\", 4268.00],\n    [None, \"Branch 3\", 3511.25], [\"South\", \"Branch 4\", 2980.10],\n    [None, \"Branch 5\", 3140.75],\n]:\n    ws.append(row)\n\nws.merge_cells(\"A2:A4\")      # North spans three branch rows\nws.merge_cells(\"A5:A6\")      # South spans two\nwb.save(\"merged.xlsx\")\n","python",[14,227,228,244,251,263,274,297,312,351,385,404,410,416,421,437,450],{"__ignoreMap":197},[201,229,230,234,238,241],{"class":123,"line":203},[201,231,233],{"class":232},"s-kum","from",[201,235,237],{"class":236},"skGVy"," openpyxl ",[201,239,240],{"class":232},"import",[201,242,243],{"class":236}," Workbook\n",[201,245,247],{"class":123,"line":246},2,[201,248,250],{"emptyLinePlaceholder":249},true,"\n",[201,252,254,257,260],{"class":123,"line":253},3,[201,255,256],{"class":236},"wb ",[201,258,259],{"class":232},"=",[201,261,262],{"class":236}," Workbook()\n",[201,264,266,269,271],{"class":123,"line":265},4,[201,267,268],{"class":236},"ws ",[201,270,259],{"class":232},[201,272,273],{"class":236}," wb.active\n",[201,275,277,280,283,286,289,291,294],{"class":123,"line":276},5,[201,278,279],{"class":236},"ws.append([",[201,281,282],{"class":210},"\"Region\"",[201,284,285],{"class":236},", ",[201,287,288],{"class":210},"\"Branch\"",[201,290,285],{"class":236},[201,292,293],{"class":210},"\"Revenue\"",[201,295,296],{"class":236},"])\n",[201,298,300,303,306,309],{"class":123,"line":299},6,[201,301,302],{"class":232},"for",[201,304,305],{"class":236}," row ",[201,307,308],{"class":232},"in",[201,310,311],{"class":236}," [\n",[201,313,315,318,321,323,326,328,332,335,338,340,343,345,348],{"class":123,"line":314},7,[201,316,317],{"class":236},"    [",[201,319,320],{"class":210},"\"North\"",[201,322,285],{"class":236},[201,324,325],{"class":210},"\"Branch 1\"",[201,327,285],{"class":236},[201,329,331],{"class":330},"sP0c6","5150.00",[201,333,334],{"class":236},"], [",[201,336,337],{"class":330},"None",[201,339,285],{"class":236},[201,341,342],{"class":210},"\"Branch 2\"",[201,344,285],{"class":236},[201,346,347],{"class":330},"4268.00",[201,349,350],{"class":236},"],\n",[201,352,354,356,358,360,363,365,368,370,373,375,378,380,383],{"class":123,"line":353},8,[201,355,317],{"class":236},[201,357,337],{"class":330},[201,359,285],{"class":236},[201,361,362],{"class":210},"\"Branch 3\"",[201,364,285],{"class":236},[201,366,367],{"class":330},"3511.25",[201,369,334],{"class":236},[201,371,372],{"class":210},"\"South\"",[201,374,285],{"class":236},[201,376,377],{"class":210},"\"Branch 4\"",[201,379,285],{"class":236},[201,381,382],{"class":330},"2980.10",[201,384,350],{"class":236},[201,386,388,390,392,394,397,399,402],{"class":123,"line":387},9,[201,389,317],{"class":236},[201,391,337],{"class":330},[201,393,285],{"class":236},[201,395,396],{"class":210},"\"Branch 5\"",[201,398,285],{"class":236},[201,400,401],{"class":330},"3140.75",[201,403,350],{"class":236},[201,405,407],{"class":123,"line":406},10,[201,408,409],{"class":236},"]:\n",[201,411,413],{"class":123,"line":412},11,[201,414,415],{"class":236},"    ws.append(row)\n",[201,417,419],{"class":123,"line":418},12,[201,420,250],{"emptyLinePlaceholder":249},[201,422,424,427,430,433],{"class":123,"line":423},13,[201,425,426],{"class":236},"ws.merge_cells(",[201,428,429],{"class":210},"\"A2:A4\"",[201,431,432],{"class":236},")      ",[201,434,436],{"class":435},"s-wDw","# North spans three branch rows\n",[201,438,440,442,445,447],{"class":123,"line":439},14,[201,441,426],{"class":236},[201,443,444],{"class":210},"\"A5:A6\"",[201,446,432],{"class":236},[201,448,449],{"class":435},"# South spans two\n",[201,451,453,456,459],{"class":123,"line":452},15,[201,454,455],{"class":236},"wb.save(",[201,457,458],{"class":210},"\"merged.xlsx\"",[201,460,461],{"class":236},")\n",[187,463,465],{"id":464},"step-1-confirm-the-blanks-really-come-from-merges","Step 1 — Confirm the blanks really come from merges",[10,467,468,469,471],{},"Do not assume. A ",[14,470,16],{}," in a label column might be a merge, or it might be genuinely missing data — and the fixes are opposite. openpyxl tells you definitively:",[192,473,475],{"className":223,"code":474,"language":225,"meta":197,"style":197},"from openpyxl import load_workbook\n\nwb = load_workbook(\"merged.xlsx\")\nws = wb.active\n\nfor rng in ws.merged_cells.ranges:\n    print(rng, \"->\", ws.cell(rng.min_row, rng.min_col).value)\n# A2:A4 -> North\n# A5:A6 -> South\n",[14,476,477,488,492,505,513,517,529,543,548],{"__ignoreMap":197},[201,478,479,481,483,485],{"class":123,"line":203},[201,480,233],{"class":232},[201,482,237],{"class":236},[201,484,240],{"class":232},[201,486,487],{"class":236}," load_workbook\n",[201,489,490],{"class":123,"line":246},[201,491,250],{"emptyLinePlaceholder":249},[201,493,494,496,498,501,503],{"class":123,"line":253},[201,495,256],{"class":236},[201,497,259],{"class":232},[201,499,500],{"class":236}," load_workbook(",[201,502,458],{"class":210},[201,504,461],{"class":236},[201,506,507,509,511],{"class":123,"line":265},[201,508,268],{"class":236},[201,510,259],{"class":232},[201,512,273],{"class":236},[201,514,515],{"class":123,"line":276},[201,516,250],{"emptyLinePlaceholder":249},[201,518,519,521,524,526],{"class":123,"line":299},[201,520,302],{"class":232},[201,522,523],{"class":236}," rng ",[201,525,308],{"class":232},[201,527,528],{"class":236}," ws.merged_cells.ranges:\n",[201,530,531,534,537,540],{"class":123,"line":314},[201,532,533],{"class":330},"    print",[201,535,536],{"class":236},"(rng, ",[201,538,539],{"class":210},"\"->\"",[201,541,542],{"class":236},", ws.cell(rng.min_row, rng.min_col).value)\n",[201,544,545],{"class":123,"line":353},[201,546,547],{"class":435},"# A2:A4 -> North\n",[201,549,550],{"class":123,"line":387},[201,551,552],{"class":435},"# A5:A6 -> South\n",[10,554,555],{},"Classify them by orientation, because vertical and horizontal merges need different handling:",[192,557,559],{"className":223,"code":558,"language":225,"meta":197,"style":197},"from openpyxl import load_workbook\nfrom openpyxl.utils import get_column_letter\n\ndef describe_merges(path, sheet_name=None):\n    \"\"\"Summarise the merged ranges in a sheet by orientation.\"\"\"\n    wb = load_workbook(path)\n    ws = wb[sheet_name] if sheet_name else wb.active\n\n    report = {\"vertical\": [], \"horizontal\": [], \"block\": []}\n    for rng in ws.merged_cells.ranges:\n        tall = rng.max_row > rng.min_row\n        wide = rng.max_col > rng.min_col\n        kind = \"block\" if (tall and wide) else (\"vertical\" if tall else \"horizontal\")\n        report[kind].append({\n            \"ref\": str(rng),\n            \"column\": get_column_letter(rng.min_col),\n            \"value\": ws.cell(rng.min_row, rng.min_col).value,\n        })\n    return report\n\ninfo = describe_merges(\"merged.xlsx\")\nprint(f\"{len(info['vertical'])} vertical, {len(info['horizontal'])} horizontal\")\n",[14,560,561,571,583,587,606,611,621,642,646,673,684,700,715,756,761,775,784,793,799,808,813,828],{"__ignoreMap":197},[201,562,563,565,567,569],{"class":123,"line":203},[201,564,233],{"class":232},[201,566,237],{"class":236},[201,568,240],{"class":232},[201,570,487],{"class":236},[201,572,573,575,578,580],{"class":123,"line":246},[201,574,233],{"class":232},[201,576,577],{"class":236}," openpyxl.utils ",[201,579,240],{"class":232},[201,581,582],{"class":236}," get_column_letter\n",[201,584,585],{"class":123,"line":253},[201,586,250],{"emptyLinePlaceholder":249},[201,588,589,592,596,599,601,603],{"class":123,"line":265},[201,590,591],{"class":232},"def",[201,593,595],{"class":594},"s_Opv"," describe_merges",[201,597,598],{"class":236},"(path, sheet_name",[201,600,259],{"class":232},[201,602,337],{"class":330},[201,604,605],{"class":236},"):\n",[201,607,608],{"class":123,"line":276},[201,609,610],{"class":210},"    \"\"\"Summarise the merged ranges in a sheet by orientation.\"\"\"\n",[201,612,613,616,618],{"class":123,"line":299},[201,614,615],{"class":236},"    wb ",[201,617,259],{"class":232},[201,619,620],{"class":236}," load_workbook(path)\n",[201,622,623,626,628,631,634,637,640],{"class":123,"line":314},[201,624,625],{"class":236},"    ws ",[201,627,259],{"class":232},[201,629,630],{"class":236}," wb[sheet_name] ",[201,632,633],{"class":232},"if",[201,635,636],{"class":236}," sheet_name ",[201,638,639],{"class":232},"else",[201,641,273],{"class":236},[201,643,644],{"class":123,"line":353},[201,645,250],{"emptyLinePlaceholder":249},[201,647,648,651,653,656,659,662,665,667,670],{"class":123,"line":387},[201,649,650],{"class":236},"    report ",[201,652,259],{"class":232},[201,654,655],{"class":236}," {",[201,657,658],{"class":210},"\"vertical\"",[201,660,661],{"class":236},": [], ",[201,663,664],{"class":210},"\"horizontal\"",[201,666,661],{"class":236},[201,668,669],{"class":210},"\"block\"",[201,671,672],{"class":236},": []}\n",[201,674,675,678,680,682],{"class":123,"line":406},[201,676,677],{"class":232},"    for",[201,679,523],{"class":236},[201,681,308],{"class":232},[201,683,528],{"class":236},[201,685,686,689,691,694,697],{"class":123,"line":412},[201,687,688],{"class":236},"        tall ",[201,690,259],{"class":232},[201,692,693],{"class":236}," rng.max_row ",[201,695,696],{"class":232},">",[201,698,699],{"class":236}," rng.min_row\n",[201,701,702,705,707,710,712],{"class":123,"line":418},[201,703,704],{"class":236},"        wide ",[201,706,259],{"class":232},[201,708,709],{"class":236}," rng.max_col ",[201,711,696],{"class":232},[201,713,714],{"class":236}," rng.min_col\n",[201,716,717,720,722,725,728,731,734,737,739,742,744,746,749,751,754],{"class":123,"line":423},[201,718,719],{"class":236},"        kind ",[201,721,259],{"class":232},[201,723,724],{"class":210}," \"block\"",[201,726,727],{"class":232}," if",[201,729,730],{"class":236}," (tall ",[201,732,733],{"class":232},"and",[201,735,736],{"class":236}," wide) ",[201,738,639],{"class":232},[201,740,741],{"class":236}," (",[201,743,658],{"class":210},[201,745,727],{"class":232},[201,747,748],{"class":236}," tall ",[201,750,639],{"class":232},[201,752,753],{"class":210}," \"horizontal\"",[201,755,461],{"class":236},[201,757,758],{"class":123,"line":439},[201,759,760],{"class":236},"        report[kind].append({\n",[201,762,763,766,769,772],{"class":123,"line":452},[201,764,765],{"class":210},"            \"ref\"",[201,767,768],{"class":236},": ",[201,770,771],{"class":330},"str",[201,773,774],{"class":236},"(rng),\n",[201,776,778,781],{"class":123,"line":777},16,[201,779,780],{"class":210},"            \"column\"",[201,782,783],{"class":236},": get_column_letter(rng.min_col),\n",[201,785,787,790],{"class":123,"line":786},17,[201,788,789],{"class":210},"            \"value\"",[201,791,792],{"class":236},": ws.cell(rng.min_row, rng.min_col).value,\n",[201,794,796],{"class":123,"line":795},18,[201,797,798],{"class":236},"        })\n",[201,800,802,805],{"class":123,"line":801},19,[201,803,804],{"class":232},"    return",[201,806,807],{"class":236}," report\n",[201,809,811],{"class":123,"line":810},20,[201,812,250],{"emptyLinePlaceholder":249},[201,814,816,819,821,824,826],{"class":123,"line":815},21,[201,817,818],{"class":236},"info ",[201,820,259],{"class":232},[201,822,823],{"class":236}," describe_merges(",[201,825,458],{"class":210},[201,827,461],{"class":236},[201,829,831,834,837,840,843,847,850,853,856,859,862,865,867,869,871,874,876,878,881],{"class":123,"line":830},22,[201,832,833],{"class":330},"print",[201,835,836],{"class":236},"(",[201,838,839],{"class":232},"f",[201,841,842],{"class":210},"\"",[201,844,846],{"class":845},"sSjpA","{",[201,848,849],{"class":330},"len",[201,851,852],{"class":236},"(info[",[201,854,855],{"class":210},"'vertical'",[201,857,858],{"class":236},"])",[201,860,861],{"class":845},"}",[201,863,864],{"class":210}," vertical, ",[201,866,846],{"class":845},[201,868,849],{"class":330},[201,870,852],{"class":236},[201,872,873],{"class":210},"'horizontal'",[201,875,858],{"class":236},[201,877,861],{"class":845},[201,879,880],{"class":210}," horizontal\"",[201,882,461],{"class":236},[10,884,885,889,890,893,894,898,899,902],{},[886,887,888],"strong",{},"Vertical"," merges are label columns and forward-fill correctly. ",[886,891,892],{},"Horizontal"," merges are usually group headers and belong in the header handling covered by ",[19,895,897],{"href":896},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fskip-rows-and-set-header-when-reading-excel-with-pandas\u002F","skipping rows and setting the header",". ",[886,900,901],{},"Block"," merges are title banners and should be excluded from the data range entirely.",[187,904,906],{"id":905},"step-2-forward-fill-the-label-column","Step 2 — Forward-fill the label column",[10,908,909,910,913],{},"For vertical merges, ",[14,911,912],{},"ffill"," restores what the sheet visually implies:",[192,915,917],{"className":223,"code":916,"language":225,"meta":197,"style":197},"import pandas as pd\n\ndf = pd.read_excel(\"merged.xlsx\")\nprint(df[\"Region\"].tolist())\n# ['North', nan, nan, 'South', nan]\n\ndf[\"Region\"] = df[\"Region\"].ffill()\nprint(df[\"Region\"].tolist())\n# ['North', 'North', 'North', 'South', 'South']\n",[14,918,919,932,936,950,962,967,971,991,1001],{"__ignoreMap":197},[201,920,921,923,926,929],{"class":123,"line":203},[201,922,240],{"class":232},[201,924,925],{"class":236}," pandas ",[201,927,928],{"class":232},"as",[201,930,931],{"class":236}," pd\n",[201,933,934],{"class":123,"line":246},[201,935,250],{"emptyLinePlaceholder":249},[201,937,938,941,943,946,948],{"class":123,"line":253},[201,939,940],{"class":236},"df ",[201,942,259],{"class":232},[201,944,945],{"class":236}," pd.read_excel(",[201,947,458],{"class":210},[201,949,461],{"class":236},[201,951,952,954,957,959],{"class":123,"line":265},[201,953,833],{"class":330},[201,955,956],{"class":236},"(df[",[201,958,282],{"class":210},[201,960,961],{"class":236},"].tolist())\n",[201,963,964],{"class":123,"line":276},[201,965,966],{"class":435},"# ['North', nan, nan, 'South', nan]\n",[201,968,969],{"class":123,"line":299},[201,970,250],{"emptyLinePlaceholder":249},[201,972,973,976,978,981,983,986,988],{"class":123,"line":314},[201,974,975],{"class":236},"df[",[201,977,282],{"class":210},[201,979,980],{"class":236},"] ",[201,982,259],{"class":232},[201,984,985],{"class":236}," df[",[201,987,282],{"class":210},[201,989,990],{"class":236},"].ffill()\n",[201,992,993,995,997,999],{"class":123,"line":353},[201,994,833],{"class":330},[201,996,956],{"class":236},[201,998,282],{"class":210},[201,1000,961],{"class":236},[201,1002,1003],{"class":123,"line":387},[201,1004,1005],{"class":435},"# ['North', 'North', 'North', 'South', 'South']\n",[10,1007,1008,1009,1012],{},"Fill only the columns you know are merged. A blanket ",[14,1010,1011],{},"df.ffill()"," propagates values across every column, which invents data in numeric fields — a missing revenue silently becomes the previous branch's revenue:",[192,1014,1016],{"className":223,"code":1015,"language":225,"meta":197,"style":197},"# Right: named columns only.\nMERGED_LABELS = [\"Region\", \"Category\"]\ndf[MERGED_LABELS] = df[MERGED_LABELS].ffill()\n\n# Wrong: fills revenue gaps with the row above.\ndf = df.ffill()\n",[14,1017,1018,1023,1044,1060,1064,1069],{"__ignoreMap":197},[201,1019,1020],{"class":123,"line":203},[201,1021,1022],{"class":435},"# Right: named columns only.\n",[201,1024,1025,1028,1031,1034,1036,1038,1041],{"class":123,"line":246},[201,1026,1027],{"class":330},"MERGED_LABELS",[201,1029,1030],{"class":232}," =",[201,1032,1033],{"class":236}," [",[201,1035,282],{"class":210},[201,1037,285],{"class":236},[201,1039,1040],{"class":210},"\"Category\"",[201,1042,1043],{"class":236},"]\n",[201,1045,1046,1048,1050,1052,1054,1056,1058],{"class":123,"line":253},[201,1047,975],{"class":236},[201,1049,1027],{"class":330},[201,1051,980],{"class":236},[201,1053,259],{"class":232},[201,1055,985],{"class":236},[201,1057,1027],{"class":330},[201,1059,990],{"class":236},[201,1061,1062],{"class":123,"line":265},[201,1063,250],{"emptyLinePlaceholder":249},[201,1065,1066],{"class":123,"line":276},[201,1067,1068],{"class":435},"# Wrong: fills revenue gaps with the row above.\n",[201,1070,1071,1073,1075],{"class":123,"line":299},[201,1072,940],{"class":236},[201,1074,259],{"class":232},[201,1076,1077],{"class":236}," df.ffill()\n",[10,1079,1080,1081,1083],{},"Two guards make the fill safe. First, a leading ",[14,1082,16],{}," has nothing above it to inherit, which means the sheet did not start where you thought:",[192,1085,1087],{"className":223,"code":1086,"language":225,"meta":197,"style":197},"if df[\"Region\"].isna().iloc[0]:\n    raise ValueError(\n        \"The first row has no Region — the header row is probably wrong.\"\n    )\n",[14,1088,1089,1104,1115,1120],{"__ignoreMap":197},[201,1090,1091,1093,1095,1097,1100,1102],{"class":123,"line":203},[201,1092,633],{"class":232},[201,1094,985],{"class":236},[201,1096,282],{"class":210},[201,1098,1099],{"class":236},"].isna().iloc[",[201,1101,47],{"class":330},[201,1103,409],{"class":236},[201,1105,1106,1109,1112],{"class":123,"line":246},[201,1107,1108],{"class":232},"    raise",[201,1110,1111],{"class":330}," ValueError",[201,1113,1114],{"class":236},"(\n",[201,1116,1117],{"class":123,"line":253},[201,1118,1119],{"class":210},"        \"The first row has no Region — the header row is probably wrong.\"\n",[201,1121,1122],{"class":123,"line":265},[201,1123,1124],{"class":236},"    )\n",[10,1126,1127],{},"Second, cap how far a value may propagate. An unbounded fill will happily carry a label across a hundred rows if the sheet has a gap in it:",[192,1129,1131],{"className":223,"code":1130,"language":225,"meta":197,"style":197},"# A merge realistically spans a handful of rows, not fifty.\ndf[\"Region\"] = df[\"Region\"].ffill(limit=20)\n\nstill_missing = df[\"Region\"].isna().sum()\nif still_missing:\n    print(f\"warning: {still_missing} rows still have no Region after filling\")\n",[14,1132,1133,1138,1166,1170,1184,1191],{"__ignoreMap":197},[201,1134,1135],{"class":123,"line":203},[201,1136,1137],{"class":435},"# A merge realistically spans a handful of rows, not fifty.\n",[201,1139,1140,1142,1144,1146,1148,1150,1152,1155,1159,1161,1164],{"class":123,"line":246},[201,1141,975],{"class":236},[201,1143,282],{"class":210},[201,1145,980],{"class":236},[201,1147,259],{"class":232},[201,1149,985],{"class":236},[201,1151,282],{"class":210},[201,1153,1154],{"class":236},"].ffill(",[201,1156,1158],{"class":1157},"sa561","limit",[201,1160,259],{"class":232},[201,1162,1163],{"class":330},"20",[201,1165,461],{"class":236},[201,1167,1168],{"class":123,"line":253},[201,1169,250],{"emptyLinePlaceholder":249},[201,1171,1172,1175,1177,1179,1181],{"class":123,"line":265},[201,1173,1174],{"class":236},"still_missing ",[201,1176,259],{"class":232},[201,1178,985],{"class":236},[201,1180,282],{"class":210},[201,1182,1183],{"class":236},"].isna().sum()\n",[201,1185,1186,1188],{"class":123,"line":276},[201,1187,633],{"class":232},[201,1189,1190],{"class":236}," still_missing:\n",[201,1192,1193,1195,1197,1199,1202,1204,1207,1209,1212],{"class":123,"line":299},[201,1194,533],{"class":330},[201,1196,836],{"class":236},[201,1198,839],{"class":232},[201,1200,1201],{"class":210},"\"warning: ",[201,1203,846],{"class":845},[201,1205,1206],{"class":236},"still_missing",[201,1208,861],{"class":845},[201,1210,1211],{"class":210}," rows still have no Region after filling\"",[201,1213,461],{"class":236},[187,1215,1217],{"id":1216},"step-3-unmerge-at-the-source-instead","Step 3 — Unmerge at the source instead",[10,1219,1220],{},"Filling in pandas is a workaround. If the same file arrives every month, flattening the workbook once is cleaner — every downstream reader then gets a rectangular sheet with no special handling at all.",[25,1222,35,1228,35,1231,35,1234,35,1237,35,1244,35,1250,35,1255,35,1260,35,1265,35,1270,35,1273,35,1279,35,1282,35,1286,35,1289,35,1292,35,1296,35,1300,35,1303,35,1307,35,1310,35,1313,35,1316],{"viewBox":1223,"role":28,"ariaLabel":1224,"ariaLabelledBy":1225,"xmlns":33,"style":34},"0 0 800 226","Unmerge and fill: for each merged range, capture the top-left value, remove the merge, then write that value into every cell of the former range.",[1226,1227],"unm-t","unm-d",[37,1229,1230],{"id":1226},"Flattening a merged sheet in three moves",[41,1232,1233],{"id":1227},"Three ordered steps applied to each merged range. First the top-left cell's value is captured, because removing the merge would otherwise lose it. Second unmerge_cells removes the merge definition, leaving one populated cell and the rest empty. Third the captured value is written into every cell of the former range, producing a rectangular sheet that any reader handles without special cases.",[45,1235],{"x":47,"y":47,"width":48,"height":1236,"fill":50},"226",[45,1238],{"x":1239,"y":1240,"width":1241,"height":1242,"rx":1243,"fill":67,"stroke":68,"style":69},"14","66","230","96","13",[52,1245,1249],{"x":1246,"y":1247,"style":1248},"129","50","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","1 · capture",[52,1251,1254],{"x":1246,"y":1252,"style":1253},"98","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","read the top-left value",[52,1256,1259],{"x":1246,"y":1257,"style":1258},"124","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","before touching the merge",[52,1261,1264],{"x":1246,"y":1262,"style":1263},"144","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","it is the only copy",[123,1266],{"x1":1267,"y1":1268,"x2":1269,"y2":1268,"stroke":68,"style":69},"244","114","272",[129,1271],{"points":1272,"fill":132},"280,114 268,108 268,120",[45,1274],{"x":1275,"y":1240,"width":1276,"height":1242,"rx":1243,"fill":1277,"stroke":1278,"style":69},"288","224","#fdefd8","var(--gold,#b4740a)",[52,1280,1281],{"x":183,"y":1247,"style":1248},"2 · unmerge",[52,1283,1285],{"x":183,"y":1252,"style":1284},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","ws.unmerge_cells(ref)",[52,1287,1288],{"x":183,"y":1257,"style":1258},"one cell keeps the value",[52,1290,1291],{"x":183,"y":1262,"style":1263},"the rest are empty",[123,1293],{"x1":1294,"y1":1268,"x2":1295,"y2":1268,"stroke":1278,"style":69},"512","540",[129,1297],{"points":1298,"fill":1299},"548,114 536,108 536,120","#b4740a",[45,1301],{"x":1302,"y":1240,"width":184,"height":1242,"rx":1243,"fill":88,"stroke":89,"style":69},"556",[52,1304,1306],{"x":1305,"y":1247,"style":1248},"670","3 · fill",[52,1308,1309],{"x":1305,"y":1252,"style":93},"write it to every cell",[52,1311,1312],{"x":1305,"y":1257,"style":1258},"the sheet is now rectangular",[52,1314,1315],{"x":1305,"y":1262,"style":1263},"no reader needs a special case",[52,1317,1320],{"x":183,"y":1318,"style":1319},"196","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","always write to a copy — flattening destroys the layout the original was designed for",[192,1322,1324],{"className":223,"code":1323,"language":225,"meta":197,"style":197},"from openpyxl import load_workbook\nfrom openpyxl.utils import range_boundaries\n\ndef unmerge_and_fill(src, dest, sheet_name=None):\n    \"\"\"Flatten every merged range so each cell carries its own value.\"\"\"\n    wb = load_workbook(src)\n    sheets = [wb[sheet_name]] if sheet_name else wb.worksheets\n    flattened = 0\n\n    for ws in sheets:\n        # Copy the list: unmerging mutates the collection we are iterating.\n        for ref in [str(r) for r in ws.merged_cells.ranges]:\n            min_col, min_row, max_col, max_row = range_boundaries(ref)\n            value = ws.cell(min_row, min_col).value\n\n            ws.unmerge_cells(ref)\n            for row in range(min_row, max_row + 1):\n                for col in range(min_col, max_col + 1):\n                    ws.cell(row, col, value)\n            flattened += 1\n\n    wb.save(dest)\n    return flattened\n\nprint(f\"flattened {unmerge_and_fill('merged.xlsx', 'flat.xlsx')} ranges\")\n",[14,1325,1326,1336,1347,1351,1367,1372,1381,1400,1410,1414,1426,1431,1458,1468,1478,1482,1487,1510,1531,1536,1547,1551,1556,1564,1569],{"__ignoreMap":197},[201,1327,1328,1330,1332,1334],{"class":123,"line":203},[201,1329,233],{"class":232},[201,1331,237],{"class":236},[201,1333,240],{"class":232},[201,1335,487],{"class":236},[201,1337,1338,1340,1342,1344],{"class":123,"line":246},[201,1339,233],{"class":232},[201,1341,577],{"class":236},[201,1343,240],{"class":232},[201,1345,1346],{"class":236}," range_boundaries\n",[201,1348,1349],{"class":123,"line":253},[201,1350,250],{"emptyLinePlaceholder":249},[201,1352,1353,1355,1358,1361,1363,1365],{"class":123,"line":265},[201,1354,591],{"class":232},[201,1356,1357],{"class":594}," unmerge_and_fill",[201,1359,1360],{"class":236},"(src, dest, sheet_name",[201,1362,259],{"class":232},[201,1364,337],{"class":330},[201,1366,605],{"class":236},[201,1368,1369],{"class":123,"line":276},[201,1370,1371],{"class":210},"    \"\"\"Flatten every merged range so each cell carries its own value.\"\"\"\n",[201,1373,1374,1376,1378],{"class":123,"line":299},[201,1375,615],{"class":236},[201,1377,259],{"class":232},[201,1379,1380],{"class":236}," load_workbook(src)\n",[201,1382,1383,1386,1388,1391,1393,1395,1397],{"class":123,"line":314},[201,1384,1385],{"class":236},"    sheets ",[201,1387,259],{"class":232},[201,1389,1390],{"class":236}," [wb[sheet_name]] ",[201,1392,633],{"class":232},[201,1394,636],{"class":236},[201,1396,639],{"class":232},[201,1398,1399],{"class":236}," wb.worksheets\n",[201,1401,1402,1405,1407],{"class":123,"line":353},[201,1403,1404],{"class":236},"    flattened ",[201,1406,259],{"class":232},[201,1408,1409],{"class":330}," 0\n",[201,1411,1412],{"class":123,"line":387},[201,1413,250],{"emptyLinePlaceholder":249},[201,1415,1416,1418,1421,1423],{"class":123,"line":406},[201,1417,677],{"class":232},[201,1419,1420],{"class":236}," ws ",[201,1422,308],{"class":232},[201,1424,1425],{"class":236}," sheets:\n",[201,1427,1428],{"class":123,"line":412},[201,1429,1430],{"class":435},"        # Copy the list: unmerging mutates the collection we are iterating.\n",[201,1432,1433,1436,1439,1441,1443,1445,1448,1450,1453,1455],{"class":123,"line":418},[201,1434,1435],{"class":232},"        for",[201,1437,1438],{"class":236}," ref ",[201,1440,308],{"class":232},[201,1442,1033],{"class":236},[201,1444,771],{"class":330},[201,1446,1447],{"class":236},"(r) ",[201,1449,302],{"class":232},[201,1451,1452],{"class":236}," r ",[201,1454,308],{"class":232},[201,1456,1457],{"class":236}," ws.merged_cells.ranges]:\n",[201,1459,1460,1463,1465],{"class":123,"line":423},[201,1461,1462],{"class":236},"            min_col, min_row, max_col, max_row ",[201,1464,259],{"class":232},[201,1466,1467],{"class":236}," range_boundaries(ref)\n",[201,1469,1470,1473,1475],{"class":123,"line":439},[201,1471,1472],{"class":236},"            value ",[201,1474,259],{"class":232},[201,1476,1477],{"class":236}," ws.cell(min_row, min_col).value\n",[201,1479,1480],{"class":123,"line":452},[201,1481,250],{"emptyLinePlaceholder":249},[201,1483,1484],{"class":123,"line":777},[201,1485,1486],{"class":236},"            ws.unmerge_cells(ref)\n",[201,1488,1489,1492,1494,1496,1499,1502,1505,1508],{"class":123,"line":786},[201,1490,1491],{"class":232},"            for",[201,1493,305],{"class":236},[201,1495,308],{"class":232},[201,1497,1498],{"class":330}," range",[201,1500,1501],{"class":236},"(min_row, max_row ",[201,1503,1504],{"class":232},"+",[201,1506,1507],{"class":330}," 1",[201,1509,605],{"class":236},[201,1511,1512,1515,1518,1520,1522,1525,1527,1529],{"class":123,"line":795},[201,1513,1514],{"class":232},"                for",[201,1516,1517],{"class":236}," col ",[201,1519,308],{"class":232},[201,1521,1498],{"class":330},[201,1523,1524],{"class":236},"(min_col, max_col ",[201,1526,1504],{"class":232},[201,1528,1507],{"class":330},[201,1530,605],{"class":236},[201,1532,1533],{"class":123,"line":801},[201,1534,1535],{"class":236},"                    ws.cell(row, col, value)\n",[201,1537,1538,1541,1544],{"class":123,"line":810},[201,1539,1540],{"class":236},"            flattened ",[201,1542,1543],{"class":232},"+=",[201,1545,1546],{"class":330}," 1\n",[201,1548,1549],{"class":123,"line":815},[201,1550,250],{"emptyLinePlaceholder":249},[201,1552,1553],{"class":123,"line":830},[201,1554,1555],{"class":236},"    wb.save(dest)\n",[201,1557,1559,1561],{"class":123,"line":1558},23,[201,1560,804],{"class":232},[201,1562,1563],{"class":236}," flattened\n",[201,1565,1567],{"class":123,"line":1566},24,[201,1568,250],{"emptyLinePlaceholder":249},[201,1570,1572,1574,1576,1578,1581,1583,1586,1589,1591,1594,1597,1599,1602],{"class":123,"line":1571},25,[201,1573,833],{"class":330},[201,1575,836],{"class":236},[201,1577,839],{"class":232},[201,1579,1580],{"class":210},"\"flattened ",[201,1582,846],{"class":845},[201,1584,1585],{"class":236},"unmerge_and_fill(",[201,1587,1588],{"class":210},"'merged.xlsx'",[201,1590,285],{"class":236},[201,1592,1593],{"class":210},"'flat.xlsx'",[201,1595,1596],{"class":236},")",[201,1598,861],{"class":845},[201,1600,1601],{"class":210}," ranges\"",[201,1603,461],{"class":236},[10,1605,1606,1607,1610,1611,1615],{},"Two details matter. The list comprehension around ",[14,1608,1609],{},"ws.merged_cells.ranges"," takes a snapshot before iterating — unmerging modifies that collection, and iterating it directly skips ranges or raises. And the value must be captured ",[1612,1613,1614],"em",{},"before"," the unmerge, because the merge is the only thing keeping it addressable as a single logical cell.",[10,1617,1618],{},"Write to a new file. Flattening is lossy in the other direction: the original layout was designed for human reading, and you cannot reconstruct which ranges were merged once they are gone.",[187,1620,1622],{"id":1621},"step-4-handle-merged-headers","Step 4 — Handle merged headers",[25,1624,35,1630,35,1633,35,1636,35,1638,35,1641,35,1647,35,1653,35,1655,35,1659,35,1662,35,1665,35,1669,35,1675,35,1678,35,1681,35,1684,35,1687,35,1692,35,1695,35,1699,35,1702],{"viewBox":1625,"role":28,"ariaLabel":1626,"ariaLabelledBy":1627,"xmlns":33,"style":34},"0 0 800 224","Three merge orientations and what each means: vertical merges are label columns to forward-fill, horizontal merges are group headers, and block merges are title banners to exclude.",[1628,1629],"orient-t","orient-d",[37,1631,1632],{"id":1628},"Merge orientation decides the fix",[41,1634,1635],{"id":1629},"Three shapes. A tall narrow merge spanning several rows in one column is a label such as a region name, and forward-filling down restores it. A wide flat merge spanning several columns in one row is a group header, and filling rightwards across the upper header level restores it. A merge spanning both rows and columns is a title banner and should be excluded from the data range entirely rather than filled.",[45,1637],{"x":47,"y":47,"width":48,"height":1276,"fill":50},[45,1639],{"x":1239,"y":1163,"width":1640,"height":54,"rx":1239,"fill":88,"stroke":89,"style":69},"248",[52,1642,1646],{"x":1643,"y":1644,"style":1645},"138","48","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","vertical",[45,1648],{"x":1649,"y":1650,"width":73,"height":1651,"rx":1652,"fill":50,"stroke":89,"style":69},"106","62","76","6",[52,1654,94],{"x":1643,"y":1649,"style":157},[52,1656,1658],{"x":1643,"y":1657,"style":107},"164","a label column",[52,1660,1661],{"x":1643,"y":54,"style":157},"ffill downwards",[45,1663],{"x":1664,"y":1163,"width":1640,"height":54,"rx":1239,"fill":67,"stroke":68,"style":69},"276",[52,1666,1668],{"x":183,"y":1644,"style":1667},"font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","horizontal",[45,1670],{"x":1671,"y":1672,"width":1673,"height":1674,"rx":1652,"fill":50,"stroke":68,"style":69},"304","82","192","36",[52,1676,1677],{"x":183,"y":1649,"style":74},"Q1",[52,1679,1680],{"x":183,"y":1657,"style":107},"a group header",[52,1682,1683],{"x":183,"y":54,"style":74},"fill rightwards",[45,1685],{"x":1686,"y":1163,"width":1640,"height":54,"rx":1239,"fill":1277,"stroke":1278,"style":69},"538",[52,1688,1691],{"x":1689,"y":1644,"style":1690},"662","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","block",[45,1693],{"x":1694,"y":1650,"width":1673,"height":1651,"rx":1652,"fill":50,"stroke":1278,"style":69},"566",[52,1696,1698],{"x":1689,"y":1649,"style":1697},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Regional report",[52,1700,1701],{"x":1689,"y":1657,"style":107},"a title banner",[52,1703,1704],{"x":1689,"y":54,"style":1697},"exclude, do not fill",[10,1706,1707,1708,1711],{},"A group header spanning three columns leaves two ",[14,1709,1710],{},"Unnamed:"," names. Read the header as a list of rows and forward-fill the upper level across:",[192,1713,1715],{"className":223,"code":1714,"language":225,"meta":197,"style":197},"import pandas as pd\n\nraw = pd.read_excel(\"quarterly.xlsx\", header=[0, 1])\n\ngroups = (\n    pd.Series([None if str(a).startswith(\"Unnamed:\") else a\n               for a, _ in raw.columns])\n    .ffill()                       # spread the group name rightwards\n)\ndetails = [b for _, b in raw.columns]\n\nraw.columns = [\n    f\"{g}_{d}\" if pd.notna(g) else str(d) for g, d in zip(groups, details)\n]\nprint(raw.columns.tolist())\n# ['Q1_Units', 'Q1_Revenue', 'Q1_Margin', 'Q2_Units', ...]\n",[14,1716,1717,1727,1731,1762,1766,1776,1802,1815,1823,1827,1847,1851,1860,1911,1915,1922],{"__ignoreMap":197},[201,1718,1719,1721,1723,1725],{"class":123,"line":203},[201,1720,240],{"class":232},[201,1722,925],{"class":236},[201,1724,928],{"class":232},[201,1726,931],{"class":236},[201,1728,1729],{"class":123,"line":246},[201,1730,250],{"emptyLinePlaceholder":249},[201,1732,1733,1736,1738,1740,1743,1745,1748,1750,1753,1755,1757,1760],{"class":123,"line":253},[201,1734,1735],{"class":236},"raw ",[201,1737,259],{"class":232},[201,1739,945],{"class":236},[201,1741,1742],{"class":210},"\"quarterly.xlsx\"",[201,1744,285],{"class":236},[201,1746,1747],{"class":1157},"header",[201,1749,259],{"class":232},[201,1751,1752],{"class":236},"[",[201,1754,47],{"class":330},[201,1756,285],{"class":236},[201,1758,1759],{"class":330},"1",[201,1761,296],{"class":236},[201,1763,1764],{"class":123,"line":265},[201,1765,250],{"emptyLinePlaceholder":249},[201,1767,1768,1771,1773],{"class":123,"line":276},[201,1769,1770],{"class":236},"groups ",[201,1772,259],{"class":232},[201,1774,1775],{"class":236}," (\n",[201,1777,1778,1781,1783,1785,1788,1791,1794,1797,1799],{"class":123,"line":299},[201,1779,1780],{"class":236},"    pd.Series([",[201,1782,337],{"class":330},[201,1784,727],{"class":232},[201,1786,1787],{"class":330}," str",[201,1789,1790],{"class":236},"(a).startswith(",[201,1792,1793],{"class":210},"\"Unnamed:\"",[201,1795,1796],{"class":236},") ",[201,1798,639],{"class":232},[201,1800,1801],{"class":236}," a\n",[201,1803,1804,1807,1810,1812],{"class":123,"line":314},[201,1805,1806],{"class":232},"               for",[201,1808,1809],{"class":236}," a, _ ",[201,1811,308],{"class":232},[201,1813,1814],{"class":236}," raw.columns])\n",[201,1816,1817,1820],{"class":123,"line":353},[201,1818,1819],{"class":236},"    .ffill()                       ",[201,1821,1822],{"class":435},"# spread the group name rightwards\n",[201,1824,1825],{"class":123,"line":387},[201,1826,461],{"class":236},[201,1828,1829,1832,1834,1837,1839,1842,1844],{"class":123,"line":406},[201,1830,1831],{"class":236},"details ",[201,1833,259],{"class":232},[201,1835,1836],{"class":236}," [b ",[201,1838,302],{"class":232},[201,1840,1841],{"class":236}," _, b ",[201,1843,308],{"class":232},[201,1845,1846],{"class":236}," raw.columns]\n",[201,1848,1849],{"class":123,"line":412},[201,1850,250],{"emptyLinePlaceholder":249},[201,1852,1853,1856,1858],{"class":123,"line":418},[201,1854,1855],{"class":236},"raw.columns ",[201,1857,259],{"class":232},[201,1859,311],{"class":236},[201,1861,1862,1865,1867,1869,1872,1874,1877,1879,1882,1884,1886,1888,1891,1893,1895,1898,1900,1903,1905,1908],{"class":123,"line":423},[201,1863,1864],{"class":232},"    f",[201,1866,842],{"class":210},[201,1868,846],{"class":845},[201,1870,1871],{"class":236},"g",[201,1873,861],{"class":845},[201,1875,1876],{"class":210},"_",[201,1878,846],{"class":845},[201,1880,1881],{"class":236},"d",[201,1883,861],{"class":845},[201,1885,842],{"class":210},[201,1887,727],{"class":232},[201,1889,1890],{"class":236}," pd.notna(g) ",[201,1892,639],{"class":232},[201,1894,1787],{"class":330},[201,1896,1897],{"class":236},"(d) ",[201,1899,302],{"class":232},[201,1901,1902],{"class":236}," g, d ",[201,1904,308],{"class":232},[201,1906,1907],{"class":330}," zip",[201,1909,1910],{"class":236},"(groups, details)\n",[201,1912,1913],{"class":123,"line":439},[201,1914,1043],{"class":236},[201,1916,1917,1919],{"class":123,"line":452},[201,1918,833],{"class":330},[201,1920,1921],{"class":236},"(raw.columns.tolist())\n",[201,1923,1924],{"class":123,"line":777},[201,1925,1926],{"class":435},"# ['Q1_Units', 'Q1_Revenue', 'Q1_Margin', 'Q2_Units', ...]\n",[10,1928,1929,1930,1932],{},"This is the horizontal mirror of the vertical fill: the merge stored ",[14,1931,1677],{}," once, so the two columns to its right inherit it.",[187,1934,1936],{"id":1935},"common-pitfalls-and-fixes","Common pitfalls and fixes",[1938,1939,1940,1956],"table",{},[1941,1942,1943],"thead",{},[1944,1945,1946,1950,1953],"tr",{},[1947,1948,1949],"th",{},"Symptom",[1947,1951,1952],{},"Cause",[1947,1954,1955],{},"Fix",[1957,1958,1959,1976,1989,2006,2023,2037,2048,2065],"tbody",{},[1944,1960,1961,1967,1970],{},[1962,1963,1964,1965],"td",{},"Label column full of ",[14,1966,16],{},[1962,1968,1969],{},"Vertical merges",[1962,1971,1972,1975],{},[14,1973,1974],{},"ffill()"," on that column only.",[1944,1977,1978,1981,1986],{},[1962,1979,1980],{},"Numeric gaps filled with the row above",[1962,1982,1983,1984],{},"Blanket ",[14,1985,1011],{},[1962,1987,1988],{},"Fill named label columns only.",[1944,1990,1991,1996,1999],{},[1962,1992,1993,1994],{},"Header half ",[14,1995,1710],{},[1962,1997,1998],{},"Horizontal merges in the header",[1962,2000,2001,2002,2005],{},"Read ",[14,2003,2004],{},"header=[0,1]"," and fill the upper level.",[1944,2007,2008,2013,2016],{},[1962,2009,2010,2011],{},"First row's label is ",[14,2012,16],{},[1962,2014,2015],{},"Header index wrong",[1962,2017,2018,2019,2022],{},"Peek with ",[14,2020,2021],{},"header=None"," and fix the index.",[1944,2024,2025,2031,2034],{},[1962,2026,2027,2030],{},[14,2028,2029],{},"RuntimeError"," while unmerging",[1962,2032,2033],{},"Iterating the live ranges collection",[1962,2035,2036],{},"Snapshot the refs into a list first.",[1944,2038,2039,2042,2045],{},[1962,2040,2041],{},"Value lost after unmerging",[1962,2043,2044],{},"Unmerged before reading the value",[1962,2046,2047],{},"Capture the top-left value first.",[1944,2049,2050,2053,2059],{},[1962,2051,2052],{},"Fill spans far too many rows",[1962,2054,2055,2056,2058],{},"Unbounded ",[14,2057,912],{}," over a real gap",[1962,2060,2061,2062,2064],{},"Pass a ",[14,2063,1158],{},", then check what remains.",[1944,2066,2067,2072,2075],{},[1962,2068,2069],{},[14,2070,2071],{},"MergedCell is read-only",[1962,2073,2074],{},"Writing to a non-anchor cell of a merge",[1962,2076,2077],{},"Unmerge the range first, then write.",[187,2079,2081],{"id":2080},"performance-and-scale-notes","Performance and scale notes",[10,2083,2084,2085,2087,2088,2091],{},"Merge handling costs little in pandas — ",[14,2086,912],{}," is a vectorised pass — but the openpyxl side is where a large workbook can hurt. ",[14,2089,2090],{},"unmerge_and_fill"," writes a value into every cell of every former range, and on a sheet with tens of thousands of small merges that is a lot of individual cell assignments.",[10,2093,2094,2095,2098,2099,2101],{},"Two ways to keep it manageable. ",[886,2096,2097],{},"Do the fill in pandas rather than in the workbook"," when you only need the data, not a flattened file. A single ",[14,2100,912],{}," over a column is orders of magnitude faster than writing the same values cell by cell:",[192,2103,2105],{"className":223,"code":2104,"language":225,"meta":197,"style":197},"import pandas as pd\n\n# Fast: one vectorised pass, no workbook rewrite.\ndf = pd.read_excel(\"merged.xlsx\")\ndf[[\"Region\"]] = df[[\"Region\"]].ffill()\n",[14,2106,2107,2117,2121,2126,2138],{"__ignoreMap":197},[201,2108,2109,2111,2113,2115],{"class":123,"line":203},[201,2110,240],{"class":232},[201,2112,925],{"class":236},[201,2114,928],{"class":232},[201,2116,931],{"class":236},[201,2118,2119],{"class":123,"line":246},[201,2120,250],{"emptyLinePlaceholder":249},[201,2122,2123],{"class":123,"line":253},[201,2124,2125],{"class":435},"# Fast: one vectorised pass, no workbook rewrite.\n",[201,2127,2128,2130,2132,2134,2136],{"class":123,"line":265},[201,2129,940],{"class":236},[201,2131,259],{"class":232},[201,2133,945],{"class":236},[201,2135,458],{"class":210},[201,2137,461],{"class":236},[201,2139,2140,2143,2145,2148,2150,2153,2155],{"class":123,"line":276},[201,2141,2142],{"class":236},"df[[",[201,2144,282],{"class":210},[201,2146,2147],{"class":236},"]] ",[201,2149,259],{"class":232},[201,2151,2152],{"class":236}," df[[",[201,2154,282],{"class":210},[201,2156,2157],{"class":236},"]].ffill()\n",[10,2159,2160,2163],{},[886,2161,2162],{},"Restrict the flatten to the columns that need it"," when you do want a flattened file. Most sheets merge one or two label columns and nothing else, so filtering the ranges first avoids touching the rest:",[192,2165,2167],{"className":223,"code":2166,"language":225,"meta":197,"style":197},"from openpyxl.utils import column_index_from_string, range_boundaries\n\ndef unmerge_columns(ws, letters):\n    \"\"\"Flatten merges only in the named columns.\"\"\"\n    wanted = {column_index_from_string(c) for c in letters}\n    for ref in [str(r) for r in ws.merged_cells.ranges]:\n        min_col, min_row, max_col, max_row = range_boundaries(ref)\n        if min_col not in wanted:\n            continue\n        value = ws.cell(min_row, min_col).value\n        ws.unmerge_cells(ref)\n        for row in range(min_row, max_row + 1):\n            ws.cell(row, min_col, value)\n",[14,2168,2169,2180,2184,2194,2199,2219,2241,2250,2267,2272,2281,2286,2304],{"__ignoreMap":197},[201,2170,2171,2173,2175,2177],{"class":123,"line":203},[201,2172,233],{"class":232},[201,2174,577],{"class":236},[201,2176,240],{"class":232},[201,2178,2179],{"class":236}," column_index_from_string, range_boundaries\n",[201,2181,2182],{"class":123,"line":246},[201,2183,250],{"emptyLinePlaceholder":249},[201,2185,2186,2188,2191],{"class":123,"line":253},[201,2187,591],{"class":232},[201,2189,2190],{"class":594}," unmerge_columns",[201,2192,2193],{"class":236},"(ws, letters):\n",[201,2195,2196],{"class":123,"line":265},[201,2197,2198],{"class":210},"    \"\"\"Flatten merges only in the named columns.\"\"\"\n",[201,2200,2201,2204,2206,2209,2211,2214,2216],{"class":123,"line":276},[201,2202,2203],{"class":236},"    wanted ",[201,2205,259],{"class":232},[201,2207,2208],{"class":236}," {column_index_from_string(c) ",[201,2210,302],{"class":232},[201,2212,2213],{"class":236}," c ",[201,2215,308],{"class":232},[201,2217,2218],{"class":236}," letters}\n",[201,2220,2221,2223,2225,2227,2229,2231,2233,2235,2237,2239],{"class":123,"line":299},[201,2222,677],{"class":232},[201,2224,1438],{"class":236},[201,2226,308],{"class":232},[201,2228,1033],{"class":236},[201,2230,771],{"class":330},[201,2232,1447],{"class":236},[201,2234,302],{"class":232},[201,2236,1452],{"class":236},[201,2238,308],{"class":232},[201,2240,1457],{"class":236},[201,2242,2243,2246,2248],{"class":123,"line":314},[201,2244,2245],{"class":236},"        min_col, min_row, max_col, max_row ",[201,2247,259],{"class":232},[201,2249,1467],{"class":236},[201,2251,2252,2255,2258,2261,2264],{"class":123,"line":353},[201,2253,2254],{"class":232},"        if",[201,2256,2257],{"class":236}," min_col ",[201,2259,2260],{"class":232},"not",[201,2262,2263],{"class":232}," in",[201,2265,2266],{"class":236}," wanted:\n",[201,2268,2269],{"class":123,"line":387},[201,2270,2271],{"class":232},"            continue\n",[201,2273,2274,2277,2279],{"class":123,"line":406},[201,2275,2276],{"class":236},"        value ",[201,2278,259],{"class":232},[201,2280,1477],{"class":236},[201,2282,2283],{"class":123,"line":412},[201,2284,2285],{"class":236},"        ws.unmerge_cells(ref)\n",[201,2287,2288,2290,2292,2294,2296,2298,2300,2302],{"class":123,"line":418},[201,2289,1435],{"class":232},[201,2291,305],{"class":236},[201,2293,308],{"class":232},[201,2295,1498],{"class":330},[201,2297,1501],{"class":236},[201,2299,1504],{"class":232},[201,2301,1507],{"class":330},[201,2303,605],{"class":236},[201,2305,2306],{"class":123,"line":423},[201,2307,2308],{"class":236},"            ws.cell(row, min_col, value)\n",[10,2310,2311,2312,2315,2316,2318,2319,2323,2324,2328],{},"One structural note: merged cells cannot be read at all in openpyxl's ",[14,2313,2314],{},"read_only"," mode — the merge definitions are not materialised, so ",[14,2317,1609],{}," comes back empty. That means the fast streaming path described in ",[19,2320,2322],{"href":2321},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fspeed-up-openpyxl-with-read-only-mode\u002F","speeding up openpyxl with read-only mode"," cannot detect merges, and a large merged workbook must be opened normally at least once. The pragmatic answer for a recurring feed is to flatten it once at ingest, as described in ",[19,2325,2327],{"href":2326},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002F","handling Excel file formats and conversions",", and let everything downstream read a rectangular file at full speed.",[187,2330,2332],{"id":2331},"conclusion","Conclusion",[10,2334,2335,2336,2339,2340,2342],{},"Merged cells are not a pandas bug — Excel really does store the value once and leave the rest of the range empty. Confirm with openpyxl's ",[14,2337,2338],{},"merged_cells.ranges"," that the blanks come from merges rather than missing data, then forward-fill the specific label columns, with a ",[14,2341,1158],{}," so a real gap cannot propagate a label down the whole sheet. For a file that arrives every month, flatten it once with unmerge-and-fill and write to a copy, so every downstream reader sees a rectangular sheet and no special case is needed again.",[187,2344,2346],{"id":2345},"frequently-asked-questions","Frequently asked questions",[10,2348,2349,2355],{},[886,2350,2351,2352,2354],{},"Why do merged cells come back as ",[14,2353,16],{}," in pandas?","\nExcel stores a merged range's value in its top-left cell only; every other cell in the range is genuinely empty in the file. pandas reads what is there, so you get one value followed by blanks.",[10,2357,2358,2364],{},[886,2359,2360,2361,2363],{},"Is ",[14,2362,912],{}," always the right fix?","\nOnly for vertical merges in a label column, and only after you have confirmed the blanks come from merges rather than from genuinely missing data. Forward-filling real gaps invents values, which is worse than leaving them blank.",[10,2366,2367,2370,2371,2373],{},[886,2368,2369],{},"How do I see which ranges are merged?","\nOpen the workbook with openpyxl and read ",[14,2372,1609],{},". It gives every merged range as a coordinate string, which you can group by orientation to see whether the merges are vertical labels or horizontal headers.",[10,2375,2376,2379,2380,2383],{},[886,2377,2378],{},"Can I unmerge without opening Excel?","\nYes. openpyxl's ",[14,2381,2382],{},"unmerge_cells"," removes the merge, and you then write the top-left value into every cell of the former range so the data survives. Do it on a copy, not the original.",[10,2385,2386,2392,2393,2395,2396,2398],{},[886,2387,2388,2389,2391],{},"My header row is half ",[14,2390,1710],{}," — is that merged cells too?","\nAlmost certainly. A group header spanning three columns stores its text once, so the two columns to its right read as blank and pandas names them ",[14,2394,1710],{},". Read with ",[14,2397,1747],{}," set to a list and forward-fill the upper level.",[187,2400,2402],{"id":2401},"related","Related",[2404,2405,2406,2413,2419,2426,2433],"ul",{},[2407,2408,2409,2410,2412],"li",{},"Up to the parent: ",[19,2411,22],{"href":21}," — the reading options merges interact with.",[2407,2414,2415,2418],{},[19,2416,2417],{"href":896},"Skip Rows and Set the Header When Reading Excel with pandas"," — the header side of the same problem.",[2407,2420,2421,2425],{},[19,2422,2424],{"href":2423},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fremove-blank-rows-from-excel-with-pandas\u002F","Remove Blank Rows from Excel with pandas"," — cleaning what is left after the fill.",[2407,2427,2428,2432],{},[19,2429,2431],{"href":2430},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ffill-missing-values-in-excel-with-pandas-fillna\u002F","Fill Missing Values in Excel with pandas fillna"," — when the blanks are genuinely missing data.",[2407,2434,2435,2439],{},[19,2436,2438],{"href":2437},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fmerge-cells-and-centre-a-report-title-with-openpyxl\u002F","Merge Cells and Centre a Report Title with openpyxl"," — creating merges deliberately, on output.",[2441,2442,2443],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":197,"searchDepth":246,"depth":246,"links":2445},[2446,2447,2448,2449,2450,2451,2452,2453,2454,2455],{"id":189,"depth":246,"text":190},{"id":464,"depth":246,"text":465},{"id":905,"depth":246,"text":906},{"id":1216,"depth":246,"text":1217},{"id":1621,"depth":246,"text":1622},{"id":1935,"depth":246,"text":1936},{"id":2080,"depth":246,"text":2081},{"id":2331,"depth":246,"text":2332},{"id":2345,"depth":246,"text":2346},{"id":2401,"depth":246,"text":2402},"2026-08-15","Merged cells read as one value and a run of blanks. Fix them in pandas with forward fill, detect merges with openpyxl, and unmerge a workbook before importing it.","md",[2460,2463,2466,2468,2470],{"q":2461,"a":2462},"Why do merged cells come back as NaN in pandas?","Excel stores a merged range's value in its top-left cell only; every other cell in the range is genuinely empty in the file. pandas reads what is there, so you get one value followed by blanks.",{"q":2464,"a":2465},"Is ffill always the right fix?","Only for vertical merges in a label column, and only after you have confirmed the blanks come from merges rather than from genuinely missing data. Forward-filling real gaps invents values, which is worse than leaving them blank.",{"q":2369,"a":2467},"Open the workbook with openpyxl and read ws.merged_cells.ranges. It gives every merged range as a coordinate string, which you can group by orientation to see whether the merges are vertical labels or horizontal headers.",{"q":2378,"a":2469},"Yes. openpyxl's unmerge_cells removes the merge, and you then write the top-left value into every cell of the former range so the data survives. Do it on a copy, not the original.",{"q":2471,"a":2472},"My header row is half Unnamed — is that merged cells too?","Almost certainly. A group header spanning three columns stores its text once, so the two columns to its right read as blank and pandas names them Unnamed. Read with header set to a list and forward-fill the upper level.",{},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhandle-merged-cells-when-reading-excel-with-pandas",{"title":2476,"description":2477},"Read Excel Merged Cells with pandas (Fix the Blanks)","Why merged Excel cells produce NaN in pandas, how to forward-fill them safely, how to find merged ranges with openpyxl, and how to unmerge a sheet before importing.","handle-merged-cells-when-reading-excel-with-pandas","getting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhandle-merged-cells-when-reading-excel-with-pandas\u002Findex","how-to","ueCXqD5_1-JWd7NeeGInhOFB1RJWCUtzNBVzju2F1O4",[2483,2487],{"title":2484,"path":2485,"stem":2486,"children":-1},"Reading Excel Files with Pandas","\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas","getting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Findex",{"title":2488,"path":2489,"stem":2490,"children":-1},"How to Read Excel Files With Pandas: A Step-by-Step Guide","\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhow-to-read-excel-with-pandas-step-by-step","getting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhow-to-read-excel-with-pandas-step-by-step\u002Findex",1786800027225]