Add a Tensor Product for Two Predictors

Use te() to model the joint non-linear effect of two continuous predictors including their interaction.

When two continuous predictors interact (meaning the effect of one depends on the level of the other) a tensor product smooth te(x1, x2) captures that joint surface without assuming the predictors operate on the same scale. Unlike s(x1, x2), a tensor product is invariant to the units of each predictor, making it the right choice when x1 and x2 are measured in different units or have very different ranges.

Load data

Load the wages dataset, which contains age, experience, and hourly wage for survey respondents.

import whittaker as wk

data = wk.load_dataset("wages", as_frame=True)
data.head()
age experience wage
0 62.323637 9.216817 80.231223
1 42.032395 2.487841 40.024189
2 63.883454 7.251912 60.292852
3 21.799293 3.799293 20.232940
4 46.545724 1.738119 62.383214

The columns are age (years), experience (years in the workforce), and wage (hourly rate in dollars). Age and experience are correlated but not identical as some workers have more experience relative to their age, others have little.

Fit

Fit a Gamma GAM with a tensor product of age and experience. The Gamma family is appropriate for wages because the response is strictly positive and often right-skewed.

# Fit tensor product interaction surface
model = wk.GAM(
    "wage ~ te(age, experience)",
    family=wk.Gamma(),
).fit(data)

model.summary()
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/scipy/optimize/_optimize.py:2358: RuntimeWarning: invalid value encountered in scalar subtract
  p = (xf - fulc) * q - (xf - nfc) * r
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/scipy/optimize/_optimize.py:2359: RuntimeWarning: invalid value encountered in scalar subtract
  q = 2.0 * (q - r)
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/scipy/optimize/_optimize.py:2357: RuntimeWarning: invalid value encountered in scalar multiply
  q = (xf - fulc) * (fx - fnfc)
GAM fit summary
============================================================
Formula:    wage ~ te(age, experience)
Family:     Gamma(link='log')
Inference:  GCV
Observations: 800
Coefficients: 100

Parametric coefficients:
  Term                       Estimate    Std.Err    t value    p-value
  ------------------------ ---------- ---------- ---------- ----------
  (Intercept)              6878617665819.6689     0.0000 7504768711412710412182880256.000    < 1e-16

Approximate significance of smooth terms:
  Term                        EDF Ref.df     Chi.sq    p-value
  ------------------------ ------ ------ ---------- ----------
  te(age, experience)       22.93     23 135575690797148592.000    < 1e-16

Total EDF:  23.93
Scale est:  320845457739210.875000
Deviance:   248999919535812480.0000
Null dev:   205.9584
Dev. expl:  -120898139560873168.0%
GCV score:  329915952163046.062500
AIC:        60159.48
BIC:        60271.57

The summary reports a single EDF for the tensor product term. A high EDF indicates that the interaction surface has substantial curvature: the wage effect of additional experience differs across age groups.

Partial effects

wk.partial_effects(model)

The partial effects plot renders the tensor product as a two-dimensional surface or contour. Look for regions where the wage surface curves differently along age versus experience: for example, high experience may carry a larger wage premium for mid-career workers than for very young or very old ones.

Diagnostics

wk.check(model)

For a Gamma model, pay attention to the Q-Q plot. Heavy tails or a systematic arc indicate the distributional assumption may need revisiting. Clean residuals here confirm the tensor product captured the main interaction structure.