[{"data":1,"prerenderedAt":2168},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumproduct-and-weighted-average-in-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumproduct-and-weighted-average-in-pandas":2136,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumproduct-and-weighted-average-in-pandas":2144,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumproduct-and-weighted-average-in-pandas":2149,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumproduct-and-weighted-average-in-pandas":2150},{"id":4,"title":5,"body":6,"dateModified":2103,"datePublished":2103,"description":2104,"extension":2105,"faq":2106,"keyword":2115,"kind":2116,"meta":2117,"navigation":470,"path":2125,"seo":2126,"slug":2129,"stem":2130,"topics":2131,"type":2134,"__hash__":2135},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumproduct-and-weighted-average-in-pandas\u002Findex.md","SUMPRODUCT and Weighted Averages in pandas",{"type":7,"value":8,"toc":2087},"minimark",[9,35,381,386,416,422,426,429,536,554,563,567,573,682,706,794,798,819,891,901,905,912,1062,1080,1084,1087,1196,1203,1207,1210,1286,1289,1399,1403,1406,1636,1644,1648,1651,1925,1938,1942,1976,1980,1989,1993,2002,2006,2024,2038,2044,2050,2054,2083],[10,11,12,16,17,20,21,24,25,28,29,34],"p",{},[13,14,15],"code",{},"SUMPRODUCT"," is the Swiss army knife of Excel formulas: ",[13,18,19],{},"=SUMPRODUCT(B2:B7, C2:C7)"," multiplies quantities by prices and adds them up, ",[13,22,23],{},"=SUMPRODUCT((A2:A7=\"North\")*B2:B7*C2:C7)"," does the same for one region, and ",[13,26,27],{},"=SUMPRODUCT(B2:B7,C2:C7)\u002FSUM(B2:B7)"," is the usual way to write a weighted average. In pandas all three are vectorised arithmetic on columns, which is shorter and easier to read. The part that needs care is not the syntax but the weighted average itself — what to do with missing weights, how to do it per group, and why averaging averages gives a different, wrong answer. This page, part of ",[30,31,33],"a",{"href":32},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","Excel Formula Equivalents in pandas",", covers all of it.",[36,37,45,46,45,50,45,54,45,62,45,69,45,78,45,82,45,88,45,92,45,96,45,100,45,104,45,108,45,112,45,116,45,120,45,124,45,129,45,131,45,135,45,137,45,140,45,142,45,145,45,147,45,150,45,152,45,155,45,158,45,162,45,164,45,168,45,170,45,173,45,175,45,178,45,180,45,183,45,188,45,192,45,195,45,199,45,201,45,203,45,205,45,208,45,210,45,213,45,215,45,218,45,220,45,223,45,226,45,230,45,232,45,234,45,236,45,239,45,241,45,244,45,246,45,249,45,251,45,254,45,257,45,260,45,262,45,265,45,267,45,269,45,271,45,274,45,276,45,279,45,281,45,284,45,287,45,291,45,293,45,295,45,297,45,299,45,301,45,304,45,306,45,309,45,311,45,314,45,317,45,321,45,323,45,326,45,328,45,330,45,332,45,334,45,337,45,339,45,347,45,352,45,359,45,370,45,376],"svg",{"viewBox":38,"role":39,"ariaLabelledBy":40,"xmlns":43,"style":44},"80 0 599 284","img",[41,42],"sp-sheet-t","sp-sheet-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:599px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[47,48,49],"title",{"id":41},"SUMPRODUCT over quantity and price",[51,52,53],"desc",{"id":42},"Order lines with units and price. SUMPRODUCT multiplies each row's units by its price — 250, 240, 200, 112 and 690 — and adds the products to give a total value of 1,492. The East row has no units and contributes nothing.",[55,56],"rect",{"x":57,"y":58,"width":59,"height":60,"fill":61},"80","0","599","284","#ffffff",[63,64,68],"text",{"x":65,"y":66,"style":67},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","=SUMPRODUCT(C2:C7, D2:D7)",[55,70],{"x":71,"y":72,"width":73,"height":74,"rx":58,"fill":75,"stroke":76,"style":77},"97.0","48.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[55,79],{"x":80,"y":72,"width":81,"height":74,"rx":58,"fill":75,"stroke":76,"style":77},"129.0","63.6",[63,83,87],{"x":84,"y":85,"style":86},"160.8","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[55,89],{"x":90,"y":72,"width":91,"height":74,"rx":58,"fill":75,"stroke":76,"style":77},"192.6","75.8",[63,93,95],{"x":94,"y":85,"style":86},"230.5","B",[55,97],{"x":98,"y":72,"width":99,"height":74,"rx":58,"fill":75,"stroke":76,"style":77},"268.4","54.8",[63,101,103],{"x":102,"y":85,"style":86},"295.8","C",[55,105],{"x":106,"y":72,"width":107,"height":74,"rx":58,"fill":75,"stroke":76,"style":77},"323.2","62.2",[63,109,111],{"x":110,"y":85,"style":86},"354.3","D",[55,113],{"x":114,"y":72,"width":115,"height":74,"rx":58,"fill":75,"stroke":76,"style":77},"385.4","57.7",[63,117,119],{"x":118,"y":85,"style":86},"414.2","E",[55,121],{"x":71,"y":122,"width":73,"height":123,"rx":58,"fill":75,"stroke":76,"style":77},"72.0","28.0",[63,125,128],{"x":126,"y":127,"style":86},"113.0","91.0","1",[55,130],{"x":80,"y":122,"width":81,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,132,134],{"x":84,"y":127,"style":133},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","region",[55,136],{"x":90,"y":122,"width":91,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,138,139],{"x":94,"y":127,"style":133},"product",[55,141],{"x":98,"y":122,"width":99,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,143,144],{"x":102,"y":127,"style":133},"units",[55,146],{"x":106,"y":122,"width":107,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,148,149],{"x":110,"y":127,"style":133},"price",[55,151],{"x":114,"y":122,"width":115,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,153,154],{"x":118,"y":127,"style":133},"value",[55,156],{"x":71,"y":157,"width":73,"height":123,"rx":58,"fill":75,"stroke":76,"style":77},"100.0",[63,159,161],{"x":126,"y":160,"style":86},"119.0","2",[55,163],{"x":80,"y":157,"width":81,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,165,167],{"x":84,"y":160,"style":166},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","North",[55,169],{"x":90,"y":157,"width":91,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,171,172],{"x":94,"y":160,"style":166},"Widgets",[55,174],{"x":98,"y":157,"width":99,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,176,177],{"x":102,"y":160,"style":166},"100",[55,179],{"x":106,"y":157,"width":107,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,181,182],{"x":110,"y":160,"style":166},"2.50",[55,184],{"x":114,"y":157,"width":115,"height":123,"rx":58,"fill":185,"stroke":186,"style":187},"#d9f4f1","var(--teal,#0f9488)","stroke-width:1.5px",[63,189,191],{"x":118,"y":160,"style":190},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","250",[55,193],{"x":71,"y":194,"width":73,"height":123,"rx":58,"fill":75,"stroke":76,"style":77},"128.0",[63,196,198],{"x":126,"y":197,"style":86},"147.0","3",[55,200],{"x":80,"y":194,"width":81,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,202,167],{"x":84,"y":197,"style":166},[55,204],{"x":90,"y":194,"width":91,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,206,207],{"x":94,"y":197,"style":166},"Gadgets",[55,209],{"x":98,"y":194,"width":99,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,211,212],{"x":102,"y":197,"style":166},"20",[55,214],{"x":106,"y":194,"width":107,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,216,217],{"x":110,"y":197,"style":166},"12.00",[55,219],{"x":114,"y":194,"width":115,"height":123,"rx":58,"fill":185,"stroke":186,"style":187},[63,221,222],{"x":118,"y":197,"style":190},"240",[55,224],{"x":71,"y":225,"width":73,"height":123,"rx":58,"fill":75,"stroke":76,"style":77},"156.0",[63,227,229],{"x":126,"y":228,"style":86},"175.0","4",[55,231],{"x":80,"y":225,"width":81,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,233,167],{"x":84,"y":228,"style":166},[55,235],{"x":90,"y":225,"width":91,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,237,238],{"x":94,"y":228,"style":166},"Gizmos",[55,240],{"x":98,"y":225,"width":99,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,242,243],{"x":102,"y":228,"style":166},"5",[55,245],{"x":106,"y":225,"width":107,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,247,248],{"x":110,"y":228,"style":166},"40.00",[55,250],{"x":114,"y":225,"width":115,"height":123,"rx":58,"fill":185,"stroke":186,"style":187},[63,252,253],{"x":118,"y":228,"style":190},"200",[55,255],{"x":71,"y":256,"width":73,"height":123,"rx":58,"fill":75,"stroke":76,"style":77},"184.0",[63,258,243],{"x":126,"y":259,"style":86},"203.0",[55,261],{"x":80,"y":256,"width":81,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,263,264],{"x":84,"y":259,"style":166},"South",[55,266],{"x":90,"y":256,"width":91,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,268,172],{"x":94,"y":259,"style":166},[55,270],{"x":98,"y":256,"width":99,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,272,273],{"x":102,"y":259,"style":166},"40",[55,275],{"x":106,"y":256,"width":107,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,277,278],{"x":110,"y":259,"style":166},"2.80",[55,280],{"x":114,"y":256,"width":115,"height":123,"rx":58,"fill":185,"stroke":186,"style":187},[63,282,283],{"x":118,"y":259,"style":190},"112",[55,285],{"x":71,"y":286,"width":73,"height":123,"rx":58,"fill":75,"stroke":76,"style":77},"212.0",[63,288,290],{"x":126,"y":289,"style":86},"231.0","6",[55,292],{"x":80,"y":286,"width":81,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,294,264],{"x":84,"y":289,"style":166},[55,296],{"x":90,"y":286,"width":91,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,298,207],{"x":94,"y":289,"style":166},[55,300],{"x":98,"y":286,"width":99,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,302,303],{"x":102,"y":289,"style":166},"60",[55,305],{"x":106,"y":286,"width":107,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,307,308],{"x":110,"y":289,"style":166},"11.50",[55,310],{"x":114,"y":286,"width":115,"height":123,"rx":58,"fill":185,"stroke":186,"style":187},[63,312,313],{"x":118,"y":289,"style":190},"690",[55,315],{"x":71,"y":316,"width":73,"height":123,"rx":58,"fill":75,"stroke":76,"style":77},"240.0",[63,318,320],{"x":126,"y":319,"style":86},"259.0","7",[55,322],{"x":80,"y":316,"width":81,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,324,325],{"x":84,"y":319,"style":166},"East",[55,327],{"x":90,"y":316,"width":91,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,329,172],{"x":94,"y":319,"style":166},[55,331],{"x":98,"y":316,"width":99,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[55,333],{"x":106,"y":316,"width":107,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[63,335,336],{"x":110,"y":319,"style":166},"2.60",[55,338],{"x":114,"y":316,"width":115,"height":123,"rx":58,"fill":61,"stroke":76,"style":77},[340,341],"line",{"x1":342,"y1":343,"x2":344,"y2":343,"stroke":345,"style":346},"451.0","158.0","483.0","var(--brand,#5b5cf0)","stroke-width:2px",[348,349],"polygon",{"points":350,"fill":351},"487.0,158.0 478.0,163.0 478.0,153.0","#5b5cf0",[55,353],{"x":354,"y":355,"width":356,"height":357,"rx":358,"fill":185,"stroke":186,"style":346},"493.0","111.5","170.0","93.0","12",[360,361,363],"g",{"transform":362},"translate(566.0,124.5) scale(1.000)",[364,365],"path",{"d":366,"fill":367,"stroke":368,"style":369},"M5 12.5l4.5 4.5L19 7","none","var(--teal-ink,#0b6157)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[63,371,375],{"x":372,"y":373,"style":374},"578.0","171.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Total 1,492",[63,377,380],{"x":372,"y":378,"style":379},"190.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","the sum of the products",[382,383,385],"h2",{"id":384},"prerequisites","Prerequisites",[387,388,393],"pre",{"className":389,"code":390,"language":391,"meta":392,"style":392},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl numpy\n","bash","",[13,394,395],{"__ignoreMap":392},[396,397,399,403,407,410,413],"span",{"class":340,"line":398},1,[396,400,402],{"class":401},"sMTad","pip",[396,404,406],{"class":405},"srMev"," install",[396,408,409],{"class":405}," pandas",[396,411,412],{"class":405}," openpyxl",[396,414,415],{"class":405}," numpy\n",[10,417,418,421],{},[13,419,420],{},"lines.xlsx"," has six order lines with units, price and margin. The East line has no units recorded.",[382,423,425],{"id":424},"step-1-sumproduct-as-multiply-then-sum","Step 1 — SUMPRODUCT as multiply-then-sum",[10,427,428],{},"The direct translation multiplies the columns row by row and sums the products:",[387,430,434],{"className":431,"code":432,"language":433,"meta":392,"style":392},"language-python shiki shiki-themes github-light github-dark-high-contrast","import numpy as np\nimport pandas as pd\n\nlines = pd.read_excel(\"lines.xlsx\")\ntotal_value = (lines[\"units\"] * lines[\"price\"]).sum()\nprint(total_value)\n# 1492.0\n","python",[13,435,436,452,465,472,490,519,529],{"__ignoreMap":392},[396,437,438,442,446,449],{"class":340,"line":398},[396,439,441],{"class":440},"s-kum","import",[396,443,445],{"class":444},"skGVy"," numpy ",[396,447,448],{"class":440},"as",[396,450,451],{"class":444}," np\n",[396,453,455,457,460,462],{"class":340,"line":454},2,[396,456,441],{"class":440},[396,458,459],{"class":444}," pandas ",[396,461,448],{"class":440},[396,463,464],{"class":444}," pd\n",[396,466,468],{"class":340,"line":467},3,[396,469,471],{"emptyLinePlaceholder":470},true,"\n",[396,473,475,478,481,484,487],{"class":340,"line":474},4,[396,476,477],{"class":444},"lines ",[396,479,480],{"class":440},"=",[396,482,483],{"class":444}," pd.read_excel(",[396,485,486],{"class":405},"\"lines.xlsx\"",[396,488,489],{"class":444},")\n",[396,491,493,496,498,501,504,507,510,513,516],{"class":340,"line":492},5,[396,494,495],{"class":444},"total_value ",[396,497,480],{"class":440},[396,499,500],{"class":444}," (lines[",[396,502,503],{"class":405},"\"units\"",[396,505,506],{"class":444},"] ",[396,508,509],{"class":440},"*",[396,511,512],{"class":444}," lines[",[396,514,515],{"class":405},"\"price\"",[396,517,518],{"class":444},"]).sum()\n",[396,520,522,526],{"class":340,"line":521},6,[396,523,525],{"class":524},"sP0c6","print",[396,527,528],{"class":444},"(total_value)\n",[396,530,532],{"class":340,"line":531},7,[396,533,535],{"class":534},"s-wDw","# 1492.0\n",[10,537,538,539,542,543,546,547,549,550,553],{},"Multiplying two columns gives a new Series of products — the \"value\" column in the diagram — and ",[13,540,541],{},"sum()"," adds them, skipping the ",[13,544,545],{},"NaN"," from the East line, just as ",[13,548,15],{}," treats an empty cell as zero. Keeping the product as a named column (",[13,551,552],{},"lines[\"value\"] = lines[\"units\"] * lines[\"price\"]",") is often clearer still, because the intermediate figures can be checked and reused.",[10,555,556,559,560,562],{},[13,557,558],{},"Series.dot"," computes the same thing in one call, but it does not skip missing values — the East line makes it return ",[13,561,545],{},". Prefer the multiply-and-sum form on real data.",[382,564,566],{"id":565},"step-2-conditional-sumproduct-with-boolean-masks","Step 2 — Conditional SUMPRODUCT with boolean masks",[10,568,569,570,572],{},"Excel users put conditions into ",[13,571,15],{}," by multiplying by comparisons that evaluate to 1 or 0. In pandas, filter with a boolean mask instead — the same logic, stated directly:",[387,574,576],{"className":431,"code":575,"language":433,"meta":392,"style":392},"north = lines[\"region\"] == \"North\"\nprint((lines.loc[north, \"units\"] * lines.loc[north, \"price\"]).sum())\n\ncheap_widgets = (lines[\"product\"] == \"Widgets\") & (lines[\"price\"] \u003C 2.75)\nprint((lines.loc[cheap_widgets, \"units\"] * lines.loc[cheap_widgets, \"price\"]).sum())\n",[13,577,578,598,619,623,662],{"__ignoreMap":392},[396,579,580,583,585,587,590,592,595],{"class":340,"line":398},[396,581,582],{"class":444},"north ",[396,584,480],{"class":440},[396,586,512],{"class":444},[396,588,589],{"class":405},"\"region\"",[396,591,506],{"class":444},[396,593,594],{"class":440},"==",[396,596,597],{"class":405}," \"North\"\n",[396,599,600,602,605,607,609,611,614,616],{"class":340,"line":454},[396,601,525],{"class":524},[396,603,604],{"class":444},"((lines.loc[north, ",[396,606,503],{"class":405},[396,608,506],{"class":444},[396,610,509],{"class":440},[396,612,613],{"class":444}," lines.loc[north, ",[396,615,515],{"class":405},[396,617,618],{"class":444},"]).sum())\n",[396,620,621],{"class":340,"line":467},[396,622,471],{"emptyLinePlaceholder":470},[396,624,625,628,630,632,635,637,639,642,645,648,650,652,654,657,660],{"class":340,"line":474},[396,626,627],{"class":444},"cheap_widgets ",[396,629,480],{"class":440},[396,631,500],{"class":444},[396,633,634],{"class":405},"\"product\"",[396,636,506],{"class":444},[396,638,594],{"class":440},[396,640,641],{"class":405}," \"Widgets\"",[396,643,644],{"class":444},") ",[396,646,647],{"class":440},"&",[396,649,500],{"class":444},[396,651,515],{"class":405},[396,653,506],{"class":444},[396,655,656],{"class":440},"\u003C",[396,658,659],{"class":524}," 2.75",[396,661,489],{"class":444},[396,663,664,666,669,671,673,675,678,680],{"class":340,"line":492},[396,665,525],{"class":524},[396,667,668],{"class":444},"((lines.loc[cheap_widgets, ",[396,670,503],{"class":405},[396,672,506],{"class":444},[396,674,509],{"class":440},[396,676,677],{"class":444}," lines.loc[cheap_widgets, ",[396,679,515],{"class":405},[396,681,618],{"class":444},[10,683,684,685,687,688,691,692,696,697,700,701,705],{},"North's lines are worth 690. The second example combines two conditions with ",[13,686,647],{},", the equivalent of ",[13,689,690],{},"=SUMPRODUCT((B2:B7=\"Widgets\")*(D2:D7\u003C2.75)*C2:C7*D2:D7)",", and gives 250. For sums by ",[693,694,695],"em",{},"every"," region at once, compute the product column and group it — ",[13,698,699],{},"lines.assign(value=lines[\"units\"] * lines[\"price\"]).groupby(\"region\")[\"value\"].sum()"," — rather than writing one mask per region. ",[30,702,704],{"href":703},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumif-and-sumifs-equivalent-in-pandas\u002F","SUMIF and SUMIFS Equivalent in pandas"," covers conditional sums of a single column.",[36,707,45,713,45,716,45,719,45,723,45,729,45,735,45,741,45,745,45,750,45,752,45,756,45,759,45,763,45,765,45,769,45,772,45,776,45,778,45,782,45,784,45,788,45,790],{"viewBox":708,"role":39,"ariaLabelledBy":709,"xmlns":43,"style":712},"4 0 752 208",[710,711],"sp-map-t","sp-map-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[47,714,715],{"id":710},"SUMPRODUCT patterns and their pandas forms",[51,717,718],{"id":711},"A translation table. SUMPRODUCT of two ranges becomes multiplying the columns and summing. A SUMPRODUCT with a condition multiplied in becomes a boolean mask and loc. SUMPRODUCT divided by SUM of the weights becomes a weighted average. A SUMPRODUCT per group becomes a product column summed with groupby.",[55,720],{"x":229,"y":58,"width":721,"height":722,"fill":61},"752","208",[55,724],{"x":725,"y":726,"width":727,"height":728,"rx":58,"fill":61,"stroke":76,"style":77},"20.0","16.0","295.9","40.0",[55,730],{"x":731,"y":726,"width":732,"height":728,"rx":58,"fill":733,"stroke":345,"style":734},"315.9","424.1","#f0f4ff","stroke-width:1.2px",[63,736,740],{"x":737,"y":738,"style":739},"528.0","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pandas",[55,742],{"x":725,"y":743,"width":727,"height":744,"rx":58,"fill":75,"stroke":76,"style":77},"56.0","34.0",[63,746,749],{"x":73,"y":747,"style":748},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","=SUMPRODUCT(B:B, C:C)",[55,751],{"x":731,"y":743,"width":732,"height":744,"rx":58,"fill":61,"stroke":76,"style":77},[63,753,755],{"x":737,"y":754,"style":379},"77.0","(df.b * df.c).sum()",[55,757],{"x":725,"y":758,"width":727,"height":744,"rx":58,"fill":75,"stroke":76,"style":77},"90.0",[63,760,762],{"x":73,"y":761,"style":748},"112.0","=SUMPRODUCT((A:A=\"North\")*B:B*C:C)",[55,764],{"x":731,"y":758,"width":732,"height":744,"rx":58,"fill":61,"stroke":76,"style":77},[63,766,768],{"x":737,"y":767,"style":379},"111.0","mask, then (b * c).sum()",[55,770],{"x":725,"y":771,"width":727,"height":744,"rx":58,"fill":75,"stroke":76,"style":77},"124.0",[63,773,775],{"x":73,"y":774,"style":748},"146.0","=SUMPRODUCT(B:B, C:C) \u002F SUM(B:B)",[55,777],{"x":731,"y":771,"width":732,"height":744,"rx":58,"fill":61,"stroke":76,"style":77},[63,779,781],{"x":737,"y":780,"style":379},"145.0","(b * c).sum() \u002F b.sum()",[55,783],{"x":725,"y":343,"width":727,"height":744,"rx":58,"fill":75,"stroke":76,"style":77},[63,785,787],{"x":73,"y":786,"style":748},"180.0","SUMPRODUCT per region",[55,789],{"x":731,"y":343,"width":732,"height":744,"rx":58,"fill":61,"stroke":76,"style":77},[63,791,793],{"x":737,"y":792,"style":379},"179.0","df.assign(v=b*c).groupby(\"region\").v.sum()",[382,795,797],{"id":796},"counting-with-sumproduct-conditions","Counting with SUMPRODUCT conditions",[10,799,800,801,803,804,807,808,811,812,814,815,818],{},"Excel users also use ",[13,802,15],{}," purely to count rows meeting several conditions — ",[13,805,806],{},"=SUMPRODUCT((A2:A7=\"North\")*(C2:C7>10))"," — because it worked before ",[13,809,810],{},"COUNTIFS"," existed and still handles conditions ",[13,813,810],{}," cannot, such as comparisons between two columns. In pandas, a boolean mask summed counts its ",[13,816,817],{},"True"," values:",[387,820,822],{"className":431,"code":821,"language":433,"meta":392,"style":392},"print(((lines[\"region\"] == \"North\") & (lines[\"units\"] > 10)).sum())\nprint((lines[\"units\"] * lines[\"price\"] > 200).sum(), \"lines worth more than 200\")\n",[13,823,824,859],{"__ignoreMap":392},[396,825,826,828,831,833,835,837,840,842,844,846,848,850,853,856],{"class":340,"line":398},[396,827,525],{"class":524},[396,829,830],{"class":444},"(((lines[",[396,832,589],{"class":405},[396,834,506],{"class":444},[396,836,594],{"class":440},[396,838,839],{"class":405}," \"North\"",[396,841,644],{"class":444},[396,843,647],{"class":440},[396,845,500],{"class":444},[396,847,503],{"class":405},[396,849,506],{"class":444},[396,851,852],{"class":440},">",[396,854,855],{"class":524}," 10",[396,857,858],{"class":444},")).sum())\n",[396,860,861,863,866,868,870,872,874,876,878,880,883,886,889],{"class":340,"line":454},[396,862,525],{"class":524},[396,864,865],{"class":444},"((lines[",[396,867,503],{"class":405},[396,869,506],{"class":444},[396,871,509],{"class":440},[396,873,512],{"class":444},[396,875,515],{"class":405},[396,877,506],{"class":444},[396,879,852],{"class":440},[396,881,882],{"class":524}," 200",[396,884,885],{"class":444},").sum(), ",[396,887,888],{"class":405},"\"lines worth more than 200\"",[396,890,489],{"class":444},[10,892,893,894,897,898,900],{},"Two North lines have more than ten units, and three lines are worth more than 200. The second count compares a ",[693,895,896],{},"computed"," value, which ",[13,899,810],{}," cannot do in Excel without a helper column; in pandas it is the same one-liner.",[382,902,904],{"id":903},"step-3-weighted-averages","Step 3 — Weighted averages",[10,906,907,908,911],{},"The average price paid is not the mean of the price column; it is total value divided by total units, which weights each price by the units sold at it. That is ",[13,909,910],{},"SUMPRODUCT(units, price) \u002F SUM(units)",":",[387,913,915],{"className":431,"code":914,"language":433,"meta":392,"style":392},"simple = lines[\"price\"].mean()\nknown = lines[\"units\"].notna() & lines[\"price\"].notna()\nweighted = (lines.loc[known, \"price\"] * lines.loc[known, \"units\"]).sum() \u002F lines.loc[known, \"units\"].sum()\nprint(f\"simple mean price: {simple:.2f}   weighted by units: {weighted:.2f}\")\nprint(np.average(lines.loc[known, \"price\"], weights=lines.loc[known, \"units\"]).round(2))\n",[13,916,917,931,954,988,1031],{"__ignoreMap":392},[396,918,919,922,924,926,928],{"class":340,"line":398},[396,920,921],{"class":444},"simple ",[396,923,480],{"class":440},[396,925,512],{"class":444},[396,927,515],{"class":405},[396,929,930],{"class":444},"].mean()\n",[396,932,933,936,938,940,942,945,947,949,951],{"class":340,"line":454},[396,934,935],{"class":444},"known ",[396,937,480],{"class":440},[396,939,512],{"class":444},[396,941,503],{"class":405},[396,943,944],{"class":444},"].notna() ",[396,946,647],{"class":440},[396,948,512],{"class":444},[396,950,515],{"class":405},[396,952,953],{"class":444},"].notna()\n",[396,955,956,959,961,964,966,968,970,973,975,978,981,983,985],{"class":340,"line":467},[396,957,958],{"class":444},"weighted ",[396,960,480],{"class":440},[396,962,963],{"class":444}," (lines.loc[known, ",[396,965,515],{"class":405},[396,967,506],{"class":444},[396,969,509],{"class":440},[396,971,972],{"class":444}," lines.loc[known, ",[396,974,503],{"class":405},[396,976,977],{"class":444},"]).sum() ",[396,979,980],{"class":440},"\u002F",[396,982,972],{"class":444},[396,984,503],{"class":405},[396,986,987],{"class":444},"].sum()\n",[396,989,990,992,995,998,1001,1005,1008,1011,1014,1017,1019,1022,1024,1026,1029],{"class":340,"line":474},[396,991,525],{"class":524},[396,993,994],{"class":444},"(",[396,996,997],{"class":440},"f",[396,999,1000],{"class":405},"\"simple mean price: ",[396,1002,1004],{"class":1003},"sSjpA","{",[396,1006,1007],{"class":444},"simple",[396,1009,1010],{"class":440},":.2f",[396,1012,1013],{"class":1003},"}",[396,1015,1016],{"class":405},"   weighted by units: ",[396,1018,1004],{"class":1003},[396,1020,1021],{"class":444},"weighted",[396,1023,1010],{"class":440},[396,1025,1013],{"class":1003},[396,1027,1028],{"class":405},"\"",[396,1030,489],{"class":444},[396,1032,1033,1035,1038,1040,1043,1047,1049,1052,1054,1057,1059],{"class":340,"line":492},[396,1034,525],{"class":524},[396,1036,1037],{"class":444},"(np.average(lines.loc[known, ",[396,1039,515],{"class":405},[396,1041,1042],{"class":444},"], ",[396,1044,1046],{"class":1045},"sa561","weights",[396,1048,480],{"class":440},[396,1050,1051],{"class":444},"lines.loc[known, ",[396,1053,503],{"class":405},[396,1055,1056],{"class":444},"]).round(",[396,1058,161],{"class":524},[396,1060,1061],{"class":444},"))\n",[10,1063,1064,1065,1068,1069,1071,1072,1075,1076,1079],{},"The simple mean is 11.90 because the expensive Gizmos, sold in tiny numbers, pull it up; the weighted average is 6.63, what customers actually paid per unit on average. ",[13,1066,1067],{},"numpy.average"," with ",[13,1070,1046],{}," computes the same figure. Build the ",[13,1073,1074],{},"known"," mask once and use it for both the numerator and the denominator: a row with a missing weight must drop out of ",[693,1077,1078],{},"both"," sums, or the average is computed over mismatched rows.",[382,1081,1083],{"id":1082},"step-4-weighted-averages-per-group","Step 4 — Weighted averages per group",[10,1085,1086],{},"A weighted average per region needs both sums per group. Compute the product column, sum both columns by group, and divide:",[387,1088,1090],{"className":431,"code":1089,"language":433,"meta":392,"style":392},"lines[\"value\"] = lines[\"units\"] * lines[\"price\"]\nby_region = lines.dropna(subset=[\"units\"]).groupby(\"region\")[[\"value\", \"units\"]].sum()\nby_region[\"avg_price\"] = (by_region[\"value\"] \u002F by_region[\"units\"]).round(2)\nprint(by_region)\n",[13,1091,1092,1119,1157,1189],{"__ignoreMap":392},[396,1093,1094,1097,1100,1102,1104,1106,1108,1110,1112,1114,1116],{"class":340,"line":398},[396,1095,1096],{"class":444},"lines[",[396,1098,1099],{"class":405},"\"value\"",[396,1101,506],{"class":444},[396,1103,480],{"class":440},[396,1105,512],{"class":444},[396,1107,503],{"class":405},[396,1109,506],{"class":444},[396,1111,509],{"class":440},[396,1113,512],{"class":444},[396,1115,515],{"class":405},[396,1117,1118],{"class":444},"]\n",[396,1120,1121,1124,1126,1129,1132,1134,1137,1139,1142,1144,1147,1149,1152,1154],{"class":340,"line":454},[396,1122,1123],{"class":444},"by_region ",[396,1125,480],{"class":440},[396,1127,1128],{"class":444}," lines.dropna(",[396,1130,1131],{"class":1045},"subset",[396,1133,480],{"class":440},[396,1135,1136],{"class":444},"[",[396,1138,503],{"class":405},[396,1140,1141],{"class":444},"]).groupby(",[396,1143,589],{"class":405},[396,1145,1146],{"class":444},")[[",[396,1148,1099],{"class":405},[396,1150,1151],{"class":444},", ",[396,1153,503],{"class":405},[396,1155,1156],{"class":444},"]].sum()\n",[396,1158,1159,1162,1165,1167,1169,1172,1174,1176,1178,1181,1183,1185,1187],{"class":340,"line":467},[396,1160,1161],{"class":444},"by_region[",[396,1163,1164],{"class":405},"\"avg_price\"",[396,1166,506],{"class":444},[396,1168,480],{"class":440},[396,1170,1171],{"class":444}," (by_region[",[396,1173,1099],{"class":405},[396,1175,506],{"class":444},[396,1177,980],{"class":440},[396,1179,1180],{"class":444}," by_region[",[396,1182,503],{"class":405},[396,1184,1056],{"class":444},[396,1186,161],{"class":524},[396,1188,489],{"class":444},[396,1190,1191,1193],{"class":340,"line":474},[396,1192,525],{"class":524},[396,1194,1195],{"class":444},"(by_region)\n",[10,1197,1198,1199,1202],{},"Summing first and dividing afterwards is the key: it gives each region's true average price per unit. ",[13,1200,1201],{},"groupby().apply(lambda g: np.average(g[\"price\"], weights=g[\"units\"]))"," returns the same numbers but is slower and harder to extend when you also want the totals. East, whose only line has no units, drops out rather than showing a meaningless figure.",[382,1204,1206],{"id":1205},"step-5-do-not-average-the-averages","Step 5 — Do not average the averages",[10,1208,1209],{},"A company-wide average price calculated as the mean of the regional averages is wrong, for the same reason the simple mean was:",[387,1211,1213],{"className":431,"code":1212,"language":433,"meta":392,"style":392},"wrong = by_region[\"avg_price\"].mean()\nright = by_region[\"value\"].sum() \u002F by_region[\"units\"].sum()\nprint(f\"mean of regional averages: {wrong:.2f}   company-wide weighted: {right:.2f}\")\n",[13,1214,1215,1228,1250],{"__ignoreMap":392},[396,1216,1217,1220,1222,1224,1226],{"class":340,"line":398},[396,1218,1219],{"class":444},"wrong ",[396,1221,480],{"class":440},[396,1223,1180],{"class":444},[396,1225,1164],{"class":405},[396,1227,930],{"class":444},[396,1229,1230,1233,1235,1237,1239,1242,1244,1246,1248],{"class":340,"line":454},[396,1231,1232],{"class":444},"right ",[396,1234,480],{"class":440},[396,1236,1180],{"class":444},[396,1238,1099],{"class":405},[396,1240,1241],{"class":444},"].sum() ",[396,1243,980],{"class":440},[396,1245,1180],{"class":444},[396,1247,503],{"class":405},[396,1249,987],{"class":444},[396,1251,1252,1254,1256,1258,1261,1263,1266,1268,1270,1273,1275,1278,1280,1282,1284],{"class":340,"line":467},[396,1253,525],{"class":524},[396,1255,994],{"class":444},[396,1257,997],{"class":440},[396,1259,1260],{"class":405},"\"mean of regional averages: ",[396,1262,1004],{"class":1003},[396,1264,1265],{"class":444},"wrong",[396,1267,1010],{"class":440},[396,1269,1013],{"class":1003},[396,1271,1272],{"class":405},"   company-wide weighted: ",[396,1274,1004],{"class":1003},[396,1276,1277],{"class":444},"right",[396,1279,1010],{"class":440},[396,1281,1013],{"class":1003},[396,1283,1028],{"class":405},[396,1285,489],{"class":444},[10,1287,1288],{},"The mean of the two regional averages is 6.77; the true company figure is 6.63. The gap is small here and can be large elsewhere — with a tiny, expensive region, the mean of averages can be off by a multiple. North sold 125 units and South 100, but the regional averages are 5.52 and 8.02 because their product mixes differ — averaging them ignores both effects. Any total row in a report of averages must be recomputed from the underlying sums, never averaged from the rows above it. The same applies to margins: an overall margin is total profit over total revenue, not the mean of the line margins.",[36,1290,45,1295,45,1298,45,1301,45,1304,45,1310,45,1318,45,1323,45,1327,45,1332,45,1339,45,1343,45,1348,45,1352,45,1357,45,1360,45,1365,45,1370,45,1374,45,1378,45,1383,45,1386,45,1391,45,1394],{"viewBox":1291,"role":39,"ariaLabelledBy":1292,"xmlns":43,"style":712},"4 0 752 207",[1293,1294],"sp-avg-t","sp-avg-d",[47,1296,1297],{"id":1293},"Averaging averages versus weighting by volume",[51,1299,1300],{"id":1294},"Two panels. The mean of the regional average prices, 5.52 for North and 8.02 for South, is 6.77 and treats both regions as equal. The weighted company average from total value over total units is 6.63 and reflects that the regions sold different volumes.",[55,1302],{"x":229,"y":58,"width":721,"height":1303,"fill":61},"207",[55,1305],{"x":725,"y":726,"width":1306,"height":228,"rx":1307,"fill":1308,"stroke":1309,"style":346},"351.0","14","#fee8f2","var(--accent,#f43f8f)",[360,1311,1313],{"transform":1312},"translate(182.5,29.0) scale(1.083)",[364,1314],{"d":1315,"fill":367,"stroke":1316,"style":1317},"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",[63,1319,1322],{"x":1320,"y":747,"style":1321},"195.5","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Mean of regional averages",[340,1324],{"x1":1325,"y1":127,"x2":1326,"y2":127,"stroke":1309,"style":77},"36.0","355.0",[63,1328,1331],{"x":1329,"y":767,"style":1330},"60.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","North 5.52, South 8.02",[360,1333,1335],{"transform":1334},"translate(38.5,99.5) scale(0.625)",[364,1336],{"d":1337,"fill":367,"stroke":1309,"style":1338},"M6 6l12 12 M18 6L6 18","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[63,1340,1342],{"x":1329,"y":1341,"style":1330},"137.0","each region counts equally",[360,1344,1346],{"transform":1345},"translate(38.5,125.5) scale(0.625)",[364,1347],{"d":1337,"fill":367,"stroke":1309,"style":1338},[63,1349,1351],{"x":1329,"y":1350,"style":1330},"163.0","ignores volumes",[360,1353,1355],{"transform":1354},"translate(38.5,151.5) scale(0.625)",[364,1356],{"d":1337,"fill":367,"stroke":1309,"style":1338},[55,1358],{"x":1359,"y":726,"width":1306,"height":228,"rx":1307,"fill":185,"stroke":186,"style":346},"389.0",[360,1361,1363],{"transform":1362},"translate(551.5,29.0) scale(1.083)",[364,1364],{"d":366,"fill":367,"stroke":368,"style":1317},[63,1366,1369],{"x":1367,"y":747,"style":1368},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Total value ÷ total units",[340,1371],{"x1":1372,"y1":127,"x2":1373,"y2":127,"stroke":186,"style":77},"405.0","724.0",[63,1375,1377],{"x":1376,"y":767,"style":1330},"429.0","1,492 ÷ 225",[360,1379,1381],{"transform":1380},"translate(407.5,99.5) scale(0.625)",[364,1382],{"d":366,"fill":367,"stroke":186,"style":1338},[63,1384,1385],{"x":1376,"y":1341,"style":1330},"6.63 per unit",[360,1387,1389],{"transform":1388},"translate(407.5,125.5) scale(0.625)",[364,1390],{"d":366,"fill":367,"stroke":186,"style":1338},[63,1392,1393],{"x":1376,"y":1350,"style":1330},"matches the whole business",[360,1395,1397],{"transform":1396},"translate(407.5,151.5) scale(0.625)",[364,1398],{"d":366,"fill":367,"stroke":186,"style":1338},[382,1400,1402],{"id":1401},"step-6-weighted-margin-and-writing-the-result","Step 6 — Weighted margin, and writing the result",[10,1404,1405],{},"Margins are a common weighted average: the overall margin percentage is the revenue-weighted average of line margins. Compute it, then write the regional table with both the averages and the sums they came from, so readers can check the arithmetic:",[387,1407,1409],{"className":431,"code":1408,"language":433,"meta":392,"style":392},"lines[\"profit\"] = lines[\"value\"] * lines[\"margin\"]\nsummary = lines.dropna(subset=[\"units\"]).groupby(\"region\")[[\"value\", \"profit\", \"units\"]].sum()\nsummary[\"margin\"] = (summary[\"profit\"] \u002F summary[\"value\"]).round(3)\nsummary[\"avg_price\"] = (summary[\"value\"] \u002F summary[\"units\"]).round(2)\ntotal = summary[[\"value\", \"profit\", \"units\"]].sum()\nsummary.loc[\"Total\"] = [*total, round(total[\"profit\"] \u002F total[\"value\"], 3), round(total[\"value\"] \u002F total[\"units\"], 2)]\nprint(summary)\nsummary.to_excel(\"weighted_summary.xlsx\")\n",[13,1410,1411,1437,1472,1503,1531,1553,1618,1625],{"__ignoreMap":392},[396,1412,1413,1415,1418,1420,1422,1424,1426,1428,1430,1432,1435],{"class":340,"line":398},[396,1414,1096],{"class":444},[396,1416,1417],{"class":405},"\"profit\"",[396,1419,506],{"class":444},[396,1421,480],{"class":440},[396,1423,512],{"class":444},[396,1425,1099],{"class":405},[396,1427,506],{"class":444},[396,1429,509],{"class":440},[396,1431,512],{"class":444},[396,1433,1434],{"class":405},"\"margin\"",[396,1436,1118],{"class":444},[396,1438,1439,1442,1444,1446,1448,1450,1452,1454,1456,1458,1460,1462,1464,1466,1468,1470],{"class":340,"line":454},[396,1440,1441],{"class":444},"summary ",[396,1443,480],{"class":440},[396,1445,1128],{"class":444},[396,1447,1131],{"class":1045},[396,1449,480],{"class":440},[396,1451,1136],{"class":444},[396,1453,503],{"class":405},[396,1455,1141],{"class":444},[396,1457,589],{"class":405},[396,1459,1146],{"class":444},[396,1461,1099],{"class":405},[396,1463,1151],{"class":444},[396,1465,1417],{"class":405},[396,1467,1151],{"class":444},[396,1469,503],{"class":405},[396,1471,1156],{"class":444},[396,1473,1474,1477,1479,1481,1483,1486,1488,1490,1492,1495,1497,1499,1501],{"class":340,"line":467},[396,1475,1476],{"class":444},"summary[",[396,1478,1434],{"class":405},[396,1480,506],{"class":444},[396,1482,480],{"class":440},[396,1484,1485],{"class":444}," (summary[",[396,1487,1417],{"class":405},[396,1489,506],{"class":444},[396,1491,980],{"class":440},[396,1493,1494],{"class":444}," summary[",[396,1496,1099],{"class":405},[396,1498,1056],{"class":444},[396,1500,198],{"class":524},[396,1502,489],{"class":444},[396,1504,1505,1507,1509,1511,1513,1515,1517,1519,1521,1523,1525,1527,1529],{"class":340,"line":474},[396,1506,1476],{"class":444},[396,1508,1164],{"class":405},[396,1510,506],{"class":444},[396,1512,480],{"class":440},[396,1514,1485],{"class":444},[396,1516,1099],{"class":405},[396,1518,506],{"class":444},[396,1520,980],{"class":440},[396,1522,1494],{"class":444},[396,1524,503],{"class":405},[396,1526,1056],{"class":444},[396,1528,161],{"class":524},[396,1530,489],{"class":444},[396,1532,1533,1536,1538,1541,1543,1545,1547,1549,1551],{"class":340,"line":492},[396,1534,1535],{"class":444},"total ",[396,1537,480],{"class":440},[396,1539,1540],{"class":444}," summary[[",[396,1542,1099],{"class":405},[396,1544,1151],{"class":444},[396,1546,1417],{"class":405},[396,1548,1151],{"class":444},[396,1550,503],{"class":405},[396,1552,1156],{"class":444},[396,1554,1555,1558,1561,1563,1565,1568,1570,1573,1576,1579,1581,1583,1585,1588,1590,1592,1594,1597,1599,1601,1603,1605,1607,1609,1611,1613,1615],{"class":340,"line":521},[396,1556,1557],{"class":444},"summary.loc[",[396,1559,1560],{"class":405},"\"Total\"",[396,1562,506],{"class":444},[396,1564,480],{"class":440},[396,1566,1567],{"class":444}," [",[396,1569,509],{"class":440},[396,1571,1572],{"class":444},"total, ",[396,1574,1575],{"class":524},"round",[396,1577,1578],{"class":444},"(total[",[396,1580,1417],{"class":405},[396,1582,506],{"class":444},[396,1584,980],{"class":440},[396,1586,1587],{"class":444}," total[",[396,1589,1099],{"class":405},[396,1591,1042],{"class":444},[396,1593,198],{"class":524},[396,1595,1596],{"class":444},"), ",[396,1598,1575],{"class":524},[396,1600,1578],{"class":444},[396,1602,1099],{"class":405},[396,1604,506],{"class":444},[396,1606,980],{"class":440},[396,1608,1587],{"class":444},[396,1610,503],{"class":405},[396,1612,1042],{"class":444},[396,1614,161],{"class":524},[396,1616,1617],{"class":444},")]\n",[396,1619,1620,1622],{"class":340,"line":531},[396,1621,525],{"class":524},[396,1623,1624],{"class":444},"(summary)\n",[396,1626,1628,1631,1634],{"class":340,"line":1627},8,[396,1629,1630],{"class":444},"summary.to_excel(",[396,1632,1633],{"class":405},"\"weighted_summary.xlsx\"",[396,1635,489],{"class":444},[10,1637,1638,1639,1643],{},"The Total row divides the summed profit by the summed value rather than averaging the margin column, so it is consistent with the rows above it. ",[30,1640,1642],{"href":1641},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-a-total-row-to-an-excel-table\u002F","Add a Total Row to an Excel Table with Python"," shows how to add such totals as live formulas instead.",[382,1645,1647],{"id":1646},"keep-the-formula-in-the-workbook-when-readers-need-it","Keep the formula in the workbook when readers need it",[10,1649,1650],{},"A weighted average written as a static number goes stale when a reader changes a quantity. When the workbook is meant to be edited, write the Excel formula instead, next to the data pandas wrote:",[387,1652,1654],{"className":431,"code":1653,"language":433,"meta":392,"style":392},"import subprocess\nfrom openpyxl import load_workbook\n\nlines[[\"region\", \"product\", \"units\", \"price\"]].to_excel(\"editable.xlsx\", index=False)\nwb = load_workbook(\"editable.xlsx\")\nws = wb.active\nlast = ws.max_row\nws[\"G1\"], ws[\"G2\"] = \"weighted avg price\", f\"=SUMPRODUCT(C2:C{last},D2:D{last})\u002FSUM(C2:C{last})\"\nwb.save(\"editable.xlsx\")\nsubprocess.run([\"soffice\", \"--headless\", \"--convert-to\", \"xlsx\", \"--outdir\", \"calc\", \"editable.xlsx\"],\n               check=True, capture_output=True, timeout=120)\nprint(round(load_workbook(\"calc\u002Feditable.xlsx\", data_only=True).active[\"G2\"].value, 2))\n",[13,1655,1656,1663,1676,1680,1717,1731,1741,1751,1807,1817,1858,1889],{"__ignoreMap":392},[396,1657,1658,1660],{"class":340,"line":398},[396,1659,441],{"class":440},[396,1661,1662],{"class":444}," subprocess\n",[396,1664,1665,1668,1671,1673],{"class":340,"line":454},[396,1666,1667],{"class":440},"from",[396,1669,1670],{"class":444}," openpyxl ",[396,1672,441],{"class":440},[396,1674,1675],{"class":444}," load_workbook\n",[396,1677,1678],{"class":340,"line":467},[396,1679,471],{"emptyLinePlaceholder":470},[396,1681,1682,1685,1687,1689,1691,1693,1695,1697,1699,1702,1705,1707,1710,1712,1715],{"class":340,"line":474},[396,1683,1684],{"class":444},"lines[[",[396,1686,589],{"class":405},[396,1688,1151],{"class":444},[396,1690,634],{"class":405},[396,1692,1151],{"class":444},[396,1694,503],{"class":405},[396,1696,1151],{"class":444},[396,1698,515],{"class":405},[396,1700,1701],{"class":444},"]].to_excel(",[396,1703,1704],{"class":405},"\"editable.xlsx\"",[396,1706,1151],{"class":444},[396,1708,1709],{"class":1045},"index",[396,1711,480],{"class":440},[396,1713,1714],{"class":524},"False",[396,1716,489],{"class":444},[396,1718,1719,1722,1724,1727,1729],{"class":340,"line":492},[396,1720,1721],{"class":444},"wb ",[396,1723,480],{"class":440},[396,1725,1726],{"class":444}," load_workbook(",[396,1728,1704],{"class":405},[396,1730,489],{"class":444},[396,1732,1733,1736,1738],{"class":340,"line":521},[396,1734,1735],{"class":444},"ws ",[396,1737,480],{"class":440},[396,1739,1740],{"class":444}," wb.active\n",[396,1742,1743,1746,1748],{"class":340,"line":531},[396,1744,1745],{"class":444},"last ",[396,1747,480],{"class":440},[396,1749,1750],{"class":444}," ws.max_row\n",[396,1752,1753,1756,1759,1762,1765,1767,1769,1772,1774,1776,1779,1781,1784,1786,1789,1791,1793,1795,1798,1800,1802,1804],{"class":340,"line":1627},[396,1754,1755],{"class":444},"ws[",[396,1757,1758],{"class":405},"\"G1\"",[396,1760,1761],{"class":444},"], ws[",[396,1763,1764],{"class":405},"\"G2\"",[396,1766,506],{"class":444},[396,1768,480],{"class":440},[396,1770,1771],{"class":405}," \"weighted avg price\"",[396,1773,1151],{"class":444},[396,1775,997],{"class":440},[396,1777,1778],{"class":405},"\"=SUMPRODUCT(C2:C",[396,1780,1004],{"class":1003},[396,1782,1783],{"class":444},"last",[396,1785,1013],{"class":1003},[396,1787,1788],{"class":405},",D2:D",[396,1790,1004],{"class":1003},[396,1792,1783],{"class":444},[396,1794,1013],{"class":1003},[396,1796,1797],{"class":405},")\u002FSUM(C2:C",[396,1799,1004],{"class":1003},[396,1801,1783],{"class":444},[396,1803,1013],{"class":1003},[396,1805,1806],{"class":405},")\"\n",[396,1808,1810,1813,1815],{"class":340,"line":1809},9,[396,1811,1812],{"class":444},"wb.save(",[396,1814,1704],{"class":405},[396,1816,489],{"class":444},[396,1818,1820,1823,1826,1828,1831,1833,1836,1838,1841,1843,1846,1848,1851,1853,1855],{"class":340,"line":1819},10,[396,1821,1822],{"class":444},"subprocess.run([",[396,1824,1825],{"class":405},"\"soffice\"",[396,1827,1151],{"class":444},[396,1829,1830],{"class":405},"\"--headless\"",[396,1832,1151],{"class":444},[396,1834,1835],{"class":405},"\"--convert-to\"",[396,1837,1151],{"class":444},[396,1839,1840],{"class":405},"\"xlsx\"",[396,1842,1151],{"class":444},[396,1844,1845],{"class":405},"\"--outdir\"",[396,1847,1151],{"class":444},[396,1849,1850],{"class":405},"\"calc\"",[396,1852,1151],{"class":444},[396,1854,1704],{"class":405},[396,1856,1857],{"class":444},"],\n",[396,1859,1861,1864,1866,1868,1870,1873,1875,1877,1879,1882,1884,1887],{"class":340,"line":1860},11,[396,1862,1863],{"class":1045},"               check",[396,1865,480],{"class":440},[396,1867,817],{"class":524},[396,1869,1151],{"class":444},[396,1871,1872],{"class":1045},"capture_output",[396,1874,480],{"class":440},[396,1876,817],{"class":524},[396,1878,1151],{"class":444},[396,1880,1881],{"class":1045},"timeout",[396,1883,480],{"class":440},[396,1885,1886],{"class":524},"120",[396,1888,489],{"class":444},[396,1890,1892,1894,1896,1898,1901,1904,1906,1909,1911,1913,1916,1918,1921,1923],{"class":340,"line":1891},12,[396,1893,525],{"class":524},[396,1895,994],{"class":444},[396,1897,1575],{"class":524},[396,1899,1900],{"class":444},"(load_workbook(",[396,1902,1903],{"class":405},"\"calc\u002Feditable.xlsx\"",[396,1905,1151],{"class":444},[396,1907,1908],{"class":1045},"data_only",[396,1910,480],{"class":440},[396,1912,817],{"class":524},[396,1914,1915],{"class":444},").active[",[396,1917,1764],{"class":405},[396,1919,1920],{"class":444},"].value, ",[396,1922,161],{"class":524},[396,1924,1061],{"class":444},[10,1926,1927,1928,1930,1931,1934,1935,1937],{},"Recalculated, the formula gives 6.63, the same as the pandas figure: ",[13,1929,15],{}," treats the empty units cell as zero and ",[13,1932,1933],{},"SUM"," ignores it, so the East line drops out of both sides, exactly like the ",[13,1936,1074],{}," mask.",[382,1939,1941],{"id":1940},"common-pitfalls","Common pitfalls",[1943,1944,1945,1958,1964,1970],"ul",{},[1946,1947,1948,1954,1955,1957],"li",{},[1949,1950,1951,1953],"strong",{},[13,1952,558],{}," on columns with gaps."," It returns ",[13,1956,545],{},"; multiply and sum instead.",[1946,1959,1960,1963],{},[1949,1961,1962],{},"Different rows in numerator and denominator."," Mask missing weights once and use the mask for both sums.",[1946,1965,1966,1969],{},[1949,1967,1968],{},"Averaging averages in total rows."," Recompute from the underlying sums.",[1946,1971,1972,1975],{},[1949,1973,1974],{},"Weighting by the wrong thing."," Average price is weighted by units; average margin by revenue.",[382,1977,1979],{"id":1978},"performance-notes","Performance notes",[10,1981,1982,1983,1068,1986,1988],{},"Column multiplication and sums are vectorised and run on millions of rows in milliseconds. Grouped sums followed by one division are much faster than ",[13,1984,1985],{},"groupby().apply",[13,1987,1067],{},", which calls Python once per group.",[382,1990,1992],{"id":1991},"conclusion","Conclusion",[10,1994,1995,1997,1998,2001],{},[13,1996,15],{}," becomes multiply-then-sum in pandas, with boolean masks in place of Excel's multiplied conditions and a product column plus ",[13,1999,2000],{},"groupby"," for per-group totals. A weighted average is the sum of value times weight divided by the sum of weights, computed on one consistent set of rows; per group, sum both parts first and divide after; and totals of averages are always recomputed from the underlying sums, never averaged.",[382,2003,2005],{"id":2004},"frequently-asked-questions","Frequently asked questions",[10,2007,2008,2011,2012,2014,2015,2017,2018,2020,2021,2023],{},[1949,2009,2010],{},"What is the pandas equivalent of SUMPRODUCT?","\nMultiply the columns and sum the result: (df",[396,2013,503],{}," * df",[396,2016,515],{},").sum(). For two columns this is also the dot product, df",[396,2019,503],{},".dot(df",[396,2022,515],{},"), though dot does not skip missing values.",[10,2025,2026,2029,2030,2014,2032,2034,2035,2037],{},[1949,2027,2028],{},"How do I compute a weighted average in pandas?","\nDivide the SUMPRODUCT of value and weight by the sum of the weights: (df",[396,2031,515],{},[396,2033,503],{},").sum() \u002F df",[396,2036,503],{},".sum(). numpy.average(values, weights=weights) gives the same result when there are no missing values.",[10,2039,2040,2043],{},[1949,2041,2042],{},"Why is the average of the regional averages different from the overall average?","\nBecause each regional average carries the weight of its own volume, and a simple mean of them treats a region with 5 units the same as one with 500. Compute the overall weighted average from the underlying sums instead.",[10,2045,2046,2049],{},[1949,2047,2048],{},"How do missing weights affect a weighted average?","\nA row with a missing weight or value should usually be left out of both the numerator and the denominator. Masking such rows once, before computing either sum, keeps the two consistent.",[382,2051,2053],{"id":2052},"related","Related",[1943,2055,2056,2062,2069,2076],{},[1946,2057,2058,2059,2061],{},"Up one level: ",[30,2060,33],{"href":32}," — every function, translated.",[1946,2063,2064,2068],{},[30,2065,2067],{"href":2066},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Faverageifs-maxifs-minifs-in-pandas\u002F","AVERAGEIFS, MAXIFS and MINIFS Equivalents in pandas"," — unweighted conditional averages.",[1946,2070,2071,2075],{},[30,2072,2074],{"href":2073},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas\u002F","Calculate Percent of Total per Group in pandas"," — shares that also need consistent totals.",[1946,2077,2078,2082],{},[30,2079,2081],{"href":2080},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fuse-different-aggregations-per-column-in-a-pivot\u002F","Use Different Aggregations per Column in a pandas Pivot"," — sums and averages side by side.",[2084,2085,2086],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":392,"searchDepth":454,"depth":454,"links":2088},[2089,2090,2091,2092,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102],{"id":384,"depth":454,"text":385},{"id":424,"depth":454,"text":425},{"id":565,"depth":454,"text":566},{"id":796,"depth":454,"text":797},{"id":903,"depth":454,"text":904},{"id":1082,"depth":454,"text":1083},{"id":1205,"depth":454,"text":1206},{"id":1401,"depth":454,"text":1402},{"id":1646,"depth":454,"text":1647},{"id":1940,"depth":454,"text":1941},{"id":1978,"depth":454,"text":1979},{"id":1991,"depth":454,"text":1992},{"id":2004,"depth":454,"text":2005},{"id":2052,"depth":454,"text":2053},"2026-09-25","Translate Excel's SUMPRODUCT to pandas — total value from quantity times price, conditional sums with boolean arrays, and weighted averages such as average price weighted by units. Covers weighted averages per group, missing weights, and why the plain mean of averages gives the wrong answer.","md",[2107,2109,2111,2113],{"q":2010,"a":2108},"Multiply the columns and sum the result: (df[\"units\"] * df[\"price\"]).sum(). For two columns this is also the dot product, df[\"units\"].dot(df[\"price\"]), though dot does not skip missing values.",{"q":2028,"a":2110},"Divide the SUMPRODUCT of value and weight by the sum of the weights: (df[\"price\"] * df[\"units\"]).sum() \u002F df[\"units\"].sum(). numpy.average(values, weights=weights) gives the same result when there are no missing values.",{"q":2042,"a":2112},"Because each regional average carries the weight of its own volume, and a simple mean of them treats a region with 5 units the same as one with 500. Compute the overall weighted average from the underlying sums instead.",{"q":2048,"a":2114},"A row with a missing weight or value should usually be left out of both the numerator and the denominator. Masking such rows once, before computing either sum, keeps the two consistent.","sumproduct and weighted average in pandas",null,{"breadcrumb":2118},[2119,2121,2124],{"name":2120,"item":980},"Home",{"name":2122,"item":2123},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":33,"item":32},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumproduct-and-weighted-average-in-pandas",{"title":2127,"description":2128},"SUMPRODUCT Equivalent and Weighted Average in pandas","Excel SUMPRODUCT in pandas: multiply and sum columns, conditional SUMPRODUCT with masks, weighted averages with numpy average and per group, missing weights, and why averaging averages is wrong.","sumproduct-and-weighted-average-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumproduct-and-weighted-average-in-pandas\u002Findex",[2132,2133,740],"formula-equivalents","pivots-and-summaries","how-to","TL0h9DZPOH0U6-NuaDhIUowZMiImhxtWrZsgSNtJ-Mc",[2137,2140],{"title":704,"path":2138,"stem":2139,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumif-and-sumifs-equivalent-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumif-and-sumifs-equivalent-in-pandas\u002Findex",{"title":2141,"path":2142,"stem":2143,"children":-1},"TEXTBEFORE, TEXTAFTER and TEXTSPLIT Equivalents in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Ftextbefore-textafter-and-textsplit-equivalents-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Ftextbefore-textafter-and-textsplit-equivalents-in-pandas\u002Findex",[2145,2147],{"path":2146,"title":33},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas",{"path":2148,"title":2122},"\u002Fadvanced-data-transformation-and-cleaning",[],[2151,2154,2157,2160,2163,2165],{"path":2152,"title":2153,"kind":2134},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvlookup-equivalent-in-pandas-for-excel-files","The VLOOKUP Equivalent in pandas for Excel Files",{"path":2155,"title":2156,"kind":2134},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas","Extract Numbers and Codes from Text with pandas",{"path":2158,"title":2159,"kind":2134},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fsplit-full-names-into-first-and-last-names-with-pandas","Split Full Names into First and Last Names with pandas",{"path":2161,"title":2162,"kind":2134},"\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":2164,"title":2074,"kind":2134},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas",{"path":2166,"title":2167,"kind":2134},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcount-distinct-values-per-group-in-pandas","Count Distinct Values per Group in pandas",1790972088719]