[{"data":1,"prerenderedAt":2112},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-forecast-with-exponential-smoothing":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-forecast-with-exponential-smoothing":2077,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-forecast-with-exponential-smoothing":2086,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-forecast-with-exponential-smoothing":2091,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-forecast-with-exponential-smoothing":2092},{"id":4,"title":5,"body":6,"dateModified":2043,"datePublished":2043,"description":2044,"extension":2045,"faq":2046,"keyword":2055,"kind":2056,"meta":2057,"navigation":99,"path":2066,"seo":2067,"slug":2070,"stem":2071,"topics":2072,"type":2075,"__hash__":2076},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-forecast-with-exponential-smoothing\u002Findex.md","Build a Sales Forecast with Exponential Smoothing",{"type":7,"value":8,"toc":2031},"minimark",[9,28,33,64,67,71,265,271,275,278,605,617,730,734,737,995,1001,1251,1255,1742,1757,1909,1913,1916,1920,1955,1959,1966,1970,1976,1982,1988,1994,1998,2027],[10,11,12,13,17,18,21,22,27],"p",{},"Most sales forecasts in spreadsheets are a trend line, last year plus a percentage, or someone's judgement typed into next year's columns. Exponential smoothing is a step up that stays understandable: the forecast is built from a smoothed level, a smoothed trend and a repeating seasonal pattern, each updated month by month with more weight on recent months than old ones. The Holt-Winters version, which handles trend and seasonality together, is what Excel's ",[14,15,16],"code",{},"FORECAST.ETS"," uses under the hood, and in Python the ",[14,19,20],{},"statsmodels"," library provides it with more control and better diagnostics. This page reads three years of monthly sales from Excel, fits a Holt-Winters model, tests it honestly on six months it was not allowed to see, compares it with a simple seasonal benchmark, produces a twelve-month forecast with a plausible range, and writes the result into a workbook with a chart. It belongs to ",[23,24,26],"a",{"href":25},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl statsmodels\n","bash","",[14,41,42],{"__ignoreMap":39},[43,44,47,51,55,58,61],"span",{"class":45,"line":46},"line",1,[43,48,50],{"class":49},"sMTad","pip",[43,52,54],{"class":53},"srMev"," install",[43,56,57],{"class":53}," pandas",[43,59,60],{"class":53}," openpyxl",[43,62,63],{"class":53}," statsmodels\n",[10,65,66],{},"The input is thirty-six months of sales, October 2023 to September 2026, with steady growth, a summer peak and a December spike — the shape of a typical seasonal business. Holt-Winters needs at least two full seasons of history to estimate a monthly pattern; three is better.",[29,68,70],{"id":69},"step-1-load-and-look-at-the-history","Step 1 — Load and look at the history",[34,72,76],{"className":73,"code":74,"language":75,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nhist = pd.read_excel(\"monthly_sales.xlsx\", index_col=\"Month\", parse_dates=True)[\"Sales\"].asfreq(\"MS\")\nby_year = hist.groupby(hist.index.month).mean().div(hist.mean()).round(2)\nprint(hist.tail(6).to_string())\nprint(\"seasonal index by month:\", by_year.to_dict())\nprint(f\"growth, last 12 months vs previous 12: {hist[-12:].sum() \u002F hist[-24:-12].sum() - 1:.1%}\")\n","python",[14,77,78,94,101,154,170,185,199],{"__ignoreMap":39},[43,79,80,84,88,91],{"class":45,"line":46},[43,81,83],{"class":82},"s-kum","import",[43,85,87],{"class":86},"skGVy"," pandas ",[43,89,90],{"class":82},"as",[43,92,93],{"class":86}," pd\n",[43,95,97],{"class":45,"line":96},2,[43,98,100],{"emptyLinePlaceholder":99},true,"\n",[43,102,104,107,110,113,116,119,123,125,128,130,133,135,139,142,145,148,151],{"class":45,"line":103},3,[43,105,106],{"class":86},"hist ",[43,108,109],{"class":82},"=",[43,111,112],{"class":86}," pd.read_excel(",[43,114,115],{"class":53},"\"monthly_sales.xlsx\"",[43,117,118],{"class":86},", ",[43,120,122],{"class":121},"sa561","index_col",[43,124,109],{"class":82},[43,126,127],{"class":53},"\"Month\"",[43,129,118],{"class":86},[43,131,132],{"class":121},"parse_dates",[43,134,109],{"class":82},[43,136,138],{"class":137},"sP0c6","True",[43,140,141],{"class":86},")[",[43,143,144],{"class":53},"\"Sales\"",[43,146,147],{"class":86},"].asfreq(",[43,149,150],{"class":53},"\"MS\"",[43,152,153],{"class":86},")\n",[43,155,157,160,162,165,168],{"class":45,"line":156},4,[43,158,159],{"class":86},"by_year ",[43,161,109],{"class":82},[43,163,164],{"class":86}," hist.groupby(hist.index.month).mean().div(hist.mean()).round(",[43,166,167],{"class":137},"2",[43,169,153],{"class":86},[43,171,173,176,179,182],{"class":45,"line":172},5,[43,174,175],{"class":137},"print",[43,177,178],{"class":86},"(hist.tail(",[43,180,181],{"class":137},"6",[43,183,184],{"class":86},").to_string())\n",[43,186,188,190,193,196],{"class":45,"line":187},6,[43,189,175],{"class":137},[43,191,192],{"class":86},"(",[43,194,195],{"class":53},"\"seasonal index by month:\"",[43,197,198],{"class":86},", by_year.to_dict())\n",[43,200,202,204,206,209,212,216,219,222,225,228,231,234,236,239,242,244,246,249,251,254,257,260,263],{"class":45,"line":201},7,[43,203,175],{"class":137},[43,205,192],{"class":86},[43,207,208],{"class":82},"f",[43,210,211],{"class":53},"\"growth, last 12 months vs previous 12: ",[43,213,215],{"class":214},"sSjpA","{",[43,217,218],{"class":86},"hist[",[43,220,221],{"class":82},"-",[43,223,224],{"class":137},"12",[43,226,227],{"class":86},":].sum() ",[43,229,230],{"class":82},"\u002F",[43,232,233],{"class":86}," hist[",[43,235,221],{"class":82},[43,237,238],{"class":137},"24",[43,240,241],{"class":86},":",[43,243,221],{"class":82},[43,245,224],{"class":137},[43,247,248],{"class":86},"].sum() ",[43,250,221],{"class":82},[43,252,253],{"class":137}," 1",[43,255,256],{"class":82},":.1%",[43,258,259],{"class":214},"}",[43,261,262],{"class":53},"\"",[43,264,153],{"class":86},[10,266,267,270],{},[14,268,269],{},"asfreq(\"MS\")"," gives the series a monthly frequency, which the model needs, and would expose any missing month as a gap rather than letting it slip by. A quick seasonal index — each month's average relative to the overall average — confirms what the model is about to learn: June to August about 18% above average, October, November and January about 20% below, and December lifted above its neighbours by the holidays. Year-on-year growth of almost 16% says the series has a trend as well. Both matter for choosing the model.",[29,272,274],{"id":273},"step-2-fit-on-most-of-the-data-test-on-the-rest","Step 2 — Fit on most of the data, test on the rest",[10,276,277],{},"A forecast is only worth trusting if it has been tested on data it did not see. Hold back the last six months, fit on the rest, and compare:",[34,279,281],{"className":73,"code":280,"language":75,"meta":39,"style":39},"import numpy as np\nfrom statsmodels.tsa.holtwinters import ExponentialSmoothing\n\ntrain, test = hist[:-6], hist[-6:]\nmodel = ExponentialSmoothing(train, trend=\"add\", damped_trend=True, seasonal=\"mul\", seasonal_periods=12).fit()\nhw = model.forecast(6)\nnaive = train[-12:-6].set_axis(test.index)                      # same months last year: the seasonal naive benchmark\n\ndef mape(actual, forecast):\n    return float(np.mean(np.abs(forecast - actual) \u002F actual))\n\nprint(pd.DataFrame({\"Actual\": test, \"Holt-Winters\": hw.round(-2), \"Last year\": naive}).to_string())\nprint(f\"MAPE Holt-Winters {mape(test, hw):.1%} | seasonal naive {mape(test, naive):.1%}\")\nprint({k: round(v, 3) for k, v in model.params.items() if k in (\"smoothing_level\", \"smoothing_trend\", \"smoothing_seasonal\", \"damping_trend\")})\n",[14,282,283,295,308,312,336,385,399,426,431,444,466,471,504,541],{"__ignoreMap":39},[43,284,285,287,290,292],{"class":45,"line":46},[43,286,83],{"class":82},[43,288,289],{"class":86}," numpy ",[43,291,90],{"class":82},[43,293,294],{"class":86}," np\n",[43,296,297,300,303,305],{"class":45,"line":96},[43,298,299],{"class":82},"from",[43,301,302],{"class":86}," statsmodels.tsa.holtwinters ",[43,304,83],{"class":82},[43,306,307],{"class":86}," ExponentialSmoothing\n",[43,309,310],{"class":45,"line":103},[43,311,100],{"emptyLinePlaceholder":99},[43,313,314,317,319,322,324,326,329,331,333],{"class":45,"line":156},[43,315,316],{"class":86},"train, test ",[43,318,109],{"class":82},[43,320,321],{"class":86}," hist[:",[43,323,221],{"class":82},[43,325,181],{"class":137},[43,327,328],{"class":86},"], hist[",[43,330,221],{"class":82},[43,332,181],{"class":137},[43,334,335],{"class":86},":]\n",[43,337,338,341,343,346,349,351,354,356,359,361,363,365,368,370,373,375,378,380,382],{"class":45,"line":172},[43,339,340],{"class":86},"model ",[43,342,109],{"class":82},[43,344,345],{"class":86}," ExponentialSmoothing(train, ",[43,347,348],{"class":121},"trend",[43,350,109],{"class":82},[43,352,353],{"class":53},"\"add\"",[43,355,118],{"class":86},[43,357,358],{"class":121},"damped_trend",[43,360,109],{"class":82},[43,362,138],{"class":137},[43,364,118],{"class":86},[43,366,367],{"class":121},"seasonal",[43,369,109],{"class":82},[43,371,372],{"class":53},"\"mul\"",[43,374,118],{"class":86},[43,376,377],{"class":121},"seasonal_periods",[43,379,109],{"class":82},[43,381,224],{"class":137},[43,383,384],{"class":86},").fit()\n",[43,386,387,390,392,395,397],{"class":45,"line":187},[43,388,389],{"class":86},"hw ",[43,391,109],{"class":82},[43,393,394],{"class":86}," model.forecast(",[43,396,181],{"class":137},[43,398,153],{"class":86},[43,400,401,404,406,409,411,413,415,417,419,422],{"class":45,"line":201},[43,402,403],{"class":86},"naive ",[43,405,109],{"class":82},[43,407,408],{"class":86}," train[",[43,410,221],{"class":82},[43,412,224],{"class":137},[43,414,241],{"class":86},[43,416,221],{"class":82},[43,418,181],{"class":137},[43,420,421],{"class":86},"].set_axis(test.index)                      ",[43,423,425],{"class":424},"s-wDw","# same months last year: the seasonal naive benchmark\n",[43,427,429],{"class":45,"line":428},8,[43,430,100],{"emptyLinePlaceholder":99},[43,432,434,437,441],{"class":45,"line":433},9,[43,435,436],{"class":82},"def",[43,438,440],{"class":439},"s_Opv"," mape",[43,442,443],{"class":86},"(actual, forecast):\n",[43,445,447,450,453,456,458,461,463],{"class":45,"line":446},10,[43,448,449],{"class":82},"    return",[43,451,452],{"class":137}," float",[43,454,455],{"class":86},"(np.mean(np.abs(forecast ",[43,457,221],{"class":82},[43,459,460],{"class":86}," actual) ",[43,462,230],{"class":82},[43,464,465],{"class":86}," actual))\n",[43,467,469],{"class":45,"line":468},11,[43,470,100],{"emptyLinePlaceholder":99},[43,472,474,476,479,482,485,488,491,493,495,498,501],{"class":45,"line":473},12,[43,475,175],{"class":137},[43,477,478],{"class":86},"(pd.DataFrame({",[43,480,481],{"class":53},"\"Actual\"",[43,483,484],{"class":86},": test, ",[43,486,487],{"class":53},"\"Holt-Winters\"",[43,489,490],{"class":86},": hw.round(",[43,492,221],{"class":82},[43,494,167],{"class":137},[43,496,497],{"class":86},"), ",[43,499,500],{"class":53},"\"Last year\"",[43,502,503],{"class":86},": naive}).to_string())\n",[43,505,507,509,511,513,516,518,521,523,525,528,530,533,535,537,539],{"class":45,"line":506},13,[43,508,175],{"class":137},[43,510,192],{"class":86},[43,512,208],{"class":82},[43,514,515],{"class":53},"\"MAPE Holt-Winters ",[43,517,215],{"class":214},[43,519,520],{"class":86},"mape(test, hw)",[43,522,256],{"class":82},[43,524,259],{"class":214},[43,526,527],{"class":53}," | seasonal naive ",[43,529,215],{"class":214},[43,531,532],{"class":86},"mape(test, naive)",[43,534,256],{"class":82},[43,536,259],{"class":214},[43,538,262],{"class":53},[43,540,153],{"class":86},[43,542,544,546,549,552,555,558,561,564,567,570,573,576,579,581,584,587,589,592,594,597,599,602],{"class":45,"line":543},14,[43,545,175],{"class":137},[43,547,548],{"class":86},"({k: ",[43,550,551],{"class":137},"round",[43,553,554],{"class":86},"(v, ",[43,556,557],{"class":137},"3",[43,559,560],{"class":86},") ",[43,562,563],{"class":82},"for",[43,565,566],{"class":86}," k, v ",[43,568,569],{"class":82},"in",[43,571,572],{"class":86}," model.params.items() ",[43,574,575],{"class":82},"if",[43,577,578],{"class":86}," k ",[43,580,569],{"class":82},[43,582,583],{"class":86}," (",[43,585,586],{"class":53},"\"smoothing_level\"",[43,588,118],{"class":86},[43,590,591],{"class":53},"\"smoothing_trend\"",[43,593,118],{"class":86},[43,595,596],{"class":53},"\"smoothing_seasonal\"",[43,598,118],{"class":86},[43,600,601],{"class":53},"\"damping_trend\"",[43,603,604],{"class":86},")})\n",[10,606,607,608,612,613,616],{},"The model has an ",[609,610,611],"em",{},"additive, damped"," trend — growth continues but flattens gradually rather than compounding for ever, which is usually the safer assumption — and ",[609,614,615],{},"multiplicative"," seasonality, because the summer peak grows in proportion as sales grow. The benchmark is the simplest sensible forecast: the same month last year. The mean absolute percentage error (MAPE) puts both on one scale. Holt-Winters beats the benchmark comfortably — 4.1% against 14.2% — mainly because it accounts for growth that \"same as last year\" ignores; it ran a little high in every one of the six months, which is worth watching but well inside what the range in Step 3 allows. If a model cannot beat that benchmark, it is not worth its complexity. The fitted smoothing parameters show how quickly each component adapts: a level parameter of 0.33 means recent months carry real weight, and a seasonal parameter of zero means the seasonal pattern was judged stable from year to year, so the model does not chase noise in individual months.",[618,619,627,628,627,632,627,636,627,644,627,653,627,660,627,665,627,669,627,674,627,679,627,684,627,688,627,692,627,695,627,700,627,705,627,709,627,713,627,716,627,721,627,726],"svg",{"viewBox":620,"role":621,"ariaLabelledBy":622,"xmlns":625,"style":626},"84 0 592 376","img",[623,624],"hw-parts-t","hw-parts-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[629,630,631],"title",{"id":623},"What Holt-Winters combines",[633,634,635],"desc",{"id":624},"The forecast for a future month is the smoothed level plus the damped trend, multiplied by the seasonal factor for that calendar month. Each component is updated every month with more weight on recent observations.",[637,638],"rect",{"x":639,"y":640,"width":641,"height":642,"fill":643},"84","0","592","376","#ffffff",[637,645],{"x":646,"y":647,"width":648,"height":649,"rx":224,"fill":650,"stroke":651,"style":652},"100.0","16.0","560.0","59.0","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[654,655,659],"text",{"x":656,"y":657,"style":658},"380.0","41.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Level",[654,661,664],{"x":656,"y":662,"style":663},"61.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","smoothed current sales",[45,666],{"x1":656,"y1":667,"x2":656,"y2":668,"stroke":651,"style":652},"80.0","102.0",[670,671],"polygon",{"points":672,"fill":673},"380.0,106.0 375.0,97.0 385.0,97.0","#5b5cf0",[637,675],{"x":646,"y":676,"width":648,"height":649,"rx":224,"fill":677,"stroke":678,"style":652},"111.0","#d9f4f1","var(--teal,#0f9488)",[654,680,683],{"x":656,"y":681,"style":682},"136.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","+ Damped trend",[654,685,687],{"x":656,"y":686,"style":663},"156.0","growth that flattens",[45,689],{"x1":656,"y1":690,"x2":656,"y2":691,"stroke":651,"style":652},"175.0","197.0",[670,693],{"points":694,"fill":673},"380.0,201.0 375.0,192.0 385.0,192.0",[637,696],{"x":646,"y":697,"width":648,"height":649,"rx":224,"fill":698,"stroke":699,"style":652},"206.0","#fdefd8","var(--gold,#b4740a)",[654,701,704],{"x":656,"y":702,"style":703},"231.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","× Seasonal factor",[654,706,708],{"x":656,"y":707,"style":663},"251.0","summer high, winter low",[45,710],{"x1":656,"y1":711,"x2":656,"y2":712,"stroke":651,"style":652},"270.0","292.0",[670,714],{"points":715,"fill":673},"380.0,296.0 375.0,287.0 385.0,287.0",[637,717],{"x":646,"y":718,"width":648,"height":649,"rx":224,"fill":719,"stroke":720,"style":652},"301.0","#e7ebef","var(--line,#cdd5e6)",[654,722,725],{"x":656,"y":723,"style":724},"326.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","= Forecast",[654,727,729],{"x":656,"y":728,"style":663},"346.0","for each future month",[29,731,733],{"id":732},"step-3-forecast-twelve-months-with-a-range","Step 3 — Forecast twelve months, with a range",[10,735,736],{},"Refit on all thirty-six months and forecast a year ahead. A single line looks more certain than any forecast is, so simulate the model forward many times to get a range:",[34,738,740],{"className":73,"code":739,"language":75,"meta":39,"style":39},"final = ExponentialSmoothing(hist, trend=\"add\", damped_trend=True, seasonal=\"mul\", seasonal_periods=12).fit()\nforecast = final.forecast(12)\nsims = final.simulate(12, repetitions=2000, error=\"mul\", rng=np.random.default_rng(7))\nlow, high = sims.quantile(0.1, axis=1), sims.quantile(0.9, axis=1)\nfc = pd.DataFrame({\"Forecast\": forecast, \"Low (P10)\": low, \"High (P90)\": high}).round(-2)\nprint(fc.to_string())\nprint(f\"next 12 months: {forecast.sum():,.0f} vs last 12 months: {hist[-12:].sum():,.0f} ({forecast.sum() \u002F hist[-12:].sum() - 1:+.1%})\")\n",[14,741,742,784,798,845,884,918,925],{"__ignoreMap":39},[43,743,744,747,749,752,754,756,758,760,762,764,766,768,770,772,774,776,778,780,782],{"class":45,"line":46},[43,745,746],{"class":86},"final ",[43,748,109],{"class":82},[43,750,751],{"class":86}," ExponentialSmoothing(hist, ",[43,753,348],{"class":121},[43,755,109],{"class":82},[43,757,353],{"class":53},[43,759,118],{"class":86},[43,761,358],{"class":121},[43,763,109],{"class":82},[43,765,138],{"class":137},[43,767,118],{"class":86},[43,769,367],{"class":121},[43,771,109],{"class":82},[43,773,372],{"class":53},[43,775,118],{"class":86},[43,777,377],{"class":121},[43,779,109],{"class":82},[43,781,224],{"class":137},[43,783,384],{"class":86},[43,785,786,789,791,794,796],{"class":45,"line":96},[43,787,788],{"class":86},"forecast ",[43,790,109],{"class":82},[43,792,793],{"class":86}," final.forecast(",[43,795,224],{"class":137},[43,797,153],{"class":86},[43,799,800,803,805,808,810,812,815,817,820,822,825,827,829,831,834,836,839,842],{"class":45,"line":103},[43,801,802],{"class":86},"sims ",[43,804,109],{"class":82},[43,806,807],{"class":86}," final.simulate(",[43,809,224],{"class":137},[43,811,118],{"class":86},[43,813,814],{"class":121},"repetitions",[43,816,109],{"class":82},[43,818,819],{"class":137},"2000",[43,821,118],{"class":86},[43,823,824],{"class":121},"error",[43,826,109],{"class":82},[43,828,372],{"class":53},[43,830,118],{"class":86},[43,832,833],{"class":121},"rng",[43,835,109],{"class":82},[43,837,838],{"class":86},"np.random.default_rng(",[43,840,841],{"class":137},"7",[43,843,844],{"class":86},"))\n",[43,846,847,850,852,855,858,860,863,865,868,871,874,876,878,880,882],{"class":45,"line":156},[43,848,849],{"class":86},"low, high ",[43,851,109],{"class":82},[43,853,854],{"class":86}," sims.quantile(",[43,856,857],{"class":137},"0.1",[43,859,118],{"class":86},[43,861,862],{"class":121},"axis",[43,864,109],{"class":82},[43,866,867],{"class":137},"1",[43,869,870],{"class":86},"), sims.quantile(",[43,872,873],{"class":137},"0.9",[43,875,118],{"class":86},[43,877,862],{"class":121},[43,879,109],{"class":82},[43,881,867],{"class":137},[43,883,153],{"class":86},[43,885,886,889,891,894,897,900,903,906,909,912,914,916],{"class":45,"line":172},[43,887,888],{"class":86},"fc ",[43,890,109],{"class":82},[43,892,893],{"class":86}," pd.DataFrame({",[43,895,896],{"class":53},"\"Forecast\"",[43,898,899],{"class":86},": forecast, ",[43,901,902],{"class":53},"\"Low (P10)\"",[43,904,905],{"class":86},": low, ",[43,907,908],{"class":53},"\"High (P90)\"",[43,910,911],{"class":86},": high}).round(",[43,913,221],{"class":82},[43,915,167],{"class":137},[43,917,153],{"class":86},[43,919,920,922],{"class":45,"line":187},[43,921,175],{"class":137},[43,923,924],{"class":86},"(fc.to_string())\n",[43,926,927,929,931,933,936,938,941,944,946,949,951,953,955,957,960,962,964,966,968,971,973,975,977,979,981,983,985,988,990,993],{"class":45,"line":201},[43,928,175],{"class":137},[43,930,192],{"class":86},[43,932,208],{"class":82},[43,934,935],{"class":53},"\"next 12 months: ",[43,937,215],{"class":214},[43,939,940],{"class":86},"forecast.sum()",[43,942,943],{"class":82},":,.0f",[43,945,259],{"class":214},[43,947,948],{"class":53}," vs last 12 months: ",[43,950,215],{"class":214},[43,952,218],{"class":86},[43,954,221],{"class":82},[43,956,224],{"class":137},[43,958,959],{"class":86},":].sum()",[43,961,943],{"class":82},[43,963,259],{"class":214},[43,965,583],{"class":53},[43,967,215],{"class":214},[43,969,970],{"class":86},"forecast.sum() ",[43,972,230],{"class":82},[43,974,233],{"class":86},[43,976,221],{"class":82},[43,978,224],{"class":137},[43,980,227],{"class":86},[43,982,221],{"class":82},[43,984,253],{"class":137},[43,986,987],{"class":82},":+.1%",[43,989,259],{"class":214},[43,991,992],{"class":53},")\"",[43,994,153],{"class":86},[10,996,997,1000],{},[14,998,999],{},"simulate"," runs the fitted model forward 2,000 times with random errors of the size it saw in the history, and the 10th and 90th percentiles of those paths give a range that should contain the outcome in about eight months out of ten. The range widens month by month, as it should: next month is more predictable than next September. For next June the central forecast is about 103,000, but the range runs from roughly 86,000 to 120,000. The twelve-month total of about 1.04 million is 16% above the last twelve months, in line with recent growth. Reporting the range alongside the central figure changes the conversation from \"will we hit 103,000 in June?\" to \"June will probably land between 86,000 and 120,000\" — which is what planning stock and staff actually needs.",[618,1002,627,1008,627,1011,627,1014,627,1019,627,1025,627,1031,627,1034,627,1038,627,1041,627,1045,627,1051,627,1056,627,1060,627,1064,627,1067,627,1071,627,1074,627,1078,627,1081,627,1086,627,1090,627,1094,627,1097,627,1101,627,1105,627,1109,627,1113,627,1117,627,1121,627,1125,627,1129,627,1135,627,1140,627,1143,627,1146,627,1149,627,1152,627,1155,627,1158,627,1161,627,1164,627,1167,627,1170,627,1173,627,1176,627,1179,627,1182,627,1185,627,1188,627,1191,627,1194,627,1197,627,1200,627,1203,627,1206,627,1209,627,1212,627,1215,627,1218,627,1221,627,1224,627,1227,627,1230,627,1233,627,1236,627,1239,627,1242,627,1245,627,1248],{"viewBox":1003,"role":621,"ariaLabelledBy":1004,"xmlns":625,"style":1007},"4 0 748 280",[1005,1006],"hw-fc-t","hw-fc-d","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[629,1009,1010],{"id":1005},"Twelve-month forecast with its range",[633,1012,1013],{"id":1006},"The forecast falls to about 67,000 in November and January, then rises to about 103,000 in June 2027. The P10 to P90 range is narrow for October, from 68,200 to 74,400, and widens to about 80,000 to 122,000 by August 2027.",[637,1015],{"x":1016,"y":640,"width":1017,"height":1018,"fill":643},"4","748","280",[637,1020],{"x":1021,"y":1022,"width":1023,"height":1023,"rx":557,"fill":650,"stroke":651,"style":1024},"64.0","18.0","14.0","stroke-width:1.5px",[654,1026,1030],{"x":1027,"y":1028,"style":1029},"84.0","29.5","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start","Forecast (k)",[637,1032],{"x":1033,"y":1022,"width":1023,"height":1023,"rx":557,"fill":650,"stroke":651,"style":1024},"185.9",[654,1035,1037],{"x":1036,"y":1028,"style":1029},"205.9","Low P10 (k)",[637,1039],{"x":1040,"y":1022,"width":1023,"height":1023,"rx":557,"fill":650,"stroke":651,"style":1024},"304.4",[654,1042,1044],{"x":1043,"y":1028,"style":1029},"324.4","High P90 (k)",[45,1046],{"x1":1047,"y1":1048,"x2":1049,"y2":1048,"stroke":720,"style":1050},"51.3","242.0","736.0","stroke-width:1px",[654,1052,640],{"x":1053,"y":1054,"style":1055},"29.6","246.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[45,1057],{"x1":1047,"y1":1058,"x2":1049,"y2":1058,"stroke":720,"style":1059},"178.7","stroke-width:1px;stroke-dasharray:5 4",[654,1061,1063],{"x":1053,"y":1062,"style":1055},"182.7","50",[45,1065],{"x1":1047,"y1":1066,"x2":1049,"y2":1066,"stroke":720,"style":1059},"115.3",[654,1068,1070],{"x":1053,"y":1069,"style":1055},"119.3","100",[45,1072],{"x1":1047,"y1":1073,"x2":1049,"y2":1073,"stroke":720,"style":1059},"52.0",[654,1075,1077],{"x":1053,"y":1076,"style":1055},"56.0","150",[45,1079],{"x1":1047,"y1":1048,"x2":1049,"y2":1048,"stroke":1080,"style":1024},"var(--muted,#5b6780)",[654,1082,1085],{"x":1083,"y":1084,"style":663},"79.8","260.0","Oct",[654,1087,1089],{"x":1088,"y":1084,"style":663},"136.9","Nov",[654,1091,1093],{"x":1092,"y":1084,"style":663},"193.9","Dec",[654,1095,1096],{"x":707,"y":1084,"style":663},"Jan",[654,1098,1100],{"x":1099,"y":1084,"style":663},"308.0","Feb",[654,1102,1104],{"x":1103,"y":1084,"style":663},"365.1","Mar",[654,1106,1108],{"x":1107,"y":1084,"style":663},"422.2","Apr",[654,1110,1112],{"x":1111,"y":1084,"style":663},"479.2","May",[654,1114,1116],{"x":1115,"y":1084,"style":663},"536.3","Jun",[654,1118,1120],{"x":1119,"y":1084,"style":663},"593.3","Jul",[654,1122,1124],{"x":1123,"y":1084,"style":663},"650.4","Aug",[654,1126,1128],{"x":1127,"y":1084,"style":663},"707.5","Sep",[1130,1131],"path",{"d":1132,"fill":1133,"stroke":651,"style":1134},"M79.8,151.7 L136.9,156.8 L193.9,142.4 L251.0,156.9 L308.0,143.1 L365.1,134.2 L422.2,124.2 L479.2,116.9 L536.3,111.7 L593.3,112.7 L650.4,114.2 L707.5,127.7","none","stroke-width:2.5px;stroke-linejoin:round;stroke-linecap:round",[1136,1137],"circle",{"cx":1083,"cy":1138,"r":1139,"fill":643,"stroke":651,"style":652},"151.7","4.0",[1136,1141],{"cx":1088,"cy":1142,"r":1139,"fill":643,"stroke":651,"style":652},"156.8",[1136,1144],{"cx":1092,"cy":1145,"r":1139,"fill":643,"stroke":651,"style":652},"142.4",[1136,1147],{"cx":707,"cy":1148,"r":1139,"fill":643,"stroke":651,"style":652},"156.9",[1136,1150],{"cx":1099,"cy":1151,"r":1139,"fill":643,"stroke":651,"style":652},"143.1",[1136,1153],{"cx":1103,"cy":1154,"r":1139,"fill":643,"stroke":651,"style":652},"134.2",[1136,1156],{"cx":1107,"cy":1157,"r":1139,"fill":643,"stroke":651,"style":652},"124.2",[1136,1159],{"cx":1111,"cy":1160,"r":1139,"fill":643,"stroke":651,"style":652},"116.9",[1136,1162],{"cx":1115,"cy":1163,"r":1139,"fill":643,"stroke":651,"style":652},"111.7",[1136,1165],{"cx":1119,"cy":1166,"r":1139,"fill":643,"stroke":651,"style":652},"112.7",[1136,1168],{"cx":1123,"cy":1169,"r":1139,"fill":643,"stroke":651,"style":652},"114.2",[1136,1171],{"cx":1127,"cy":1172,"r":1139,"fill":643,"stroke":651,"style":652},"127.7",[1130,1174],{"d":1175,"fill":1133,"stroke":651,"style":1134},"M79.8,155.6 L136.9,160.9 L193.9,148.1 L251.0,162.8 L308.0,151.4 L365.1,145.1 L422.2,138.6 L479.2,134.5 L536.3,133.3 L593.3,137.1 L650.4,140.9 L707.5,154.1",[1136,1177],{"cx":1083,"cy":1178,"r":1139,"fill":643,"stroke":651,"style":652},"155.6",[1136,1180],{"cx":1088,"cy":1181,"r":1139,"fill":643,"stroke":651,"style":652},"160.9",[1136,1183],{"cx":1092,"cy":1184,"r":1139,"fill":643,"stroke":651,"style":652},"148.1",[1136,1186],{"cx":707,"cy":1187,"r":1139,"fill":643,"stroke":651,"style":652},"162.8",[1136,1189],{"cx":1099,"cy":1190,"r":1139,"fill":643,"stroke":651,"style":652},"151.4",[1136,1192],{"cx":1103,"cy":1193,"r":1139,"fill":643,"stroke":651,"style":652},"145.1",[1136,1195],{"cx":1107,"cy":1196,"r":1139,"fill":643,"stroke":651,"style":652},"138.6",[1136,1198],{"cx":1111,"cy":1199,"r":1139,"fill":643,"stroke":651,"style":652},"134.5",[1136,1201],{"cx":1115,"cy":1202,"r":1139,"fill":643,"stroke":651,"style":652},"133.3",[1136,1204],{"cx":1119,"cy":1205,"r":1139,"fill":643,"stroke":651,"style":652},"137.1",[1136,1207],{"cx":1123,"cy":1208,"r":1139,"fill":643,"stroke":651,"style":652},"140.9",[1136,1210],{"cx":1127,"cy":1211,"r":1139,"fill":643,"stroke":651,"style":652},"154.1",[1130,1213],{"d":1214,"fill":1133,"stroke":651,"style":1134},"M79.8,147.8 L136.9,152.7 L193.9,136.6 L251.0,151.3 L308.0,134.6 L365.1,123.3 L422.2,110.1 L479.2,99.8 L536.3,90.6 L593.3,88.4 L650.4,87.6 L707.5,101.4",[1136,1216],{"cx":1083,"cy":1217,"r":1139,"fill":643,"stroke":651,"style":652},"147.8",[1136,1219],{"cx":1088,"cy":1220,"r":1139,"fill":643,"stroke":651,"style":652},"152.7",[1136,1222],{"cx":1092,"cy":1223,"r":1139,"fill":643,"stroke":651,"style":652},"136.6",[1136,1225],{"cx":707,"cy":1226,"r":1139,"fill":643,"stroke":651,"style":652},"151.3",[1136,1228],{"cx":1099,"cy":1229,"r":1139,"fill":643,"stroke":651,"style":652},"134.6",[1136,1231],{"cx":1103,"cy":1232,"r":1139,"fill":643,"stroke":651,"style":652},"123.3",[1136,1234],{"cx":1107,"cy":1235,"r":1139,"fill":643,"stroke":651,"style":652},"110.1",[1136,1237],{"cx":1111,"cy":1238,"r":1139,"fill":643,"stroke":651,"style":652},"99.8",[1136,1240],{"cx":1115,"cy":1241,"r":1139,"fill":643,"stroke":651,"style":652},"90.6",[1136,1243],{"cx":1119,"cy":1244,"r":1139,"fill":643,"stroke":651,"style":652},"88.4",[1136,1246],{"cx":1123,"cy":1247,"r":1139,"fill":643,"stroke":651,"style":652},"87.6",[1136,1249],{"cx":1127,"cy":1250,"r":1139,"fill":643,"stroke":651,"style":652},"101.4",[29,1252,1254],{"id":1253},"step-4-write-the-forecast-workbook-with-a-chart","Step 4 — Write the forecast workbook with a chart",[34,1256,1258],{"className":73,"code":1257,"language":75,"meta":39,"style":39},"from openpyxl import load_workbook\nfrom openpyxl.chart import LineChart, Reference\n\nout = pd.concat([hist.rename(\"Actual\").to_frame(), fc], axis=1)\nout.index = out.index.strftime(\"%b %Y\")\npath = \"sales_forecast.xlsx\"\nwith pd.ExcelWriter(path, engine=\"openpyxl\") as xw:\n    out.to_excel(xw, sheet_name=\"Forecast\", index_label=\"Month\")\n    pd.DataFrame({\"Model\": [\"Holt-Winters (damped additive trend, multiplicative seasonality)\", \"Seasonal naive\"],\n                  \"Holdout MAPE\": [mape(test, hw), mape(test, naive)]}).to_excel(xw, sheet_name=\"Model check\", index=False)\nwb = load_workbook(path)\nws = wb[\"Forecast\"]\nchart = LineChart()\nchart.title, chart.y_axis.title = \"Monthly sales: actual and forecast\", \"Sales\"\nchart.add_data(Reference(ws, min_col=2, max_col=5, min_row=1, max_row=ws.max_row), titles_from_data=True)\nchart.set_categories(Reference(ws, min_col=1, min_row=2, max_row=ws.max_row))\nfor s in chart.series:\n    s.smooth = False\nchart.series[2].graphicalProperties.line.dashStyle = \"dash\"\nchart.series[3].graphicalProperties.line.dashStyle = \"dash\"\nchart.x_axis.delete = chart.y_axis.delete = False\nchart.width, chart.height = 24, 10\nws.add_chart(chart, \"G2\")\nfor row in ws.iter_rows(min_row=2, min_col=2):\n    for c in row:\n        c.number_format = \"#,##0\"\nwb.save(path)\nprint(wb.sheetnames, \"rows:\", ws.max_row - 1)\n",[14,1259,1260,1272,1284,1288,1311,1331,1341,1364,1387,1409,1436,1446,1461,1471,1486,1537,1566,1579,1590,1606,1619,1634,1650,1661,1691,1705,1716,1722],{"__ignoreMap":39},[43,1261,1262,1264,1267,1269],{"class":45,"line":46},[43,1263,299],{"class":82},[43,1265,1266],{"class":86}," openpyxl ",[43,1268,83],{"class":82},[43,1270,1271],{"class":86}," load_workbook\n",[43,1273,1274,1276,1279,1281],{"class":45,"line":96},[43,1275,299],{"class":82},[43,1277,1278],{"class":86}," openpyxl.chart ",[43,1280,83],{"class":82},[43,1282,1283],{"class":86}," LineChart, Reference\n",[43,1285,1286],{"class":45,"line":103},[43,1287,100],{"emptyLinePlaceholder":99},[43,1289,1290,1293,1295,1298,1300,1303,1305,1307,1309],{"class":45,"line":156},[43,1291,1292],{"class":86},"out ",[43,1294,109],{"class":82},[43,1296,1297],{"class":86}," pd.concat([hist.rename(",[43,1299,481],{"class":53},[43,1301,1302],{"class":86},").to_frame(), fc], ",[43,1304,862],{"class":121},[43,1306,109],{"class":82},[43,1308,867],{"class":137},[43,1310,153],{"class":86},[43,1312,1313,1316,1318,1321,1323,1326,1329],{"class":45,"line":172},[43,1314,1315],{"class":86},"out.index ",[43,1317,109],{"class":82},[43,1319,1320],{"class":86}," out.index.strftime(",[43,1322,262],{"class":53},[43,1324,1325],{"class":214},"%b",[43,1327,1328],{"class":53}," %Y\"",[43,1330,153],{"class":86},[43,1332,1333,1336,1338],{"class":45,"line":187},[43,1334,1335],{"class":86},"path ",[43,1337,109],{"class":82},[43,1339,1340],{"class":53}," \"sales_forecast.xlsx\"\n",[43,1342,1343,1346,1349,1352,1354,1357,1359,1361],{"class":45,"line":201},[43,1344,1345],{"class":82},"with",[43,1347,1348],{"class":86}," pd.ExcelWriter(path, ",[43,1350,1351],{"class":121},"engine",[43,1353,109],{"class":82},[43,1355,1356],{"class":53},"\"openpyxl\"",[43,1358,560],{"class":86},[43,1360,90],{"class":82},[43,1362,1363],{"class":86}," xw:\n",[43,1365,1366,1369,1372,1374,1376,1378,1381,1383,1385],{"class":45,"line":428},[43,1367,1368],{"class":86},"    out.to_excel(xw, ",[43,1370,1371],{"class":121},"sheet_name",[43,1373,109],{"class":82},[43,1375,896],{"class":53},[43,1377,118],{"class":86},[43,1379,1380],{"class":121},"index_label",[43,1382,109],{"class":82},[43,1384,127],{"class":53},[43,1386,153],{"class":86},[43,1388,1389,1392,1395,1398,1401,1403,1406],{"class":45,"line":433},[43,1390,1391],{"class":86},"    pd.DataFrame({",[43,1393,1394],{"class":53},"\"Model\"",[43,1396,1397],{"class":86},": [",[43,1399,1400],{"class":53},"\"Holt-Winters (damped additive trend, multiplicative seasonality)\"",[43,1402,118],{"class":86},[43,1404,1405],{"class":53},"\"Seasonal naive\"",[43,1407,1408],{"class":86},"],\n",[43,1410,1411,1414,1417,1419,1421,1424,1426,1429,1431,1434],{"class":45,"line":446},[43,1412,1413],{"class":53},"                  \"Holdout MAPE\"",[43,1415,1416],{"class":86},": [mape(test, hw), mape(test, naive)]}).to_excel(xw, ",[43,1418,1371],{"class":121},[43,1420,109],{"class":82},[43,1422,1423],{"class":53},"\"Model check\"",[43,1425,118],{"class":86},[43,1427,1428],{"class":121},"index",[43,1430,109],{"class":82},[43,1432,1433],{"class":137},"False",[43,1435,153],{"class":86},[43,1437,1438,1441,1443],{"class":45,"line":468},[43,1439,1440],{"class":86},"wb ",[43,1442,109],{"class":82},[43,1444,1445],{"class":86}," load_workbook(path)\n",[43,1447,1448,1451,1453,1456,1458],{"class":45,"line":473},[43,1449,1450],{"class":86},"ws ",[43,1452,109],{"class":82},[43,1454,1455],{"class":86}," wb[",[43,1457,896],{"class":53},[43,1459,1460],{"class":86},"]\n",[43,1462,1463,1466,1468],{"class":45,"line":506},[43,1464,1465],{"class":86},"chart ",[43,1467,109],{"class":82},[43,1469,1470],{"class":86}," LineChart()\n",[43,1472,1473,1476,1478,1481,1483],{"class":45,"line":543},[43,1474,1475],{"class":86},"chart.title, chart.y_axis.title ",[43,1477,109],{"class":82},[43,1479,1480],{"class":53}," \"Monthly sales: actual and forecast\"",[43,1482,118],{"class":86},[43,1484,1485],{"class":53},"\"Sales\"\n",[43,1487,1489,1492,1495,1497,1499,1501,1504,1506,1509,1511,1514,1516,1518,1520,1523,1525,1528,1531,1533,1535],{"class":45,"line":1488},15,[43,1490,1491],{"class":86},"chart.add_data(Reference(ws, ",[43,1493,1494],{"class":121},"min_col",[43,1496,109],{"class":82},[43,1498,167],{"class":137},[43,1500,118],{"class":86},[43,1502,1503],{"class":121},"max_col",[43,1505,109],{"class":82},[43,1507,1508],{"class":137},"5",[43,1510,118],{"class":86},[43,1512,1513],{"class":121},"min_row",[43,1515,109],{"class":82},[43,1517,867],{"class":137},[43,1519,118],{"class":86},[43,1521,1522],{"class":121},"max_row",[43,1524,109],{"class":82},[43,1526,1527],{"class":86},"ws.max_row), ",[43,1529,1530],{"class":121},"titles_from_data",[43,1532,109],{"class":82},[43,1534,138],{"class":137},[43,1536,153],{"class":86},[43,1538,1540,1543,1545,1547,1549,1551,1553,1555,1557,1559,1561,1563],{"class":45,"line":1539},16,[43,1541,1542],{"class":86},"chart.set_categories(Reference(ws, ",[43,1544,1494],{"class":121},[43,1546,109],{"class":82},[43,1548,867],{"class":137},[43,1550,118],{"class":86},[43,1552,1513],{"class":121},[43,1554,109],{"class":82},[43,1556,167],{"class":137},[43,1558,118],{"class":86},[43,1560,1522],{"class":121},[43,1562,109],{"class":82},[43,1564,1565],{"class":86},"ws.max_row))\n",[43,1567,1569,1571,1574,1576],{"class":45,"line":1568},17,[43,1570,563],{"class":82},[43,1572,1573],{"class":86}," s ",[43,1575,569],{"class":82},[43,1577,1578],{"class":86}," chart.series:\n",[43,1580,1582,1585,1587],{"class":45,"line":1581},18,[43,1583,1584],{"class":86},"    s.smooth ",[43,1586,109],{"class":82},[43,1588,1589],{"class":137}," False\n",[43,1591,1593,1596,1598,1601,1603],{"class":45,"line":1592},19,[43,1594,1595],{"class":86},"chart.series[",[43,1597,167],{"class":137},[43,1599,1600],{"class":86},"].graphicalProperties.line.dashStyle ",[43,1602,109],{"class":82},[43,1604,1605],{"class":53}," \"dash\"\n",[43,1607,1609,1611,1613,1615,1617],{"class":45,"line":1608},20,[43,1610,1595],{"class":86},[43,1612,557],{"class":137},[43,1614,1600],{"class":86},[43,1616,109],{"class":82},[43,1618,1605],{"class":53},[43,1620,1622,1625,1627,1630,1632],{"class":45,"line":1621},21,[43,1623,1624],{"class":86},"chart.x_axis.delete ",[43,1626,109],{"class":82},[43,1628,1629],{"class":86}," chart.y_axis.delete ",[43,1631,109],{"class":82},[43,1633,1589],{"class":137},[43,1635,1637,1640,1642,1645,1647],{"class":45,"line":1636},22,[43,1638,1639],{"class":86},"chart.width, chart.height ",[43,1641,109],{"class":82},[43,1643,1644],{"class":137}," 24",[43,1646,118],{"class":86},[43,1648,1649],{"class":137},"10\n",[43,1651,1653,1656,1659],{"class":45,"line":1652},23,[43,1654,1655],{"class":86},"ws.add_chart(chart, ",[43,1657,1658],{"class":53},"\"G2\"",[43,1660,153],{"class":86},[43,1662,1664,1666,1669,1671,1674,1676,1678,1680,1682,1684,1686,1688],{"class":45,"line":1663},24,[43,1665,563],{"class":82},[43,1667,1668],{"class":86}," row ",[43,1670,569],{"class":82},[43,1672,1673],{"class":86}," ws.iter_rows(",[43,1675,1513],{"class":121},[43,1677,109],{"class":82},[43,1679,167],{"class":137},[43,1681,118],{"class":86},[43,1683,1494],{"class":121},[43,1685,109],{"class":82},[43,1687,167],{"class":137},[43,1689,1690],{"class":86},"):\n",[43,1692,1694,1697,1700,1702],{"class":45,"line":1693},25,[43,1695,1696],{"class":82},"    for",[43,1698,1699],{"class":86}," c ",[43,1701,569],{"class":82},[43,1703,1704],{"class":86}," row:\n",[43,1706,1708,1711,1713],{"class":45,"line":1707},26,[43,1709,1710],{"class":86},"        c.number_format ",[43,1712,109],{"class":82},[43,1714,1715],{"class":53}," \"#,##0\"\n",[43,1717,1719],{"class":45,"line":1718},27,[43,1720,1721],{"class":86},"wb.save(path)\n",[43,1723,1725,1727,1730,1733,1736,1738,1740],{"class":45,"line":1724},28,[43,1726,175],{"class":137},[43,1728,1729],{"class":86},"(wb.sheetnames, ",[43,1731,1732],{"class":53},"\"rows:\"",[43,1734,1735],{"class":86},", ws.max_row ",[43,1737,221],{"class":82},[43,1739,253],{"class":137},[43,1741,153],{"class":86},[10,1743,1744,1745,1748,1749,1752,1753,27],{},"The ",[609,1746,1747],{},"Forecast"," sheet holds actuals and forecast in one table, so the forecast continues straight on from the history, with the low and high columns either side; the chart draws actuals and forecast as lines and the range as dashed lines. The ",[609,1750,1751],{},"Model check"," sheet records how the model did on the held-back months, against the benchmark — the first thing a sceptical reader should be shown. Turning a forecast into a budget comparison later is covered in ",[23,1754,1756],{"href":1755},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-budget-vs-actual-variance-report\u002F","Build a Budget vs Actual Variance Report",[618,1758,627,1764,627,1767,627,1770,627,1774,627,1779,627,1788,627,1793,627,1797,627,1801,627,1806,627,1809,627,1812,627,1819,627,1823,627,1826,627,1829,627,1833,627,1836,627,1839,627,1846,627,1849,627,1853,627,1856,627,1860,627,1863,627,1866,627,1873,627,1877,627,1880,627,1883,627,1887,627,1890,627,1893,627,1899,627,1903,627,1906],{"viewBox":1759,"role":621,"ariaLabelledBy":1760,"xmlns":625,"style":1763},"4 0 752 142",[1761,1762],"hw-flow-t","hw-flow-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[629,1765,1766],{"id":1761},"From history to forecast workbook",[633,1768,1769],{"id":1762},"Load monthly sales with a monthly frequency, hold back the last six months, fit Holt-Winters and compare it with the same months last year, refit on all data, forecast twelve months with a simulated range, and write actuals, forecast, range and the model check to Excel with a chart.",[637,1771],{"x":1016,"y":640,"width":1772,"height":1773,"fill":643},"752","142",[637,1775],{"x":1776,"y":647,"width":1777,"height":1778,"rx":224,"fill":719,"stroke":720,"style":652},"20.0","115.2","110.5",[1780,1781,1783],"g",{"transform":1782},"translate(65.6,29.0) scale(1.000)",[1130,1784],{"d":1785,"fill":1133,"stroke":1786,"style":1787},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[654,1789,1792],{"x":1790,"y":1791,"style":724},"77.6","75.5","Load history",[654,1794,1796],{"x":1790,"y":1795,"style":663},"95.0","monthly",[654,1798,1800],{"x":1790,"y":1799,"style":663},"112.5","frequency",[45,1802],{"x1":1803,"y1":1804,"x2":1805,"y2":1804,"stroke":651,"style":652},"140.2","71.2","162.2",[670,1807],{"points":1808,"fill":673},"166.2,71.2 157.2,76.2 157.2,66.2",[637,1810],{"x":1811,"y":647,"width":1777,"height":1778,"rx":224,"fill":698,"stroke":699,"style":652},"171.2",[1780,1813,1815],{"transform":1814},"translate(216.8,29.0) scale(1.000)",[1130,1816],{"d":1817,"fill":1133,"stroke":1818,"style":1787},"M9 3h6 M10 3v6l-5 9a2 2 0 0 0 1.8 3h10.4a2 2 0 0 0 1.8-3l-5-9V3","var(--gold-ink,#7a4e06)",[654,1820,1822],{"x":1821,"y":1791,"style":703},"228.8","Holdout test",[654,1824,1825],{"x":1821,"y":1795,"style":663},"6 months,",[654,1827,1828],{"x":1821,"y":1799,"style":663},"MAPE",[45,1830],{"x1":1831,"y1":1804,"x2":1832,"y2":1804,"stroke":651,"style":652},"291.4","313.4",[670,1834],{"points":1835,"fill":673},"317.4,71.2 308.4,76.2 308.4,66.2",[637,1837],{"x":1838,"y":647,"width":1777,"height":1778,"rx":224,"fill":677,"stroke":678,"style":652},"322.4",[1780,1840,1842],{"transform":1841},"translate(368.0,29.0) scale(1.000)",[1130,1843],{"d":1844,"fill":1133,"stroke":1845,"style":1787},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[654,1847,1848],{"x":656,"y":1791,"style":682},"Beat the",[654,1850,1852],{"x":656,"y":1851,"style":682},"93.0","benchmark?",[654,1854,1855],{"x":656,"y":1799,"style":663},"vs last year",[45,1857],{"x1":1858,"y1":1804,"x2":1859,"y2":1804,"stroke":651,"style":652},"442.6","464.6",[670,1861],{"points":1862,"fill":673},"468.6,71.2 459.6,76.2 459.6,66.2",[637,1864],{"x":1865,"y":647,"width":1777,"height":1778,"rx":224,"fill":650,"stroke":651,"style":652},"473.6",[1780,1867,1869],{"transform":1868},"translate(519.2,29.0) scale(1.000)",[1130,1870],{"d":1871,"fill":1133,"stroke":1872,"style":1787},"M4 20h16 M4 20V4 M6 16l4-5 4 3 6-7","var(--brand-strong,#4338ca)",[654,1874,1876],{"x":1875,"y":1791,"style":658},"531.2","Forecast +",[654,1878,1879],{"x":1875,"y":1851,"style":658},"range",[654,1881,1882],{"x":1875,"y":1799,"style":663},"P10 – P90",[45,1884],{"x1":1885,"y1":1804,"x2":1886,"y2":1804,"stroke":651,"style":652},"593.8","615.8",[670,1888],{"points":1889,"fill":673},"619.8,71.2 610.8,76.2 610.8,66.2",[637,1891],{"x":1892,"y":647,"width":1777,"height":1778,"rx":224,"fill":719,"stroke":720,"style":652},"624.8",[1780,1894,1896],{"transform":1895},"translate(670.4,29.0) scale(1.000)",[1130,1897],{"d":1898,"fill":1133,"stroke":1786,"style":1787},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6",[654,1900,1902],{"x":1901,"y":1791,"style":724},"682.4","Excel + chart",[654,1904,1905],{"x":1901,"y":1795,"style":663},"with model",[654,1907,1908],{"x":1901,"y":1799,"style":663},"check",[29,1910,1912],{"id":1911},"what-exponential-smoothing-cannot-know","What exponential smoothing cannot know",[10,1914,1915],{},"The model extrapolates the patterns in the history, which is exactly its strength and its limit. It does not know that a price rise is planned for January, that a large customer has just been won or lost, or that last year's March was inflated by a one-off order. Those are the business's knowledge and belong in the forecast as explicit adjustments, recorded next to the statistical forecast rather than typed over it, so the next comparison of forecast and actual can tell which part was wrong. Unusual months in the history deserve the same care: replace a known one-off with a typical value before fitting, or the model will expect it to repeat. And refit monthly as new actuals arrive; the holdout check is cheap enough to run every time, and a model whose error starts to climb is the earliest warning that the business has changed.",[29,1917,1919],{"id":1918},"common-pitfalls","Common pitfalls",[1921,1922,1923,1931,1937,1943,1949],"ul",{},[1924,1925,1926,1930],"li",{},[1927,1928,1929],"strong",{},"No holdout test."," Measure accuracy on months the model has not seen.",[1924,1932,1933,1936],{},[1927,1934,1935],{},"No benchmark."," Compare with \"same month last year\"; beat it or keep it simple.",[1924,1938,1939,1942],{},[1927,1940,1941],{},"Undamped trends."," Growth compounding for ever is rarely realistic; damp it.",[1924,1944,1945,1948],{},[1927,1946,1947],{},"A single line."," Show a range; forecasts are uncertain.",[1924,1950,1951,1954],{},[1927,1952,1953],{},"Fitting through one-offs."," Adjust exceptional months before fitting.",[29,1956,1958],{"id":1957},"conclusion","Conclusion",[10,1960,1961,1962,1965],{},"To forecast monthly sales from Excel, load the history with a monthly frequency, fit ",[14,1963,1964],{},"ExponentialSmoothing"," with a damped additive trend and multiplicative seasonality, and test it on the last six months against a same-month-last-year benchmark. Refit on all the data, forecast twelve months with a P10–P90 range from simulation, and write actuals, forecast, range and the accuracy check to a workbook with a chart — with business adjustments recorded separately.",[29,1967,1969],{"id":1968},"frequently-asked-questions","Frequently asked questions",[10,1971,1972,1975],{},[1927,1973,1974],{},"How do I forecast sales with exponential smoothing in Python?","\nUse statsmodels' ExponentialSmoothing with trend and seasonal settings, call fit(), then forecast(n) for the next n periods.",[10,1977,1978,1981],{},[1927,1979,1980],{},"How do I know if my forecast is any good?","\nHold back the last few months, fit on the rest, and compare the forecast with the actuals using a measure such as MAPE, alongside a simple benchmark like the same month last year.",[10,1983,1984,1987],{},[1927,1985,1986],{},"Should seasonality be additive or multiplicative?","\nMultiplicative when the seasonal swing grows with the level of sales, additive when it stays roughly constant in absolute terms.",[10,1989,1990,1993],{},[1927,1991,1992],{},"How do I get a forecast range instead of a single number?","\nSimulate the fitted model forward many times with simulate() and take percentiles such as the 10th and 90th of the simulated paths.",[29,1995,1997],{"id":1996},"related","Related",[1921,1999,2000,2006,2013,2020],{},[1924,2001,2002,2003,2005],{},"Up one level: ",[23,2004,26],{"href":25}," — finance reports from Python.",[1924,2007,2008,2012],{},[23,2009,2011],{"href":2010},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-weekly-sales-report\u002F","Build a Weekly Sales Report in Excel with Python"," — the actuals side.",[1924,2014,2015,2019],{},[23,2016,2018],{"href":2017},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fforecast-trend-and-slope-equivalents-in-python\u002F","FORECAST, TREND, SLOPE and RSQ Equivalents in Python"," — linear forecasts.",[1924,2021,2022,2026],{},[23,2023,2025],{"href":2024},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fbuild-a-rolling-12-month-chart-on-an-excel-dashboard\u002F","Build a Rolling 12-Month Chart on an Excel Dashboard"," — presenting the trend.",[2028,2029,2030],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":39,"searchDepth":96,"depth":96,"links":2032},[2033,2034,2035,2036,2037,2038,2039,2040,2041,2042],{"id":31,"depth":96,"text":32},{"id":69,"depth":96,"text":70},{"id":273,"depth":96,"text":274},{"id":732,"depth":96,"text":733},{"id":1253,"depth":96,"text":1254},{"id":1911,"depth":96,"text":1912},{"id":1918,"depth":96,"text":1919},{"id":1957,"depth":96,"text":1958},{"id":1968,"depth":96,"text":1969},{"id":1996,"depth":96,"text":1997},"2026-10-02","Forecast monthly sales from an Excel history with Holt-Winters exponential smoothing in Python: fit trend and seasonality with statsmodels, test the model on the months it did not see, compare it with a naive seasonal forecast, produce a twelve-month forecast with a range, and write the actuals, forecast and chart into a workbook the business can read.","md",[2047,2049,2051,2053],{"q":1974,"a":2048},"Use statsmodels' ExponentialSmoothing with trend and seasonal settings, call fit(), then forecast(n) for the next n periods.",{"q":1980,"a":2050},"Hold back the last few months, fit on the rest, and compare the forecast with the actuals using a measure such as MAPE, alongside a simple benchmark like the same month last year.",{"q":1986,"a":2052},"Multiplicative when the seasonal swing grows with the level of sales, additive when it stays roughly constant in absolute terms.",{"q":1992,"a":2054},"Simulate the fitted model forward many times with simulate() and take percentiles such as the 10th and 90th of the simulated paths.","sales forecast exponential smoothing python",null,{"breadcrumb":2058},[2059,2061,2064],{"name":2060,"item":230},"Home",{"name":2062,"item":2063},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2065,"item":25},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-forecast-with-exponential-smoothing",{"title":2068,"description":2069},"Build a Sales Forecast with Exponential Smoothing in Python","Holt-Winters sales forecasting from Excel data: statsmodels ExponentialSmoothing with trend and seasonality, a holdout backtest with MAPE, a seasonal naive benchmark, simulated forecast ranges and an Excel report with a chart.","build-a-sales-forecast-with-exponential-smoothing","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-forecast-with-exponential-smoothing\u002Findex",[2073,2074],"report-layouts","pandas","how-to","XU4IR1PwRZnZ_f8VlB95SgIXne-olNLdw5QhzVjMskA",[2078,2082],{"title":2079,"path":2080,"stem":2081,"children":-1},"Build a Sales Commission Report with Python and Excel","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-commission-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-commission-report\u002Findex",{"title":2083,"path":2084,"stem":2085,"children":-1},"Build a Sales Pipeline Report from a CRM Export","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-pipeline-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-pipeline-report\u002Findex",[2087,2089],{"path":2088,"title":2065},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2090,"title":2062},"\u002Fautomating-reporting-workflows",[],[2093,2097,2100,2103,2106,2109],{"path":2094,"title":2095,"kind":2096},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2098,"title":2099,"kind":2075},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-data-dictionary-sheet-to-an-excel-report","Add a Data Dictionary Sheet to an Excel Report",{"path":2101,"title":2102,"kind":2075},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard","Add a KPI Summary Block to an Excel Dashboard",{"path":2104,"title":2105,"kind":2075},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel","Add a Table of Contents Sheet with Hyperlinks in Excel",{"path":2107,"title":2108,"kind":2075},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-slicer-style-filters-to-an-excel-dashboard","Add a Dropdown Filter That Drives an Excel Dashboard",{"path":2110,"title":2111,"kind":2075},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python","Add a Summary Sheet to an Excel Report with Python",1790972127234]