[{"data":1,"prerenderedAt":2163},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-grouped-data-with-blank-rows-between-groups":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-grouped-data-with-blank-rows-between-groups":2129,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-grouped-data-with-blank-rows-between-groups":2138,"children:\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-grouped-data-with-blank-rows-between-groups":2143,"related:\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-grouped-data-with-blank-rows-between-groups":2144},{"id":4,"title":5,"body":6,"dateModified":2094,"datePublished":2094,"description":2095,"extension":2096,"faq":2097,"keyword":2106,"kind":2107,"meta":2108,"navigation":95,"path":2118,"seo":2119,"slug":2122,"stem":2123,"topics":2124,"type":2127,"__hash__":2128},"docs\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-grouped-data-with-blank-rows-between-groups\u002Findex.md","Write Grouped Data with Blank Rows Between Groups",{"type":7,"value":8,"toc":2082},"minimark",[9,27,32,60,63,67,491,507,730,734,1500,1527,1531,1598,1609,1678,1682,1757,1763,1931,1935,1950,1954,1993,1997,2003,2007,2017,2029,2042,2048,2052,2079],[10,11,12,16,17,20,21,26],"p",{},[13,14,15],"code",{},"df.to_excel()"," writes one long table, which is right for data that other programs will read and wrong for a report that people will read. A printed list of three hundred orders is far easier to follow when it is broken up by region: a heading for each region, its orders, a subtotal, a blank line, then the next region. People often get there by writing the DataFrame and then inserting rows with openpyxl, which is slow, shifts every row below each insert and breaks any formula already written. The better approach is to plan the layout first — work out which row each heading, detail line, subtotal and gap goes in — and then write everything in a single pass. This page does that with pandas and openpyxl, uses live ",[13,18,19],{},"SUBTOTAL"," formulas so the totals stay correct if a figure is edited, adds a grand total that does not count the subtotals twice, groups the detail rows into collapsible outline levels, and checks the result by recalculating it. It belongs to ",[22,23,25],"a",{"href":24},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002F","Writing DataFrames to Excel with pandas",".",[28,29,31],"h2",{"id":30},"prerequisites","Prerequisites",[33,34,39],"pre",{"className":35,"code":36,"language":37,"meta":38,"style":38},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[13,40,41],{"__ignoreMap":38},[42,43,46,50,54,57],"span",{"class":44,"line":45},"line",1,[42,47,49],{"class":48},"sMTad","pip",[42,51,53],{"class":52},"srMev"," install",[42,55,56],{"class":52}," pandas",[42,58,59],{"class":52}," openpyxl\n",[10,61,62],{},"The input is a flat list of fourteen orders with region, order number, customer, units and revenue. The report groups them by region.",[28,64,66],{"id":65},"step-1-plan-the-layout-before-writing-anything","Step 1 — Plan the layout before writing anything",[33,68,72],{"className":69,"code":70,"language":71,"meta":38,"style":38},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\norders = pd.read_excel(\"orders.xlsx\").sort_values([\"Region\", \"Order\"])\nCOLUMNS = [\"Order\", \"Customer\", \"Units\", \"Revenue\"]\nSUM_COLS = [\"Units\", \"Revenue\"]\n\nlayout, r = [], 2                                   # row 1 is the column header\nfor region, g in orders.groupby(\"Region\", sort=True):\n    layout.append((\"heading\", r, region)); r += 1\n    first = r\n    for rec in g[COLUMNS].itertuples(index=False):\n        layout.append((\"detail\", r, rec)); r += 1\n    layout.append((\"subtotal\", r, (region, first, r - 1))); r += 1\n    layout.append((\"blank\", r, None)); r += 1\nlayout.append((\"grand\", r, (2, r - 1)))\nprint(f\"{len(orders)} orders in {orders['Region'].nunique()} groups -> {r} rows\")  # header + 3 rows per group + details + grand total\nfor kind, row, payload in layout[:6]:\n    print(row, kind, payload if kind != \"detail\" else tuple(payload)[:2])\n","python",[13,73,74,90,97,127,160,178,183,201,232,250,261,290,306,330,351,375,438,457],{"__ignoreMap":38},[42,75,76,80,84,87],{"class":44,"line":45},[42,77,79],{"class":78},"s-kum","import",[42,81,83],{"class":82},"skGVy"," pandas ",[42,85,86],{"class":78},"as",[42,88,89],{"class":82}," pd\n",[42,91,93],{"class":44,"line":92},2,[42,94,96],{"emptyLinePlaceholder":95},true,"\n",[42,98,100,103,106,109,112,115,118,121,124],{"class":44,"line":99},3,[42,101,102],{"class":82},"orders ",[42,104,105],{"class":78},"=",[42,107,108],{"class":82}," pd.read_excel(",[42,110,111],{"class":52},"\"orders.xlsx\"",[42,113,114],{"class":82},").sort_values([",[42,116,117],{"class":52},"\"Region\"",[42,119,120],{"class":82},", ",[42,122,123],{"class":52},"\"Order\"",[42,125,126],{"class":82},"])\n",[42,128,130,134,137,140,142,144,147,149,152,154,157],{"class":44,"line":129},4,[42,131,133],{"class":132},"sP0c6","COLUMNS",[42,135,136],{"class":78}," =",[42,138,139],{"class":82}," [",[42,141,123],{"class":52},[42,143,120],{"class":82},[42,145,146],{"class":52},"\"Customer\"",[42,148,120],{"class":82},[42,150,151],{"class":52},"\"Units\"",[42,153,120],{"class":82},[42,155,156],{"class":52},"\"Revenue\"",[42,158,159],{"class":82},"]\n",[42,161,163,166,168,170,172,174,176],{"class":44,"line":162},5,[42,164,165],{"class":132},"SUM_COLS",[42,167,136],{"class":78},[42,169,139],{"class":82},[42,171,151],{"class":52},[42,173,120],{"class":82},[42,175,156],{"class":52},[42,177,159],{"class":82},[42,179,181],{"class":44,"line":180},6,[42,182,96],{"emptyLinePlaceholder":95},[42,184,186,189,191,194,197],{"class":44,"line":185},7,[42,187,188],{"class":82},"layout, r ",[42,190,105],{"class":78},[42,192,193],{"class":82}," [], ",[42,195,196],{"class":132},"2",[42,198,200],{"class":199},"s-wDw","                                   # row 1 is the column header\n",[42,202,204,207,210,213,216,218,220,224,226,229],{"class":44,"line":203},8,[42,205,206],{"class":78},"for",[42,208,209],{"class":82}," region, g ",[42,211,212],{"class":78},"in",[42,214,215],{"class":82}," orders.groupby(",[42,217,117],{"class":52},[42,219,120],{"class":82},[42,221,223],{"class":222},"sa561","sort",[42,225,105],{"class":78},[42,227,228],{"class":132},"True",[42,230,231],{"class":82},"):\n",[42,233,235,238,241,244,247],{"class":44,"line":234},9,[42,236,237],{"class":82},"    layout.append((",[42,239,240],{"class":52},"\"heading\"",[42,242,243],{"class":82},", r, region)); r ",[42,245,246],{"class":78},"+=",[42,248,249],{"class":132}," 1\n",[42,251,253,256,258],{"class":44,"line":252},10,[42,254,255],{"class":82},"    first ",[42,257,105],{"class":78},[42,259,260],{"class":82}," r\n",[42,262,264,267,270,272,275,277,280,283,285,288],{"class":44,"line":263},11,[42,265,266],{"class":78},"    for",[42,268,269],{"class":82}," rec ",[42,271,212],{"class":78},[42,273,274],{"class":82}," g[",[42,276,133],{"class":132},[42,278,279],{"class":82},"].itertuples(",[42,281,282],{"class":222},"index",[42,284,105],{"class":78},[42,286,287],{"class":132},"False",[42,289,231],{"class":82},[42,291,293,296,299,302,304],{"class":44,"line":292},12,[42,294,295],{"class":82},"        layout.append((",[42,297,298],{"class":52},"\"detail\"",[42,300,301],{"class":82},", r, rec)); r ",[42,303,246],{"class":78},[42,305,249],{"class":132},[42,307,309,311,314,317,320,323,326,328],{"class":44,"line":308},13,[42,310,237],{"class":82},[42,312,313],{"class":52},"\"subtotal\"",[42,315,316],{"class":82},", r, (region, first, r ",[42,318,319],{"class":78},"-",[42,321,322],{"class":132}," 1",[42,324,325],{"class":82},"))); r ",[42,327,246],{"class":78},[42,329,249],{"class":132},[42,331,333,335,338,341,344,347,349],{"class":44,"line":332},14,[42,334,237],{"class":82},[42,336,337],{"class":52},"\"blank\"",[42,339,340],{"class":82},", r, ",[42,342,343],{"class":132},"None",[42,345,346],{"class":82},")); r ",[42,348,246],{"class":78},[42,350,249],{"class":132},[42,352,354,357,360,363,365,368,370,372],{"class":44,"line":353},15,[42,355,356],{"class":82},"layout.append((",[42,358,359],{"class":52},"\"grand\"",[42,361,362],{"class":82},", r, (",[42,364,196],{"class":132},[42,366,367],{"class":82},", r ",[42,369,319],{"class":78},[42,371,322],{"class":132},[42,373,374],{"class":82},")))\n",[42,376,378,381,384,387,390,394,397,400,403,406,408,411,414,417,419,422,424,427,429,432,435],{"class":44,"line":377},16,[42,379,380],{"class":132},"print",[42,382,383],{"class":82},"(",[42,385,386],{"class":78},"f",[42,388,389],{"class":52},"\"",[42,391,393],{"class":392},"sSjpA","{",[42,395,396],{"class":132},"len",[42,398,399],{"class":82},"(orders)",[42,401,402],{"class":392},"}",[42,404,405],{"class":52}," orders in ",[42,407,393],{"class":392},[42,409,410],{"class":82},"orders[",[42,412,413],{"class":52},"'Region'",[42,415,416],{"class":82},"].nunique()",[42,418,402],{"class":392},[42,420,421],{"class":52}," groups -> ",[42,423,393],{"class":392},[42,425,426],{"class":82},"r",[42,428,402],{"class":392},[42,430,431],{"class":52}," rows\"",[42,433,434],{"class":82},")  ",[42,436,437],{"class":199},"# header + 3 rows per group + details + grand total\n",[42,439,441,443,446,448,451,454],{"class":44,"line":440},17,[42,442,206],{"class":78},[42,444,445],{"class":82}," kind, row, payload ",[42,447,212],{"class":78},[42,449,450],{"class":82}," layout[:",[42,452,453],{"class":132},"6",[42,455,456],{"class":82},"]:\n",[42,458,460,463,466,469,472,475,478,481,484,487,489],{"class":44,"line":459},18,[42,461,462],{"class":132},"    print",[42,464,465],{"class":82},"(row, kind, payload ",[42,467,468],{"class":78},"if",[42,470,471],{"class":82}," kind ",[42,473,474],{"class":78},"!=",[42,476,477],{"class":52}," \"detail\"",[42,479,480],{"class":78}," else",[42,482,483],{"class":132}," tuple",[42,485,486],{"class":82},"(payload)[:",[42,488,196],{"class":132},[42,490,126],{"class":82},[10,492,493,494,498,499,502,503,506],{},"The plan is a list of ",[495,496,497],"em",{},"(kind, row, payload)"," entries built in one walk over the sorted groups. Each group contributes a heading row, its detail rows, a subtotal row and a blank row; the subtotal remembers the first and last detail row of its group, which is what its formula will need. The row counter is the only state, so the positions are correct by construction — there is no inserting and no shifting. Sorting by region and then by order number first makes the report predictable from run to run. The same plan could be written by any library; separating ",[495,500,501],{},"where things go"," from ",[495,504,505],{},"how they are written"," is what keeps the code simple. For fourteen orders in three regions the plan has 25 rows: the column header, three extra rows per group for the heading, subtotal and gap, the fourteen detail rows and the grand total. East, with six orders, occupies rows 2 to 10.",[508,509,517,518,517,522,517,526,517,534,517,543,517,547,517,554,517,558,517,562,517,566,517,570,517,574,517,579,517,584,517,588,517,590,517,593,517,595,517,598,517,601,517,604,517,606,517,609,517,611,517,614,517,616,517,619,517,622,517,626,517,628,517,630,517,634,517,638,517,640,517,643,517,646,517,650,517,652,517,655,517,657,517,660,517,662,517,665,517,668,517,672,517,674,517,677,517,681,517,685,517,687,517,690,517,693,517,696,517,698,517,701,517,703,517,707,517,709,517,712,517,716,517,718,517,721,517,723,517,725,517,727],"svg",{"viewBox":510,"role":511,"ariaLabelledBy":512,"xmlns":515,"style":516},"171 0 418 252","img",[513,514],"grp-layout-t","grp-layout-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:418px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[519,520,521],"title",{"id":513},"The planned rows for the first group",[523,524,525],"desc",{"id":514},"Row 1 holds the column headings. The East group starts with a heading row, then its detail rows, a subtotal row with SUBTOTAL formulas for units and revenue, and a blank row before the next group's heading.",[527,528],"rect",{"x":529,"y":530,"width":531,"height":532,"fill":533},"171","0","418","252","#ffffff",[527,535],{"x":536,"y":537,"width":538,"height":539,"rx":530,"fill":540,"stroke":541,"style":542},"187.4","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[527,544],{"x":545,"y":537,"width":546,"height":539,"rx":530,"fill":540,"stroke":541,"style":542},"219.4","50.7",[548,549,553],"text",{"x":550,"y":551,"style":552},"244.8","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[527,555],{"x":556,"y":537,"width":557,"height":539,"rx":530,"fill":540,"stroke":541,"style":542},"270.1","74.5",[548,559,561],{"x":560,"y":551,"style":552},"307.4","B",[527,563],{"x":564,"y":537,"width":565,"height":539,"rx":530,"fill":540,"stroke":541,"style":542},"344.7","227.9",[548,567,569],{"x":568,"y":551,"style":552},"458.6","C",[527,571],{"x":536,"y":572,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},"40.0","28.0",[548,575,578],{"x":576,"y":577,"style":552},"203.4","59.0","1",[527,580],{"x":545,"y":572,"width":546,"height":573,"rx":530,"fill":581,"stroke":582,"style":583},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[548,585,587],{"x":550,"y":577,"style":586},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Row",[527,589],{"x":556,"y":572,"width":557,"height":573,"rx":530,"fill":581,"stroke":582,"style":583},[548,591,592],{"x":560,"y":577,"style":586},"Kind",[527,594],{"x":564,"y":572,"width":565,"height":573,"rx":530,"fill":581,"stroke":582,"style":583},[548,596,597],{"x":568,"y":577,"style":586},"Content",[527,599],{"x":536,"y":600,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},"68.0",[548,602,196],{"x":576,"y":603,"style":552},"87.0",[527,605],{"x":545,"y":600,"width":546,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,607,578],{"x":550,"y":603,"style":608},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[527,610],{"x":556,"y":600,"width":557,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,612,613],{"x":560,"y":603,"style":608},"header",[527,615],{"x":564,"y":600,"width":565,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,617,618],{"x":568,"y":603,"style":608},"Order, Customer, Units, Revenue",[527,620],{"x":536,"y":621,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},"96.0",[548,623,625],{"x":576,"y":624,"style":552},"115.0","3",[527,627],{"x":545,"y":621,"width":546,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,629,196],{"x":550,"y":624,"style":608},[527,631],{"x":556,"y":621,"width":557,"height":573,"rx":530,"fill":632,"stroke":633,"style":583},"#d9f4f1","var(--teal,#0f9488)",[548,635,637],{"x":560,"y":624,"style":636},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","heading",[527,639],{"x":564,"y":621,"width":565,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,641,642],{"x":568,"y":624,"style":608},"East",[527,644],{"x":536,"y":645,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},"124.0",[548,647,649],{"x":576,"y":648,"style":552},"143.0","4",[527,651],{"x":545,"y":645,"width":546,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,653,654],{"x":550,"y":648,"style":608},"3–8",[527,656],{"x":556,"y":645,"width":557,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,658,659],{"x":560,"y":648,"style":608},"detail",[527,661],{"x":564,"y":645,"width":565,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,663,664],{"x":568,"y":648,"style":608},"six East orders",[527,666],{"x":536,"y":667,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},"152.0",[548,669,671],{"x":576,"y":670,"style":552},"171.0","5",[527,673],{"x":545,"y":667,"width":546,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,675,676],{"x":550,"y":670,"style":608},"9",[527,678],{"x":556,"y":667,"width":557,"height":573,"rx":530,"fill":679,"stroke":680,"style":583},"#fdefd8","var(--gold,#b4740a)",[548,682,684],{"x":560,"y":670,"style":683},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","subtotal",[527,686],{"x":564,"y":667,"width":565,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,688,689],{"x":568,"y":670,"style":608},"=SUBTOTAL(9,C3:C8) …",[527,691],{"x":536,"y":692,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},"180.0",[548,694,453],{"x":576,"y":695,"style":552},"199.0",[527,697],{"x":545,"y":692,"width":546,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,699,700],{"x":550,"y":695,"style":608},"10",[527,702],{"x":556,"y":692,"width":557,"height":573,"rx":530,"fill":540,"stroke":541,"style":583},[548,704,706],{"x":560,"y":695,"style":705},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","blank",[527,708],{"x":564,"y":692,"width":565,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[527,710],{"x":536,"y":711,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},"208.0",[548,713,715],{"x":576,"y":714,"style":552},"227.0","7",[527,717],{"x":545,"y":711,"width":546,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,719,720],{"x":550,"y":714,"style":608},"11",[527,722],{"x":556,"y":711,"width":557,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,724,637],{"x":560,"y":714,"style":608},[527,726],{"x":564,"y":711,"width":565,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,728,729],{"x":568,"y":714,"style":608},"North",[28,731,733],{"id":732},"step-2-write-the-plan-in-one-pass","Step 2 — Write the plan in one pass",[33,735,737],{"className":69,"code":736,"language":71,"meta":38,"style":38},"from openpyxl import Workbook\nfrom openpyxl.styles import Border, Font, PatternFill, Side\n\nwb = Workbook()\nws = wb.active\nws.title = \"Orders by region\"\nws.append(COLUMNS)\nfor cell in ws[1]:\n    cell.font = Font(bold=True, color=\"FFFFFF\")\n    cell.fill = PatternFill(\"solid\", fgColor=\"1F3864\")\ncol = {name: i + 1 for i, name in enumerate(COLUMNS)}\nletter = {name: ws.cell(1, i).column_letter for name, i in col.items()}\ntop = Border(top=Side(style=\"thin\"))\n\nfor kind, row, payload in layout:\n    if kind == \"heading\":\n        ws.cell(row, 1, payload).font = Font(bold=True, size=12, color=\"1F3864\")\n    elif kind == \"detail\":\n        for name, value in zip(COLUMNS, payload):\n            ws.cell(row, col[name], value)\n    elif kind == \"subtotal\":\n        region, first, last = payload\n        ws.cell(row, 1, f\"{region} total\").font = Font(bold=True)\n        for name in SUM_COLS:\n            c = ws.cell(row, col[name], f\"=SUBTOTAL(9,{letter[name]}{first}:{letter[name]}{last})\")\n            c.font, c.border = Font(bold=True), top\n    elif kind == \"grand\":\n        first, last = payload\n        ws.cell(row, 1, \"Grand total\").font = Font(bold=True, size=12)\n        for name in SUM_COLS:\n            c = ws.cell(row, col[name], f\"=SUBTOTAL(9,{letter[name]}{first}:{letter[name]}{last})\")\n            c.font = Font(bold=True, size=12)\n            c.border = Border(top=Side(style=\"thin\"), bottom=Side(style=\"double\"))\nfor row in ws.iter_rows(min_row=2, min_col=col[\"Revenue\"], max_col=col[\"Revenue\"]):\n    row[0].number_format = \"£#,##0\"\nws.column_dimensions[\"A\"].width, ws.column_dimensions[\"B\"].width = 14, 12\nws.freeze_panes = \"A2\"\n",[13,738,739,752,764,768,778,788,798,808,824,853,878,911,936,965,969,980,996,1036,1049,1070,1076,1090,1101,1139,1154,1202,1221,1235,1245,1279,1292,1331,1357,1398,1445,1461,1489],{"__ignoreMap":38},[42,740,741,744,747,749],{"class":44,"line":45},[42,742,743],{"class":78},"from",[42,745,746],{"class":82}," openpyxl ",[42,748,79],{"class":78},[42,750,751],{"class":82}," Workbook\n",[42,753,754,756,759,761],{"class":44,"line":92},[42,755,743],{"class":78},[42,757,758],{"class":82}," openpyxl.styles ",[42,760,79],{"class":78},[42,762,763],{"class":82}," Border, Font, PatternFill, Side\n",[42,765,766],{"class":44,"line":99},[42,767,96],{"emptyLinePlaceholder":95},[42,769,770,773,775],{"class":44,"line":129},[42,771,772],{"class":82},"wb ",[42,774,105],{"class":78},[42,776,777],{"class":82}," Workbook()\n",[42,779,780,783,785],{"class":44,"line":162},[42,781,782],{"class":82},"ws ",[42,784,105],{"class":78},[42,786,787],{"class":82}," wb.active\n",[42,789,790,793,795],{"class":44,"line":180},[42,791,792],{"class":82},"ws.title ",[42,794,105],{"class":78},[42,796,797],{"class":52}," \"Orders by region\"\n",[42,799,800,803,805],{"class":44,"line":185},[42,801,802],{"class":82},"ws.append(",[42,804,133],{"class":132},[42,806,807],{"class":82},")\n",[42,809,810,812,815,817,820,822],{"class":44,"line":203},[42,811,206],{"class":78},[42,813,814],{"class":82}," cell ",[42,816,212],{"class":78},[42,818,819],{"class":82}," ws[",[42,821,578],{"class":132},[42,823,456],{"class":82},[42,825,826,829,831,834,837,839,841,843,846,848,851],{"class":44,"line":234},[42,827,828],{"class":82},"    cell.font ",[42,830,105],{"class":78},[42,832,833],{"class":82}," Font(",[42,835,836],{"class":222},"bold",[42,838,105],{"class":78},[42,840,228],{"class":132},[42,842,120],{"class":82},[42,844,845],{"class":222},"color",[42,847,105],{"class":78},[42,849,850],{"class":52},"\"FFFFFF\"",[42,852,807],{"class":82},[42,854,855,858,860,863,866,868,871,873,876],{"class":44,"line":252},[42,856,857],{"class":82},"    cell.fill ",[42,859,105],{"class":78},[42,861,862],{"class":82}," PatternFill(",[42,864,865],{"class":52},"\"solid\"",[42,867,120],{"class":82},[42,869,870],{"class":222},"fgColor",[42,872,105],{"class":78},[42,874,875],{"class":52},"\"1F3864\"",[42,877,807],{"class":82},[42,879,880,883,885,888,891,893,896,899,901,904,906,908],{"class":44,"line":263},[42,881,882],{"class":82},"col ",[42,884,105],{"class":78},[42,886,887],{"class":82}," {name: i ",[42,889,890],{"class":78},"+",[42,892,322],{"class":132},[42,894,895],{"class":78}," for",[42,897,898],{"class":82}," i, name ",[42,900,212],{"class":78},[42,902,903],{"class":132}," enumerate",[42,905,383],{"class":82},[42,907,133],{"class":132},[42,909,910],{"class":82},")}\n",[42,912,913,916,918,921,923,926,928,931,933],{"class":44,"line":292},[42,914,915],{"class":82},"letter ",[42,917,105],{"class":78},[42,919,920],{"class":82}," {name: ws.cell(",[42,922,578],{"class":132},[42,924,925],{"class":82},", i).column_letter ",[42,927,206],{"class":78},[42,929,930],{"class":82}," name, i ",[42,932,212],{"class":78},[42,934,935],{"class":82}," col.items()}\n",[42,937,938,941,943,946,949,951,954,957,959,962],{"class":44,"line":308},[42,939,940],{"class":82},"top ",[42,942,105],{"class":78},[42,944,945],{"class":82}," Border(",[42,947,948],{"class":222},"top",[42,950,105],{"class":78},[42,952,953],{"class":82},"Side(",[42,955,956],{"class":222},"style",[42,958,105],{"class":78},[42,960,961],{"class":52},"\"thin\"",[42,963,964],{"class":82},"))\n",[42,966,967],{"class":44,"line":332},[42,968,96],{"emptyLinePlaceholder":95},[42,970,971,973,975,977],{"class":44,"line":353},[42,972,206],{"class":78},[42,974,445],{"class":82},[42,976,212],{"class":78},[42,978,979],{"class":82}," layout:\n",[42,981,982,985,987,990,993],{"class":44,"line":377},[42,983,984],{"class":78},"    if",[42,986,471],{"class":82},[42,988,989],{"class":78},"==",[42,991,992],{"class":52}," \"heading\"",[42,994,995],{"class":82},":\n",[42,997,998,1001,1003,1006,1008,1010,1012,1014,1016,1018,1021,1023,1026,1028,1030,1032,1034],{"class":44,"line":440},[42,999,1000],{"class":82},"        ws.cell(row, ",[42,1002,578],{"class":132},[42,1004,1005],{"class":82},", payload).font ",[42,1007,105],{"class":78},[42,1009,833],{"class":82},[42,1011,836],{"class":222},[42,1013,105],{"class":78},[42,1015,228],{"class":132},[42,1017,120],{"class":82},[42,1019,1020],{"class":222},"size",[42,1022,105],{"class":78},[42,1024,1025],{"class":132},"12",[42,1027,120],{"class":82},[42,1029,845],{"class":222},[42,1031,105],{"class":78},[42,1033,875],{"class":52},[42,1035,807],{"class":82},[42,1037,1038,1041,1043,1045,1047],{"class":44,"line":459},[42,1039,1040],{"class":78},"    elif",[42,1042,471],{"class":82},[42,1044,989],{"class":78},[42,1046,477],{"class":52},[42,1048,995],{"class":82},[42,1050,1052,1055,1058,1060,1063,1065,1067],{"class":44,"line":1051},19,[42,1053,1054],{"class":78},"        for",[42,1056,1057],{"class":82}," name, value ",[42,1059,212],{"class":78},[42,1061,1062],{"class":132}," zip",[42,1064,383],{"class":82},[42,1066,133],{"class":132},[42,1068,1069],{"class":82},", payload):\n",[42,1071,1073],{"class":44,"line":1072},20,[42,1074,1075],{"class":82},"            ws.cell(row, col[name], value)\n",[42,1077,1079,1081,1083,1085,1088],{"class":44,"line":1078},21,[42,1080,1040],{"class":78},[42,1082,471],{"class":82},[42,1084,989],{"class":78},[42,1086,1087],{"class":52}," \"subtotal\"",[42,1089,995],{"class":82},[42,1091,1093,1096,1098],{"class":44,"line":1092},22,[42,1094,1095],{"class":82},"        region, first, last ",[42,1097,105],{"class":78},[42,1099,1100],{"class":82}," payload\n",[42,1102,1104,1106,1108,1110,1112,1114,1116,1119,1121,1124,1127,1129,1131,1133,1135,1137],{"class":44,"line":1103},23,[42,1105,1000],{"class":82},[42,1107,578],{"class":132},[42,1109,120],{"class":82},[42,1111,386],{"class":78},[42,1113,389],{"class":52},[42,1115,393],{"class":392},[42,1117,1118],{"class":82},"region",[42,1120,402],{"class":392},[42,1122,1123],{"class":52}," total\"",[42,1125,1126],{"class":82},").font ",[42,1128,105],{"class":78},[42,1130,833],{"class":82},[42,1132,836],{"class":222},[42,1134,105],{"class":78},[42,1136,228],{"class":132},[42,1138,807],{"class":82},[42,1140,1142,1144,1147,1149,1152],{"class":44,"line":1141},24,[42,1143,1054],{"class":78},[42,1145,1146],{"class":82}," name ",[42,1148,212],{"class":78},[42,1150,1151],{"class":132}," SUM_COLS",[42,1153,995],{"class":82},[42,1155,1157,1160,1162,1165,1167,1170,1172,1175,1178,1181,1183,1186,1188,1190,1192,1195,1197,1200],{"class":44,"line":1156},25,[42,1158,1159],{"class":82},"            c ",[42,1161,105],{"class":78},[42,1163,1164],{"class":82}," ws.cell(row, col[name], ",[42,1166,386],{"class":78},[42,1168,1169],{"class":52},"\"=SUBTOTAL(9,",[42,1171,393],{"class":392},[42,1173,1174],{"class":82},"letter[name]",[42,1176,1177],{"class":392},"}{",[42,1179,1180],{"class":82},"first",[42,1182,402],{"class":392},[42,1184,1185],{"class":52},":",[42,1187,393],{"class":392},[42,1189,1174],{"class":82},[42,1191,1177],{"class":392},[42,1193,1194],{"class":82},"last",[42,1196,402],{"class":392},[42,1198,1199],{"class":52},")\"",[42,1201,807],{"class":82},[42,1203,1205,1208,1210,1212,1214,1216,1218],{"class":44,"line":1204},26,[42,1206,1207],{"class":82},"            c.font, c.border ",[42,1209,105],{"class":78},[42,1211,833],{"class":82},[42,1213,836],{"class":222},[42,1215,105],{"class":78},[42,1217,228],{"class":132},[42,1219,1220],{"class":82},"), top\n",[42,1222,1224,1226,1228,1230,1233],{"class":44,"line":1223},27,[42,1225,1040],{"class":78},[42,1227,471],{"class":82},[42,1229,989],{"class":78},[42,1231,1232],{"class":52}," \"grand\"",[42,1234,995],{"class":82},[42,1236,1238,1241,1243],{"class":44,"line":1237},28,[42,1239,1240],{"class":82},"        first, last ",[42,1242,105],{"class":78},[42,1244,1100],{"class":82},[42,1246,1248,1250,1252,1254,1257,1259,1261,1263,1265,1267,1269,1271,1273,1275,1277],{"class":44,"line":1247},29,[42,1249,1000],{"class":82},[42,1251,578],{"class":132},[42,1253,120],{"class":82},[42,1255,1256],{"class":52},"\"Grand total\"",[42,1258,1126],{"class":82},[42,1260,105],{"class":78},[42,1262,833],{"class":82},[42,1264,836],{"class":222},[42,1266,105],{"class":78},[42,1268,228],{"class":132},[42,1270,120],{"class":82},[42,1272,1020],{"class":222},[42,1274,105],{"class":78},[42,1276,1025],{"class":132},[42,1278,807],{"class":82},[42,1280,1282,1284,1286,1288,1290],{"class":44,"line":1281},30,[42,1283,1054],{"class":78},[42,1285,1146],{"class":82},[42,1287,212],{"class":78},[42,1289,1151],{"class":132},[42,1291,995],{"class":82},[42,1293,1295,1297,1299,1301,1303,1305,1307,1309,1311,1313,1315,1317,1319,1321,1323,1325,1327,1329],{"class":44,"line":1294},31,[42,1296,1159],{"class":82},[42,1298,105],{"class":78},[42,1300,1164],{"class":82},[42,1302,386],{"class":78},[42,1304,1169],{"class":52},[42,1306,393],{"class":392},[42,1308,1174],{"class":82},[42,1310,1177],{"class":392},[42,1312,1180],{"class":82},[42,1314,402],{"class":392},[42,1316,1185],{"class":52},[42,1318,393],{"class":392},[42,1320,1174],{"class":82},[42,1322,1177],{"class":392},[42,1324,1194],{"class":82},[42,1326,402],{"class":392},[42,1328,1199],{"class":52},[42,1330,807],{"class":82},[42,1332,1334,1337,1339,1341,1343,1345,1347,1349,1351,1353,1355],{"class":44,"line":1333},32,[42,1335,1336],{"class":82},"            c.font ",[42,1338,105],{"class":78},[42,1340,833],{"class":82},[42,1342,836],{"class":222},[42,1344,105],{"class":78},[42,1346,228],{"class":132},[42,1348,120],{"class":82},[42,1350,1020],{"class":222},[42,1352,105],{"class":78},[42,1354,1025],{"class":132},[42,1356,807],{"class":82},[42,1358,1360,1363,1365,1367,1369,1371,1373,1375,1377,1379,1382,1385,1387,1389,1391,1393,1396],{"class":44,"line":1359},33,[42,1361,1362],{"class":82},"            c.border ",[42,1364,105],{"class":78},[42,1366,945],{"class":82},[42,1368,948],{"class":222},[42,1370,105],{"class":78},[42,1372,953],{"class":82},[42,1374,956],{"class":222},[42,1376,105],{"class":78},[42,1378,961],{"class":52},[42,1380,1381],{"class":82},"), ",[42,1383,1384],{"class":222},"bottom",[42,1386,105],{"class":78},[42,1388,953],{"class":82},[42,1390,956],{"class":222},[42,1392,105],{"class":78},[42,1394,1395],{"class":52},"\"double\"",[42,1397,964],{"class":82},[42,1399,1401,1403,1406,1408,1411,1414,1416,1418,1420,1423,1425,1428,1430,1433,1436,1438,1440,1442],{"class":44,"line":1400},34,[42,1402,206],{"class":78},[42,1404,1405],{"class":82}," row ",[42,1407,212],{"class":78},[42,1409,1410],{"class":82}," ws.iter_rows(",[42,1412,1413],{"class":222},"min_row",[42,1415,105],{"class":78},[42,1417,196],{"class":132},[42,1419,120],{"class":82},[42,1421,1422],{"class":222},"min_col",[42,1424,105],{"class":78},[42,1426,1427],{"class":82},"col[",[42,1429,156],{"class":52},[42,1431,1432],{"class":82},"], ",[42,1434,1435],{"class":222},"max_col",[42,1437,105],{"class":78},[42,1439,1427],{"class":82},[42,1441,156],{"class":52},[42,1443,1444],{"class":82},"]):\n",[42,1446,1448,1451,1453,1456,1458],{"class":44,"line":1447},35,[42,1449,1450],{"class":82},"    row[",[42,1452,530],{"class":132},[42,1454,1455],{"class":82},"].number_format ",[42,1457,105],{"class":78},[42,1459,1460],{"class":52}," \"£#,##0\"\n",[42,1462,1464,1467,1470,1473,1476,1479,1481,1484,1486],{"class":44,"line":1463},36,[42,1465,1466],{"class":82},"ws.column_dimensions[",[42,1468,1469],{"class":52},"\"A\"",[42,1471,1472],{"class":82},"].width, ws.column_dimensions[",[42,1474,1475],{"class":52},"\"B\"",[42,1477,1478],{"class":82},"].width ",[42,1480,105],{"class":78},[42,1482,1483],{"class":132}," 14",[42,1485,120],{"class":82},[42,1487,1488],{"class":132},"12\n",[42,1490,1492,1495,1497],{"class":44,"line":1491},37,[42,1493,1494],{"class":82},"ws.freeze_panes ",[42,1496,105],{"class":78},[42,1498,1499],{"class":52}," \"A2\"\n",[10,1501,1502,1503,1506,1507,1510,1511,1513,1514,1517,1518,1520,1521,1523,1524,1526],{},"Writing is a loop over the plan with one branch per kind of row. Detail values are written as they are, so numbers stay numbers. The subtotal rows use ",[13,1504,1505],{},"SUBTOTAL(9, …)"," — function 9 is a sum — over their group's detail rows. The grand total uses the same function over the ",[495,1508,1509],{},"whole"," block, headings, subtotals and all, and that is the reason to use ",[13,1512,19],{}," rather than ",[13,1515,1516],{},"SUM",": ",[13,1519,19],{}," ignores cells that are themselves ",[13,1522,19],{}," results, so the grand total adds each order once even though the group subtotals sit inside its range. A ",[13,1525,1516],{}," over the same range would count everything twice. A thin line above each subtotal and a double line under the grand total follow the usual accounting convention. Because the totals are formulas rather than numbers computed in Python, someone who corrects an order in the sheet sees the subtotal and grand total update immediately.",[28,1528,1530],{"id":1529},"step-3-collapsible-groups","Step 3 — Collapsible groups",[33,1532,1534],{"className":69,"code":1533,"language":71,"meta":38,"style":38},"for kind, row, payload in layout:\n    if kind == \"detail\":\n        ws.row_dimensions[row].outline_level = 1\nws.sheet_properties.outlinePr.summaryBelow = True\nwb.save(\"orders_by_region.xlsx\")\nprint(\"saved\")\n",[13,1535,1536,1546,1558,1567,1577,1587],{"__ignoreMap":38},[42,1537,1538,1540,1542,1544],{"class":44,"line":45},[42,1539,206],{"class":78},[42,1541,445],{"class":82},[42,1543,212],{"class":78},[42,1545,979],{"class":82},[42,1547,1548,1550,1552,1554,1556],{"class":44,"line":92},[42,1549,984],{"class":78},[42,1551,471],{"class":82},[42,1553,989],{"class":78},[42,1555,477],{"class":52},[42,1557,995],{"class":82},[42,1559,1560,1563,1565],{"class":44,"line":99},[42,1561,1562],{"class":82},"        ws.row_dimensions[row].outline_level ",[42,1564,105],{"class":78},[42,1566,249],{"class":132},[42,1568,1569,1572,1574],{"class":44,"line":129},[42,1570,1571],{"class":82},"ws.sheet_properties.outlinePr.summaryBelow ",[42,1573,105],{"class":78},[42,1575,1576],{"class":132}," True\n",[42,1578,1579,1582,1585],{"class":44,"line":162},[42,1580,1581],{"class":82},"wb.save(",[42,1583,1584],{"class":52},"\"orders_by_region.xlsx\"",[42,1586,807],{"class":82},[42,1588,1589,1591,1593,1596],{"class":44,"line":180},[42,1590,380],{"class":132},[42,1592,383],{"class":82},[42,1594,1595],{"class":52},"\"saved\"",[42,1597,807],{"class":82},[10,1599,1600,1601,1604,1605,26],{},"Giving the detail rows an outline level of 1 adds Excel's grouping controls in the left margin: a reader can collapse every group to see just the headings and subtotals, then expand one region to see its orders. ",[13,1602,1603],{},"summaryBelow"," tells Excel that each group's summary row comes after its details, so the collapse button sits next to the subtotal. More on outlines in ",[22,1606,1608],{"href":1607},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fgroup-and-outline-rows-in-excel-with-openpyxl\u002F","Group and Outline Rows in Excel with openpyxl",[508,1610,517,1616,517,1619,517,1622,517,1626,517,1635,517,1641,517,1646,517,1650,517,1654,517,1658,517,1661,517,1666,517,1670,517,1672,517,1675],{"viewBox":1611,"role":511,"ariaLabelledBy":1612,"xmlns":515,"style":1615},"4 0 752 173",[1613,1614],"grp-sum-vs-subtotal-t","grp-sum-vs-subtotal-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[519,1617,1618],{"id":1613},"SUM or SUBTOTAL for the grand total",[523,1620,1621],{"id":1614},"A SUM over the whole block adds the detail rows and the group subtotals, so every order is counted twice. SUBTOTAL over the same block skips cells that are themselves SUBTOTAL results, so each order is counted once.",[527,1623],{"x":649,"y":530,"width":1624,"height":1625,"fill":533},"752","173",[527,1627],{"x":1628,"y":537,"width":1629,"height":1630,"rx":1631,"fill":1632,"stroke":1633,"style":1634},"20.0","351.0","141.0","14","#fce9e9","var(--danger,#dc2626)","stroke-width:2px",[548,1636,1640],{"x":1637,"y":1638,"style":1639},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","SUM over the block",[44,1642],{"x1":1643,"y1":1644,"x2":1645,"y2":1644,"stroke":1633,"style":542},"36.0","57.0","355.0",[548,1647,1649],{"x":1637,"y":1648,"style":608},"77.0","adds detail rows",[548,1651,1653],{"x":1637,"y":1652,"style":608},"103.0","adds group subtotals too",[548,1655,1657],{"x":1637,"y":1656,"style":608},"129.0","every order counted twice",[527,1659],{"x":1660,"y":537,"width":1629,"height":1630,"rx":1631,"fill":632,"stroke":633,"style":1634},"389.0",[548,1662,1665],{"x":1663,"y":1638,"style":1664},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","SUBTOTAL(9, …) over the block",[44,1667],{"x1":1668,"y1":1644,"x2":1669,"y2":1644,"stroke":633,"style":542},"405.0","724.0",[548,1671,1649],{"x":1663,"y":1648,"style":608},[548,1673,1674],{"x":1663,"y":1652,"style":608},"skips nested SUBTOTAL cells",[548,1676,1677],{"x":1663,"y":1656,"style":608},"every order counted once",[28,1679,1681],{"id":1680},"step-4-check-the-totals-by-recalculating","Step 4 — Check the totals by recalculating",[33,1683,1685],{"className":35,"code":1684,"language":37,"meta":38,"style":38},"soffice --headless --convert-to 'csv:Text - txt - csv (StarCalc):44,34,76,1,,0,false,true,true' orders_by_region.xlsx > \u002Fdev\u002Fnull 2>&1\ngrep -E \"total|^East|^North|^South\" orders_by_region.csv\npython -c \"\nimport pandas as pd\no = pd.read_excel('orders.xlsx')\nprint(o.groupby('Region')[['Units','Revenue']].sum().to_dict('index'))\nprint('all:', o[['Units','Revenue']].sum().to_dict())\"\n",[13,1686,1687,1713,1727,1737,1742,1747,1752],{"__ignoreMap":38},[42,1688,1689,1692,1695,1698,1701,1704,1707,1710],{"class":44,"line":45},[42,1690,1691],{"class":48},"soffice",[42,1693,1694],{"class":132}," --headless",[42,1696,1697],{"class":132}," --convert-to",[42,1699,1700],{"class":52}," 'csv:Text - txt - csv (StarCalc):44,34,76,1,,0,false,true,true'",[42,1702,1703],{"class":52}," orders_by_region.xlsx",[42,1705,1706],{"class":78}," >",[42,1708,1709],{"class":52}," \u002Fdev\u002Fnull",[42,1711,1712],{"class":78}," 2>&1\n",[42,1714,1715,1718,1721,1724],{"class":44,"line":92},[42,1716,1717],{"class":48},"grep",[42,1719,1720],{"class":132}," -E",[42,1722,1723],{"class":52}," \"total|^East|^North|^South\"",[42,1725,1726],{"class":52}," orders_by_region.csv\n",[42,1728,1729,1731,1734],{"class":44,"line":99},[42,1730,71],{"class":48},[42,1732,1733],{"class":132}," -c",[42,1735,1736],{"class":52}," \"\n",[42,1738,1739],{"class":44,"line":129},[42,1740,1741],{"class":52},"import pandas as pd\n",[42,1743,1744],{"class":44,"line":162},[42,1745,1746],{"class":52},"o = pd.read_excel('orders.xlsx')\n",[42,1748,1749],{"class":44,"line":180},[42,1750,1751],{"class":52},"print(o.groupby('Region')[['Units','Revenue']].sum().to_dict('index'))\n",[42,1753,1754],{"class":44,"line":185},[42,1755,1756],{"class":52},"print('all:', o[['Units','Revenue']].sum().to_dict())\"\n",[10,1758,1759,1760,1762],{},"openpyxl writes formulas but does not calculate them, so the check recalculates the workbook in LibreOffice and prints the subtotal and grand total rows as displayed, next to the same sums computed directly in pandas. They agree group by group — East 159 units and £13,681, North 117 and £19,384, South 113 and £13,182 — and the grand total of 389 units and £46,247 equals the plain sum of all fourteen orders, confirming that the nested subtotals were not counted twice. A ",[13,1761,1516],{}," over the same block would have shown £92,494, exactly double.",[508,1764,517,1770,517,1773,517,1776,517,1781,517,1784,517,1788,517,1791,517,1795,517,1798,517,1802,517,1805,517,1809,517,1813,517,1815,517,1818,517,1820,517,1822,517,1824,517,1827,517,1829,517,1832,517,1834,517,1837,517,1839,517,1841,517,1843,517,1846,517,1848,517,1851,517,1853,517,1856,517,1858,517,1861,517,1863,517,1865,517,1867,517,1870,517,1872,517,1875,517,1877,517,1880,517,1882,517,1884,517,1886,517,1888,517,1890,517,1893,517,1895,517,1898,517,1900,517,1903,517,1905,517,1907,517,1909,517,1911,517,1913,517,1916,517,1918,517,1921,517,1923,517,1926,517,1928],{"viewBox":1765,"role":511,"ariaLabelledBy":1766,"xmlns":515,"style":1769},"163 0 434 196",[1767,1768],"grp-check-t","grp-check-d","width:100%;max-width:434px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[519,1771,1772],{"id":1767},"Recalculated totals against pandas",[523,1774,1775],{"id":1768},"Each group subtotal recalculated in LibreOffice matches the pandas group sum, and the grand total of 389 units and 46,247 pounds matches the sum of all orders.",[527,1777],{"x":1778,"y":530,"width":1779,"height":1780,"fill":533},"163","434","196",[527,1782],{"x":1783,"y":537,"width":538,"height":539,"rx":530,"fill":540,"stroke":541,"style":542},"179.0",[527,1785],{"x":1786,"y":537,"width":1787,"height":539,"rx":530,"fill":540,"stroke":541,"style":542},"211.0","92.4",[548,1789,553],{"x":1790,"y":551,"style":552},"257.2",[527,1792],{"x":1793,"y":537,"width":1794,"height":539,"rx":530,"fill":540,"stroke":541,"style":542},"303.4","56.2",[548,1796,561],{"x":1797,"y":551,"style":552},"331.5",[527,1799],{"x":1800,"y":537,"width":1801,"height":539,"rx":530,"fill":540,"stroke":541,"style":542},"359.5","78.1",[548,1803,569],{"x":1804,"y":551,"style":552},"398.6",[527,1806],{"x":1807,"y":537,"width":1808,"height":539,"rx":530,"fill":540,"stroke":541,"style":542},"437.6","143.4",[548,1810,1812],{"x":1811,"y":551,"style":552},"509.3","D",[527,1814],{"x":1783,"y":572,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},[548,1816,578],{"x":1817,"y":577,"style":552},"195.0",[527,1819],{"x":1786,"y":572,"width":1787,"height":573,"rx":530,"fill":581,"stroke":582,"style":583},[548,1821,587],{"x":1790,"y":577,"style":586},[527,1823],{"x":1793,"y":572,"width":1794,"height":573,"rx":530,"fill":581,"stroke":582,"style":583},[548,1825,1826],{"x":1797,"y":577,"style":586},"Units",[527,1828],{"x":1800,"y":572,"width":1801,"height":573,"rx":530,"fill":581,"stroke":582,"style":583},[548,1830,1831],{"x":1804,"y":577,"style":586},"Revenue",[527,1833],{"x":1807,"y":572,"width":1808,"height":573,"rx":530,"fill":581,"stroke":582,"style":583},[548,1835,1836],{"x":1811,"y":577,"style":586},"pandas check",[527,1838],{"x":1783,"y":600,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},[548,1840,196],{"x":1817,"y":603,"style":552},[527,1842],{"x":1786,"y":600,"width":1787,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1844,1845],{"x":1790,"y":603,"style":608},"East total",[527,1847],{"x":1793,"y":600,"width":1794,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1849,1850],{"x":1797,"y":603,"style":608},"159",[527,1852],{"x":1800,"y":600,"width":1801,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1854,1855],{"x":1804,"y":603,"style":608},"£13,681",[527,1857],{"x":1807,"y":600,"width":1808,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1859,1860],{"x":1811,"y":603,"style":608},"match",[527,1862],{"x":1783,"y":621,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},[548,1864,625],{"x":1817,"y":624,"style":552},[527,1866],{"x":1786,"y":621,"width":1787,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1868,1869],{"x":1790,"y":624,"style":608},"North total",[527,1871],{"x":1793,"y":621,"width":1794,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1873,1874],{"x":1797,"y":624,"style":608},"117",[527,1876],{"x":1800,"y":621,"width":1801,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1878,1879],{"x":1804,"y":624,"style":608},"£19,384",[527,1881],{"x":1807,"y":621,"width":1808,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1883,1860],{"x":1811,"y":624,"style":608},[527,1885],{"x":1783,"y":645,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},[548,1887,649],{"x":1817,"y":648,"style":552},[527,1889],{"x":1786,"y":645,"width":1787,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1891,1892],{"x":1790,"y":648,"style":608},"South total",[527,1894],{"x":1793,"y":645,"width":1794,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1896,1897],{"x":1797,"y":648,"style":608},"113",[527,1899],{"x":1800,"y":645,"width":1801,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1901,1902],{"x":1804,"y":648,"style":608},"£13,182",[527,1904],{"x":1807,"y":645,"width":1808,"height":573,"rx":530,"fill":533,"stroke":541,"style":542},[548,1906,1860],{"x":1811,"y":648,"style":608},[527,1908],{"x":1783,"y":667,"width":538,"height":573,"rx":530,"fill":540,"stroke":541,"style":542},[548,1910,671],{"x":1817,"y":670,"style":552},[527,1912],{"x":1786,"y":667,"width":1787,"height":573,"rx":530,"fill":632,"stroke":633,"style":583},[548,1914,1915],{"x":1790,"y":670,"style":636},"Grand total",[527,1917],{"x":1793,"y":667,"width":1794,"height":573,"rx":530,"fill":632,"stroke":633,"style":583},[548,1919,1920],{"x":1797,"y":670,"style":636},"389",[527,1922],{"x":1800,"y":667,"width":1801,"height":573,"rx":530,"fill":632,"stroke":633,"style":583},[548,1924,1925],{"x":1804,"y":670,"style":636},"£46,247",[527,1927],{"x":1807,"y":667,"width":1808,"height":573,"rx":530,"fill":632,"stroke":633,"style":583},[548,1929,1930],{"x":1811,"y":670,"style":636},"match, not doubled",[28,1932,1934],{"id":1933},"when-not-to-add-blank-rows","When not to add blank rows",[10,1936,1937,1938,1941,1942,1945,1946,26],{},"Blank rows, group headings and subtotal rows make a sheet easier to read and harder to reuse. Sorting, filtering, pivot tables and ",[13,1939,1940],{},"read_excel"," all treat a blank row as the end of the data or a heading row as an order, so a sheet laid out this way is a ",[495,1943,1944],{},"report",", not a data source. The usual answer is to write both: the grouped report for people, and the flat table on a second sheet for anyone who needs to analyse the data, which also gives the formulas in the report something to be checked against. For grouped summaries without the detail, a pandas pivot with margins is simpler — see ",[22,1947,1949],{"href":1948},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fadd-subtotals-and-grand-totals-to-a-pandas-pivot-table\u002F","Add Subtotals and Grand Totals to a pandas Pivot Table",[28,1951,1953],{"id":1952},"common-pitfalls","Common pitfalls",[1955,1956,1957,1965,1975,1981,1987],"ul",{},[1958,1959,1960,1964],"li",{},[1961,1962,1963],"strong",{},"Inserting rows after writing."," It is slow and shifts formulas; plan the rows and write once.",[1958,1966,1967,1972,1973,26],{},[1961,1968,1969,1971],{},[13,1970,1516],{}," for the grand total."," It double counts the subtotals; use ",[13,1974,1505],{},[1958,1976,1977,1980],{},[1961,1978,1979],{},"Unsorted data."," Sort by the group key first, or a group appears in several pieces.",[1958,1982,1983,1986],{},[1961,1984,1985],{},"Numbers written as text."," Write detail values directly so they stay numeric.",[1958,1988,1989,1992],{},[1961,1990,1991],{},"Reports used as data."," Keep a flat copy of the data on another sheet.",[28,1994,1996],{"id":1995},"conclusion","Conclusion",[10,1998,1999,2000,2002],{},"To write grouped data with blank rows between groups, plan the layout first — a heading, the detail rows, a subtotal and a blank row for each group, with the row numbers counted as you go — and then write the plan in one pass with openpyxl. Use ",[13,2001,1505],{}," for group totals and for the grand total over the whole block, so nothing is counted twice, give detail rows an outline level for collapsible groups, and recalculate the file once to confirm the totals.",[28,2004,2006],{"id":2005},"frequently-asked-questions","Frequently asked questions",[10,2008,2009,2012,2013,2016],{},[1961,2010,2011],{},"How do I add a blank row between groups when writing Excel from pandas?","\nBuild a plan of rows — heading, details, subtotal, blank — while looping over ",[13,2014,2015],{},"groupby",", then write each planned row with openpyxl.",[10,2018,2019,2022,2023,2025,2026,2028],{},[1961,2020,2021],{},"Why use SUBTOTAL instead of SUM for group totals?","\nA ",[13,2024,19],{}," over a range ignores other ",[13,2027,19],{}," cells inside it, so a grand total over groups with subtotals does not double count.",[10,2030,2031,2034,2035,2038,2039,26],{},[1961,2032,2033],{},"How do I make groups collapsible in Excel with openpyxl?","\nSet ",[13,2036,2037],{},"ws.row_dimensions[row].outline_level = 1"," on the detail rows and ",[13,2040,2041],{},"ws.sheet_properties.outlinePr.summaryBelow = True",[10,2043,2044,2047],{},[1961,2045,2046],{},"Is it better to insert rows with openpyxl afterwards?","\nNo; inserting shifts every row below and does not update formulas, so it is slower and riskier than planning rows first.",[28,2049,2051],{"id":2050},"related","Related",[1955,2053,2054,2060,2067,2072],{},[1958,2055,2056,2057,2059],{},"Up one level: ",[22,2058,25],{"href":24}," — getting DataFrames into Excel.",[1958,2061,2062,2066],{},[22,2063,2065],{"href":2064},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsubtotal-and-aggregate-equivalents-in-pandas\u002F","SUBTOTAL and AGGREGATE Equivalents in pandas"," — reading such reports back.",[1958,2068,2069,2071],{},[22,2070,1608],{"href":1607}," — outline levels.",[1958,2073,2074,2078],{},[22,2075,2077],{"href":2076},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python\u002F","Add a Summary Sheet to an Excel Report with Python"," — totals on their own sheet.",[956,2080,2081],{},"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":38,"searchDepth":92,"depth":92,"links":2083},[2084,2085,2086,2087,2088,2089,2090,2091,2092,2093],{"id":30,"depth":92,"text":31},{"id":65,"depth":92,"text":66},{"id":732,"depth":92,"text":733},{"id":1529,"depth":92,"text":1530},{"id":1680,"depth":92,"text":1681},{"id":1933,"depth":92,"text":1934},{"id":1952,"depth":92,"text":1953},{"id":1995,"depth":92,"text":1996},{"id":2005,"depth":92,"text":2006},{"id":2050,"depth":92,"text":2051},"2026-10-02","Write a pandas DataFrame to Excel as a readable grouped report — a heading row for each group, its detail rows, a subtotal row with a live SUBTOTAL formula and a blank row before the next group — by planning the row positions first and writing once, instead of inserting rows afterwards; add a grand total that does not double count, optional outline levels, and check the formulas recalculate correctly.","md",[2098,2100,2102,2104],{"q":2011,"a":2099},"Build a plan of rows — heading, details, subtotal, blank — while looping over groupby, then write each planned row with openpyxl.",{"q":2021,"a":2101},"A SUBTOTAL over a range ignores other SUBTOTAL cells inside it, so a grand total over groups with subtotals does not double count.",{"q":2033,"a":2103},"Set ws.row_dimensions[row].outline_level = 1 on the detail rows and ws.sheet_properties.outlinePr.summaryBelow = True.",{"q":2046,"a":2105},"No; inserting shifts every row below and does not update formulas, so it is slower and riskier than planning rows first.","blank rows between groups excel python",null,{"breadcrumb":2109},[2110,2113,2116],{"name":2111,"item":2112},"Home","\u002F",{"name":2114,"item":2115},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":2117,"item":24},"Writing DataFrames to Excel with Pandas","\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-grouped-data-with-blank-rows-between-groups",{"title":2120,"description":2121},"Write Grouped Data with Blank Rows Between Groups in Excel","Grouped Excel reports from pandas: group headings, detail rows, SUBTOTAL formulas, blank separator rows, a grand total that skips subtotals and outline levels, written in one pass with openpyxl.","write-grouped-data-with-blank-rows-between-groups","getting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-grouped-data-with-blank-rows-between-groups\u002Findex",[2125,2126],"writing-excel","report-layouts","how-to","HZ82q2AwpuvXIqauVk10YvqOskqekiZ6RiDT6mxchFk",[2130,2134],{"title":2131,"path":2132,"stem":2133,"children":-1},"Write Excel Formulas from a pandas DataFrame","\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-excel-formulas-from-a-pandas-dataframe","getting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-excel-formulas-from-a-pandas-dataframe\u002Findex",{"title":2135,"path":2136,"stem":2137,"children":-1},"Write a Pandas DataFrame to Excel Without the Index","\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-pandas-dataframe-to-excel-without-index","getting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-pandas-dataframe-to-excel-without-index\u002Findex",[2139,2141],{"path":2140,"title":2114},"\u002Fgetting-started-with-python-excel-automation",{"path":2142,"title":2117},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas",[],[2145,2148,2151,2154,2157,2160],{"path":2146,"title":2147,"kind":2127},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fwrite-multiple-dataframes-to-one-excel-file","Write Multiple DataFrames to One Excel File",{"path":2149,"title":2150,"kind":2127},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-a-formatted-excel-report-with-xlsxwriter","Write a Formatted Excel Report with xlsxwriter",{"path":2152,"title":2153,"kind":2127},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-and-replace-text-in-excel-with-openpyxl","Find and Replace Text in Excel with openpyxl",{"path":2155,"title":2156,"kind":2127},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Finsert-and-delete-rows-and-columns-with-openpyxl","Insert and Delete Rows and Columns with openpyxl",{"path":2158,"title":2159,"kind":2127},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fopenpyxl-append-data-to-existing-excel-sheet","openpyxl: Append Data to an Existing Excel Sheet",{"path":2161,"title":2162,"kind":2127},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Freorder-columns-to-match-a-template-layout-with-pandas","Reorder Columns to Match a Template Layout with pandas",1790972077925]