1.4.6 Categorical predictors
A factor is a categorical variable: its values name categories, such as a species or a number of cylinders, rather than measure a quantity. R’s model.matrix expands a factor into indicator columns, and so does the formula front end. The examples in this section use R’s mtcars, whose "cyl" counts each car’s cylinders, and R’s iris, whose "Species" names each flower’s species as a string. glmnet/datasets provides them as tables:
> (require glmnet/datasets) > (table-column-names iris) '("Sepal.Length" "Sepal.Width" "Petal.Length" "Petal.Width" "Species")
> (vector-ref (cdr (assoc "Species" iris)) 100) "virginica"
1.4.6.1 Which columns are factors
A column is a factor when its values are strings or symbols, as a character column is in R. A string and a symbol with the same text are the same category, so "setosa" and 'setosa are. A column of booleans is a factor too, as R’s logical columns are, with the categories "FALSE" and "TRUE". A column of numbers is a number, as in R, and (factor x), R’s factor(x), makes a factor of it, for numbers that are codes, as cylinder counts are:
> (formula-predictor-names (Sepal.Length . ~ . Petal.Width + Species) iris) '("Petal.Width" "Speciesversicolor" "Speciesvirginica")
> (formula-predictor-names (mpg . ~ . wt + cyl) mtcars) '("wt" "cyl")
> (formula-predictor-names (mpg . ~ . wt + (factor cyl)) mtcars) '("wt" "(factor cyl)6" "(factor cyl)8")
A factor’s columns are named as R names them, by the variable and the category, so "Speciesversicolor" is R’s Speciesversicolor. (factor cyl) is named by its source, as a transform is, so R’s factor(cyl)6 is "(factor cyl)6" here. A column that mixes numbers with strings, or either with booleans, is an error that names it.
1.4.6.2 Levels and baselines
The categories of a factor are its levels: its distinct values, sorted as R’s factor() sorts them. Numbers are sorted by value, strings and symbols with string<?, and booleans "FALSE" before "TRUE". The first level is the baseline, which the other levels are compared with. A factor with one level is an error, as in R, since it has nothing to compare.
string<? compares characters by code point, which is how R’s sort orders strings in the C locale: digits before capital letters, and capital letters before small ones, so "B" comes before "a". An R session in another locale, such as en_US, sorts letters without regard to case first, so for levels that differ in case R there can choose a different baseline.
A fitted model keeps its factors’ levels, as R’s xlevels, and formula-model-levels returns them:
> (define cars (formula-fit (mpg . ~ . wt + (factor cyl)) mtcars #:lambda 0.1)) > (formula-model-levels cars) '(("(factor cyl)" "4" "6" "8"))
> (coef cars)
'(("(Intercept)" . 33.87775166152442)
("wt" . -3.2721654656479635)
("(factor cyl)6" . -3.7451819901218832)
("(factor cyl)8" . -5.578271444181383))
Four cylinders is the baseline. At λ = 0.1, a six-cylinder car does 3.7 miles per gallon fewer than a four-cylinder car of the same weight, and an eight-cylinder car 5.6 fewer.
1.4.6.3 Treatment contrasts
The design matrix codes a factor by treatment contrasts, R’s default: a column for each level but the baseline, which is 1 in the rows at that level and 0 in the others. A coefficient is then the difference between its level and the baseline. For the cylinder counts:
Cylinders |
| "(factor cyl)6" |
| "(factor cyl)8" |
4 (baseline) |
| 0 |
| 0 |
6 |
| 1 |
| 0 |
8 |
| 0 |
| 1 |
The first cars of mtcars have six, four and eight cylinders:
> (define C (formula-design-matrix (~ mpg (factor cyl)) mtcars)) > (design-matrix->rows (design-matrix-select-rows C '(0 2 4))) '((1.0 0.0) (0.0 0.0) (0.0 1.0))
Without an intercept, R’s model.matrix codes the first factor by dummies: a column for every level, which together take the intercept’s place. So does the formula front end. It does this for the first factor only, and the others keep their contrasts:
> (formula-predictor-names (mpg . ~ . 0 + wt + (factor cyl) + (factor gear)) mtcars)
'("wt"
"(factor cyl)4"
"(factor cyl)6"
"(factor cyl)8"
"(factor gear)4"
"(factor gear)5")
1.4.6.4 Factors in interactions
The interaction of a number and a factor has a column for each of the factor’s columns, their product. (* wt (factor cyl)) gives each cylinder count a slope of its own, as a difference from the baseline’s:
> (formula-predictor-names (mpg . ~ . wt * (factor cyl)) mtcars) '("wt" "(factor cyl)6" "(factor cyl)8" "wt:(factor cyl)6" "wt:(factor cyl)8")
Which columns a factor has in a term follows R’s rule of marginality, the factors attribute of R’s terms. A factor is coded by contrasts in a term when the rest of the term, without the factor, is empty or is contained in an earlier term of the formula, that is, when every variable of the rest is a variable of one earlier term. It is coded by dummies when it is not. In (* wt (factor cyl)), wt is a term, so the interaction has contrasts. wt : (factor cyl) alone gives each level a slope and has dummies, and so does (factor cyl) + wt : (factor cyl), where each level has its own intercept and its own slope:
> (formula-predictor-names (mpg . ~ . wt : (factor cyl)) mtcars) '("wt:(factor cyl)4" "wt:(factor cyl)6" "wt:(factor cyl)8")
> (formula-predictor-names (mpg . ~ . (factor cyl) + wt : (factor cyl)) mtcars)
'("(factor cyl)6"
"(factor cyl)8"
"(factor cyl)4:wt"
"(factor cyl)6:wt"
"(factor cyl)8:wt")
The earlier term need not be the rest itself. After wt : hp, which contains wt, wt : (factor cyl) has contrasts, as in R, though wt is not a term of its own:
> (formula-predictor-names (mpg . ~ . wt : hp + wt : (factor cyl)) mtcars) '("wt:hp" "wt:(factor cyl)6" "wt:(factor cyl)8")
Two factors interact level by level, with the first factor’s levels varying fastest:
> (formula-predictor-names (mpg . ~ . (factor cyl) * (factor am)) mtcars)
'("(factor cyl)6"
"(factor cyl)8"
"(factor am)1"
"(factor cyl)6:(factor am)1"
"(factor cyl)8:(factor am)1")
1.4.6.5 Transforms that make factors
A transform whose values are strings, symbols or booleans is a factor too, as in R, and (factor expr) makes a factor of any transform’s values, so the categories can be computed from numbers:
> (formula-predictor-names (mpg . ~ . (> hp 150) + wt) mtcars) '("(> hp 150)TRUE" "wt")
> (formula-predictor-names (mpg . ~ . (if (> hp 150) "high" "low") + wt) mtcars) '("(if (> hp 150) \"high\" \"low\")low" "wt")
> (formula-predictor-names (mpg . ~ . (factor (quotient hp 100)) + wt) mtcars)
'("(factor (quotient hp 100))1"
"(factor (quotient hp 100))2"
"(factor (quotient hp 100))3"
"wt")
"high" sorts before "low", so it is the second transform’s baseline.
A transform reads a column of strings, symbols or booleans as its values, as R’s calls read a character or logical column, and a column of numbers as flonums. So a transform can compare or recode a factor’s values, as R’s I(Species == "setosa") and factor(toupper(Species)) do:
> (formula-predictor-names (Sepal.Length . ~ . (equal? Species "setosa") + Petal.Width) iris) '("(equal? Species \"setosa\")TRUE" "Petal.Width")
> (formula-predictor-names (Sepal.Length . ~ . (factor (string-upcase Species)) + Petal.Width) iris)
'("(factor (string-upcase Species))VERSICOLOR"
"(factor (string-upcase Species))VIRGINICA"
"Petal.Width")
A symbol stays a symbol, so a comparison with a string is false for a column of symbols; compare it with eq? and a quoted symbol instead. A boolean column’s values are #t and #f, so R’s I(!b) is (not b), a factor, and R’s I(b * x) is (if b x 0), a number. A column that a transform reads must hold one kind of value, numbers, strings, symbols or booleans, with strings and symbols counting as two kinds, since a transform tells them apart, and an error names the transform and the column where it does not.
1.4.6.6 New data and levels
predict codes a new table’s factors with the model’s levels, as R’s predict does with the model’s xlevels. The new rows need not have every level, in any order, and their values match the levels by label: the number 8.0 is the level "8", and a symbol is the level of the string with its text. A level that the model was not fitted with is an error that names it, as R’s "factor has new levels" is:
> (define new-cars (list (cons "wt" '(2.5 3.5)) (cons "cyl" '(8 4)))) > (predict cars new-cars) '(20.119066553223128 22.425172531756548)
> (predict cars (list (cons "wt" '(3.0)) (cons "cyl" '(5)))) predict: a factor has levels that the model was not fitted
with
factor: "(factor cyl)"
new levels: '("5")
levels: '("4" "6" "8")
A column that a transform reads must hold the kind of value it held when the model was fitted, since the transform sees the values themselves. R’s functions convert their arguments, so R’s ifelse(b, x, 0) reads 1 and 0 as TRUE and FALSE, but in Racket 0 is true, and (if b x 0) would change silently. So would (equal? Species "setosa") on a column of symbols, which it never finds equal. predict raises an error that names the transform, the column and both kinds:
> (define setosa (formula-fit (Sepal.Length . ~ . (equal? Species "setosa") + Petal.Width) iris #:lambda 0.05))
> (predict setosa (list (cons "Petal.Width" '(0.2)) (cons "Species" '(setosa)))) predict: a transform reads a column whose values are of
another kind than when the model was fitted
transform: "(equal? Species \"setosa\")"
column: "Species"
fitted with: strings
given: symbols
1.4.6.7 Categorical responses
For the binomial and multinomial families, a response of strings, symbols or booleans is categorical, as a factor response is for R’s glmnet: its levels are the classes. The second of a binomial response’s two classes is the one whose probability the model gives. coef keys a multinomial model’s coefficients by class, and predict with #:type 'class returns classes:
> (define species (formula-fit (~ Species all) iris #:family 'multinomial #:lambda 0.05)) > (map car (coef species)) '("setosa" "versicolor" "virginica")
> (define flowers (list (cons "Sepal.Length" '(5.0 6.5)) (cons "Sepal.Width" '(3.4 3.0)) (cons "Petal.Length" '(1.5 5.6)) (cons "Petal.Width" '(0.2 2.1)))) > (predict species flowers #:type 'class) '("setosa" "virginica")
1.4.6.8 From R’s factors
R |
| Racket |
| Columns |
factor(cyl) |
| (factor cyl) |
| "(factor cyl)6", "(factor cyl)8" |
f, a character column |
| f, strings or symbols |
| "fb", "fc" |
b, a logical column |
| b, booleans |
| "bTRUE" |
I(x > 3) |
| (> x 3) |
| "(> x 3)TRUE" |
ifelse(x > 3, "hi", "lo") |
|
| "(if (> x 3) \"hi\" \"lo\")lo" | |
factor(x > 3) |
| (factor (> x 3)) |
| "(factor (> x 3))TRUE" |
I(f == "a") |
| (equal? f "a") |
| "(equal? f \"a\")TRUE" |
factor(toupper(f)) |
| (factor (string-upcase f)) |
| "(factor (string-upcase f))B", "(factor (string-upcase f))C" |
I(!b) |
| (not b) |
| "(not b)TRUE" |
x*f |
| (* x f) |
| "x", "fb", "fc", "x:fb", "x:fc" |
y ~ 0 + f |
| (~ y 0 f) |
| "fa", "fb", "fc" |
R’s other contrasts and its ways to choose a baseline have no counterpart here (see Matching R).