[{"data":1,"prerenderedAt":1761},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling":1727,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling":1736,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling":1741,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling":1742},{"id":4,"title":5,"body":6,"dateModified":1692,"datePublished":1692,"description":1693,"extension":1694,"faq":1695,"keyword":1704,"kind":1705,"meta":1706,"navigation":350,"path":1715,"seo":1716,"slug":1719,"stem":1720,"topics":1721,"type":1725,"__hash__":1726},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling\u002Findex.md","Calculate Moving Averages with pandas rolling",{"type":7,"value":8,"toc":1677},"minimark",[9,28,273,278,305,311,315,322,460,475,479,489,533,544,548,564,670,689,803,807,818,892,895,899,906,1010,1031,1293,1297,1300,1373,1382,1386,1389,1505,1517,1521,1573,1577,1593,1597,1612,1616,1622,1628,1634,1640,1644,1673],[10,11,12,13,17,18,21,22,27],"p",{},"Daily figures are noisy: a Saturday dip, a promotion spike, a delivery that landed on Monday instead of Friday. A moving average smooths them into a trend a reader can see. In Excel it is an ",[14,15,16],"code",{},"AVERAGE"," over a range that slides down with the row, or a chart trendline; in pandas it is ",[14,19,20],{},"rolling(...).mean()",". The call is short, but it hides three choices that change the numbers — whether the window counts rows or days, what happens before the window is full, and how the window is anchored — plus one trap when rolling within groups. This page, part of ",[23,24,26],"a",{"href":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002F","Creating Pivot Tables from Excel Data with Pandas",", covers each.",[29,30,38,39,38,43,38,47,38,55,38,64,38,71,38,76,38,80,38,87,38,92,38,96,38,100,38,103,38,107,38,110,38,114,38,117,38,121,38,124,38,130,38,134,38,137,38,140,38,144,38,148,38,152,38,156,38,160,38,164,38,168,38,172,38,176,38,180,38,186,38,192,38,195,38,198,38,201,38,204,38,207,38,210,38,213,38,216,38,219,38,221,38,224,38,227,38,230,38,233,38,235,38,238,38,241,38,244,38,247,38,250,38,253,38,256,38,259,38,262,38,265,38,268,38,270],"svg",{"viewBox":31,"role":32,"ariaLabelledBy":33,"xmlns":36,"style":37},"4 0 748 260","img",[34,35],"ma-chart-t","ma-chart-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[40,41,42],"title",{"id":34},"North daily sales and their 7-day moving average",[44,45,46],"desc",{"id":35},"A line chart of North's daily sales from 1 to 14 March, with dips on weekends such as 90 and 85 on the 6th and 7th, alongside a 7-day trailing moving average. The first six points average the days available so far; from the 7th the full window gives 122.6, rising steadily to 130.4 by the 14th.",[48,49],"rect",{"x":50,"y":51,"width":52,"height":53,"fill":54},"4","0","748","260","#ffffff",[48,56],{"x":57,"y":58,"width":59,"height":59,"rx":60,"fill":61,"stroke":62,"style":63},"64.0","18.0","14.0","3","#e7ebef","var(--line,#cdd5e6)","stroke-width:1.5px",[65,66,70],"text",{"x":67,"y":68,"style":69},"84.0","29.5","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start","daily sales",[48,72],{"x":73,"y":58,"width":59,"height":59,"rx":60,"fill":74,"stroke":75,"style":63},"177.0","#f0f4ff","var(--brand,#5b5cf0)",[65,77,79],{"x":78,"y":68,"style":69},"197.0","7-day average",[81,82],"line",{"x1":83,"y1":84,"x2":85,"y2":84,"stroke":62,"style":86},"53.6","222.0","736.0","stroke-width:1px",[65,88,51],{"x":89,"y":90,"style":91},"30.8","226.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[81,93],{"x1":83,"y1":94,"x2":85,"y2":94,"stroke":62,"style":95},"179.5","stroke-width:1px;stroke-dasharray:5 4",[65,97,99],{"x":89,"y":98,"style":91},"183.5","50",[81,101],{"x1":83,"y1":102,"x2":85,"y2":102,"stroke":62,"style":95},"137.0",[65,104,106],{"x":89,"y":105,"style":91},"141.0","100",[81,108],{"x1":83,"y1":109,"x2":85,"y2":109,"stroke":62,"style":95},"94.5",[65,111,113],{"x":89,"y":112,"style":91},"98.5","150",[81,115],{"x1":83,"y1":116,"x2":85,"y2":116,"stroke":62,"style":95},"52.0",[65,118,120],{"x":89,"y":119,"style":91},"56.0","200",[81,122],{"x1":83,"y1":84,"x2":85,"y2":84,"stroke":123,"style":63},"var(--muted,#5b6780)",[65,125,129],{"x":126,"y":127,"style":128},"78.0","240.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","1",[65,131,133],{"x":132,"y":127,"style":128},"126.7","2",[65,135,60],{"x":136,"y":127,"style":128},"175.5",[65,138,50],{"x":139,"y":127,"style":128},"224.2",[65,141,143],{"x":142,"y":127,"style":128},"273.0","5",[65,145,147],{"x":146,"y":127,"style":128},"321.7","6",[65,149,151],{"x":150,"y":127,"style":128},"370.4","7",[65,153,155],{"x":154,"y":127,"style":128},"419.2","8",[65,157,159],{"x":158,"y":127,"style":128},"467.9","9",[65,161,163],{"x":162,"y":127,"style":128},"516.7","10",[65,165,167],{"x":166,"y":127,"style":128},"565.4","11",[65,169,171],{"x":170,"y":127,"style":128},"614.1","12",[65,173,175],{"x":174,"y":127,"style":128},"662.9","13",[65,177,179],{"x":178,"y":127,"style":128},"711.6","14",[181,182],"path",{"d":183,"fill":184,"stroke":123,"style":185},"M78.0,120.0 L126.7,107.2 L175.5,113.2 L224.2,103.0 L273.0,86.0 L321.7,145.5 L370.4,149.8 L419.2,111.5 L467.9,101.3 L516.7,104.7 L565.4,94.5 L614.1,77.5 L662.9,141.2 L711.6,147.2","none","stroke-width:2.5px;stroke-linejoin:round;stroke-linecap:round",[187,188],"circle",{"cx":126,"cy":189,"r":190,"fill":54,"stroke":123,"style":191},"120.0","4.0","stroke-width:2px",[187,193],{"cx":132,"cy":194,"r":190,"fill":54,"stroke":123,"style":191},"107.2",[187,196],{"cx":136,"cy":197,"r":190,"fill":54,"stroke":123,"style":191},"113.2",[187,199],{"cx":139,"cy":200,"r":190,"fill":54,"stroke":123,"style":191},"103.0",[187,202],{"cx":142,"cy":203,"r":190,"fill":54,"stroke":123,"style":191},"86.0",[187,205],{"cx":146,"cy":206,"r":190,"fill":54,"stroke":123,"style":191},"145.5",[187,208],{"cx":150,"cy":209,"r":190,"fill":54,"stroke":123,"style":191},"149.8",[187,211],{"cx":154,"cy":212,"r":190,"fill":54,"stroke":123,"style":191},"111.5",[187,214],{"cx":158,"cy":215,"r":190,"fill":54,"stroke":123,"style":191},"101.3",[187,217],{"cx":162,"cy":218,"r":190,"fill":54,"stroke":123,"style":191},"104.7",[187,220],{"cx":166,"cy":109,"r":190,"fill":54,"stroke":123,"style":191},[187,222],{"cx":170,"cy":223,"r":190,"fill":54,"stroke":123,"style":191},"77.5",[187,225],{"cx":174,"cy":226,"r":190,"fill":54,"stroke":123,"style":191},"141.2",[187,228],{"cx":178,"cy":229,"r":190,"fill":54,"stroke":123,"style":191},"147.2",[181,231],{"d":232,"fill":184,"stroke":75,"style":185},"M78.0,120.0 L126.7,113.6 L175.5,113.5 L224.2,110.8 L273.0,105.9 L321.7,112.5 L370.4,117.8 L419.2,116.6 L467.9,115.8 L516.7,114.6 L565.4,113.3 L614.1,112.1 L662.9,111.5 L711.6,111.2",[187,234],{"cx":126,"cy":189,"r":190,"fill":54,"stroke":75,"style":191},[187,236],{"cx":132,"cy":237,"r":190,"fill":54,"stroke":75,"style":191},"113.6",[187,239],{"cx":136,"cy":240,"r":190,"fill":54,"stroke":75,"style":191},"113.5",[187,242],{"cx":139,"cy":243,"r":190,"fill":54,"stroke":75,"style":191},"110.8",[187,245],{"cx":142,"cy":246,"r":190,"fill":54,"stroke":75,"style":191},"105.9",[187,248],{"cx":146,"cy":249,"r":190,"fill":54,"stroke":75,"style":191},"112.5",[187,251],{"cx":150,"cy":252,"r":190,"fill":54,"stroke":75,"style":191},"117.8",[187,254],{"cx":154,"cy":255,"r":190,"fill":54,"stroke":75,"style":191},"116.6",[187,257],{"cx":158,"cy":258,"r":190,"fill":54,"stroke":75,"style":191},"115.8",[187,260],{"cx":162,"cy":261,"r":190,"fill":54,"stroke":75,"style":191},"114.6",[187,263],{"cx":166,"cy":264,"r":190,"fill":54,"stroke":75,"style":191},"113.3",[187,266],{"cx":170,"cy":267,"r":190,"fill":54,"stroke":75,"style":191},"112.1",[187,269],{"cx":174,"cy":212,"r":190,"fill":54,"stroke":75,"style":191},[187,271],{"cx":178,"cy":272,"r":190,"fill":54,"stroke":75,"style":191},"111.2",[274,275,277],"h2",{"id":276},"prerequisites","Prerequisites",[279,280,285],"pre",{"className":281,"code":282,"language":283,"meta":284,"style":284},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,286,287],{"__ignoreMap":284},[288,289,291,295,299,302],"span",{"class":81,"line":290},1,[288,292,294],{"class":293},"sMTad","pip",[288,296,298],{"class":297},"srMev"," install",[288,300,301],{"class":297}," pandas",[288,303,304],{"class":297}," openpyxl\n",[10,306,307,310],{},[14,308,309],{},"daily_sales.xlsx"," has two weeks of sales for two stores. North has a row for every day; South only has rows on six of the fourteen days, the typical shape of an export that records only days with activity.",[274,312,314],{"id":313},"step-1-a-trailing-7-day-average-on-complete-data","Step 1 — A trailing 7-day average on complete data",[10,316,317,318,321],{},"For a series with one row per day, ",[14,319,320],{},"rolling(7).mean()"," averages each row with the six before it:",[279,323,327],{"className":324,"code":325,"language":326,"meta":284,"style":284},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nsales = pd.read_excel(\"daily_sales.xlsx\").sort_values([\"store\", \"date\"])\nnorth = sales[sales[\"store\"] == \"North\"].set_index(\"date\")[\"sales\"]\nma = north.rolling(7).mean()\nprint(pd.DataFrame({\"sales\": north, \"ma7\": ma.round(1)}))\n","python",[14,328,329,345,352,382,418,435],{"__ignoreMap":284},[288,330,331,335,339,342],{"class":81,"line":290},[288,332,334],{"class":333},"s-kum","import",[288,336,338],{"class":337},"skGVy"," pandas ",[288,340,341],{"class":333},"as",[288,343,344],{"class":337}," pd\n",[288,346,348],{"class":81,"line":347},2,[288,349,351],{"emptyLinePlaceholder":350},true,"\n",[288,353,355,358,361,364,367,370,373,376,379],{"class":81,"line":354},3,[288,356,357],{"class":337},"sales ",[288,359,360],{"class":333},"=",[288,362,363],{"class":337}," pd.read_excel(",[288,365,366],{"class":297},"\"daily_sales.xlsx\"",[288,368,369],{"class":337},").sort_values([",[288,371,372],{"class":297},"\"store\"",[288,374,375],{"class":337},", ",[288,377,378],{"class":297},"\"date\"",[288,380,381],{"class":337},"])\n",[288,383,385,388,390,393,395,398,401,404,407,409,412,415],{"class":81,"line":384},4,[288,386,387],{"class":337},"north ",[288,389,360],{"class":333},[288,391,392],{"class":337}," sales[sales[",[288,394,372],{"class":297},[288,396,397],{"class":337},"] ",[288,399,400],{"class":333},"==",[288,402,403],{"class":297}," \"North\"",[288,405,406],{"class":337},"].set_index(",[288,408,378],{"class":297},[288,410,411],{"class":337},")[",[288,413,414],{"class":297},"\"sales\"",[288,416,417],{"class":337},"]\n",[288,419,421,424,426,429,432],{"class":81,"line":420},5,[288,422,423],{"class":337},"ma ",[288,425,360],{"class":333},[288,427,428],{"class":337}," north.rolling(",[288,430,151],{"class":431},"sP0c6",[288,433,434],{"class":337},").mean()\n",[288,436,438,441,444,446,449,452,455,457],{"class":81,"line":437},6,[288,439,440],{"class":431},"print",[288,442,443],{"class":337},"(pd.DataFrame({",[288,445,414],{"class":297},[288,447,448],{"class":337},": north, ",[288,450,451],{"class":297},"\"ma7\"",[288,453,454],{"class":337},": ma.round(",[288,456,129],{"class":431},[288,458,459],{"class":337},")}))\n",[10,461,462,463,466,467,470,471,474],{},"The first six values are ",[14,464,465],{},"NaN",", because a seven-row window is not full until the seventh day; from 7 March onwards each value is the average of that day and the six before, the same as ",[14,468,469],{},"=AVERAGE(B2:B8)"," in Excel dragged down the column. Sort by date first — ",[14,472,473],{},"rolling"," uses the rows in the order they are, and an unsorted export produces averages of unrelated days.",[274,476,478],{"id":477},"step-2-decide-what-happens-before-the-window-is-full","Step 2 — Decide what happens before the window is full",[10,480,481,484,485,488],{},[14,482,483],{},"min_periods"," sets how many rows the window needs before it returns a value. With ",[14,486,487],{},"min_periods=1",", the first rows average whatever is available:",[279,490,492],{"className":324,"code":491,"language":326,"meta":284,"style":284},"early = north.rolling(7, min_periods=1).mean()\nprint(early.head(7).round(1).tolist())\n",[14,493,494,516],{"__ignoreMap":284},[288,495,496,499,501,503,505,507,510,512,514],{"class":81,"line":290},[288,497,498],{"class":337},"early ",[288,500,360],{"class":333},[288,502,428],{"class":337},[288,504,151],{"class":431},[288,506,375],{"class":337},[288,508,483],{"class":509},"sa561",[288,511,360],{"class":333},[288,513,129],{"class":431},[288,515,434],{"class":337},[288,517,518,520,523,525,528,530],{"class":81,"line":347},[288,519,440],{"class":431},[288,521,522],{"class":337},"(early.head(",[288,524,151],{"class":431},[288,526,527],{"class":337},").round(",[288,529,129],{"class":431},[288,531,532],{"class":337},").tolist())\n",[10,534,535,536,539,540,543],{},"That removes the gap at the start of a chart, at the cost of early values based on fewer days — the first \"7-day average\" is a single day. If you show them, mark them: a lighter colour in the chart, or a ",[14,537,538],{},"window_days"," column that says how many days each value used. For a dashboard that must show the trend from day one, a sensible compromise is ",[14,541,542],{},"min_periods=4",", which starts once more than half the window is filled.",[274,545,547],{"id":546},"step-3-count-days-not-rows-when-days-are-missing","Step 3 — Count days, not rows, when days are missing",[10,549,550,551,554,555,559,560,563],{},"South has no rows on the quiet days. ",[14,552,553],{},"rolling(7)"," still takes seven ",[556,557,558],"em",{},"rows",", which for South spans far more than a week; ",[14,561,562],{},"rolling(\"7D\")"," on a date index takes the rows whose dates fall within the last seven days:",[279,565,567],{"className":324,"code":566,"language":326,"meta":284,"style":284},"south = sales[sales[\"store\"] == \"South\"].set_index(\"date\")[\"sales\"]\ncompare = pd.DataFrame({\n    \"sales\": south,\n    \"rows_7\": south.rolling(7, min_periods=1).mean().round(1),\n    \"days_7D\": south.rolling(\"7D\").mean().round(1),\n})\nprint(compare)\n",[14,568,569,597,607,615,641,657,662],{"__ignoreMap":284},[288,570,571,574,576,578,580,582,584,587,589,591,593,595],{"class":81,"line":290},[288,572,573],{"class":337},"south ",[288,575,360],{"class":333},[288,577,392],{"class":337},[288,579,372],{"class":297},[288,581,397],{"class":337},[288,583,400],{"class":333},[288,585,586],{"class":297}," \"South\"",[288,588,406],{"class":337},[288,590,378],{"class":297},[288,592,411],{"class":337},[288,594,414],{"class":297},[288,596,417],{"class":337},[288,598,599,602,604],{"class":81,"line":347},[288,600,601],{"class":337},"compare ",[288,603,360],{"class":333},[288,605,606],{"class":337}," pd.DataFrame({\n",[288,608,609,612],{"class":81,"line":354},[288,610,611],{"class":297},"    \"sales\"",[288,613,614],{"class":337},": south,\n",[288,616,617,620,623,625,627,629,631,633,636,638],{"class":81,"line":384},[288,618,619],{"class":297},"    \"rows_7\"",[288,621,622],{"class":337},": south.rolling(",[288,624,151],{"class":431},[288,626,375],{"class":337},[288,628,483],{"class":509},[288,630,360],{"class":333},[288,632,129],{"class":431},[288,634,635],{"class":337},").mean().round(",[288,637,129],{"class":431},[288,639,640],{"class":337},"),\n",[288,642,643,646,648,651,653,655],{"class":81,"line":420},[288,644,645],{"class":297},"    \"days_7D\"",[288,647,622],{"class":337},[288,649,650],{"class":297},"\"7D\"",[288,652,635],{"class":337},[288,654,129],{"class":431},[288,656,640],{"class":337},[288,658,659],{"class":81,"line":437},[288,660,661],{"class":337},"})\n",[288,663,665,667],{"class":81,"line":664},7,[288,666,440],{"class":431},[288,668,669],{"class":337},"(compare)\n",[10,671,672,673,676,677,680,681,683,684,688],{},"On 12 March, ",[14,674,675],{},"rows_7"," averages every row back to 1 March — twelve days of data — while ",[14,678,679],{},"days_7D"," averages only the rows from 6 to 12 March. Neither is a true seven-day average of ",[556,682,70],{},", though, because missing days are really zero-sales days. When a missing row means \"nothing happened\", fill the missing dates with zero first and then roll; when it means \"no data\", the time-based window over the rows that exist is the honest option. ",[23,685,687],{"href":686},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffill-missing-dates-in-a-time-series-with-pandas\u002F","Fill Missing Dates in a Time Series from Excel with pandas"," shows the filling step.",[29,690,38,696,38,699,38,702,38,706,38,712,38,717,38,723,38,726,38,730,38,733,38,737,38,740,38,744,38,746,38,750,38,752,38,755,38,757,38,760,38,763,38,766,38,768,38,772,38,774,38,777,38,779,38,781,38,784,38,788,38,790,38,794,38,796,38,798,38,800],{"viewBox":691,"role":32,"ariaLabelledBy":692,"xmlns":36,"style":695},"4 0 752 174",[693,694],"ma-windows-t","ma-windows-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,697,698],{"id":693},"What each window averages for South on 12 March",[44,700,701],{"id":694},"A comparison of three ways to compute South's 7-day average on 12 March. rolling(7) averages the last seven rows, which reach back to 1 March. rolling(\"7D\") averages the three rows dated 6 to 12 March. Filling missing dates with zero and then using rolling(7) averages seven days including four zero days.",[48,703],{"x":50,"y":51,"width":704,"height":705,"fill":54},"752","174",[48,707],{"x":708,"y":709,"width":710,"height":711,"rx":51,"fill":54,"stroke":62,"style":86},"20.0","16.0","153.5","40.0",[48,713],{"x":714,"y":709,"width":715,"height":711,"rx":51,"fill":74,"stroke":75,"style":716},"173.5","188.8","stroke-width:1.2px",[65,718,722],{"x":719,"y":720,"style":721},"267.9","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Rows used",[48,724],{"x":725,"y":709,"width":715,"height":711,"rx":51,"fill":74,"stroke":75,"style":716},"362.3",[65,727,729],{"x":728,"y":720,"style":721},"456.8","Period covered",[48,731],{"x":732,"y":709,"width":715,"height":711,"rx":51,"fill":74,"stroke":75,"style":716},"551.2",[65,734,736],{"x":735,"y":720,"style":721},"645.6","Treats missing days as",[48,738],{"x":708,"y":119,"width":710,"height":739,"rx":51,"fill":61,"stroke":62,"style":86},"34.0",[65,741,553],{"x":742,"y":126,"style":743},"32.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[48,745],{"x":714,"y":119,"width":715,"height":739,"rx":51,"fill":54,"stroke":62,"style":86},[65,747,749],{"x":719,"y":748,"style":128},"77.0","all 6 rows",[48,751],{"x":725,"y":119,"width":715,"height":739,"rx":51,"fill":54,"stroke":62,"style":86},[65,753,754],{"x":728,"y":748,"style":128},"1–12 Mar",[48,756],{"x":732,"y":119,"width":715,"height":739,"rx":51,"fill":54,"stroke":62,"style":86},[65,758,759],{"x":735,"y":748,"style":128},"ignored",[48,761],{"x":708,"y":762,"width":710,"height":739,"rx":51,"fill":61,"stroke":62,"style":86},"90.0",[65,764,562],{"x":742,"y":765,"style":743},"112.0",[48,767],{"x":714,"y":762,"width":715,"height":739,"rx":51,"fill":54,"stroke":62,"style":86},[65,769,771],{"x":719,"y":770,"style":128},"111.0","3 rows",[48,773],{"x":725,"y":762,"width":715,"height":739,"rx":51,"fill":54,"stroke":62,"style":86},[65,775,776],{"x":728,"y":770,"style":128},"6–12 Mar",[48,778],{"x":732,"y":762,"width":715,"height":739,"rx":51,"fill":54,"stroke":62,"style":86},[65,780,759],{"x":735,"y":770,"style":128},[48,782],{"x":708,"y":783,"width":710,"height":739,"rx":51,"fill":61,"stroke":62,"style":86},"124.0",[65,785,787],{"x":742,"y":786,"style":743},"146.0","fill 0, then rolling(7)",[48,789],{"x":714,"y":783,"width":715,"height":739,"rx":51,"fill":54,"stroke":62,"style":86},[65,791,793],{"x":719,"y":792,"style":128},"145.0","7 days",[48,795],{"x":725,"y":783,"width":715,"height":739,"rx":51,"fill":54,"stroke":62,"style":86},[65,797,776],{"x":728,"y":792,"style":128},[48,799],{"x":732,"y":783,"width":715,"height":739,"rx":51,"fill":54,"stroke":62,"style":86},[65,801,802],{"x":735,"y":792,"style":128},"zero sales",[274,804,806],{"id":805},"step-4-trailing-or-centred","Step 4 — Trailing or centred",[10,808,809,810,813,814,817],{},"A trailing window, the default, averages each day with the days ",[556,811,812],{},"before"," it, so the average lags the data: a rise shows up in the average a few days late. ",[14,815,816],{},"center=True"," averages days on both sides, which lines up with the data and is better for analysing past trends:",[279,819,821],{"className":324,"code":820,"language":326,"meta":284,"style":284},"both = pd.DataFrame({\"sales\": north,\n                     \"trailing\": north.rolling(7).mean().round(1),\n                     \"centred\": north.rolling(7, center=True).mean().round(1)})\nprint(both.tail(5))\n",[14,822,823,838,854,880],{"__ignoreMap":284},[288,824,825,828,830,833,835],{"class":81,"line":290},[288,826,827],{"class":337},"both ",[288,829,360],{"class":333},[288,831,832],{"class":337}," pd.DataFrame({",[288,834,414],{"class":297},[288,836,837],{"class":337},": north,\n",[288,839,840,843,846,848,850,852],{"class":81,"line":347},[288,841,842],{"class":297},"                     \"trailing\"",[288,844,845],{"class":337},": north.rolling(",[288,847,151],{"class":431},[288,849,635],{"class":337},[288,851,129],{"class":431},[288,853,640],{"class":337},[288,855,856,859,861,863,865,868,870,873,875,877],{"class":81,"line":354},[288,857,858],{"class":297},"                     \"centred\"",[288,860,845],{"class":337},[288,862,151],{"class":431},[288,864,375],{"class":337},[288,866,867],{"class":509},"center",[288,869,360],{"class":333},[288,871,872],{"class":431},"True",[288,874,635],{"class":337},[288,876,129],{"class":431},[288,878,879],{"class":337},")})\n",[288,881,882,884,887,889],{"class":81,"line":384},[288,883,440],{"class":431},[288,885,886],{"class":337},"(both.tail(",[288,888,143],{"class":431},[288,890,891],{"class":337},"))\n",[10,893,894],{},"The centred average has no value for the last three days — it needs days that have not happened yet. That is why dashboards showing today use trailing averages, and why a centred chart line stops short of the right-hand edge. Excel's chart trendline \"Moving Average\" is a trailing average, which matters if you are matching a chart someone built by hand.",[274,896,898],{"id":897},"step-5-roll-within-each-group","Step 5 — Roll within each group",[10,900,901,902,905],{},"With several stores in one table, each store needs its own window: a moving average that runs across the boundary from North's last day into South's first mixes the two. Roll within groups, and keep the result aligned with the original rows by using ",[14,903,904],{},"transform",":",[279,907,909],{"className":324,"code":908,"language":326,"meta":284,"style":284},"sales = sales.sort_values([\"store\", \"date\"])\nsales[\"ma7\"] = (sales.groupby(\"store\")[\"sales\"]\n                     .transform(lambda s: s.rolling(7, min_periods=4).mean())\n                     .round(1))\nprint(sales.groupby(\"store\").tail(3).to_string(index=False))\n",[14,910,911,928,950,974,983],{"__ignoreMap":284},[288,912,913,915,917,920,922,924,926],{"class":81,"line":290},[288,914,357],{"class":337},[288,916,360],{"class":333},[288,918,919],{"class":337}," sales.sort_values([",[288,921,372],{"class":297},[288,923,375],{"class":337},[288,925,378],{"class":297},[288,927,381],{"class":337},[288,929,930,933,935,937,939,942,944,946,948],{"class":81,"line":347},[288,931,932],{"class":337},"sales[",[288,934,451],{"class":297},[288,936,397],{"class":337},[288,938,360],{"class":333},[288,940,941],{"class":337}," (sales.groupby(",[288,943,372],{"class":297},[288,945,411],{"class":337},[288,947,414],{"class":297},[288,949,417],{"class":337},[288,951,952,955,958,961,963,965,967,969,971],{"class":81,"line":354},[288,953,954],{"class":337},"                     .transform(",[288,956,957],{"class":333},"lambda",[288,959,960],{"class":337}," s: s.rolling(",[288,962,151],{"class":431},[288,964,375],{"class":337},[288,966,483],{"class":509},[288,968,360],{"class":333},[288,970,50],{"class":431},[288,972,973],{"class":337},").mean())\n",[288,975,976,979,981],{"class":81,"line":384},[288,977,978],{"class":337},"                     .round(",[288,980,129],{"class":431},[288,982,891],{"class":337},[288,984,985,987,990,992,995,997,1000,1003,1005,1008],{"class":81,"line":420},[288,986,440],{"class":431},[288,988,989],{"class":337},"(sales.groupby(",[288,991,372],{"class":297},[288,993,994],{"class":337},").tail(",[288,996,60],{"class":431},[288,998,999],{"class":337},").to_string(",[288,1001,1002],{"class":509},"index",[288,1004,360],{"class":333},[288,1006,1007],{"class":431},"False",[288,1009,891],{"class":337},[10,1011,1012,1014,1015,1018,1019,1022,1023,1026,1027,1030],{},[14,1013,904],{}," returns a Series indexed like ",[14,1016,1017],{},"sales",", so it can be assigned straight back as a column. The more obvious ",[14,1020,1021],{},"sales.groupby(\"store\")[\"sales\"].rolling(7).mean()"," returns a result indexed by store ",[556,1024,1025],{},"and"," original row number, which cannot be assigned back without resetting the index — a common source of \"cannot reindex\" errors and of values landing on the wrong rows. For time-based windows per group, set the date as the index inside the lambda: ",[14,1028,1029],{},"lambda s: s.set_axis(dates.loc[s.index]).rolling(\"7D\").mean().values",", or roll each store separately.",[29,1032,38,1038,38,1041,38,1044,38,1049,38,1055,38,1060,38,1064,38,1070,38,1074,38,1078,38,1082,38,1086,38,1090,38,1094,38,1098,38,1102,38,1104,38,1108,38,1110,38,1113,38,1115,38,1117,38,1119,38,1122,38,1125,38,1128,38,1130,38,1134,38,1136,38,1139,38,1141,38,1144,38,1146,38,1149,38,1152,38,1155,38,1157,38,1160,38,1162,38,1164,38,1166,38,1169,38,1171,38,1174,38,1177,38,1180,38,1182,38,1185,38,1187,38,1189,38,1191,38,1194,38,1196,38,1199,38,1202,38,1205,38,1209,38,1213,38,1215,38,1218,38,1220,38,1223,38,1225,38,1228,38,1231,38,1233,38,1236,38,1238,38,1240,38,1242,38,1245,38,1247,38,1249,38,1252,38,1254,38,1257,38,1259,38,1261,38,1263,38,1266,38,1268,38,1271,38,1274,38,1276,38,1279,38,1281,38,1283,38,1285,38,1288,38,1290],{"viewBox":1033,"role":32,"ariaLabelledBy":1034,"xmlns":36,"style":1037},"216 0 328 312",[1035,1036],"ma-groups-t","ma-groups-d","width:100%;max-width:328px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,1039,1040],{"id":1035},"Moving averages computed separately for each store",[44,1042,1043],{"id":1036},"The sales table sorted by store and date with a ma7 column. Each store's moving average uses only its own rows: North's values continue its own series, and South's first values are empty until four South rows exist, instead of borrowing North's last days.",[48,1045],{"x":1046,"y":51,"width":1047,"height":1048,"fill":54},"216","328","312",[65,1050,1054],{"x":1051,"y":1052,"style":1053},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","groupby('store').transform(rolling mean)",[48,1056],{"x":1057,"y":1058,"width":742,"height":1059,"rx":51,"fill":61,"stroke":62,"style":86},"242.4","48.0","24.0",[48,1061],{"x":1062,"y":1058,"width":1063,"height":1059,"rx":51,"fill":61,"stroke":62,"style":86},"274.4","67.8",[65,1065,1069],{"x":1066,"y":1067,"style":1068},"308.3","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[48,1071],{"x":1072,"y":1058,"width":1073,"height":1059,"rx":51,"fill":61,"stroke":62,"style":86},"342.2","60.3",[65,1075,1077],{"x":1076,"y":1067,"style":1068},"372.4","B",[48,1079],{"x":1080,"y":1058,"width":1081,"height":1059,"rx":51,"fill":61,"stroke":62,"style":86},"402.5","56.4",[65,1083,1085],{"x":1084,"y":1067,"style":1068},"430.7","C",[48,1087],{"x":1088,"y":1058,"width":1089,"height":1059,"rx":51,"fill":61,"stroke":62,"style":86},"458.9","58.8",[65,1091,1093],{"x":1092,"y":1067,"style":1068},"488.2","D",[48,1095],{"x":1057,"y":1096,"width":742,"height":1097,"rx":51,"fill":61,"stroke":62,"style":86},"72.0","28.0",[65,1099,129],{"x":1100,"y":1101,"style":1068},"258.4","91.0",[48,1103],{"x":1062,"y":1096,"width":1063,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1105,1107],{"x":1066,"y":1101,"style":1106},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","date",[48,1109],{"x":1072,"y":1096,"width":1073,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1111,1112],{"x":1076,"y":1101,"style":1106},"store",[48,1114],{"x":1080,"y":1096,"width":1081,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1116,1017],{"x":1084,"y":1101,"style":1106},[48,1118],{"x":1088,"y":1096,"width":1089,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1120,1121],{"x":1092,"y":1101,"style":1106},"ma7",[48,1123],{"x":1057,"y":1124,"width":742,"height":1097,"rx":51,"fill":61,"stroke":62,"style":86},"100.0",[65,1126,133],{"x":1100,"y":1127,"style":1068},"119.0",[48,1129],{"x":1062,"y":1124,"width":1063,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1131,1133],{"x":1066,"y":1127,"style":1132},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Mar 12",[48,1135],{"x":1072,"y":1124,"width":1073,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1137,1138],{"x":1076,"y":1127,"style":1132},"North",[48,1140],{"x":1080,"y":1124,"width":1081,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1142,1143],{"x":1084,"y":1127,"style":1132},"170",[48,1145],{"x":1088,"y":1124,"width":1089,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1147,1148],{"x":1092,"y":1127,"style":1132},"129.3",[48,1150],{"x":1057,"y":1151,"width":742,"height":1097,"rx":51,"fill":61,"stroke":62,"style":86},"128.0",[65,1153,60],{"x":1100,"y":1154,"style":1068},"147.0",[48,1156],{"x":1062,"y":1151,"width":1063,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1158,1159],{"x":1066,"y":1154,"style":1132},"Mar 13",[48,1161],{"x":1072,"y":1151,"width":1073,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1163,1138],{"x":1076,"y":1154,"style":1132},[48,1165],{"x":1080,"y":1151,"width":1081,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1167,1168],{"x":1084,"y":1154,"style":1132},"95",[48,1170],{"x":1088,"y":1151,"width":1089,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1172,1173],{"x":1092,"y":1154,"style":1132},"130",[48,1175],{"x":1057,"y":1176,"width":742,"height":1097,"rx":51,"fill":61,"stroke":62,"style":86},"156.0",[65,1178,50],{"x":1100,"y":1179,"style":1068},"175.0",[48,1181],{"x":1062,"y":1176,"width":1063,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1183,1184],{"x":1066,"y":1179,"style":1132},"Mar 14",[48,1186],{"x":1072,"y":1176,"width":1073,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1188,1138],{"x":1076,"y":1179,"style":1132},[48,1190],{"x":1080,"y":1176,"width":1081,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1192,1193],{"x":1084,"y":1179,"style":1132},"88",[48,1195],{"x":1088,"y":1176,"width":1089,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1197,1198],{"x":1092,"y":1179,"style":1132},"130.4",[48,1200],{"x":1057,"y":1201,"width":742,"height":1097,"rx":51,"fill":61,"stroke":62,"style":86},"184.0",[65,1203,143],{"x":1100,"y":1204,"style":1068},"203.0",[48,1206],{"x":1062,"y":1201,"width":1063,"height":1097,"rx":51,"fill":1207,"stroke":1208,"style":63},"#d9f4f1","var(--teal,#0f9488)",[65,1210,1212],{"x":1066,"y":1204,"style":1211},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Mar 01",[48,1214],{"x":1072,"y":1201,"width":1073,"height":1097,"rx":51,"fill":1207,"stroke":1208,"style":63},[65,1216,1217],{"x":1076,"y":1204,"style":1211},"South",[48,1219],{"x":1080,"y":1201,"width":1081,"height":1097,"rx":51,"fill":1207,"stroke":1208,"style":63},[65,1221,1222],{"x":1084,"y":1204,"style":1211},"60",[48,1224],{"x":1088,"y":1201,"width":1089,"height":1097,"rx":51,"fill":1207,"stroke":1208,"style":63},[48,1226],{"x":1057,"y":1227,"width":742,"height":1097,"rx":51,"fill":61,"stroke":62,"style":86},"212.0",[65,1229,147],{"x":1100,"y":1230,"style":1068},"231.0",[48,1232],{"x":1062,"y":1227,"width":1063,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1234,1235],{"x":1066,"y":1230,"style":1132},"Mar 02",[48,1237],{"x":1072,"y":1227,"width":1073,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1239,1217],{"x":1076,"y":1230,"style":1132},[48,1241],{"x":1080,"y":1227,"width":1081,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1243,1244],{"x":1084,"y":1230,"style":1132},"64",[48,1246],{"x":1088,"y":1227,"width":1089,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[48,1248],{"x":1057,"y":127,"width":742,"height":1097,"rx":51,"fill":61,"stroke":62,"style":86},[65,1250,151],{"x":1100,"y":1251,"style":1068},"259.0",[48,1253],{"x":1062,"y":127,"width":1063,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1255,1256],{"x":1066,"y":1251,"style":1132},"Mar 05",[48,1258],{"x":1072,"y":127,"width":1073,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1260,1217],{"x":1076,"y":1251,"style":1132},[48,1262],{"x":1080,"y":127,"width":1081,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1264,1265],{"x":1084,"y":1251,"style":1132},"70",[48,1267],{"x":1088,"y":127,"width":1089,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[48,1269],{"x":1057,"y":1270,"width":742,"height":1097,"rx":51,"fill":61,"stroke":62,"style":86},"268.0",[65,1272,155],{"x":1100,"y":1273,"style":1068},"287.0",[48,1275],{"x":1062,"y":1270,"width":1063,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1277,1278],{"x":1066,"y":1273,"style":1132},"Mar 06",[48,1280],{"x":1072,"y":1270,"width":1073,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1282,1217],{"x":1076,"y":1273,"style":1132},[48,1284],{"x":1080,"y":1270,"width":1081,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1286,1287],{"x":1084,"y":1273,"style":1132},"75",[48,1289],{"x":1088,"y":1270,"width":1089,"height":1097,"rx":51,"fill":54,"stroke":62,"style":86},[65,1291,1292],{"x":1092,"y":1273,"style":1132},"67.2",[274,1294,1296],{"id":1295},"exponential-averages-react-faster","Exponential averages react faster",[10,1298,1299],{},"A simple moving average gives every day in the window the same weight, so a spike stays in the average for exactly seven days and then drops out abruptly. An exponentially weighted average gives recent days more weight and older days progressively less, which reacts faster to real changes and has no cliff edge:",[279,1301,1303],{"className":324,"code":1302,"language":326,"meta":284,"style":284},"smooth = pd.DataFrame({\"sales\": north,\n                       \"sma7\": north.rolling(7).mean().round(1),\n                       \"ewm7\": north.ewm(span=7, adjust=False).mean().round(1)})\nprint(smooth.tail(4))\n",[14,1304,1305,1318,1333,1362],{"__ignoreMap":284},[288,1306,1307,1310,1312,1314,1316],{"class":81,"line":290},[288,1308,1309],{"class":337},"smooth ",[288,1311,360],{"class":333},[288,1313,832],{"class":337},[288,1315,414],{"class":297},[288,1317,837],{"class":337},[288,1319,1320,1323,1325,1327,1329,1331],{"class":81,"line":347},[288,1321,1322],{"class":297},"                       \"sma7\"",[288,1324,845],{"class":337},[288,1326,151],{"class":431},[288,1328,635],{"class":337},[288,1330,129],{"class":431},[288,1332,640],{"class":337},[288,1334,1335,1338,1341,1343,1345,1347,1349,1352,1354,1356,1358,1360],{"class":81,"line":354},[288,1336,1337],{"class":297},"                       \"ewm7\"",[288,1339,1340],{"class":337},": north.ewm(",[288,1342,288],{"class":509},[288,1344,360],{"class":333},[288,1346,151],{"class":431},[288,1348,375],{"class":337},[288,1350,1351],{"class":509},"adjust",[288,1353,360],{"class":333},[288,1355,1007],{"class":431},[288,1357,635],{"class":337},[288,1359,129],{"class":431},[288,1361,879],{"class":337},[288,1363,1364,1366,1369,1371],{"class":81,"line":384},[288,1365,440],{"class":431},[288,1367,1368],{"class":337},"(smooth.tail(",[288,1370,50],{"class":431},[288,1372,891],{"class":337},[10,1374,1375,1378,1379,1381],{},[14,1376,1377],{},"span=7"," gives a weighting comparable to a seven-day window. The exponential version follows the weekend dip on 13 and 14 March more closely than the simple average, which is still dominated by the strong midweek days. Use it for alerting and short-term monitoring; keep the simple average where readers need to reproduce the figure with ",[14,1380,16],{}," in Excel.",[274,1383,1385],{"id":1384},"step-6-write-the-average-next-to-the-data","Step 6 — Write the average next to the data",[10,1387,1388],{},"Write the moving average as a column beside the daily figures, so a chart in the workbook can plot both:",[279,1390,1392],{"className":324,"code":1391,"language":326,"meta":284,"style":284},"out = sales[[\"date\", \"store\", \"sales\", \"ma7\"]]\nwith pd.ExcelWriter(\"sales_ma.xlsx\", engine=\"xlsxwriter\", datetime_format=\"yyyy-mm-dd\") as writer:\n    out.to_excel(writer, index=False, sheet_name=\"Daily\")\nprint(pd.read_excel(\"sales_ma.xlsx\")[\"ma7\"].notna().sum(), \"rows with a moving average\")\n",[14,1393,1394,1421,1460,1484],{"__ignoreMap":284},[288,1395,1396,1399,1401,1404,1406,1408,1410,1412,1414,1416,1418],{"class":81,"line":290},[288,1397,1398],{"class":337},"out ",[288,1400,360],{"class":333},[288,1402,1403],{"class":337}," sales[[",[288,1405,378],{"class":297},[288,1407,375],{"class":337},[288,1409,372],{"class":297},[288,1411,375],{"class":337},[288,1413,414],{"class":297},[288,1415,375],{"class":337},[288,1417,451],{"class":297},[288,1419,1420],{"class":337},"]]\n",[288,1422,1423,1426,1429,1432,1434,1437,1439,1442,1444,1447,1449,1452,1455,1457],{"class":81,"line":347},[288,1424,1425],{"class":333},"with",[288,1427,1428],{"class":337}," pd.ExcelWriter(",[288,1430,1431],{"class":297},"\"sales_ma.xlsx\"",[288,1433,375],{"class":337},[288,1435,1436],{"class":509},"engine",[288,1438,360],{"class":333},[288,1440,1441],{"class":297},"\"xlsxwriter\"",[288,1443,375],{"class":337},[288,1445,1446],{"class":509},"datetime_format",[288,1448,360],{"class":333},[288,1450,1451],{"class":297},"\"yyyy-mm-dd\"",[288,1453,1454],{"class":337},") ",[288,1456,341],{"class":333},[288,1458,1459],{"class":337}," writer:\n",[288,1461,1462,1465,1467,1469,1471,1473,1476,1478,1481],{"class":81,"line":354},[288,1463,1464],{"class":337},"    out.to_excel(writer, ",[288,1466,1002],{"class":509},[288,1468,360],{"class":333},[288,1470,1007],{"class":431},[288,1472,375],{"class":337},[288,1474,1475],{"class":509},"sheet_name",[288,1477,360],{"class":333},[288,1479,1480],{"class":297},"\"Daily\"",[288,1482,1483],{"class":337},")\n",[288,1485,1486,1488,1491,1493,1495,1497,1500,1503],{"class":81,"line":384},[288,1487,440],{"class":431},[288,1489,1490],{"class":337},"(pd.read_excel(",[288,1492,1431],{"class":297},[288,1494,411],{"class":337},[288,1496,451],{"class":297},[288,1498,1499],{"class":337},"].notna().sum(), ",[288,1501,1502],{"class":297},"\"rows with a moving average\"",[288,1504,1483],{"class":337},[10,1506,1507,1508,1511,1512,1516],{},"If readers will add days themselves, a formula column keeps working where a static number would not: ",[14,1509,1510],{},"=IF(COUNT(C2:C8)=7, AVERAGE(C2:C8), \"\")"," in row 8, filled down within each store's block. ",[23,1513,1515],{"href":1514},"\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"," builds the chart.",[274,1518,1520],{"id":1519},"common-pitfalls","Common pitfalls",[1522,1523,1524,1532,1544,1554,1564],"ul",{},[1525,1526,1527,1531],"li",{},[1528,1529,1530],"strong",{},"Rolling unsorted data."," Sort by date (and group) first.",[1525,1533,1534,1537,1538,1540,1541,1543],{},[1528,1535,1536],{},"Row windows over gappy data."," ",[14,1539,553],{}," on six sparse rows spans weeks; use ",[14,1542,650],{}," or fill the dates.",[1525,1545,1546,1549,1550,1553],{},[1528,1547,1548],{},"Rolling across groups."," Use ",[14,1551,1552],{},"groupby().transform"," so each store has its own window.",[1525,1555,1556,1563],{},[1528,1557,1558,1559,1562],{},"Assigning ",[14,1560,1561],{},"groupby().rolling()"," directly."," Its MultiIndex does not line up with the frame.",[1525,1565,1566,1572],{},[1528,1567,1568,1569,1571],{},"Early values from ",[14,1570,487],{},"."," They are one-day \"averages\"; mark them or start later.",[274,1574,1576],{"id":1575},"performance-notes","Performance notes",[10,1578,1579,1582,1583,1585,1586,1588,1589,1592],{},[14,1580,1581],{},"rolling().mean()"," is implemented in compiled code and handles millions of rows quickly. The ",[14,1584,904],{}," with a lambda calls Python once per group, which is fine for hundreds of groups; for tens of thousands, ",[14,1587,1561],{}," followed by ",[14,1590,1591],{},"reset_index(level=0, drop=True)"," avoids the per-group Python call and aligns the result by the original row index.",[274,1594,1596],{"id":1595},"conclusion","Conclusion",[10,1598,1599,1600,1602,1603,1605,1606,1608,1609,1611],{},"Sort by date, then ",[14,1601,320],{}," gives a trailing seven-row average — correct when there is one row per day. Use ",[14,1604,562],{}," or fill missing dates first when days are absent, set ",[14,1607,483],{}," deliberately and mark partial windows, choose trailing for dashboards and centred for analysis, and roll within groups with ",[14,1610,1552],{}," so every store gets its own window and the result lines up with its rows.",[274,1613,1615],{"id":1614},"frequently-asked-questions","Frequently asked questions",[10,1617,1618,1621],{},[1528,1619,1620],{},"What is the difference between rolling(7) and rolling(\"7D\")?","\nrolling(7) averages the last seven rows, whatever dates they cover. rolling(\"7D\") averages the rows whose dates fall in the last seven days. With one row per day they agree; when days are missing, rolling(7) stretches over a longer period and rolling(\"7D\") averages fewer rows.",[10,1623,1624,1627],{},[1528,1625,1626],{},"Why are the first six values NaN?","\nA seven-row window is not full until the seventh row, and by default rolling returns NaN until it is. Pass min_periods=1 (or another minimum) to average whatever is available, and label those early values as partial.",[10,1629,1630,1633],{},[1528,1631,1632],{},"How do I calculate a moving average per store?","\nSort by store and date, group by store, and apply the rolling mean inside transform with a lambda, as shown in Step 5. transform keeps the result aligned with the original rows, so it can be assigned straight back as a column.",[10,1635,1636,1639],{},[1528,1637,1638],{},"Is a centred moving average better?","\nIt lines up better with the data for analysis of past trends, because each point averages days on both sides. It cannot be computed for the most recent days, so dashboards that must show today usually use a trailing average.",[274,1641,1643],{"id":1642},"related","Related",[1522,1645,1646,1652,1659,1666],{},[1525,1647,1648,1649,1651],{},"Up one level: ",[23,1650,26],{"href":25}," — pivots and summaries.",[1525,1653,1654,1658],{},[23,1655,1657],{"href":1656},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frunning-totals-and-year-over-year-growth-in-pandas\u002F","Running Totals and Year-Over-Year Growth in pandas"," — cumulative and comparative windows.",[1525,1660,1661,1665],{},[23,1662,1664],{"href":1663},"\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"," — calendar aggregation instead of smoothing.",[1525,1667,1668,1672],{},[23,1669,1671],{"href":1670},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-a-trendline-to-an-excel-chart-with-python\u002F","Add a Trendline to an Excel Chart with Python"," — letting the chart compute the smoothing.",[1674,1675,1676],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":284,"searchDepth":347,"depth":347,"links":1678},[1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691],{"id":276,"depth":347,"text":277},{"id":313,"depth":347,"text":314},{"id":477,"depth":347,"text":478},{"id":546,"depth":347,"text":547},{"id":805,"depth":347,"text":806},{"id":897,"depth":347,"text":898},{"id":1295,"depth":347,"text":1296},{"id":1384,"depth":347,"text":1385},{"id":1519,"depth":347,"text":1520},{"id":1575,"depth":347,"text":1576},{"id":1595,"depth":347,"text":1596},{"id":1614,"depth":347,"text":1615},{"id":1642,"depth":347,"text":1643},"2026-09-25","Smooth daily sales with a 7-day moving average, per store, the way an Excel AVERAGE over a sliding range does. Learn why rolling(7) and rolling(\"7D\") differ when days are missing, what min_periods does to the first rows, trailing versus centred windows, and how to roll within groups without scrambling rows.","md",[1696,1698,1700,1702],{"q":1620,"a":1697},"rolling(7) averages the last seven rows, whatever dates they cover. rolling(\"7D\") averages the rows whose dates fall in the last seven days. With one row per day they agree; when days are missing, rolling(7) stretches over a longer period and rolling(\"7D\") averages fewer rows.",{"q":1626,"a":1699},"A seven-row window is not full until the seventh row, and by default rolling returns NaN until it is. Pass min_periods=1 (or another minimum) to average whatever is available, and label those early values as partial.",{"q":1632,"a":1701},"Sort by store and date, group by store, and apply the rolling mean inside transform with a lambda, as shown in Step 5. transform keeps the result aligned with the original rows, so it can be assigned straight back as a column.",{"q":1638,"a":1703},"It lines up better with the data for analysis of past trends, because each point averages days on both sides. It cannot be computed for the most recent days, so dashboards that must show today usually use a trailing average.","moving average rolling window pandas",null,{"breadcrumb":1707},[1708,1711,1714],{"name":1709,"item":1710},"Home","\u002F",{"name":1712,"item":1713},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":26,"item":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling",{"title":1717,"description":1718},"Moving Average in pandas: rolling(7) vs rolling(\"7D\"), per Group","Compute moving averages of Excel data with pandas rolling: row-count versus time-based windows with missing days, min_periods, centred windows, groupby rolling per store, and writing the result next to the data.","calculate-moving-averages-with-pandas-rolling","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling\u002Findex",[1722,1723,1724],"pivots-and-summaries","dates-and-times","pandas","how-to","27Zn52ntBKl_DpFr_7QFs_A5ZxgdLFINChDp4cC4sHk",[1728,1732],{"title":1729,"path":1730,"stem":1731,"children":-1},"Calculate Customer Lifetime Value from Order History","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-customer-lifetime-value-from-order-history","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-customer-lifetime-value-from-order-history\u002Findex",{"title":1733,"path":1734,"stem":1735,"children":-1},"Calculate Percent of Total per Group in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas\u002Findex",[1737,1739],{"path":1738,"title":26},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data",{"path":1740,"title":1712},"\u002Fadvanced-data-transformation-and-cleaning",[],[1743,1746,1749,1752,1755,1758],{"path":1744,"title":1745,"kind":1725},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data","Build a Customer Churn Report from Subscription Data",{"path":1747,"title":1748,"kind":1725},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Frefresh-pivot-tables-and-queries-in-excel-with-python","Refresh Excel Pivot Tables and Queries with Python",{"path":1750,"title":1751,"kind":1725},"\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":1753,"title":1754,"kind":1725},"\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":1756,"title":1757,"kind":1725},"\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":1759,"title":1760,"kind":1725},"\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",1790972086335]