[{"data":1,"prerenderedAt":2296},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-profitability-report":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-profitability-report":2261,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-profitability-report":2270,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-profitability-report":2275,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-profitability-report":2276},{"id":4,"title":5,"body":6,"dateModified":2227,"datePublished":2227,"description":2228,"extension":2229,"faq":2230,"keyword":2239,"kind":2240,"meta":2241,"navigation":88,"path":2250,"seo":2251,"slug":2254,"stem":2255,"topics":2256,"type":2259,"__hash__":2260},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-profitability-report\u002Findex.md","Build a Customer Profitability Report",{"type":7,"value":8,"toc":2215},"minimark",[9,19,24,53,56,60,740,748,897,901,1194,1197,1201,1319,1326,1442,1576,1580,2081,2097,2101,2104,2108,2143,2147,2150,2154,2160,2166,2172,2178,2182,2211],[10,11,12,13,18],"p",{},"Sales reports rank customers by revenue, and the biggest customers get the most attention. Yet a large customer can be unprofitable: they negotiate the deepest discounts, order in small, frequent drops that are expensive to pick and deliver, return more and pay late. A customer profitability report puts those costs back against each customer and shows what is left. The method is simple in principle — start from revenue, subtract the cost of the products, then subtract every cost that varies by customer — and the result is often surprising: typically a minority of customers produce more than all of the profit and a tail of customers loses money. This page builds the report with pandas from order, returns and payment data: gross margin per customer, rebates, freight, order-handling and returns costs allocated by activity, the cost of carrying their unpaid invoices, a profit waterfall per customer and a \"whale curve\" for the whole portfolio, written to a ranked workbook. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," pandas",[35,51,52],{"class":45}," openpyxl\n",[10,54,55],{},"The input is a year of orders for eight customers — revenue, product cost, freight and the value of goods returned on each order — plus each customer's annual rebate rate and average days to pay. Two cost rates come from finance: what it costs to process an order (picking, packing, invoicing) and to handle a return, and the annual cost of money tied up in receivables.",[20,57,59],{"id":58},"step-1-revenue-gross-margin-and-the-customer-specific-costs","Step 1 — Revenue, gross margin and the customer-specific costs",[25,61,65],{"className":62,"code":63,"language":64,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\norders = pd.read_excel(\"customer_data.xlsx\", sheet_name=\"Orders\")\nterms = pd.read_excel(\"customer_data.xlsx\", sheet_name=\"Terms\").set_index(\"Customer\")\nCOST_PER_ORDER, COST_PER_RETURN, COST_OF_CAPITAL = 42.0, 65.0, 0.08\n\nc = orders.groupby(\"Customer\").agg(Orders=(\"Order\", \"count\"), Revenue=(\"Revenue\", \"sum\"), Product_cost=(\"Product cost\", \"sum\"),\n                                   Freight=(\"Freight\", \"sum\"), Returned=(\"Returned\", \"sum\"),\n                                   Returns=(\"Returned\", lambda r: int((r > 0).sum())))\nc = c.join(terms)\nc[\"Net revenue\"] = c[\"Revenue\"] - c[\"Returned\"]\nc[\"Gross margin\"] = c[\"Net revenue\"] - c[\"Product_cost\"] * (1 - c[\"Returned\"] \u002F c[\"Revenue\"])\nc[\"Rebates\"] = c[\"Net revenue\"] * c[\"Rebate %\"]\nc[\"Order handling\"] = c[\"Orders\"] * COST_PER_ORDER\nc[\"Returns handling\"] = c[\"Returns\"] * COST_PER_RETURN\nc[\"Cost of credit\"] = c[\"Net revenue\"] * c[\"Days to pay\"] \u002F 365 * COST_OF_CAPITAL\nc[\"Customer profit\"] = c[\"Gross margin\"] - c[\"Rebates\"] - c[\"Freight\"] - c[\"Order handling\"] - c[\"Returns handling\"] - c[\"Cost of credit\"]\nc[\"Profit %\"] = c[\"Customer profit\"] \u002F c[\"Net revenue\"]\nview = c[[\"Orders\", \"Net revenue\", \"Gross margin\", \"Rebates\", \"Freight\", \"Order handling\", \"Returns handling\", \"Cost of credit\", \"Customer profit\", \"Profit %\"]]\nprint(view.sort_values(\"Customer profit\", ascending=False).round({c: 0 for c in view.columns if c != \"Profit %\"} | {\"Profit %\": 3}).to_string())\n","python",[32,66,67,83,90,120,149,182,187,256,291,326,336,367,422,449,472,496,534,592,618,670],{"__ignoreMap":30},[35,68,69,73,77,80],{"class":37,"line":38},[35,70,72],{"class":71},"s-kum","import",[35,74,76],{"class":75},"skGVy"," pandas ",[35,78,79],{"class":71},"as",[35,81,82],{"class":75}," pd\n",[35,84,86],{"class":37,"line":85},2,[35,87,89],{"emptyLinePlaceholder":88},true,"\n",[35,91,93,96,99,102,105,108,112,114,117],{"class":37,"line":92},3,[35,94,95],{"class":75},"orders ",[35,97,98],{"class":71},"=",[35,100,101],{"class":75}," pd.read_excel(",[35,103,104],{"class":45},"\"customer_data.xlsx\"",[35,106,107],{"class":75},", ",[35,109,111],{"class":110},"sa561","sheet_name",[35,113,98],{"class":71},[35,115,116],{"class":45},"\"Orders\"",[35,118,119],{"class":75},")\n",[35,121,123,126,128,130,132,134,136,138,141,144,147],{"class":37,"line":122},4,[35,124,125],{"class":75},"terms ",[35,127,98],{"class":71},[35,129,101],{"class":75},[35,131,104],{"class":45},[35,133,107],{"class":75},[35,135,111],{"class":110},[35,137,98],{"class":71},[35,139,140],{"class":45},"\"Terms\"",[35,142,143],{"class":75},").set_index(",[35,145,146],{"class":45},"\"Customer\"",[35,148,119],{"class":75},[35,150,152,156,158,161,163,166,169,172,174,177,179],{"class":37,"line":151},5,[35,153,155],{"class":154},"sP0c6","COST_PER_ORDER",[35,157,107],{"class":75},[35,159,160],{"class":154},"COST_PER_RETURN",[35,162,107],{"class":75},[35,164,165],{"class":154},"COST_OF_CAPITAL",[35,167,168],{"class":71}," =",[35,170,171],{"class":154}," 42.0",[35,173,107],{"class":75},[35,175,176],{"class":154},"65.0",[35,178,107],{"class":75},[35,180,181],{"class":154},"0.08\n",[35,183,185],{"class":37,"line":184},6,[35,186,89],{"emptyLinePlaceholder":88},[35,188,190,193,195,198,200,203,206,208,211,214,216,219,222,225,227,229,232,234,237,239,242,244,246,249,251,253],{"class":37,"line":189},7,[35,191,192],{"class":75},"c ",[35,194,98],{"class":71},[35,196,197],{"class":75}," orders.groupby(",[35,199,146],{"class":45},[35,201,202],{"class":75},").agg(",[35,204,205],{"class":110},"Orders",[35,207,98],{"class":71},[35,209,210],{"class":75},"(",[35,212,213],{"class":45},"\"Order\"",[35,215,107],{"class":75},[35,217,218],{"class":45},"\"count\"",[35,220,221],{"class":75},"), ",[35,223,224],{"class":110},"Revenue",[35,226,98],{"class":71},[35,228,210],{"class":75},[35,230,231],{"class":45},"\"Revenue\"",[35,233,107],{"class":75},[35,235,236],{"class":45},"\"sum\"",[35,238,221],{"class":75},[35,240,241],{"class":110},"Product_cost",[35,243,98],{"class":71},[35,245,210],{"class":75},[35,247,248],{"class":45},"\"Product cost\"",[35,250,107],{"class":75},[35,252,236],{"class":45},[35,254,255],{"class":75},"),\n",[35,257,259,262,264,266,269,271,273,275,278,280,282,285,287,289],{"class":37,"line":258},8,[35,260,261],{"class":110},"                                   Freight",[35,263,98],{"class":71},[35,265,210],{"class":75},[35,267,268],{"class":45},"\"Freight\"",[35,270,107],{"class":75},[35,272,236],{"class":45},[35,274,221],{"class":75},[35,276,277],{"class":110},"Returned",[35,279,98],{"class":71},[35,281,210],{"class":75},[35,283,284],{"class":45},"\"Returned\"",[35,286,107],{"class":75},[35,288,236],{"class":45},[35,290,255],{"class":75},[35,292,294,297,299,301,303,305,308,311,314,317,320,323],{"class":37,"line":293},9,[35,295,296],{"class":110},"                                   Returns",[35,298,98],{"class":71},[35,300,210],{"class":75},[35,302,284],{"class":45},[35,304,107],{"class":75},[35,306,307],{"class":71},"lambda",[35,309,310],{"class":75}," r: ",[35,312,313],{"class":154},"int",[35,315,316],{"class":75},"((r ",[35,318,319],{"class":71},">",[35,321,322],{"class":154}," 0",[35,324,325],{"class":75},").sum())))\n",[35,327,329,331,333],{"class":37,"line":328},10,[35,330,192],{"class":75},[35,332,98],{"class":71},[35,334,335],{"class":75}," c.join(terms)\n",[35,337,339,342,345,348,350,353,355,357,360,362,364],{"class":37,"line":338},11,[35,340,341],{"class":75},"c[",[35,343,344],{"class":45},"\"Net revenue\"",[35,346,347],{"class":75},"] ",[35,349,98],{"class":71},[35,351,352],{"class":75}," c[",[35,354,231],{"class":45},[35,356,347],{"class":75},[35,358,359],{"class":71},"-",[35,361,352],{"class":75},[35,363,284],{"class":45},[35,365,366],{"class":75},"]\n",[35,368,370,372,375,377,379,381,383,385,387,389,392,394,397,400,403,406,408,410,412,415,417,419],{"class":37,"line":369},12,[35,371,341],{"class":75},[35,373,374],{"class":45},"\"Gross margin\"",[35,376,347],{"class":75},[35,378,98],{"class":71},[35,380,352],{"class":75},[35,382,344],{"class":45},[35,384,347],{"class":75},[35,386,359],{"class":71},[35,388,352],{"class":75},[35,390,391],{"class":45},"\"Product_cost\"",[35,393,347],{"class":75},[35,395,396],{"class":71},"*",[35,398,399],{"class":75}," (",[35,401,402],{"class":154},"1",[35,404,405],{"class":71}," -",[35,407,352],{"class":75},[35,409,284],{"class":45},[35,411,347],{"class":75},[35,413,414],{"class":71},"\u002F",[35,416,352],{"class":75},[35,418,231],{"class":45},[35,420,421],{"class":75},"])\n",[35,423,425,427,430,432,434,436,438,440,442,444,447],{"class":37,"line":424},13,[35,426,341],{"class":75},[35,428,429],{"class":45},"\"Rebates\"",[35,431,347],{"class":75},[35,433,98],{"class":71},[35,435,352],{"class":75},[35,437,344],{"class":45},[35,439,347],{"class":75},[35,441,396],{"class":71},[35,443,352],{"class":75},[35,445,446],{"class":45},"\"Rebate %\"",[35,448,366],{"class":75},[35,450,452,454,457,459,461,463,465,467,469],{"class":37,"line":451},14,[35,453,341],{"class":75},[35,455,456],{"class":45},"\"Order handling\"",[35,458,347],{"class":75},[35,460,98],{"class":71},[35,462,352],{"class":75},[35,464,116],{"class":45},[35,466,347],{"class":75},[35,468,396],{"class":71},[35,470,471],{"class":154}," COST_PER_ORDER\n",[35,473,475,477,480,482,484,486,489,491,493],{"class":37,"line":474},15,[35,476,341],{"class":75},[35,478,479],{"class":45},"\"Returns handling\"",[35,481,347],{"class":75},[35,483,98],{"class":71},[35,485,352],{"class":75},[35,487,488],{"class":45},"\"Returns\"",[35,490,347],{"class":75},[35,492,396],{"class":71},[35,494,495],{"class":154}," COST_PER_RETURN\n",[35,497,499,501,504,506,508,510,512,514,516,518,521,523,525,528,531],{"class":37,"line":498},16,[35,500,341],{"class":75},[35,502,503],{"class":45},"\"Cost of credit\"",[35,505,347],{"class":75},[35,507,98],{"class":71},[35,509,352],{"class":75},[35,511,344],{"class":45},[35,513,347],{"class":75},[35,515,396],{"class":71},[35,517,352],{"class":75},[35,519,520],{"class":45},"\"Days to pay\"",[35,522,347],{"class":75},[35,524,414],{"class":71},[35,526,527],{"class":154}," 365",[35,529,530],{"class":71}," *",[35,532,533],{"class":154}," COST_OF_CAPITAL\n",[35,535,537,539,542,544,546,548,550,552,554,556,558,560,562,564,566,568,570,572,574,576,578,580,582,584,586,588,590],{"class":37,"line":536},17,[35,538,341],{"class":75},[35,540,541],{"class":45},"\"Customer profit\"",[35,543,347],{"class":75},[35,545,98],{"class":71},[35,547,352],{"class":75},[35,549,374],{"class":45},[35,551,347],{"class":75},[35,553,359],{"class":71},[35,555,352],{"class":75},[35,557,429],{"class":45},[35,559,347],{"class":75},[35,561,359],{"class":71},[35,563,352],{"class":75},[35,565,268],{"class":45},[35,567,347],{"class":75},[35,569,359],{"class":71},[35,571,352],{"class":75},[35,573,456],{"class":45},[35,575,347],{"class":75},[35,577,359],{"class":71},[35,579,352],{"class":75},[35,581,479],{"class":45},[35,583,347],{"class":75},[35,585,359],{"class":71},[35,587,352],{"class":75},[35,589,503],{"class":45},[35,591,366],{"class":75},[35,593,595,597,600,602,604,606,608,610,612,614,616],{"class":37,"line":594},18,[35,596,341],{"class":75},[35,598,599],{"class":45},"\"Profit %\"",[35,601,347],{"class":75},[35,603,98],{"class":71},[35,605,352],{"class":75},[35,607,541],{"class":45},[35,609,347],{"class":75},[35,611,414],{"class":71},[35,613,352],{"class":75},[35,615,344],{"class":45},[35,617,366],{"class":75},[35,619,621,624,626,629,631,633,635,637,639,641,643,645,647,649,651,653,655,657,659,661,663,665,667],{"class":37,"line":620},19,[35,622,623],{"class":75},"view ",[35,625,98],{"class":71},[35,627,628],{"class":75}," c[[",[35,630,116],{"class":45},[35,632,107],{"class":75},[35,634,344],{"class":45},[35,636,107],{"class":75},[35,638,374],{"class":45},[35,640,107],{"class":75},[35,642,429],{"class":45},[35,644,107],{"class":75},[35,646,268],{"class":45},[35,648,107],{"class":75},[35,650,456],{"class":45},[35,652,107],{"class":75},[35,654,479],{"class":45},[35,656,107],{"class":75},[35,658,503],{"class":45},[35,660,107],{"class":75},[35,662,541],{"class":45},[35,664,107],{"class":75},[35,666,599],{"class":45},[35,668,669],{"class":75},"]]\n",[35,671,673,676,679,681,683,686,688,691,694,697,700,703,706,709,712,714,717,720,723,726,729,731,734,737],{"class":37,"line":672},20,[35,674,675],{"class":154},"print",[35,677,678],{"class":75},"(view.sort_values(",[35,680,541],{"class":45},[35,682,107],{"class":75},[35,684,685],{"class":110},"ascending",[35,687,98],{"class":71},[35,689,690],{"class":154},"False",[35,692,693],{"class":75},").round({c: ",[35,695,696],{"class":154},"0",[35,698,699],{"class":71}," for",[35,701,702],{"class":75}," c ",[35,704,705],{"class":71},"in",[35,707,708],{"class":75}," view.columns ",[35,710,711],{"class":71},"if",[35,713,702],{"class":75},[35,715,716],{"class":71},"!=",[35,718,719],{"class":45}," \"Profit %\"",[35,721,722],{"class":75},"} ",[35,724,725],{"class":71},"|",[35,727,728],{"class":75}," {",[35,730,599],{"class":45},[35,732,733],{"class":75},": ",[35,735,736],{"class":154},"3",[35,738,739],{"class":75},"}).to_string())\n",[10,741,742,743,747],{},"Gross margin is net revenue — after returns — less the product cost of what was kept. Then come the costs that genuinely differ by customer. Rebates are a percentage of net revenue agreed in each contract. Freight is the actual delivery cost per order. Order handling and returns handling are ",[744,745,746],"em",{},"activity-based",": every order and every return costs the same to process, so a customer who places 220 small orders consumes far more of the warehouse and finance team than one who places 18 large ones. The cost of credit values the money tied up while the customer takes time to pay: net revenue × days to pay ÷ 365 × the annual cost of capital. Each line is a modest, defensible estimate; the point is that they add up differently for every customer.",[749,750,758,759,758,763,758,767,758,774,758,784,758,791,758,796,758,801,758,806,758,810,758,815,758,819,758,823,758,826,758,831,758,836,758,840,758,844,758,847,758,850,758,854,758,858,758,862,758,865,758,868,758,872,758,876,758,880,758,883,758,888,758,893],"svg",{"viewBox":751,"role":752,"ariaLabelledBy":753,"xmlns":756,"style":757},"84 0 592 566","img",[754,755],"cp-layers-t","cp-layers-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[760,761,762],"title",{"id":754},"From revenue to customer profit",[764,765,766],"desc",{"id":755},"Net revenue after returns, less product cost, gives gross margin. From gross margin, rebates, freight, order handling at a cost per order, returns handling at a cost per return and the cost of credit from days to pay are subtracted to give customer profit.",[768,769],"rect",{"x":770,"y":696,"width":771,"height":772,"fill":773},"84","592","566","#ffffff",[768,775],{"x":776,"y":777,"width":778,"height":779,"rx":780,"fill":781,"stroke":782,"style":783},"100.0","16.0","560.0","59.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[785,786,790],"text",{"x":787,"y":788,"style":789},"380.0","41.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Net revenue",[785,792,795],{"x":787,"y":793,"style":794},"61.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","after returns",[37,797],{"x1":787,"y1":798,"x2":787,"y2":799,"stroke":800,"style":783},"80.0","102.0","var(--brand,#5b5cf0)",[802,803],"polygon",{"points":804,"fill":805},"380.0,106.0 375.0,97.0 385.0,97.0","#5b5cf0",[768,807],{"x":776,"y":808,"width":778,"height":779,"rx":780,"fill":809,"stroke":800,"style":783},"111.0","#f0f4ff",[785,811,814],{"x":787,"y":812,"style":813},"136.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","− Product cost",[785,816,818],{"x":787,"y":817,"style":794},"156.0","= gross margin",[37,820],{"x1":787,"y1":821,"x2":787,"y2":822,"stroke":800,"style":783},"175.0","197.0",[802,824],{"points":825,"fill":805},"380.0,201.0 375.0,192.0 385.0,192.0",[768,827],{"x":776,"y":828,"width":778,"height":779,"rx":780,"fill":829,"stroke":830,"style":783},"206.0","#fdefd8","var(--gold,#b4740a)",[785,832,835],{"x":787,"y":833,"style":834},"231.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","− Rebates and freight",[785,837,839],{"x":787,"y":838,"style":794},"251.0","contract and delivery",[37,841],{"x1":787,"y1":842,"x2":787,"y2":843,"stroke":800,"style":783},"270.0","292.0",[802,845],{"points":846,"fill":805},"380.0,296.0 375.0,287.0 385.0,287.0",[768,848],{"x":776,"y":849,"width":778,"height":779,"rx":780,"fill":829,"stroke":830,"style":783},"301.0",[785,851,853],{"x":787,"y":852,"style":834},"326.5","− Order and returns handling",[785,855,857],{"x":787,"y":856,"style":794},"346.0","cost per activity",[37,859],{"x1":787,"y1":860,"x2":787,"y2":861,"stroke":800,"style":783},"365.0","387.0",[802,863],{"points":864,"fill":805},"380.0,391.0 375.0,382.0 385.0,382.0",[768,866],{"x":776,"y":867,"width":778,"height":779,"rx":780,"fill":829,"stroke":830,"style":783},"396.0",[785,869,871],{"x":787,"y":870,"style":834},"421.5","− Cost of credit",[785,873,875],{"x":787,"y":874,"style":794},"441.0","days to pay × cost of capital",[37,877],{"x1":787,"y1":878,"x2":787,"y2":879,"stroke":800,"style":783},"460.0","482.0",[802,881],{"points":882,"fill":805},"380.0,486.0 375.0,477.0 385.0,477.0",[768,884],{"x":776,"y":885,"width":778,"height":779,"rx":780,"fill":886,"stroke":887,"style":783},"491.0","#d9f4f1","var(--teal,#0f9488)",[785,889,892],{"x":787,"y":890,"style":891},"516.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","= Customer profit",[785,894,896],{"x":787,"y":895,"style":794},"536.0","what is really left",[20,898,900],{"id":899},"step-2-where-the-profit-goes-for-each-customer","Step 2 — Where the profit goes for each customer",[25,902,904],{"className":62,"code":903,"language":64,"meta":30,"style":30},"ranked = view.sort_values(\"Customer profit\", ascending=False)\nbest, worst = ranked.index[0], ranked.index[-1]\nfor name in (best, worst):\n    r = ranked.loc[name]\n    print(f\"{name}: revenue {r['Net revenue']:,.0f} -> margin {r['Gross margin']:,.0f} -> profit {r['Customer profit']:,.0f} \"\n          f\"({r['Profit %']:.1%}); handling {r['Order handling'] + r['Returns handling']:,.0f}, credit {r['Cost of credit']:,.0f}\")\nby_revenue = c[\"Net revenue\"].rank(ascending=False).astype(int)\nby_profit = c[\"Customer profit\"].rank(ascending=False).astype(int)\nprint(pd.DataFrame({\"Rank by revenue\": by_revenue, \"Rank by profit\": by_profit}).sort_values(\"Rank by revenue\").to_string())\n",[32,905,906,928,949,962,972,1049,1118,1145,1170],{"__ignoreMap":30},[35,907,908,911,913,916,918,920,922,924,926],{"class":37,"line":38},[35,909,910],{"class":75},"ranked ",[35,912,98],{"class":71},[35,914,915],{"class":75}," view.sort_values(",[35,917,541],{"class":45},[35,919,107],{"class":75},[35,921,685],{"class":110},[35,923,98],{"class":71},[35,925,690],{"class":154},[35,927,119],{"class":75},[35,929,930,933,935,938,940,943,945,947],{"class":37,"line":85},[35,931,932],{"class":75},"best, worst ",[35,934,98],{"class":71},[35,936,937],{"class":75}," ranked.index[",[35,939,696],{"class":154},[35,941,942],{"class":75},"], ranked.index[",[35,944,359],{"class":71},[35,946,402],{"class":154},[35,948,366],{"class":75},[35,950,951,954,957,959],{"class":37,"line":92},[35,952,953],{"class":71},"for",[35,955,956],{"class":75}," name ",[35,958,705],{"class":71},[35,960,961],{"class":75}," (best, worst):\n",[35,963,964,967,969],{"class":37,"line":122},[35,965,966],{"class":75},"    r ",[35,968,98],{"class":71},[35,970,971],{"class":75}," ranked.loc[name]\n",[35,973,974,977,979,982,985,989,992,995,998,1000,1003,1006,1009,1012,1014,1017,1019,1021,1024,1026,1028,1030,1033,1035,1037,1040,1042,1044,1046],{"class":37,"line":151},[35,975,976],{"class":154},"    print",[35,978,210],{"class":75},[35,980,981],{"class":71},"f",[35,983,984],{"class":45},"\"",[35,986,988],{"class":987},"sSjpA","{",[35,990,991],{"class":75},"name",[35,993,994],{"class":987},"}",[35,996,997],{"class":45},": revenue ",[35,999,988],{"class":987},[35,1001,1002],{"class":75},"r[",[35,1004,1005],{"class":45},"'Net revenue'",[35,1007,1008],{"class":75},"]",[35,1010,1011],{"class":71},":,.0f",[35,1013,994],{"class":987},[35,1015,1016],{"class":45}," -> margin ",[35,1018,988],{"class":987},[35,1020,1002],{"class":75},[35,1022,1023],{"class":45},"'Gross margin'",[35,1025,1008],{"class":75},[35,1027,1011],{"class":71},[35,1029,994],{"class":987},[35,1031,1032],{"class":45}," -> profit ",[35,1034,988],{"class":987},[35,1036,1002],{"class":75},[35,1038,1039],{"class":45},"'Customer profit'",[35,1041,1008],{"class":75},[35,1043,1011],{"class":71},[35,1045,994],{"class":987},[35,1047,1048],{"class":45}," \"\n",[35,1050,1051,1054,1057,1059,1061,1064,1066,1069,1071,1074,1076,1078,1081,1083,1086,1089,1092,1094,1096,1098,1101,1103,1105,1108,1110,1112,1114,1116],{"class":37,"line":184},[35,1052,1053],{"class":71},"          f",[35,1055,1056],{"class":45},"\"(",[35,1058,988],{"class":987},[35,1060,1002],{"class":75},[35,1062,1063],{"class":45},"'Profit %'",[35,1065,1008],{"class":75},[35,1067,1068],{"class":71},":.1%",[35,1070,994],{"class":987},[35,1072,1073],{"class":45},"); handling ",[35,1075,988],{"class":987},[35,1077,1002],{"class":75},[35,1079,1080],{"class":45},"'Order handling'",[35,1082,347],{"class":75},[35,1084,1085],{"class":71},"+",[35,1087,1088],{"class":75}," r[",[35,1090,1091],{"class":45},"'Returns handling'",[35,1093,1008],{"class":75},[35,1095,1011],{"class":71},[35,1097,994],{"class":987},[35,1099,1100],{"class":45},", credit ",[35,1102,988],{"class":987},[35,1104,1002],{"class":75},[35,1106,1107],{"class":45},"'Cost of credit'",[35,1109,1008],{"class":75},[35,1111,1011],{"class":71},[35,1113,994],{"class":987},[35,1115,984],{"class":45},[35,1117,119],{"class":75},[35,1119,1120,1123,1125,1127,1129,1132,1134,1136,1138,1141,1143],{"class":37,"line":189},[35,1121,1122],{"class":75},"by_revenue ",[35,1124,98],{"class":71},[35,1126,352],{"class":75},[35,1128,344],{"class":45},[35,1130,1131],{"class":75},"].rank(",[35,1133,685],{"class":110},[35,1135,98],{"class":71},[35,1137,690],{"class":154},[35,1139,1140],{"class":75},").astype(",[35,1142,313],{"class":154},[35,1144,119],{"class":75},[35,1146,1147,1150,1152,1154,1156,1158,1160,1162,1164,1166,1168],{"class":37,"line":258},[35,1148,1149],{"class":75},"by_profit ",[35,1151,98],{"class":71},[35,1153,352],{"class":75},[35,1155,541],{"class":45},[35,1157,1131],{"class":75},[35,1159,685],{"class":110},[35,1161,98],{"class":71},[35,1163,690],{"class":154},[35,1165,1140],{"class":75},[35,1167,313],{"class":154},[35,1169,119],{"class":75},[35,1171,1172,1174,1177,1180,1183,1186,1189,1191],{"class":37,"line":293},[35,1173,675],{"class":154},[35,1175,1176],{"class":75},"(pd.DataFrame({",[35,1178,1179],{"class":45},"\"Rank by revenue\"",[35,1181,1182],{"class":75},": by_revenue, ",[35,1184,1185],{"class":45},"\"Rank by profit\"",[35,1187,1188],{"class":75},": by_profit}).sort_values(",[35,1190,1179],{"class":45},[35,1192,1193],{"class":75},").to_string())\n",[10,1195,1196],{},"Comparing the rank by revenue with the rank by profit is usually the most persuasive single table in the report. Northwind Retail is the largest customer by revenue but only third by profit: 220 small orders and frequent returns cost it more than 23,000 in handling, and its rebate and slow payment take more, so it keeps about 15% of its revenue as profit against roughly 25% for Zenith and 30% for Oakridge, which order in bulk. At the other end, three customers — Atlas Supply, Café Señor and Harbour Print — lose money: Harbour Print's 95 small orders and frequent returns cost over 10,000 to handle on just 16,306 of revenue, a loss of 37 pence on every pound. These are the facts that support a conversation about minimum order sizes, delivery charges or payment terms.",[20,1198,1200],{"id":1199},"step-3-the-whale-curve","Step 3 — The whale curve",[25,1202,1204],{"className":62,"code":1203,"language":64,"meta":30,"style":30},"whale = ranked[\"Customer profit\"].cumsum() \u002F ranked[\"Customer profit\"].sum()\npeak = whale.max()\nprint((whale * 100).round(0).astype(int).to_string())\nprint(f\"cumulative profit peaks at {peak:.0%} of total after {int(whale.values.argmax()) + 1} of {len(whale)} customers\")\n",[32,1205,1206,1230,1240,1263],{"__ignoreMap":30},[35,1207,1208,1211,1213,1216,1218,1221,1223,1225,1227],{"class":37,"line":38},[35,1209,1210],{"class":75},"whale ",[35,1212,98],{"class":71},[35,1214,1215],{"class":75}," ranked[",[35,1217,541],{"class":45},[35,1219,1220],{"class":75},"].cumsum() ",[35,1222,414],{"class":71},[35,1224,1215],{"class":75},[35,1226,541],{"class":45},[35,1228,1229],{"class":75},"].sum()\n",[35,1231,1232,1235,1237],{"class":37,"line":85},[35,1233,1234],{"class":75},"peak ",[35,1236,98],{"class":71},[35,1238,1239],{"class":75}," whale.max()\n",[35,1241,1242,1244,1247,1249,1252,1255,1257,1259,1261],{"class":37,"line":92},[35,1243,675],{"class":154},[35,1245,1246],{"class":75},"((whale ",[35,1248,396],{"class":71},[35,1250,1251],{"class":154}," 100",[35,1253,1254],{"class":75},").round(",[35,1256,696],{"class":154},[35,1258,1140],{"class":75},[35,1260,313],{"class":154},[35,1262,1193],{"class":75},[35,1264,1265,1267,1269,1271,1274,1276,1279,1282,1284,1287,1289,1291,1294,1296,1299,1301,1304,1306,1309,1312,1314,1317],{"class":37,"line":122},[35,1266,675],{"class":154},[35,1268,210],{"class":75},[35,1270,981],{"class":71},[35,1272,1273],{"class":45},"\"cumulative profit peaks at ",[35,1275,988],{"class":987},[35,1277,1278],{"class":75},"peak",[35,1280,1281],{"class":71},":.0%",[35,1283,994],{"class":987},[35,1285,1286],{"class":45}," of total after ",[35,1288,988],{"class":987},[35,1290,313],{"class":154},[35,1292,1293],{"class":75},"(whale.values.argmax()) ",[35,1295,1085],{"class":71},[35,1297,1298],{"class":154}," 1",[35,1300,994],{"class":987},[35,1302,1303],{"class":45}," of ",[35,1305,988],{"class":987},[35,1307,1308],{"class":154},"len",[35,1310,1311],{"class":75},"(whale)",[35,1313,994],{"class":987},[35,1315,1316],{"class":45}," customers\"",[35,1318,119],{"class":75},[10,1320,1321,1322,1325],{},"Sorting customers from most to least profitable and plotting cumulative profit as a share of the total gives a ",[744,1323,1324],{},"whale curve",": it rises above 100% as the profitable customers are added, then falls back to 100% as the loss-makers subtract from it. The height of the hump is the profit the business would make if the unprofitable customers' losses disappeared. Here it peaks at 108% after the five profitable customers: the three loss-makers together give back 8% of the total. in real portfolios of hundreds of customers it commonly peaks at 150% or more, which is why the analysis is worth doing.",[749,1327,758,1333,758,1336,758,1339,758,1344,758,1350,758,1355,758,1359,758,1363,758,1366,758,1370,758,1373,758,1377,758,1381,758,1385,758,1389,758,1392,758,1395,758,1399,758,1403,758,1407,758,1411,758,1417,758,1422,758,1425,758,1428,758,1431,758,1434,758,1437,758,1440],{"viewBox":1328,"role":752,"ariaLabelledBy":1329,"xmlns":756,"style":1332},"4 0 748 252",[1330,1331],"cp-whale-t","cp-whale-d","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[760,1334,1335],{"id":1330},"Cumulative profit by customer, most profitable first",[764,1337,1338],{"id":1331},"Cumulative customer profit as a share of total profit rises to 108 percent after the five profitable customers, then falls back to 100 percent as the three loss-making customers are added.",[768,1340],{"x":1341,"y":696,"width":1342,"height":1343,"fill":773},"4","748","252",[37,1345],{"x1":1346,"y1":1347,"x2":1348,"y2":1347,"stroke":782,"style":1349},"51.3","214.0","736.0","stroke-width:1px",[785,1351,696],{"x":1352,"y":1353,"style":1354},"29.6","218.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[37,1356],{"x1":1346,"y1":1357,"x2":1348,"y2":1357,"stroke":782,"style":1358},"150.7","stroke-width:1px;stroke-dasharray:5 4",[785,1360,1362],{"x":1352,"y":1361,"style":1354},"154.7","50",[37,1364],{"x1":1346,"y1":1365,"x2":1348,"y2":1365,"stroke":782,"style":1358},"87.3",[785,1367,1369],{"x":1352,"y":1368,"style":1354},"91.3","100",[37,1371],{"x1":1346,"y1":1372,"x2":1348,"y2":1372,"stroke":782,"style":1358},"24.0",[785,1374,1376],{"x":1352,"y":1375,"style":1354},"28.0","150",[37,1378],{"x1":1346,"y1":1347,"x2":1348,"y2":1347,"stroke":1379,"style":1380},"var(--muted,#5b6780)","stroke-width:1.5px",[785,1382,402],{"x":1383,"y":1384,"style":794},"94.1","232.0",[785,1386,1388],{"x":1387,"y":1384,"style":794},"179.7","2",[785,1390,736],{"x":1391,"y":1384,"style":794},"265.3",[785,1393,1341],{"x":1394,"y":1384,"style":794},"350.8",[785,1396,1398],{"x":1397,"y":1384,"style":794},"436.4","5",[785,1400,1402],{"x":1401,"y":1384,"style":794},"522.0","6",[785,1404,1406],{"x":1405,"y":1384,"style":794},"607.6","7",[785,1408,1410],{"x":1409,"y":1384,"style":794},"693.2","8",[1412,1413],"path",{"d":1414,"fill":1415,"stroke":800,"style":1416},"M94.1,176.0 L179.7,144.3 L265.3,116.5 L350.8,91.1 L436.4,77.2 L522.0,78.5 L607.6,81.0 L693.2,87.3","none","stroke-width:2.5px;stroke-linejoin:round;stroke-linecap:round",[1418,1419],"circle",{"cx":1383,"cy":1420,"r":1421,"fill":773,"stroke":800,"style":783},"176.0","4.0",[1418,1423],{"cx":1387,"cy":1424,"r":1421,"fill":773,"stroke":800,"style":783},"144.3",[1418,1426],{"cx":1391,"cy":1427,"r":1421,"fill":773,"stroke":800,"style":783},"116.5",[1418,1429],{"cx":1394,"cy":1430,"r":1421,"fill":773,"stroke":800,"style":783},"91.1",[1418,1432],{"cx":1397,"cy":1433,"r":1421,"fill":773,"stroke":800,"style":783},"77.2",[1418,1435],{"cx":1401,"cy":1436,"r":1421,"fill":773,"stroke":800,"style":783},"78.5",[1418,1438],{"cx":1405,"cy":1439,"r":1421,"fill":773,"stroke":800,"style":783},"81.0",[1418,1441],{"cx":1409,"cy":1365,"r":1421,"fill":773,"stroke":800,"style":783},[749,1443,758,1449,758,1452,758,1455,758,1459,758,1463,758,1468,758,1472,758,1477,758,1480,758,1484,758,1487,758,1491,758,1494,758,1498,758,1501,758,1505,758,1508,758,1512,758,1515,758,1519,758,1521,758,1525,758,1528,758,1532,758,1535,758,1539,758,1542,758,1547,758,1550,758,1554,758,1557,758,1561,758,1564,758,1568,758,1571],{"viewBox":1444,"role":752,"ariaLabelledBy":1445,"xmlns":756,"style":1448},"4 0 744 340",[1446,1447],"cp-profit-t","cp-profit-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[760,1450,1451],{"id":1446},"Customer profit, most profitable first",[764,1453,1454],{"id":1447},"Zenith Logistics earns 40,086, Oakridge Farms 33,025, Northwind Retail 30,008, Granite Tools 26,620 and Brightside 15,066. Atlas Supply loses 728, Café Señor 3,964 and Harbour Print 6,043.",[768,1456],{"x":1341,"y":696,"width":1457,"height":1458,"fill":773},"744","340",[768,1460],{"x":1461,"y":777,"width":1462,"height":1375,"rx":1406,"fill":781,"stroke":782,"style":1349},"20.0","134.6",[785,1464,1467],{"x":1365,"y":1465,"style":1466},"34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Zenith Logistics",[768,1469],{"x":1470,"y":777,"width":1471,"height":1375,"rx":1406,"fill":886,"stroke":887,"style":1380},"164.6","513.3",[785,1473,1476],{"x":1474,"y":1465,"style":1475},"685.9","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","40,086",[768,1478],{"x":1461,"y":1479,"width":1462,"height":1375,"rx":1406,"fill":781,"stroke":782,"style":1349},"56.0",[785,1481,1483],{"x":1365,"y":1482,"style":1466},"74.5","Oakridge Farms",[768,1485],{"x":1470,"y":1479,"width":1486,"height":1375,"rx":1406,"fill":886,"stroke":887,"style":1380},"422.9",[785,1488,1490],{"x":1489,"y":1482,"style":1475},"595.5","33,025",[768,1492],{"x":1461,"y":1493,"width":1462,"height":1375,"rx":1406,"fill":781,"stroke":782,"style":1349},"96.0",[785,1495,1497],{"x":1365,"y":1496,"style":1466},"114.5","Northwind Retail",[768,1499],{"x":1470,"y":1493,"width":1500,"height":1375,"rx":1406,"fill":886,"stroke":887,"style":1380},"384.2",[785,1502,1504],{"x":1503,"y":1496,"style":1475},"556.9","30,008",[768,1506],{"x":1461,"y":1507,"width":1462,"height":1375,"rx":1406,"fill":781,"stroke":782,"style":1349},"136.0",[785,1509,1511],{"x":1365,"y":1510,"style":1466},"154.5","Granite Tools",[768,1513],{"x":1470,"y":1507,"width":1514,"height":1375,"rx":1406,"fill":886,"stroke":887,"style":1380},"340.9",[785,1516,1518],{"x":1517,"y":1510,"style":1475},"513.5","26,620",[768,1520],{"x":1461,"y":1420,"width":1462,"height":1375,"rx":1406,"fill":781,"stroke":782,"style":1349},[785,1522,1524],{"x":1365,"y":1523,"style":1466},"194.5","Brightside & Co",[768,1526],{"x":1470,"y":1420,"width":1527,"height":1375,"rx":1406,"fill":886,"stroke":887,"style":1380},"192.9",[785,1529,1531],{"x":1530,"y":1523,"style":1475},"365.5","15,066",[768,1533],{"x":1461,"y":1534,"width":1462,"height":1375,"rx":1406,"fill":781,"stroke":782,"style":1349},"216.0",[785,1536,1538],{"x":1365,"y":1537,"style":1466},"234.5","Atlas Supply",[768,1540],{"x":1470,"y":1534,"width":1541,"height":1375,"rx":1406,"fill":829,"stroke":830,"style":1380},"9.3",[785,1543,1546],{"x":1544,"y":1537,"style":1545},"181.9","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","−728",[768,1548],{"x":1461,"y":1549,"width":1462,"height":1375,"rx":1406,"fill":781,"stroke":782,"style":1349},"256.0",[785,1551,1553],{"x":1365,"y":1552,"style":1466},"274.5","Café Señor",[768,1555],{"x":1470,"y":1549,"width":1556,"height":1375,"rx":1406,"fill":829,"stroke":830,"style":1380},"50.8",[785,1558,1560],{"x":1559,"y":1552,"style":1545},"223.4","−3,964",[768,1562],{"x":1461,"y":1563,"width":1462,"height":1375,"rx":1406,"fill":781,"stroke":782,"style":1349},"296.0",[785,1565,1567],{"x":1365,"y":1566,"style":1466},"314.5","Harbour Print",[768,1569],{"x":1470,"y":1563,"width":1570,"height":1375,"rx":1406,"fill":809,"stroke":800,"style":1380},"77.4",[785,1572,1575],{"x":1573,"y":1566,"style":1574},"250.0","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","−6,043",[20,1577,1579],{"id":1578},"step-4-the-ranked-workbook","Step 4 — The ranked workbook",[25,1581,1583],{"className":62,"code":1582,"language":64,"meta":30,"style":30},"from openpyxl import load_workbook\nfrom openpyxl.formatting.rule import CellIsRule\nfrom openpyxl.styles import Font, PatternFill\n\npath = \"customer_profitability_2026.xlsx\"\nout = ranked.round(2)\nout[\"Cumulative profit %\"] = whale.round(4)\nwith pd.ExcelWriter(path, engine=\"openpyxl\") as xw:\n    out.to_excel(xw, sheet_name=\"Customers\", index_label=\"Customer\")\n    pd.DataFrame({\"Assumption\": [\"Cost per order\", \"Cost per return\", \"Cost of capital\"],\n                  \"Value\": [COST_PER_ORDER, COST_PER_RETURN, COST_OF_CAPITAL]}).to_excel(xw, sheet_name=\"Assumptions\", index=False)\nwb = load_workbook(path)\nws = wb[\"Customers\"]\nheaders = [c.value for c in ws[1]]\nprofit_col = ws.cell(1, headers.index(\"Customer profit\") + 1).column_letter\nws.conditional_formatting.add(f\"{profit_col}2:{profit_col}{ws.max_row}\",\n                              CellIsRule(operator=\"lessThan\", formula=[\"0\"], fill=PatternFill(\"solid\", fgColor=\"FFC7CE\"), font=Font(color=\"9C0006\")))\nfor row in ws.iter_rows(min_row=2, min_col=2):\n    for cell in row:\n        h = headers[cell.column - 1]\n        cell.number_format = \"0.0%\" if \"%\" in h else \"#,##0\"\nws.column_dimensions[\"A\"].width = 20\nwb.save(path)\nprint(wb.sheetnames)\n",[32,1584,1585,1598,1610,1622,1626,1636,1650,1669,1693,1716,1743,1781,1791,1805,1828,1854,1890,1961,1992,2005,2021,2050,2067,2073],{"__ignoreMap":30},[35,1586,1587,1590,1593,1595],{"class":37,"line":38},[35,1588,1589],{"class":71},"from",[35,1591,1592],{"class":75}," openpyxl ",[35,1594,72],{"class":71},[35,1596,1597],{"class":75}," load_workbook\n",[35,1599,1600,1602,1605,1607],{"class":37,"line":85},[35,1601,1589],{"class":71},[35,1603,1604],{"class":75}," openpyxl.formatting.rule ",[35,1606,72],{"class":71},[35,1608,1609],{"class":75}," CellIsRule\n",[35,1611,1612,1614,1617,1619],{"class":37,"line":92},[35,1613,1589],{"class":71},[35,1615,1616],{"class":75}," openpyxl.styles ",[35,1618,72],{"class":71},[35,1620,1621],{"class":75}," Font, PatternFill\n",[35,1623,1624],{"class":37,"line":122},[35,1625,89],{"emptyLinePlaceholder":88},[35,1627,1628,1631,1633],{"class":37,"line":151},[35,1629,1630],{"class":75},"path ",[35,1632,98],{"class":71},[35,1634,1635],{"class":45}," \"customer_profitability_2026.xlsx\"\n",[35,1637,1638,1641,1643,1646,1648],{"class":37,"line":184},[35,1639,1640],{"class":75},"out ",[35,1642,98],{"class":71},[35,1644,1645],{"class":75}," ranked.round(",[35,1647,1388],{"class":154},[35,1649,119],{"class":75},[35,1651,1652,1655,1658,1660,1662,1665,1667],{"class":37,"line":189},[35,1653,1654],{"class":75},"out[",[35,1656,1657],{"class":45},"\"Cumulative profit %\"",[35,1659,347],{"class":75},[35,1661,98],{"class":71},[35,1663,1664],{"class":75}," whale.round(",[35,1666,1341],{"class":154},[35,1668,119],{"class":75},[35,1670,1671,1674,1677,1680,1682,1685,1688,1690],{"class":37,"line":258},[35,1672,1673],{"class":71},"with",[35,1675,1676],{"class":75}," pd.ExcelWriter(path, ",[35,1678,1679],{"class":110},"engine",[35,1681,98],{"class":71},[35,1683,1684],{"class":45},"\"openpyxl\"",[35,1686,1687],{"class":75},") ",[35,1689,79],{"class":71},[35,1691,1692],{"class":75}," xw:\n",[35,1694,1695,1698,1700,1702,1705,1707,1710,1712,1714],{"class":37,"line":293},[35,1696,1697],{"class":75},"    out.to_excel(xw, ",[35,1699,111],{"class":110},[35,1701,98],{"class":71},[35,1703,1704],{"class":45},"\"Customers\"",[35,1706,107],{"class":75},[35,1708,1709],{"class":110},"index_label",[35,1711,98],{"class":71},[35,1713,146],{"class":45},[35,1715,119],{"class":75},[35,1717,1718,1721,1724,1727,1730,1732,1735,1737,1740],{"class":37,"line":328},[35,1719,1720],{"class":75},"    pd.DataFrame({",[35,1722,1723],{"class":45},"\"Assumption\"",[35,1725,1726],{"class":75},": [",[35,1728,1729],{"class":45},"\"Cost per order\"",[35,1731,107],{"class":75},[35,1733,1734],{"class":45},"\"Cost per return\"",[35,1736,107],{"class":75},[35,1738,1739],{"class":45},"\"Cost of capital\"",[35,1741,1742],{"class":75},"],\n",[35,1744,1745,1748,1750,1752,1754,1756,1758,1760,1763,1765,1767,1770,1772,1775,1777,1779],{"class":37,"line":338},[35,1746,1747],{"class":45},"                  \"Value\"",[35,1749,1726],{"class":75},[35,1751,155],{"class":154},[35,1753,107],{"class":75},[35,1755,160],{"class":154},[35,1757,107],{"class":75},[35,1759,165],{"class":154},[35,1761,1762],{"class":75},"]}).to_excel(xw, ",[35,1764,111],{"class":110},[35,1766,98],{"class":71},[35,1768,1769],{"class":45},"\"Assumptions\"",[35,1771,107],{"class":75},[35,1773,1774],{"class":110},"index",[35,1776,98],{"class":71},[35,1778,690],{"class":154},[35,1780,119],{"class":75},[35,1782,1783,1786,1788],{"class":37,"line":369},[35,1784,1785],{"class":75},"wb ",[35,1787,98],{"class":71},[35,1789,1790],{"class":75}," load_workbook(path)\n",[35,1792,1793,1796,1798,1801,1803],{"class":37,"line":424},[35,1794,1795],{"class":75},"ws ",[35,1797,98],{"class":71},[35,1799,1800],{"class":75}," wb[",[35,1802,1704],{"class":45},[35,1804,366],{"class":75},[35,1806,1807,1810,1812,1815,1817,1819,1821,1824,1826],{"class":37,"line":451},[35,1808,1809],{"class":75},"headers ",[35,1811,98],{"class":71},[35,1813,1814],{"class":75}," [c.value ",[35,1816,953],{"class":71},[35,1818,702],{"class":75},[35,1820,705],{"class":71},[35,1822,1823],{"class":75}," ws[",[35,1825,402],{"class":154},[35,1827,669],{"class":75},[35,1829,1830,1833,1835,1838,1840,1843,1845,1847,1849,1851],{"class":37,"line":474},[35,1831,1832],{"class":75},"profit_col ",[35,1834,98],{"class":71},[35,1836,1837],{"class":75}," ws.cell(",[35,1839,402],{"class":154},[35,1841,1842],{"class":75},", headers.index(",[35,1844,541],{"class":45},[35,1846,1687],{"class":75},[35,1848,1085],{"class":71},[35,1850,1298],{"class":154},[35,1852,1853],{"class":75},").column_letter\n",[35,1855,1856,1859,1861,1863,1865,1868,1870,1873,1875,1877,1880,1883,1885,1887],{"class":37,"line":498},[35,1857,1858],{"class":75},"ws.conditional_formatting.add(",[35,1860,981],{"class":71},[35,1862,984],{"class":45},[35,1864,988],{"class":987},[35,1866,1867],{"class":75},"profit_col",[35,1869,994],{"class":987},[35,1871,1872],{"class":45},"2:",[35,1874,988],{"class":987},[35,1876,1867],{"class":75},[35,1878,1879],{"class":987},"}{",[35,1881,1882],{"class":75},"ws.max_row",[35,1884,994],{"class":987},[35,1886,984],{"class":45},[35,1888,1889],{"class":75},",\n",[35,1891,1892,1895,1898,1900,1903,1905,1908,1910,1913,1916,1919,1922,1924,1927,1930,1932,1935,1937,1940,1942,1945,1947,1950,1953,1955,1958],{"class":37,"line":536},[35,1893,1894],{"class":75},"                              CellIsRule(",[35,1896,1897],{"class":110},"operator",[35,1899,98],{"class":71},[35,1901,1902],{"class":45},"\"lessThan\"",[35,1904,107],{"class":75},[35,1906,1907],{"class":110},"formula",[35,1909,98],{"class":71},[35,1911,1912],{"class":75},"[",[35,1914,1915],{"class":45},"\"0\"",[35,1917,1918],{"class":75},"], ",[35,1920,1921],{"class":110},"fill",[35,1923,98],{"class":71},[35,1925,1926],{"class":75},"PatternFill(",[35,1928,1929],{"class":45},"\"solid\"",[35,1931,107],{"class":75},[35,1933,1934],{"class":110},"fgColor",[35,1936,98],{"class":71},[35,1938,1939],{"class":45},"\"FFC7CE\"",[35,1941,221],{"class":75},[35,1943,1944],{"class":110},"font",[35,1946,98],{"class":71},[35,1948,1949],{"class":75},"Font(",[35,1951,1952],{"class":110},"color",[35,1954,98],{"class":71},[35,1956,1957],{"class":45},"\"9C0006\"",[35,1959,1960],{"class":75},")))\n",[35,1962,1963,1965,1968,1970,1973,1976,1978,1980,1982,1985,1987,1989],{"class":37,"line":594},[35,1964,953],{"class":71},[35,1966,1967],{"class":75}," row ",[35,1969,705],{"class":71},[35,1971,1972],{"class":75}," ws.iter_rows(",[35,1974,1975],{"class":110},"min_row",[35,1977,98],{"class":71},[35,1979,1388],{"class":154},[35,1981,107],{"class":75},[35,1983,1984],{"class":110},"min_col",[35,1986,98],{"class":71},[35,1988,1388],{"class":154},[35,1990,1991],{"class":75},"):\n",[35,1993,1994,1997,2000,2002],{"class":37,"line":620},[35,1995,1996],{"class":71},"    for",[35,1998,1999],{"class":75}," cell ",[35,2001,705],{"class":71},[35,2003,2004],{"class":75}," row:\n",[35,2006,2007,2010,2012,2015,2017,2019],{"class":37,"line":672},[35,2008,2009],{"class":75},"        h ",[35,2011,98],{"class":71},[35,2013,2014],{"class":75}," headers[cell.column ",[35,2016,359],{"class":71},[35,2018,1298],{"class":154},[35,2020,366],{"class":75},[35,2022,2024,2027,2029,2032,2035,2038,2041,2044,2047],{"class":37,"line":2023},21,[35,2025,2026],{"class":75},"        cell.number_format ",[35,2028,98],{"class":71},[35,2030,2031],{"class":45}," \"0.0%\"",[35,2033,2034],{"class":71}," if",[35,2036,2037],{"class":45}," \"%\"",[35,2039,2040],{"class":71}," in",[35,2042,2043],{"class":75}," h ",[35,2045,2046],{"class":71},"else",[35,2048,2049],{"class":45}," \"#,##0\"\n",[35,2051,2053,2056,2059,2062,2064],{"class":37,"line":2052},22,[35,2054,2055],{"class":75},"ws.column_dimensions[",[35,2057,2058],{"class":45},"\"A\"",[35,2060,2061],{"class":75},"].width ",[35,2063,98],{"class":71},[35,2065,2066],{"class":154}," 20\n",[35,2068,2070],{"class":37,"line":2069},23,[35,2071,2072],{"class":75},"wb.save(path)\n",[35,2074,2076,2078],{"class":37,"line":2075},24,[35,2077,675],{"class":154},[35,2079,2080],{"class":75},"(wb.sheetnames)\n",[10,2082,2083,2084,2087,2088,2091,2092,2096],{},"The ",[744,2085,2086],{},"Customers"," sheet lists customers from most to least profitable with every cost line, profit as a share of revenue and the cumulative profit share that forms the whale curve; loss-making customers are highlighted. The ",[744,2089,2090],{},"Assumptions"," sheet records the cost rates used, because the first question any sales director will ask is how the handling cost was worked out. Showing each customer's waterfall as a chart for an account review uses the same data; ",[14,2093,2095],{"href":2094},"\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.",[20,2098,2100],{"id":2099},"using-the-result-carefully","Using the result carefully",[10,2102,2103],{},"Customer profitability is an estimate built on allocations, and it should be presented that way. The cost per order and per return are averages: if a few large orders take much longer to pick than many small ones, the allocation favours small orders and the picture changes. Test the conclusions against the assumptions — if a customer is unprofitable at a handling cost of 42 per order but profitable at 30, that customer is marginal, not a problem. Fixed costs that would not disappear if the customer left, such as the warehouse building, are deliberately not allocated, so \"customer profit\" here means contribution: what the business would lose if the customer went. And the remedy for an unprofitable customer is rarely to drop them; it is usually to change what makes them expensive — order frequency, delivery terms, payment terms or rebate structure — which is why the report shows each cost separately.",[20,2105,2107],{"id":2106},"common-pitfalls","Common pitfalls",[2109,2110,2111,2119,2125,2131,2137],"ul",{},[2112,2113,2114,2118],"li",{},[2115,2116,2117],"strong",{},"Ranking by revenue."," Big customers are not necessarily profitable ones.",[2112,2120,2121,2124],{},[2115,2122,2123],{},"Allocating every cost by revenue."," Use activities — orders, returns — for costs that follow activity.",[2112,2126,2127,2130],{},[2115,2128,2129],{},"Ignoring payment behaviour."," Slow payers tie up capital; include the cost of credit.",[2112,2132,2133,2136],{},[2115,2134,2135],{},"Hiding the assumptions."," Publish the cost rates next to the results.",[2112,2138,2139,2142],{},[2115,2140,2141],{},"Allocating fixed overheads."," Customer profit is contribution; leave unavoidable costs out.",[20,2144,2146],{"id":2145},"conclusion","Conclusion",[10,2148,2149],{},"To build a customer profitability report, start from revenue net of returns and product cost, then subtract each customer's rebates and freight, order and returns handling costs allocated by activity, and the cost of credit implied by their days to pay. Rank customers by the resulting profit, compare with their revenue rank, draw the whale curve of cumulative profit, and write the ranked table with highlighted loss-makers and the assumptions used.",[20,2151,2153],{"id":2152},"frequently-asked-questions","Frequently asked questions",[10,2155,2156,2159],{},[2115,2157,2158],{},"How do I calculate profit by customer in pandas?","\nGroup orders by customer for revenue, product cost and freight, then subtract rebates, activity-based costs such as cost per order and per return, and a cost of credit based on days to pay.",[10,2161,2162,2165],{},[2115,2163,2164],{},"What is activity-based costing for customers?","\nAllocating service costs by the activities each customer causes, such as the number of orders, deliveries or returns, instead of in proportion to revenue.",[10,2167,2168,2171],{},[2115,2169,2170],{},"What is a whale curve?","\nA chart of cumulative profit, with customers sorted from most to least profitable; it rises above 100 percent and falls back as loss-making customers are added.",[10,2173,2174,2177],{},[2115,2175,2176],{},"How do I include slow payment in customer profitability?","\nEstimate the cost of credit as net revenue × days to pay ÷ 365 × the annual cost of capital.",[20,2179,2181],{"id":2180},"related","Related",[2109,2183,2184,2190,2197,2204],{},[2112,2185,2186,2187,2189],{},"Up one level: ",[14,2188,17],{"href":16}," — finance reports from Python.",[2112,2191,2192,2196],{},[14,2193,2195],{"href":2194},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report\u002F","Build a Product Margin Report"," — profit by product instead.",[2112,2198,2199,2203],{},[14,2200,2202],{"href":2201},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fallocate-shared-costs-across-departments\u002F","Allocate Shared Costs Across Departments with Python"," — allocation techniques.",[2112,2205,2206,2210],{},[14,2207,2209],{"href":2208},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis\u002F","Build a Price-Volume-Mix Analysis"," — explaining revenue changes.",[2212,2213,2214],"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}",{"title":30,"searchDepth":85,"depth":85,"links":2216},[2217,2218,2219,2220,2221,2222,2223,2224,2225,2226],{"id":22,"depth":85,"text":23},{"id":58,"depth":85,"text":59},{"id":899,"depth":85,"text":900},{"id":1199,"depth":85,"text":1200},{"id":1578,"depth":85,"text":1579},{"id":2099,"depth":85,"text":2100},{"id":2106,"depth":85,"text":2107},{"id":2145,"depth":85,"text":2146},{"id":2152,"depth":85,"text":2153},{"id":2180,"depth":85,"text":2181},"2026-10-02","Find out which customers actually make money with pandas: start from revenue and product margin, then subtract the costs that differ by customer — discounts and rebates, delivery, the cost of handling many small orders and returns, and the cost of slow payment — build a profit waterfall for each customer and a whale curve for the portfolio, and write a ranked Excel report.","md",[2231,2233,2235,2237],{"q":2158,"a":2232},"Group orders by customer for revenue, product cost and freight, then subtract rebates, activity-based costs such as cost per order and per return, and a cost of credit based on days to pay.",{"q":2164,"a":2234},"Allocating service costs by the activities each customer causes, such as the number of orders, deliveries or returns, instead of in proportion to revenue.",{"q":2170,"a":2236},"A chart of cumulative profit, with customers sorted from most to least profitable; it rises above 100 percent and falls back as loss-making customers are added.",{"q":2176,"a":2238},"Estimate the cost of credit as net revenue × days to pay ÷ 365 × the annual cost of capital.","customer profitability report python",null,{"breadcrumb":2242},[2243,2245,2248],{"name":2244,"item":414},"Home",{"name":2246,"item":2247},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2249,"item":16},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-profitability-report",{"title":2252,"description":2253},"Build a Customer Profitability Report in Excel with Python","Customer profitability with pandas: gross margin, rebates, freight, activity-based order and returns handling costs, cost of credit from days to pay, per-customer profit waterfall, whale curve and a ranked Excel report.","build-a-customer-profitability-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-profitability-report\u002Findex",[2257,2258],"report-layouts","pivots-and-summaries","how-to","TXWPrctG9nXahzLqonJTu9qBJL0cIFzvLd61nNGKEP4",[2262,2266],{"title":2263,"path":2264,"stem":2265,"children":-1},"Build a Customer Credit Limit Report","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-credit-limit-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-credit-limit-report\u002Findex",{"title":2267,"path":2268,"stem":2269,"children":-1},"Build a Deferred Revenue Schedule","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-deferred-revenue-schedule","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-deferred-revenue-schedule\u002Findex",[2271,2273],{"path":2272,"title":2249},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2274,"title":2246},"\u002Fautomating-reporting-workflows",[],[2277,2281,2284,2287,2290,2293],{"path":2278,"title":2279,"kind":2280},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2282,"title":2283,"kind":2259},"\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":2285,"title":2286,"kind":2259},"\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":2288,"title":2289,"kind":2259},"\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":2291,"title":2292,"kind":2259},"\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":2294,"title":2295,"kind":2259},"\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",1790972121381]