1.2 Feature Engineering and Feature Selection in BigQuery ML
Key Takeaways
- The TRANSFORM clause stores preprocessing inside the model, so ML.PREDICT applies the identical transformations at inference and training-serving skew becomes structurally impossible.
- BigQuery ML applies automatic preprocessing even with no TRANSFORM clause — standardization for numeric columns, one-hot encoding for STRING columns (label encoding for tree models), multi-hot for non-numeric ARRAY columns, mean imputation for numeric NULLs, and a separate category for categorical NULLs.
- ML.FEATURE_INFO reports per-column min, max, mean, median, standard deviation, null count, and category count, making it the fastest way to spot a leaking or degenerate feature before training.
- Manual preprocessing functions such as ML.BUCKETIZE, ML.QUANTILE_BUCKETIZE, ML.STANDARD_SCALER, ML.FEATURE_CROSS, and ML.NGRAMS must be written inside TRANSFORM to be replayed at prediction time.
- For linear and logistic models, L1 regularization drives coefficients to exactly zero and therefore doubles as automated feature selection; ML.WEIGHTS shows which features survived.
1.2 Feature Engineering and Feature Selection in BigQuery ML
Blueprint reference: Section 1.1, "Performing feature engineering or selection using BigQuery ML."
Most candidates arrive at this topic knowing how to write CREATE MODEL. Far fewer can answer the question the exam actually asks, which is almost always some variation of: "the model scores well offline but produces garbage in production — what is wrong?" On BigQuery ML the answer is very often that the engineer preprocessed features in a separate query, trained on the result, and then forgot to reproduce that same query at prediction time. BigQuery ML has a purpose-built cure for this, and knowing it is the difference between two plausible-looking answer options.
Why the TRANSFORM Clause Is the Whole Topic
Training-serving skew is any difference between the transformations applied during training and those applied during inference. If you bucketize age into deciles in a training query and then send raw ages to ML.PREDICT, the model receives values it has never seen and silently degrades.
The TRANSFORM clause solves this by storing the preprocessing logic inside the model artifact. Expressions written inside TRANSFORM are executed on the training data and then re-executed automatically on every row passed to ML.PREDICT, ML.EVALUATE, and ML.EXPLAIN_PREDICT. The caller passes raw columns; BigQuery replays the transformations.
CREATE OR REPLACE MODEL `analytics.churn_model`
TRANSFORM (
ML.STANDARD_SCALER(monthly_spend) OVER () AS monthly_spend_z,
ML.QUANTILE_BUCKETIZE(tenure_days, 10) OVER () AS tenure_decile,
ML.FEATURE_CROSS(STRUCT(plan_tier, region)) AS plan_region,
churned
)
OPTIONS (
model_type = 'LOGISTIC_REG',
input_label_cols = ['churned']
) AS
SELECT monthly_spend, tenure_days, plan_tier, region, churned
FROM `analytics.customers`;
Two details are exam-relevant. First, the label column must be listed inside TRANSFORM or it is dropped and training fails. Second, analytic preprocessing functions such as ML.STANDARD_SCALER and ML.QUANTILE_BUCKETIZE require an OVER () clause because they compute statistics across the whole training set — and those statistics are frozen into the model, which is precisely what makes the replay correct.
Automatic Preprocessing You Get for Free
Even with no TRANSFORM clause, BigQuery ML preprocesses inputs based on column type. Knowing these defaults prevents a whole family of wrong answers where a candidate assumes manual encoding is mandatory.
| Input type | Automatic treatment |
|---|---|
INT64, NUMERIC, BIGNUMERIC, FLOAT64 | Standardized (centred at zero) — except for boosted tree, random forest, and k-means models. NULLs are replaced with the column's mean |
STRING, BOOL, BYTES, DATE, DATETIME, TIME | One-hot encoded — except boosted tree and random forest models, which use label encoding. NULLs become an additional category, not the mode |
ARRAY of non-numeric values | Multi-hot encoded — the natural representation for tag or basket columns |
TIMESTAMP | Unix time standardized; the extracted calendar components one-hot encoded |
STRUCT | Expanded into its constituent fields, each preprocessed by its own type |
ARRAY<NUMERIC>, ARRAY<STRUCT> | No transformation |
Two of those defaults are worth reading twice. First, tree-based models take label encoding rather than one-hot, which is why a boosted tree tolerates high-cardinality categoricals that would blow up a linear model's feature space. Second, a NULL in a categorical column becomes its own category rather than being filled with the most common value — so "missing" is itself a signal the model can learn from, which is usually what you want and occasionally a leak (a field that is only ever null for one class).
The practical consequence: to train a churn model on a table containing a plan_tier STRING column, you do not need to write a pivot query. Passing the raw column is both sufficient and safer.
Manual Transformation Functions Worth Memorizing
| Function | Purpose | Typical use |
|---|---|---|
ML.BUCKETIZE | Split a numeric column at explicit boundaries | Regulatory age bands, price tiers |
ML.QUANTILE_BUCKETIZE | Split into equal-population buckets | Skewed distributions such as income or spend |
ML.STANDARD_SCALER | Zero mean, unit variance | Linear and DNN models with mixed scales |
ML.MIN_MAX_SCALER | Rescale to [0, 1] | Bounded inputs, image-like features |
ML.FEATURE_CROSS | Cartesian product of categoricals | Capturing interactions a linear model cannot learn |
ML.NGRAMS | Token n-grams from arrays of words | Text classification without a full NLP stack |
ML.POLYNOMIAL_EXPAND | Polynomial and interaction terms | Non-linear structure in a linear model |
ML.LABEL_ENCODER | Categorical to integer index | Tree models where ordinality is harmless |
ML.HASH_BUCKETIZE | Hash strings into a fixed number of buckets | Very high cardinality identifiers |
ML.FEATURE_CROSS deserves special attention. A logistic regression on plan_tier and region learns one weight per tier and one per region; it structurally cannot represent "enterprise customers in EMEA behave differently." A crossed feature gives it that capacity without moving to a boosted tree.
Inspecting Features Before You Train
ML.FEATURE_INFO is the fastest sanity check in the entire BigQuery ML surface. It returns, per input column: minimum, maximum, mean, median, standard deviation, category count, and null count.
SELECT * FROM ML.FEATURE_INFO(MODEL `analytics.churn_model`);
Read it for three failure signatures. A column whose category_count roughly equals the row count is an identifier masquerading as a feature. A column with a near-zero standard deviation carries no signal and wastes capacity. A column with a very high null fraction will be dominated by imputed values, so the model is largely learning the imputation constant.
Feature Selection: L1 Regularization and ML.WEIGHTS
BigQuery ML has no separate "feature selector" product. Selection is done through regularization and inspection.
For LINEAR_REG and LOGISTIC_REG, set l1_reg. L1 (lasso) regularization drives uninformative coefficients to exactly zero, unlike L2 (l2_reg), which only shrinks them toward zero. That distinction is a favourite exam discriminator: if a scenario asks for a sparse model or explicitly for feature elimination, L1 is the answer; if it asks only to control overfitting from correlated features, L2 is fine.
CREATE OR REPLACE MODEL `analytics.churn_sparse`
OPTIONS (model_type = 'LOGISTIC_REG', l1_reg = 0.1, input_label_cols = ['churned'])
AS SELECT * EXCEPT (customer_id) FROM `analytics.customers`;
SELECT * FROM ML.WEIGHTS(MODEL `analytics.churn_sparse`)
ORDER BY ABS(weight) DESC;
Features whose weight is zero have been eliminated. For tree-based models (BOOSTED_TREE_CLASSIFIER, RANDOM_FOREST_CLASSIFIER), weights do not exist; use ML.FEATURE_IMPORTANCE instead, which reports gain, cover, and weight-based importance. ML.GLOBAL_EXPLAIN provides model-agnostic Shapley-based attributions when the model was created with enable_global_explain = TRUE.
Exam Traps
- Leakage through a post-outcome column. A feature such as
cancellation_reasonis only populated for churned customers. It will produce a near-perfect AUC offline and a useless model in production.ML.FEATURE_INFOnull counts expose it. - Preprocessing outside TRANSFORM. Any option that preprocesses in a view or a scheduled query, then predicts on raw data, is wrong.
- Confusing
ML.BUCKETIZEwithML.QUANTILE_BUCKETIZE. Explicit boundaries versus equal-population boundaries. - Assuming you must one-hot encode manually. BigQuery ML already does it for STRING columns.
A team trains a BigQuery ML logistic regression using a view that bucketizes tenure into deciles and standardizes monthly spend. Offline AUC is 0.91. In production, an application calls ML.PREDICT with raw tenure and spend values from the operational table, and precision collapses. What is the correct remediation?
A risk team must ship a credit model where auditors require that the final model use as few input features as possible, with unused features provably eliminated rather than merely down-weighted. The current model is a BigQuery ML LOGISTIC_REG with 140 candidate columns. Which approach satisfies the requirement?
An analyst wants a BigQuery ML model to learn that enterprise-tier customers in the EMEA region churn differently from enterprise-tier customers elsewhere. The model is a linear classifier with plan_tier and region as separate STRING columns. What is the minimal correct change?
A BigQuery ML churn model reaches 0.997 AUC on the evaluation split. ML.FEATURE_INFO shows that the column support_closure_code has a null fraction of 0.94 and that the non-null rows correspond almost entirely to the positive class. What is the most likely explanation?