Services Process Blog Demo

Get in touch

hello@sovont.com
Back to blog
· Sovont · 3 min read

The Default Value That Lied to Your Model

Sentinel values and bad defaults look like real data. They pass every schema check, corrupt your features, and make your model confidently wrong in production.

Data Engineering

Your schema says the column is non-null. Your pipeline passes validation. Your feature looks fine in the notebook. Then your model starts underperforming on a segment of users and you spend two weeks debugging before someone notices that every missing age is stored as 0, every unknown country as "XX", and every failed lookup as -1.

Sentinel values are the data quality problem that looks like clean data. They survive every NULL check, every NOT NULL constraint, every row count assertion. They carry structure with no meaning — and your model treats them as signal.


Here’s how this happens in practice.

A source system was built years ago by engineers who needed something for the age field when the user didn’t provide one. Zero was convenient. The schema said INTEGER NOT NULL, so they put 0. Same for -1 on IDs that didn’t resolve. Same for "N/A", "Unknown", "NONE" as string sentinels scattered across categorical columns.

Your pipeline ingests this faithfully. Validation passes — the values are valid types. Your feature engineering doesn’t filter them because there are no NULLs to filter. Your model trains on them and learns that age=0 is a real segment with real behavior. It isn’t. It’s a bucket of everyone who didn’t tell you their age.

Now you have a model that makes confident predictions about a ghost population, and you have no idea.


The patterns that cause this:

Legacy NOT NULL constraints with no semantic enforcement. Databases enforce type and nullability, not meaning. A 0 in an age column is structurally valid and semantically useless. No constraint catches the difference.

Sentinel values from external sources. Third-party exports use their own conventions for missing data — -999, 9999, "N/A", "NULL" as a string. You ingest them as-is, they flow downstream, and your pipeline never knew to treat them as missing.

Default values in ORM configs. Django, Rails, and their friends will happily write 0 or "" to every column that hits a validation error rather than fail loudly. That default is in production and has been writing garbage to your events table for three years.

Imputation before exploration. Someone noticed the NULLs, ran fillna(0), and called it handled. Except 0 is now indistinguishable from a legitimate zero. The imputation destroyed information instead of preserving it.


How to fix it systematically:

Inventory your sentinel values before feature engineering. For every numeric column, check the distribution. Spikes at 0, -1, 9999 are flags. For categoricals, look for "Unknown", "N/A", "". Document what they mean and whether they should be NULL.

Convert sentinels to real NULLs at the ingestion boundary. Make the pipeline responsible for semantic normalization, not the model. If age=0 means unknown, store it as NULL. Downstream everything is cleaner.

Validate for sentinel patterns, not just types. Your data quality checks should include business-rule assertions: age NOT IN (0, -1, 999), country_code NOT IN ('XX', 'N/A', 'NONE'). These are cheap and catch real problems.

Track imputation explicitly. When you do need to impute, create a companion column — age_imputed: bool. That way the model can learn that imputed values are different from observed ones. More importantly, you can segment your evaluations on it.


Your model doesn’t know what your data means. It knows what patterns your data has. Feed it sentinels as signal and it learns the wrong patterns — confidently, at scale, in production.

Clean up at the boundary. That’s the only place it’s cheap.