[{"data":1,"prerenderedAt":1933},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas":1900,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas":1909,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas":1914,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas":1915},{"id":4,"title":5,"body":6,"dateModified":1866,"datePublished":1866,"description":1867,"extension":1868,"faq":1869,"keyword":1878,"kind":1879,"meta":1880,"navigation":209,"path":1888,"seo":1889,"slug":1892,"stem":1893,"topics":1894,"type":1898,"__hash__":1899},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas\u002Findex.md","Calculate Percent of Total per Group in pandas",{"type":7,"value":8,"toc":1851},"minimark",[9,28,135,140,168,174,178,181,308,311,315,322,434,450,650,654,661,750,761,765,788,933,945,949,952,1253,1256,1357,1361,1368,1470,1477,1481,1484,1682,1694,1698,1749,1753,1764,1768,1786,1790,1796,1802,1808,1814,1818,1847],[10,11,12,13,17,18,21,22,27],"p",{},"\"What share of North's sales is Widgets?\" and \"what share of the company is North?\" are the questions behind half of all management reports. In Excel they are a pivot table with \"Show values as % of parent row\" or a ",[14,15,16],"code",{},"=B2\u002FSUMIF(...)"," column. In pandas they take one line each — once you know that ",[14,19,20],{},"groupby().transform"," gives you the group total on every row — plus some care over two details that make percentages lie: rows whose group is blank, and rounding that makes a column of shares add up to 99%. This page, part of ",[23,24,26],"a",{"href":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002F","Creating Pivot Tables from Excel Data with Pandas",", covers shares of the grand total, shares within groups and fully normalised tables.",[29,30,38,39,38,43,38,47,38,55,38,62,38,72,38,83,38,88,38,93,38,98,38,105,38,110,38,114,38,119,38,126,38,131],"svg",{"viewBox":31,"role":32,"ariaLabelledBy":33,"xmlns":36,"style":37},"54 0 652 363","img",[34,35],"pct-levels-t","pct-levels-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:652px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[40,41,42],"title",{"id":34},"Three different percentages from the same sales",[44,45,46],"desc",{"id":35},"Three stacked questions about the same data. Share of grand total: North Widgets is 500 of 2,300, or 21.7%. Share within the group: North Widgets is 500 of North's 1,000, or 50%. Group share of the total: North is 1,000 of 2,300, or 43.5%. Each divides by a different total.",[48,49],"rect",{"x":50,"y":51,"width":52,"height":53,"fill":54},"54","0","652","363","#ffffff",[56,57,61],"text",{"x":58,"y":59,"style":60},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","North Widgets sold 500 — as a percentage of what?",[48,63],{"x":64,"y":65,"width":66,"height":67,"rx":68,"fill":69,"stroke":70,"style":71},"70.0","48.0","620.0","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[73,74,76],"g",{"transform":75},"translate(368.0,61.0) scale(1.000)",[77,78],"path",{"d":79,"fill":80,"stroke":81,"style":82},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[56,84,87],{"x":58,"y":85,"style":86},"107.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Share of the grand total: 500 \u002F 2,300 = 21.7%",[56,89,92],{"x":58,"y":90,"style":91},"127.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","value \u002F df[\"sales\"].sum()",[48,94],{"x":64,"y":95,"width":66,"height":67,"rx":68,"fill":96,"stroke":97,"style":71},"151.0","#d9f4f1","var(--teal,#0f9488)",[73,99,101],{"transform":100},"translate(368.0,164.0) scale(1.000)",[77,102],{"d":103,"fill":80,"stroke":104,"style":82},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","var(--teal-ink,#0b6157)",[56,106,109],{"x":58,"y":107,"style":108},"210.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Share within its region: 500 \u002F 1,000 = 50%",[56,111,113],{"x":58,"y":112,"style":91},"230.0","value \u002F groupby(\"region\").transform(\"sum\")",[48,115],{"x":64,"y":116,"width":66,"height":67,"rx":68,"fill":117,"stroke":118,"style":71},"254.0","#fdefd8","var(--gold,#b4740a)",[73,120,122],{"transform":121},"translate(368.0,267.0) scale(1.000)",[77,123],{"d":124,"fill":80,"stroke":125,"style":82},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--gold-ink,#7a4e06)",[56,127,130],{"x":58,"y":128,"style":129},"313.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Region share of the company: 1,000 \u002F 2,300 = 43.5%",[56,132,134],{"x":58,"y":133,"style":91},"333.0","groupby(\"region\").sum() \u002F total",[136,137,139],"h2",{"id":138},"prerequisites","Prerequisites",[141,142,147],"pre",{"className":143,"code":144,"language":145,"meta":146,"style":146},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,148,149],{"__ignoreMap":146},[150,151,154,158,162,165],"span",{"class":152,"line":153},"line",1,[150,155,157],{"class":156},"sMTad","pip",[150,159,161],{"class":160},"srMev"," install",[150,163,164],{"class":160}," pandas",[150,166,167],{"class":160}," openpyxl\n",[10,169,170,173],{},[14,171,172],{},"sales.xlsx"," has eight rows of sales by region and product. One row has no region — a data quality problem that turns out to matter for the percentages.",[136,175,177],{"id":176},"step-1-share-of-the-grand-total","Step 1 — Share of the grand total",[10,179,180],{},"The simplest percentage divides each row by the column total:",[141,182,186],{"className":183,"code":184,"language":185,"meta":146,"style":146},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nsales = pd.read_excel(\"sales.xlsx\")\nsales[\"share_of_total\"] = sales[\"sales\"] \u002F sales[\"sales\"].sum()\nprint(sales.round(3).to_string(index=False))\nprint(\"check:\", sales[\"share_of_total\"].sum())\n","python",[14,187,188,204,211,229,261,289],{"__ignoreMap":146},[150,189,190,194,198,201],{"class":152,"line":153},[150,191,193],{"class":192},"s-kum","import",[150,195,197],{"class":196},"skGVy"," pandas ",[150,199,200],{"class":192},"as",[150,202,203],{"class":196}," pd\n",[150,205,207],{"class":152,"line":206},2,[150,208,210],{"emptyLinePlaceholder":209},true,"\n",[150,212,214,217,220,223,226],{"class":152,"line":213},3,[150,215,216],{"class":196},"sales ",[150,218,219],{"class":192},"=",[150,221,222],{"class":196}," pd.read_excel(",[150,224,225],{"class":160},"\"sales.xlsx\"",[150,227,228],{"class":196},")\n",[150,230,232,235,238,241,243,246,249,251,254,256,258],{"class":152,"line":231},4,[150,233,234],{"class":196},"sales[",[150,236,237],{"class":160},"\"share_of_total\"",[150,239,240],{"class":196},"] ",[150,242,219],{"class":192},[150,244,245],{"class":196}," sales[",[150,247,248],{"class":160},"\"sales\"",[150,250,240],{"class":196},[150,252,253],{"class":192},"\u002F",[150,255,245],{"class":196},[150,257,248],{"class":160},[150,259,260],{"class":196},"].sum()\n",[150,262,264,268,271,274,277,281,283,286],{"class":152,"line":263},5,[150,265,267],{"class":266},"sP0c6","print",[150,269,270],{"class":196},"(sales.round(",[150,272,273],{"class":266},"3",[150,275,276],{"class":196},").to_string(",[150,278,280],{"class":279},"sa561","index",[150,282,219],{"class":192},[150,284,285],{"class":266},"False",[150,287,288],{"class":196},"))\n",[150,290,292,294,297,300,303,305],{"class":152,"line":291},6,[150,293,267],{"class":266},[150,295,296],{"class":196},"(",[150,298,299],{"class":160},"\"check:\"",[150,301,302],{"class":196},", sales[",[150,304,237],{"class":160},[150,306,307],{"class":196},"].sum())\n",[10,309,310],{},"Keep shares as fractions (0.217) in the data and format them as percentages only when writing; that way they can be summed, compared and charted without converting back. The check line is worth keeping in real code: shares of a total always add up to 1, and anything else means rows were filtered or duplicated between computing the total and using it.",[136,312,314],{"id":313},"step-2-share-within-each-group-with-transform","Step 2 — Share within each group with transform",[10,316,317,318,321],{},"For shares within a region, each row needs its own region's total. ",[14,319,320],{},"groupby().transform(\"sum\")"," returns exactly that — a Series the same length as the data, holding each row's group total — so the division stays row by row:",[141,323,325],{"className":183,"code":324,"language":185,"meta":146,"style":146},"sales[\"region_total\"] = sales.groupby(\"region\", dropna=False)[\"sales\"].transform(\"sum\")\nsales[\"share_of_region\"] = sales[\"sales\"] \u002F sales[\"region_total\"]\nprint(sales[[\"region\", \"product\", \"sales\", \"region_total\", \"share_of_region\"]].round(3).to_string(index=False))\n",[14,326,327,367,393],{"__ignoreMap":146},[150,328,329,331,334,336,338,341,344,347,350,352,354,357,359,362,365],{"class":152,"line":153},[150,330,234],{"class":196},[150,332,333],{"class":160},"\"region_total\"",[150,335,240],{"class":196},[150,337,219],{"class":192},[150,339,340],{"class":196}," sales.groupby(",[150,342,343],{"class":160},"\"region\"",[150,345,346],{"class":196},", ",[150,348,349],{"class":279},"dropna",[150,351,219],{"class":192},[150,353,285],{"class":266},[150,355,356],{"class":196},")[",[150,358,248],{"class":160},[150,360,361],{"class":196},"].transform(",[150,363,364],{"class":160},"\"sum\"",[150,366,228],{"class":196},[150,368,369,371,374,376,378,380,382,384,386,388,390],{"class":152,"line":206},[150,370,234],{"class":196},[150,372,373],{"class":160},"\"share_of_region\"",[150,375,240],{"class":196},[150,377,219],{"class":192},[150,379,245],{"class":196},[150,381,248],{"class":160},[150,383,240],{"class":196},[150,385,253],{"class":192},[150,387,245],{"class":196},[150,389,333],{"class":160},[150,391,392],{"class":196},"]\n",[150,394,395,397,400,402,404,407,409,411,413,415,417,419,422,424,426,428,430,432],{"class":152,"line":213},[150,396,267],{"class":266},[150,398,399],{"class":196},"(sales[[",[150,401,343],{"class":160},[150,403,346],{"class":196},[150,405,406],{"class":160},"\"product\"",[150,408,346],{"class":196},[150,410,248],{"class":160},[150,412,346],{"class":196},[150,414,333],{"class":160},[150,416,346],{"class":196},[150,418,373],{"class":160},[150,420,421],{"class":196},"]].round(",[150,423,273],{"class":266},[150,425,276],{"class":196},[150,427,280],{"class":279},[150,429,219],{"class":192},[150,431,285],{"class":266},[150,433,288],{"class":196},[10,435,436,437,440,441,444,445,449],{},"North's three products now show 50%, 30% and 20%, adding to 100% within North. ",[14,438,439],{},"transform"," is the pandas counterpart of the Excel pattern ",[14,442,443],{},"=B2\u002FSUMIF($A:$A,A2,$B:$B)",", and far faster, because the group sums are computed once rather than once per row. ",[23,446,448],{"href":447},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumif-and-sumifs-equivalent-in-pandas\u002F","SUMIF and SUMIFS Equivalent in pandas"," covers the conditional sums themselves.",[29,451,38,457,38,460,38,463,38,468,38,471,38,479,38,483,38,489,38,493,38,497,38,501,38,505,38,509,38,513,38,517,38,521,38,525,38,530,38,532,38,536,38,538,38,541,38,543,38,546,38,548,38,551,38,553,38,556,38,559,38,563,38,565,38,569,38,571,38,574,38,576,38,579,38,582,38,586,38,588,38,592,38,595,38,598,38,600,38,602,38,604,38,607,38,609,38,612,38,614,38,616,38,618,38,621,38,624,38,628,38,630,38,632,38,634,38,637,38,639,38,641,38,643,38,645,38,647],{"viewBox":452,"role":32,"ariaLabelledBy":453,"xmlns":36,"style":456},"172 0 416 200",[454,455],"pct-transform-t","pct-transform-d","width:100%;max-width:416px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,458,459],{"id":454},"Group totals repeated on every row by transform",[44,461,462],{"id":455},"The North rows of the sales sheet with two added columns. region_total holds 1,000 on every North row, the sum of North's sales. share_of_region divides each row's sales by it: Widgets 50%, Gadgets 30%, Gizmos 20%.",[48,464],{"x":465,"y":51,"width":466,"height":467,"fill":54},"172","416","200",[56,469,470],{"x":58,"y":59,"style":60},"groupby(\"region\")[\"sales\"].transform(\"sum\")",[48,472],{"x":473,"y":65,"width":474,"height":475,"rx":51,"fill":476,"stroke":477,"style":478},"188.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[48,480],{"x":481,"y":65,"width":482,"height":475,"rx":51,"fill":476,"stroke":477,"style":478},"220.0","63.6",[56,484,488],{"x":485,"y":486,"style":487},"251.8","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[48,490],{"x":491,"y":65,"width":492,"height":475,"rx":51,"fill":476,"stroke":477,"style":478},"283.6","75.8",[56,494,496],{"x":495,"y":486,"style":487},"321.5","B",[48,498],{"x":499,"y":65,"width":500,"height":475,"rx":51,"fill":476,"stroke":477,"style":478},"359.5","56.4",[56,502,504],{"x":503,"y":486,"style":487},"387.7","C",[48,506],{"x":507,"y":65,"width":508,"height":475,"rx":51,"fill":476,"stroke":477,"style":478},"415.8","97.3",[56,510,512],{"x":511,"y":486,"style":487},"464.5","D",[48,514],{"x":515,"y":65,"width":516,"height":475,"rx":51,"fill":476,"stroke":477,"style":478},"513.1","58.9",[56,518,520],{"x":519,"y":486,"style":487},"542.5","E",[48,522],{"x":473,"y":523,"width":474,"height":524,"rx":51,"fill":476,"stroke":477,"style":478},"72.0","28.0",[56,526,529],{"x":527,"y":528,"style":487},"204.0","91.0","1",[48,531],{"x":481,"y":523,"width":482,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,533,535],{"x":485,"y":528,"style":534},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","region",[48,537],{"x":491,"y":523,"width":492,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,539,540],{"x":495,"y":528,"style":534},"product",[48,542],{"x":499,"y":523,"width":500,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,544,545],{"x":503,"y":528,"style":534},"sales",[48,547],{"x":507,"y":523,"width":508,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,549,550],{"x":511,"y":528,"style":534},"region_total",[48,552],{"x":515,"y":523,"width":516,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,554,555],{"x":519,"y":528,"style":534},"share",[48,557],{"x":473,"y":558,"width":474,"height":524,"rx":51,"fill":476,"stroke":477,"style":478},"100.0",[56,560,562],{"x":527,"y":561,"style":487},"119.0","2",[48,564],{"x":481,"y":558,"width":482,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,566,568],{"x":485,"y":561,"style":567},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","North",[48,570],{"x":491,"y":558,"width":492,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,572,573],{"x":495,"y":561,"style":567},"Widgets",[48,575],{"x":499,"y":558,"width":500,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,577,578],{"x":503,"y":561,"style":567},"500",[48,580],{"x":507,"y":558,"width":508,"height":524,"rx":51,"fill":69,"stroke":70,"style":581},"stroke-width:1.5px",[56,583,585],{"x":511,"y":561,"style":584},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","1000",[48,587],{"x":515,"y":558,"width":516,"height":524,"rx":51,"fill":96,"stroke":97,"style":581},[56,589,591],{"x":519,"y":561,"style":590},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","50%",[48,593],{"x":473,"y":594,"width":474,"height":524,"rx":51,"fill":476,"stroke":477,"style":478},"128.0",[56,596,273],{"x":527,"y":597,"style":487},"147.0",[48,599],{"x":481,"y":594,"width":482,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,601,568],{"x":485,"y":597,"style":567},[48,603],{"x":491,"y":594,"width":492,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,605,606],{"x":495,"y":597,"style":567},"Gadgets",[48,608],{"x":499,"y":594,"width":500,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,610,611],{"x":503,"y":597,"style":567},"300",[48,613],{"x":507,"y":594,"width":508,"height":524,"rx":51,"fill":69,"stroke":70,"style":581},[56,615,585],{"x":511,"y":597,"style":584},[48,617],{"x":515,"y":594,"width":516,"height":524,"rx":51,"fill":96,"stroke":97,"style":581},[56,619,620],{"x":519,"y":597,"style":590},"30%",[48,622],{"x":473,"y":623,"width":474,"height":524,"rx":51,"fill":476,"stroke":477,"style":478},"156.0",[56,625,627],{"x":527,"y":626,"style":487},"175.0","4",[48,629],{"x":481,"y":623,"width":482,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,631,568],{"x":485,"y":626,"style":567},[48,633],{"x":491,"y":623,"width":492,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,635,636],{"x":495,"y":626,"style":567},"Gizmos",[48,638],{"x":499,"y":623,"width":500,"height":524,"rx":51,"fill":54,"stroke":477,"style":478},[56,640,467],{"x":503,"y":626,"style":567},[48,642],{"x":507,"y":623,"width":508,"height":524,"rx":51,"fill":69,"stroke":70,"style":581},[56,644,585],{"x":511,"y":626,"style":584},[48,646],{"x":515,"y":623,"width":516,"height":524,"rx":51,"fill":96,"stroke":97,"style":581},[56,648,649],{"x":519,"y":626,"style":590},"20%",[136,651,653],{"id":652},"step-3-do-not-let-blank-groups-disappear","Step 3 — Do not let blank groups disappear",[10,655,656,657,660],{},"The row with no region is the trap. By default ",[14,658,659],{},"groupby"," drops missing keys, so a summary by region silently omits its sales:",[141,662,664],{"className":183,"code":663,"language":185,"meta":146,"style":146},"by_region = sales.groupby(\"region\")[\"sales\"].sum()\nprint(by_region.to_dict(), \"sum:\", by_region.sum(), \"vs total:\", sales[\"sales\"].sum())\n\nby_region = sales.groupby(\"region\", dropna=False)[\"sales\"].sum()\nprint((by_region \u002F by_region.sum()).round(3).to_dict())\n",[14,665,666,683,705,709,733],{"__ignoreMap":146},[150,667,668,671,673,675,677,679,681],{"class":152,"line":153},[150,669,670],{"class":196},"by_region ",[150,672,219],{"class":192},[150,674,340],{"class":196},[150,676,343],{"class":160},[150,678,356],{"class":196},[150,680,248],{"class":160},[150,682,260],{"class":196},[150,684,685,687,690,693,696,699,701,703],{"class":152,"line":206},[150,686,267],{"class":266},[150,688,689],{"class":196},"(by_region.to_dict(), ",[150,691,692],{"class":160},"\"sum:\"",[150,694,695],{"class":196},", by_region.sum(), ",[150,697,698],{"class":160},"\"vs total:\"",[150,700,302],{"class":196},[150,702,248],{"class":160},[150,704,307],{"class":196},[150,706,707],{"class":152,"line":213},[150,708,210],{"emptyLinePlaceholder":209},[150,710,711,713,715,717,719,721,723,725,727,729,731],{"class":152,"line":231},[150,712,670],{"class":196},[150,714,219],{"class":192},[150,716,340],{"class":196},[150,718,343],{"class":160},[150,720,346],{"class":196},[150,722,349],{"class":279},[150,724,219],{"class":192},[150,726,285],{"class":266},[150,728,356],{"class":196},[150,730,248],{"class":160},[150,732,260],{"class":196},[150,734,735,737,740,742,745,747],{"class":152,"line":263},[150,736,267],{"class":266},[150,738,739],{"class":196},"((by_region ",[150,741,253],{"class":192},[150,743,744],{"class":196}," by_region.sum()).round(",[150,746,273],{"class":266},[150,748,749],{"class":196},").to_dict())\n",[10,751,752,753,756,757,760],{},"Without ",[14,754,755],{},"dropna=False",", the regions add up to 2,250 while total sales are 2,300, and each region's share is computed against a total that is missing 50. With it, the blank region appears as its own line and the shares describe all the data. In a report, relabel it — ",[14,758,759],{},"sales[\"region\"].fillna(\"Unassigned\")"," — so readers see the gap instead of a blank row label.",[136,762,764],{"id":763},"step-4-normalised-tables-with-crosstab","Step 4 — Normalised tables with crosstab",[10,766,767,768,771,772,775,776,779,780,783,784,787],{},"For a whole table of shares — products across regions — ",[14,769,770],{},"pd.crosstab"," builds the grid and ",[14,773,774],{},"normalize"," chooses what each cell is a share of: ",[14,777,778],{},"\"index\""," for each row's total, ",[14,781,782],{},"\"columns\""," for each column's, ",[14,785,786],{},"\"all\""," for the grand total:",[141,789,791],{"className":183,"code":790,"language":185,"meta":146,"style":146},"data = sales.assign(region=sales[\"region\"].fillna(\"Unassigned\"))\nby_row = pd.crosstab(data[\"region\"], data[\"product\"], values=data[\"sales\"], aggfunc=\"sum\", normalize=\"index\")\nby_col = pd.crosstab(data[\"region\"], data[\"product\"], values=data[\"sales\"], aggfunc=\"sum\", normalize=\"columns\")\nprint(by_row.round(3))\nprint(by_col.round(3))\n",[14,792,793,819,868,911,922],{"__ignoreMap":146},[150,794,795,798,800,803,805,807,809,811,814,817],{"class":152,"line":153},[150,796,797],{"class":196},"data ",[150,799,219],{"class":192},[150,801,802],{"class":196}," sales.assign(",[150,804,535],{"class":279},[150,806,219],{"class":192},[150,808,234],{"class":196},[150,810,343],{"class":160},[150,812,813],{"class":196},"].fillna(",[150,815,816],{"class":160},"\"Unassigned\"",[150,818,288],{"class":196},[150,820,821,824,826,829,831,834,836,839,842,844,847,849,851,854,856,858,860,862,864,866],{"class":152,"line":206},[150,822,823],{"class":196},"by_row ",[150,825,219],{"class":192},[150,827,828],{"class":196}," pd.crosstab(data[",[150,830,343],{"class":160},[150,832,833],{"class":196},"], data[",[150,835,406],{"class":160},[150,837,838],{"class":196},"], ",[150,840,841],{"class":279},"values",[150,843,219],{"class":192},[150,845,846],{"class":196},"data[",[150,848,248],{"class":160},[150,850,838],{"class":196},[150,852,853],{"class":279},"aggfunc",[150,855,219],{"class":192},[150,857,364],{"class":160},[150,859,346],{"class":196},[150,861,774],{"class":279},[150,863,219],{"class":192},[150,865,778],{"class":160},[150,867,228],{"class":196},[150,869,870,873,875,877,879,881,883,885,887,889,891,893,895,897,899,901,903,905,907,909],{"class":152,"line":213},[150,871,872],{"class":196},"by_col ",[150,874,219],{"class":192},[150,876,828],{"class":196},[150,878,343],{"class":160},[150,880,833],{"class":196},[150,882,406],{"class":160},[150,884,838],{"class":196},[150,886,841],{"class":279},[150,888,219],{"class":192},[150,890,846],{"class":196},[150,892,248],{"class":160},[150,894,838],{"class":196},[150,896,853],{"class":279},[150,898,219],{"class":192},[150,900,364],{"class":160},[150,902,346],{"class":196},[150,904,774],{"class":279},[150,906,219],{"class":192},[150,908,782],{"class":160},[150,910,228],{"class":196},[150,912,913,915,918,920],{"class":152,"line":231},[150,914,267],{"class":266},[150,916,917],{"class":196},"(by_row.round(",[150,919,273],{"class":266},[150,921,288],{"class":196},[150,923,924,926,929,931],{"class":152,"line":263},[150,925,267],{"class":266},[150,927,928],{"class":196},"(by_col.round(",[150,930,273],{"class":266},[150,932,288],{"class":196},[10,934,935,936,939,940,944],{},"Each row of the first table adds to 1 — the product mix within each region — and each column of the second adds to 1 — the regional split of each product. These are the equivalents of Excel's \"% of Row Total\" and \"% of Column Total\" pivot options. Combinations with no sales show ",[14,937,938],{},"0.0"," after normalising, which is right here — East sold no Gadgets — but check that a zero means \"none\" and not \"no data\" before publishing it. ",[23,941,943],{"href":942},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-crosstab-from-excel-data-with-pandas\u002F","Build a Crosstab from Excel Data with pandas"," covers crosstabs in general.",[136,946,948],{"id":947},"step-5-round-shares-so-they-still-add-to-100","Step 5 — Round shares so they still add to 100%",[10,950,951],{},"Rounded independently, shares often total 99% or 101%, which readers notice. Three equal thirds rounded to whole percentages give 33 + 33 + 33 = 99. When the displayed figures must total exactly 100, use the largest-remainder method: round everything down, then hand the missing points to the shares with the biggest remainders:",[141,953,955],{"className":183,"code":954,"language":185,"meta":146,"style":146},"import numpy as np\n\ndef round_to_100(shares, decimals=0):\n    scale = 100 * 10 ** decimals\n    raw = np.asarray(shares, dtype=float) * scale\n    floored = np.floor(raw)\n    missing = int(round(scale - floored.sum()))\n    order = np.argsort(-(raw - floored))\n    floored[order[:missing]] += 1\n    return floored \u002F 10 ** decimals\n\nthirds = [1 \u002F 3, 1 \u002F 3, 1 \u002F 3]\nprint(np.round(np.array(thirds) * 100).tolist(), \"->\", round_to_100(thirds).tolist())\n\nregion_share = by_region \u002F by_region.sum()\nprint({k: float(v) for k, v in zip(region_share.index.fillna(\"Unassigned\"), round_to_100(region_share, decimals=1))})\n",[14,956,957,969,973,992,1014,1041,1051,1076,1097,1109,1126,1131,1168,1189,1194,1210],{"__ignoreMap":146},[150,958,959,961,964,966],{"class":152,"line":153},[150,960,193],{"class":192},[150,962,963],{"class":196}," numpy ",[150,965,200],{"class":192},[150,967,968],{"class":196}," np\n",[150,970,971],{"class":152,"line":206},[150,972,210],{"emptyLinePlaceholder":209},[150,974,975,978,982,985,987,989],{"class":152,"line":213},[150,976,977],{"class":192},"def",[150,979,981],{"class":980},"s_Opv"," round_to_100",[150,983,984],{"class":196},"(shares, decimals",[150,986,219],{"class":192},[150,988,51],{"class":266},[150,990,991],{"class":196},"):\n",[150,993,994,997,999,1002,1005,1008,1011],{"class":152,"line":231},[150,995,996],{"class":196},"    scale ",[150,998,219],{"class":192},[150,1000,1001],{"class":266}," 100",[150,1003,1004],{"class":192}," *",[150,1006,1007],{"class":266}," 10",[150,1009,1010],{"class":192}," **",[150,1012,1013],{"class":196}," decimals\n",[150,1015,1016,1019,1021,1024,1027,1029,1032,1035,1038],{"class":152,"line":263},[150,1017,1018],{"class":196},"    raw ",[150,1020,219],{"class":192},[150,1022,1023],{"class":196}," np.asarray(shares, ",[150,1025,1026],{"class":279},"dtype",[150,1028,219],{"class":192},[150,1030,1031],{"class":266},"float",[150,1033,1034],{"class":196},") ",[150,1036,1037],{"class":192},"*",[150,1039,1040],{"class":196}," scale\n",[150,1042,1043,1046,1048],{"class":152,"line":291},[150,1044,1045],{"class":196},"    floored ",[150,1047,219],{"class":192},[150,1049,1050],{"class":196}," np.floor(raw)\n",[150,1052,1054,1057,1059,1062,1064,1067,1070,1073],{"class":152,"line":1053},7,[150,1055,1056],{"class":196},"    missing ",[150,1058,219],{"class":192},[150,1060,1061],{"class":266}," int",[150,1063,296],{"class":196},[150,1065,1066],{"class":266},"round",[150,1068,1069],{"class":196},"(scale ",[150,1071,1072],{"class":192},"-",[150,1074,1075],{"class":196}," floored.sum()))\n",[150,1077,1079,1082,1084,1087,1089,1092,1094],{"class":152,"line":1078},8,[150,1080,1081],{"class":196},"    order ",[150,1083,219],{"class":192},[150,1085,1086],{"class":196}," np.argsort(",[150,1088,1072],{"class":192},[150,1090,1091],{"class":196},"(raw ",[150,1093,1072],{"class":192},[150,1095,1096],{"class":196}," floored))\n",[150,1098,1100,1103,1106],{"class":152,"line":1099},9,[150,1101,1102],{"class":196},"    floored[order[:missing]] ",[150,1104,1105],{"class":192},"+=",[150,1107,1108],{"class":266}," 1\n",[150,1110,1112,1115,1118,1120,1122,1124],{"class":152,"line":1111},10,[150,1113,1114],{"class":192},"    return",[150,1116,1117],{"class":196}," floored ",[150,1119,253],{"class":192},[150,1121,1007],{"class":266},[150,1123,1010],{"class":192},[150,1125,1013],{"class":196},[150,1127,1129],{"class":152,"line":1128},11,[150,1130,210],{"emptyLinePlaceholder":209},[150,1132,1134,1137,1139,1142,1144,1147,1150,1152,1154,1156,1158,1160,1162,1164,1166],{"class":152,"line":1133},12,[150,1135,1136],{"class":196},"thirds ",[150,1138,219],{"class":192},[150,1140,1141],{"class":196}," [",[150,1143,529],{"class":266},[150,1145,1146],{"class":192}," \u002F",[150,1148,1149],{"class":266}," 3",[150,1151,346],{"class":196},[150,1153,529],{"class":266},[150,1155,1146],{"class":192},[150,1157,1149],{"class":266},[150,1159,346],{"class":196},[150,1161,529],{"class":266},[150,1163,1146],{"class":192},[150,1165,1149],{"class":266},[150,1167,392],{"class":196},[150,1169,1171,1173,1176,1178,1180,1183,1186],{"class":152,"line":1170},13,[150,1172,267],{"class":266},[150,1174,1175],{"class":196},"(np.round(np.array(thirds) ",[150,1177,1037],{"class":192},[150,1179,1001],{"class":266},[150,1181,1182],{"class":196},").tolist(), ",[150,1184,1185],{"class":160},"\"->\"",[150,1187,1188],{"class":196},", round_to_100(thirds).tolist())\n",[150,1190,1192],{"class":152,"line":1191},14,[150,1193,210],{"emptyLinePlaceholder":209},[150,1195,1197,1200,1202,1205,1207],{"class":152,"line":1196},15,[150,1198,1199],{"class":196},"region_share ",[150,1201,219],{"class":192},[150,1203,1204],{"class":196}," by_region ",[150,1206,253],{"class":192},[150,1208,1209],{"class":196}," by_region.sum()\n",[150,1211,1213,1215,1218,1220,1223,1226,1229,1232,1235,1238,1240,1243,1246,1248,1250],{"class":152,"line":1212},16,[150,1214,267],{"class":266},[150,1216,1217],{"class":196},"({k: ",[150,1219,1031],{"class":266},[150,1221,1222],{"class":196},"(v) ",[150,1224,1225],{"class":192},"for",[150,1227,1228],{"class":196}," k, v ",[150,1230,1231],{"class":192},"in",[150,1233,1234],{"class":266}," zip",[150,1236,1237],{"class":196},"(region_share.index.fillna(",[150,1239,816],{"class":160},[150,1241,1242],{"class":196},"), round_to_100(region_share, ",[150,1244,1245],{"class":279},"decimals",[150,1247,219],{"class":192},[150,1249,529],{"class":266},[150,1251,1252],{"class":196},"))})\n",[10,1254,1255],{},"The thirds become 34, 33, 33 — one share absorbs the rounding — and the regional shares at one decimal place add to exactly 100.0. Use this only for display: the unrounded fractions stay in the data for any further calculation, and the adjusted figure is never more than one rounding unit away from its true value.",[29,1257,38,1263,38,1266,38,1269,38,1273,38,1282,38,1290,38,1296,38,1300,38,1306,38,1309,38,1312,38,1316,38,1323,38,1326,38,1332,38,1337,38,1341,38,1345,38,1347,38,1349,38,1352],{"viewBox":1258,"role":32,"ariaLabelledBy":1259,"xmlns":36,"style":1262},"4 0 752 233",[1260,1261],"pct-round-t","pct-round-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,1264,1265],{"id":1260},"Independent rounding versus largest remainder",[44,1267,1268],{"id":1261},"Two panels showing three equal shares of one third. Rounding each share independently gives 33, 33 and 33, which totals 99. The largest remainder method rounds down and gives the leftover point to the largest remainder, producing 34, 33 and 33, which totals 100.",[48,1270],{"x":627,"y":51,"width":1271,"height":1272,"fill":54},"752","233",[48,1274],{"x":1275,"y":1276,"width":1277,"height":1278,"rx":1279,"fill":1280,"stroke":1281,"style":71},"20.0","16.0","351.0","201.0","14","#fee8f2","var(--accent,#f43f8f)",[73,1283,1285],{"transform":1284},"translate(182.5,29.0) scale(1.083)",[77,1286],{"d":1287,"fill":80,"stroke":1288,"style":1289},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--accent-ink,#be185d)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[56,1291,1295],{"x":1292,"y":1293,"style":1294},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Round each share",[152,1297],{"x1":1298,"y1":528,"x2":1299,"y2":528,"stroke":1281,"style":478},"36.0","355.0",[56,1301,1305],{"x":1302,"y":1303,"style":1304},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","33.33 → 33",[56,1307,1305],{"x":1302,"y":1308,"style":1304},"137.0",[56,1310,1305],{"x":1302,"y":1311,"style":1304},"163.0",[56,1313,1315],{"x":1302,"y":1314,"style":1304},"189.0","total 99",[73,1317,1319],{"transform":1318},"translate(38.5,177.5) scale(0.625)",[77,1320],{"d":1321,"fill":80,"stroke":1281,"style":1322},"M6 6l12 12 M18 6L6 18","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[48,1324],{"x":1325,"y":1276,"width":1277,"height":1278,"rx":1279,"fill":96,"stroke":97,"style":71},"389.0",[73,1327,1329],{"transform":1328},"translate(551.5,29.0) scale(1.083)",[77,1330],{"d":1331,"fill":80,"stroke":104,"style":1289},"M5 12.5l4.5 4.5L19 7",[56,1333,1336],{"x":1334,"y":1293,"style":1335},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Largest remainder",[152,1338],{"x1":1339,"y1":528,"x2":1340,"y2":528,"stroke":97,"style":478},"405.0","724.0",[56,1342,1344],{"x":1343,"y":1303,"style":1304},"429.0","33.33 → 34",[56,1346,1305],{"x":1343,"y":1308,"style":1304},[56,1348,1305],{"x":1343,"y":1311,"style":1304},[56,1350,1351],{"x":1343,"y":1314,"style":1304},"total 100",[73,1353,1355],{"transform":1354},"translate(407.5,177.5) scale(0.625)",[77,1356],{"d":1331,"fill":80,"stroke":97,"style":1322},[136,1358,1360],{"id":1359},"shares-next-to-totals-in-a-pivot-table","Shares next to totals in a pivot table",[10,1362,1363,1364,1367],{},"Readers often want the amount and its share side by side, with a total row. Build the pivot with ",[14,1365,1366],{},"margins=True",", which adds an \"All\" row and column, and divide by the grand total it computed:",[141,1369,1371],{"className":183,"code":1370,"language":185,"meta":146,"style":146},"pivot = pd.pivot_table(data, index=\"region\", values=\"sales\", aggfunc=\"sum\", margins=True, margins_name=\"Total\")\npivot[\"share\"] = pivot[\"sales\"] \u002F pivot.loc[\"Total\", \"sales\"]\nprint(pivot.round(3))\n",[14,1372,1373,1427,1459],{"__ignoreMap":146},[150,1374,1375,1378,1380,1383,1385,1387,1389,1391,1393,1395,1397,1399,1401,1403,1405,1407,1410,1412,1415,1417,1420,1422,1425],{"class":152,"line":153},[150,1376,1377],{"class":196},"pivot ",[150,1379,219],{"class":192},[150,1381,1382],{"class":196}," pd.pivot_table(data, ",[150,1384,280],{"class":279},[150,1386,219],{"class":192},[150,1388,343],{"class":160},[150,1390,346],{"class":196},[150,1392,841],{"class":279},[150,1394,219],{"class":192},[150,1396,248],{"class":160},[150,1398,346],{"class":196},[150,1400,853],{"class":279},[150,1402,219],{"class":192},[150,1404,364],{"class":160},[150,1406,346],{"class":196},[150,1408,1409],{"class":279},"margins",[150,1411,219],{"class":192},[150,1413,1414],{"class":266},"True",[150,1416,346],{"class":196},[150,1418,1419],{"class":279},"margins_name",[150,1421,219],{"class":192},[150,1423,1424],{"class":160},"\"Total\"",[150,1426,228],{"class":196},[150,1428,1429,1432,1435,1437,1439,1442,1444,1446,1448,1451,1453,1455,1457],{"class":152,"line":206},[150,1430,1431],{"class":196},"pivot[",[150,1433,1434],{"class":160},"\"share\"",[150,1436,240],{"class":196},[150,1438,219],{"class":192},[150,1440,1441],{"class":196}," pivot[",[150,1443,248],{"class":160},[150,1445,240],{"class":196},[150,1447,253],{"class":192},[150,1449,1450],{"class":196}," pivot.loc[",[150,1452,1424],{"class":160},[150,1454,346],{"class":196},[150,1456,248],{"class":160},[150,1458,392],{"class":196},[150,1460,1461,1463,1466,1468],{"class":152,"line":213},[150,1462,267],{"class":266},[150,1464,1465],{"class":196},"(pivot.round(",[150,1467,273],{"class":266},[150,1469,288],{"class":196},[10,1471,1472,1473,1476],{},"The Total row shows 2,300 and a share of 1.0, which doubles as a check that nothing was lost on the way. For a product-by-region pivot, divide the whole table by the Total column to get row shares, or by the Total row to get column shares — the same results as ",[14,1474,1475],{},"crosstab(normalize=...)",", but with the totals visible in the same table.",[136,1478,1480],{"id":1479},"step-6-write-shares-with-a-percent-format","Step 6 — Write shares with a percent format",[10,1482,1483],{},"Write the fractions as numbers and apply a percent number format, so Excel can sort and chart them while readers see percentages:",[141,1485,1487],{"className":183,"code":1486,"language":185,"meta":146,"style":146},"from openpyxl import load_workbook\n\nout = sales[[\"region\", \"product\", \"sales\", \"share_of_total\", \"share_of_region\"]].fillna({\"region\": \"Unassigned\"})\nout.to_excel(\"sales_shares.xlsx\", index=False)\nwb = load_workbook(\"sales_shares.xlsx\")\nws = wb.active\nfor row in ws.iter_rows(min_row=2, min_col=4, max_col=5):\n    for cell in row:\n        cell.number_format = \"0.0%\"\nwb.save(\"sales_shares.xlsx\")\nprint(pd.read_excel(\"sales_shares.xlsx\")[\"share_of_region\"].round(3).tolist())\n",[14,1488,1489,1502,1506,1547,1565,1579,1589,1629,1642,1652,1661],{"__ignoreMap":146},[150,1490,1491,1494,1497,1499],{"class":152,"line":153},[150,1492,1493],{"class":192},"from",[150,1495,1496],{"class":196}," openpyxl ",[150,1498,193],{"class":192},[150,1500,1501],{"class":196}," load_workbook\n",[150,1503,1504],{"class":152,"line":206},[150,1505,210],{"emptyLinePlaceholder":209},[150,1507,1508,1511,1513,1516,1518,1520,1522,1524,1526,1528,1530,1532,1534,1537,1539,1542,1544],{"class":152,"line":213},[150,1509,1510],{"class":196},"out ",[150,1512,219],{"class":192},[150,1514,1515],{"class":196}," sales[[",[150,1517,343],{"class":160},[150,1519,346],{"class":196},[150,1521,406],{"class":160},[150,1523,346],{"class":196},[150,1525,248],{"class":160},[150,1527,346],{"class":196},[150,1529,237],{"class":160},[150,1531,346],{"class":196},[150,1533,373],{"class":160},[150,1535,1536],{"class":196},"]].fillna({",[150,1538,343],{"class":160},[150,1540,1541],{"class":196},": ",[150,1543,816],{"class":160},[150,1545,1546],{"class":196},"})\n",[150,1548,1549,1552,1555,1557,1559,1561,1563],{"class":152,"line":231},[150,1550,1551],{"class":196},"out.to_excel(",[150,1553,1554],{"class":160},"\"sales_shares.xlsx\"",[150,1556,346],{"class":196},[150,1558,280],{"class":279},[150,1560,219],{"class":192},[150,1562,285],{"class":266},[150,1564,228],{"class":196},[150,1566,1567,1570,1572,1575,1577],{"class":152,"line":263},[150,1568,1569],{"class":196},"wb ",[150,1571,219],{"class":192},[150,1573,1574],{"class":196}," load_workbook(",[150,1576,1554],{"class":160},[150,1578,228],{"class":196},[150,1580,1581,1584,1586],{"class":152,"line":291},[150,1582,1583],{"class":196},"ws ",[150,1585,219],{"class":192},[150,1587,1588],{"class":196}," wb.active\n",[150,1590,1591,1593,1596,1598,1601,1604,1606,1608,1610,1613,1615,1617,1619,1622,1624,1627],{"class":152,"line":1053},[150,1592,1225],{"class":192},[150,1594,1595],{"class":196}," row ",[150,1597,1231],{"class":192},[150,1599,1600],{"class":196}," ws.iter_rows(",[150,1602,1603],{"class":279},"min_row",[150,1605,219],{"class":192},[150,1607,562],{"class":266},[150,1609,346],{"class":196},[150,1611,1612],{"class":279},"min_col",[150,1614,219],{"class":192},[150,1616,627],{"class":266},[150,1618,346],{"class":196},[150,1620,1621],{"class":279},"max_col",[150,1623,219],{"class":192},[150,1625,1626],{"class":266},"5",[150,1628,991],{"class":196},[150,1630,1631,1634,1637,1639],{"class":152,"line":1078},[150,1632,1633],{"class":192},"    for",[150,1635,1636],{"class":196}," cell ",[150,1638,1231],{"class":192},[150,1640,1641],{"class":196}," row:\n",[150,1643,1644,1647,1649],{"class":152,"line":1099},[150,1645,1646],{"class":196},"        cell.number_format ",[150,1648,219],{"class":192},[150,1650,1651],{"class":160}," \"0.0%\"\n",[150,1653,1654,1657,1659],{"class":152,"line":1111},[150,1655,1656],{"class":196},"wb.save(",[150,1658,1554],{"class":160},[150,1660,228],{"class":196},[150,1662,1663,1665,1668,1670,1672,1674,1677,1679],{"class":152,"line":1128},[150,1664,267],{"class":266},[150,1666,1667],{"class":196},"(pd.read_excel(",[150,1669,1554],{"class":160},[150,1671,356],{"class":196},[150,1673,373],{"class":160},[150,1675,1676],{"class":196},"].round(",[150,1678,273],{"class":266},[150,1680,1681],{"class":196},").tolist())\n",[10,1683,1684,1685,1688,1689,1693],{},"For a pivot-style layout, write the ",[14,1686,1687],{},"crosstab"," result instead and format its cells the same way. ",[23,1690,1692],{"href":1691},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fexport-pandas-pivot-table-to-excel-formatted\u002F","Export a Pandas Pivot Table to Excel (Formatted)"," covers the layout details.",[136,1695,1697],{"id":1696},"common-pitfalls","Common pitfalls",[1699,1700,1701,1719,1725,1731,1737,1743],"ul",{},[1702,1703,1704,1708,1709,1711,1712,1715,1716,1718],"li",{},[1705,1706,1707],"strong",{},"Dropping blank groups."," ",[14,1710,659],{}," omits ",[14,1713,1714],{},"NaN"," keys; use ",[14,1717,755],{}," or fill them.",[1702,1720,1721,1724],{},[1705,1722,1723],{},"Merging totals back instead of using transform."," It works, but it is slower and easy to get wrong when keys have duplicates.",[1702,1726,1727,1730],{},[1705,1728,1729],{},"Storing percentages as 21.7 instead of 0.217."," Keep fractions; format at the end.",[1702,1732,1733,1736],{},[1705,1734,1735],{},"Filtering after computing totals."," Shares then refer to a total that includes rows no longer shown.",[1702,1738,1739,1742],{},[1705,1740,1741],{},"Averaging percentages across groups."," The mean of each region's share of its own products is not a company-wide share; recompute from the underlying sums instead.",[1702,1744,1745,1748],{},[1705,1746,1747],{},"Negative values in the total."," Returns or credits can make shares exceed 100% or go negative; decide whether to net them or show them separately.",[136,1750,1752],{"id":1751},"performance-notes","Performance notes",[10,1754,1755,1757,1758,1760,1761,1763],{},[14,1756,320],{}," uses pandas' optimised group sums and handles millions of rows in well under a second; passing a string such as ",[14,1759,364],{}," is much faster than a Python lambda. ",[14,1762,1687],{}," builds its grid through a pivot and is equally quick for typical report sizes.",[136,1765,1767],{"id":1766},"conclusion","Conclusion",[10,1769,1770,1771,1774,1775,1778,1779,1781,1782,1785],{},"Divide by ",[14,1772,1773],{},"df[col].sum()"," for shares of the grand total and by ",[14,1776,1777],{},"groupby(...)[col].transform(\"sum\")"," for shares within each group, keeping ",[14,1780,755],{}," so rows with blank groups are not silently dropped from the totals. Use ",[14,1783,1784],{},"pd.crosstab(..., normalize=...)"," for tables of row, column or overall shares, round displayed shares with the largest-remainder method when they must total exactly 100%, and write the fractions with a percent format.",[136,1787,1789],{"id":1788},"frequently-asked-questions","Frequently asked questions",[10,1791,1792,1795],{},[1705,1793,1794],{},"How do I get each row's share of its group total?","\nDivide the value by the group total from groupby with transform(\"sum\"). transform returns the group total aligned to every row, so the division happens row by row without a merge.",[10,1797,1798,1801],{},[1705,1799,1800],{},"What is the pandas equivalent of Excel's \"Show values as % of row total\"?","\npd.crosstab with values, aggfunc=\"sum\" and normalize=\"index\" gives each row as fractions of its row total; normalize=\"columns\" gives column shares and normalize=\"all\" shares of the grand total.",[10,1803,1804,1807],{},[1705,1805,1806],{},"Why do my rounded percentages add up to 99% or 101%?","\nRounding each share independently loses or gains fractions of a percent. When the displayed shares must total exactly 100%, use the largest-remainder method: round down, then give the leftover points to the shares with the largest remainders.",[10,1809,1810,1813],{},[1705,1811,1812],{},"What happens to rows whose group is blank?","\ngroupby drops NaN keys by default, so their values vanish from the totals and every share is computed against a smaller total. Pass dropna=False, or fill the blank with a label such as \"Unassigned\", so the shares still describe all the data.",[136,1815,1817],{"id":1816},"related","Related",[1699,1819,1820,1826,1833,1840],{},[1702,1821,1822,1823,1825],{},"Up one level: ",[23,1824,26],{"href":25}," — pivots, subtotals and exports.",[1702,1827,1828,1832],{},[23,1829,1831],{"href":1830},"\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"," — totals alongside the shares.",[1702,1834,1835,1839],{},[23,1836,1838],{"href":1837},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Frun-a-pareto-analysis-on-excel-data-with-pandas\u002F","Run a Pareto Analysis on Excel Data with pandas"," — cumulative shares.",[1702,1841,1842,1846],{},[23,1843,1845],{"href":1844},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-percentage-strings-to-numbers-with-pandas\u002F","Convert Percentage Strings Like \"12.5%\" to Numbers with pandas"," — reading percentages back in.",[1848,1849,1850],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":146,"searchDepth":206,"depth":206,"links":1852},[1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865],{"id":138,"depth":206,"text":139},{"id":176,"depth":206,"text":177},{"id":313,"depth":206,"text":314},{"id":652,"depth":206,"text":653},{"id":763,"depth":206,"text":764},{"id":947,"depth":206,"text":948},{"id":1359,"depth":206,"text":1360},{"id":1479,"depth":206,"text":1480},{"id":1696,"depth":206,"text":1697},{"id":1751,"depth":206,"text":1752},{"id":1766,"depth":206,"text":1767},{"id":1788,"depth":206,"text":1789},{"id":1816,"depth":206,"text":1817},"2026-09-25","Show each product's share of its region's sales, each region's share of the company, and a crosstab where every row adds to 100%. Use groupby transform for shares within groups, crosstab normalize for tables, keep rows with missing groups, and round shares so they still add up to exactly 100%.","md",[1870,1872,1874,1876],{"q":1794,"a":1871},"Divide the value by the group total from groupby with transform(\"sum\"). transform returns the group total aligned to every row, so the division happens row by row without a merge.",{"q":1800,"a":1873},"pd.crosstab with values, aggfunc=\"sum\" and normalize=\"index\" gives each row as fractions of its row total; normalize=\"columns\" gives column shares and normalize=\"all\" shares of the grand total.",{"q":1806,"a":1875},"Rounding each share independently loses or gains fractions of a percent. When the displayed shares must total exactly 100%, use the largest-remainder method: round down, then give the leftover points to the shares with the largest remainders.",{"q":1812,"a":1877},"groupby drops NaN keys by default, so their values vanish from the totals and every share is computed against a smaller total. Pass dropna=False, or fill the blank with a label such as \"Unassigned\", so the shares still describe all the data.","calculate percent of total per group in pandas",null,{"breadcrumb":1881},[1882,1884,1887],{"name":1883,"item":253},"Home",{"name":1885,"item":1886},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":26,"item":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas",{"title":1890,"description":1891},"Percent of Total and Share Within Group in pandas","Calculate percentage of total in pandas: share of the grand total, share within each group with groupby transform, crosstab normalize by row or column, missing group values, and rounding shares that sum to 100%.","calculate-percent-of-total-per-group-in-pandas","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas\u002Findex",[1895,1896,1897],"pivots-and-summaries","pandas","formula-equivalents","how-to","CfaoVZnPsNPiA52wuYcyMvL4QpaNKco4_DVNinfIhSo",[1901,1905],{"title":1902,"path":1903,"stem":1904,"children":-1},"Calculate Moving Averages with pandas rolling","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling\u002Findex",{"title":1906,"path":1907,"stem":1908,"children":-1},"Compare Two Periods Side by Side in a Pivot","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcompare-two-periods-side-by-side-in-a-pivot","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcompare-two-periods-side-by-side-in-a-pivot\u002Findex",[1910,1912],{"path":1911,"title":26},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data",{"path":1913,"title":1885},"\u002Fadvanced-data-transformation-and-cleaning",[],[1916,1919,1922,1925,1927,1930],{"path":1917,"title":1918,"kind":1898},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data","Build a Customer Churn Report from Subscription Data",{"path":1920,"title":1921,"kind":1898},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Frefresh-pivot-tables-and-queries-in-excel-with-python","Refresh Excel Pivot Tables and Queries with Python",{"path":1923,"title":1924,"kind":1898},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fadd-a-native-excel-pivot-table-with-python","Add a Native Excel Pivot Table with Python",{"path":1926,"title":1831,"kind":1898},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fadd-subtotals-and-grand-totals-to-a-pandas-pivot-table",{"path":1928,"title":1929,"kind":1898},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbin-values-into-ranges-with-pandas-cut","Bin Values into Ranges Like Excel FREQUENCY with pd.cut",{"path":1931,"title":1932,"kind":1898},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-cohort-retention-table-from-excel-data","Build a Cohort Retention Table from Excel Data",1790972086358]