[{"data":1,"prerenderedAt":2014},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis":1980,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis":1988,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis":1993,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis":1994},{"id":4,"title":5,"body":6,"dateModified":1946,"datePublished":1946,"description":1947,"extension":1948,"faq":1949,"keyword":1958,"kind":1959,"meta":1960,"navigation":109,"path":1969,"seo":1970,"slug":1973,"stem":1974,"topics":1975,"type":1978,"__hash__":1979},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis\u002Findex.md","Build a Price-Volume-Mix Analysis",{"type":7,"value":8,"toc":1934},"minimark",[9,32,37,66,77,81,451,462,466,469,841,869,986,1097,1101,1231,1242,1246,1624,1635,1804,1808,1823,1827,1862,1866,1869,1873,1879,1885,1891,1897,1901,1930],[10,11,12,13,17,18,21,22,25,26,31],"p",{},"\"Revenue is up 20% on last year\" invites the obvious follow-up: because we sold more, because we charged more, or because we sold more of the expensive products? A price-volume-mix analysis answers it by splitting the change in revenue into three effects that add up exactly to the total. The ",[14,15,16],"em",{},"volume"," effect is what the change would have been if only the number of units had changed, at last year's average price. The ",[14,19,20],{},"mix"," effect is what moving sales towards more or less expensive products contributed. The ",[14,23,24],{},"price"," effect is what charging different prices for the same products contributed. Finance teams present the result as a bridge from last year's revenue to this year's. This page builds the analysis from two periods of sales lines with pandas, handles products that are new this year or no longer sold, proves the effects reconcile to the penny, breaks each effect down by product and writes an Excel report. It belongs to ",[27,28,30],"a",{"href":29},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,44],"pre",{"className":40,"code":41,"language":42,"meta":43,"style":43},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[45,46,47],"code",{"__ignoreMap":43},[48,49,52,56,60,63],"span",{"class":50,"line":51},"line",1,[48,53,55],{"class":54},"sMTad","pip",[48,57,59],{"class":58},"srMev"," install",[48,61,62],{"class":58}," pandas",[48,64,65],{"class":58}," openpyxl\n",[10,67,68,69,72,73,76],{},"The input is a year of sales lines for 2025 and 2026: product, quantity and revenue per line. Four products sold in 2025; in 2026 one of them, ",[14,70,71],{},"Legacy",", was discontinued and a new one, ",[14,74,75],{},"Eco",", was launched — the situation that most simple PVM templates handle badly.",[33,78,80],{"id":79},"step-1-quantity-revenue-and-price-per-product-and-period","Step 1 — Quantity, revenue and price per product and period",[38,82,86],{"className":83,"code":84,"language":85,"meta":43,"style":43},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nlines = pd.read_excel(\"sales_lines.xlsx\")\npv = lines.pivot_table(index=\"Product\", columns=\"Year\", values=[\"Qty\", \"Revenue\"], aggfunc=\"sum\", fill_value=0)\nQ0, Q1 = pv[\"Qty\"][2025], pv[\"Qty\"][2026]\nR0, R1 = pv[\"Revenue\"][2025], pv[\"Revenue\"][2026]\nP0, P1 = (R0 \u002F Q0).where(Q0 > 0), (R1 \u002F Q1).where(Q1 > 0)\nsummary = pd.DataFrame({\"Qty 2025\": Q0, \"Qty 2026\": Q1, \"Price 2025\": P0.round(2), \"Price 2026\": P1.round(2),\n                        \"Revenue 2025\": R0, \"Revenue 2026\": R1})\nprint(summary.to_string())\nprint(f\"revenue {R0.sum():,.2f} -> {R1.sum():,.2f} ({R1.sum() - R0.sum():+,.2f}, {R1.sum() \u002F R0.sum() - 1:+.1%})\")\n","python",[45,87,88,104,111,129,202,234,260,297,343,358,367],{"__ignoreMap":43},[48,89,90,94,98,101],{"class":50,"line":51},[48,91,93],{"class":92},"s-kum","import",[48,95,97],{"class":96},"skGVy"," pandas ",[48,99,100],{"class":92},"as",[48,102,103],{"class":96}," pd\n",[48,105,107],{"class":50,"line":106},2,[48,108,110],{"emptyLinePlaceholder":109},true,"\n",[48,112,114,117,120,123,126],{"class":50,"line":113},3,[48,115,116],{"class":96},"lines ",[48,118,119],{"class":92},"=",[48,121,122],{"class":96}," pd.read_excel(",[48,124,125],{"class":58},"\"sales_lines.xlsx\"",[48,127,128],{"class":96},")\n",[48,130,132,135,137,140,144,146,149,152,155,157,160,162,165,167,170,173,175,178,181,184,186,189,191,194,196,200],{"class":50,"line":131},4,[48,133,134],{"class":96},"pv ",[48,136,119],{"class":92},[48,138,139],{"class":96}," lines.pivot_table(",[48,141,143],{"class":142},"sa561","index",[48,145,119],{"class":92},[48,147,148],{"class":58},"\"Product\"",[48,150,151],{"class":96},", ",[48,153,154],{"class":142},"columns",[48,156,119],{"class":92},[48,158,159],{"class":58},"\"Year\"",[48,161,151],{"class":96},[48,163,164],{"class":142},"values",[48,166,119],{"class":92},[48,168,169],{"class":96},"[",[48,171,172],{"class":58},"\"Qty\"",[48,174,151],{"class":96},[48,176,177],{"class":58},"\"Revenue\"",[48,179,180],{"class":96},"], ",[48,182,183],{"class":142},"aggfunc",[48,185,119],{"class":92},[48,187,188],{"class":58},"\"sum\"",[48,190,151],{"class":96},[48,192,193],{"class":142},"fill_value",[48,195,119],{"class":92},[48,197,199],{"class":198},"sP0c6","0",[48,201,128],{"class":96},[48,203,205,208,210,213,215,218,221,224,226,228,231],{"class":50,"line":204},5,[48,206,207],{"class":96},"Q0, Q1 ",[48,209,119],{"class":92},[48,211,212],{"class":96}," pv[",[48,214,172],{"class":58},[48,216,217],{"class":96},"][",[48,219,220],{"class":198},"2025",[48,222,223],{"class":96},"], pv[",[48,225,172],{"class":58},[48,227,217],{"class":96},[48,229,230],{"class":198},"2026",[48,232,233],{"class":96},"]\n",[48,235,237,240,242,244,246,248,250,252,254,256,258],{"class":50,"line":236},6,[48,238,239],{"class":96},"R0, R1 ",[48,241,119],{"class":92},[48,243,212],{"class":96},[48,245,177],{"class":58},[48,247,217],{"class":96},[48,249,220],{"class":198},[48,251,223],{"class":96},[48,253,177],{"class":58},[48,255,217],{"class":96},[48,257,230],{"class":198},[48,259,233],{"class":96},[48,261,263,266,268,271,274,277,280,283,286,288,291,293,295],{"class":50,"line":262},7,[48,264,265],{"class":96},"P0, P1 ",[48,267,119],{"class":92},[48,269,270],{"class":96}," (R0 ",[48,272,273],{"class":92},"\u002F",[48,275,276],{"class":96}," Q0).where(Q0 ",[48,278,279],{"class":92},">",[48,281,282],{"class":198}," 0",[48,284,285],{"class":96},"), (R1 ",[48,287,273],{"class":92},[48,289,290],{"class":96}," Q1).where(Q1 ",[48,292,279],{"class":92},[48,294,282],{"class":198},[48,296,128],{"class":96},[48,298,300,303,305,308,311,314,317,320,323,326,329,332,335,338,340],{"class":50,"line":299},8,[48,301,302],{"class":96},"summary ",[48,304,119],{"class":92},[48,306,307],{"class":96}," pd.DataFrame({",[48,309,310],{"class":58},"\"Qty 2025\"",[48,312,313],{"class":96},": Q0, ",[48,315,316],{"class":58},"\"Qty 2026\"",[48,318,319],{"class":96},": Q1, ",[48,321,322],{"class":58},"\"Price 2025\"",[48,324,325],{"class":96},": P0.round(",[48,327,328],{"class":198},"2",[48,330,331],{"class":96},"), ",[48,333,334],{"class":58},"\"Price 2026\"",[48,336,337],{"class":96},": P1.round(",[48,339,328],{"class":198},[48,341,342],{"class":96},"),\n",[48,344,346,349,352,355],{"class":50,"line":345},9,[48,347,348],{"class":58},"                        \"Revenue 2025\"",[48,350,351],{"class":96},": R0, ",[48,353,354],{"class":58},"\"Revenue 2026\"",[48,356,357],{"class":96},": R1})\n",[48,359,361,364],{"class":50,"line":360},10,[48,362,363],{"class":198},"print",[48,365,366],{"class":96},"(summary.to_string())\n",[48,368,370,372,375,378,381,385,388,391,394,397,399,402,404,406,409,411,414,417,420,423,425,427,429,431,433,436,438,441,444,446,449],{"class":50,"line":369},11,[48,371,363],{"class":198},[48,373,374],{"class":96},"(",[48,376,377],{"class":92},"f",[48,379,380],{"class":58},"\"revenue ",[48,382,384],{"class":383},"sSjpA","{",[48,386,387],{"class":96},"R0.sum()",[48,389,390],{"class":92},":,.2f",[48,392,393],{"class":383},"}",[48,395,396],{"class":58}," -> ",[48,398,384],{"class":383},[48,400,401],{"class":96},"R1.sum()",[48,403,390],{"class":92},[48,405,393],{"class":383},[48,407,408],{"class":58}," (",[48,410,384],{"class":383},[48,412,413],{"class":96},"R1.sum() ",[48,415,416],{"class":92},"-",[48,418,419],{"class":96}," R0.sum()",[48,421,422],{"class":92},":+,.2f",[48,424,393],{"class":383},[48,426,151],{"class":58},[48,428,384],{"class":383},[48,430,413],{"class":96},[48,432,273],{"class":92},[48,434,435],{"class":96}," R0.sum() ",[48,437,416],{"class":92},[48,439,440],{"class":198}," 1",[48,442,443],{"class":92},":+.1%",[48,445,393],{"class":383},[48,447,448],{"class":58},")\"",[48,450,128],{"class":96},[10,452,453,454,457,458,461],{},"Price per product is revenue divided by quantity for the period — the ",[14,455,456],{},"average"," price actually achieved, which already includes discounts, so the price effect will capture discounting as well as list-price changes. A product with no sales in a period has no price, and ",[45,459,460],{},"where(Q > 0)"," leaves it missing rather than dividing by zero. Revenue grew from 191,907.68 to 229,940.46, up 38,032.78 or almost 20%; the question is what that is made of.",[33,463,465],{"id":464},"step-2-the-three-effects-for-continuing-products","Step 2 — The three effects for continuing products",[10,467,468],{},"Separate the products sold in both years from those in only one, and apply the standard formulas to the continuing ones:",[38,470,472],{"className":83,"code":471,"language":85,"meta":43,"style":43},"both = Q0.gt(0) & Q1.gt(0)\nq0, q1, p0, p1 = Q0[both], Q1[both], P0[both], P1[both]\navg_p0 = (q0 * p0).sum() \u002F q0.sum()                         # last year's average price per unit\nshare0, share1 = q0 \u002F q0.sum(), q1 \u002F q1.sum()\n\nvolume = (q1.sum() - q0.sum()) * avg_p0\nmix = (q1.sum() * (share1 - share0) * p0)\nprice = (p1 - p0) * q1\nnew = R1[Q0.eq(0) & Q1.gt(0)].sum()\nlost = -R0[Q0.gt(0) & Q1.eq(0)].sum()\n\nbridge = pd.Series({\"Revenue 2025\": R0.sum(), \"Volume\": volume, \"Mix\": mix.sum(), \"Price\": price.sum(),\n                    \"New products\": new, \"Discontinued\": lost, \"Revenue 2026\": R1.sum()})\nprint(bridge.round(2).to_string())\nexplained = bridge[\"Revenue 2025\"] + bridge[[\"Volume\", \"Mix\", \"Price\", \"New products\", \"Discontinued\"]].sum()\nprint(f\"reconciles: {abs(explained - R1.sum()) \u003C 0.005} (difference {explained - R1.sum():.6f})\")\n",[45,473,474,499,509,534,554,558,578,602,622,645,671,675,710,730,743,787],{"__ignoreMap":43},[48,475,476,479,481,484,486,489,492,495,497],{"class":50,"line":51},[48,477,478],{"class":96},"both ",[48,480,119],{"class":92},[48,482,483],{"class":96}," Q0.gt(",[48,485,199],{"class":198},[48,487,488],{"class":96},") ",[48,490,491],{"class":92},"&",[48,493,494],{"class":96}," Q1.gt(",[48,496,199],{"class":198},[48,498,128],{"class":96},[48,500,501,504,506],{"class":50,"line":106},[48,502,503],{"class":96},"q0, q1, p0, p1 ",[48,505,119],{"class":92},[48,507,508],{"class":96}," Q0[both], Q1[both], P0[both], P1[both]\n",[48,510,511,514,516,519,522,525,527,530],{"class":50,"line":113},[48,512,513],{"class":96},"avg_p0 ",[48,515,119],{"class":92},[48,517,518],{"class":96}," (q0 ",[48,520,521],{"class":92},"*",[48,523,524],{"class":96}," p0).sum() ",[48,526,273],{"class":92},[48,528,529],{"class":96}," q0.sum()                         ",[48,531,533],{"class":532},"s-wDw","# last year's average price per unit\n",[48,535,536,539,541,544,546,549,551],{"class":50,"line":131},[48,537,538],{"class":96},"share0, share1 ",[48,540,119],{"class":92},[48,542,543],{"class":96}," q0 ",[48,545,273],{"class":92},[48,547,548],{"class":96}," q0.sum(), q1 ",[48,550,273],{"class":92},[48,552,553],{"class":96}," q1.sum()\n",[48,555,556],{"class":50,"line":204},[48,557,110],{"emptyLinePlaceholder":109},[48,559,560,563,565,568,570,573,575],{"class":50,"line":236},[48,561,562],{"class":96},"volume ",[48,564,119],{"class":92},[48,566,567],{"class":96}," (q1.sum() ",[48,569,416],{"class":92},[48,571,572],{"class":96}," q0.sum()) ",[48,574,521],{"class":92},[48,576,577],{"class":96}," avg_p0\n",[48,579,580,583,585,587,589,592,594,597,599],{"class":50,"line":262},[48,581,582],{"class":96},"mix ",[48,584,119],{"class":92},[48,586,567],{"class":96},[48,588,521],{"class":92},[48,590,591],{"class":96}," (share1 ",[48,593,416],{"class":92},[48,595,596],{"class":96}," share0) ",[48,598,521],{"class":92},[48,600,601],{"class":96}," p0)\n",[48,603,604,607,609,612,614,617,619],{"class":50,"line":299},[48,605,606],{"class":96},"price ",[48,608,119],{"class":92},[48,610,611],{"class":96}," (p1 ",[48,613,416],{"class":92},[48,615,616],{"class":96}," p0) ",[48,618,521],{"class":92},[48,620,621],{"class":96}," q1\n",[48,623,624,627,629,632,634,636,638,640,642],{"class":50,"line":345},[48,625,626],{"class":96},"new ",[48,628,119],{"class":92},[48,630,631],{"class":96}," R1[Q0.eq(",[48,633,199],{"class":198},[48,635,488],{"class":96},[48,637,491],{"class":92},[48,639,494],{"class":96},[48,641,199],{"class":198},[48,643,644],{"class":96},")].sum()\n",[48,646,647,650,652,655,658,660,662,664,667,669],{"class":50,"line":360},[48,648,649],{"class":96},"lost ",[48,651,119],{"class":92},[48,653,654],{"class":92}," -",[48,656,657],{"class":96},"R0[Q0.gt(",[48,659,199],{"class":198},[48,661,488],{"class":96},[48,663,491],{"class":92},[48,665,666],{"class":96}," Q1.eq(",[48,668,199],{"class":198},[48,670,644],{"class":96},[48,672,673],{"class":50,"line":369},[48,674,110],{"emptyLinePlaceholder":109},[48,676,678,681,683,686,689,692,695,698,701,704,707],{"class":50,"line":677},12,[48,679,680],{"class":96},"bridge ",[48,682,119],{"class":92},[48,684,685],{"class":96}," pd.Series({",[48,687,688],{"class":58},"\"Revenue 2025\"",[48,690,691],{"class":96},": R0.sum(), ",[48,693,694],{"class":58},"\"Volume\"",[48,696,697],{"class":96},": volume, ",[48,699,700],{"class":58},"\"Mix\"",[48,702,703],{"class":96},": mix.sum(), ",[48,705,706],{"class":58},"\"Price\"",[48,708,709],{"class":96},": price.sum(),\n",[48,711,713,716,719,722,725,727],{"class":50,"line":712},13,[48,714,715],{"class":58},"                    \"New products\"",[48,717,718],{"class":96},": new, ",[48,720,721],{"class":58},"\"Discontinued\"",[48,723,724],{"class":96},": lost, ",[48,726,354],{"class":58},[48,728,729],{"class":96},": R1.sum()})\n",[48,731,733,735,738,740],{"class":50,"line":732},14,[48,734,363],{"class":198},[48,736,737],{"class":96},"(bridge.round(",[48,739,328],{"class":198},[48,741,742],{"class":96},").to_string())\n",[48,744,746,749,751,754,756,759,762,765,767,769,771,773,775,777,780,782,784],{"class":50,"line":745},15,[48,747,748],{"class":96},"explained ",[48,750,119],{"class":92},[48,752,753],{"class":96}," bridge[",[48,755,688],{"class":58},[48,757,758],{"class":96},"] ",[48,760,761],{"class":92},"+",[48,763,764],{"class":96}," bridge[[",[48,766,694],{"class":58},[48,768,151],{"class":96},[48,770,700],{"class":58},[48,772,151],{"class":96},[48,774,706],{"class":58},[48,776,151],{"class":96},[48,778,779],{"class":58},"\"New products\"",[48,781,151],{"class":96},[48,783,721],{"class":58},[48,785,786],{"class":96},"]].sum()\n",[48,788,790,792,794,796,799,801,804,807,809,812,815,818,820,823,825,827,829,832,835,837,839],{"class":50,"line":789},16,[48,791,363],{"class":198},[48,793,374],{"class":96},[48,795,377],{"class":92},[48,797,798],{"class":58},"\"reconciles: ",[48,800,384],{"class":383},[48,802,803],{"class":198},"abs",[48,805,806],{"class":96},"(explained ",[48,808,416],{"class":92},[48,810,811],{"class":96}," R1.sum()) ",[48,813,814],{"class":92},"\u003C",[48,816,817],{"class":198}," 0.005",[48,819,393],{"class":383},[48,821,822],{"class":58}," (difference ",[48,824,384],{"class":383},[48,826,748],{"class":96},[48,828,416],{"class":92},[48,830,831],{"class":96}," R1.sum()",[48,833,834],{"class":92},":.6f",[48,836,393],{"class":383},[48,838,448],{"class":58},[48,840,128],{"class":96},[10,842,843,844,846,847,849,850,853,854,856,857,859,860,862,863,865,866,868],{},"The ",[14,845,16],{}," effect values the change in total units of continuing products at last year's average price per unit: if nothing but the number of units had changed, this is what revenue would have moved by. The ",[14,848,20],{}," effect values each product's change in ",[14,851,852],{},"share"," of units at its own prior price — selling a larger share of Pro and a smaller share of Standard raises revenue even at the same total volume and the same prices. The ",[14,855,24],{}," effect is the change in each product's price times this year's quantity. For continuing products these three add up exactly to the change in their revenue, which is the property that makes PVM trustworthy; ",[14,858,75],{}," and ",[14,861,71],{}," cannot be split, because one has no prior price and the other no current quantity, so they appear as their own lines. The final check proves the whole bridge reconciles. Of the 38,033 increase, 25,058 came from selling more units, 12,594 from a richer mix, 5,341 from higher realised prices and 6,130 from the new ",[14,864,75],{}," range, offset by 11,090 of ",[14,867,71],{}," sales that disappeared.",[870,871,879,880,879,884,879,888,879,895,879,905,879,912,879,919,879,924,879,927,879,931,879,934,879,938,879,941,879,945,879,948,879,952,879,955,879,959,879,964,879,969,879,972,879,976,879,981],"svg",{"viewBox":872,"role":873,"ariaLabelledBy":874,"xmlns":877,"style":878},"4 0 744 220","img",[875,876],"pvm-bridge-t","pvm-bridge-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[881,882,883],"title",{"id":875},"The revenue bridge from 2025 to 2026",[885,886,887],"desc",{"id":876},"Revenue rose from 191,908 to 229,940. Volume added 25,058, mix 12,594, price 5,341 and the new Eco range 6,130, while the discontinued Legacy range removed 11,090.",[889,890],"rect",{"x":891,"y":199,"width":892,"height":893,"fill":894},"4","744","220","#ffffff",[889,896],{"x":897,"y":898,"width":899,"height":900,"rx":901,"fill":902,"stroke":903,"style":904},"20.0","16.0","118.1","28.0","7","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[906,907,911],"text",{"x":908,"y":909,"style":910},"79.0","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Volume",[889,913],{"x":914,"y":898,"width":915,"height":900,"rx":901,"fill":916,"stroke":917,"style":918},"148.1","522.9","#d9f4f1","var(--teal,#0f9488)","stroke-width:1.5px",[906,920,923],{"x":921,"y":909,"style":922},"678.9","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","+25,058",[889,925],{"x":897,"y":926,"width":899,"height":900,"rx":901,"fill":902,"stroke":903,"style":904},"56.0",[906,928,930],{"x":908,"y":929,"style":910},"74.5","Mix",[889,932],{"x":914,"y":926,"width":933,"height":900,"rx":901,"fill":916,"stroke":917,"style":918},"262.8",[906,935,937],{"x":936,"y":929,"style":922},"418.9","+12,594",[889,939],{"x":897,"y":940,"width":899,"height":900,"rx":901,"fill":902,"stroke":903,"style":904},"96.0",[906,942,944],{"x":908,"y":943,"style":910},"114.5","Price",[889,946],{"x":914,"y":940,"width":947,"height":900,"rx":901,"fill":916,"stroke":917,"style":918},"111.4",[906,949,951],{"x":950,"y":943,"style":922},"267.5","+5,341",[889,953],{"x":897,"y":954,"width":899,"height":900,"rx":901,"fill":902,"stroke":903,"style":904},"136.0",[906,956,958],{"x":908,"y":957,"style":910},"154.5","New products",[889,960],{"x":914,"y":954,"width":961,"height":900,"rx":901,"fill":962,"stroke":963,"style":918},"127.9","#f0f4ff","var(--brand,#5b5cf0)",[906,965,968],{"x":966,"y":957,"style":967},"284.0","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","+6,130",[889,970],{"x":897,"y":971,"width":899,"height":900,"rx":901,"fill":902,"stroke":903,"style":904},"176.0",[906,973,975],{"x":908,"y":974,"style":910},"194.5","Discontinued",[889,977],{"x":914,"y":971,"width":978,"height":900,"rx":901,"fill":979,"stroke":980,"style":918},"231.4","#fdefd8","var(--gold,#b4740a)",[906,982,985],{"x":983,"y":974,"style":984},"387.5","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","−11,090",[870,987,879,993,879,996,879,999,879,1003,879,1007,879,1012,879,1018,879,1021,879,1025,879,1028,879,1033,879,1035,879,1040,879,1042,879,1045,879,1048,879,1051,879,1053,879,1057,879,1059,879,1062,879,1065,879,1068,879,1070,879,1074,879,1076,879,1079,879,1082,879,1086,879,1088,879,1092,879,1094],{"viewBox":988,"role":873,"ariaLabelledBy":989,"xmlns":877,"style":992},"4 0 752 208",[990,991],"pvm-formulas-t","pvm-formulas-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[881,994,995],{"id":990},"The price, volume and mix formulas",[885,997,998],{"id":991},"Volume is the change in total units times last year's average price. Mix is current total units times the change in each product's share times its prior price, summed. Price is the change in each product's price times its current quantity, summed. New and discontinued products are shown as their own revenue.",[889,1000],{"x":891,"y":199,"width":1001,"height":1002,"fill":894},"752","208",[889,1004],{"x":897,"y":898,"width":1005,"height":1006,"rx":199,"fill":894,"stroke":903,"style":904},"152.2","40.0",[889,1008],{"x":1009,"y":898,"width":1010,"height":1006,"rx":199,"fill":962,"stroke":963,"style":1011},"172.2","283.9","stroke-width:1.2px",[906,1013,1017],{"x":1014,"y":1015,"style":1016},"314.2","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","formula",[889,1019],{"x":1020,"y":898,"width":1010,"height":1006,"rx":199,"fill":962,"stroke":963,"style":1011},"456.1",[906,1022,1024],{"x":1023,"y":1015,"style":1016},"598.1","what it measures",[889,1026],{"x":897,"y":926,"width":1005,"height":1027,"rx":199,"fill":902,"stroke":903,"style":904},"34.0",[906,1029,911],{"x":1030,"y":1031,"style":1032},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[889,1034],{"x":1009,"y":926,"width":1010,"height":1027,"rx":199,"fill":894,"stroke":903,"style":904},[906,1036,1039],{"x":1014,"y":1037,"style":1038},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","(ΣQ1 − ΣQ0) × avg P0",[889,1041],{"x":1020,"y":926,"width":1010,"height":1027,"rx":199,"fill":894,"stroke":903,"style":904},[906,1043,1044],{"x":1023,"y":1037,"style":1038},"more or fewer units",[889,1046],{"x":897,"y":1047,"width":1005,"height":1027,"rx":199,"fill":902,"stroke":903,"style":904},"90.0",[906,1049,930],{"x":1030,"y":1050,"style":1032},"112.0",[889,1052],{"x":1009,"y":1047,"width":1010,"height":1027,"rx":199,"fill":894,"stroke":903,"style":904},[906,1054,1056],{"x":1014,"y":1055,"style":1038},"111.0","Σ ΣQ1 × (s1 − s0) × P0",[889,1058],{"x":1020,"y":1047,"width":1010,"height":1027,"rx":199,"fill":894,"stroke":903,"style":904},[906,1060,1061],{"x":1023,"y":1055,"style":1038},"shift to dearer or cheaper products",[889,1063],{"x":897,"y":1064,"width":1005,"height":1027,"rx":199,"fill":902,"stroke":903,"style":904},"124.0",[906,1066,944],{"x":1030,"y":1067,"style":1032},"146.0",[889,1069],{"x":1009,"y":1064,"width":1010,"height":1027,"rx":199,"fill":894,"stroke":903,"style":904},[906,1071,1073],{"x":1014,"y":1072,"style":1038},"145.0","Σ (P1 − P0) × Q1",[889,1075],{"x":1020,"y":1064,"width":1010,"height":1027,"rx":199,"fill":894,"stroke":903,"style":904},[906,1077,1078],{"x":1023,"y":1072,"style":1038},"price and discount changes",[889,1080],{"x":897,"y":1081,"width":1005,"height":1027,"rx":199,"fill":902,"stroke":903,"style":904},"158.0",[906,1083,1085],{"x":1030,"y":1084,"style":1032},"180.0","New \u002F discontinued",[889,1087],{"x":1009,"y":1081,"width":1010,"height":1027,"rx":199,"fill":894,"stroke":903,"style":904},[906,1089,1091],{"x":1014,"y":1090,"style":1038},"179.0","R1 or −R0",[889,1093],{"x":1020,"y":1081,"width":1010,"height":1027,"rx":199,"fill":894,"stroke":903,"style":904},[906,1095,1096],{"x":1023,"y":1090,"style":1038},"products in one year only",[33,1098,1100],{"id":1099},"step-3-which-products-drove-each-effect","Step 3 — Which products drove each effect",[38,1102,1104],{"className":83,"code":1103,"language":85,"meta":43,"style":43},"by_product = pd.DataFrame({\"Volume share\": volume * share0, \"Mix\": mix, \"Price\": price},\n                          index=Q0.index).fillna(0)                   # volume spread by prior unit share, for display\nby_product[\"New \u002F discontinued\"] = pd.Series({\"Eco\": new, \"Legacy\": lost}).reindex(by_product.index, fill_value=0)\nby_product[\"Total\"] = by_product.sum(axis=1)\nby_product = by_product.round(2)\nprint(by_product.to_string())\n",[45,1105,1106,1136,1154,1187,1211,1224],{"__ignoreMap":43},[48,1107,1108,1111,1113,1115,1118,1121,1123,1126,1128,1131,1133],{"class":50,"line":51},[48,1109,1110],{"class":96},"by_product ",[48,1112,119],{"class":92},[48,1114,307],{"class":96},[48,1116,1117],{"class":58},"\"Volume share\"",[48,1119,1120],{"class":96},": volume ",[48,1122,521],{"class":92},[48,1124,1125],{"class":96}," share0, ",[48,1127,700],{"class":58},[48,1129,1130],{"class":96},": mix, ",[48,1132,706],{"class":58},[48,1134,1135],{"class":96},": price},\n",[48,1137,1138,1141,1143,1146,1148,1151],{"class":50,"line":106},[48,1139,1140],{"class":142},"                          index",[48,1142,119],{"class":92},[48,1144,1145],{"class":96},"Q0.index).fillna(",[48,1147,199],{"class":198},[48,1149,1150],{"class":96},")                   ",[48,1152,1153],{"class":532},"# volume spread by prior unit share, for display\n",[48,1155,1156,1159,1162,1164,1166,1168,1171,1173,1176,1179,1181,1183,1185],{"class":50,"line":113},[48,1157,1158],{"class":96},"by_product[",[48,1160,1161],{"class":58},"\"New \u002F discontinued\"",[48,1163,758],{"class":96},[48,1165,119],{"class":92},[48,1167,685],{"class":96},[48,1169,1170],{"class":58},"\"Eco\"",[48,1172,718],{"class":96},[48,1174,1175],{"class":58},"\"Legacy\"",[48,1177,1178],{"class":96},": lost}).reindex(by_product.index, ",[48,1180,193],{"class":142},[48,1182,119],{"class":92},[48,1184,199],{"class":198},[48,1186,128],{"class":96},[48,1188,1189,1191,1194,1196,1198,1201,1204,1206,1209],{"class":50,"line":131},[48,1190,1158],{"class":96},[48,1192,1193],{"class":58},"\"Total\"",[48,1195,758],{"class":96},[48,1197,119],{"class":92},[48,1199,1200],{"class":96}," by_product.sum(",[48,1202,1203],{"class":142},"axis",[48,1205,119],{"class":92},[48,1207,1208],{"class":198},"1",[48,1210,128],{"class":96},[48,1212,1213,1215,1217,1220,1222],{"class":50,"line":204},[48,1214,1110],{"class":96},[48,1216,119],{"class":92},[48,1218,1219],{"class":96}," by_product.round(",[48,1221,328],{"class":198},[48,1223,128],{"class":96},[48,1225,1226,1228],{"class":50,"line":236},[48,1227,363],{"class":198},[48,1229,1230],{"class":96},"(by_product.to_string())\n",[10,1232,1233,1234,1237,1238,31],{},"The product view shows where each effect came from. Its ",[14,1235,1236],{},"Total"," column adds up to the overall change, but an individual product's total is not that product's own change in revenue — Plus shows 21,823 against a revenue increase of 23,457 — because the volume figure is shared out by prior share for display rather than measured product by product. Mix is positive for products whose share of units rose and that are priced above average — Plus and Pro here — and negative for Standard, whose share fell. The price effect shows which products' realised prices moved: Standard and Plus up, Pro slightly down, perhaps through discounting to win volume. Volume is a single figure in the method above; the product column spreads it by each product's prior share purely so the table sums, and is labelled accordingly. Analysing margins rather than revenue uses the same structure with unit margin in place of unit price, as in ",[27,1239,1241],{"href":1240},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report\u002F","Build a Product Margin Report",[33,1243,1245],{"id":1244},"step-4-the-bridge-in-excel","Step 4 — The bridge in Excel",[38,1247,1249],{"className":83,"code":1248,"language":85,"meta":43,"style":43},"from openpyxl import load_workbook\nfrom openpyxl.styles import Font\n\npath = \"pvm_2026_vs_2025.xlsx\"\ntable = bridge.round(2).rename(\"Amount\").to_frame()\nwith pd.ExcelWriter(path, engine=\"openpyxl\") as xw:\n    table.to_excel(xw, sheet_name=\"Bridge\", index_label=\"Step\")\n    by_product.to_excel(xw, sheet_name=\"By product\")\n    summary.to_excel(xw, sheet_name=\"Price and quantity\")\nwb = load_workbook(path)\nws = wb[\"Bridge\"]\nn = ws.max_row\nws.cell(n + 2, 1, \"Check: 2025 + effects - 2026\").font = Font(italic=True)\nws.cell(n + 2, 2, f\"=B2+SUM(B3:B{n - 1})-B{n}\")\nfor r in range(2, n + 3):\n    ws.cell(r, 2).number_format = \"#,##0.00;[Red]-#,##0.00;-\"\nfor r in (2, n):\n    ws.cell(r, 1).font = ws.cell(r, 2).font = Font(bold=True)\nws.column_dimensions[\"A\"].width = 30\nwb.save(path)\nprint(table.to_string())\n",[45,1250,1251,1264,1276,1280,1290,1311,1334,1359,1373,1387,1397,1411,1421,1458,1502,1531,1546,1562,1593,1610,1616],{"__ignoreMap":43},[48,1252,1253,1256,1259,1261],{"class":50,"line":51},[48,1254,1255],{"class":92},"from",[48,1257,1258],{"class":96}," openpyxl ",[48,1260,93],{"class":92},[48,1262,1263],{"class":96}," load_workbook\n",[48,1265,1266,1268,1271,1273],{"class":50,"line":106},[48,1267,1255],{"class":92},[48,1269,1270],{"class":96}," openpyxl.styles ",[48,1272,93],{"class":92},[48,1274,1275],{"class":96}," Font\n",[48,1277,1278],{"class":50,"line":113},[48,1279,110],{"emptyLinePlaceholder":109},[48,1281,1282,1285,1287],{"class":50,"line":131},[48,1283,1284],{"class":96},"path ",[48,1286,119],{"class":92},[48,1288,1289],{"class":58}," \"pvm_2026_vs_2025.xlsx\"\n",[48,1291,1292,1295,1297,1300,1302,1305,1308],{"class":50,"line":204},[48,1293,1294],{"class":96},"table ",[48,1296,119],{"class":92},[48,1298,1299],{"class":96}," bridge.round(",[48,1301,328],{"class":198},[48,1303,1304],{"class":96},").rename(",[48,1306,1307],{"class":58},"\"Amount\"",[48,1309,1310],{"class":96},").to_frame()\n",[48,1312,1313,1316,1319,1322,1324,1327,1329,1331],{"class":50,"line":236},[48,1314,1315],{"class":92},"with",[48,1317,1318],{"class":96}," pd.ExcelWriter(path, ",[48,1320,1321],{"class":142},"engine",[48,1323,119],{"class":92},[48,1325,1326],{"class":58},"\"openpyxl\"",[48,1328,488],{"class":96},[48,1330,100],{"class":92},[48,1332,1333],{"class":96}," xw:\n",[48,1335,1336,1339,1342,1344,1347,1349,1352,1354,1357],{"class":50,"line":262},[48,1337,1338],{"class":96},"    table.to_excel(xw, ",[48,1340,1341],{"class":142},"sheet_name",[48,1343,119],{"class":92},[48,1345,1346],{"class":58},"\"Bridge\"",[48,1348,151],{"class":96},[48,1350,1351],{"class":142},"index_label",[48,1353,119],{"class":92},[48,1355,1356],{"class":58},"\"Step\"",[48,1358,128],{"class":96},[48,1360,1361,1364,1366,1368,1371],{"class":50,"line":299},[48,1362,1363],{"class":96},"    by_product.to_excel(xw, ",[48,1365,1341],{"class":142},[48,1367,119],{"class":92},[48,1369,1370],{"class":58},"\"By product\"",[48,1372,128],{"class":96},[48,1374,1375,1378,1380,1382,1385],{"class":50,"line":345},[48,1376,1377],{"class":96},"    summary.to_excel(xw, ",[48,1379,1341],{"class":142},[48,1381,119],{"class":92},[48,1383,1384],{"class":58},"\"Price and quantity\"",[48,1386,128],{"class":96},[48,1388,1389,1392,1394],{"class":50,"line":360},[48,1390,1391],{"class":96},"wb ",[48,1393,119],{"class":92},[48,1395,1396],{"class":96}," load_workbook(path)\n",[48,1398,1399,1402,1404,1407,1409],{"class":50,"line":369},[48,1400,1401],{"class":96},"ws ",[48,1403,119],{"class":92},[48,1405,1406],{"class":96}," wb[",[48,1408,1346],{"class":58},[48,1410,233],{"class":96},[48,1412,1413,1416,1418],{"class":50,"line":677},[48,1414,1415],{"class":96},"n ",[48,1417,119],{"class":92},[48,1419,1420],{"class":96}," ws.max_row\n",[48,1422,1423,1426,1428,1431,1433,1435,1437,1440,1443,1445,1448,1451,1453,1456],{"class":50,"line":712},[48,1424,1425],{"class":96},"ws.cell(n ",[48,1427,761],{"class":92},[48,1429,1430],{"class":198}," 2",[48,1432,151],{"class":96},[48,1434,1208],{"class":198},[48,1436,151],{"class":96},[48,1438,1439],{"class":58},"\"Check: 2025 + effects - 2026\"",[48,1441,1442],{"class":96},").font ",[48,1444,119],{"class":92},[48,1446,1447],{"class":96}," Font(",[48,1449,1450],{"class":142},"italic",[48,1452,119],{"class":92},[48,1454,1455],{"class":198},"True",[48,1457,128],{"class":96},[48,1459,1460,1462,1464,1466,1468,1470,1472,1474,1477,1479,1481,1483,1485,1487,1490,1492,1495,1497,1500],{"class":50,"line":732},[48,1461,1425],{"class":96},[48,1463,761],{"class":92},[48,1465,1430],{"class":198},[48,1467,151],{"class":96},[48,1469,328],{"class":198},[48,1471,151],{"class":96},[48,1473,377],{"class":92},[48,1475,1476],{"class":58},"\"=B2+SUM(B3:B",[48,1478,384],{"class":383},[48,1480,1415],{"class":96},[48,1482,416],{"class":92},[48,1484,440],{"class":198},[48,1486,393],{"class":383},[48,1488,1489],{"class":58},")-B",[48,1491,384],{"class":383},[48,1493,1494],{"class":96},"n",[48,1496,393],{"class":383},[48,1498,1499],{"class":58},"\"",[48,1501,128],{"class":96},[48,1503,1504,1507,1510,1513,1516,1518,1520,1523,1525,1528],{"class":50,"line":745},[48,1505,1506],{"class":92},"for",[48,1508,1509],{"class":96}," r ",[48,1511,1512],{"class":92},"in",[48,1514,1515],{"class":198}," range",[48,1517,374],{"class":96},[48,1519,328],{"class":198},[48,1521,1522],{"class":96},", n ",[48,1524,761],{"class":92},[48,1526,1527],{"class":198}," 3",[48,1529,1530],{"class":96},"):\n",[48,1532,1533,1536,1538,1541,1543],{"class":50,"line":789},[48,1534,1535],{"class":96},"    ws.cell(r, ",[48,1537,328],{"class":198},[48,1539,1540],{"class":96},").number_format ",[48,1542,119],{"class":92},[48,1544,1545],{"class":58}," \"#,##0.00;[Red]-#,##0.00;-\"\n",[48,1547,1549,1551,1553,1555,1557,1559],{"class":50,"line":1548},17,[48,1550,1506],{"class":92},[48,1552,1509],{"class":96},[48,1554,1512],{"class":92},[48,1556,408],{"class":96},[48,1558,328],{"class":198},[48,1560,1561],{"class":96},", n):\n",[48,1563,1565,1567,1569,1571,1573,1576,1578,1580,1582,1584,1587,1589,1591],{"class":50,"line":1564},18,[48,1566,1535],{"class":96},[48,1568,1208],{"class":198},[48,1570,1442],{"class":96},[48,1572,119],{"class":92},[48,1574,1575],{"class":96}," ws.cell(r, ",[48,1577,328],{"class":198},[48,1579,1442],{"class":96},[48,1581,119],{"class":92},[48,1583,1447],{"class":96},[48,1585,1586],{"class":142},"bold",[48,1588,119],{"class":92},[48,1590,1455],{"class":198},[48,1592,128],{"class":96},[48,1594,1596,1599,1602,1605,1607],{"class":50,"line":1595},19,[48,1597,1598],{"class":96},"ws.column_dimensions[",[48,1600,1601],{"class":58},"\"A\"",[48,1603,1604],{"class":96},"].width ",[48,1606,119],{"class":92},[48,1608,1609],{"class":198}," 30\n",[48,1611,1613],{"class":50,"line":1612},20,[48,1614,1615],{"class":96},"wb.save(path)\n",[48,1617,1619,1621],{"class":50,"line":1618},21,[48,1620,363],{"class":198},[48,1622,1623],{"class":96},"(table.to_string())\n",[10,1625,843,1626,1629,1630,1634],{},[14,1627,1628],{},"Bridge"," sheet runs from 2025 revenue through each effect to 2026 revenue, with a check formula that should show a dash — zero — beneath it; the other sheets hold the product detail and the underlying prices and quantities, so every number can be traced. Drawn as a waterfall chart, the bridge is the standard way to present PVM in a board pack; ",[27,1631,1633],{"href":1632},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fbuild-a-waterfall-chart-in-excel-with-python\u002F","Build a Waterfall Chart in Excel with Python"," shows how to build one.",[870,1636,879,1641,879,1644,879,1647,879,1650,879,1656,879,1667,879,1673,879,1677,879,1681,879,1686,879,1691,879,1694,879,1701,879,1706,879,1710,879,1713,879,1717,879,1720,879,1723,879,1730,879,1736,879,1740,879,1744,879,1748,879,1752,879,1755,879,1758,879,1765,879,1770,879,1772,879,1775,879,1778,879,1782,879,1785,879,1788,879,1794,879,1798,879,1801],{"viewBox":1637,"role":873,"ariaLabelledBy":1638,"xmlns":877,"style":992},"4 0 752 160",[1639,1640],"pvm-flow-t","pvm-flow-d",[881,1642,1643],{"id":1639},"From sales lines to a PVM bridge",[885,1645,1646],{"id":1640},"Aggregate quantity and revenue by product and year, derive average prices, separate continuing, new and discontinued products, compute volume, mix and price for the continuing ones, check the bridge reconciles, and write the bridge and product detail to Excel.",[889,1648],{"x":891,"y":199,"width":1001,"height":1649,"fill":894},"160",[889,1651],{"x":897,"y":898,"width":1652,"height":1653,"rx":1654,"fill":902,"stroke":903,"style":1655},"115.2","128.0","12","stroke-width:2px",[1657,1658,1660],"g",{"transform":1659},"translate(65.6,37.8) scale(1.000)",[1661,1662],"path",{"d":1663,"fill":1664,"stroke":1665,"style":1666},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[906,1668,1672],{"x":1669,"y":1670,"style":1671},"77.6","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Aggregate",[906,1674,1676],{"x":1669,"y":1675,"style":1038},"103.8","qty and revenue",[906,1678,1680],{"x":1669,"y":1679,"style":1038},"121.2","by year",[50,1682],{"x1":1683,"y1":1684,"x2":1685,"y2":1684,"stroke":963,"style":1655},"140.2","80.0","162.2",[1687,1688],"polygon",{"points":1689,"fill":1690},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[889,1692],{"x":1693,"y":898,"width":1652,"height":1653,"rx":1654,"fill":962,"stroke":963,"style":1655},"171.2",[1657,1695,1697],{"transform":1696},"translate(216.8,37.8) scale(1.000)",[1661,1698],{"d":1699,"fill":1664,"stroke":1700,"style":1666},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","var(--brand-strong,#4338ca)",[906,1702,1705],{"x":1703,"y":1670,"style":1704},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Average",[906,1707,1709],{"x":1703,"y":1708,"style":1704},"101.8","prices",[906,1711,1712],{"x":1703,"y":1679,"style":1038},"revenue ÷ qty",[50,1714],{"x1":1715,"y1":1684,"x2":1716,"y2":1684,"stroke":963,"style":1655},"291.4","313.4",[1687,1718],{"points":1719,"fill":1690},"317.4,80.0 308.4,85.0 308.4,75.0",[889,1721],{"x":1722,"y":898,"width":1652,"height":1653,"rx":1654,"fill":979,"stroke":980,"style":1655},"322.4",[1657,1724,1726],{"transform":1725},"translate(368.0,29.0) scale(1.000)",[1661,1727],{"d":1728,"fill":1664,"stroke":1729,"style":1666},"M4 12h6 M10 12l6-6h4 M10 12l6 6h4 M17 3l3 3-3 3 M17 15l3 3-3 3","var(--gold-ink,#7a4e06)",[906,1731,1735],{"x":1732,"y":1733,"style":1734},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Split",[906,1737,1739],{"x":1732,"y":1738,"style":1734},"93.0","products",[906,1741,1743],{"x":1732,"y":1742,"style":1038},"112.5","continuing,",[906,1745,1747],{"x":1732,"y":1746,"style":1038},"130.0","new, lost",[50,1749],{"x1":1750,"y1":1684,"x2":1751,"y2":1684,"stroke":963,"style":1655},"442.6","464.6",[1687,1753],{"points":1754,"fill":1690},"468.6,80.0 459.6,85.0 459.6,75.0",[889,1756],{"x":1757,"y":898,"width":1652,"height":1653,"rx":1654,"fill":916,"stroke":917,"style":1655},"473.6",[1657,1759,1761],{"transform":1760},"translate(519.2,29.0) scale(1.000)",[1661,1762],{"d":1763,"fill":1664,"stroke":1764,"style":1666},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","var(--teal-ink,#0b6157)",[906,1766,1769],{"x":1767,"y":1733,"style":1768},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Volume, mix,",[906,1771,24],{"x":1767,"y":1738,"style":1768},[906,1773,1774],{"x":1767,"y":1742,"style":1038},"standard",[906,1776,1777],{"x":1767,"y":1746,"style":1038},"formulas",[50,1779],{"x1":1780,"y1":1684,"x2":1781,"y2":1684,"stroke":963,"style":1655},"593.8","615.8",[1687,1783],{"points":1784,"fill":1690},"619.8,80.0 610.8,85.0 610.8,75.0",[889,1786],{"x":1787,"y":898,"width":1652,"height":1653,"rx":1654,"fill":902,"stroke":903,"style":1655},"624.8",[1657,1789,1791],{"transform":1790},"translate(670.4,37.8) scale(1.000)",[1661,1792],{"d":1793,"fill":1664,"stroke":1665,"style":1666},"M5 12.5l4.5 4.5L19 7",[906,1795,1797],{"x":1796,"y":1670,"style":1671},"682.4","Reconcile +",[906,1799,1800],{"x":1796,"y":1708,"style":1671},"write",[906,1802,1803],{"x":1796,"y":1679,"style":1038},"bridge to Excel",[33,1805,1807],{"id":1806},"choices-that-change-the-numbers","Choices that change the numbers",[10,1809,1810,1811,1814,1815,1818,1819,1822],{},"PVM looks objective, but several choices move amounts between the effects, and readers should know which were made. Measuring price at ",[14,1812,1813],{},"current"," quantities and mix at ",[14,1816,1817],{},"prior"," prices, as here, is one convention; using prior quantities for price leaves an extra ",[14,1820,1821],{},"price × volume"," interaction term that some teams show separately. The level at which mix is measured matters too: mix between product families and mix between individual SKUs give different answers, and the finer the level, the more of the change is attributed to mix. Units must be comparable across products for the volume and mix effects to mean anything — pieces of very different products, or units versus kilograms, are better analysed within families. Whatever the choices, keep them fixed from period to period and state them on the report, so a change in the effects reflects the business rather than the method.",[33,1824,1826],{"id":1825},"common-pitfalls","Common pitfalls",[1828,1829,1830,1838,1844,1850,1856],"ul",{},[1831,1832,1833,1837],"li",{},[1834,1835,1836],"strong",{},"Effects that do not add up."," Use formulas that reconcile exactly, and check them.",[1831,1839,1840,1843],{},[1834,1841,1842],{},"Forcing new and discontinued products into price or mix."," Show them as their own lines.",[1831,1845,1846,1849],{},[1834,1847,1848],{},"List prices instead of realised prices."," Use revenue ÷ quantity so discounts count.",[1831,1851,1852,1855],{},[1834,1853,1854],{},"Mixing incomparable units."," Analyse within families when units differ.",[1831,1857,1858,1861],{},[1834,1859,1860],{},"Changing conventions between periods."," State the method and keep it.",[33,1863,1865],{"id":1864},"conclusion","Conclusion",[10,1867,1868],{},"A price-volume-mix analysis splits the change in revenue into volume — the change in total units at last year's average price — mix — shifts in product share at prior prices — and price — changes in realised price on current quantities — with new and discontinued products shown separately. Compute average prices from revenue and quantity, separate continuing products, apply the three formulas, prove the bridge reconciles, break the effects down by product and write the bridge with its check to Excel.",[33,1870,1872],{"id":1871},"frequently-asked-questions","Frequently asked questions",[10,1874,1875,1878],{},[1834,1876,1877],{},"How do I calculate price, volume and mix effects in pandas?","\nAggregate quantity and revenue by product for both periods, compute average prices, then volume = change in total units × prior average price, mix = Σ current total units × change in share × prior price, and price = Σ change in price × current quantity.",[10,1880,1881,1884],{},[1834,1882,1883],{},"Why do my price, volume and mix effects not add up to the revenue change?","\nThe formulas must use consistent quantities and prices; with prior average price for volume, prior prices for mix and current quantities for price, the three add up exactly for continuing products.",[10,1886,1887,1890],{},[1834,1888,1889],{},"How do I handle new and discontinued products in a PVM analysis?","\nShow their revenue as separate new-product and discontinued-product lines, because they have no price in one of the periods.",[10,1892,1893,1896],{},[1834,1894,1895],{},"Can PVM be done on margin instead of revenue?","\nYes. Use unit margin instead of unit price in the same formulas to explain the change in gross margin.",[33,1898,1900],{"id":1899},"related","Related",[1828,1902,1903,1909,1916,1923],{},[1831,1904,1905,1906,1908],{},"Up one level: ",[27,1907,30],{"href":29}," — finance reports from Python.",[1831,1910,1911,1915],{},[27,1912,1914],{"href":1913},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-budget-vs-actual-variance-report\u002F","Build a Budget vs Actual Variance Report"," — variance against plan.",[1831,1917,1918,1922],{},[27,1919,1921],{"href":1920},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcompare-two-periods-side-by-side-in-a-pivot\u002F","Compare Two Periods Side by Side in a Pivot"," — the period comparison.",[1831,1924,1925,1929],{},[27,1926,1928],{"href":1927},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-profitability-report\u002F","Build a Customer Profitability Report"," — profit by customer.",[1931,1932,1933],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":43,"searchDepth":106,"depth":106,"links":1935},[1936,1937,1938,1939,1940,1941,1942,1943,1944,1945],{"id":35,"depth":106,"text":36},{"id":79,"depth":106,"text":80},{"id":464,"depth":106,"text":465},{"id":1099,"depth":106,"text":1100},{"id":1244,"depth":106,"text":1245},{"id":1806,"depth":106,"text":1807},{"id":1825,"depth":106,"text":1826},{"id":1864,"depth":106,"text":1865},{"id":1871,"depth":106,"text":1872},{"id":1899,"depth":106,"text":1900},"2026-10-02","Explain why revenue changed between two periods by splitting the change into price, volume and mix effects with pandas: per-product price and quantity, the standard PVM formulas, separate lines for new and discontinued products, a check that the effects add up exactly to the change, a breakdown by product, and an Excel report with a bridge table.","md",[1950,1952,1954,1956],{"q":1877,"a":1951},"Aggregate quantity and revenue by product for both periods, compute average prices, then volume = change in total units × prior average price, mix = Σ current total units × change in share × prior price, and price = Σ change in price × current quantity.",{"q":1883,"a":1953},"The formulas must use consistent quantities and prices; with prior average price for volume, prior prices for mix and current quantities for price, the three add up exactly for continuing products.",{"q":1889,"a":1955},"Show their revenue as separate new-product and discontinued-product lines, because they have no price in one of the periods.",{"q":1895,"a":1957},"Yes. Use unit margin instead of unit price in the same formulas to explain the change in gross margin.","price volume mix analysis python",null,{"breadcrumb":1961},[1962,1964,1967],{"name":1963,"item":273},"Home",{"name":1965,"item":1966},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":1968,"item":29},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis",{"title":1971,"description":1972},"Build a Price-Volume-Mix (PVM) Revenue Analysis with Python","Price, volume and mix variance analysis in pandas: per-product average prices, volume at prior average price, mix from changes in sales share, price on current quantities, new and lost products, a reconciliation check and an Excel bridge.","build-a-price-volume-mix-analysis","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis\u002Findex",[1976,1977],"report-layouts","pivots-and-summaries","how-to","IXwnD5SaqlhTb10yshZYcvhhwl7c3BV58yWKwaaKcEU",[1981,1985],{"title":1982,"path":1983,"stem":1984,"children":-1},"Build a Monthly KPI Scorecard with Targets","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets\u002Findex",{"title":1241,"path":1986,"stem":1987,"children":-1},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report\u002Findex",[1989,1991],{"path":1990,"title":1968},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":1992,"title":1965},"\u002Fautomating-reporting-workflows",[],[1995,1999,2002,2005,2008,2011],{"path":1996,"title":1997,"kind":1998},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2000,"title":2001,"kind":1978},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-data-dictionary-sheet-to-an-excel-report","Add a Data Dictionary Sheet to an Excel Report",{"path":2003,"title":2004,"kind":1978},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard","Add a KPI Summary Block to an Excel Dashboard",{"path":2006,"title":2007,"kind":1978},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel","Add a Table of Contents Sheet with Hyperlinks in Excel",{"path":2009,"title":2010,"kind":1978},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-slicer-style-filters-to-an-excel-dashboard","Add a Dropdown Filter That Drives an Excel Dashboard",{"path":2012,"title":2013,"kind":1978},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python","Add a Summary Sheet to an Excel Report with Python",1790972125466]