# Add a Tensor Product for Two Predictors

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.


``` python
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.


``` python
# 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


``` python
wk.partial_effects(model)
```


<style>
  #altair-viz-84935f5532ec4b99a9bdde349af626b3.vega-embed {
    width: 100%;
    display: flex;
  }

  #altair-viz-84935f5532ec4b99a9bdde349af626b3.vega-embed details,
  #altair-viz-84935f5532ec4b99a9bdde349af626b3.vega-embed details summary {
    position: relative;
  }
</style>


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


``` python
wk.check(model)
```


<style>
  #altair-viz-ab077d701e4243ac9457280d163a02ec.vega-embed {
    width: 100%;
    display: flex;
  }

  #altair-viz-ab077d701e4243ac9457280d163a02ec.vega-embed details,
  #altair-viz-ab077d701e4243ac9457280d163a02ec.vega-embed details summary {
    position: relative;
  }
</style>


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.
