Choosing MaxEnt Feature Classes for Small Samples in Python
Feature classes decide how much shape MaxEnt is allowed to carve into each response curve, and with a small occurrence set the wrong choice silently converts sampling noise into confident-looking habitat. This guide solves the narrow decision of which feature classes to enable as a function of sample size, and how to set them explicitly in Python. It is one operation inside the parent MaxEnt Model Training & Tuning workflow, part of the broader Species Distribution Modeling with MaxEnt pipeline. The occurrence count that drives this decision comes straight out of Presence-Only Data Preparation, so thin and deduplicate before you count — the number that matters is effective presences, not raw records.
The five feature classes
MaxEnt transforms each raw predictor into one or more features before fitting. The five classes, with their standard one-letter codes, are:
- Linear (L) — the predictor itself; constrains the fitted mean to the presence mean.
- Quadratic (Q) — the square; constrains the variance, allowing a single-peaked (unimodal) response.
- Product (P) — pairwise products of predictors; captures interactions between variables.
- Threshold (T) — step functions at learned breakpoints; a hard on/off response.
- Hinge (H) — piecewise-linear ramps from learned knots; flexible monotone-then-flat shapes without the discontinuity of thresholds.
Richer classes add parameters. Linear and quadratic together need only a handful of coefficients; hinge, product, and threshold can each add dozens. With few presences those extra degrees of freedom have nothing to constrain them, so they fit the idiosyncrasies of where the surveyor happened to walk.
When to use which classes: the sample-size rule
The default MaxEnt auto-feature rules (Phillips & Dudík 2008) scale feature richness with the number of presence samples. They are conservative by design and remain the best starting point for small-sample work. Treat them as the decision table below; override only with a reason.
| Presence samples | Feature classes | elapid feature_types |
Rationale |
|---|---|---|---|
< 10 |
L | ["linear"] |
Too few points to estimate a curve; fit only the mean and avoid any bend |
10 – 14 |
LQ | ["linear", "quadratic"] |
Enough to add a single unimodal peak, the commonest ecological response |
15 – 79 |
LQH | ["linear", "quadratic", "hinge"] |
Hinge features approximate flexible non-linear shapes without threshold discontinuities |
≥ 80 |
LQPTH | ["linear", "quadratic", "product", "threshold", "hinge"] |
Enough data to support interactions and step responses without overfitting |
The thresholds are not magic numbers so much as bias–variance guardrails: below each cutoff the added flexibility of the next class costs more in variance than it buys in fit. For genuinely tiny samples (under ~15), staying at L or LQ is almost always the honest choice, even when a richer model reports a higher training AUC.
Minimal reproducible example
elapid’s MaxentModel takes feature_types as a list of full names (or the abbreviations l, q, p, t, h). The helper below reads the effective presence count and returns the default class set, which you then pass straight into the model. Inputs are the covariate matrix x and the label vector y (1 presence, 0 background).
import numpy as np
import elapid
def default_feature_types(n_presence):
"""Return the default MaxEnt feature classes for a presence count."""
if n_presence < 10:
return ["linear"]
if n_presence < 15:
return ["linear", "quadratic"]
if n_presence < 80:
return ["linear", "quadratic", "hinge"]
return ["linear", "quadratic", "product", "threshold", "hinge"]
n_presence = int(np.sum(y == 1))
fc = default_feature_types(n_presence)
print(f"{n_presence} presences -> feature_types={fc}")
model = elapid.MaxentModel(
feature_types=fc,
beta_multiplier=1.0, # tune separately once classes are fixed
clamp=True,
transform="cloglog",
)
model.fit(x, y)
suitability = model.predict(x)
Fixing the feature classes by sample size first, then searching the regularization multiplier, keeps the tuning grid small and honest. The two axes interact, so once you have a class set, sweep with a betamultiplier grid search rather than trusting the default .
Parameter reference
| Argument | Type | Default | Recommended | Ecological rationale |
|---|---|---|---|---|
feature_types |
list[str] |
auto | by sample size (table above) | Richness must match how many presences can constrain it; excess classes memorize survey artifacts |
n_presence |
derived int |
— | count after thinning | Duplicate or clustered records inflate the count and license feature classes the data cannot support |
n_hinge_features |
int |
50 |
10–30 for small samples |
Fewer knots make hinge ramps smoother and reduce parameters when presences are scarce |
n_threshold_features |
int |
50 |
≤ 20 or disable |
Threshold steps are the quickest route to overfitting; withhold them below ~80 presences |
beta_multiplier |
float |
1.0 |
tune after fixing classes | Regularization and feature richness interact; a rich set may still be safe at high |
clamp |
bool |
True |
True |
Prevents hinge and threshold features from extrapolating wildly beyond the training range |
Expected output and verification
The fit should produce a model whose non-zero coefficient count is comfortably below the presence count — a model with more free parameters than presences cannot be identified and will not transfer. Check it explicitly instead of trusting the AUC.
import numpy as np
def verify_feature_choice(model, y, max_ratio=0.5):
"""Confirm the fitted feature set is not overparameterized."""
n_presence = int(np.sum(y == 1))
coefs = np.asarray(model.estimator.coef_).ravel()
k = int(np.sum(np.abs(coefs) > 1e-8))
ratio = k / n_presence
assert ratio <= 1.0, (
f"{k} parameters for {n_presence} presences — not identifiable"
)
if ratio > max_ratio:
print(f"WARNING: {k} params / {n_presence} presences = {ratio:.2f}; "
"consider dropping hinge/threshold classes")
else:
print(f"OK — {k} params for {n_presence} presences (ratio {ratio:.2f})")
return k
As a rule of thumb, keep the parameter-to-presence ratio below about 0.5 for reported models. If a richer class set pushes it higher, either drop back one row in the decision table or raise the regularization multiplier until the surplus coefficients shrink to zero. Then confirm the response curves are ecologically plausible — a hinge feature that spikes at the edge of the sampled range is memorized noise, not a real preference.
Common pitfalls
- Counting raw records instead of thinned presences. Clustered citizen-science points inflate the count and license hinge or product classes the independent data cannot support. Count after the spatial thinning in Presence-Only Data Preparation.
- Leaving auto-feature selection on. If the engine picks feature classes by its own sample-size heuristic, your explicit
feature_typesis ignored and the whole tuning grid collapses. Set the classes deliberately and disable auto-selection. - Adding threshold features to small samples. Threshold steps overfit faster than any other class; below ~80 presences they encode where individual points fell. Withhold T until the sample is large.
- Reading a high training AUC as validation. Richer feature classes always raise the training AUC — that is exactly the overfitting symptom. Judge the choice on held-out spatial-fold performance, never on the fit to training data.
Frequently asked questions
Can I use hinge features with only 20 presences?
The default rule allows LQH from 15 presences, so 20 is on the low end of acceptable. Reduce n_hinge_features to around 10–20 so each ramp adds fewer parameters, raise the regularization multiplier, and inspect the response curves for edge spikes. If the curves look memorized, drop back to LQ.
Do these thresholds change with more background points?
No. The feature-class rules are driven by the number of presence samples, not background points. Adding background improves the entropy estimate of the landscape but does nothing to constrain the extra parameters that hinge, product, and threshold features introduce, so the presence count remains the limiting quantity.
Related
- MaxEnt Model Training & Tuning — the parent workflow that fixes feature classes then tunes regularization
- Tuning the MaxEnt Regularization Multiplier via Grid Search — the second tuning axis, swept once classes are set
- Presence-Only Data Preparation — thin and deduplicate to get the effective presence count this decision depends on
- Model Validation & AUC Metrics — judge feature-class choices on held-out spatial folds, not training AUC
- Species Distribution Modeling with MaxEnt — the full presence-only modeling pipeline