[{"data":1,"prerenderedAt":1932},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Frun-a-regression-with-python-in-excel":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Frun-a-regression-with-python-in-excel":1899,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Frun-a-regression-with-python-in-excel":1907,"children:\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Frun-a-regression-with-python-in-excel":1912,"related:\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Frun-a-regression-with-python-in-excel":1913},{"id":4,"title":5,"body":6,"dateModified":1865,"datePublished":1865,"description":1866,"extension":1867,"faq":1868,"keyword":1877,"kind":1878,"meta":1879,"navigation":450,"path":1888,"seo":1889,"slug":1892,"stem":1893,"topics":1894,"type":1897,"__hash__":1898},"docs\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Frun-a-regression-with-python-in-excel\u002Findex.md","Run a Regression with Python in Excel",{"type":7,"value":8,"toc":1851},"minimark",[9,31,36,51,130,137,310,314,320,381,416,420,427,532,544,548,559,717,720,912,916,923,1085,1103,1107,1110,1370,1382,1476,1480,1487,1538,1553,1557,1567,1681,1695,1699,1754,1758,1782,1786,1792,1798,1807,1813,1817,1847],[10,11,12,13,17,18,21,22,24,25,30],"p",{},"Excel can fit a regression with ",[14,15,16],"code",{},"LINEST"," or the Analysis ToolPak, but both give a block of unlabelled numbers and nothing to check the fit with. Python in Excel puts statsmodels one ",[14,19,20],{},"=PY("," away: the same least-squares fit, with named coefficients, standard errors, p-values, prediction intervals and residual plots, recalculating whenever the table changes. This page fits a sales model on a monthly table step by step, returns each result to the grid in a shape people can read, and checks the numbers against ",[14,23,16],{},". It is part of ",[26,27,29],"a",{"href":28},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002F","Using Python in Excel",".",[32,33,35],"h2",{"id":34},"prerequisites","Prerequisites",[10,37,38,39,42,43,46,47,50],{},"Python in Excel is part of Microsoft 365 on Windows, Mac and the web for supported plans; statsmodels, pandas and matplotlib are provided in Microsoft's cloud runtime, with ",[14,40,41],{},"statsmodels.api"," already imported as ",[14,44,45],{},"sm",". The workbook has an Excel table named ",[14,48,49],{},"Sales"," with three years of monthly data: advertising spend in thousands, average selling price, a 0\u002F1 flag for months with a promotion, and units sold.",[52,53,54,76],"table",{},[55,56,57],"thead",{},[58,59,60,64,67,70,73],"tr",{},[61,62,63],"th",{},"Month",[61,65,66],{},"AdSpend",[61,68,69],{},"Price",[61,71,72],{},"Promo",[61,74,75],{},"Units",[77,78,79,97,114],"tbody",{},[58,80,81,85,88,91,94],{},[82,83,84],"td",{},"2023-10",[82,86,87],{},"28.8",[82,89,90],{},"9.48",[82,92,93],{},"0",[82,95,96],{},"757",[58,98,99,102,105,108,111],{},[82,100,101],{},"2023-11",[82,103,104],{},"36.9",[82,106,107],{},"8.01",[82,109,110],{},"1",[82,112,113],{},"990",[58,115,116,119,122,125,127],{},[82,117,118],{},"2023-12",[82,120,121],{},"33.3",[82,123,124],{},"11.32",[82,126,93],{},[82,128,129],{},"710",[10,131,132,133,136],{},"The question: how many extra units does each thousand of advertising buy, what does a price rise cost, and is the promotion worth running? The code below runs unchanged in ",[14,134,135],{},"=PY"," cells; each block is one cell, entered in order from left to right or top to bottom.",[138,139,147,148,147,152,147,156,147,163,147,173,147,184,147,191,147,196,147,203,147,208,147,212,147,219,147,224,147,227,147,231,147,234,147,239,147,246,147,251,147,254,147,258,147,261,147,264,147,270,147,274,147,277,147,281,147,284,147,289,147,296,147,302,147,306],"svg",{"viewBox":140,"role":141,"ariaLabelledBy":142,"xmlns":145,"style":146},"4 0 752 142","img",[143,144],"rg-flow-t","rg-flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[149,150,151],"title",{"id":143},"One PY cell per step",[153,154,155],"desc",{"id":144},"The Sales table is read with xl into a DataFrame, a regression is fitted with statsmodels, and three separate cells return the coefficient table, a prediction with its interval, and a residual chart to the grid.",[157,158],"rect",{"x":159,"y":93,"width":160,"height":161,"fill":162},"4","752","142","#ffffff",[157,164],{"x":165,"y":166,"width":167,"height":168,"rx":169,"fill":170,"stroke":171,"style":172},"20.0","16.0","115.2","110.5","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[174,175,177],"g",{"transform":176},"translate(65.6,37.8) scale(1.000)",[178,179],"path",{"d":180,"fill":181,"stroke":182,"style":183},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[185,186,190],"text",{"x":187,"y":188,"style":189},"77.6","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Sales table",[185,192,195],{"x":187,"y":193,"style":194},"103.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","xl(\"Sales[#All]\")",[197,198],"line",{"x1":199,"y1":200,"x2":201,"y2":200,"stroke":202,"style":172},"140.2","71.2","162.2","var(--brand,#5b5cf0)",[204,205],"polygon",{"points":206,"fill":207},"166.2,71.2 157.2,76.2 157.2,66.2","#5b5cf0",[157,209],{"x":210,"y":166,"width":167,"height":168,"rx":169,"fill":211,"stroke":202,"style":172},"171.2","#f0f4ff",[174,213,215],{"transform":214},"translate(216.8,37.8) scale(1.000)",[178,216],{"d":217,"fill":181,"stroke":218,"style":183},"M12 3c-4 0-4 1.6-4 3v2h4.2v1H6c-2 0-3 1.4-3 4s1 4 3 4h2v-2.6c0-1.6 1.3-2.9 2.9-2.9h4.2c1.3 0 2.4-1.1 2.4-2.4V6c0-1.6-1.8-3-5.5-3z M10 5.5h.01 M12 21c4 0 4-1.6 4-3v-2h-4.2v-1H18c2 0 3-1.4 3-4s-1-4-3-4h-2v2.6c0 1.6-1.3 2.9-2.9 2.9H8.9c-1.3 0-2.4 1.1-2.4 2.4V18c0 1.6 1.8 3 5.5 3z M14 18.5h.01","var(--brand-strong,#4338ca)",[185,220,223],{"x":221,"y":188,"style":222},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Fit",[185,225,226],{"x":221,"y":193,"style":194},"smf.ols",[197,228],{"x1":229,"y1":200,"x2":230,"y2":200,"stroke":202,"style":172},"291.4","313.4",[204,232],{"points":233,"fill":207},"317.4,71.2 308.4,76.2 308.4,66.2",[157,235],{"x":236,"y":166,"width":167,"height":168,"rx":169,"fill":237,"stroke":238,"style":172},"322.4","#d9f4f1","var(--teal,#0f9488)",[174,240,242],{"transform":241},"translate(368.0,37.8) scale(1.000)",[178,243],{"d":244,"fill":181,"stroke":245,"style":183},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","var(--teal-ink,#0b6157)",[185,247,250],{"x":248,"y":188,"style":249},"380.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Coefficients",[185,252,253],{"x":248,"y":193,"style":194},"spilled to cells",[197,255],{"x1":256,"y1":200,"x2":257,"y2":200,"stroke":202,"style":172},"442.6","464.6",[204,259],{"points":260,"fill":207},"468.6,71.2 459.6,76.2 459.6,66.2",[157,262],{"x":263,"y":166,"width":167,"height":168,"rx":169,"fill":237,"stroke":238,"style":172},"473.6",[174,265,267],{"transform":266},"translate(519.2,37.8) scale(1.000)",[178,268],{"d":269,"fill":181,"stroke":245,"style":183},"M4 20h16 M4 20V4 M6 16l4-5 4 3 6-7",[185,271,273],{"x":272,"y":188,"style":249},"531.2","Prediction",[185,275,276],{"x":272,"y":193,"style":194},"with an interval",[197,278],{"x1":279,"y1":200,"x2":280,"y2":200,"stroke":202,"style":172},"593.8","615.8",[204,282],{"points":283,"fill":207},"619.8,71.2 610.8,76.2 610.8,66.2",[157,285],{"x":286,"y":166,"width":167,"height":168,"rx":169,"fill":287,"stroke":288,"style":172},"624.8","#fdefd8","var(--gold,#b4740a)",[174,290,292],{"transform":291},"translate(670.4,29.0) scale(1.000)",[178,293],{"d":294,"fill":181,"stroke":295,"style":183},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8","var(--gold-ink,#7a4e06)",[185,297,301],{"x":298,"y":299,"style":300},"682.4","75.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Residual",[185,303,305],{"x":298,"y":304,"style":300},"93.0","chart",[185,307,309],{"x":298,"y":308,"style":194},"112.5","image in a cell",[32,311,313],{"id":312},"step-1-read-the-table","Step 1 — Read the table",[10,315,316,317,319],{},"In an empty cell, type ",[14,318,20],{},", read the whole table with its headers, and commit with Ctrl+Enter:",[321,322,327],"pre",{"className":323,"code":324,"language":325,"meta":326,"style":326},"language-python shiki shiki-themes github-light github-dark-high-contrast","sales = xl(\"Sales[#All]\", headers=True)\nprint(sales.shape)\nprint(sales.dtypes.to_string())\n","python","",[14,328,329,364,373],{"__ignoreMap":326},[330,331,333,337,341,344,348,351,355,357,361],"span",{"class":197,"line":332},1,[330,334,336],{"class":335},"skGVy","sales ",[330,338,340],{"class":339},"s-kum","=",[330,342,343],{"class":335}," xl(",[330,345,347],{"class":346},"srMev","\"Sales[#All]\"",[330,349,350],{"class":335},", ",[330,352,354],{"class":353},"sa561","headers",[330,356,340],{"class":339},[330,358,360],{"class":359},"sP0c6","True",[330,362,363],{"class":335},")\n",[330,365,367,370],{"class":197,"line":366},2,[330,368,369],{"class":359},"print",[330,371,372],{"class":335},"(sales.shape)\n",[330,374,376,378],{"class":197,"line":375},3,[330,377,369],{"class":359},[330,379,380],{"class":335},"(sales.dtypes.to_string())\n",[10,382,383,384,387,388,391,392,395,396,391,399,402,403,406,407,411,412,415],{},"In a PY cell, ",[14,385,386],{},"print()"," output goes to the Diagnostics pane and the cell itself shows the value of the last expression; the examples print so their output can be shown here. Numeric columns arrive as ",[14,389,390],{},"float64"," or ",[14,393,394],{},"int64","; if one shows ",[14,397,398],{},"object",[14,400,401],{},"str"," instead, some of its cells hold text, and the fit will fail or silently drop rows. Fix that in the table or convert with ",[14,404,405],{},"pd.to_numeric(..., errors=\"coerce\")"," before fitting. ",[26,408,410],{"href":409},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Freference-cells-ranges-and-tables-with-xl-in-python-in-excel\u002F","Reference Cells, Ranges and Tables with xl() in Python in Excel"," covers the other ways to point ",[14,413,414],{},"xl()"," at data.",[32,417,419],{"id":418},"step-2-fit-the-model","Step 2 — Fit the model",[10,421,422,423,426],{},"The formula interface names the outcome on the left of ",[14,424,425],{},"~"," and the predictors on the right, and adds the intercept for you:",[321,428,430],{"className":323,"code":429,"language":325,"meta":326,"style":326},"import statsmodels.formula.api as smf\n\nmodel = smf.ols(\"Units ~ AdSpend + Price + Promo\", data=sales).fit()\nprint(f\"R² {model.rsquared:.4f}   adjusted R² {model.rsquared_adj:.4f}   n={int(model.nobs)}\")\n",[14,431,432,446,452,475],{"__ignoreMap":326},[330,433,434,437,440,443],{"class":197,"line":332},[330,435,436],{"class":339},"import",[330,438,439],{"class":335}," statsmodels.formula.api ",[330,441,442],{"class":339},"as",[330,444,445],{"class":335}," smf\n",[330,447,448],{"class":197,"line":366},[330,449,451],{"emptyLinePlaceholder":450},true,"\n",[330,453,454,457,459,462,465,467,470,472],{"class":197,"line":375},[330,455,456],{"class":335},"model ",[330,458,340],{"class":339},[330,460,461],{"class":335}," smf.ols(",[330,463,464],{"class":346},"\"Units ~ AdSpend + Price + Promo\"",[330,466,350],{"class":335},[330,468,469],{"class":353},"data",[330,471,340],{"class":339},[330,473,474],{"class":335},"sales).fit()\n",[330,476,478,480,483,486,489,493,496,499,502,505,507,510,512,514,517,519,522,525,527,530],{"class":197,"line":477},4,[330,479,369],{"class":359},[330,481,482],{"class":335},"(",[330,484,485],{"class":339},"f",[330,487,488],{"class":346},"\"R² ",[330,490,492],{"class":491},"sSjpA","{",[330,494,495],{"class":335},"model.rsquared",[330,497,498],{"class":339},":.4f",[330,500,501],{"class":491},"}",[330,503,504],{"class":346},"   adjusted R² ",[330,506,492],{"class":491},[330,508,509],{"class":335},"model.rsquared_adj",[330,511,498],{"class":339},[330,513,501],{"class":491},[330,515,516],{"class":346},"   n=",[330,518,492],{"class":491},[330,520,521],{"class":359},"int",[330,523,524],{"class":335},"(model.nobs)",[330,526,501],{"class":491},[330,528,529],{"class":346},"\"",[330,531,363],{"class":335},[10,533,534,535,538,539,543],{},"In the cell, ",[14,536,537],{},"model"," shows as a Python object card. Keep the fit in its own cell so the cells that follow can use it without refitting; Python cells share one namespace per workbook and run in row-major order. ",[26,540,542],{"href":541},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Freturn-dataframes-and-values-from-python-in-excel-cells\u002F","Return DataFrames and Values from Python in Excel Cells"," explains the object card and how to switch a cell's output to values.",[32,545,547],{"id":546},"step-3-return-a-readable-coefficient-table","Step 3 — Return a readable coefficient table",[10,549,550,553,554,558],{},[14,551,552],{},"model.summary()"," is a text report that looks fine in a notebook but lands as one cell of text in Excel. Build a DataFrame instead, switch the cell's output to ",[555,556,557],"em",{},"Excel value"," (Ctrl+Alt+Shift+M), and it spills into a labelled table that formulas and charts can reference:",[321,560,562],{"className":323,"code":561,"language":325,"meta":326,"style":326},"coef = model.summary2().tables[1].rename(columns={\n    \"Coef.\": \"Estimate\", \"Std.Err.\": \"Std error\", \"P>|t|\": \"p-value\",\n    \"[0.025\": \"Low 95%\", \"0.975]\": \"High 95%\",\n})\ncoef = coef.round({\"Estimate\": 3, \"Std error\": 3, \"t\": 2, \"p-value\": 4, \"Low 95%\": 3, \"High 95%\": 3})\nprint(coef.to_string())\n",[14,563,564,587,621,643,648,709],{"__ignoreMap":326},[330,565,566,569,571,574,576,579,582,584],{"class":197,"line":332},[330,567,568],{"class":335},"coef ",[330,570,340],{"class":339},[330,572,573],{"class":335}," model.summary2().tables[",[330,575,110],{"class":359},[330,577,578],{"class":335},"].rename(",[330,580,581],{"class":353},"columns",[330,583,340],{"class":339},[330,585,586],{"class":335},"{\n",[330,588,589,592,595,598,600,603,605,608,610,613,615,618],{"class":197,"line":366},[330,590,591],{"class":346},"    \"Coef.\"",[330,593,594],{"class":335},": ",[330,596,597],{"class":346},"\"Estimate\"",[330,599,350],{"class":335},[330,601,602],{"class":346},"\"Std.Err.\"",[330,604,594],{"class":335},[330,606,607],{"class":346},"\"Std error\"",[330,609,350],{"class":335},[330,611,612],{"class":346},"\"P>|t|\"",[330,614,594],{"class":335},[330,616,617],{"class":346},"\"p-value\"",[330,619,620],{"class":335},",\n",[330,622,623,626,628,631,633,636,638,641],{"class":197,"line":375},[330,624,625],{"class":346},"    \"[0.025\"",[330,627,594],{"class":335},[330,629,630],{"class":346},"\"Low 95%\"",[330,632,350],{"class":335},[330,634,635],{"class":346},"\"0.975]\"",[330,637,594],{"class":335},[330,639,640],{"class":346},"\"High 95%\"",[330,642,620],{"class":335},[330,644,645],{"class":197,"line":477},[330,646,647],{"class":335},"})\n",[330,649,651,653,655,658,660,662,665,667,669,671,673,675,678,680,683,685,687,689,691,693,695,697,699,701,703,705,707],{"class":197,"line":650},5,[330,652,568],{"class":335},[330,654,340],{"class":339},[330,656,657],{"class":335}," coef.round({",[330,659,597],{"class":346},[330,661,594],{"class":335},[330,663,664],{"class":359},"3",[330,666,350],{"class":335},[330,668,607],{"class":346},[330,670,594],{"class":335},[330,672,664],{"class":359},[330,674,350],{"class":335},[330,676,677],{"class":346},"\"t\"",[330,679,594],{"class":335},[330,681,682],{"class":359},"2",[330,684,350],{"class":335},[330,686,617],{"class":346},[330,688,594],{"class":335},[330,690,159],{"class":359},[330,692,350],{"class":335},[330,694,630],{"class":346},[330,696,594],{"class":335},[330,698,664],{"class":359},[330,700,350],{"class":335},[330,702,640],{"class":346},[330,704,594],{"class":335},[330,706,664],{"class":359},[330,708,647],{"class":335},[330,710,712,714],{"class":197,"line":711},6,[330,713,369],{"class":359},[330,715,716],{"class":335},"(coef.to_string())\n",[10,718,719],{},"Read it as: each extra thousand of advertising adds about 14.2 units, a one-unit price rise loses about 64 units, and promotion months sell about 44 more units, holding the other two constant. The confidence intervals are the honest version of those numbers: the promotion effect could plausibly be anywhere between about 22 and 67 units. All three p-values are well below 0.05, so none of the effects is plausibly zero in this data.",[138,721,147,727,147,730,147,733,147,738,147,744,147,749,147,754,147,758,147,763,147,767,147,771,147,775,147,779,147,783,147,787,147,790,147,793,147,795,147,800,147,802,147,805,147,807,147,810,147,812,147,815,147,817,147,820,147,823,147,826,147,828,147,830,147,832,147,836,147,838,147,841,147,843,147,846,147,848,147,851,147,854,147,857,147,859,147,861,147,863,147,866,147,868,147,871,147,873,147,876,147,878,147,881,147,884,147,888,147,890,147,892,147,894,147,897,147,899,147,902,147,904,147,907,147,909],{"viewBox":722,"role":141,"ariaLabelledBy":723,"xmlns":145,"style":726},"158 0 444 172",[724,725],"rg-coef-t","rg-coef-d","width:100%;max-width:444px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[149,728,729],{"id":724},"The coefficient table spilled into the grid",[153,731,732],{"id":725},"A spilled table with a row for the intercept and each predictor, and columns for the estimate, standard error and the low and high ends of the 95 percent interval. The AdSpend, Price and Promo rows are highlighted.",[157,734],{"x":735,"y":93,"width":736,"height":737,"fill":162},"158","444","172",[157,739],{"x":740,"y":166,"width":741,"height":742,"rx":93,"fill":170,"stroke":171,"style":743},"174.4","32.0","28.0","stroke-width:1px",[185,745,110],{"x":746,"y":747,"style":748},"190.4","35.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle",[157,750],{"x":751,"y":166,"width":752,"height":742,"rx":93,"fill":211,"stroke":202,"style":753},"206.4","76.1","stroke-width:1.5px",[157,755],{"x":756,"y":166,"width":757,"height":742,"rx":93,"fill":211,"stroke":202,"style":753},"282.5","73.2",[185,759,762],{"x":760,"y":747,"style":761},"319.1","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Estimate",[157,764],{"x":765,"y":166,"width":766,"height":742,"rx":93,"fill":211,"stroke":202,"style":753},"355.7","73.9",[185,768,770],{"x":769,"y":747,"style":761},"392.7","Std error",[157,772],{"x":773,"y":166,"width":774,"height":742,"rx":93,"fill":211,"stroke":202,"style":753},"429.6","76.6",[185,776,778],{"x":777,"y":747,"style":761},"467.9","Low 95%",[157,780],{"x":781,"y":166,"width":782,"height":742,"rx":93,"fill":211,"stroke":202,"style":753},"506.2","79.4",[185,784,786],{"x":785,"y":747,"style":761},"545.9","High 95%",[157,788],{"x":740,"y":789,"width":741,"height":742,"rx":93,"fill":170,"stroke":171,"style":743},"44.0",[185,791,682],{"x":746,"y":792,"style":748},"63.0",[157,794],{"x":751,"y":789,"width":752,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,796,799],{"x":797,"y":792,"style":798},"244.5","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Intercept",[157,801],{"x":756,"y":789,"width":757,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,803,804],{"x":760,"y":792,"style":798},"939.150",[157,806],{"x":765,"y":789,"width":766,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,808,809],{"x":769,"y":792,"style":798},"54.980",[157,811],{"x":773,"y":789,"width":774,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,813,814],{"x":777,"y":792,"style":798},"827.159",[157,816],{"x":781,"y":789,"width":782,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,818,819],{"x":785,"y":792,"style":798},"1051.141",[157,821],{"x":740,"y":822,"width":741,"height":742,"rx":93,"fill":170,"stroke":171,"style":743},"72.0",[185,824,664],{"x":746,"y":825,"style":748},"91.0",[157,827],{"x":751,"y":822,"width":752,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,829,66],{"x":797,"y":825,"style":798},[157,831],{"x":756,"y":822,"width":757,"height":742,"rx":93,"fill":237,"stroke":238,"style":753},[185,833,835],{"x":760,"y":825,"style":834},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","14.173",[157,837],{"x":765,"y":822,"width":766,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,839,840],{"x":769,"y":825,"style":798},"0.617",[157,842],{"x":773,"y":822,"width":774,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,844,845],{"x":777,"y":825,"style":798},"12.917",[157,847],{"x":781,"y":822,"width":782,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,849,850],{"x":785,"y":825,"style":798},"15.429",[157,852],{"x":740,"y":853,"width":741,"height":742,"rx":93,"fill":170,"stroke":171,"style":743},"100.0",[185,855,159],{"x":746,"y":856,"style":748},"119.0",[157,858],{"x":751,"y":853,"width":752,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,860,69],{"x":797,"y":856,"style":798},[157,862],{"x":756,"y":853,"width":757,"height":742,"rx":93,"fill":237,"stroke":238,"style":753},[185,864,865],{"x":760,"y":856,"style":834},"-64.378",[157,867],{"x":765,"y":853,"width":766,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,869,870],{"x":769,"y":856,"style":798},"4.988",[157,872],{"x":773,"y":853,"width":774,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,874,875],{"x":777,"y":856,"style":798},"-74.538",[157,877],{"x":781,"y":853,"width":782,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,879,880],{"x":785,"y":856,"style":798},"-54.218",[157,882],{"x":740,"y":883,"width":741,"height":742,"rx":93,"fill":170,"stroke":171,"style":743},"128.0",[185,885,887],{"x":746,"y":886,"style":748},"147.0","5",[157,889],{"x":751,"y":883,"width":752,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,891,72],{"x":797,"y":886,"style":798},[157,893],{"x":756,"y":883,"width":757,"height":742,"rx":93,"fill":237,"stroke":238,"style":753},[185,895,896],{"x":760,"y":886,"style":834},"44.310",[157,898],{"x":765,"y":883,"width":766,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,900,901],{"x":769,"y":886,"style":798},"10.981",[157,903],{"x":773,"y":883,"width":774,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,905,906],{"x":777,"y":886,"style":798},"21.943",[157,908],{"x":781,"y":883,"width":782,"height":742,"rx":93,"fill":162,"stroke":171,"style":743},[185,910,911],{"x":785,"y":886,"style":798},"66.677",[32,913,915],{"id":914},"step-4-predict-next-month-with-an-interval","Step 4 — Predict next month, with an interval",[10,917,918,919,922],{},"A point forecast on its own invites false precision. ",[14,920,921],{},"get_prediction"," gives both the interval for the average month with those inputs and the wider interval for a single month:",[321,924,926],{"className":323,"code":925,"language":325,"meta":326,"style":326},"plan = pd.DataFrame({\"AdSpend\": [30.0, 30.0], \"Price\": [9.50, 9.99], \"Promo\": [1, 1]},\n                    index=[\"Plan A\", \"Plan B\"])\npred = model.get_prediction(plan).summary_frame(alpha=0.05)\nforecast = pred[[\"mean\", \"obs_ci_lower\", \"obs_ci_upper\"]].round(0)\nforecast.columns = [\"Forecast\", \"Low 95%\", \"High 95%\"]\nprint(forecast.to_string())\n",[14,927,928,983,1004,1024,1054,1078],{"__ignoreMap":326},[330,929,930,933,935,938,941,944,947,949,951,954,957,959,962,964,967,969,972,974,976,978,980],{"class":197,"line":332},[330,931,932],{"class":335},"plan ",[330,934,340],{"class":339},[330,936,937],{"class":335}," pd.DataFrame({",[330,939,940],{"class":346},"\"AdSpend\"",[330,942,943],{"class":335},": [",[330,945,946],{"class":359},"30.0",[330,948,350],{"class":335},[330,950,946],{"class":359},[330,952,953],{"class":335},"], ",[330,955,956],{"class":346},"\"Price\"",[330,958,943],{"class":335},[330,960,961],{"class":359},"9.50",[330,963,350],{"class":335},[330,965,966],{"class":359},"9.99",[330,968,953],{"class":335},[330,970,971],{"class":346},"\"Promo\"",[330,973,943],{"class":335},[330,975,110],{"class":359},[330,977,350],{"class":335},[330,979,110],{"class":359},[330,981,982],{"class":335},"]},\n",[330,984,985,988,990,993,996,998,1001],{"class":197,"line":366},[330,986,987],{"class":353},"                    index",[330,989,340],{"class":339},[330,991,992],{"class":335},"[",[330,994,995],{"class":346},"\"Plan A\"",[330,997,350],{"class":335},[330,999,1000],{"class":346},"\"Plan B\"",[330,1002,1003],{"class":335},"])\n",[330,1005,1006,1009,1011,1014,1017,1019,1022],{"class":197,"line":375},[330,1007,1008],{"class":335},"pred ",[330,1010,340],{"class":339},[330,1012,1013],{"class":335}," model.get_prediction(plan).summary_frame(",[330,1015,1016],{"class":353},"alpha",[330,1018,340],{"class":339},[330,1020,1021],{"class":359},"0.05",[330,1023,363],{"class":335},[330,1025,1026,1029,1031,1034,1037,1039,1042,1044,1047,1050,1052],{"class":197,"line":477},[330,1027,1028],{"class":335},"forecast ",[330,1030,340],{"class":339},[330,1032,1033],{"class":335}," pred[[",[330,1035,1036],{"class":346},"\"mean\"",[330,1038,350],{"class":335},[330,1040,1041],{"class":346},"\"obs_ci_lower\"",[330,1043,350],{"class":335},[330,1045,1046],{"class":346},"\"obs_ci_upper\"",[330,1048,1049],{"class":335},"]].round(",[330,1051,93],{"class":359},[330,1053,363],{"class":335},[330,1055,1056,1059,1061,1064,1067,1069,1071,1073,1075],{"class":197,"line":650},[330,1057,1058],{"class":335},"forecast.columns ",[330,1060,340],{"class":339},[330,1062,1063],{"class":335}," [",[330,1065,1066],{"class":346},"\"Forecast\"",[330,1068,350],{"class":335},[330,1070,630],{"class":346},[330,1072,350],{"class":335},[330,1074,640],{"class":346},[330,1076,1077],{"class":335},"]\n",[330,1079,1080,1082],{"class":197,"line":711},[330,1081,369],{"class":359},[330,1083,1084],{"class":335},"(forecast.to_string())\n",[10,1086,1087,1090,1091,1094,1095,1098,1099,1102],{},[14,1088,1089],{},"obs_ci_lower"," and ",[14,1092,1093],{},"obs_ci_upper"," are the prediction interval for one new month; that is the range to plan stock against. The narrower ",[14,1096,1097],{},"mean_ci_*"," columns describe the average and are rarely what a planner wants. Point the plan's inputs at cells, with ",[14,1100,1101],{},"xl(\"B40:D41\", headers=True)",", and the forecast updates whenever someone types a new scenario.",[32,1104,1106],{"id":1105},"step-5-check-the-residuals","Step 5 — Check the residuals",[10,1108,1109],{},"A regression can report a high R² and still be wrong: a curve where the model assumes a line, a trend over time the predictors do not explain, a few outliers carrying the fit. Plot residuals against fitted values and against time. Return the figure from the cell and Excel shows it as an image:",[321,1111,1113],{"className":323,"code":1112,"language":325,"meta":326,"style":326},"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 3.2))\nax1.scatter(model.fittedvalues, model.resid, s=14)\nax1.axhline(0, color=\"grey\", lw=1)\nax1.set(xlabel=\"Fitted units\", ylabel=\"Residual\", title=\"Residuals vs fitted\")\nax2.plot(sales[\"Month\"], model.resid, marker=\"o\", ms=3)\nax2.axhline(0, color=\"grey\", lw=1)\nax2.set(title=\"Residuals over time\")\nax2.set_xticks(sales[\"Month\"][::6])\nfig.tight_layout()\nprint(f\"residual std error: {np.sqrt(model.scale):.2f} units; largest miss: {model.resid.abs().max():.0f} units\")\nfig  # the last expression is what the cell returns\n",[14,1114,1115,1151,1166,1194,1228,1258,1283,1298,1314,1320,1360],{"__ignoreMap":326},[330,1116,1117,1120,1122,1125,1127,1129,1131,1133,1136,1138,1140,1143,1145,1148],{"class":197,"line":332},[330,1118,1119],{"class":335},"fig, (ax1, ax2) ",[330,1121,340],{"class":339},[330,1123,1124],{"class":335}," plt.subplots(",[330,1126,110],{"class":359},[330,1128,350],{"class":335},[330,1130,682],{"class":359},[330,1132,350],{"class":335},[330,1134,1135],{"class":353},"figsize",[330,1137,340],{"class":339},[330,1139,482],{"class":335},[330,1141,1142],{"class":359},"9",[330,1144,350],{"class":335},[330,1146,1147],{"class":359},"3.2",[330,1149,1150],{"class":335},"))\n",[330,1152,1153,1156,1159,1161,1164],{"class":197,"line":366},[330,1154,1155],{"class":335},"ax1.scatter(model.fittedvalues, model.resid, ",[330,1157,1158],{"class":353},"s",[330,1160,340],{"class":339},[330,1162,1163],{"class":359},"14",[330,1165,363],{"class":335},[330,1167,1168,1171,1173,1175,1178,1180,1183,1185,1188,1190,1192],{"class":197,"line":375},[330,1169,1170],{"class":335},"ax1.axhline(",[330,1172,93],{"class":359},[330,1174,350],{"class":335},[330,1176,1177],{"class":353},"color",[330,1179,340],{"class":339},[330,1181,1182],{"class":346},"\"grey\"",[330,1184,350],{"class":335},[330,1186,1187],{"class":353},"lw",[330,1189,340],{"class":339},[330,1191,110],{"class":359},[330,1193,363],{"class":335},[330,1195,1196,1199,1202,1204,1207,1209,1212,1214,1217,1219,1221,1223,1226],{"class":197,"line":477},[330,1197,1198],{"class":335},"ax1.set(",[330,1200,1201],{"class":353},"xlabel",[330,1203,340],{"class":339},[330,1205,1206],{"class":346},"\"Fitted units\"",[330,1208,350],{"class":335},[330,1210,1211],{"class":353},"ylabel",[330,1213,340],{"class":339},[330,1215,1216],{"class":346},"\"Residual\"",[330,1218,350],{"class":335},[330,1220,149],{"class":353},[330,1222,340],{"class":339},[330,1224,1225],{"class":346},"\"Residuals vs fitted\"",[330,1227,363],{"class":335},[330,1229,1230,1233,1236,1239,1242,1244,1247,1249,1252,1254,1256],{"class":197,"line":650},[330,1231,1232],{"class":335},"ax2.plot(sales[",[330,1234,1235],{"class":346},"\"Month\"",[330,1237,1238],{"class":335},"], model.resid, ",[330,1240,1241],{"class":353},"marker",[330,1243,340],{"class":339},[330,1245,1246],{"class":346},"\"o\"",[330,1248,350],{"class":335},[330,1250,1251],{"class":353},"ms",[330,1253,340],{"class":339},[330,1255,664],{"class":359},[330,1257,363],{"class":335},[330,1259,1260,1263,1265,1267,1269,1271,1273,1275,1277,1279,1281],{"class":197,"line":711},[330,1261,1262],{"class":335},"ax2.axhline(",[330,1264,93],{"class":359},[330,1266,350],{"class":335},[330,1268,1177],{"class":353},[330,1270,340],{"class":339},[330,1272,1182],{"class":346},[330,1274,350],{"class":335},[330,1276,1187],{"class":353},[330,1278,340],{"class":339},[330,1280,110],{"class":359},[330,1282,363],{"class":335},[330,1284,1286,1289,1291,1293,1296],{"class":197,"line":1285},7,[330,1287,1288],{"class":335},"ax2.set(",[330,1290,149],{"class":353},[330,1292,340],{"class":339},[330,1294,1295],{"class":346},"\"Residuals over time\"",[330,1297,363],{"class":335},[330,1299,1301,1304,1306,1309,1312],{"class":197,"line":1300},8,[330,1302,1303],{"class":335},"ax2.set_xticks(sales[",[330,1305,1235],{"class":346},[330,1307,1308],{"class":335},"][::",[330,1310,1311],{"class":359},"6",[330,1313,1003],{"class":335},[330,1315,1317],{"class":197,"line":1316},9,[330,1318,1319],{"class":335},"fig.tight_layout()\n",[330,1321,1323,1325,1327,1329,1332,1334,1337,1340,1342,1345,1347,1350,1353,1355,1358],{"class":197,"line":1322},10,[330,1324,369],{"class":359},[330,1326,482],{"class":335},[330,1328,485],{"class":339},[330,1330,1331],{"class":346},"\"residual std error: ",[330,1333,492],{"class":491},[330,1335,1336],{"class":335},"np.sqrt(model.scale)",[330,1338,1339],{"class":339},":.2f",[330,1341,501],{"class":491},[330,1343,1344],{"class":346}," units; largest miss: ",[330,1346,492],{"class":491},[330,1348,1349],{"class":335},"model.resid.abs().max()",[330,1351,1352],{"class":339},":.0f",[330,1354,501],{"class":491},[330,1356,1357],{"class":346}," units\"",[330,1359,363],{"class":335},[330,1361,1363,1366],{"class":197,"line":1362},11,[330,1364,1365],{"class":335},"fig  ",[330,1367,1369],{"class":1368},"s-wDw","# the last expression is what the cell returns\n",[10,1371,1372,1373,1376,1377,1381],{},"What you want to see is a shapeless band around zero in both panels. A funnel shape means the error grows with the level, a curve means a missing term such as ",[14,1374,1375],{},"I(Price**2)",", and a drift over time means something seasonal or trending is not in the model. ",[26,1378,1380],{"href":1379},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Fplot-charts-with-matplotlib-in-python-in-excel\u002F","Plot Charts with matplotlib in Python in Excel"," covers sizing the image in the cell.",[138,1383,147,1388,147,1391,147,1394,147,1397,147,1401,147,1408,147,1414,147,1418,147,1423,147,1427,147,1430,147,1436,147,1440,147,1444,147,1447,147,1450,147,1455,147,1461,147,1466,147,1470,147,1473],{"viewBox":1384,"role":141,"ariaLabelledBy":1385,"xmlns":145,"style":146},"4 0 752 181",[1386,1387],"rg-resid-t","rg-resid-d",[149,1389,1390],{"id":1386},"What the residual plot is telling you",[153,1392,1393],{"id":1387},"Three panels. A shapeless band around zero means the model is adequate. A funnel that widens with the fitted value means the error grows with the level, so consider modelling the logarithm. A curve means a missing term, such as a squared price.",[157,1395],{"x":159,"y":93,"width":160,"height":1396,"fill":162},"181",[157,1398],{"x":165,"y":166,"width":1399,"height":1400,"rx":1163,"fill":237,"stroke":238,"style":172},"228.0","149.0",[174,1402,1404],{"transform":1403},"translate(121.0,29.0) scale(1.083)",[178,1405],{"d":1406,"fill":181,"stroke":245,"style":1407},"M5 12.5l4.5 4.5L19 7","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[185,1409,1413],{"x":1410,"y":1411,"style":1412},"134.0","78.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Shapeless band",[197,1415],{"x1":1416,"y1":825,"x2":1417,"y2":825,"stroke":238,"style":743},"36.0","232.0",[185,1419,1422],{"x":1410,"y":1420,"style":1421},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","the model is adequate",[185,1424,1426],{"x":1410,"y":1425,"style":1421},"137.0","use the intervals as they are",[157,1428],{"x":1429,"y":166,"width":1399,"height":1400,"rx":1163,"fill":287,"stroke":288,"style":172},"266.0",[174,1431,1433],{"transform":1432},"translate(367.0,29.0) scale(1.083)",[178,1434],{"d":1435,"fill":181,"stroke":295,"style":1407},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4",[185,1437,1439],{"x":248,"y":1411,"style":1438},"font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Funnel",[197,1441],{"x1":1442,"y1":825,"x2":1443,"y2":825,"stroke":288,"style":743},"282.0","478.0",[185,1445,1446],{"x":248,"y":1420,"style":1421},"error grows with level",[185,1448,1449],{"x":248,"y":1425,"style":1421},"try np.log(Units)",[157,1451],{"x":1452,"y":166,"width":1399,"height":1400,"rx":1163,"fill":1453,"stroke":1454,"style":172},"512.0","#fce9e9","var(--danger,#dc2626)",[174,1456,1458],{"transform":1457},"translate(613.0,29.0) scale(1.083)",[178,1459],{"d":269,"fill":181,"stroke":1460,"style":1407},"var(--accent-ink,#be185d)",[185,1462,1465],{"x":1463,"y":1411,"style":1464},"626.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Curve",[197,1467],{"x1":1468,"y1":825,"x2":1469,"y2":825,"stroke":1454,"style":743},"528.0","724.0",[185,1471,1472],{"x":1463,"y":1420,"style":1421},"a missing term",[185,1474,1475],{"x":1463,"y":1425,"style":1421},"try I(Price**2)",[32,1477,1479],{"id":1478},"step-6-confirm-against-linest","Step 6 — Confirm against LINEST",[10,1481,1482,1483,1486],{},"It is worth checking once that the Python fit and Excel agree, especially before a model replaces a spreadsheet someone trusts. Enter ",[14,1484,1485],{},"=LINEST(Sales[Units], Sales[[AdSpend]:[Promo]], TRUE, TRUE)"," in an empty cell; it spills a five-row block. For this table the first two rows are:",[52,1488,1489,1503],{},[55,1490,1491],{},[58,1492,1493,1495,1497,1499,1501],{},[61,1494],{},[61,1496,72],{},[61,1498,69],{},[61,1500,66],{},[61,1502,799],{},[77,1504,1505,1522],{},[58,1506,1507,1510,1513,1516,1519],{},[82,1508,1509],{},"Coefficient",[82,1511,1512],{},"44.31",[82,1514,1515],{},"-64.3783",[82,1517,1518],{},"14.1728",[82,1520,1521],{},"939.1498",[58,1523,1524,1526,1529,1532,1535],{},[82,1525,770],{},[82,1527,1528],{},"10.9806",[82,1530,1531],{},"4.9878",[82,1533,1534],{},"0.6166",[82,1536,1537],{},"54.9803",[10,1539,1540,1541,1544,1545,1547,1548,1552],{},"These are the same numbers as the statsmodels table, but in ",[555,1542,1543],{},"reverse"," column order with the intercept last, which is the usual source of \"the numbers don't match\" when people compare. The third row starts with R² (0.9623) and the standard error of the estimate (31.98), matching ",[14,1546,495],{}," and the residual standard error above. ",[26,1549,1551],{"href":1550},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fcorrel-and-linest-equivalents-in-python\u002F","CORREL and LINEST Equivalents in Python"," goes through the rest of the block.",[32,1554,1556],{"id":1555},"categorical-predictors-and-interactions","Categorical predictors and interactions",[10,1558,1559,1560,1562,1563,1566],{},"The formula interface handles what ",[14,1561,16],{}," makes painful. A text column such as region becomes a set of indicator columns with ",[14,1564,1565],{},"C()",", and an interaction asks whether the promotion works better when advertising is high:",[321,1568,1570],{"className":323,"code":1569,"language":325,"meta":326,"style":326},"sales[\"Quarter\"] = \"Q\" + pd.PeriodIndex(sales[\"Month\"], freq=\"M\").quarter.astype(str)\nm2 = smf.ols(\"Units ~ AdSpend + Price + Promo + C(Quarter) + Promo:AdSpend\", data=sales).fit()\nprint(m2.params.round(2).to_string())\nprint(f\"adjusted R² {m2.rsquared_adj:.4f} vs {model.rsquared_adj:.4f} without the extra terms\")\n",[14,1571,1572,1613,1633,1645],{"__ignoreMap":326},[330,1573,1574,1577,1580,1583,1585,1588,1591,1594,1596,1598,1601,1603,1606,1609,1611],{"class":197,"line":332},[330,1575,1576],{"class":335},"sales[",[330,1578,1579],{"class":346},"\"Quarter\"",[330,1581,1582],{"class":335},"] ",[330,1584,340],{"class":339},[330,1586,1587],{"class":346}," \"Q\"",[330,1589,1590],{"class":339}," +",[330,1592,1593],{"class":335}," pd.PeriodIndex(sales[",[330,1595,1235],{"class":346},[330,1597,953],{"class":335},[330,1599,1600],{"class":353},"freq",[330,1602,340],{"class":339},[330,1604,1605],{"class":346},"\"M\"",[330,1607,1608],{"class":335},").quarter.astype(",[330,1610,401],{"class":359},[330,1612,363],{"class":335},[330,1614,1615,1618,1620,1622,1625,1627,1629,1631],{"class":197,"line":366},[330,1616,1617],{"class":335},"m2 ",[330,1619,340],{"class":339},[330,1621,461],{"class":335},[330,1623,1624],{"class":346},"\"Units ~ AdSpend + Price + Promo + C(Quarter) + Promo:AdSpend\"",[330,1626,350],{"class":335},[330,1628,469],{"class":353},[330,1630,340],{"class":339},[330,1632,474],{"class":335},[330,1634,1635,1637,1640,1642],{"class":197,"line":375},[330,1636,369],{"class":359},[330,1638,1639],{"class":335},"(m2.params.round(",[330,1641,682],{"class":359},[330,1643,1644],{"class":335},").to_string())\n",[330,1646,1647,1649,1651,1653,1656,1658,1661,1663,1665,1668,1670,1672,1674,1676,1679],{"class":197,"line":477},[330,1648,369],{"class":359},[330,1650,482],{"class":335},[330,1652,485],{"class":339},[330,1654,1655],{"class":346},"\"adjusted R² ",[330,1657,492],{"class":491},[330,1659,1660],{"class":335},"m2.rsquared_adj",[330,1662,498],{"class":339},[330,1664,501],{"class":491},[330,1666,1667],{"class":346}," vs ",[330,1669,492],{"class":491},[330,1671,509],{"class":335},[330,1673,498],{"class":339},[330,1675,501],{"class":491},[330,1677,1678],{"class":346}," without the extra terms\"",[330,1680,363],{"class":335},[10,1682,1683,1684,1687,1688,1691,1692,1694],{},"Adjusted R² is the fair comparison here, because plain R² always rises when terms are added. It rose a little, from 0.9588 to 0.9654, but that is not the whole story: print ",[14,1685,1686],{},"m2.conf_int()"," and the 95% intervals for every quarter term and for ",[14,1689,1690],{},"Promo:AdSpend"," (about -4.5 to 0.7) include zero. With three years of data there is no reliable evidence of a seasonal pattern or of the promotion interacting with advertising, so the simpler model is the one to report. Note too that with an interaction in the model, the ",[14,1693,72],{}," coefficient on its own means the promotion effect at zero advertising, not the average effect.",[32,1696,1698],{"id":1697},"common-pitfalls","Common pitfalls",[1700,1701,1702,1717,1727,1733,1739,1745],"ul",{},[1703,1704,1705,1709,1710,1713,1714,1716],"li",{},[1706,1707,1708],"strong",{},"Text in numeric columns."," A single ",[14,1711,1712],{},"\"n\u002Fa\""," makes the column ",[14,1715,398],{},"; convert and count what became missing before fitting.",[1703,1718,1719,1722,1723,1726],{},[1706,1720,1721],{},"Missing values dropped silently."," statsmodels' formula interface drops rows with missing values by default; compare ",[14,1724,1725],{},"model.nobs"," with the table's row count.",[1703,1728,1729,1732],{},[1706,1730,1731],{},"Reading causation into coefficients."," The model shows association in this data. Advertising may have been raised in months that were going to be strong anyway.",[1703,1734,1735,1738],{},[1706,1736,1737],{},"Extrapolating."," Predictions for inputs outside the range in the table (here advertising 10–40, price 8–12) are guesses the model cannot check.",[1703,1740,1741,1744],{},[1706,1742,1743],{},"Comparing with LINEST column by column."," LINEST reverses the predictor order; match by name.",[1703,1746,1747,1753],{},[1706,1748,1749,1750,30],{},"Returning ",[14,1751,1752],{},"summary()"," It lands as text in one cell; return DataFrames and switch the output to values.",[32,1755,1757],{"id":1756},"conclusion","Conclusion",[10,1759,1760,1761,1763,1764,1767,1768,1771,1772,1775,1776,1778,1779,1781],{},"Read the table with ",[14,1762,414],{},", fit with ",[14,1765,1766],{},"smf.ols(\"y ~ x1 + x2\", data=...)"," in its own cell, and return results as DataFrames — the ",[14,1769,1770],{},"summary2()"," coefficient table, ",[14,1773,1774],{},"get_prediction(...).summary_frame()"," for forecasts with intervals — spilled as Excel values. Check the fit with residual plots before trusting it, and confirm once against ",[14,1777,16],{},", remembering its reversed order. Add categorical terms with ",[14,1780,1565],{}," and judge extra terms with adjusted R².",[32,1783,1785],{"id":1784},"frequently-asked-questions","Frequently asked questions",[10,1787,1788,1791],{},[1706,1789,1790],{},"Is statsmodels available in Python in Excel?","\nYes. It is part of the Anaconda runtime Microsoft provides, and statsmodels.api is imported as sm by default. Import statsmodels.formula.api as smf yourself to use formulas.",[10,1793,1794,1797],{},[1706,1795,1796],{},"Why do my Python and LINEST coefficients look different?","\nLINEST returns coefficients in reverse order of the predictor columns, with the intercept last. Matched by name, the estimates, standard errors and R² are the same for ordinary least squares.",[10,1799,1800,1803,1804,1806],{},[1706,1801,1802],{},"How do I show the regression output in cells instead of a Python object?","\nReturn a DataFrame, such as model.summary2().tables",[330,1805,110],{},", and switch the PY cell's output to Excel value. The table spills into the grid with labels.",[10,1808,1809,1812],{},[1706,1810,1811],{},"Can the regression update when the data changes?","\nYes. PY cells recalculate like formulas, so adding a month to the Sales table refits the model and updates every cell that uses it.",[32,1814,1816],{"id":1815},"related","Related",[1700,1818,1819,1825,1830,1835,1840],{},[1703,1820,1821,1822,1824],{},"Up one level: ",[26,1823,29],{"href":28}," — the =PY function and how cells work together.",[1703,1826,1827,1829],{},[26,1828,1551],{"href":1550}," — the same statistics outside Excel.",[1703,1831,1832,1834],{},[26,1833,542],{"href":541}," — spilling results into the grid.",[1703,1836,1837,1839],{},[26,1838,1380],{"href":1379}," — charts from PY cells.",[1703,1841,1842,1846],{},[26,1843,1845],{"href":1844},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Fpython-in-excel-vs-openpyxl-and-xlwings\u002F","Python in Excel vs openpyxl and xlwings"," — when a script is the better home.",[1848,1849,1850],"style",{},"html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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 .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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":326,"searchDepth":366,"depth":366,"links":1852},[1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864],{"id":34,"depth":366,"text":35},{"id":312,"depth":366,"text":313},{"id":418,"depth":366,"text":419},{"id":546,"depth":366,"text":547},{"id":914,"depth":366,"text":915},{"id":1105,"depth":366,"text":1106},{"id":1478,"depth":366,"text":1479},{"id":1555,"depth":366,"text":1556},{"id":1697,"depth":366,"text":1698},{"id":1756,"depth":366,"text":1757},{"id":1784,"depth":366,"text":1785},{"id":1815,"depth":366,"text":1816},"2026-09-29","Fit a multiple linear regression in a =PY cell with statsmodels: read a table with xl(), fit with a formula, return a tidy coefficient table to the grid, predict with intervals, check the residuals with a chart, and confirm the numbers against LINEST.","md",[1869,1871,1873,1875],{"q":1790,"a":1870},"Yes. It is part of the Anaconda runtime Microsoft provides, and statsmodels.api is imported as sm by default. Import statsmodels.formula.api as smf yourself to use formulas.",{"q":1796,"a":1872},"LINEST returns coefficients in reverse order of the predictor columns, with the intercept last. Matched by name, the estimates, standard errors and R² are the same for ordinary least squares.",{"q":1802,"a":1874},"Return a DataFrame, such as model.summary2().tables[1], and switch the PY cell's output to Excel value. The table spills into the grid with labels.",{"q":1811,"a":1876},"Yes. PY cells recalculate like formulas, so adding a month to the Sales table refits the model and updates every cell that uses it.","run a regression with python in excel",null,{"breadcrumb":1880},[1881,1884,1887],{"name":1882,"item":1883},"Home","\u002F",{"name":1885,"item":1886},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":29,"item":28},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Frun-a-regression-with-python-in-excel",{"title":1890,"description":1891},"Run a Linear Regression in Python in Excel (statsmodels)","Multiple regression in Python in Excel with statsmodels OLS: read the table with xl(), fit Units ~ AdSpend + Price + Promo, spill coefficients and R² to cells, predict next month with an interval, plot residuals, and match LINEST.","run-a-regression-with-python-in-excel","getting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Frun-a-regression-with-python-in-excel\u002Findex",[1895,1896],"python-in-excel","formula-equivalents","how-to","loY4N-tk4Tn_X87JsB_nlDXfHMBOdLaBH0U0ibgPW5c",[1900,1903],{"title":542,"path":1901,"stem":1902,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Freturn-dataframes-and-values-from-python-in-excel-cells","getting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Freturn-dataframes-and-values-from-python-in-excel-cells\u002Findex",{"title":1904,"path":1905,"stem":1906,"children":-1},"Working with Excel Formulas in Python","\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python","getting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Findex",[1908,1910],{"path":1909,"title":1885},"\u002Fgetting-started-with-python-excel-automation",{"path":1911,"title":29},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel",[],[1914,1917,1921,1924,1927,1929],{"path":1915,"title":1916,"kind":1897},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Fclean-data-with-pandas-in-python-in-excel","Clean Data with pandas in Python in Excel",{"path":1918,"title":1919,"kind":1920},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Ffix-python-in-excel-errors","Fix Python in Excel Errors","fix",{"path":1922,"title":1923,"kind":1897},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Fforecast-a-time-series-with-python-in-excel","Forecast a Time Series with Python in Excel",{"path":1925,"title":1926,"kind":1897},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Fget-external-data-into-python-in-excel","Get External Data into Python in Excel with Power Query",{"path":1928,"title":1380,"kind":1897},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Fplot-charts-with-matplotlib-in-python-in-excel",{"path":1930,"title":1845,"kind":1931},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002Fpython-in-excel-vs-openpyxl-and-xlwings","compare",1790972075484]