Repository: matthewwardrop/formulaic

Issue:
How do I set the reference level for a categorical term?
I'd like to define the level that should be used as the reference level in my formula string. Is it possible to do so with the current implementation?

Discussion:
codecov[bot]: ## [Codecov](https://codecov.io/gh/matthewwardrop/formulaic/pull/133?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Matthew+Wardrop) Report
Patch coverage: **`100.00`**% and no project coverage change.
> Comparison is base [(`4d05e67`)](https://codecov.io/gh/matthewwardrop/formulaic/commit/4d05e672c4975bd207a01e265a8255c8b71f9dfd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Matthew+Wardrop) 100.00% compared to head [(`4c0021a`)](https://codecov.io/gh/matthewwardrop/formulaic/pull/133?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Matthew+Wardrop) 100.00%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main      #133   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           46        46           
  Lines         2455      2469   +14     
=========================================
+ Hits          2455      2469   +14     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `100.00% <100.00%> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Matthew+Wardrop#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/matthewwardrop/formulaic/pull/133?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Matthew+Wardrop) | Coverage Δ | |
|---|---|---|
| [formulaic/transforms/contrasts.py](https://codecov.io/gh/matthewwardrop/formulaic/pull/133?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Matthew+Wardrop#diff-Zm9ybXVsYWljL3RyYW5zZm9ybXMvY29udHJhc3RzLnB5) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Matthew+Wardrop). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Matthew+Wardrop)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/matthewwardrop/formulaic/pull/133?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Matthew+Wardrop).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Matthew+Wardrop).


Patch:
diff --git a/docsite/docs/guides/contrasts.ipynb b/docsite/docs/guides/contrasts.ipynb
new file mode 100644
index 0000000..1970205
--- /dev/null
+++ b/docsite/docs/guides/contrasts.ipynb
@@ -0,0 +1,1404 @@
+{
+ "cells": [
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Categorical data (also known as \"factors\") is encoded in model matrices using\n",
+    "\"contrast codings\" that transform categorical vectors into a collection of\n",
+    "numerical vectors suitable for use in regression models. In this guide we begin\n",
+    "with some basic examples, before introducing the concepts behind contrast\n",
+    "codings, how to select and/or design your own coding, and (for more advanced\n",
+    "readers) describe how we guarantee structural full-rankness of formulae with\n",
+    "complex interactions between categorical and other features."
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Basic usage\n",
+    "\n",
+    "Formulaic follows in the stead of R and Patsy by automatically inferring from \n",
+    "the data whether a feature needs to categorically encoded. For example:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Intercept</th>\n",
+       "      <th>letters[T.b]</th>\n",
+       "      <th>letters[T.c]</th>\n",
+       "      <th>numbers[T.2]</th>\n",
+       "      <th>numbers[T.3]</th>\n",
+       "      <th>values</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>20</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>200</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>30</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Intercept  letters[T.b]  letters[T.c]  numbers[T.2]  numbers[T.3]  values\n",
+       "0        1.0             0             0             0             0      20\n",
+       "1        1.0             1             0             1             0     200\n",
+       "2        1.0             0             1             0             1      30"
+      ]
+     },
+     "execution_count": 1,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from formulaic import model_matrix\n",
+    "from pandas import DataFrame, Categorical\n",
+    "\n",
+    "df = DataFrame({\n",
+    "    \"letters\": [\"a\", \"b\", \"c\"],\n",
+    "    \"numbers\": Categorical([1,2,3]),\n",
+    "    \"values\": [20, 200, 30],\n",
+    "})\n",
+    "\n",
+    "model_matrix(\"letters + numbers + values\", df)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Here `letters` was identified as a categorical variable because of it consisted\n",
+    "of strings, `numbers` was identified as categorical because of its data type,\n",
+    "and `values` was treated as a vector of numerical values. The categorical data\n",
+    "was encoded using the default encoding of \"Treatment\" (aka. \"Dummy\", see below\n",
+    "for more details).\n",
+    "\n",
+    "If we wanted to force formulaic to treat a column as categorical, we can use the\n",
+    "`C()` transform (just as in patsy and R). For example:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Intercept</th>\n",
+       "      <th>C(values)[T.30]</th>\n",
+       "      <th>C(values)[T.200]</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Intercept  C(values)[T.30]  C(values)[T.200]\n",
+       "0        1.0                0                 0\n",
+       "1        1.0                0                 1\n",
+       "2        1.0                1                 0"
+      ]
+     },
+     "execution_count": 2,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model_matrix(\"C(values)\", df)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The `C()` transform tells Formulaic that the column should be encoded as\n",
+    "categorical data, and allows you to customise how the encoding is performed. For\n",
+    "example, we could use polynomial coding (detailed below) and explicitly specify\n",
+    "the categorical levels and their order using:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "/home/matthew/Repositories/github/formulaic/formulaic/transforms/contrasts.py:124: DataMismatchWarning: Data has categories outside of the nominated levels (or that were not seen in original dataset): {200}. They are being  cast to nan, which will likely skew the results of your analyses.\n",
+      "  warnings.warn(\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Intercept</th>\n",
+       "      <th>C(values, contr.poly, levels=[10, 20, 30]).L</th>\n",
+       "      <th>C(values, contr.poly, levels=[10, 20, 30]).Q</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.000000</td>\n",
+       "      <td>-0.816497</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.000000</td>\n",
+       "      <td>0.000000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.707107</td>\n",
+       "      <td>0.408248</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Intercept  C(values, contr.poly, levels=[10, 20, 30]).L  \\\n",
+       "0        1.0                                      0.000000   \n",
+       "1        1.0                                      0.000000   \n",
+       "2        1.0                                      0.707107   \n",
+       "\n",
+       "   C(values, contr.poly, levels=[10, 20, 30]).Q  \n",
+       "0                                     -0.816497  \n",
+       "1                                      0.000000  \n",
+       "2                                      0.408248  "
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model_matrix(\"C(values, contr.poly, levels=[10, 20, 30])\", df)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Where possible, as you can see above, we also provide warnings when a\n",
+    "categorical encoding does not reflect the structure of the data."
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## How does contrast coding work?\n",
+    "\n",
+    "As you have seen, contrast coding transforms categorical vectors into a matrix\n",
+    "of numbers that can be used during modeling. If your data has $K$ mutually\n",
+    "exclusive categories, these matrices typically consist of $K-1$ columns. This\n",
+    "reduction in dimensionality reflects the fact that membership of the $K$th\n",
+    "category could be inferred from the lack of membership in any other category,\n",
+    "and so is redundant in the presence of a global intercept. You can read more\n",
+    "about this in the [full rankness](#guaranteeing-structural-full-rankness)\n",
+    "discussion below.\n",
+    "\n",
+    "The first step toward generating numerical vectors from categorical data is to\n",
+    "dummy encode it. This transforms the single vector of $K$ categories into $K$\n",
+    "boolean vectors, each having a $1$ only in rows that are members of the\n",
+    "corresponding category. If you do not have a global intercept, you can directly\n",
+    "use this dummy encoding with the full $K$ columns and contrasts are unnecessary.\n",
+    "This is not always the case, which requires you to reduce the rank of your\n",
+    "coding by thinking about contrasts (or differences) between the levels.\n",
+    "\n",
+    "In practice, this dimension reduction using \"contrasts\" looks like constructing\n",
+    "a $K \\times (K-1)$ \"coding matrix\" that describes the contrasts of interest. You\n",
+    "can then post-multiply your dummy-encoded columns by it. That is: $$ E = DC $$\n",
+    "where $E \\in \\mathbb{R}^{N \\times (K-1)}$ is the contrast coded categorical\n",
+    "data, $D \\in \\{0, 1\\}^{N \\times K}$ is the dummy encoded data, and $C \\in\n",
+    "\\mathbb{R}^{K \\times (K-1)}$ is the coding matrix.\n",
+    "\n",
+    "The easiest way to construct a coding matrix is to start with a \"coefficient\n",
+    "matrix\" $Z \\in \\mathbb{R}^{K \\times K}$ which describes the contrasts that you\n",
+    "want the coefficients of a trained linear regression model to represent (with\n",
+    "columns representing the untransformed levels, and rows representing the\n",
+    "transforms). For a consistently chosen set of contrasts, this matrix will be\n",
+    "full-rank, and the inverse of this matrix will have a constant column\n",
+    "representing the global intercept. Removing this column results in the $K \\times\n",
+    "(K-1)$ coding matrix that should be apply to the dummy encoded data in order for\n",
+    "the coefficients to have the desired interpretation.\n",
+    "\n",
+    "For example, if we wanted all of the levels to be compared to the first level,\n",
+    "we would build a matrix $Z$ as:\n",
+    "$$\n",
+    "\\begin{align}\n",
+    "Z =& \\left(\\begin{array}{c c c c}\n",
+    "1 & 0 & 0 & 0 \\\\\n",
+    "-1 & 1 & 0 & 0\\\\\n",
+    "-1 & 0 & 1 & 0\\\\\n",
+    "-1 & 0 & 0 & 1\n",
+    "\\end{array}\\right)\\\\\n",
+    "\\therefore Z^{-1} =& \\left(\\begin{array}{c c c c}\n",
+    "1 & 0 & 0 & 0 \\\\\n",
+    "1 & 1 & 0 & 0\\\\\n",
+    "1 & 0 & 1 & 0\\\\\n",
+    "1 & 0 & 0 & 1\n",
+    "\\end{array}\\right)\\\\\n",
+    "\\implies C =& \\left(\\begin{array}{c c c}\n",
+    "0 & 0 & 0 \\\\\n",
+    "1 & 0 & 0\\\\\n",
+    "0 & 1 & 0\\\\\n",
+    "0 & 0 & 1\n",
+    "\\end{array}\\right)\n",
+    "\\end{align}\n",
+    "$$\n",
+    "This is none other than the default \"treatment\" coding described below, which\n",
+    "applies one-hot coding to the categorical data.\n",
+    "\n",
+    "It is important to note that while your choice of contrast coding will change\n",
+    "the interpretation and values of your coefficients, all contrast encodings\n",
+    "ultimately result in equivalent regressions, and it is possible to\n",
+    "restrospectively infer any other set of interesting contrasts given the\n",
+    "regression covariance matrix. The task is therefore to find the most useful \n",
+    "representation, not the \"correct\" one.\n",
+    "\n",
+    "For those interested in reading more, the [R Documentation on Coding\n",
+    "Matrices](https://cran.r-project.org/web/packages/codingMatrices/vignettes/codingMatrices.pdf)\n",
+    "covers this in more detail."
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Contrast codings\n",
+    "\n",
+    "This section introduces the contrast encodings that are shipped as part of Formulaic. These implementations live in `formulaic.transforms.contrasts`, and are surfaced by default in formulae as an attribute of `contr` (e.g. `contr.treatment`, in order to be consistent with R). You can always [implement your own contrasts](#writing-your-own-encoding) if the need arises.\n",
+    "\n",
+    "If you would like to dig deeper and see the actual contrast/coefficient matrices for various parameters you can directly import these contrast implementations and play with them in a Python shell, but otherwise for brevity we will not exhaustively show these in the following documentation. For example:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>A</th>\n",
+       "      <th>C</th>\n",
+       "      <th>D</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>A</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>B</th>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>C</th>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>D</th>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "     A    C    D\n",
+       "A  1.0  0.0  0.0\n",
+       "B  0.0  0.0  0.0\n",
+       "C  0.0  1.0  0.0\n",
+       "D  0.0  0.0  1.0"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from formulaic.transforms.contrasts import C, TreatmentContrasts\n",
+    "\n",
+    "TreatmentContrasts(base=\"B\").get_coding_matrix([\"A\", \"B\", \"C\", \"D\"])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>A</th>\n",
+       "      <th>B</th>\n",
+       "      <th>C</th>\n",
+       "      <th>D</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>B</th>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>A-B</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>-1.0</td>\n",
+       "      <td>-0.0</td>\n",
+       "      <td>-0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>C-B</th>\n",
+       "      <td>0.0</td>\n",
+       "      <td>-1.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>D-B</th>\n",
+       "      <td>0.0</td>\n",
+       "      <td>-1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "       A    B    C    D\n",
+       "B    0.0  1.0  0.0  0.0\n",
+       "A-B  1.0 -1.0 -0.0 -0.0\n",
+       "C-B  0.0 -1.0  1.0  0.0\n",
+       "D-B  0.0 -1.0  0.0  1.0"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "TreatmentContrasts(base=\"B\").get_coefficient_matrix([\"A\", \"B\", \"C\", \"D\"])"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Treatment (aka. dummy)\n",
+    "\n",
+    "This contrast coding compares each level with some reference level. If not\n",
+    "specified, the reference level is taken to be the first level. The reference\n",
+    "level can be specified as the first argument to the\n",
+    "`TreatmentContrasts`/`contr.treatment` constructor.\n",
+    "\n",
+    "**Example formulae:**\n",
+    "\n",
+    "- `~ X`: Assuming `X` is categorical, the treatment encoding will be used by\n",
+    "  default.\n",
+    "- `~ C(X)`: You can also explicitly flag a feature to be encoded as categorical,\n",
+    "  whereupon the default is treatment encoding.\n",
+    "- `~ C(X, contr.treatment)`: Explicitly indicate that the treatment encoding\n",
+    "  should be used.\n",
+    "- `~ C(X, contr.treatment(\"x\"))`: Indicate that the reference treatment should\n",
+    "  be \"x\" instead of the first index.\n",
+    "- `~ C(X, contr.treatment(base=\"x\"))`: As above."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Intercept</th>\n",
+       "      <th>C(letters, contr.treatment)[T.b]</th>\n",
+       "      <th>C(letters, contr.treatment)[T.c]</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Intercept  C(letters, contr.treatment)[T.b]  \\\n",
+       "0        1.0                                 0   \n",
+       "1        1.0                                 1   \n",
+       "2        1.0                                 0   \n",
+       "\n",
+       "   C(letters, contr.treatment)[T.c]  \n",
+       "0                                 0  \n",
+       "1                                 0  \n",
+       "2                                 1  "
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model_matrix(\"C(letters, contr.treatment)\", df)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### SAS Treatment\n",
+    "\n",
+    "This constrasts generated by this class are the same as\n",
+    "the above, but with the reference level defaulting to the last\n",
+    "level (the default in SAS).\n",
+    "\n",
+    "**Example formulae:**\n",
+    "\n",
+    "- `~ C(X, contr.SAS)`: Basic use-case.\n",
+    "- `~ C(X, contr.SAS(\"x\"))`: Same as treatment encoding case above.\n",
+    "- `~ C(X, contr.SAS(base=\"x\"))`: Same as treatment encoding case above."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Intercept</th>\n",
+       "      <th>C(letters, contr.SAS)[T.a]</th>\n",
+       "      <th>C(letters, contr.SAS)[T.b]</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Intercept  C(letters, contr.SAS)[T.a]  C(letters, contr.SAS)[T.b]\n",
+       "0        1.0                           1                           0\n",
+       "1        1.0                           0                           1\n",
+       "2        1.0                           0                           0"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model_matrix(\"C(letters, contr.SAS)\", df)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Sum (or Deviation)\n",
+    "\n",
+    "These contrasts compare each level (except the last, which is redundant) to the global average of all levels.\n",
+    "\n",
+    "**Example formulae:**\n",
+    "\n",
+    "- `~ C(X, contr.sum)`: Encode categorical data using the sum coding."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Intercept</th>\n",
+       "      <th>C(letters, contr.sum)[S.a]</th>\n",
+       "      <th>C(letters, contr.sum)[S.b]</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>-1.0</td>\n",
+       "      <td>-1.0</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Intercept  C(letters, contr.sum)[S.a]  C(letters, contr.sum)[S.b]\n",
+       "0        1.0                         1.0                         0.0\n",
+       "1        1.0                         0.0                         1.0\n",
+       "2        1.0                        -1.0                        -1.0"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model_matrix(\"C(letters, contr.sum)\", df)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Helmert\n",
+    "\n",
+    "These contrasts compare each successive level to the average all\n",
+    "previous/subsequent levels. It has two configurable parameters: `reverse` which\n",
+    "controls the direction of comparison, and `scale` which controls whether to\n",
+    "scale the encoding to simplify interpretation of coefficients (results in a\n",
+    "floating point model matrix instead of an integer one). When `reverse` is\n",
+    "`True`, the contrasts compare a level to all previous levels; and when `False`,\n",
+    "it compares it to all subsequent levels.\n",
+    "\n",
+    "The default parameter values are chosen to match the R implementation,\n",
+    "which corresponds to a reversed and unscaled Helmert coding.\n",
+    "\n",
+    "**Example formulae:**\n",
+    "\n",
+    "- `~ C(X, contr.helmert)`: Unscaled reverse coding.\n",
+    "- `~ C(X, contr.helmert(reverse=False))`: Unscaled forward coding.\n",
+    "- `~ C(X, contr.helmert(scale=True))`: Scaled reverse coding.\n",
+    "- `~ C(X, contr.helmert(scale=True, reverse=False))`: Scaled forward coding."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Intercept</th>\n",
+       "      <th>C(letters, contr.helmert)[H.b]</th>\n",
+       "      <th>C(letters, contr.helmert)[H.c]</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>-1.0</td>\n",
+       "      <td>-1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>-1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>2.0</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Intercept  C(letters, contr.helmert)[H.b]  C(letters, contr.helmert)[H.c]\n",
+       "0        1.0                            -1.0                            -1.0\n",
+       "1        1.0                             1.0                            -1.0\n",
+       "2        1.0                             0.0                             2.0"
+      ]
+     },
+     "execution_count": 9,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model_matrix(\"C(letters, contr.helmert)\", df)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Diff\n",
+    "\n",
+    "These contrasts take the difference of each level with the previous level. It\n",
+    "has one parameter, `forward`, which indicates that the difference should be\n",
+    "inverted such the difference is taken between the previous level and the current\n",
+    "level. The default attribute values are chosen to match the R implemention, and\n",
+    "correspond to a backward difference coding.\n",
+    "\n",
+    "**Example formulae:**\n",
+    "\n",
+    "- `~ C(X, contr.diff)`: Backward coding.\n",
+    "- `~ C(X, contr.diff(forward=True))`: Forward coding."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Intercept</th>\n",
+       "      <th>C(letters, contr.diff)[D.b]</th>\n",
+       "      <th>C(letters, contr.diff)[D.c]</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>-0.666667</td>\n",
+       "      <td>-0.333333</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.333333</td>\n",
+       "      <td>-0.333333</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.333333</td>\n",
+       "      <td>0.666667</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Intercept  C(letters, contr.diff)[D.b]  C(letters, contr.diff)[D.c]\n",
+       "0        1.0                    -0.666667                    -0.333333\n",
+       "1        1.0                     0.333333                    -0.333333\n",
+       "2        1.0                     0.333333                     0.666667"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model_matrix(\"C(letters, contr.diff)\", df)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Polynomial\n",
+    "\n",
+    "The \"contrasts\" represent a categorical variable that is assumed to equal (or\n",
+    "known) spacing/scores, and allow us to model non-linear polynomial behaviour of\n",
+    "the dependent variable with respect to the ordered levels by projecting the\n",
+    "spacing onto a basis of orthogonal polynomials. It has one parameter, `scores`\n",
+    "which indicates the spacing of the categories. It must have the same length as\n",
+    "the number of levels. If not provided, the categories are assumed equidistant\n",
+    "and spaced by 1.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Intercept</th>\n",
+       "      <th>C(letters, contr.poly).L</th>\n",
+       "      <th>C(letters, contr.poly).Q</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>-0.707107</td>\n",
+       "      <td>0.408248</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.000000</td>\n",
+       "      <td>-0.816497</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.707107</td>\n",
+       "      <td>0.408248</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Intercept  C(letters, contr.poly).L  C(letters, contr.poly).Q\n",
+       "0        1.0                 -0.707107                  0.408248\n",
+       "1        1.0                  0.000000                 -0.816497\n",
+       "2        1.0                  0.707107                  0.408248"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model_matrix(\"C(letters, contr.poly)\", df)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Aliasing categorical features\n",
+    "\n",
+    "The feature names of categorical variables can become quite unwieldy, as you may\n",
+    "have noticed. Fortunately this is easily remedied by aliasing the variable\n",
+    "outside of your formula (and then making it available via formula context). This\n",
+    "is done automatically if you use the `model_matrix` function. For example:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Intercept</th>\n",
+       "      <th>my_letters[T.a]</th>\n",
+       "      <th>my_letters[T.c]</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Intercept  my_letters[T.a]  my_letters[T.c]\n",
+       "0        1.0                1                0\n",
+       "1        1.0                0                0\n",
+       "2        1.0                0                1"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "my_letters = C(df.letters, TreatmentContrasts(base=\"b\"))\n",
+    "model_matrix(\"my_letters\", df)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Writing your own coding\n",
+    "\n",
+    "It may be useful to define your own coding matrices in some contexts. This is\n",
+    "readily achieved using the `CustomContrasts` class directly or via the\n",
+    "`contr.custom` alias. In these cases, you are responsible for providing the\n",
+    "coding matrix ($C$ from above). For example, if you had four levels: A, B, C and\n",
+    "D, and wanted to compute the contrasts: B - A, C - B, and D - A, you could\n",
+    "write:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([[0., 0., 0.],\n",
+       "       [1., 0., 0.],\n",
+       "       [1., 1., 0.],\n",
+       "       [0., 0., 1.]])"
+      ]
+     },
+     "execution_count": 13,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "import numpy\n",
+    "Z = numpy.array([\n",
+    "    [1, 0, 0, 0],  # A\n",
+    "    [-1, 1, 0, 0], # B - A\n",
+    "    [0, -1, 1, 0],  # C - B\n",
+    "    [-1, 0, 0, 1],  # D - A\n",
+    "])\n",
+    "coding = numpy.linalg.inv(Z)[:,1:]\n",
+    "coding"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Intercept</th>\n",
+       "      <th>C(letters, contr.custom(coding))[1]</th>\n",
+       "      <th>C(letters, contr.custom(coding))[2]</th>\n",
+       "      <th>C(letters, contr.custom(coding))[3]</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Intercept  C(letters, contr.custom(coding))[1]  \\\n",
+       "0        1.0                                  0.0   \n",
+       "1        1.0                                  1.0   \n",
+       "2        1.0                                  1.0   \n",
+       "3        1.0                                  0.0   \n",
+       "\n",
+       "   C(letters, contr.custom(coding))[2]  C(letters, contr.custom(coding))[3]  \n",
+       "0                                  0.0                                  0.0  \n",
+       "1                                  0.0                                  0.0  \n",
+       "2                                  1.0                                  0.0  \n",
+       "3                                  0.0                                  1.0  "
+      ]
+     },
+     "execution_count": 14,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model_matrix(\"C(letters, contr.custom(coding))\", DataFrame({\"letters\": [\"A\", \"B\", \"C\", \"D\"]}))"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Guaranteeing Structural Full Rankness"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The model matrices generated from formulae are often consumed directly by linear\n",
+    "regression algorithms. In these cases, if your model matrix is not full rank,\n",
+    "then the features in your model are not linearly independent, and the resulting\n",
+    "coefficients (assuming they can be computed at all) cannot be uniquely\n",
+    "determined. While there are ways to handle this, such as regularization, it is\n",
+    "usually easiest to put in a little more effort during the model matrix creation\n",
+    "process, and make the incoming vectors in your model matrix linearly independent\n",
+    "from the outset. As noted in the text above, categorical coding requires\n",
+    "consideration about the overlap of the coding with the intercept in order to\n",
+    "remain full rank. The good news is that Formulaic will do most of the heavy\n",
+    "lifting for you, and does so by default.\n",
+    "\n",
+    "It is important to note at this point that Formulaic **does not** protect \n",
+    "against all forms of linear dependence, only **structural** linear dependence; \n",
+    "i.e. linear dependence that results from multiple categorical variables \n",
+    "overlapping in vectorspace. If you have two identical numerical vectors called\n",
+    "by two different names in your model matrix, Formulaic will happily build the\n",
+    "model matrix you requested, and you're on your own. This is intentional. While\n",
+    "Formulaic strives to make the model matrix generation process as painless as \n",
+    "possible, it also doesn't want to make more assumptions about the use of the \n",
+    "data than is necessary. Note that you can also disable Formulaic's structural\n",
+    "full-rankness algorithms by passing `ensure_full_rank=False` to `model_matrix()`\n",
+    "or `.get_model_matrix()` methods.\n",
+    "\n",
+    "The algorithm that Formulaic uses was heavily inspired by `patsy`[^1]. The basic\n",
+    "idea is to recognize that all categorical codings span the intercept[^2]; and\n",
+    "then to break that coding up into two pieces: a single column that can be\n",
+    "dropped to avoid spanning the intercept, and the remaining body of the coding\n",
+    "that will always be present. You expand associatively the categorical factors,\n",
+    "and then greedily recombine the components, omitting any that would lead to\n",
+    "structural linear dependence. The result is a set of categorical codings that\n",
+    "only spans the intercept when it is safe to do so, guaranteeing structural full\n",
+    "rankness. The [patsy\n",
+    "documentation](https://patsy.readthedocs.io/en/latest/formulas.html#redundancy-and-categorical-factors)\n",
+    "goes into this in much more detail if this is interesting to you.\n",
+    "\n",
+    "[^1]: https://patsy.readthedocs.io/en/latest/formulas.html#redundancy-and-categorical-factors\n",
+    "[^2]: This assumes that categories are \"complete\", in that each unit has been \n",
+    "    assigned a category. You can \"complete\" categories by treating all those \n",
+    "    unassigned as being a member of an imputed \"null\" category."
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.2"
+  },
+  "orig_nbformat": 4,
+  "vscode": {
+   "interpreter": {
+    "hash": "50719501685d4a11538a064c34e0a9cefdca3be93333d41da3ad32545f9c4436"
+   }
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/docsite/requirements.txt b/docsite/requirements.txt
index 7aabc0e..f14a4d1 100644
--- a/docsite/requirements.txt
+++ b/docsite/requirements.txt
@@ -21,7 +21,10 @@ fastjsonschema==2.16.2
 ghp-import==2.1.0
     # via mkdocs
 importlib-metadata==4.12.0
-    # via mkdocs
+    # via
+    #   jupyter-client
+    #   markdown
+    #   mkdocs
 jinja2==3.1.2
     # via
     #   mkdocs
@@ -48,7 +51,7 @@ markdown==3.4.1
     #   mkdocs
     #   mkdocs-material
     #   pymdown-extensions
-markdown-it-py==2.2.0
+markdown-it-py==2.1.0
     # via
     #   jupytext
     #   mdit-py-plugins
diff --git a/formulaic/transforms/contrasts.py b/formulaic/transforms/contrasts.py
index 88bd6da..4f17178 100644
--- a/formulaic/transforms/contrasts.py
+++ b/formulaic/transforms/contrasts.py
@@ -38,9 +38,9 @@ def C(
     Args:
         data: The data to be marked as categorical.
         contrasts:  The specification of the contrasts that are to be computed.
-            Should be a `Constrasts` instance, a dictionary mapping a key for
+            Should be a `Contrasts` instance, a dictionary mapping a key for
             the contrast with a vector of weights for the categories, or a
-            numpy array with columns representing the constrasts, and rows
+            numpy array with columns representing the contrasts, and rows
             representing the weights over the categories in the data. If not
             specified, a `Treatment` encoding is assumed.
         levels: The categorical levels associated with `data`. If not present,
@@ -88,14 +88,14 @@ def encode_contrasts(
     _spec=None,
 ) -> FactorValues[Union[pandas.DataFrame, spsparse.spmatrix]]:
     """
-    Encode a categorical dataset into one or more "constrasts".
+    Encode a categorical dataset into one or more "contrasts".
 
     Args:
         data: The categorical data array/series to be encoded.
         contrasts: The specification of the contrasts that are to be computed.
-            Should be a `Constrasts` instance, a dictionary mapping a key for
+            Should be a `Contrasts` instance, a dictionary mapping a key for
             the contrast with a vector of weights for the categories, or a
-            numpy array with columns representing the constrasts, and rows
+            numpy array with columns representing the contrasts, and rows
             representing the weights over the categories in the data. If not
             specified, a `Treatment` encoding is assumed.
         levels: The complete set of levels (categories) posited to be present in
@@ -168,7 +168,7 @@ class Contrasts(metaclass=InterfaceMeta):
         output: Optional[str] = None,
     ):
         """
-        Apply the contrasts defined by this `Constrasts` instance to `dummies`
+        Apply the contrasts defined by this `Contrasts` instance to `dummies`
         (the dummy encoding of the values of interest).
 
         Args:
@@ -449,7 +449,7 @@ class SASContrasts(TreatmentContrasts):
     """
     SAS (treatment) contrast coding.
 
-    This constrasts generated by this class are the same as
+    This contrasts generated by this class are the same as
     `TreatmentContrasts`, but with the reference level defaulting to the last
     level (the default in SAS).
     """
@@ -506,14 +506,26 @@ class SumContrasts(Contrasts):
 
 class HelmertContrasts(Contrasts):
     """
-    (Reverse) Helmert coding.
+    Helmert coding.
 
     These contrasts compare the mean of the dependent variable for each
-    successive level to the average all previous levels.
+    successive level to the average all previous levels. The default
+    attribute values are chosen to match the R implementation, which
+    corresponds to a reversed and unscaled Helmert coding.
+
+    Attributes:
+        reverse: Whether to iterate over successive levels in reverse order.
+        scale: Whether to scale the encoding to simplify interpretation of
+            coefficients (results in a floating point model matrix instead of an
+            integer one).
     """
 
     FACTOR_FORMAT = "{name}[H.{field}]"
 
+    def __init__(self, *, reverse: bool = True, scale: bool = False):
+        self.reverse = reverse
+        self.scale = scale
+
     @Contrasts.override
     def _get_coding_matrix(self, levels, reduced_rank=True, sparse=False):
         n = len(levels)
@@ -522,34 +534,58 @@ class HelmertContrasts(Contrasts):
 
         contr = spsparse.lil_matrix((n, n - 1)) if sparse else numpy.zeros((n, n - 1))
         for i in range(len(levels) - 1):
-            contr[i + 1, i] = i + 1
-        contr[numpy.triu_indices(n - 1)] = -1
+            if self.reverse:
+                contr[i + 1, i] = i + 1
+            else:
+                contr[i, i] = n - i - 1
+        contr[
+            numpy.triu_indices(n - 1) if self.reverse else numpy.tril_indices(n, k=-1)
+        ] = -1
+
+        if self.scale:
+            for i in range(n - 1):
+                contr[:, i] /= i + 2 if self.reverse else n - i
 
         return contr
 
     @Contrasts.override
     def get_coding_column_names(self, levels, reduced_rank=True):
         if reduced_rank:
-            return levels[:-1]
+            return levels[1:] if self.reverse else levels[:-1]
         return levels
 
     @Contrasts.override
     def get_coefficient_row_names(self, levels, reduced_rank=True):
         if reduced_rank:
-            return ["avg", *(f"{level} - rolling_avg" for level in levels[1:])]
+            return [
+                "avg",
+                *(
+                    f"{level} - rolling_avg"
+                    for level in (levels[1:] if self.reverse else levels[:-1])
+                ),
+            ]
         return levels
 
 
 class DiffContrasts(Contrasts):
     """
-    (Backward) Difference coding.
+    Difference coding.
 
     These contrasts compare the mean of the dependent variable for each level
-    with that of the previous level.
+    with that of the previous level. The default attribute values are chosen to
+    match the R implemention, and correspond to a reverse (or backward)
+    difference coding.
+
+    Attributes:
+        backward: Whether to reverse the sign of the difference (e.g. Level 2 -
+            Level 1 cf. Level 1 - Level 2).
     """
 
     FACTOR_FORMAT = "{name}[D.{field}]"
 
+    def __init__(self, backward: bool = True):
+        self.backward = backward
+
     @Contrasts.override
     def _get_coding_matrix(self, levels, reduced_rank=True, sparse=False):
         n = len(levels)
@@ -557,6 +593,8 @@ class DiffContrasts(Contrasts):
             return spsparse.eye(n).tocsc() if sparse else numpy.eye(n)
         contr = numpy.repeat([numpy.arange(1, n)], n, axis=0) / n
         contr[numpy.triu_indices(n, m=n - 1)] -= 1
+        if not self.backward:
+            contr *= -1
         if sparse:
             return spsparse.csc_matrix(contr)
         return contr
@@ -564,7 +602,7 @@ class DiffContrasts(Contrasts):
     @Contrasts.override
     def get_coding_column_names(self, levels, reduced_rank=True):
         if reduced_rank:
-            return levels[1:]
+            return levels[1:] if self.backward else levels[:-1]
         return levels
 
     @Contrasts.override
@@ -572,7 +610,14 @@ class DiffContrasts(Contrasts):
         if reduced_rank:
             return [
                 "avg",
-                *(f"{level} - {ref}" for level, ref in zip(levels[1:], levels)),
+                *(
+                    f"{level} - {ref}"
+                    for level, ref in (
+                        zip(levels[1:], levels)
+                        if self.backward
+                        else zip(levels, levels[1:])
+                    )
+                ),
             ]
         return levels
 
@@ -584,6 +629,10 @@ class PolyContrasts(Contrasts):
     These "contrasts" represent a categorical variable that is assumed to have
     equal (or known) spacing/scores, and allow us to model non-linear polynomial
     behaviour of the dependent variable with respect to the ordered levels.
+
+    Attributes:
+        scores: The "scores" of the categorical variable. If provided, it must
+            have the same cardinality as the categories being coded.
     """
 
     FACTOR_FORMAT = "{name}{field}"
@@ -601,6 +650,10 @@ class PolyContrasts(Contrasts):
         n = len(levels)
         if not reduced_rank:
             return spsparse.eye(n).tocsc() if sparse else numpy.eye(n)
+        if self.scores and not len(self.scores) == n:
+            raise ValueError(
+                "`PolyContrasts.scores` must have the same cardinality as the categories."
+            )
         scores = self.scores or numpy.arange(n)
         coding_matrix = poly(scores, degree=n - 1)
         if sparse:


Test patch:
diff --git a/tests/transforms/test_contrasts.py b/tests/transforms/test_contrasts.py
index 7116e62..7a31f0e 100644
--- a/tests/transforms/test_contrasts.py
+++ b/tests/transforms/test_contrasts.py
@@ -1,4 +1,5 @@
-from functools import reduce
+import re
+
 import numpy
 import pandas
 import pytest
@@ -561,8 +562,8 @@ class TestHelmertContrasts:
 
         reference_reduced = pandas.DataFrame(
             {
-                "a": [-1, 1, 0],
-                "b": [-1, -1, 2],
+                "b": [-1, 1, 0],
+                "c": [-1, -1, 2],
             },
             index=["a", "b", "c"],
         )
@@ -578,6 +579,30 @@ class TestHelmertContrasts:
             coding_matrix_reduced_sparse.toarray() == reference_reduced.values
         )
 
+        reference_forward = pandas.DataFrame(
+            {
+                "a": [2, -1, -1],
+                "b": [0, 1, -1],
+            },
+            index=["a", "b", "c"],
+        )
+        coding_matrix_forward = contr.helmert(reverse=False).get_coding_matrix(
+            ["a", "b", "c"], reduced_rank=True
+        )
+        assert numpy.all(coding_matrix_forward == reference_forward)
+
+        reference_scaled = pandas.DataFrame(
+            {
+                "b": [-0.5, 0.5, 0],
+                "c": [-1 / 3, -1 / 3, 2 / 3],
+            },
+            index=["a", "b", "c"],
+        )
+        coding_matrix_scaled = contr.helmert(scale=True).get_coding_matrix(
+            ["a", "b", "c"], reduced_rank=True
+        )
+        assert numpy.all(coding_matrix_scaled == reference_scaled)
+
     def test_coefficient_matrix(self):
         reference = pandas.DataFrame(
             {
@@ -640,6 +665,18 @@ class TestDiffContrasts:
             coding_matrix_reduced_sparse.toarray(), reference_reduced.values
         )
 
+        reference_forward = pandas.DataFrame(
+            {
+                "a": [2.0 / 3, -1.0 / 3, -1.0 / 3],
+                "b": [1.0 / 3, 1.0 / 3, -2.0 / 3],
+            },
+            index=["a", "b", "c"],
+        )
+        coding_matrix_forward = contr.diff(backward=False).get_coding_matrix(
+            ["a", "b", "c"], reduced_rank=True
+        )
+        assert numpy.allclose(coding_matrix_forward, reference_forward)
+
     def test_coefficient_matrix(self):
         reference = pandas.DataFrame(
             {
@@ -714,6 +751,14 @@ class TestPolyContrasts:
         )
         assert numpy.allclose(coding_matrix_scores, reference_scores)
 
+        with pytest.raises(
+            ValueError,
+            match=re.escape(
+                "`PolyContrasts.scores` must have the same cardinality as the categories."
+            ),
+        ):
+            contr.poly(scores=[1, 2, 3]).get_coding_matrix(levels=["a", "b"])
+
     def test_coefficient_matrix(self):
         reference = pandas.DataFrame(
             {