User manual › Curve fitting in the command window
kind: manual#console#curve-fitting#fit#fittype#fitoptions#spline#csapi#csape#spaps#manual

Curve fitting in the command window#

Every name on this page is typed at the command window (or passed to ICoreBlocks --console "<line>") and answers what MATLAB's Curve Fitting Toolbox answers for the same arguments — the same model library, the same defaults, the same start points, and the same refusals. If you have MATLAB code that fits a curve and reads its coefficients and bounds, it should paste in and run.

That promise is harder here than for most toolboxes, and it is worth saying why before the first example.

The start point is the answer, not a detail#

A nonlinear fit finds a local minimum. Two programs that agree exactly on the model and disagree on where the iteration started will answer different numbers, and both of them are correct least-squares fits — just of different basins. So matching MATLAB here means matching the rule it uses to pick the start, not only the criterion it then minimises.

Those rules are transcribed for every library model that has one: the geometric-sequence estimate behind exp1, the four-partition quadratic behind exp2, the mean-of-logs behind power1 and power2, the peel-a-peak loop behind gauss1…gauss8, and the FFT peak searches behind sin1…sin8 and fourier1…fourier8.

⚠ Three families have no rule at all — in MATLAB either. rat11…rat55, weibull, and any fittype you write yourself have no start-point routine, so MATLAB draws a random one and warns you that it did. Here they start at 1 for every coefficient, deterministically, so the same line answers the same numbers twice. Give a "StartPoint" when the answer matters:

fit(x, y, ft, "StartPoint", [1, 1])

Your first fit#

>>> x = transpose([0,1,2,3,4,5,6,7,8,9,10]); y = transpose([0.2,1.1,1.9,3.2,3.9,5.1,5.8,7.2,8.1,8.9,10.2]); f = fit(x, y, "poly1")
f = 
     Linear model Poly1:
     f(x) = p1*x + p2
     Coefficients (with 95% confidence bounds):
       p1 =      0.9973  (0.9631, 1.031)
       p2 =     0.06818  (-0.1342, 0.2706)  # Curve Fit

That display is MATLAB's, line for line. The value itself is then read the way MATLAB reads a cfit:

>>> coeffvalues(f)
[[0.997273, 0.0681818]]  # Matrix of Double

>>> f(2.5)
2.56136  # Double

>>> f.p1()
0.997273  # Double

⚠ f.p1 needs its parentheses here. Every member on this console is a call — f.p1(), not f.p1 — which is the same rule a model's .Ts() follows. The value is the same one MATLAB's f.p1 gives.

⚠ x and y must be COLUMNS, and the error says so in MATLAB's own words (X must be a matrix with one or two columns.). That is what prepareCurveData is for: it column-ises, drops non-finite points pairwise so the two vectors stay aligned, and hands back what fit wants. (prepareSurfaceData does the same for a surface's three vectors, expanding two header vectors and a table of values the way meshgrid would.)

The model library#

FamilyNamesFitted by
Polynomialpoly1 … poly9one least-squares solve — exact
Exponentialexp1, exp2iteration from MATLAB's own start
Powerpower1, power2iteration from MATLAB's own start
Rationalrat11 … rat55iteration from a start of ones (see above)
Fourierfourier1 … fourier8iteration from the FFT peak
Gaussiangauss1 … gauss8iteration, one peak at a time
Sum of sinessin1 … sin8iteration from the FFT peaks
Weibullweibulliteration from a start of ones (see above)
Smoothingsmooth(y, span, method)six local fits, no model object

Write your own with fittype:

>>> ft = fittype("a*exp(-b*x)")
ft = 
     General model:
     ft(a,b,x) = a*exp(-b*x)  # Curve Fit Model

⚠ The coefficients are the free names in ALPHABETICAL order, which is MATLAB's rule and the trap for a "StartPoint" vector written in the order you typed the terms:

>>> coeffnames(fittype("c*x^2 + a*x + b"))
'a b c'  # String

So "StartPoint", [1, 2, 3] sets a = 1, b = 2, c = 3 — not the order the expression reads in.

Bounds, derivatives and integrals#

>>> confint(f)
[[0.963059, -0.134227], [1.03149, 0.270591]]  # Matrix of Double

>>> predint(f, [1; 5])
[[0.666462, 1.46445], [4.67976, 5.42933]]  # Matrix of Double

>>> differentiate(f, [1; 5])
[[0.997273], [0.997273]]  # Matrix of Double

>>> integrate(f, [1; 5], 0)
[[0.566818], [12.8068]]  # Matrix of Double

confint answers 2 rows by one column per coefficient — lower row then upper — and takes a level: confint(f, 0.9). predint answers one row per point and takes all four of MATLAB's interval kinds:

predint(f, xq, level, "observation" | "functional", "on" | "off")

"observation" (the default) predicts a new measurement, "functional" the fitted curve; "on" makes the band simultaneous over every point.

⚠ confint and predint refuse an interpolant and a spline, on both sides — there are no fitted coefficients to bound. And a coefficient you pinned at a bound comes back NaN, because it was not estimated.

differentiate is exact for every library model — each carries its own derivative — and answers a second output too: [d1, d2] = differentiate(f, xq). integrate(f, xq, x0) integrates from x0 to each query point, in closed form where the model has one.

Options: fitoptions#

An option set is a string carrying its own call, the same shape odeset and optimoptions use on this console:

>>> fitoptions("Method", "NonlinearLeastSquares", "StartPoint", [1, 1])
'fitoptions('StartPoint',[1 1],'Method','NonlinearLeastSquares')'  # String

>>> fitoptions(fittype("poly2"))
'fitoptions('Method','LinearLeastSquares')'  # String

You can pass the set to fit, or pass the same names directly on the call — fit(x, y, "poly2", "Normalize", "on") is the shorter spelling of the same thing.

⚠ The property list is per METHOD, and a name off it is refused rather than ignored. "StartPoint" really is not a LinearLeastSquares property, and a linear fit that accepted one and then dropped it would be the one answer that cannot be right. The message names the method, as MATLAB's does.

Outliers: three different answers to the same data#

Move one point of that straight line a long way off it and the plain fit follows it:

>>> coeffvalues(fit(x, yr, "poly1"))
[[0.923636, 1.17273]]  # Matrix of Double

"Robust" discounts it, "Exclude" drops it, and a weight would pull the line towards it:

>>> coeffvalues(fit(x, yr, "poly1", "Robust", "Bisquare"))
[[0.995658, 0.0957771]]  # Matrix of Double

>>> coeffvalues(fit(x, yr, "poly1", "Exclude", m))
[[0.995684, 0.092011]]  # Matrix of Double

⚠ "Exclude" takes the points to LEAVE OUT — true means excluded, which is the opposite of what the name suggests to most people. excludedata builds that mask for you from a range, a domain, a box or a list of indices.

⚠ "Robust" is "Bisquare" or "LAR", and they are genuinely different weightings, not two names for one. Both match MATLAB, including the details that are MATLAB's own rather than the textbook's — the leverage adjustment on each residual, the scale estimate taken after discarding the smallest residuals, and the floor under that scale that stops a near-perfect fit from calling every point an outlier.

Normalising#

>>> coeffvalues(fit(x, y, "poly2", "Normalize", "on"))
[[0.0679487, 3.30758, 4.99277]]  # Matrix of Double

"Normalize", "on" centres and scales x before fitting, which is worth doing for a high-order polynomial over a wide range. The coefficients are then of a different polynomial — the curve is the same, the numbers are not — and the display says which two numbers were used, so a fit always tells you whether it was normalised.

Interpolants and the smoothing spline#

Five of the model names do not fit coefficients at all — they build a piecewise polynomial through (or near) your points:

NameWhat it is
"cubicinterp"the not-a-knot cubic — the same curve spline(x, y, xq) gives
"pchipinterp"the shape-preserving cubic — the same curve pchip gives
"linearinterp"straight lines between the points, extrapolating past the ends
"nearestinterp"the nearest sample, clamping past the ends; a tie takes the sample above
"smoothingspline"the cubic smoothing spline, with "SmoothingParam" between 0 and 1

⚠ The two cheap interpolants differ only at the ends, and it is worth knowing which way round: "linearinterp" continues the end segment, and "nearestinterp" holds the end value. Inside the data they agree with what their names say.

"smoothingspline" with no "SmoothingParam" picks one by MATLAB's own rule — p = 1 interpolates, p = 0 is the least-squares straight line, and the automatic choice sits where the two trade off (0.9 on an evenly spaced grid). csaps is the same curve under its own name, and [f, p] = csaps(x, y) hands back the parameter it chose:

csaps(x, y)              the fit, with the automatic p
csaps(x, y, p)           the fit, with your p
csaps(x, y, p, xq)       the smoothed VALUES at xq

⚠ These five have no coefficients, so coeffvalues, confint and predint all refuse them by name — as MATLAB's do. differentiate and integrate still work and are exact, because the derivative of a piecewise polynomial is another one.

The spline forms: pp and B-#

Everything above hands you a fit — a model with named coefficients. The spline half of the toolbox hands you a form instead: the piecewise polynomial itself, as its breaks and its coefficients, which you can then evaluate, differentiate, integrate or convert.

A form is a struct on both sides, with MATLAB's own field names:

> x = [0, 1, 2, 3, 4, 5]; y = [0, 0.8, 0.9, 0.1, -0.8, -1]; p = csapi(x, y)
p =
    breaks: [1×6 double]
     coefs: [5×4 double]
    pieces: 5
     order: 4
       dim: 1  # Struct

Read a field with p.breaks, p.coefs, p.pieces or p.order, and evaluate with fnval:

> fnval(p, [0.5, 2.5, 4.5])
[[0.458333, 0.575, -1.03333]]  # Matrix of Double

⚠ p.form refuses. It is the character string 'pp' in MATLAB, and a struct here holds numbers only — so the refusal says which rule it met and how to tell the two forms apart: a form with breaks is a pp, and one with knots is a B-form.

Building one#

CallWhat you get
csapi(x, y)the not-a-knot cubic interpolant — the same curve as spline
csapi(x, y, xx)its values at xx, which is what csapi.m itself does
csape(x, y)the cubic interpolant with estimated end slopes
csape(x, y, conds)"complete"/"clamped", "not-a-knot", "periodic", "second", "variational", or the pair [left right] of 0, 1, 2
csape(x, y, conds, valconds)the same, with the two end values you name
spap2(knots, k, x, y)the least-squares spline of order k
spap2(pieces, k, x, y)the same, with the knots chosen for you
spaps(x, y, tol)the smoothing spline that keeps the error at tol
ppmak(breaks, coefs, 1)a pp you built yourself
spmak(knots, coefs)a B-form you built yourself

⚠ csape(x, y) does not clamp the end slopes to zero. With no end values it estimates each one by local interpolation through the first three or four points — so csape(x, y) and csape(x, y, "complete", [0, 0]) are different curves, and the difference is visible in the first coefficient row. This is MATLAB's behaviour, transcribed; it surprises people in both consoles equally.

⚠ csape reads the first letter of the condition word and nothing else, so "clamped" and "complete" are one condition, and so are "second" and anything else beginning with s.

The smoothing spline by tolerance#

spaps is csaps's dual: csaps takes the smoothing parameter and spaps takes the error you are willing to accept, then finds the parameter that delivers it.

> [sp, values, rho] = spaps(x, y, 0.05)
sp =
     knots: [1×12 double]
     coefs: [1×8 double]
    number: 8
     order: 4
       dim: 1  # Struct
values = [[0.146016, 0.7527, 0.786114, 0.117646, -0.669475, -1.11999]]  # Matrix of Double
rho = 8.80269  # Double

values is the smoothed data at your own sites, and rho is what the search settled on. Three things about it are worth knowing before you compare numbers with anyone:

  • The default weights are the trapezoidal rule, not ones. The first and last samples carry half the weight of an interior one. Pass w yourself if that is not what you want — and expect different coefficients when you do.
  • A negative tol names rho directly and skips the search entirely.
  • m — the fourth-and-fifth-argument form spaps(x, y, tol, w, m) — is the derivative whose size is being penalised: 1 gives a piecewise linear answer, 2 (the default) the usual cubic smoothing spline, 3 a quintic.

Reading, differentiating, converting#

fnval evaluates a form, fnder differentiates it, fnint integrates it, and fn2fm converts between the two forms:

CallWhat it does
fnval(f, xx)evaluate — the two arguments work in either order, as in MATLAB
fnder(f)differentiate once; fnder(f, n) differentiates n times
fnder(f, -1)a negative order integrates, and answers exactly what fnint does
fnint(f)integrate; fnint(f, value) starts the integral from value
fn2fm(f, "B-")the same spline in B-form
fn2fm(f, "pp")and back
fn2fm(f, "B-", sconds)the same conversion with the smoothness at each interior break named, rather than guessed

Each of them answers a form of its own, so fnder(csapi(x, y)) is a pp one order lower and fnint(spaps(x, y, tol)) is a B-form one order higher.

⚠ Converting a pp to a B-form drops the breaks the spline is smooth across, which is not a bug and not an optimisation — it is what the B-form means. The not-a-knot cubic through six points is three times differentiable at the second and second-to-last break, so those two breaks need no knot at all:

> b = fn2fm(csapi(x, y), "B-"); b.knots
[[0, 0, 0, 0, 2, 3, 5, 5, 5, 5]]  # Matrix of Double

Six breaks in, four distinct knots out. Round-tripping back through fn2fm(…, "pp") gives you the same curve to the last digit.

The third argument is how you take that decision yourself, and it is two arguments in one — MATLAB's own rule: a single number strictly between 0 and 1 is the tolerance the guess uses, and anything else is the list of smoothness counts, one per interior break. fn2fm(pp, "B-", [3 3 3 3]) keeps every interior break as a single knot; [0 0 0 0] gives each of them four, which is the same curve written as one Bézier piece per interval.

What the spline forms will not do#

MATLABHere
f.formRefused by name: 'pp' and 'B-' are character strings, and a struct here holds numbers. The fields tell the forms apart
fn2fm(f, "BB")Refused: the Bernstein-Bézier form's struct would carry the same fields as a B-form's, so the two would be indistinguishable once built. "B-" is the same spline
fnplt(f)Refused: it draws. plot(xx, fnval(f, xx)) is the same picture
ppmak(breaks, coefs) with a matrixRefused: MATLAB reads the rows as a vector-valued spline, which has no type here. ppmak(breaks, coefs, 1) reads the same matrix as pieces-by-order
csapi({x1, x2}, y), spap2({…}, …)Refused: gridded, tensor-product splines need a cell array, which this console has no type for
spaps(x, y, 0, w, 3)Refused: the quintic variational interpolant is the one call MATLAB answers through spapi and fncmb. Every other spaps call answers

How good is the fit?#

[f, gof] = fit(x, y, "poly2")

gof carries MATLAB's five fields, in MATLAB's order: sse, rsquare, dfe, adjrsquare, rmse. Read one with gof.rsquare() — every member on this console is a call.

⚠ gof.rmse is sqrt(sse/dfe), not sqrt(sse/n). Base MATLAB's own rmse(y, yfit) divides by n, so the two give different numbers for the same fit and neither is wrong — one is the residual standard error and the other is the root mean square. If you are comparing against a number from elsewhere, check which one it is.

⚠ An interpolant's gof is sse 0, rsquare 1, dfe 0, and NaN for the last two — it passes through every point, so there is no error to report and nothing to divide by. That is an answer, not a gap.

What a fit will tell you about itself#

>>> type(f)
'poly1'  # String

>>> category(f)
'library'  # String

>>> class(f)
'cfit'  # String

>>> class(fittype("poly1"))
'fittype'  # String

type answers the model's own name, category says which of the four kinds of model it is (library, custom, interpolant or spline), and coeffvalues, formula, numcoeffs, indepnames, dependnames and argnames answer as MATLAB's do. ⚠ Four of them answer a single string here where MATLAB answers a cell array — coeffnames, indepnames, dependnames and argnames — with the names separated by single spaces, because this console has one string type and no cell type. Split on spaces where MATLAB code indexes a cell.

Drawing a fit#

plot means something different when its first argument is a fit. It is the same overload MATLAB has — one verb, told apart by the kind of what you hand it — so nothing about plot(x, y) changes:

plot(f)
plot(f, "r--")
plot(f, x, y [, outliers] [, type | {type, ...}] [, level] )

plot(f) on its own draws the fitted curve at 1001 points over the range of the data the fit was made from. The fit remembers that range: it is the data that survived "Exclude", in the coordinates you gave, and "Normalize" does not move it. Pass x and y as well and the data is drawn as markers with the curve over it, and the drawing range becomes the range of the x you passed — so a subset of the data draws over the subset.

TypeWhat it draws
"fit" (the default)the data, then the curve
"residuals"y - f(x) at each data point, and a zero line
"predobs"the data, the curve, and predint's observation band
"predfunc"the same with its functional band — the narrower pair
"deriv1", "deriv2"the first or second derivative alone: no data markers, even when you passed data
"integral"the integral from the range's own start, alone

A braced list draws one axes per type — plot(f, x, y, {"fit", "residuals"}) puts the fit in the chart and the residuals in a subchart under it.

⚠ That braced form is the one shape of this verb that will not cross to MATLAB, in either direction: MATLAB's several-types argument is a cell array and this console has no cell type, so toMatlab and fromMatlab both refuse it by name. Everything else here crosses argument for argument. Write it as two calls with hold on between them if the script has to survive the trip.

Every line carries MATLAB's own legend name — Data, Excluded data, Fitted curve, Zero line, Prediction bounds - 95%, First derivative, Second derivative, Integral from 0 to x — and c.path(i).info reads them back:

>>> x = [1; 2; 3; 4; 5]; y = [2.1; 3.9; 6.2; 7.8; 10.1]; f = fit(x, y, "poly1"); c = plot(f, x, y, "predobs")
plot(poly1): 4 line(s) — Data, Fitted curve, Prediction bounds - 95%

>>> p = c.path(1); p.info
p: name='Fitted curve', points=1001, bounds=[x: 1..5, y: 2.04..10]

>>> p = c.path(2); p.info
p: name='Prediction bounds - 95%', points=1001, bounds=[x: 1..5, y: 1.27976..9.23976]

⚠ Four lines, three names. The upper prediction bound is deliberately unnamed, on both sides — MATLAB's legend carries one entry for the pair, not two, and this console draws it the same way.

⚠ The confidence level goes after the type, and a line spec goes before it. plot(f, x, y, "predobs", 0.9) is the way to widen or narrow the band; plot(f, x, y, "r--", "predobs") is the way to style it. The other order — plot(f, x, y, "predobs", "r--") — is refused, because the slot after a type is the level and nothing else. That reads backwards, and it is MATLAB's rule rather than a restriction added here.

⚠ A line spec styles the DATA, not the curve. With x and y in the call, plot(f, x, y, "r--") makes the data red and dashed and leaves the fitted curve alone. With no data there is no data line, so plot(f, "r--") styles the curve. Measured on both sides.

⚠ "stem" is not a plot type, whatever older documentation says. R2026a answers Invalid color, marker, or line style for it — it falls through to the line-spec reader, exactly as a misspelling does — and so does this console.

The fourth positional argument, before any type, is the outliers vector that excludedata answers (or a list of 1-based indices). Those points leave the Data line and get an Excluded data line of their own:

>>> o = excludedata(x, y, "indices", 2); c = plot(f, x, y, o); p = c.path(1); p.info
p: name='Excluded data', points=1, bounds=[x: 2..2, y: 3.9..3.9]

⚠ predobs and predfunc refuse an interpolant and a spline, as MATLAB does — there are no coefficients to put a band on — but deriv1, deriv2 and integral work for them, because a piecewise polynomial has derivatives.

Surfaces#

A fit can have two independent variables. Pass x and y as the two columns of one matrix and ask for one of MATLAB's surface polynomials:

>>> x = [0; 1; 2; 3; 0; 1; 2; 3; 0; 1; 2; 3]; y = [0; 0; 0; 0; 1; 1; 1; 1; 2; 2; 2; 2]; z = 1 + 2*x + 3*y + 0.5*x.*y + 0.1*x.^2; sf = fit([x y], z, "poly21")
sf = 
     Linear model Poly21:
     sf(x,y) = p00 + p10*x + p01*y + p20*x^2 + p11*x*y
     Coefficients (with 95% confidence bounds):
       p00 =           1  (1, 1)
       p10 =           2  (2, 2)
       p01 =           3  (3, 3)
       p20 =         0.1  (0.1, 0.1)
       p11 =         0.5  (0.5, 0.5)  # Curve Fit

poly00 through poly55: the first digit is the degree in x and the second the degree in y.

The transcripts further down this section run on a thirteen-point scattered fixture rather than on this grid, because several of the settings they show answer the same number on a regular grid:

>>> sx = [0.82; 0.98; 0.73; 0.34; 0.58; 0.11; 0.91; 0.88; 0.82; 0.26; 0.59; 0.023; 0.43]; sy = [0.31; 0.16; 0.18; 0.42; 0.094; 0.6; 0.47; 0.7; 0.7; 0.64; 0.034; 0.069; 0.32]; sz = [0.6; 0.2; 0.48; 0.6; 0.39; 0.4; 0.32; 0.098; 0.12; 0.32; 0.36; 0.83; 0.61];

⚠ The terms are not the whole grid. polyIJ carries x^i*y^j when i <= I and j <= J and i + j <= max(I, J), so poly12 has no p20 and poly23 has neither p30 nor p22. The coefficients come out by total degree, and by descending power of x within each degree — p00, p10, p01, p20, p11, p02, … Both rules are MATLAB's, and coeffnames(fittype("poly23")) will tell you the answer for any of them.

Evaluate it any of the three ways MATLAB does — a scalar broadcasts against a vector:

>>> sf(2, 1)
9.4  # Double

>>> sf([2, 1])
9.4  # Double

>>> sf([1; 2], [1; 1])
[[6.6], [9.4]]  # Matrix of Double

[sf, gof] = fit(...), confint, predint and differentiate all read as they do for a curve, because a surface polynomial is a linear model and reaches them through the same solve.

⚠ differentiate means something else here, and it is MATLAB's naming: on a curve [d1, d2] are the first and second derivative, on a surface they are the two partial first derivatives, df/dx and df/dy. With one output you get df/dx on both.

⚠ predint takes only the matrix form — predint(sf, [x y]). Its third argument is the confidence level, so there is no room for a second coordinate vector; MATLAB answers XY must have two columns. and so does this.

⚠ integrate does not exist for a surface on either side.

"Normalize" works, and it centres and scales each variable by its own mean and deviation — which is why the display writes two lines for it:

>>> sf = fit([sx sy], sz, "lowess", "Normalize", "on")
sf = 
     Locally weighted smoothing linear regression:
       sf(x,y) = lowess (linear) smoothing regression computed from p
       where x is normalized by mean 0.5748 and std 0.3175
       and where y is normalized by mean 0.3613 and std 0.2451
     Coefficients:
       p = coefficient structure  # Curve Fit

The local regressions#

"lowess" fits a local plane and "loess" a local quadratic, weighted by distance with the tricube:

>>> sf = fit([sx sy], sz, "lowess")
sf = 
     Locally weighted smoothing linear regression:
       sf(x,y) = lowess (linear) smoothing regression computed from p
     Coefficients:
       p = coefficient structure  # Curve Fit

>>> sf = fit([sx sy], sz, "loess"); sf([0.5, 0.5; 0.2, 0.3])
[[0.555346], [0.750278]]  # Matrix of Double

>>> sf = fit([sx sy], sz, "lowess", "Span", 0.5, "Robust", "Bisquare"); sf([0.5, 0.5; 0.2, 0.3])
[[0.416714], [0.555369]]  # Matrix of Double

"Span" is the proportion of the points each local regression sees, and it has a floor: the neighbour count is max(ceil(span*n), t + 2) with t the number of terms in the local polynomial, so a lowess never runs on fewer than 5 points and a loess never on fewer than 8. On thirteen points "Span", 0 and the default 0.25 are therefore the same fit.

⚠ A lowess goodness record has a fractional dfe, because a local regression has no integer parameter count. It is n - 2*(1 + n/k) with k the neighbour count, and rmse and adjrsquare divide by that:

>>> [sf, g] = fit([sx sy], sz, "lowess"); [g.sse, g.dfe, g.rmse]
[[0.0152969, 5.8, 0.0513556]]  # Matrix of Double

The two surface interpolants#

"thinplateinterp" and "biharmonicinterp" each pass through every point. Both are one dense linear solve over the raw scattered points — no triangulation, which is why these two are here and the other three are not:

>>> sf = fit([sx sy], sz, "thinplateinterp")
sf = 
     Thin-plate spline interpolant:
       sf(x,y) = thin-plate spline computed from p
       with thin-plate extrapolation
     Coefficients:
       p = coefficient structure  # Curve Fit

>>> sf = fit([sx sy], sz, "biharmonicinterp"); sf(0.5, 0.5)
0.494691  # Double

Neither has coefficients to read or to bound, so coeffvalues, confint and predint are refused for them and for the local regressions:

>>> sf = fit([sx sy], sz, "lowess"); coeffvalues(sf)
error: coeffvalues(f): a lowess fit has no coefficients -- MATLAB answers an OBJECT (curvefit.LowessFit, a SurfaceInterpolant) here and this console has no kind for one (evaluate it with f(x) instead)

⚠ All four are surface-only in MATLAB too. fit(x, y, "lowess") is an error there, not a curve fit — getfittypes.m allows these four two independent variables and not one — so this console answers MATLAB's own sentence rather than inventing a curve:

>>> fit(sx, sz, "lowess")
error: The library function 'Locally weighted smoothing linear regression' is for fitting surfaces and requires two independent variables, that is, X must have two columns.

The curve half of the same local regression is smooth(y, "lowess") — see The model library above — and it answers numbers rather than a fit.

A model of your own, in two variables#

Name both independents after "independent". MATLAB spells the pair as the cell {'x', 'y'}; this console has no cell type, so the two names are written out and the list ends at the next option word:

>>> ft = fittype("a*exp(b*x) + c*y", "independent", "x", "y", "dependent", "z"); cf = fit([sx sy], sz, ft, "StartPoint", [1, 1, 1])
cf = 
     General model:
     cf(x,y) = a*exp(b*x) + c*y
     Coefficients (with 95% confidence bounds):
       a =      0.8094  (0.5578, 1.061)
       b =     -0.6724  (-1.201, -0.1433)
       c =     -0.4205  (-0.8002, -0.04092)  # Curve Fit

The coefficient rule is the same one a curve follows, with two names removed instead of one: every free name that is not an independent variable, in alphabetical order. fittype("terms", "x", "y", "1", "independent", "x", "y") is the linear form.

⚠ fittype("a*x + b*y") with no "independent" is an error on both sides, and MATLAB's sentence says why: y is the default dependent name, so the expression's y would have to be a coefficient as well, and it cannot be both.

What a surface will not do yet#

MATLABHere
fit([x y], z, "linearinterp" | "cubicinterp" | "nearestinterp")Refused by name: scattered-data interpolation needs a Delaunay triangulation this console has nothing to hold
plot(sf)Refused by name: it draws a surface, and this console's chart draws in two dimensions — the same reason surf, mesh and plot3 are refused. Evaluate a slice and plot that: plot(xq, sf(xq, y0))

What is not here yet#

MATLABHere
[f, gof, output] = fit(…) — the third and fourth outputsRefused by name: they are the solver record and its convergence MESSAGE, and a message is a character field this console's record cannot hold
coeffvalues of an interpolantRefused by name: MATLAB answers a pp struct there — an object for the surface interpolants and the local regressions — and this console has no type for either. Evaluate it with f(xq)
cftool, sftoolNever — they are click-driven apps. fit is what they drive
fittype({'x', '1'}), 'coefficients', {'a','b'} and 'independent', {'x','y'}Spelled fittype("terms", "x", "1"), fittype(…, "coefficients", "a", "b") and fittype(…, "independent", "x", "y") — this console has no cell type

Every transcript on this page is real output, captured with ICoreBlocks --console "<line>" on 2026-09-05 from a build of commit 3c8ea988 — the local-regression, surface-interpolant and two-variable fittype transcripts from the build that landed those the same day — and the interpolant, spline and goodness transcripts from the build that landed those the same night. The spline-form transcripts were captured on 2026-09-05 from the build that landed csapi, csape, spap2, spaps and their readers, and every number in them was diffed against R2026a with the Curve Fitting Toolbox installed before it was written down — the worst disagreement over the whole section was 5.3e-15. The drawing transcripts were captured the same day from the build that landed plot(f, ...), and every claim on this page about what MATLAB draws was read off MATLAB's own handles on R2026a before it was written down.