Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@

## Unreleased

### New features

- **Responses:** `ModelSpec(terms, response=...)` and
`spec.build_response(X)`, the response built from the same data as the
design. A response is a column, a list of columns, or
- `Surv(time, status)` or `Surv(start, stop, status)`, as R's: a
`pd.DataFrame` of these columns, as glmstar's `CoxFamily` or lifelines
take it;
- `Binomial('y')` for a binary column (0/1, boolean or two levels; the
second level, or `level=`, is a success), or `Binomial(successes,
trials=...)` for the successes and failures (`output='counts'`, R's
`cbind`) or the proportion and number of trials (`output='proportion'`);
- `Multinomial('class')` for the class of each row, as a categorical or
one-hot (`output='onehot'`), or `Multinomial([...])` for counts.

Responses learn their levels at fit; a level unseen at fit raises.
The response's columns cannot be in a term.
- **`wildcard`**, R's `.`: every column of `X` but the response, so
`ModelSpec([wildcard], response='y')` is `y ~ .`. `wildcard(pow=2)` is
`.^2`, `wildcard(exclude=['x'])` is `. - x`, `(wildcard, 'g')` is `.:g`,
and `pca(wildcard, n_components=3)` a feature of every column. It is
expanded at fit into `expanded_terms_`; columns that are terms already
are not repeated, as in R's `y ~ x + .`.
- `pca()`'s `name` is optional.
- `fit(X, y)` checks that the indices of `X` and `y` agree when both are
pandas objects.

### Documentation

- A page on responses and the wildcard.

## 0.1.0 (2026-10-26)

`ModelSpec`, from [ISLP](https://github.com/intro-stat-learning/ISLP) 0.6.0,
Expand Down
1 change: 1 addition & 0 deletions docs/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ For the guide and examples, see the `modelmatrix documentation <../>`_.

ModelSpec
terms
responses
transforms
11 changes: 11 additions & 0 deletions docs/api/responses.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Responses
=========

.. automodule:: modelmatrix.response
:no-members:

.. autoclass:: modelmatrix.Surv
.. autoclass:: modelmatrix.Binomial
.. autoclass:: modelmatrix.Multinomial
.. autoclass:: modelmatrix.Response
:members: fit, transform
7 changes: 7 additions & 0 deletions docs/api/terms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ Helpers that make terms
.. autofunction:: modelmatrix.derived_feature
.. autofunction:: modelmatrix.build_columns

The wildcard
------------

.. autodata:: modelmatrix.wildcard
:annotation: = Wildcard()
.. autoclass:: modelmatrix.Wildcard

Terms and columns
-----------------

Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pip install modelmatrix

- [Building design matrices with `ModelSpec`](spec.md)
- Features: [polynomials](poly.md), [splines](splines.md) and [PCA](PCA.md)
- [Responses and the wildcard](responses.md): `y ~ .`, `Surv`, `Binomial` and `Multinomial`
- [ANOVA](anova.md) with `build_sequence`
- [`ModelSpec` compared](comparison.md) with `ColumnTransformer`, patsy and formulaic
- The [API reference](https://intro-stat-learning.github.io/modelmatrix/api/)
Expand Down
1 change: 1 addition & 0 deletions docs/myst.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ project:
- file: poly.md
- file: splines.md
- file: PCA.md
- file: responses.md
- file: anova.md
- file: comparison.md
- title: API Reference
Expand Down
161 changes: 161 additions & 0 deletions docs/responses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
jupytext:
formats: ipynb,md:myst
text_representation:
extension: .md
format_name: myst
format_version: 0.13
kernelspec:
display_name: python3
language: python
name: python3
---

# Responses and the wildcard

A `ModelSpec` can also describe the response of a model,
`ModelSpec(terms, response=...)`, and `build_response` then builds it
from the same data as the design. With a response,
`wildcard` stands for every other column, as `.` in R's
formulae: `ModelSpec([wildcard], response='Sales')` is
`Sales ~ .`.

```{code-cell} ipython3
import numpy as np, pandas as pd
import statsmodels.api as sm
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import make_pipeline

from ISLP import load_data
from modelmatrix import (ModelSpec as MS,
wildcard,
pca,
poly,
Surv,
Binomial,
Multinomial)

Carseats = load_data('Carseats')
```

## `y ~ .`

```{code-cell} ipython3
spec = MS([wildcard], response='Sales', intercept=True).fit(Carseats)
X, y = spec.transform(Carseats), spec.build_response(Carseats)
sm.OLS(y, X).fit().params
```

The wildcard is expanded when the `ModelSpec` is fit: `terms` is
unchanged, and `expanded_terms_` holds what it stood for.

```{code-cell} ipython3
spec.terms, spec.expanded_terms_
```

Columns that are terms themselves are not repeated, as in R's
`Sales ~ Price + .`, and `wildcard(exclude=...)` is R's `. - x`:

```{code-cell} ipython3
MS([poly('Price', 2), 'Price', wildcard(exclude=['Population', 'Education'])],
response='Sales').fit(Carseats).expanded_terms_
```

## `y ~ .^2` and `.:x`

`wildcard(pow=2)` adds every interaction of two columns, as R's `.^2`,
and the wildcard can be a part of an interaction, as R's `.:US`:

```{code-cell} ipython3
small = Carseats[['Sales', 'Price', 'Income', 'US']]
MS([wildcard(pow=2)], response='Sales').fit(small).expanded_terms_
```

```{code-cell} ipython3
MS(['US', (wildcard, 'US')], response='Sales').fit(small).expanded_terms_
```

## Features of the wildcard

A wildcard can be a variable of a feature: here, the first principal
components of the numeric columns, but the response.

```{code-cell} ipython3
pc = pca(wildcard(exclude=['ShelveLoc', 'Urban', 'US']), n_components=3, scale=True)
spec = MS([pc, 'ShelveLoc'], response='Sales').fit(Carseats)
spec.transform(Carseats).iloc[:5]
```

Its parameters are tuned as those of any term:

```{code-cell} ipython3
y = spec.build_response(Carseats)
pipe = make_pipeline(MS([pc, 'ShelveLoc'], response='Sales'), LinearRegression())
grid = GridSearchCV(pipe, {'modelspec__terms__0__n_components': [1, 3, 5, 7]}, cv=5)
grid.fit(Carseats, y)
grid.best_params_
```

The response is passed to `fit` as `y` (a `Pipeline` passes the design's
data only to the `ModelSpec`). When both are pandas objects, `ModelSpec.fit`
checks that their indices agree.

## Survival responses: `Surv`

`Surv(time, status)` is R's survival response, and `Surv(start, stop,
status)` that of (start, stop] intervals. The response is a
`pd.DataFrame` with these columns, named as in the data, as glmstar's
`CoxFamily(event_id=..., status_id=..., start_id=...)` or lifelines take it.

```{code-cell} ipython3
BrainCancer = load_data('BrainCancer').dropna()
spec = MS([wildcard], response=Surv('time', 'status')).fit(BrainCancer)
X, y = spec.transform(BrainCancer), spec.build_response(BrainCancer)
y.iloc[:5]
```

```{code-cell} ipython3
sm.PHReg(y['time'], X, status=y['status']).fit().params
```

## Binomial responses

`Binomial('US')` codes a binary column as 0 and 1, the second level, as in
R, being a success (or the one given by `level=`):

```{code-cell} ipython3
spec = MS(['Price', 'Income'], response=Binomial('US'), intercept=True).fit(Carseats)
y = spec.build_response(Carseats)
pd.crosstab(y, Carseats['US'])
```

With the number of trials, `Binomial(successes, trials=...)` gives the
successes and the failures, as R's `cbind(successes, failures)` (which
statsmodels' `GLM` takes too), or with `output='proportion'` the proportion
and the number of trials, as a response and weights:

```{code-cell} ipython3
rng = np.random.default_rng(0)
D = pd.DataFrame({'x': rng.standard_normal(50), 'size': rng.integers(5, 20, 50)})
D['cases'] = rng.binomial(D['size'], 1 / (1 + np.exp(-D['x'])))

counts = MS(['x'], response=Binomial('cases', trials='size')).fit(D).build_response(D)
prop = MS(['x'], response=Binomial('cases', trials='size', output='proportion')).fit(D).build_response(D)
X = MS(['x'], intercept=True).fit_transform(D)
family = sm.families.Binomial()
(sm.GLM(counts, X, family=family).fit().params,
sm.GLM(prop['cases/size'], X, family=family, var_weights=prop['size']).fit().params)
```

## Multinomial responses

`Multinomial('ShelveLoc')` gives the class of each row as a categorical
whose categories are the levels seen at fit, and with `output='onehot'`
an indicator column for each level; `Multinomial([...])` takes a column
of counts for each class.

```{code-cell} ipython3
spec = MS(['Price', 'Sales'], response=Multinomial('ShelveLoc', output='onehot')).fit(Carseats)
spec.build_response(Carseats).iloc[:5]
```
8 changes: 6 additions & 2 deletions modelmatrix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
pca,
contrast,
ordinal,
build_columns)
build_columns,
wildcard,
Wildcard)
from .columns import Column
from .response import Response, Surv, Binomial, Multinomial

try:
from ._version import version as __version__
Expand All @@ -23,4 +26,5 @@

__all__ = ['ModelSpec', 'Feature', 'Column', 'Contrast', 'Ordinal',
'poly', 'ns', 'bs', 'derived_feature', 'pca', 'contrast',
'ordinal', 'build_columns']
'ordinal', 'build_columns', 'wildcard', 'Wildcard',
'Response', 'Surv', 'Binomial', 'Multinomial']
Loading
Loading