[{"data":1,"prerenderedAt":2254},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frunning-totals-and-year-over-year-growth-in-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frunning-totals-and-year-over-year-growth-in-pandas":2246},{"id":4,"title":5,"body":6,"dateModified":2219,"datePublished":2219,"description":2220,"extension":2221,"faq":2222,"meta":2231,"navigation":212,"path":2239,"seo":2240,"slug":2242,"stem":2243,"type":2244,"__hash__":2245},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frunning-totals-and-year-over-year-growth-in-pandas\u002Findex.md","Running Totals and Year-Over-Year Growth in pandas",{"type":7,"value":8,"toc":2205},"minimark",[9,19,150,155,184,429,433,539,550,553,563,613,617,694,700,803,808,815,870,873,1013,1017,1124,1133,1137,1140,1333,1336,1340,1343,1490,1493,1496,1560,1572,1576,1579,1768,1775,1779,1882,1886,1984,1991,1994,2096,2102,2106,2126,2130,2141,2147,2153,2159,2163,2201],[10,11,12,13,18],"p",{},"Running totals and period comparisons are where a spreadsheet's relative references shine and then\nbetray it: the formula is easy to write and silently wrong the moment a row is inserted, a month is\nmissing or the sheet is sorted differently. pandas expresses the same ideas as operations on an\nordered column, which makes the ordering assumption explicit. This guide is part of\n",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","Excel Formula Equivalents in pandas",".",[20,21,29,30,29,34,29,38,29,45,29,54,29,61,29,65,29,69,29,74,29,79,29,83,29,86,29,91,29,96,29,99,29,102,29,105,29,109,29,112,29,115,29,120,29,125,29,128,29,131,29,134,29,138,29,141,29,144],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 760 296","img",[25,26],"run-map-t","run-map-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[31,32,33],"title",{"id":25},"Relative-reference formulas and their column operations",[35,36,37],"desc",{"id":26},"A running total becomes cumsum, a reference to the row above becomes shift, period growth becomes pct_change, and a dragged average over a relative range becomes rolling with an explicit window.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","760","296","#ffffff",[39,46],{"x":47,"y":48,"width":49,"height":50,"rx":51,"fill":52,"stroke":53},"18","30","724","34","8","#e7ebef","var(--line,#cdd5e6)",[55,56,60],"text",{"x":57,"y":58,"style":59},"126.6","52","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","Excel",[55,62,64],{"x":63,"y":58,"style":59},"361.9","pandas",[55,66,68],{"x":67,"y":58,"style":59},"615.3000000000001","The assumption",[39,70],{"x":47,"y":71,"width":49,"height":50,"rx":51,"fill":72,"stroke":73},"67","#d9f4f1","var(--teal,#0f9488)",[55,75,78],{"x":57,"y":76,"style":77},"88.0","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","SUM($C$2:C2)",[55,80,82],{"x":63,"y":76,"style":81},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","cumsum()",[55,84,85],{"x":67,"y":76,"style":81},"row order is the date order",[39,87],{"x":47,"y":88,"width":49,"height":50,"rx":51,"fill":89,"stroke":90},"107","#f0f4ff","var(--brand,#5b5cf0)",[55,92,95],{"x":57,"y":93,"style":94},"128.0","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","=C3-C2",[55,97,98],{"x":63,"y":93,"style":81},"shift(1)",[55,100,101],{"x":67,"y":93,"style":81},"no gaps in the series",[39,103],{"x":47,"y":104,"width":49,"height":50,"rx":51,"fill":89,"stroke":90},"147",[55,106,108],{"x":57,"y":107,"style":94},"168.0","=(C3-C2)\u002FC2",[55,110,111],{"x":63,"y":107,"style":81},"pct_change()",[55,113,114],{"x":67,"y":107,"style":81},"prior value is not zero",[39,116],{"x":47,"y":117,"width":49,"height":50,"rx":51,"fill":118,"stroke":119},"187","#fdefd8","var(--gold,#b4740a)",[55,121,124],{"x":57,"y":122,"style":123},"208.0","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","AVERAGE(C1:C3)",[55,126,127],{"x":63,"y":122,"style":81},"rolling(3).mean()",[55,129,130],{"x":67,"y":122,"style":81},"window fullness at the start",[39,132],{"x":47,"y":133,"width":49,"height":50,"rx":51,"fill":72,"stroke":73},"227",[55,135,137],{"x":57,"y":136,"style":77},"248.0","YTD with an IF",[55,139,140],{"x":63,"y":136,"style":81},"groupby(year).cumsum()",[55,142,143],{"x":67,"y":136,"style":81},"the key defines the reset",[55,145,149],{"x":146,"y":147,"style":148},"380.0","284","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","each row carries an assumption the formula never states",[151,152,154],"h2",{"id":153},"prerequisites","Prerequisites",[156,157,162],"pre",{"className":158,"code":159,"language":160,"meta":161,"style":161},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[163,164,165],"code",{"__ignoreMap":161},[166,167,170,174,178,181],"span",{"class":168,"line":169},"line",1,[166,171,173],{"class":172},"sMTad","pip",[166,175,177],{"class":176},"srMev"," install",[166,179,180],{"class":176}," pandas",[166,182,183],{"class":176}," openpyxl\n",[156,185,189],{"className":186,"code":187,"language":188,"meta":161,"style":161},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nmonthly = pd.DataFrame({\n    \"Month\": pd.period_range(\"2025-01\", periods=15, freq=\"M\").astype(str),\n    \"Region\": [\"North\"] * 15,\n    \"Revenue\": [\n        31200.0, 28400.0, 34100.0, 29800.0, 36500.0, 41200.0,\n        38900.0, 33400.0, 45100.0, 47800.0, 51200.0, 58900.0,\n        35600.0, 32900.0, 39400.0,\n    ],\n})\nmonthly[\"Month\"] = pd.PeriodIndex(monthly[\"Month\"], freq=\"M\")\n","python",[163,190,191,207,214,226,270,294,303,336,369,387,393,399],{"__ignoreMap":161},[166,192,193,197,201,204],{"class":168,"line":169},[166,194,196],{"class":195},"s-kum","import",[166,198,200],{"class":199},"skGVy"," pandas ",[166,202,203],{"class":195},"as",[166,205,206],{"class":199}," pd\n",[166,208,210],{"class":168,"line":209},2,[166,211,213],{"emptyLinePlaceholder":212},true,"\n",[166,215,217,220,223],{"class":168,"line":216},3,[166,218,219],{"class":199},"monthly ",[166,221,222],{"class":195},"=",[166,224,225],{"class":199}," pd.DataFrame({\n",[166,227,229,232,235,238,241,245,247,251,253,256,258,261,264,267],{"class":168,"line":228},4,[166,230,231],{"class":176},"    \"Month\"",[166,233,234],{"class":199},": pd.period_range(",[166,236,237],{"class":176},"\"2025-01\"",[166,239,240],{"class":199},", ",[166,242,244],{"class":243},"sa561","periods",[166,246,222],{"class":195},[166,248,250],{"class":249},"sP0c6","15",[166,252,240],{"class":199},[166,254,255],{"class":243},"freq",[166,257,222],{"class":195},[166,259,260],{"class":176},"\"M\"",[166,262,263],{"class":199},").astype(",[166,265,266],{"class":249},"str",[166,268,269],{"class":199},"),\n",[166,271,273,276,279,282,285,288,291],{"class":168,"line":272},5,[166,274,275],{"class":176},"    \"Region\"",[166,277,278],{"class":199},": [",[166,280,281],{"class":176},"\"North\"",[166,283,284],{"class":199},"] ",[166,286,287],{"class":195},"*",[166,289,290],{"class":249}," 15",[166,292,293],{"class":199},",\n",[166,295,297,300],{"class":168,"line":296},6,[166,298,299],{"class":176},"    \"Revenue\"",[166,301,302],{"class":199},": [\n",[166,304,306,309,311,314,316,319,321,324,326,329,331,334],{"class":168,"line":305},7,[166,307,308],{"class":249},"        31200.0",[166,310,240],{"class":199},[166,312,313],{"class":249},"28400.0",[166,315,240],{"class":199},[166,317,318],{"class":249},"34100.0",[166,320,240],{"class":199},[166,322,323],{"class":249},"29800.0",[166,325,240],{"class":199},[166,327,328],{"class":249},"36500.0",[166,330,240],{"class":199},[166,332,333],{"class":249},"41200.0",[166,335,293],{"class":199},[166,337,339,342,344,347,349,352,354,357,359,362,364,367],{"class":168,"line":338},8,[166,340,341],{"class":249},"        38900.0",[166,343,240],{"class":199},[166,345,346],{"class":249},"33400.0",[166,348,240],{"class":199},[166,350,351],{"class":249},"45100.0",[166,353,240],{"class":199},[166,355,356],{"class":249},"47800.0",[166,358,240],{"class":199},[166,360,361],{"class":249},"51200.0",[166,363,240],{"class":199},[166,365,366],{"class":249},"58900.0",[166,368,293],{"class":199},[166,370,372,375,377,380,382,385],{"class":168,"line":371},9,[166,373,374],{"class":249},"        35600.0",[166,376,240],{"class":199},[166,378,379],{"class":249},"32900.0",[166,381,240],{"class":199},[166,383,384],{"class":249},"39400.0",[166,386,293],{"class":199},[166,388,390],{"class":168,"line":389},10,[166,391,392],{"class":199},"    ],\n",[166,394,396],{"class":168,"line":395},11,[166,397,398],{"class":199},"})\n",[166,400,402,405,408,410,412,415,417,420,422,424,426],{"class":168,"line":401},12,[166,403,404],{"class":199},"monthly[",[166,406,407],{"class":176},"\"Month\"",[166,409,284],{"class":199},[166,411,222],{"class":195},[166,413,414],{"class":199}," pd.PeriodIndex(monthly[",[166,416,407],{"class":176},[166,418,419],{"class":199},"], ",[166,421,255],{"class":243},[166,423,222],{"class":195},[166,425,260],{"class":176},[166,427,428],{"class":199},")\n",[151,430,432],{"id":431},"running-totals-cumsum","Running totals: cumsum",[156,434,436],{"className":186,"code":435,"language":188,"meta":161,"style":161},"# =SUM($C$2:C2) dragged down\nmonthly = monthly.sort_values(\"Month\")\nmonthly[\"Running_Total\"] = monthly[\"Revenue\"].cumsum()\n\n# Restarting each calendar year — a formula would need an IF on the month\nmonthly[\"YTD\"] = monthly.groupby(monthly[\"Month\"].dt.year)[\"Revenue\"].cumsum()\nprint(monthly[[\"Month\", \"Revenue\", \"Running_Total\", \"YTD\"]].head(15))\n",[163,437,438,444,457,477,481,486,509],{"__ignoreMap":161},[166,439,440],{"class":168,"line":169},[166,441,443],{"class":442},"s-wDw","# =SUM($C$2:C2) dragged down\n",[166,445,446,448,450,453,455],{"class":168,"line":209},[166,447,219],{"class":199},[166,449,222],{"class":195},[166,451,452],{"class":199}," monthly.sort_values(",[166,454,407],{"class":176},[166,456,428],{"class":199},[166,458,459,461,464,466,468,471,474],{"class":168,"line":216},[166,460,404],{"class":199},[166,462,463],{"class":176},"\"Running_Total\"",[166,465,284],{"class":199},[166,467,222],{"class":195},[166,469,470],{"class":199}," monthly[",[166,472,473],{"class":176},"\"Revenue\"",[166,475,476],{"class":199},"].cumsum()\n",[166,478,479],{"class":168,"line":228},[166,480,213],{"emptyLinePlaceholder":212},[166,482,483],{"class":168,"line":272},[166,484,485],{"class":442},"# Restarting each calendar year — a formula would need an IF on the month\n",[166,487,488,490,493,495,497,500,502,505,507],{"class":168,"line":296},[166,489,404],{"class":199},[166,491,492],{"class":176},"\"YTD\"",[166,494,284],{"class":199},[166,496,222],{"class":195},[166,498,499],{"class":199}," monthly.groupby(monthly[",[166,501,407],{"class":176},[166,503,504],{"class":199},"].dt.year)[",[166,506,473],{"class":176},[166,508,476],{"class":199},[166,510,511,514,517,519,521,523,525,527,529,531,534,536],{"class":168,"line":305},[166,512,513],{"class":249},"print",[166,515,516],{"class":199},"(monthly[[",[166,518,407],{"class":176},[166,520,240],{"class":199},[166,522,473],{"class":176},[166,524,240],{"class":199},[166,526,463],{"class":176},[166,528,240],{"class":199},[166,530,492],{"class":176},[166,532,533],{"class":199},"]].head(",[166,535,250],{"class":249},[166,537,538],{"class":199},"))\n",[10,540,541,542,545,546,549],{},"The ",[163,543,544],{},"sort_values"," call is not decoration. ",[163,547,548],{},"cumsum"," accumulates in the frame's current row order, so\nan unsorted frame produces a running total that is arithmetically correct and meaningless. Sorting\nexplicitly before any cumulative operation is a habit worth forming.",[10,551,552],{},"The year-to-date column is the one that shows the difference in expressiveness. In a sheet it needs\neither a formula that tests whether the year changed or a separate block per year; here it is a\ngrouping key.",[10,554,555,558,559,562],{},[163,556,557],{},"cummax"," and ",[163,560,561],{},"cummin"," follow the same pattern and answer questions a report often wants: the best\nmonth so far, and whether this month set a record.",[156,564,566],{"className":186,"code":565,"language":188,"meta":161,"style":161},"monthly[\"Best_So_Far\"] = monthly[\"Revenue\"].cummax()\nmonthly[\"New_Record\"] = monthly[\"Revenue\"] >= monthly[\"Best_So_Far\"]\n",[163,567,568,586],{"__ignoreMap":161},[166,569,570,572,575,577,579,581,583],{"class":168,"line":169},[166,571,404],{"class":199},[166,573,574],{"class":176},"\"Best_So_Far\"",[166,576,284],{"class":199},[166,578,222],{"class":195},[166,580,470],{"class":199},[166,582,473],{"class":176},[166,584,585],{"class":199},"].cummax()\n",[166,587,588,590,593,595,597,599,601,603,606,608,610],{"class":168,"line":209},[166,589,404],{"class":199},[166,591,592],{"class":176},"\"New_Record\"",[166,594,284],{"class":199},[166,596,222],{"class":195},[166,598,470],{"class":199},[166,600,473],{"class":176},[166,602,284],{"class":199},[166,604,605],{"class":195},">=",[166,607,470],{"class":199},[166,609,574],{"class":176},[166,611,612],{"class":199},"]\n",[151,614,616],{"id":615},"period-over-period-change-shift","Period-over-period change: shift",[20,618,29,623,29,626,29,629,29,632,29,637,29,644,29,649,29,654,29,659,29,664,29,667,29,669,29,672,29,676,29,679,29,682,29,687,29,690],{"viewBox":619,"role":23,"ariaLabelledBy":620,"xmlns":27,"style":28},"0 0 760 232",[621,622],"run-shift-t","run-shift-d",[31,624,625],{"id":621},"What shift actually does",[35,627,628],{"id":622},"Shifting a column by one position places each row's previous value beside it, after which the change and the growth rate are ordinary column arithmetic.",[39,630],{"x":41,"y":41,"width":42,"height":631,"fill":44},"232",[55,633,636],{"x":146,"y":634,"style":635},"32","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","period comparison",[39,638],{"x":639,"y":640,"width":122,"height":641,"rx":642,"fill":89,"stroke":90,"style":643},"24.0","74","96","12","stroke-width:2px",[55,645,648],{"x":93,"y":646,"style":647},"114","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Revenue",[55,650,653],{"x":93,"y":651,"style":652},"136","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","the ordered series",[168,655],{"x1":656,"y1":657,"x2":658,"y2":657,"stroke":90,"style":643},"237.0","122.0","269.0",[660,661],"polygon",{"points":662,"fill":663},"269.0,122.0 260.0,117.0 260.0,127.0","#5b5cf0",[39,665],{"x":666,"y":640,"width":122,"height":641,"rx":642,"fill":89,"stroke":90,"style":643},"276.0",[55,668,98],{"x":146,"y":646,"style":647},[55,670,671],{"x":146,"y":651,"style":652},"previous value, aligned",[168,673],{"x1":674,"y1":657,"x2":675,"y2":657,"stroke":90,"style":643},"489.0","521.0",[660,677],{"points":678,"fill":663},"521.0,122.0 512.0,117.0 512.0,127.0",[39,680],{"x":681,"y":640,"width":122,"height":641,"rx":642,"fill":72,"stroke":73,"style":643},"528.0",[55,683,686],{"x":684,"y":646,"style":685},"632.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","difference or ratio",[55,688,689],{"x":684,"y":651,"style":652},"change and growth",[55,691,693],{"x":146,"y":692,"style":148},"210","the first row is NaN because there is no previous value",[10,695,696,697,699],{},"A formula referencing the cell above becomes ",[163,698,98],{},", which moves the whole column down by one\nposition.",[156,701,703],{"className":186,"code":702,"language":188,"meta":161,"style":161},"monthly[\"Prev_Month\"] = monthly[\"Revenue\"].shift(1)\nmonthly[\"MoM_Change\"] = monthly[\"Revenue\"] - monthly[\"Prev_Month\"]\nmonthly[\"MoM_Pct\"] = monthly[\"Revenue\"].pct_change()\n\nprint(monthly[[\"Month\", \"Revenue\", \"MoM_Change\", \"MoM_Pct\"]].head(6))\n",[163,704,705,728,754,772,776],{"__ignoreMap":161},[166,706,707,709,712,714,716,718,720,723,726],{"class":168,"line":169},[166,708,404],{"class":199},[166,710,711],{"class":176},"\"Prev_Month\"",[166,713,284],{"class":199},[166,715,222],{"class":195},[166,717,470],{"class":199},[166,719,473],{"class":176},[166,721,722],{"class":199},"].shift(",[166,724,725],{"class":249},"1",[166,727,428],{"class":199},[166,729,730,732,735,737,739,741,743,745,748,750,752],{"class":168,"line":209},[166,731,404],{"class":199},[166,733,734],{"class":176},"\"MoM_Change\"",[166,736,284],{"class":199},[166,738,222],{"class":195},[166,740,470],{"class":199},[166,742,473],{"class":176},[166,744,284],{"class":199},[166,746,747],{"class":195},"-",[166,749,470],{"class":199},[166,751,711],{"class":176},[166,753,612],{"class":199},[166,755,756,758,761,763,765,767,769],{"class":168,"line":216},[166,757,404],{"class":199},[166,759,760],{"class":176},"\"MoM_Pct\"",[166,762,284],{"class":199},[166,764,222],{"class":195},[166,766,470],{"class":199},[166,768,473],{"class":176},[166,770,771],{"class":199},"].pct_change()\n",[166,773,774],{"class":168,"line":228},[166,775,213],{"emptyLinePlaceholder":212},[166,777,778,780,782,784,786,788,790,792,794,796,798,801],{"class":168,"line":272},[166,779,513],{"class":249},[166,781,516],{"class":199},[166,783,407],{"class":176},[166,785,240],{"class":199},[166,787,473],{"class":176},[166,789,240],{"class":199},[166,791,734],{"class":176},[166,793,240],{"class":199},[166,795,760],{"class":176},[166,797,533],{"class":199},[166,799,800],{"class":249},"6",[166,802,538],{"class":199},[10,804,805,807],{},[163,806,111],{}," is the shorthand for the change expressed as a proportion, and it handles the first\nrow the way it should: NaN, because there is no previous value. A spreadsheet usually hides that with\na blanked first cell, which means the same information is present and undocumented.",[10,809,810,811,814],{},"Year-over-year on monthly data is ",[163,812,813],{},"shift(12)",":",[156,816,818],{"className":186,"code":817,"language":188,"meta":161,"style":161},"monthly[\"YoY_Pct\"] = monthly[\"Revenue\"].pct_change(periods=12)\nprint(monthly[[\"Month\", \"Revenue\", \"YoY_Pct\"]].tail(3))\n",[163,819,820,846],{"__ignoreMap":161},[166,821,822,824,827,829,831,833,835,838,840,842,844],{"class":168,"line":169},[166,823,404],{"class":199},[166,825,826],{"class":176},"\"YoY_Pct\"",[166,828,284],{"class":199},[166,830,222],{"class":195},[166,832,470],{"class":199},[166,834,473],{"class":176},[166,836,837],{"class":199},"].pct_change(",[166,839,244],{"class":243},[166,841,222],{"class":195},[166,843,642],{"class":249},[166,845,428],{"class":199},[166,847,848,850,852,854,856,858,860,862,865,868],{"class":168,"line":209},[166,849,513],{"class":249},[166,851,516],{"class":199},[166,853,407],{"class":176},[166,855,240],{"class":199},[166,857,473],{"class":176},[166,859,240],{"class":199},[166,861,826],{"class":176},[166,863,864],{"class":199},"]].tail(",[166,866,867],{"class":249},"3",[166,869,538],{"class":199},[10,871,872],{},"That works only if the series is complete — twelve rows really is twelve months. When months can be\nmissing, join the frame to itself on a shifted key instead, which is correct regardless of gaps:",[156,874,876],{"className":186,"code":875,"language":188,"meta":161,"style":161},"prior = monthly[[\"Month\", \"Revenue\"]].copy()\nprior[\"Month\"] = prior[\"Month\"] + 12\nprior = prior.rename(columns={\"Revenue\": \"Revenue_LY\"})\n\ncompared = monthly.merge(prior, on=\"Month\", how=\"left\")\ncompared[\"YoY_Pct\"] = compared[\"Revenue\"] \u002F compared[\"Revenue_LY\"] - 1\n",[163,877,878,897,921,948,952,981],{"__ignoreMap":161},[166,879,880,883,885,888,890,892,894],{"class":168,"line":169},[166,881,882],{"class":199},"prior ",[166,884,222],{"class":195},[166,886,887],{"class":199}," monthly[[",[166,889,407],{"class":176},[166,891,240],{"class":199},[166,893,473],{"class":176},[166,895,896],{"class":199},"]].copy()\n",[166,898,899,902,904,906,908,911,913,915,918],{"class":168,"line":209},[166,900,901],{"class":199},"prior[",[166,903,407],{"class":176},[166,905,284],{"class":199},[166,907,222],{"class":195},[166,909,910],{"class":199}," prior[",[166,912,407],{"class":176},[166,914,284],{"class":199},[166,916,917],{"class":195},"+",[166,919,920],{"class":249}," 12\n",[166,922,923,925,927,930,933,935,938,940,943,946],{"class":168,"line":216},[166,924,882],{"class":199},[166,926,222],{"class":195},[166,928,929],{"class":199}," prior.rename(",[166,931,932],{"class":243},"columns",[166,934,222],{"class":195},[166,936,937],{"class":199},"{",[166,939,473],{"class":176},[166,941,942],{"class":199},": ",[166,944,945],{"class":176},"\"Revenue_LY\"",[166,947,398],{"class":199},[166,949,950],{"class":168,"line":228},[166,951,213],{"emptyLinePlaceholder":212},[166,953,954,957,959,962,965,967,969,971,974,976,979],{"class":168,"line":272},[166,955,956],{"class":199},"compared ",[166,958,222],{"class":195},[166,960,961],{"class":199}," monthly.merge(prior, ",[166,963,964],{"class":243},"on",[166,966,222],{"class":195},[166,968,407],{"class":176},[166,970,240],{"class":199},[166,972,973],{"class":243},"how",[166,975,222],{"class":195},[166,977,978],{"class":176},"\"left\"",[166,980,428],{"class":199},[166,982,983,986,988,990,992,995,997,999,1002,1004,1006,1008,1010],{"class":168,"line":296},[166,984,985],{"class":199},"compared[",[166,987,826],{"class":176},[166,989,284],{"class":199},[166,991,222],{"class":195},[166,993,994],{"class":199}," compared[",[166,996,473],{"class":176},[166,998,284],{"class":199},[166,1000,1001],{"class":195},"\u002F",[166,1003,994],{"class":199},[166,1005,945],{"class":176},[166,1007,284],{"class":199},[166,1009,747],{"class":195},[166,1011,1012],{"class":249}," 1\n",[151,1014,1016],{"id":1015},"rolling-windows","Rolling windows",[156,1018,1020],{"className":186,"code":1019,"language":188,"meta":161,"style":161},"monthly[\"Rolling_3\"] = monthly[\"Revenue\"].rolling(3).mean()\nmonthly[\"Rolling_3_Min1\"] = monthly[\"Revenue\"].rolling(3, min_periods=1).mean()\nmonthly[\"Rolling_12_Sum\"] = monthly[\"Revenue\"].rolling(12).sum()\nprint(monthly[[\"Month\", \"Revenue\", \"Rolling_3\", \"Rolling_3_Min1\"]].head(5))\n",[163,1021,1022,1045,1075,1097],{"__ignoreMap":161},[166,1023,1024,1026,1029,1031,1033,1035,1037,1040,1042],{"class":168,"line":169},[166,1025,404],{"class":199},[166,1027,1028],{"class":176},"\"Rolling_3\"",[166,1030,284],{"class":199},[166,1032,222],{"class":195},[166,1034,470],{"class":199},[166,1036,473],{"class":176},[166,1038,1039],{"class":199},"].rolling(",[166,1041,867],{"class":249},[166,1043,1044],{"class":199},").mean()\n",[166,1046,1047,1049,1052,1054,1056,1058,1060,1062,1064,1066,1069,1071,1073],{"class":168,"line":209},[166,1048,404],{"class":199},[166,1050,1051],{"class":176},"\"Rolling_3_Min1\"",[166,1053,284],{"class":199},[166,1055,222],{"class":195},[166,1057,470],{"class":199},[166,1059,473],{"class":176},[166,1061,1039],{"class":199},[166,1063,867],{"class":249},[166,1065,240],{"class":199},[166,1067,1068],{"class":243},"min_periods",[166,1070,222],{"class":195},[166,1072,725],{"class":249},[166,1074,1044],{"class":199},[166,1076,1077,1079,1082,1084,1086,1088,1090,1092,1094],{"class":168,"line":216},[166,1078,404],{"class":199},[166,1080,1081],{"class":176},"\"Rolling_12_Sum\"",[166,1083,284],{"class":199},[166,1085,222],{"class":195},[166,1087,470],{"class":199},[166,1089,473],{"class":176},[166,1091,1039],{"class":199},[166,1093,642],{"class":249},[166,1095,1096],{"class":199},").sum()\n",[166,1098,1099,1101,1103,1105,1107,1109,1111,1113,1115,1117,1119,1122],{"class":168,"line":228},[166,1100,513],{"class":249},[166,1102,516],{"class":199},[166,1104,407],{"class":176},[166,1106,240],{"class":199},[166,1108,473],{"class":176},[166,1110,240],{"class":199},[166,1112,1028],{"class":176},[166,1114,240],{"class":199},[166,1116,1051],{"class":176},[166,1118,533],{"class":199},[166,1120,1121],{"class":249},"5",[166,1123,538],{"class":199},[10,1125,1126,1128,1129,1132],{},[163,1127,1068],{}," decides what happens at the start of the series: the default leaves the first two rows\nNaN because a three-month window is not yet full, while ",[163,1130,1131],{},"min_periods=1"," averages whatever exists. The\nfirst is more honest in a chart; the second is friendlier in a table. Excel's dragged AVERAGE over a\nrelative range silently does the second by shrinking the range at the top of the column, which is\nworth knowing when the numbers do not match.",[151,1134,1136],{"id":1135},"accumulating-within-groups","Accumulating within groups",[10,1138,1139],{},"Everything above extends to several series in one frame, which is where the pandas version pulls\nclearly ahead of a sheet per region.",[156,1141,1143],{"className":186,"code":1142,"language":188,"meta":161,"style":161},"multi = pd.concat([\n    monthly.assign(Region=\"North\"),\n    monthly.assign(Region=\"South\", Revenue=monthly[\"Revenue\"] * 0.7),\n], ignore_index=True).sort_values([\"Region\", \"Month\"])\n\nmulti[\"Running_Total\"] = multi.groupby(\"Region\")[\"Revenue\"].cumsum()\nmulti[\"MoM_Pct\"] = multi.groupby(\"Region\")[\"Revenue\"].pct_change()\nmulti[\"Rolling_3\"] = multi.groupby(\"Region\")[\"Revenue\"].transform(\n    lambda s: s.rolling(3, min_periods=1).mean()\n)\nprint(multi.groupby(\"Region\").tail(2))\n",[163,1144,1145,1155,1169,1199,1224,1228,1251,1271,1292,1312,1316],{"__ignoreMap":161},[166,1146,1147,1150,1152],{"class":168,"line":169},[166,1148,1149],{"class":199},"multi ",[166,1151,222],{"class":195},[166,1153,1154],{"class":199}," pd.concat([\n",[166,1156,1157,1160,1163,1165,1167],{"class":168,"line":209},[166,1158,1159],{"class":199},"    monthly.assign(",[166,1161,1162],{"class":243},"Region",[166,1164,222],{"class":195},[166,1166,281],{"class":176},[166,1168,269],{"class":199},[166,1170,1171,1173,1175,1177,1180,1182,1184,1186,1188,1190,1192,1194,1197],{"class":168,"line":216},[166,1172,1159],{"class":199},[166,1174,1162],{"class":243},[166,1176,222],{"class":195},[166,1178,1179],{"class":176},"\"South\"",[166,1181,240],{"class":199},[166,1183,648],{"class":243},[166,1185,222],{"class":195},[166,1187,404],{"class":199},[166,1189,473],{"class":176},[166,1191,284],{"class":199},[166,1193,287],{"class":195},[166,1195,1196],{"class":249}," 0.7",[166,1198,269],{"class":199},[166,1200,1201,1203,1206,1208,1211,1214,1217,1219,1221],{"class":168,"line":228},[166,1202,419],{"class":199},[166,1204,1205],{"class":243},"ignore_index",[166,1207,222],{"class":195},[166,1209,1210],{"class":249},"True",[166,1212,1213],{"class":199},").sort_values([",[166,1215,1216],{"class":176},"\"Region\"",[166,1218,240],{"class":199},[166,1220,407],{"class":176},[166,1222,1223],{"class":199},"])\n",[166,1225,1226],{"class":168,"line":272},[166,1227,213],{"emptyLinePlaceholder":212},[166,1229,1230,1233,1235,1237,1239,1242,1244,1247,1249],{"class":168,"line":296},[166,1231,1232],{"class":199},"multi[",[166,1234,463],{"class":176},[166,1236,284],{"class":199},[166,1238,222],{"class":195},[166,1240,1241],{"class":199}," multi.groupby(",[166,1243,1216],{"class":176},[166,1245,1246],{"class":199},")[",[166,1248,473],{"class":176},[166,1250,476],{"class":199},[166,1252,1253,1255,1257,1259,1261,1263,1265,1267,1269],{"class":168,"line":305},[166,1254,1232],{"class":199},[166,1256,760],{"class":176},[166,1258,284],{"class":199},[166,1260,222],{"class":195},[166,1262,1241],{"class":199},[166,1264,1216],{"class":176},[166,1266,1246],{"class":199},[166,1268,473],{"class":176},[166,1270,771],{"class":199},[166,1272,1273,1275,1277,1279,1281,1283,1285,1287,1289],{"class":168,"line":338},[166,1274,1232],{"class":199},[166,1276,1028],{"class":176},[166,1278,284],{"class":199},[166,1280,222],{"class":195},[166,1282,1241],{"class":199},[166,1284,1216],{"class":176},[166,1286,1246],{"class":199},[166,1288,473],{"class":176},[166,1290,1291],{"class":199},"].transform(\n",[166,1293,1294,1297,1300,1302,1304,1306,1308,1310],{"class":168,"line":371},[166,1295,1296],{"class":195},"    lambda",[166,1298,1299],{"class":199}," s: s.rolling(",[166,1301,867],{"class":249},[166,1303,240],{"class":199},[166,1305,1068],{"class":243},[166,1307,222],{"class":195},[166,1309,725],{"class":249},[166,1311,1044],{"class":199},[166,1313,1314],{"class":168,"line":389},[166,1315,428],{"class":199},[166,1317,1318,1320,1323,1325,1328,1331],{"class":168,"line":395},[166,1319,513],{"class":249},[166,1321,1322],{"class":199},"(multi.groupby(",[166,1324,1216],{"class":176},[166,1326,1327],{"class":199},").tail(",[166,1329,1330],{"class":249},"2",[166,1332,538],{"class":199},[10,1334,1335],{},"Every one of those would be a separate block of formulas per region in a workbook, kept in step by\nhand. Here the group key does the work, and adding a third region changes nothing.",[151,1337,1339],{"id":1338},"filling-the-gaps-before-you-accumulate","Filling the gaps before you accumulate",[10,1341,1342],{},"Every calculation in this guide assumes a row exists for each period. Real data rarely obliges: a\nregion with no sales in August has no August row, and a running total or a shift then quietly treats\nSeptember as the month after July. Reindexing over a complete period range fixes it before anything\ndownstream can go wrong.",[156,1344,1346],{"className":186,"code":1345,"language":188,"meta":161,"style":161},"import pandas as pd\n\nfull_range = pd.period_range(monthly[\"Month\"].min(), monthly[\"Month\"].max(), freq=\"M\")\n\ncomplete = (\n    monthly.set_index(\"Month\")\n           .reindex(full_range)\n           .rename_axis(\"Month\")\n           .reset_index()\n)\ncomplete[\"Revenue\"] = complete[\"Revenue\"].fillna(0.0)\ncomplete[\"Region\"] = complete[\"Region\"].ffill()\nprint(complete.tail(4))\n",[163,1347,1348,1358,1362,1390,1394,1404,1413,1418,1427,1432,1436,1460,1477],{"__ignoreMap":161},[166,1349,1350,1352,1354,1356],{"class":168,"line":169},[166,1351,196],{"class":195},[166,1353,200],{"class":199},[166,1355,203],{"class":195},[166,1357,206],{"class":199},[166,1359,1360],{"class":168,"line":209},[166,1361,213],{"emptyLinePlaceholder":212},[166,1363,1364,1367,1369,1372,1374,1377,1379,1382,1384,1386,1388],{"class":168,"line":216},[166,1365,1366],{"class":199},"full_range ",[166,1368,222],{"class":195},[166,1370,1371],{"class":199}," pd.period_range(monthly[",[166,1373,407],{"class":176},[166,1375,1376],{"class":199},"].min(), monthly[",[166,1378,407],{"class":176},[166,1380,1381],{"class":199},"].max(), ",[166,1383,255],{"class":243},[166,1385,222],{"class":195},[166,1387,260],{"class":176},[166,1389,428],{"class":199},[166,1391,1392],{"class":168,"line":228},[166,1393,213],{"emptyLinePlaceholder":212},[166,1395,1396,1399,1401],{"class":168,"line":272},[166,1397,1398],{"class":199},"complete ",[166,1400,222],{"class":195},[166,1402,1403],{"class":199}," (\n",[166,1405,1406,1409,1411],{"class":168,"line":296},[166,1407,1408],{"class":199},"    monthly.set_index(",[166,1410,407],{"class":176},[166,1412,428],{"class":199},[166,1414,1415],{"class":168,"line":305},[166,1416,1417],{"class":199},"           .reindex(full_range)\n",[166,1419,1420,1423,1425],{"class":168,"line":338},[166,1421,1422],{"class":199},"           .rename_axis(",[166,1424,407],{"class":176},[166,1426,428],{"class":199},[166,1428,1429],{"class":168,"line":371},[166,1430,1431],{"class":199},"           .reset_index()\n",[166,1433,1434],{"class":168,"line":389},[166,1435,428],{"class":199},[166,1437,1438,1441,1443,1445,1447,1450,1452,1455,1458],{"class":168,"line":395},[166,1439,1440],{"class":199},"complete[",[166,1442,473],{"class":176},[166,1444,284],{"class":199},[166,1446,222],{"class":195},[166,1448,1449],{"class":199}," complete[",[166,1451,473],{"class":176},[166,1453,1454],{"class":199},"].fillna(",[166,1456,1457],{"class":249},"0.0",[166,1459,428],{"class":199},[166,1461,1462,1464,1466,1468,1470,1472,1474],{"class":168,"line":401},[166,1463,1440],{"class":199},[166,1465,1216],{"class":176},[166,1467,284],{"class":199},[166,1469,222],{"class":195},[166,1471,1449],{"class":199},[166,1473,1216],{"class":176},[166,1475,1476],{"class":199},"].ffill()\n",[166,1478,1480,1482,1485,1488],{"class":168,"line":1479},13,[166,1481,513],{"class":249},[166,1483,1484],{"class":199},"(complete.tail(",[166,1486,1487],{"class":249},"4",[166,1489,538],{"class":199},[10,1491,1492],{},"The two fill choices are deliberate and different. Revenue becomes zero because no sales genuinely\nmeans zero; the region is forward-filled because it is a label rather than a measurement. Getting\nthat distinction wrong — filling a measure forward, or zeroing a label — produces a report that looks\ncomplete and is fabricated, which is worse than one with a visible gap.",[10,1494,1495],{},"For grouped data the same reindexing runs per group, and building a full cross-product of periods and\ngroups first is the reliable way to do it:",[156,1497,1499],{"className":186,"code":1498,"language":188,"meta":161,"style":161},"skeleton = pd.MultiIndex.from_product(\n    [multi[\"Region\"].unique(), full_range], names=[\"Region\", \"Month\"]\n)\ndense = multi.set_index([\"Region\", \"Month\"]).reindex(skeleton).reset_index()\n",[163,1500,1501,1511,1537,1541],{"__ignoreMap":161},[166,1502,1503,1506,1508],{"class":168,"line":169},[166,1504,1505],{"class":199},"skeleton ",[166,1507,222],{"class":195},[166,1509,1510],{"class":199}," pd.MultiIndex.from_product(\n",[166,1512,1513,1516,1518,1521,1524,1526,1529,1531,1533,1535],{"class":168,"line":209},[166,1514,1515],{"class":199},"    [multi[",[166,1517,1216],{"class":176},[166,1519,1520],{"class":199},"].unique(), full_range], ",[166,1522,1523],{"class":243},"names",[166,1525,222],{"class":195},[166,1527,1528],{"class":199},"[",[166,1530,1216],{"class":176},[166,1532,240],{"class":199},[166,1534,407],{"class":176},[166,1536,612],{"class":199},[166,1538,1539],{"class":168,"line":216},[166,1540,428],{"class":199},[166,1542,1543,1546,1548,1551,1553,1555,1557],{"class":168,"line":228},[166,1544,1545],{"class":199},"dense ",[166,1547,222],{"class":195},[166,1549,1550],{"class":199}," multi.set_index([",[166,1552,1216],{"class":176},[166,1554,240],{"class":199},[166,1556,407],{"class":176},[166,1558,1559],{"class":199},"]).reindex(skeleton).reset_index()\n",[10,1561,1562,1566,1567,1571],{},[14,1563,1565],{"href":1564},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ffill-missing-values-in-excel-with-pandas-fillna\u002F","Fill Missing Values in Excel with pandas fillna","\ncovers the choice of fill strategy in more depth; the point here is that it has to happen ",[1568,1569,1570],"em",{},"before","\nthe accumulation, not after.",[151,1573,1575],{"id":1574},"presenting-the-result","Presenting the result",[10,1577,1578],{},"A growth column is only useful if the reader can tell a good number from a bad one at a glance, and\nthat is a formatting decision rather than a calculation. Writing the raw proportion and applying a\npercentage number format keeps the value sortable and chartable while showing it the way a reader\nexpects.",[156,1580,1582],{"className":186,"code":1581,"language":188,"meta":161,"style":161},"import pandas as pd\n\nwith pd.ExcelWriter(\"growth.xlsx\", engine=\"xlsxwriter\") as writer:\n    monthly.to_excel(writer, sheet_name=\"Monthly\", index=False)\n    book, sheet = writer.book, writer.sheets[\"Monthly\"]\n    percent = book.add_format({\"num_format\": \"0.0%\"})\n    money = book.add_format({\"num_format\": \"#,##0\"})\n    sheet.set_column(\"C:C\", 14, money)\n    sheet.set_column(\"F:F\", 12, percent)\n    sheet.conditional_format(\"F2:F200\", {\"type\": \"3_color_scale\"})\n    sheet.freeze_panes(1, 0)\n",[163,1583,1584,1594,1598,1627,1652,1666,1686,1704,1720,1734,1755],{"__ignoreMap":161},[166,1585,1586,1588,1590,1592],{"class":168,"line":169},[166,1587,196],{"class":195},[166,1589,200],{"class":199},[166,1591,203],{"class":195},[166,1593,206],{"class":199},[166,1595,1596],{"class":168,"line":209},[166,1597,213],{"emptyLinePlaceholder":212},[166,1599,1600,1603,1606,1609,1611,1614,1616,1619,1622,1624],{"class":168,"line":216},[166,1601,1602],{"class":195},"with",[166,1604,1605],{"class":199}," pd.ExcelWriter(",[166,1607,1608],{"class":176},"\"growth.xlsx\"",[166,1610,240],{"class":199},[166,1612,1613],{"class":243},"engine",[166,1615,222],{"class":195},[166,1617,1618],{"class":176},"\"xlsxwriter\"",[166,1620,1621],{"class":199},") ",[166,1623,203],{"class":195},[166,1625,1626],{"class":199}," writer:\n",[166,1628,1629,1632,1635,1637,1640,1642,1645,1647,1650],{"class":168,"line":228},[166,1630,1631],{"class":199},"    monthly.to_excel(writer, ",[166,1633,1634],{"class":243},"sheet_name",[166,1636,222],{"class":195},[166,1638,1639],{"class":176},"\"Monthly\"",[166,1641,240],{"class":199},[166,1643,1644],{"class":243},"index",[166,1646,222],{"class":195},[166,1648,1649],{"class":249},"False",[166,1651,428],{"class":199},[166,1653,1654,1657,1659,1662,1664],{"class":168,"line":272},[166,1655,1656],{"class":199},"    book, sheet ",[166,1658,222],{"class":195},[166,1660,1661],{"class":199}," writer.book, writer.sheets[",[166,1663,1639],{"class":176},[166,1665,612],{"class":199},[166,1667,1668,1671,1673,1676,1679,1681,1684],{"class":168,"line":296},[166,1669,1670],{"class":199},"    percent ",[166,1672,222],{"class":195},[166,1674,1675],{"class":199}," book.add_format({",[166,1677,1678],{"class":176},"\"num_format\"",[166,1680,942],{"class":199},[166,1682,1683],{"class":176},"\"0.0%\"",[166,1685,398],{"class":199},[166,1687,1688,1691,1693,1695,1697,1699,1702],{"class":168,"line":305},[166,1689,1690],{"class":199},"    money ",[166,1692,222],{"class":195},[166,1694,1675],{"class":199},[166,1696,1678],{"class":176},[166,1698,942],{"class":199},[166,1700,1701],{"class":176},"\"#,##0\"",[166,1703,398],{"class":199},[166,1705,1706,1709,1712,1714,1717],{"class":168,"line":338},[166,1707,1708],{"class":199},"    sheet.set_column(",[166,1710,1711],{"class":176},"\"C:C\"",[166,1713,240],{"class":199},[166,1715,1716],{"class":249},"14",[166,1718,1719],{"class":199},", money)\n",[166,1721,1722,1724,1727,1729,1731],{"class":168,"line":371},[166,1723,1708],{"class":199},[166,1725,1726],{"class":176},"\"F:F\"",[166,1728,240],{"class":199},[166,1730,642],{"class":249},[166,1732,1733],{"class":199},", percent)\n",[166,1735,1736,1739,1742,1745,1748,1750,1753],{"class":168,"line":389},[166,1737,1738],{"class":199},"    sheet.conditional_format(",[166,1740,1741],{"class":176},"\"F2:F200\"",[166,1743,1744],{"class":199},", {",[166,1746,1747],{"class":176},"\"type\"",[166,1749,942],{"class":199},[166,1751,1752],{"class":176},"\"3_color_scale\"",[166,1754,398],{"class":199},[166,1756,1757,1760,1762,1764,1766],{"class":168,"line":395},[166,1758,1759],{"class":199},"    sheet.freeze_panes(",[166,1761,725],{"class":249},[166,1763,240],{"class":199},[166,1765,41],{"class":249},[166,1767,428],{"class":199},[10,1769,1770,1771,18],{},"A three-colour scale over the growth column turns a table of numbers into something readable in a\nsecond, and it costs one line. The wider set of options is in\n",[14,1772,1774],{"href":1773},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fapply-conditional-formatting-with-xlsxwriter\u002F","Apply Conditional Formatting with XlsxWriter",[151,1776,1778],{"id":1777},"common-pitfalls","Common pitfalls",[1780,1781,1782,1798],"table",{},[1783,1784,1785],"thead",{},[1786,1787,1788,1792,1795],"tr",{},[1789,1790,1791],"th",{},"Symptom",[1789,1793,1794],{},"Cause",[1789,1796,1797],{},"Fix",[1799,1800,1801,1815,1830,1843,1857,1871],"tbody",{},[1786,1802,1803,1807,1810],{},[1804,1805,1806],"td",{},"Running total looks random",[1804,1808,1809],{},"The frame was not sorted by date",[1804,1811,1812,1814],{},[163,1813,544],{}," before any cumulative operation",[1786,1816,1817,1820,1825],{},[1804,1818,1819],{},"A group's total continues from the previous group",[1804,1821,1822,1824],{},[163,1823,548],{}," applied to the whole column",[1804,1826,1827],{},[163,1828,1829],{},"groupby(key)[col].cumsum()",[1786,1831,1832,1835,1840],{},[1804,1833,1834],{},"Year-over-year is wrong after a missing month",[1804,1836,1837,1839],{},[163,1838,813],{}," assumes a complete series",[1804,1841,1842],{},"Merge on a shifted period key instead",[1786,1844,1845,1848,1851],{},[1804,1846,1847],{},"The first rows of a rolling mean are NaN",[1804,1849,1850],{},"The window is not yet full",[1804,1852,1853,1854,1856],{},"Pass ",[163,1855,1068],{}," deliberately",[1786,1858,1859,1862,1868],{},[1804,1860,1861],{},"Percentages are 100 times too small",[1804,1863,1864,1867],{},[163,1865,1866],{},"pct_change"," returns a proportion, not a percentage",[1804,1869,1870],{},"Multiply by 100, or format as a percentage in the sheet",[1786,1872,1873,1876,1879],{},[1804,1874,1875],{},"Growth is infinite",[1804,1877,1878],{},"The prior period was zero",[1804,1880,1881],{},"Guard the division, or report the absolute change instead",[151,1883,1885],{"id":1884},"performance-and-scale","Performance and scale",[20,1887,29,1892,29,1895,29,1898,29,1901,29,1909,29,1914,29,1920,29,1924,29,1928,29,1932,29,1935,29,1940,29,1944,29,1947,29,1950,29,1953,29,1961,29,1966,29,1971,29,1974,29,1978,29,1981],{"viewBox":1888,"role":23,"ariaLabelledBy":1889,"xmlns":27,"style":28},"0 0 760 201",[1890,1891],"run-gap-t","run-gap-d",[31,1893,1894],{"id":1890},"Shift assumes a complete series; a join does not",[35,1896,1897],{"id":1891},"Shifting by twelve rows gives the wrong comparison when a month is missing from the data, while joining the frame to itself on a year-shifted period key stays correct regardless of gaps.",[39,1899],{"x":41,"y":41,"width":42,"height":1900,"fill":44},"201",[39,1902],{"x":1903,"y":1904,"width":1905,"height":1906,"rx":1716,"fill":1907,"stroke":1908,"style":643},"20","28","270.0","139","#fee8f2","var(--accent,#d81b73)",[55,1910,813],{"x":1911,"y":1912,"style":1913},"155.0","54","font-size:13px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:middle",[168,1915],{"x1":1916,"y1":1917,"x2":1918,"y2":1917,"stroke":1908,"style":1919},"36","64","274.0","stroke-width:1px",[55,1921,1923],{"x":1911,"y":1922,"style":81},"86","counts rows, not months",[55,1925,1927],{"x":1911,"y":1926,"style":81},"109","wrong after a gap",[55,1929,1931],{"x":1911,"y":1930,"style":81},"132","no warning",[39,1933],{"x":1934,"y":1904,"width":1905,"height":1906,"rx":1716,"fill":72,"stroke":73,"style":643},"470.0",[55,1936,1939],{"x":1937,"y":1912,"style":1938},"605.0","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","merge on a shifted key",[168,1941],{"x1":1942,"y1":1917,"x2":1943,"y2":1917,"stroke":73,"style":1919},"486.0","724.0",[55,1945,1946],{"x":1937,"y":1922,"style":81},"matches by period",[55,1948,1949],{"x":1937,"y":1926,"style":81},"gap becomes NaN",[55,1951,1952],{"x":1937,"y":1930,"style":81},"correct either way",[39,1954],{"x":1955,"y":1956,"width":1957,"height":1958,"rx":1959,"fill":1960,"stroke":90},"316.0","78.5","128","38","19","#ebebfd",[55,1962,1965],{"x":146,"y":1963,"style":1964},"102.5","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","year on year",[168,1967],{"x1":1968,"y1":1969,"x2":1970,"y2":1969,"stroke":90,"style":643},"295.0","97.5","309.0",[660,1972],{"points":1973,"fill":663},"309.0,97.5 300.0,92.5 300.0,102.5",[168,1975],{"x1":1976,"y1":1969,"x2":1977,"y2":1969,"stroke":90,"style":643},"449.0","463.0",[660,1979],{"points":1980,"fill":663},"463.0,97.5 454.0,92.5 454.0,102.5",[55,1982,1983],{"x":146,"y":117,"style":148},"when months can be missing, join rather than shift",[10,1985,1986,1987,1990],{},"Cumulative and rolling operations are single passes over the column, so they are fast and stay fast.\nThe cost that does appear is in the grouped ",[163,1988,1989],{},"transform"," with a lambda, which runs the function once\nper group — fine for a dozen regions, noticeable for a hundred thousand customer identifiers.",[10,1992,1993],{},"Where a rolling calculation must run per group over many groups, sorting once and using the built-in\ngrouped rolling is materially quicker than a lambda:",[156,1995,1997],{"className":186,"code":1996,"language":188,"meta":161,"style":161},"# Faster than transform with a lambda when there are many groups\nrolled = (\n    multi.sort_values([\"Region\", \"Month\"])\n         .groupby(\"Region\")[\"Revenue\"]\n         .rolling(3, min_periods=1).mean()\n         .reset_index(level=0, drop=True)\n)\nmulti[\"Rolling_3\"] = rolled\n",[163,1998,1999,2004,2013,2026,2039,2056,2079,2083],{"__ignoreMap":161},[166,2000,2001],{"class":168,"line":169},[166,2002,2003],{"class":442},"# Faster than transform with a lambda when there are many groups\n",[166,2005,2006,2009,2011],{"class":168,"line":209},[166,2007,2008],{"class":199},"rolled ",[166,2010,222],{"class":195},[166,2012,1403],{"class":199},[166,2014,2015,2018,2020,2022,2024],{"class":168,"line":216},[166,2016,2017],{"class":199},"    multi.sort_values([",[166,2019,1216],{"class":176},[166,2021,240],{"class":199},[166,2023,407],{"class":176},[166,2025,1223],{"class":199},[166,2027,2028,2031,2033,2035,2037],{"class":168,"line":228},[166,2029,2030],{"class":199},"         .groupby(",[166,2032,1216],{"class":176},[166,2034,1246],{"class":199},[166,2036,473],{"class":176},[166,2038,612],{"class":199},[166,2040,2041,2044,2046,2048,2050,2052,2054],{"class":168,"line":272},[166,2042,2043],{"class":199},"         .rolling(",[166,2045,867],{"class":249},[166,2047,240],{"class":199},[166,2049,1068],{"class":243},[166,2051,222],{"class":195},[166,2053,725],{"class":249},[166,2055,1044],{"class":199},[166,2057,2058,2061,2064,2066,2068,2070,2073,2075,2077],{"class":168,"line":296},[166,2059,2060],{"class":199},"         .reset_index(",[166,2062,2063],{"class":243},"level",[166,2065,222],{"class":195},[166,2067,41],{"class":249},[166,2069,240],{"class":199},[166,2071,2072],{"class":243},"drop",[166,2074,222],{"class":195},[166,2076,1210],{"class":249},[166,2078,428],{"class":199},[166,2080,2081],{"class":168,"line":305},[166,2082,428],{"class":199},[166,2084,2085,2087,2089,2091,2093],{"class":168,"line":338},[166,2086,1232],{"class":199},[166,2088,1028],{"class":176},[166,2090,284],{"class":199},[166,2092,222],{"class":195},[166,2094,2095],{"class":199}," rolled\n",[10,2097,541,2098,2101],{},[163,2099,2100],{},"reset_index"," step is what aligns the result back to the original frame — grouped rolling returns\na multi-level index, and dropping the group level restores the row alignment.",[151,2103,2105],{"id":2104},"conclusion","Conclusion",[10,2107,2108,2110,2111,2114,2115,2118,2119,2121,2122,2125],{},[163,2109,548],{}," replaces a running-total formula and ",[163,2112,2113],{},"groupby().cumsum()"," restarts it per year or region.\n",[163,2116,2117],{},"shift"," replaces any reference to the row above, ",[163,2120,1866],{}," gives period-over-period growth, and\n",[163,2123,2124],{},"rolling"," gives moving averages with explicit control over the start of the series. The one rule that\nmatters throughout: sort the frame first, because every one of these operations follows row order and\nnone of them will warn you when that order is wrong.",[151,2127,2129],{"id":2128},"frequently-asked-questions","Frequently asked questions",[10,2131,2132,2136,2137,2140],{},[2133,2134,2135],"strong",{},"Why does my running total restart in the wrong place?","\nBecause cumsum follows the frame's current row order, not a date column. Sort by the date before accumulating, and if the total should restart per group use groupby(key)",[166,2138,2139],{},"col",".cumsum().",[10,2142,2143,2146],{},[2133,2144,2145],{},"What is the equivalent of a formula referencing the row above?","\nshift(1), which moves a column down by one row. A month-over-month change is col - col.shift(1), and the first row is NaN because there is no previous value — which is the honest answer Excel hides behind a manually blanked cell.",[10,2148,2149,2152],{},[2133,2150,2151],{},"How do I compare against the same month last year?","\nSet a period index and use shift(12) on monthly data, or merge the frame against itself on a year-shifted key. The second is safer when months may be missing, because shift assumes a complete series.",[10,2154,2155,2158],{},[2133,2156,2157],{},"Can I do a rolling three-month average?","\nrolling(3).mean(), optionally with min_periods to control what happens at the start of the series. Excel needs an AVERAGE over a relative range dragged down the column, which breaks whenever a row is inserted.",[151,2160,2162],{"id":2161},"related","Related",[2164,2165,2166,2173,2180,2187,2194],"ul",{},[2167,2168,2169,2170,2172],"li",{},"Up one level: ",[14,2171,17],{"href":16}," — the wider function map.",[2167,2174,2175,2179],{},[14,2176,2178],{"href":2177},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas\u002F","Group Excel Rows by Month and Quarter with Pandas"," — building the period column these calculations run over.",[2167,2181,2182,2186],{},[14,2183,2185],{"href":2184},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fparse-excel-dates-into-python-datetimes-with-pandas\u002F","Parse Excel Dates into Python Datetimes with Pandas"," — getting a usable date column in the first place.",[2167,2188,2189,2193],{},[14,2190,2192],{"href":2191},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-line-chart-to-excel-report-with-python\u002F","Add a Line Chart to an Excel Report with Python"," — plotting a rolling average once it is computed.",[2167,2195,2196,2200],{},[14,2197,2199],{"href":2198},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumif-and-sumifs-equivalent-in-pandas\u002F","SUMIF and SUMIFS Equivalent in pandas"," — the conditional totals these build on.",[2202,2203,2204],"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 .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":161,"searchDepth":209,"depth":209,"links":2206},[2207,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218],{"id":153,"depth":209,"text":154},{"id":431,"depth":209,"text":432},{"id":615,"depth":209,"text":616},{"id":1015,"depth":209,"text":1016},{"id":1135,"depth":209,"text":1136},{"id":1338,"depth":209,"text":1339},{"id":1574,"depth":209,"text":1575},{"id":1777,"depth":209,"text":1778},{"id":1884,"depth":209,"text":1885},{"id":2104,"depth":209,"text":2105},{"id":2128,"depth":209,"text":2129},{"id":2161,"depth":209,"text":2162},"2026-09-04","cumsum for running totals, shift and pct_change for period comparisons, rolling for moving averages — and why a year-over-year join beats shift when months go missing.","md",[2223,2225,2227,2229],{"q":2135,"a":2224},"Because cumsum follows the frame's current row order, not a date column. Sort by the date before accumulating, and if the total should restart per group use groupby(key)[col].cumsum().",{"q":2145,"a":2226},"shift(1), which moves a column down by one row. A month-over-month change is col - col.shift(1), and the first row is NaN because there is no previous value — which is the honest answer Excel hides behind a manually blanked cell.",{"q":2151,"a":2228},"Set a period index and use shift(12) on monthly data, or merge the frame against itself on a year-shifted key. The second is safer when months may be missing, because shift assumes a complete series.",{"q":2157,"a":2230},"rolling(3).mean(), optionally with min_periods to control what happens at the start of the series. Excel needs an AVERAGE over a relative range dragged down the column, which breaks whenever a row is inserted.",{"breadcrumb":2232},[2233,2235,2238],{"name":2234,"item":1001},"Home",{"name":2236,"item":2237},"Advanced Data Transformation and Cleaning","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":17,"item":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frunning-totals-and-year-over-year-growth-in-pandas",{"title":5,"description":2241},"Replace dragged relative-reference formulas with cumsum, groupby cumsum for year-to-date, shift and pct_change for growth, and rolling windows with explicit min_periods.","running-totals-and-year-over-year-growth-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frunning-totals-and-year-over-year-growth-in-pandas\u002Findex","how-to","Rnk783ShHHsX_mfPS9EtpQ6EArfW89uBxGEZyYoZhvw",[2247,2251],{"title":2248,"path":2249,"stem":2250,"children":-1},"RANK and PERCENTILE Formulas in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frank-and-percentile-formulas-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frank-and-percentile-formulas-in-pandas\u002Findex",{"title":2199,"path":2252,"stem":2253,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumif-and-sumifs-equivalent-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumif-and-sumifs-equivalent-in-pandas\u002Findex",1788710154434]