[{"data":1,"prerenderedAt":2660},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data":2625,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data":2634,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data":2639,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data":2640},{"id":4,"title":5,"body":6,"dateModified":2592,"datePublished":2592,"description":2593,"extension":2594,"faq":2595,"keyword":2604,"kind":2605,"meta":2606,"navigation":96,"path":2615,"seo":2616,"slug":2619,"stem":2620,"topics":2621,"type":2623,"__hash__":2624},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data\u002Findex.md","Build a Customer Churn Report from Subscription Data",{"type":7,"value":8,"toc":2581},"minimark",[9,24,29,58,61,65,68,288,295,299,302,840,843,854,1077,1080,1213,1217,1220,1504,1507,1775,1779,2453,2468,2471,2475,2509,2513,2516,2520,2526,2532,2538,2544,2548,2577],[10,11,12,13,17,18,23],"p",{},"For a subscription business, the monthly revenue figure hides the story that matters: how many customers left, how much revenue left with them, and whether the customers who stayed are paying more or less than before. A churn report answers those questions from the same data the billing system already holds — what each customer paid each month. This page builds one with pandas: for every month, the customers at the start, gained and lost, and the logo churn rate; a monthly recurring revenue (MRR) ",[14,15,16],"em",{},"bridge"," from opening to closing MRR through new, expansion, contraction and churned revenue; net revenue retention; and a cohort table showing what share of each month's new customers is still subscribed months later. Each month's bridge is checked to add up, and the report is written to a workbook with a colour scale on the cohort table. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," pandas",[40,56,57],{"class":50}," openpyxl\n",[10,59,60],{},"The input is a long table with one row per customer per month they were billed: customer ID, month and MRR. Most billing systems can export this directly, or it can be built from subscription start and end dates. Twelve months are used here, from October 2025 to September 2026; the first month is the opening book of customers.",[25,62,64],{"id":63},"step-1-a-customer-by-month-grid","Step 1 — A customer-by-month grid",[10,66,67],{},"Pivot the long table into one row per customer and one column per month, with 0 where a customer was not billed. Every movement can then be found by comparing a month's column with the previous one:",[30,69,73],{"className":70,"code":71,"language":72,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nmrr = pd.read_excel(\"subscription_mrr.xlsx\")\ngrid = mrr.pivot_table(index=\"customer_id\", columns=\"month\", values=\"mrr\", aggfunc=\"sum\", fill_value=0)\nmonths = list(grid.columns)\nprint(grid.shape[0], \"customers over\", len(months), \"months;\", f\"MRR in {months[-1]:%b %Y}: {grid[months[-1]].sum():,.0f}\")\n","python",[37,74,75,91,98,116,180,194],{"__ignoreMap":35},[40,76,77,81,85,88],{"class":42,"line":43},[40,78,80],{"class":79},"s-kum","import",[40,82,84],{"class":83},"skGVy"," pandas ",[40,86,87],{"class":79},"as",[40,89,90],{"class":83}," pd\n",[40,92,94],{"class":42,"line":93},2,[40,95,97],{"emptyLinePlaceholder":96},true,"\n",[40,99,101,104,107,110,113],{"class":42,"line":100},3,[40,102,103],{"class":83},"mrr ",[40,105,106],{"class":79},"=",[40,108,109],{"class":83}," pd.read_excel(",[40,111,112],{"class":50},"\"subscription_mrr.xlsx\"",[40,114,115],{"class":83},")\n",[40,117,119,122,124,127,131,133,136,139,142,144,147,149,152,154,157,159,162,164,167,169,172,174,178],{"class":42,"line":118},4,[40,120,121],{"class":83},"grid ",[40,123,106],{"class":79},[40,125,126],{"class":83}," mrr.pivot_table(",[40,128,130],{"class":129},"sa561","index",[40,132,106],{"class":79},[40,134,135],{"class":50},"\"customer_id\"",[40,137,138],{"class":83},", ",[40,140,141],{"class":129},"columns",[40,143,106],{"class":79},[40,145,146],{"class":50},"\"month\"",[40,148,138],{"class":83},[40,150,151],{"class":129},"values",[40,153,106],{"class":79},[40,155,156],{"class":50},"\"mrr\"",[40,158,138],{"class":83},[40,160,161],{"class":129},"aggfunc",[40,163,106],{"class":79},[40,165,166],{"class":50},"\"sum\"",[40,168,138],{"class":83},[40,170,171],{"class":129},"fill_value",[40,173,106],{"class":79},[40,175,177],{"class":176},"sP0c6","0",[40,179,115],{"class":83},[40,181,183,186,188,191],{"class":42,"line":182},5,[40,184,185],{"class":83},"months ",[40,187,106],{"class":79},[40,189,190],{"class":176}," list",[40,192,193],{"class":83},"(grid.columns)\n",[40,195,197,200,203,205,208,211,213,216,219,222,224,227,230,234,237,240,243,246,249,252,255,257,260,263,266,268,271,273,275,278,281,283,286],{"class":42,"line":196},6,[40,198,199],{"class":176},"print",[40,201,202],{"class":83},"(grid.shape[",[40,204,177],{"class":176},[40,206,207],{"class":83},"], ",[40,209,210],{"class":50},"\"customers over\"",[40,212,138],{"class":83},[40,214,215],{"class":176},"len",[40,217,218],{"class":83},"(months), ",[40,220,221],{"class":50},"\"months;\"",[40,223,138],{"class":83},[40,225,226],{"class":79},"f",[40,228,229],{"class":50},"\"MRR in ",[40,231,233],{"class":232},"sSjpA","{",[40,235,236],{"class":83},"months[",[40,238,239],{"class":79},"-",[40,241,242],{"class":176},"1",[40,244,245],{"class":83},"]",[40,247,248],{"class":79},":",[40,250,251],{"class":83},"%",[40,253,254],{"class":79},"b ",[40,256,251],{"class":83},[40,258,259],{"class":79},"Y",[40,261,262],{"class":232},"}",[40,264,265],{"class":50},": ",[40,267,233],{"class":232},[40,269,270],{"class":83},"grid[months[",[40,272,239],{"class":79},[40,274,242],{"class":176},[40,276,277],{"class":83},"]].sum()",[40,279,280],{"class":79},":,.0f",[40,282,262],{"class":232},[40,284,285],{"class":50},"\"",[40,287,115],{"class":83},[10,289,290,291,294],{},"The ",[37,292,293],{},"aggfunc=\"sum\""," matters if a customer can have more than one subscription: their lines are added together, so movements are measured per customer, which is what churn means — a customer who swaps one plan for another has not churned. Filling gaps with 0 turns \"not billed\" into an explicit zero that the comparisons below can use.",[25,296,298],{"id":297},"step-2-monthly-movements-and-the-mrr-bridge","Step 2 — Monthly movements and the MRR bridge",[10,300,301],{},"For each month, classify every customer by comparing this month's MRR with last month's:",[30,303,305],{"className":70,"code":304,"language":72,"meta":35,"style":35},"rows = []\nfor prev, cur in zip(months, months[1:]):\n    a, b = grid[prev], grid[cur]\n    was, now = a > 0, b > 0\n    rows.append({\n        \"month\": cur,\n        \"customers_start\": int(was.sum()),\n        \"new_customers\": int((~was & now).sum()),\n        \"churned_customers\": int((was & ~now).sum()),\n        \"customers_end\": int(now.sum()),\n        \"mrr_start\": a.sum(),\n        \"new_mrr\": b[~was & now].sum(),\n        \"expansion_mrr\": (b - a)[was & now & (b > a)].sum(),\n        \"contraction_mrr\": (b - a)[was & now & (b \u003C a)].sum(),\n        \"churned_mrr\": -a[was & ~now].sum(),\n        \"mrr_end\": b.sum(),\n    })\nreport = pd.DataFrame(rows).set_index(\"month\")\nreport[\"logo_churn\"] = report[\"churned_customers\"] \u002F report[\"customers_start\"]\nreport[\"nrr\"] = (report[\"mrr_start\"] + report[\"expansion_mrr\"] + report[\"contraction_mrr\"] + report[\"churned_mrr\"]) \u002F report[\"mrr_start\"]\n\nbridge = report[\"mrr_start\"] + report[\"new_mrr\"] + report[\"expansion_mrr\"] + report[\"contraction_mrr\"] + report[\"churned_mrr\"]\nassert (bridge - report[\"mrr_end\"]).abs().max() \u003C 0.01, \"MRR bridge does not add up\"\ncols = [\"customers_start\", \"new_customers\", \"churned_customers\", \"logo_churn\", \"mrr_start\", \"new_mrr\",\n        \"expansion_mrr\", \"contraction_mrr\", \"churned_mrr\", \"mrr_end\", \"nrr\"]\nprint(report[cols].tail(3).round(3).to_string())\n",[37,306,307,317,339,349,373,378,386,400,425,446,459,468,486,515,540,560,569,575,590,623,680,685,732,761,798,821],{"__ignoreMap":35},[40,308,309,312,314],{"class":42,"line":43},[40,310,311],{"class":83},"rows ",[40,313,106],{"class":79},[40,315,316],{"class":83}," []\n",[40,318,319,322,325,328,331,334,336],{"class":42,"line":93},[40,320,321],{"class":79},"for",[40,323,324],{"class":83}," prev, cur ",[40,326,327],{"class":79},"in",[40,329,330],{"class":176}," zip",[40,332,333],{"class":83},"(months, months[",[40,335,242],{"class":176},[40,337,338],{"class":83},":]):\n",[40,340,341,344,346],{"class":42,"line":100},[40,342,343],{"class":83},"    a, b ",[40,345,106],{"class":79},[40,347,348],{"class":83}," grid[prev], grid[cur]\n",[40,350,351,354,356,359,362,365,368,370],{"class":42,"line":118},[40,352,353],{"class":83},"    was, now ",[40,355,106],{"class":79},[40,357,358],{"class":83}," a ",[40,360,361],{"class":79},">",[40,363,364],{"class":176}," 0",[40,366,367],{"class":83},", b ",[40,369,361],{"class":79},[40,371,372],{"class":176}," 0\n",[40,374,375],{"class":42,"line":182},[40,376,377],{"class":83},"    rows.append({\n",[40,379,380,383],{"class":42,"line":196},[40,381,382],{"class":50},"        \"month\"",[40,384,385],{"class":83},": cur,\n",[40,387,389,392,394,397],{"class":42,"line":388},7,[40,390,391],{"class":50},"        \"customers_start\"",[40,393,265],{"class":83},[40,395,396],{"class":176},"int",[40,398,399],{"class":83},"(was.sum()),\n",[40,401,403,406,408,410,413,416,419,422],{"class":42,"line":402},8,[40,404,405],{"class":50},"        \"new_customers\"",[40,407,265],{"class":83},[40,409,396],{"class":176},[40,411,412],{"class":83},"((",[40,414,415],{"class":79},"~",[40,417,418],{"class":83},"was ",[40,420,421],{"class":79},"&",[40,423,424],{"class":83}," now).sum()),\n",[40,426,428,431,433,435,438,440,443],{"class":42,"line":427},9,[40,429,430],{"class":50},"        \"churned_customers\"",[40,432,265],{"class":83},[40,434,396],{"class":176},[40,436,437],{"class":83},"((was ",[40,439,421],{"class":79},[40,441,442],{"class":79}," ~",[40,444,445],{"class":83},"now).sum()),\n",[40,447,449,452,454,456],{"class":42,"line":448},10,[40,450,451],{"class":50},"        \"customers_end\"",[40,453,265],{"class":83},[40,455,396],{"class":176},[40,457,458],{"class":83},"(now.sum()),\n",[40,460,462,465],{"class":42,"line":461},11,[40,463,464],{"class":50},"        \"mrr_start\"",[40,466,467],{"class":83},": a.sum(),\n",[40,469,471,474,477,479,481,483],{"class":42,"line":470},12,[40,472,473],{"class":50},"        \"new_mrr\"",[40,475,476],{"class":83},": b[",[40,478,415],{"class":79},[40,480,418],{"class":83},[40,482,421],{"class":79},[40,484,485],{"class":83}," now].sum(),\n",[40,487,489,492,495,497,500,502,505,507,510,512],{"class":42,"line":488},13,[40,490,491],{"class":50},"        \"expansion_mrr\"",[40,493,494],{"class":83},": (b ",[40,496,239],{"class":79},[40,498,499],{"class":83}," a)[was ",[40,501,421],{"class":79},[40,503,504],{"class":83}," now ",[40,506,421],{"class":79},[40,508,509],{"class":83}," (b ",[40,511,361],{"class":79},[40,513,514],{"class":83}," a)].sum(),\n",[40,516,518,521,523,525,527,529,531,533,535,538],{"class":42,"line":517},14,[40,519,520],{"class":50},"        \"contraction_mrr\"",[40,522,494],{"class":83},[40,524,239],{"class":79},[40,526,499],{"class":83},[40,528,421],{"class":79},[40,530,504],{"class":83},[40,532,421],{"class":79},[40,534,509],{"class":83},[40,536,537],{"class":79},"\u003C",[40,539,514],{"class":83},[40,541,543,546,548,550,553,555,557],{"class":42,"line":542},15,[40,544,545],{"class":50},"        \"churned_mrr\"",[40,547,265],{"class":83},[40,549,239],{"class":79},[40,551,552],{"class":83},"a[was ",[40,554,421],{"class":79},[40,556,442],{"class":79},[40,558,559],{"class":83},"now].sum(),\n",[40,561,563,566],{"class":42,"line":562},16,[40,564,565],{"class":50},"        \"mrr_end\"",[40,567,568],{"class":83},": b.sum(),\n",[40,570,572],{"class":42,"line":571},17,[40,573,574],{"class":83},"    })\n",[40,576,578,581,583,586,588],{"class":42,"line":577},18,[40,579,580],{"class":83},"report ",[40,582,106],{"class":79},[40,584,585],{"class":83}," pd.DataFrame(rows).set_index(",[40,587,146],{"class":50},[40,589,115],{"class":83},[40,591,593,596,599,602,604,607,610,612,615,617,620],{"class":42,"line":592},19,[40,594,595],{"class":83},"report[",[40,597,598],{"class":50},"\"logo_churn\"",[40,600,601],{"class":83},"] ",[40,603,106],{"class":79},[40,605,606],{"class":83}," report[",[40,608,609],{"class":50},"\"churned_customers\"",[40,611,601],{"class":83},[40,613,614],{"class":79},"\u002F",[40,616,606],{"class":83},[40,618,619],{"class":50},"\"customers_start\"",[40,621,622],{"class":83},"]\n",[40,624,626,628,631,633,635,638,641,643,646,648,651,653,655,657,660,662,664,666,669,672,674,676,678],{"class":42,"line":625},20,[40,627,595],{"class":83},[40,629,630],{"class":50},"\"nrr\"",[40,632,601],{"class":83},[40,634,106],{"class":79},[40,636,637],{"class":83}," (report[",[40,639,640],{"class":50},"\"mrr_start\"",[40,642,601],{"class":83},[40,644,645],{"class":79},"+",[40,647,606],{"class":83},[40,649,650],{"class":50},"\"expansion_mrr\"",[40,652,601],{"class":83},[40,654,645],{"class":79},[40,656,606],{"class":83},[40,658,659],{"class":50},"\"contraction_mrr\"",[40,661,601],{"class":83},[40,663,645],{"class":79},[40,665,606],{"class":83},[40,667,668],{"class":50},"\"churned_mrr\"",[40,670,671],{"class":83},"]) ",[40,673,614],{"class":79},[40,675,606],{"class":83},[40,677,640],{"class":50},[40,679,622],{"class":83},[40,681,683],{"class":42,"line":682},21,[40,684,97],{"emptyLinePlaceholder":96},[40,686,688,691,693,695,697,699,701,703,706,708,710,712,714,716,718,720,722,724,726,728,730],{"class":42,"line":687},22,[40,689,690],{"class":83},"bridge ",[40,692,106],{"class":79},[40,694,606],{"class":83},[40,696,640],{"class":50},[40,698,601],{"class":83},[40,700,645],{"class":79},[40,702,606],{"class":83},[40,704,705],{"class":50},"\"new_mrr\"",[40,707,601],{"class":83},[40,709,645],{"class":79},[40,711,606],{"class":83},[40,713,650],{"class":50},[40,715,601],{"class":83},[40,717,645],{"class":79},[40,719,606],{"class":83},[40,721,659],{"class":50},[40,723,601],{"class":83},[40,725,645],{"class":79},[40,727,606],{"class":83},[40,729,668],{"class":50},[40,731,622],{"class":83},[40,733,735,738,741,743,745,748,751,753,756,758],{"class":42,"line":734},23,[40,736,737],{"class":79},"assert",[40,739,740],{"class":83}," (bridge ",[40,742,239],{"class":79},[40,744,606],{"class":83},[40,746,747],{"class":50},"\"mrr_end\"",[40,749,750],{"class":83},"]).abs().max() ",[40,752,537],{"class":79},[40,754,755],{"class":176}," 0.01",[40,757,138],{"class":83},[40,759,760],{"class":50},"\"MRR bridge does not add up\"\n",[40,762,764,767,769,772,774,776,779,781,783,785,787,789,791,793,795],{"class":42,"line":763},24,[40,765,766],{"class":83},"cols ",[40,768,106],{"class":79},[40,770,771],{"class":83}," [",[40,773,619],{"class":50},[40,775,138],{"class":83},[40,777,778],{"class":50},"\"new_customers\"",[40,780,138],{"class":83},[40,782,609],{"class":50},[40,784,138],{"class":83},[40,786,598],{"class":50},[40,788,138],{"class":83},[40,790,640],{"class":50},[40,792,138],{"class":83},[40,794,705],{"class":50},[40,796,797],{"class":83},",\n",[40,799,801,803,805,807,809,811,813,815,817,819],{"class":42,"line":800},25,[40,802,491],{"class":50},[40,804,138],{"class":83},[40,806,659],{"class":50},[40,808,138],{"class":83},[40,810,668],{"class":50},[40,812,138],{"class":83},[40,814,747],{"class":50},[40,816,138],{"class":83},[40,818,630],{"class":50},[40,820,622],{"class":83},[40,822,824,826,829,832,835,837],{"class":42,"line":823},26,[40,825,199],{"class":176},[40,827,828],{"class":83},"(report[cols].tail(",[40,830,831],{"class":176},"3",[40,833,834],{"class":83},").round(",[40,836,831],{"class":176},[40,838,839],{"class":83},").to_string())\n",[10,841,842],{},"A customer billed last month but not this month has churned, taking last month's MRR with them; one billed this month but not last is new. Customers billed in both months either pay more (expansion), less (contraction) or the same. Contraction and churn are stored as negative numbers so the bridge reads naturally — opening MRR plus every movement equals closing MRR — and the assertion proves it for every month. A bridge that does not add up means a customer was double-counted or missed, which is worth knowing before the figures reach a board pack.",[10,844,845,849,850,853],{},[846,847,848],"strong",{},"Logo churn"," is the share of opening customers lost in the month. ",[846,851,852],{},"Net revenue retention"," (NRR) looks only at revenue from customers who were there at the start of the month: what fraction of it remains after their expansions, contractions and cancellations. NRR above 100% means existing customers grew faster than they left — the business would grow even without new sales.",[855,856,864,865,864,869,864,873,864,880,864,890,864,901,864,908,864,912,864,917,864,921,864,925,864,931,864,936,864,941,864,948,864,953,864,956,864,959,864,963,864,966,864,969,864,975,864,979,864,983,864,987,864,991,864,995,864,998,864,1003,864,1010,864,1015,864,1019,864,1022,864,1026,864,1029,864,1032,864,1038,864,1041,864,1044,864,1047,864,1049,864,1053,864,1056,864,1060,864,1067,864,1072,864,1074],"svg",{"viewBox":857,"role":858,"ariaLabelledBy":859,"xmlns":862,"style":863},"4 0 752 178","img",[860,861],"ch-bridge-t","ch-bridge-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[866,867,868],"title",{"id":860},"The MRR bridge for one month",[870,871,872],"desc",{"id":861},"Opening MRR plus revenue from new customers plus expansion from existing customers, less contraction from customers who downgraded and less the MRR of customers who left, equals closing MRR. The check asserts this for every month.",[874,875],"rect",{"x":876,"y":177,"width":877,"height":878,"fill":879},"4","752","178","#ffffff",[874,881],{"x":882,"y":883,"width":884,"height":885,"rx":886,"fill":887,"stroke":888,"style":889},"20.0","16.0","90.0","145.5","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[891,892,894],"g",{"transform":893},"translate(53.0,29.0) scale(1.000)",[895,896],"path",{"d":897,"fill":898,"stroke":899,"style":900},"M5 6c0-1.7 3.1-3 7-3s7 1.3 7 3-3.1 3-7 3-7-1.3-7-3z M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6 M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[902,903,907],"text",{"x":904,"y":905,"style":906},"65.0","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Opening",[902,909,911],{"x":904,"y":910,"style":906},"93.0","MRR",[902,913,916],{"x":904,"y":914,"style":915},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","last",[902,918,920],{"x":904,"y":919,"style":915},"130.0","month's",[902,922,924],{"x":904,"y":923,"style":915},"147.5","total",[42,926],{"x1":927,"y1":928,"x2":929,"y2":928,"stroke":930,"style":889},"115.0","88.8","137.0","var(--brand,#5b5cf0)",[932,933],"polygon",{"points":934,"fill":935},"141.0,88.8 132.0,93.8 132.0,83.8","#5b5cf0",[874,937],{"x":938,"y":883,"width":884,"height":885,"rx":886,"fill":939,"stroke":940,"style":889},"146.0","#d9f4f1","var(--teal,#0f9488)",[891,942,944],{"transform":943},"translate(179.0,46.5) scale(1.000)",[895,945],{"d":946,"fill":898,"stroke":947,"style":900},"M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M4 21c0-4 3.6-7 8-7s8 3 8 7","var(--teal-ink,#0b6157)",[902,949,952],{"x":950,"y":910,"style":951},"191.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","+ New",[902,954,955],{"x":950,"y":914,"style":915},"first month",[902,957,958],{"x":950,"y":919,"style":915},"billed",[42,960],{"x1":961,"y1":928,"x2":962,"y2":928,"stroke":930,"style":889},"241.0","263.0",[932,964],{"points":965,"fill":935},"267.0,88.8 258.0,93.8 258.0,83.8",[874,967],{"x":968,"y":883,"width":884,"height":885,"rx":886,"fill":939,"stroke":940,"style":889},"272.0",[891,970,972],{"transform":971},"translate(305.0,37.8) scale(1.000)",[895,973],{"d":974,"fill":898,"stroke":947,"style":900},"M4 20h16 M4 20V4 M6 16l4-5 4 3 6-7",[902,976,645],{"x":977,"y":978,"style":951},"317.0","84.2",[902,980,982],{"x":977,"y":981,"style":951},"101.8","Expansion",[902,984,986],{"x":977,"y":985,"style":915},"121.2","paying",[902,988,990],{"x":977,"y":989,"style":915},"138.8","more",[42,992],{"x1":993,"y1":928,"x2":994,"y2":928,"stroke":930,"style":889},"367.0","389.0",[932,996],{"points":997,"fill":935},"393.0,88.8 384.0,93.8 384.0,83.8",[874,999],{"x":1000,"y":883,"width":884,"height":885,"rx":886,"fill":1001,"stroke":1002,"style":889},"398.0","#fdefd8","var(--gold,#b4740a)",[891,1004,1006],{"transform":1005},"translate(431.0,46.5) scale(1.000)",[895,1007],{"d":1008,"fill":898,"stroke":1009,"style":900},"M3 5h18l-7 8v6l-4 2v-8z","var(--gold-ink,#7a4e06)",[902,1011,1014],{"x":1012,"y":910,"style":1013},"443.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","−",[902,1016,1018],{"x":1012,"y":1017,"style":1013},"110.5","Contraction",[902,1020,1021],{"x":1012,"y":919,"style":915},"paying less",[42,1023],{"x1":1024,"y1":928,"x2":1025,"y2":928,"stroke":930,"style":889},"493.0","515.0",[932,1027],{"points":1028,"fill":935},"519.0,88.8 510.0,93.8 510.0,83.8",[874,1030],{"x":1031,"y":883,"width":884,"height":885,"rx":886,"fill":1001,"stroke":1002,"style":889},"524.0",[891,1033,1035],{"transform":1034},"translate(557.0,37.8) scale(1.000)",[895,1036],{"d":1037,"fill":898,"stroke":1009,"style":900},"M6 6l12 12 M18 6L6 18",[902,1039,1014],{"x":1040,"y":978,"style":1013},"569.0",[902,1042,1043],{"x":1040,"y":981,"style":1013},"Churned",[902,1045,1046],{"x":1040,"y":985,"style":915},"no longer",[902,1048,958],{"x":1040,"y":989,"style":915},[42,1050],{"x1":1051,"y1":928,"x2":1052,"y2":928,"stroke":930,"style":889},"619.0","641.0",[932,1054],{"points":1055,"fill":935},"645.0,88.8 636.0,93.8 636.0,83.8",[874,1057],{"x":1058,"y":883,"width":884,"height":885,"rx":886,"fill":1059,"stroke":930,"style":889},"650.0","#f0f4ff",[891,1061,1063],{"transform":1062},"translate(683.0,46.5) scale(1.000)",[895,1064],{"d":1065,"fill":898,"stroke":1066,"style":900},"M5 12.5l4.5 4.5L19 7","var(--brand-strong,#4338ca)",[902,1068,1071],{"x":1069,"y":910,"style":1070},"695.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","= Closing",[902,1073,911],{"x":1069,"y":1017,"style":1070},[902,1075,1076],{"x":1069,"y":919,"style":915},"checked",[10,1078,1079],{},"Over the year, logo churn moves between 2.7% and 6.0% a month and NRR between 95% and 102%. September is the weakest month for revenue retention: 19 of 347 customers left, taking 1,691 of MRR, and NRR fell to 95.0%. MRR still grew, from 27,033 to 27,271, because 31 new customers added 1,579 — growth that depends entirely on new sales is the pattern a churn report is there to expose.",[855,1081,864,1087,864,1090,864,1093,864,1097,864,1103,864,1108,864,1112,864,1116,864,1119,864,1122,864,1125,864,1129,864,1133,864,1138,864,1142,864,1146,864,1150,864,1154,864,1158,864,1162,864,1166,864,1170,864,1173,864,1177,864,1181,864,1185,864,1188,864,1191,864,1194,864,1197,864,1200,864,1202,864,1205,864,1207,864,1210],{"viewBox":1082,"role":858,"ariaLabelledBy":1083,"xmlns":862,"style":1086},"4 0 748 252",[1084,1085],"ch-trend-t","ch-trend-d","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[866,1088,1089],{"id":1084},"Monthly logo churn",[870,1091,1092],{"id":1085},"Monthly logo churn ranges from 2.7 percent in April 2026 to 6.0 percent in May 2026 and ends at 5.5 percent in September 2026.",[874,1094],{"x":876,"y":177,"width":1095,"height":1096,"fill":879},"748","252",[42,1098],{"x1":1099,"y1":1100,"x2":1101,"y2":1100,"stroke":888,"style":1102},"39.5","214.0","736.0","stroke-width:1px",[902,1104,177],{"x":1105,"y":1106,"style":1107},"23.7","218.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[42,1109],{"x1":1099,"y1":1110,"x2":1101,"y2":1110,"stroke":888,"style":1111},"150.7","stroke-width:1px;stroke-dasharray:5 4",[902,1113,1115],{"x":1105,"y":1114,"style":1107},"154.7","2",[42,1117],{"x1":1099,"y1":1118,"x2":1101,"y2":1118,"stroke":888,"style":1111},"87.3",[902,1120,876],{"x":1105,"y":1121,"style":1107},"91.3",[42,1123],{"x1":1099,"y1":1124,"x2":1101,"y2":1124,"stroke":888,"style":1111},"24.0",[902,1126,1128],{"x":1105,"y":1127,"style":1107},"28.0","6",[42,1130],{"x1":1099,"y1":1100,"x2":1101,"y2":1100,"stroke":1131,"style":1132},"var(--muted,#5b6780)","stroke-width:1.5px",[902,1134,1137],{"x":1135,"y":1136,"style":915},"71.1","232.0","Nov",[902,1139,1141],{"x":1140,"y":1136,"style":915},"134.4","Dec",[902,1143,1145],{"x":1144,"y":1136,"style":915},"197.8","Jan",[902,1147,1149],{"x":1148,"y":1136,"style":915},"261.1","Feb",[902,1151,1153],{"x":1152,"y":1136,"style":915},"324.4","Mar",[902,1155,1157],{"x":1156,"y":1136,"style":915},"387.7","Apr",[902,1159,1161],{"x":1160,"y":1136,"style":915},"451.1","May",[902,1163,1165],{"x":1164,"y":1136,"style":915},"514.4","Jun",[902,1167,1169],{"x":1168,"y":1136,"style":915},"577.7","Jul",[902,1171,1172],{"x":1052,"y":1136,"style":915},"Aug",[902,1174,1176],{"x":1175,"y":1136,"style":915},"704.3","Sep",[895,1178],{"d":1179,"fill":898,"stroke":930,"style":1180},"M71.1,84.2 L134.4,62.0 L197.8,36.7 L261.1,106.3 L324.4,109.5 L387.7,128.5 L451.1,24.0 L514.4,74.7 L577.7,87.3 L641.0,93.7 L704.3,39.8","stroke-width:2.5px;stroke-linejoin:round;stroke-linecap:round",[1182,1183],"circle",{"cx":1135,"cy":978,"r":1184,"fill":879,"stroke":930,"style":889},"4.0",[1182,1186],{"cx":1140,"cy":1187,"r":1184,"fill":879,"stroke":930,"style":889},"62.0",[1182,1189],{"cx":1144,"cy":1190,"r":1184,"fill":879,"stroke":930,"style":889},"36.7",[1182,1192],{"cx":1148,"cy":1193,"r":1184,"fill":879,"stroke":930,"style":889},"106.3",[1182,1195],{"cx":1152,"cy":1196,"r":1184,"fill":879,"stroke":930,"style":889},"109.5",[1182,1198],{"cx":1156,"cy":1199,"r":1184,"fill":879,"stroke":930,"style":889},"128.5",[1182,1201],{"cx":1160,"cy":1124,"r":1184,"fill":879,"stroke":930,"style":889},[1182,1203],{"cx":1164,"cy":1204,"r":1184,"fill":879,"stroke":930,"style":889},"74.7",[1182,1206],{"cx":1168,"cy":1118,"r":1184,"fill":879,"stroke":930,"style":889},[1182,1208],{"cx":1052,"cy":1209,"r":1184,"fill":879,"stroke":930,"style":889},"93.7",[1182,1211],{"cx":1175,"cy":1212,"r":1184,"fill":879,"stroke":930,"style":889},"39.8",[25,1214,1216],{"id":1215},"step-3-cohort-retention","Step 3 — Cohort retention",[10,1218,1219],{},"Monthly churn rates mix customers of every age. A cohort table separates them: group customers by the month they started and show, for each later month, the share still billed:",[30,1221,1223],{"className":70,"code":1222,"language":72,"meta":35,"style":35},"first = grid.gt(0).idxmax(axis=1)                         # first month with MRR\nactive = grid.gt(0)\nrecords = []\nfor start in months[1:]:\n    members = first.index[first == start]\n    later = [m for m in months if m >= start]\n    shares = [active.loc[members, m].mean() for m in later]\n    records.append([len(members)] + shares + [None] * (len(months) - 1 - len(later)))\ncohorts = pd.DataFrame(records, index=months[1:], columns=[\"customers\"] + [f\"M{i}\" for i in range(len(months) - 1)])\nprint(cohorts.iloc[:4, :6].round(2).to_string())\n",[37,1224,1225,1254,1267,1276,1293,1309,1339,1358,1407,1483],{"__ignoreMap":35},[40,1226,1227,1230,1232,1235,1237,1240,1243,1245,1247,1250],{"class":42,"line":43},[40,1228,1229],{"class":83},"first ",[40,1231,106],{"class":79},[40,1233,1234],{"class":83}," grid.gt(",[40,1236,177],{"class":176},[40,1238,1239],{"class":83},").idxmax(",[40,1241,1242],{"class":129},"axis",[40,1244,106],{"class":79},[40,1246,242],{"class":176},[40,1248,1249],{"class":83},")                         ",[40,1251,1253],{"class":1252},"s-wDw","# first month with MRR\n",[40,1255,1256,1259,1261,1263,1265],{"class":42,"line":93},[40,1257,1258],{"class":83},"active ",[40,1260,106],{"class":79},[40,1262,1234],{"class":83},[40,1264,177],{"class":176},[40,1266,115],{"class":83},[40,1268,1269,1272,1274],{"class":42,"line":100},[40,1270,1271],{"class":83},"records ",[40,1273,106],{"class":79},[40,1275,316],{"class":83},[40,1277,1278,1280,1283,1285,1288,1290],{"class":42,"line":118},[40,1279,321],{"class":79},[40,1281,1282],{"class":83}," start ",[40,1284,327],{"class":79},[40,1286,1287],{"class":83}," months[",[40,1289,242],{"class":176},[40,1291,1292],{"class":83},":]:\n",[40,1294,1295,1298,1300,1303,1306],{"class":42,"line":182},[40,1296,1297],{"class":83},"    members ",[40,1299,106],{"class":79},[40,1301,1302],{"class":83}," first.index[first ",[40,1304,1305],{"class":79},"==",[40,1307,1308],{"class":83}," start]\n",[40,1310,1311,1314,1316,1319,1321,1324,1326,1329,1332,1334,1337],{"class":42,"line":196},[40,1312,1313],{"class":83},"    later ",[40,1315,106],{"class":79},[40,1317,1318],{"class":83}," [m ",[40,1320,321],{"class":79},[40,1322,1323],{"class":83}," m ",[40,1325,327],{"class":79},[40,1327,1328],{"class":83}," months ",[40,1330,1331],{"class":79},"if",[40,1333,1323],{"class":83},[40,1335,1336],{"class":79},">=",[40,1338,1308],{"class":83},[40,1340,1341,1344,1346,1349,1351,1353,1355],{"class":42,"line":388},[40,1342,1343],{"class":83},"    shares ",[40,1345,106],{"class":79},[40,1347,1348],{"class":83}," [active.loc[members, m].mean() ",[40,1350,321],{"class":79},[40,1352,1323],{"class":83},[40,1354,327],{"class":79},[40,1356,1357],{"class":83}," later]\n",[40,1359,1360,1363,1365,1368,1370,1373,1375,1377,1380,1382,1385,1388,1390,1393,1395,1398,1401,1404],{"class":42,"line":402},[40,1361,1362],{"class":83},"    records.append([",[40,1364,215],{"class":176},[40,1366,1367],{"class":83},"(members)] ",[40,1369,645],{"class":79},[40,1371,1372],{"class":83}," shares ",[40,1374,645],{"class":79},[40,1376,771],{"class":83},[40,1378,1379],{"class":176},"None",[40,1381,601],{"class":83},[40,1383,1384],{"class":79},"*",[40,1386,1387],{"class":83}," (",[40,1389,215],{"class":176},[40,1391,1392],{"class":83},"(months) ",[40,1394,239],{"class":79},[40,1396,1397],{"class":176}," 1",[40,1399,1400],{"class":79}," -",[40,1402,1403],{"class":176}," len",[40,1405,1406],{"class":83},"(later)))\n",[40,1408,1409,1412,1414,1417,1419,1421,1423,1425,1428,1430,1432,1435,1438,1440,1442,1444,1446,1449,1451,1454,1456,1458,1461,1464,1466,1469,1472,1474,1476,1478,1480],{"class":42,"line":427},[40,1410,1411],{"class":83},"cohorts ",[40,1413,106],{"class":79},[40,1415,1416],{"class":83}," pd.DataFrame(records, ",[40,1418,130],{"class":129},[40,1420,106],{"class":79},[40,1422,236],{"class":83},[40,1424,242],{"class":176},[40,1426,1427],{"class":83},":], ",[40,1429,141],{"class":129},[40,1431,106],{"class":79},[40,1433,1434],{"class":83},"[",[40,1436,1437],{"class":50},"\"customers\"",[40,1439,601],{"class":83},[40,1441,645],{"class":79},[40,1443,771],{"class":83},[40,1445,226],{"class":79},[40,1447,1448],{"class":50},"\"M",[40,1450,233],{"class":232},[40,1452,1453],{"class":83},"i",[40,1455,262],{"class":232},[40,1457,285],{"class":50},[40,1459,1460],{"class":79}," for",[40,1462,1463],{"class":83}," i ",[40,1465,327],{"class":79},[40,1467,1468],{"class":176}," range",[40,1470,1471],{"class":83},"(",[40,1473,215],{"class":176},[40,1475,1392],{"class":83},[40,1477,239],{"class":79},[40,1479,1397],{"class":176},[40,1481,1482],{"class":83},")])\n",[40,1484,1485,1487,1490,1492,1495,1497,1500,1502],{"class":42,"line":448},[40,1486,199],{"class":176},[40,1488,1489],{"class":83},"(cohorts.iloc[:",[40,1491,876],{"class":176},[40,1493,1494],{"class":83},", :",[40,1496,1128],{"class":176},[40,1498,1499],{"class":83},"].round(",[40,1501,1115],{"class":176},[40,1503,839],{"class":83},[10,1505,1506],{},"Each row is a cohort of new customers; column M0 is their first month (always 100%), M1 the next, and so on, so the table forms a triangle — recent cohorts have fewer months of history. The opening month is left out, because customers billed in October 2025 include everyone who joined before the data starts and are not a true cohort. In this data the oldest cohort, November 2025, still has 63% of its customers after ten months, while the April and May 2026 cohorts had already fallen to 83% and 79% by their third month — earlier and steeper losses than the winter cohorts showed at the same age. Reading down a column compares cohorts at the same age: if recent cohorts retain worse at M3 than older ones did, something changed in who is being signed up or how they are onboarded.",[855,1508,864,1514,864,1517,864,1520,864,1525,864,1529,864,1533,864,1539,864,1543,864,1547,864,1551,864,1555,864,1559,864,1563,864,1566,864,1570,864,1574,864,1578,864,1581,864,1585,864,1587,864,1591,864,1593,864,1596,864,1598,864,1601,864,1603,864,1606,864,1608,864,1611,864,1613,864,1616,864,1619,864,1622,864,1624,864,1628,864,1630,864,1633,864,1635,864,1639,864,1641,864,1644,864,1646,864,1648,864,1650,864,1653,864,1656,864,1658,864,1660,864,1663,864,1665,864,1668,864,1670,864,1672,864,1674,864,1677,864,1679,864,1682,864,1684,864,1687,864,1690,864,1693,864,1695,864,1698,864,1700,864,1702,864,1704,864,1706,864,1708,864,1710,864,1712,864,1715,864,1717,864,1719,864,1722,864,1726,864,1728,864,1731,864,1733,864,1736,864,1738,864,1740,864,1742,864,1745,864,1747,864,1749,864,1752,864,1755,864,1757,864,1760,864,1762,864,1765,864,1767,864,1769,864,1771,864,1773],{"viewBox":1509,"role":858,"ariaLabelledBy":1510,"xmlns":862,"style":1513},"149 0 462 224",[1511,1512],"ch-cohort-t","ch-cohort-d","width:100%;max-width:462px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[866,1515,1516],{"id":1511},"Reading the cohort table",[870,1518,1519],{"id":1512},"Each row is the customers who started in one month. M0 is always 100 percent, and each later column shows the share still billed that many months later. Recent cohorts have fewer columns, so the table is a triangle. The colour scale makes weaker retention stand out.",[874,1521],{"x":1522,"y":177,"width":1523,"height":1524,"fill":879},"149","462","224",[874,1526],{"x":1527,"y":883,"width":1528,"height":1124,"rx":177,"fill":887,"stroke":888,"style":1102},"165.3","32.0",[874,1530],{"x":1531,"y":883,"width":1532,"height":1124,"rx":177,"fill":887,"stroke":888,"style":1102},"197.3","86.2",[902,1534,1538],{"x":1535,"y":1536,"style":1537},"240.4","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[874,1540],{"x":1541,"y":883,"width":1542,"height":1124,"rx":177,"fill":887,"stroke":888,"style":1102},"283.5","91.8",[902,1544,1546],{"x":1545,"y":1536,"style":1537},"329.4","B",[874,1548],{"x":1549,"y":883,"width":1550,"height":1124,"rx":177,"fill":887,"stroke":888,"style":1102},"375.3","59.2",[902,1552,1554],{"x":1553,"y":1536,"style":1537},"404.9","C",[874,1556],{"x":1557,"y":883,"width":1558,"height":1124,"rx":177,"fill":887,"stroke":888,"style":1102},"434.5","53.5",[902,1560,1562],{"x":1561,"y":1536,"style":1537},"461.3","D",[874,1564],{"x":1565,"y":883,"width":1558,"height":1124,"rx":177,"fill":887,"stroke":888,"style":1102},"488.0",[902,1567,1569],{"x":1568,"y":1536,"style":1537},"514.7","E",[874,1571],{"x":1572,"y":883,"width":1573,"height":1124,"rx":177,"fill":887,"stroke":888,"style":1102},"541.5","53.2",[902,1575,1577],{"x":1576,"y":1536,"style":1537},"568.1","F",[874,1579],{"x":1527,"y":1580,"width":1528,"height":1127,"rx":177,"fill":887,"stroke":888,"style":1102},"40.0",[902,1582,242],{"x":1583,"y":1584,"style":1537},"181.3","59.0",[874,1586],{"x":1531,"y":1580,"width":1532,"height":1127,"rx":177,"fill":1059,"stroke":930,"style":1132},[902,1588,1590],{"x":1535,"y":1584,"style":1589},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Cohort",[874,1592],{"x":1541,"y":1580,"width":1542,"height":1127,"rx":177,"fill":1059,"stroke":930,"style":1132},[902,1594,1595],{"x":1545,"y":1584,"style":1589},"Customers",[874,1597],{"x":1549,"y":1580,"width":1550,"height":1127,"rx":177,"fill":1059,"stroke":930,"style":1132},[902,1599,1600],{"x":1553,"y":1584,"style":1589},"M0",[874,1602],{"x":1557,"y":1580,"width":1558,"height":1127,"rx":177,"fill":1059,"stroke":930,"style":1132},[902,1604,1605],{"x":1561,"y":1584,"style":1589},"M1",[874,1607],{"x":1565,"y":1580,"width":1558,"height":1127,"rx":177,"fill":1059,"stroke":930,"style":1132},[902,1609,1610],{"x":1568,"y":1584,"style":1589},"M2",[874,1612],{"x":1572,"y":1580,"width":1573,"height":1127,"rx":177,"fill":1059,"stroke":930,"style":1132},[902,1614,1615],{"x":1576,"y":1584,"style":1589},"M3",[874,1617],{"x":1527,"y":1618,"width":1528,"height":1127,"rx":177,"fill":887,"stroke":888,"style":1102},"68.0",[902,1620,1115],{"x":1583,"y":1621,"style":1537},"87.0",[874,1623],{"x":1531,"y":1618,"width":1532,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1625,1627],{"x":1535,"y":1621,"style":1626},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Nov 2025",[874,1629],{"x":1541,"y":1618,"width":1542,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1631,1632],{"x":1545,"y":1621,"style":1626},"19",[874,1634],{"x":1549,"y":1618,"width":1550,"height":1127,"rx":177,"fill":939,"stroke":940,"style":1132},[902,1636,1638],{"x":1553,"y":1621,"style":1637},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","100%",[874,1640],{"x":1557,"y":1618,"width":1558,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1642,1643],{"x":1561,"y":1621,"style":1626},"95%",[874,1645],{"x":1565,"y":1618,"width":1558,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1647,1643],{"x":1568,"y":1621,"style":1626},[874,1649],{"x":1572,"y":1618,"width":1573,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1651,1652],{"x":1576,"y":1621,"style":1626},"89%",[874,1654],{"x":1527,"y":1655,"width":1528,"height":1127,"rx":177,"fill":887,"stroke":888,"style":1102},"96.0",[902,1657,831],{"x":1583,"y":927,"style":1537},[874,1659],{"x":1531,"y":1655,"width":1532,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1661,1662],{"x":1535,"y":927,"style":1626},"Apr 2026",[874,1664],{"x":1541,"y":1655,"width":1542,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1666,1667],{"x":1545,"y":927,"style":1626},"30",[874,1669],{"x":1549,"y":1655,"width":1550,"height":1127,"rx":177,"fill":939,"stroke":940,"style":1132},[902,1671,1638],{"x":1553,"y":927,"style":1637},[874,1673],{"x":1557,"y":1655,"width":1558,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1675,1676],{"x":1561,"y":927,"style":1626},"97%",[874,1678],{"x":1565,"y":1655,"width":1558,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1680,1681],{"x":1568,"y":927,"style":1626},"90%",[874,1683],{"x":1572,"y":1655,"width":1573,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1685,1686],{"x":1576,"y":927,"style":1626},"83%",[874,1688],{"x":1527,"y":1689,"width":1528,"height":1127,"rx":177,"fill":887,"stroke":888,"style":1102},"124.0",[902,1691,876],{"x":1583,"y":1692,"style":1537},"143.0",[874,1694],{"x":1531,"y":1689,"width":1532,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1696,1697],{"x":1535,"y":1692,"style":1626},"May 2026",[874,1699],{"x":1541,"y":1689,"width":1542,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1701,1632],{"x":1545,"y":1692,"style":1626},[874,1703],{"x":1549,"y":1689,"width":1550,"height":1127,"rx":177,"fill":939,"stroke":940,"style":1132},[902,1705,1638],{"x":1553,"y":1692,"style":1637},[874,1707],{"x":1557,"y":1689,"width":1558,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1709,1652],{"x":1561,"y":1692,"style":1626},[874,1711],{"x":1565,"y":1689,"width":1558,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1713,1714],{"x":1568,"y":1692,"style":1626},"79%",[874,1716],{"x":1572,"y":1689,"width":1573,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1718,1714],{"x":1576,"y":1692,"style":1626},[874,1720],{"x":1527,"y":1721,"width":1528,"height":1127,"rx":177,"fill":887,"stroke":888,"style":1102},"152.0",[902,1723,1725],{"x":1583,"y":1724,"style":1537},"171.0","5",[874,1727],{"x":1531,"y":1721,"width":1532,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1729,1730],{"x":1535,"y":1724,"style":1626},"Aug 2026",[874,1732],{"x":1541,"y":1721,"width":1542,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1734,1735],{"x":1545,"y":1724,"style":1626},"18",[874,1737],{"x":1549,"y":1721,"width":1550,"height":1127,"rx":177,"fill":939,"stroke":940,"style":1132},[902,1739,1638],{"x":1553,"y":1724,"style":1637},[874,1741],{"x":1557,"y":1721,"width":1558,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1743,1744],{"x":1561,"y":1724,"style":1626},"94%",[874,1746],{"x":1565,"y":1721,"width":1558,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[874,1748],{"x":1572,"y":1721,"width":1573,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[874,1750],{"x":1527,"y":1751,"width":1528,"height":1127,"rx":177,"fill":887,"stroke":888,"style":1102},"180.0",[902,1753,1128],{"x":1583,"y":1754,"style":1537},"199.0",[874,1756],{"x":1531,"y":1751,"width":1532,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1758,1759],{"x":1535,"y":1754,"style":1626},"Sep 2026",[874,1761],{"x":1541,"y":1751,"width":1542,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[902,1763,1764],{"x":1545,"y":1754,"style":1626},"31",[874,1766],{"x":1549,"y":1751,"width":1550,"height":1127,"rx":177,"fill":939,"stroke":940,"style":1132},[902,1768,1638],{"x":1553,"y":1754,"style":1637},[874,1770],{"x":1557,"y":1751,"width":1558,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[874,1772],{"x":1565,"y":1751,"width":1558,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[874,1774],{"x":1572,"y":1751,"width":1573,"height":1127,"rx":177,"fill":879,"stroke":888,"style":1102},[25,1776,1778],{"id":1777},"step-4-write-the-report","Step 4 — Write the report",[30,1780,1782],{"className":70,"code":1781,"language":72,"meta":35,"style":35},"from openpyxl import load_workbook\nfrom openpyxl.formatting.rule import ColorScaleRule\n\npath = \"churn_report_2026-09.xlsx\"\nout = report[cols].copy()\nout.index = out.index.strftime(\"%b %Y\")\ncoh = cohorts.copy()\ncoh.index = coh.index.strftime(\"%b %Y\")\nwith pd.ExcelWriter(path, engine=\"openpyxl\") as xw:\n    out.to_excel(xw, sheet_name=\"Monthly\", index_label=\"Month\")\n    coh.to_excel(xw, sheet_name=\"Cohorts\", index_label=\"Cohort\")\n\nwb = load_workbook(path)\nws = wb[\"Monthly\"]\nfor col_idx, name in enumerate([\"Month\"] + cols, start=1):\n    fmt = \"0.0%\" if name in (\"logo_churn\", \"nrr\") else \"#,##0\"\n    for cell in list(ws.iter_cols(min_col=col_idx, max_col=col_idx, min_row=2))[0]:\n        cell.number_format = fmt if name != \"Month\" else \"@\"\nws_c = wb[\"Cohorts\"]\nlast_col = ws_c.cell(1, ws_c.max_column).column_letter\nws_c.conditional_formatting.add(f\"C2:{last_col}{ws_c.max_row}\",\n    ColorScaleRule(start_type=\"num\", start_value=0.5, start_color=\"F8696B\", mid_type=\"num\", mid_value=0.8, mid_color=\"FFEB84\",\n                   end_type=\"num\", end_value=1, end_color=\"63BE7B\"))\nfor row in ws_c.iter_rows(min_row=2, min_col=3):\n    for cell in row:\n        cell.number_format = \"0%\"\nwb.save(path)\nlatest = report.iloc[-1]\nprint(f\"{months[-1]:%b %Y}: logo churn {latest['logo_churn']:.1%}, NRR {latest['nrr']:.1%}, \"\n      f\"MRR {latest['mrr_start']:,.0f} -> {latest['mrr_end']:,.0f}\")\n",[37,1783,1784,1797,1809,1813,1823,1833,1853,1863,1881,1905,1930,1953,1957,1967,1981,2015,2049,2094,2120,2133,2148,2175,2239,2270,2298,2309,2318,2324,2341,2411],{"__ignoreMap":35},[40,1785,1786,1789,1792,1794],{"class":42,"line":43},[40,1787,1788],{"class":79},"from",[40,1790,1791],{"class":83}," openpyxl ",[40,1793,80],{"class":79},[40,1795,1796],{"class":83}," load_workbook\n",[40,1798,1799,1801,1804,1806],{"class":42,"line":93},[40,1800,1788],{"class":79},[40,1802,1803],{"class":83}," openpyxl.formatting.rule ",[40,1805,80],{"class":79},[40,1807,1808],{"class":83}," ColorScaleRule\n",[40,1810,1811],{"class":42,"line":100},[40,1812,97],{"emptyLinePlaceholder":96},[40,1814,1815,1818,1820],{"class":42,"line":118},[40,1816,1817],{"class":83},"path ",[40,1819,106],{"class":79},[40,1821,1822],{"class":50}," \"churn_report_2026-09.xlsx\"\n",[40,1824,1825,1828,1830],{"class":42,"line":182},[40,1826,1827],{"class":83},"out ",[40,1829,106],{"class":79},[40,1831,1832],{"class":83}," report[cols].copy()\n",[40,1834,1835,1838,1840,1843,1845,1848,1851],{"class":42,"line":196},[40,1836,1837],{"class":83},"out.index ",[40,1839,106],{"class":79},[40,1841,1842],{"class":83}," out.index.strftime(",[40,1844,285],{"class":50},[40,1846,1847],{"class":232},"%b",[40,1849,1850],{"class":50}," %Y\"",[40,1852,115],{"class":83},[40,1854,1855,1858,1860],{"class":42,"line":388},[40,1856,1857],{"class":83},"coh ",[40,1859,106],{"class":79},[40,1861,1862],{"class":83}," cohorts.copy()\n",[40,1864,1865,1868,1870,1873,1875,1877,1879],{"class":42,"line":402},[40,1866,1867],{"class":83},"coh.index ",[40,1869,106],{"class":79},[40,1871,1872],{"class":83}," coh.index.strftime(",[40,1874,285],{"class":50},[40,1876,1847],{"class":232},[40,1878,1850],{"class":50},[40,1880,115],{"class":83},[40,1882,1883,1886,1889,1892,1894,1897,1900,1902],{"class":42,"line":427},[40,1884,1885],{"class":79},"with",[40,1887,1888],{"class":83}," pd.ExcelWriter(path, ",[40,1890,1891],{"class":129},"engine",[40,1893,106],{"class":79},[40,1895,1896],{"class":50},"\"openpyxl\"",[40,1898,1899],{"class":83},") ",[40,1901,87],{"class":79},[40,1903,1904],{"class":83}," xw:\n",[40,1906,1907,1910,1913,1915,1918,1920,1923,1925,1928],{"class":42,"line":448},[40,1908,1909],{"class":83},"    out.to_excel(xw, ",[40,1911,1912],{"class":129},"sheet_name",[40,1914,106],{"class":79},[40,1916,1917],{"class":50},"\"Monthly\"",[40,1919,138],{"class":83},[40,1921,1922],{"class":129},"index_label",[40,1924,106],{"class":79},[40,1926,1927],{"class":50},"\"Month\"",[40,1929,115],{"class":83},[40,1931,1932,1935,1937,1939,1942,1944,1946,1948,1951],{"class":42,"line":461},[40,1933,1934],{"class":83},"    coh.to_excel(xw, ",[40,1936,1912],{"class":129},[40,1938,106],{"class":79},[40,1940,1941],{"class":50},"\"Cohorts\"",[40,1943,138],{"class":83},[40,1945,1922],{"class":129},[40,1947,106],{"class":79},[40,1949,1950],{"class":50},"\"Cohort\"",[40,1952,115],{"class":83},[40,1954,1955],{"class":42,"line":470},[40,1956,97],{"emptyLinePlaceholder":96},[40,1958,1959,1962,1964],{"class":42,"line":488},[40,1960,1961],{"class":83},"wb ",[40,1963,106],{"class":79},[40,1965,1966],{"class":83}," load_workbook(path)\n",[40,1968,1969,1972,1974,1977,1979],{"class":42,"line":517},[40,1970,1971],{"class":83},"ws ",[40,1973,106],{"class":79},[40,1975,1976],{"class":83}," wb[",[40,1978,1917],{"class":50},[40,1980,622],{"class":83},[40,1982,1983,1985,1988,1990,1993,1996,1998,2000,2002,2005,2008,2010,2012],{"class":42,"line":542},[40,1984,321],{"class":79},[40,1986,1987],{"class":83}," col_idx, name ",[40,1989,327],{"class":79},[40,1991,1992],{"class":176}," enumerate",[40,1994,1995],{"class":83},"([",[40,1997,1927],{"class":50},[40,1999,601],{"class":83},[40,2001,645],{"class":79},[40,2003,2004],{"class":83}," cols, ",[40,2006,2007],{"class":129},"start",[40,2009,106],{"class":79},[40,2011,242],{"class":176},[40,2013,2014],{"class":83},"):\n",[40,2016,2017,2020,2022,2025,2028,2031,2033,2035,2037,2039,2041,2043,2046],{"class":42,"line":562},[40,2018,2019],{"class":83},"    fmt ",[40,2021,106],{"class":79},[40,2023,2024],{"class":50}," \"0.0%\"",[40,2026,2027],{"class":79}," if",[40,2029,2030],{"class":83}," name ",[40,2032,327],{"class":79},[40,2034,1387],{"class":83},[40,2036,598],{"class":50},[40,2038,138],{"class":83},[40,2040,630],{"class":50},[40,2042,1899],{"class":83},[40,2044,2045],{"class":79},"else",[40,2047,2048],{"class":50}," \"#,##0\"\n",[40,2050,2051,2054,2057,2059,2061,2064,2067,2069,2072,2075,2077,2079,2082,2084,2086,2089,2091],{"class":42,"line":571},[40,2052,2053],{"class":79},"    for",[40,2055,2056],{"class":83}," cell ",[40,2058,327],{"class":79},[40,2060,190],{"class":176},[40,2062,2063],{"class":83},"(ws.iter_cols(",[40,2065,2066],{"class":129},"min_col",[40,2068,106],{"class":79},[40,2070,2071],{"class":83},"col_idx, ",[40,2073,2074],{"class":129},"max_col",[40,2076,106],{"class":79},[40,2078,2071],{"class":83},[40,2080,2081],{"class":129},"min_row",[40,2083,106],{"class":79},[40,2085,1115],{"class":176},[40,2087,2088],{"class":83},"))[",[40,2090,177],{"class":176},[40,2092,2093],{"class":83},"]:\n",[40,2095,2096,2099,2101,2104,2106,2108,2111,2114,2117],{"class":42,"line":577},[40,2097,2098],{"class":83},"        cell.number_format ",[40,2100,106],{"class":79},[40,2102,2103],{"class":83}," fmt ",[40,2105,1331],{"class":79},[40,2107,2030],{"class":83},[40,2109,2110],{"class":79},"!=",[40,2112,2113],{"class":50}," \"Month\"",[40,2115,2116],{"class":79}," else",[40,2118,2119],{"class":50}," \"@\"\n",[40,2121,2122,2125,2127,2129,2131],{"class":42,"line":592},[40,2123,2124],{"class":83},"ws_c ",[40,2126,106],{"class":79},[40,2128,1976],{"class":83},[40,2130,1941],{"class":50},[40,2132,622],{"class":83},[40,2134,2135,2138,2140,2143,2145],{"class":42,"line":625},[40,2136,2137],{"class":83},"last_col ",[40,2139,106],{"class":79},[40,2141,2142],{"class":83}," ws_c.cell(",[40,2144,242],{"class":176},[40,2146,2147],{"class":83},", ws_c.max_column).column_letter\n",[40,2149,2150,2153,2155,2158,2160,2163,2166,2169,2171,2173],{"class":42,"line":682},[40,2151,2152],{"class":83},"ws_c.conditional_formatting.add(",[40,2154,226],{"class":79},[40,2156,2157],{"class":50},"\"C2:",[40,2159,233],{"class":232},[40,2161,2162],{"class":83},"last_col",[40,2164,2165],{"class":232},"}{",[40,2167,2168],{"class":83},"ws_c.max_row",[40,2170,262],{"class":232},[40,2172,285],{"class":50},[40,2174,797],{"class":83},[40,2176,2177,2180,2183,2185,2188,2190,2193,2195,2198,2200,2203,2205,2208,2210,2213,2215,2217,2219,2222,2224,2227,2229,2232,2234,2237],{"class":42,"line":687},[40,2178,2179],{"class":83},"    ColorScaleRule(",[40,2181,2182],{"class":129},"start_type",[40,2184,106],{"class":79},[40,2186,2187],{"class":50},"\"num\"",[40,2189,138],{"class":83},[40,2191,2192],{"class":129},"start_value",[40,2194,106],{"class":79},[40,2196,2197],{"class":176},"0.5",[40,2199,138],{"class":83},[40,2201,2202],{"class":129},"start_color",[40,2204,106],{"class":79},[40,2206,2207],{"class":50},"\"F8696B\"",[40,2209,138],{"class":83},[40,2211,2212],{"class":129},"mid_type",[40,2214,106],{"class":79},[40,2216,2187],{"class":50},[40,2218,138],{"class":83},[40,2220,2221],{"class":129},"mid_value",[40,2223,106],{"class":79},[40,2225,2226],{"class":176},"0.8",[40,2228,138],{"class":83},[40,2230,2231],{"class":129},"mid_color",[40,2233,106],{"class":79},[40,2235,2236],{"class":50},"\"FFEB84\"",[40,2238,797],{"class":83},[40,2240,2241,2244,2246,2248,2250,2253,2255,2257,2259,2262,2264,2267],{"class":42,"line":734},[40,2242,2243],{"class":129},"                   end_type",[40,2245,106],{"class":79},[40,2247,2187],{"class":50},[40,2249,138],{"class":83},[40,2251,2252],{"class":129},"end_value",[40,2254,106],{"class":79},[40,2256,242],{"class":176},[40,2258,138],{"class":83},[40,2260,2261],{"class":129},"end_color",[40,2263,106],{"class":79},[40,2265,2266],{"class":50},"\"63BE7B\"",[40,2268,2269],{"class":83},"))\n",[40,2271,2272,2274,2277,2279,2282,2284,2286,2288,2290,2292,2294,2296],{"class":42,"line":763},[40,2273,321],{"class":79},[40,2275,2276],{"class":83}," row ",[40,2278,327],{"class":79},[40,2280,2281],{"class":83}," ws_c.iter_rows(",[40,2283,2081],{"class":129},[40,2285,106],{"class":79},[40,2287,1115],{"class":176},[40,2289,138],{"class":83},[40,2291,2066],{"class":129},[40,2293,106],{"class":79},[40,2295,831],{"class":176},[40,2297,2014],{"class":83},[40,2299,2300,2302,2304,2306],{"class":42,"line":800},[40,2301,2053],{"class":79},[40,2303,2056],{"class":83},[40,2305,327],{"class":79},[40,2307,2308],{"class":83}," row:\n",[40,2310,2311,2313,2315],{"class":42,"line":823},[40,2312,2098],{"class":83},[40,2314,106],{"class":79},[40,2316,2317],{"class":50}," \"0%\"\n",[40,2319,2321],{"class":42,"line":2320},27,[40,2322,2323],{"class":83},"wb.save(path)\n",[40,2325,2327,2330,2332,2335,2337,2339],{"class":42,"line":2326},28,[40,2328,2329],{"class":83},"latest ",[40,2331,106],{"class":79},[40,2333,2334],{"class":83}," report.iloc[",[40,2336,239],{"class":79},[40,2338,242],{"class":176},[40,2340,622],{"class":83},[40,2342,2344,2346,2348,2350,2352,2354,2356,2358,2360,2362,2364,2366,2368,2370,2372,2374,2377,2379,2382,2385,2387,2390,2392,2395,2397,2399,2402,2404,2406,2408],{"class":42,"line":2343},29,[40,2345,199],{"class":176},[40,2347,1471],{"class":83},[40,2349,226],{"class":79},[40,2351,285],{"class":50},[40,2353,233],{"class":232},[40,2355,236],{"class":83},[40,2357,239],{"class":79},[40,2359,242],{"class":176},[40,2361,245],{"class":83},[40,2363,248],{"class":79},[40,2365,251],{"class":83},[40,2367,254],{"class":79},[40,2369,251],{"class":83},[40,2371,259],{"class":79},[40,2373,262],{"class":232},[40,2375,2376],{"class":50},": logo churn ",[40,2378,233],{"class":232},[40,2380,2381],{"class":83},"latest[",[40,2383,2384],{"class":50},"'logo_churn'",[40,2386,245],{"class":83},[40,2388,2389],{"class":79},":.1%",[40,2391,262],{"class":232},[40,2393,2394],{"class":50},", NRR ",[40,2396,233],{"class":232},[40,2398,2381],{"class":83},[40,2400,2401],{"class":50},"'nrr'",[40,2403,245],{"class":83},[40,2405,2389],{"class":79},[40,2407,262],{"class":232},[40,2409,2410],{"class":50},", \"\n",[40,2412,2414,2417,2420,2422,2424,2427,2429,2431,2433,2436,2438,2440,2443,2445,2447,2449,2451],{"class":42,"line":2413},30,[40,2415,2416],{"class":79},"      f",[40,2418,2419],{"class":50},"\"MRR ",[40,2421,233],{"class":232},[40,2423,2381],{"class":83},[40,2425,2426],{"class":50},"'mrr_start'",[40,2428,245],{"class":83},[40,2430,280],{"class":79},[40,2432,262],{"class":232},[40,2434,2435],{"class":50}," -> ",[40,2437,233],{"class":232},[40,2439,2381],{"class":83},[40,2441,2442],{"class":50},"'mrr_end'",[40,2444,245],{"class":83},[40,2446,280],{"class":79},[40,2448,262],{"class":232},[40,2450,285],{"class":50},[40,2452,115],{"class":83},[10,2454,290,2455,2458,2459,2462,2463,2467],{},[14,2456,2457],{},"Monthly"," sheet holds movements, churn and NRR; the ",[14,2460,2461],{},"Cohorts"," sheet holds the retention triangle with a red-to-green colour scale anchored at fixed values (50%, 80%, 100%), so colours mean the same thing every month rather than being relative to whatever the table contains. Blank cells in the triangle are left uncoloured. For a board pack, a chart of the MRR bridge — opening, the four movements, closing — is the natural companion; ",[19,2464,2466],{"href":2465},"\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.",[10,2469,2470],{},"A note on definitions before the report circulates: churn can be measured per customer or per subscription, monthly or annualised, and \"active\" can mean billed, paid or not yet cancelled. None of these is wrong, but a report that switches definitions between months, or differs from the finance team's numbers without saying why, loses trust quickly. State the definitions on a notes sheet in the workbook — billed MRR per customer, monthly logo churn, NRR excluding new customers — so readers compare like with like.",[25,2472,2474],{"id":2473},"common-pitfalls","Common pitfalls",[2476,2477,2478,2485,2491,2497,2503],"ul",{},[2479,2480,2481,2484],"li",{},[846,2482,2483],{},"Counting plan changes as churn."," Sum MRR per customer before comparing months.",[2479,2486,2487,2490],{},[846,2488,2489],{},"A bridge that does not add up."," Assert opening plus movements equals closing every month.",[2479,2492,2493,2496],{},[846,2494,2495],{},"Treating the opening month as a cohort."," It contains every customer who joined earlier.",[2479,2498,2499,2502],{},[846,2500,2501],{},"Mixing logo and revenue churn."," Report both; they tell different stories.",[2479,2504,2505,2508],{},[846,2506,2507],{},"Relative colour scales."," Anchor colour thresholds to fixed values.",[25,2510,2512],{"id":2511},"conclusion","Conclusion",[10,2514,2515],{},"Pivot billed MRR into a customer-by-month grid and compare each month with the previous one to classify new, churned, expanding and contracting customers. From those, report logo churn, an MRR bridge that is asserted to add up, and net revenue retention, and group customers by start month into a cohort retention triangle. Writing both to Excel with consistent number formats and a fixed colour scale gives a churn report that reads the same way every month.",[25,2517,2519],{"id":2518},"frequently-asked-questions","Frequently asked questions",[10,2521,2522,2525],{},[846,2523,2524],{},"How do I calculate customer churn rate in pandas?","\nPivot billing data into customers by month, count customers billed last month but not this month, and divide by the number billed last month.",[10,2527,2528,2531],{},[846,2529,2530],{},"What is an MRR bridge?","\nA reconciliation from opening to closing monthly recurring revenue through new, expansion, contraction and churned MRR; the movements must add up exactly.",[10,2533,2534,2537],{},[846,2535,2536],{},"What is net revenue retention?","\nRevenue from customers present at the start of a period, after their expansions, contractions and cancellations, divided by their starting revenue; above 100 percent means existing customers grew on balance.",[10,2539,2540,2543],{},[846,2541,2542],{},"How do I build a cohort retention table in pandas?","\nFind each customer's first billed month, group customers by it, and for each later month take the share of the group still billed.",[25,2545,2547],{"id":2546},"related","Related",[2476,2549,2550,2556,2563,2570],{},[2479,2551,2552,2553,2555],{},"Up one level: ",[19,2554,22],{"href":21}," — finance reports from Python.",[2479,2557,2558,2562],{},[19,2559,2561],{"href":2560},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets\u002F","Build a Monthly KPI Scorecard with Targets"," — putting churn alongside other KPIs.",[2479,2564,2565,2569],{},[19,2566,2568],{"href":2567},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-customer-lifetime-value-from-order-history\u002F","Calculate Customer Lifetime Value from Order History"," — the value side of retention.",[2479,2571,2572,2576],{},[19,2573,2575],{"href":2574},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-deferred-revenue-schedule\u002F","Build a Deferred Revenue Schedule"," — subscription revenue recognition.",[2578,2579,2580],"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":35,"searchDepth":93,"depth":93,"links":2582},[2583,2584,2585,2586,2587,2588,2589,2590,2591],{"id":27,"depth":93,"text":28},{"id":63,"depth":93,"text":64},{"id":297,"depth":93,"text":298},{"id":1215,"depth":93,"text":1216},{"id":1777,"depth":93,"text":1778},{"id":2473,"depth":93,"text":2474},{"id":2511,"depth":93,"text":2512},{"id":2518,"depth":93,"text":2519},{"id":2546,"depth":93,"text":2547},"2026-10-02","Turn monthly subscription data into a churn report with pandas: customers gained and lost each month, logo churn rate, an MRR bridge from new, expansion, contraction and churned revenue, net revenue retention, and a cohort retention table written to Excel with a colour scale, with checks that every month's bridge adds up.","md",[2596,2598,2600,2602],{"q":2524,"a":2597},"Pivot billing data into customers by month, count customers billed last month but not this month, and divide by the number billed last month.",{"q":2530,"a":2599},"A reconciliation from opening to closing monthly recurring revenue through new, expansion, contraction and churned MRR; the movements must add up exactly.",{"q":2536,"a":2601},"Revenue from customers present at the start of a period, after their expansions, contractions and cancellations, divided by their starting revenue; above 100 percent means existing customers grew on balance.",{"q":2542,"a":2603},"Find each customer's first billed month, group customers by it, and for each later month take the share of the group still billed.","customer churn report from subscription data",null,{"breadcrumb":2607},[2608,2610,2613],{"name":2609,"item":614},"Home",{"name":2611,"item":2612},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2614,"item":21},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data",{"title":2617,"description":2618},"Build a Customer Churn Report from Subscription Data in Python","Churn reporting with pandas and openpyxl: monthly customer movements, logo churn, an MRR bridge with expansion and contraction, net revenue retention, cohort retention heatmaps, and bridge checks.","build-a-customer-churn-report-from-subscription-data","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data\u002Findex",[2622],"pivots-and-summaries","how-to","gyrM4_X0hPZnNkYhbZxijyEg8q2hRggJZ_SMGMRUTO8",[2626,2630],{"title":2627,"path":2628,"stem":2629,"children":-1},"Build a Credit Control Dunning Letter Run","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run\u002Findex",{"title":2631,"path":2632,"stem":2633,"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",[2635,2637],{"path":2636,"title":2614},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2638,"title":2611},"\u002Fautomating-reporting-workflows",[],[2641,2645,2648,2651,2654,2657],{"path":2642,"title":2643,"kind":2644},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data","Creating Pivot Tables from Excel Data with Pandas","guide",{"path":2646,"title":2647,"kind":2623},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fadd-a-native-excel-pivot-table-with-python","Add a Native Excel Pivot Table with Python",{"path":2649,"title":2650,"kind":2623},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fadd-subtotals-and-grand-totals-to-a-pandas-pivot-table","Add Subtotals and Grand Totals to a pandas Pivot Table",{"path":2652,"title":2653,"kind":2623},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbin-values-into-ranges-with-pandas-cut","Bin Values into Ranges Like Excel FREQUENCY with pd.cut",{"path":2655,"title":2656,"kind":2623},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-cohort-retention-table-from-excel-data","Build a Cohort Retention Table from Excel Data",{"path":2658,"title":2659,"kind":2623},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-crosstab-from-excel-data-with-pandas","Build a Crosstab from Excel Data with pandas",1790972121309]