A model that scores 0.93 in a notebook and 0.68 in production is rarely a modelling problem. Most of the time, something went wrong before the first call to fit(): in the cleaning steps that feel like admin work and get rushed.
Cleaning is where the quiet damage happens. Nothing errors out, the notebook runs top to bottom, and the validation numbers look excellent. Here are five traps worth checking on your current dataset, and what to do instead.
The default move is to fill numeric columns with the mean or median and categorical ones with the most frequent value. It works often enough to become a habit, which is exactly the problem. A missing value is frequently information: a blank income field on a loan application, an empty "last contact date" for a customer who was never contacted, a missing serial number on a returned unit.
Filling a tenure column with 0 is worse than filling it with the median. Zero months of tenure is not a neutral guess; it is a specific and usually false claim about that customer. The model will learn from the fiction you supplied.
Before imputing, study the missingness itself. Count it per column and per row, then check whether the target rate differs between rows that have the value and rows that do not. A rough triage:
Time series need separate care. Forward-filling is reasonable when a sensor reports on a schedule and the value genuinely persists between readings. It is wrong when the gap means the sensor was offline, because you have just invented measurements.
This is the most common source of leakage. You load the full table, impute with the global mean, scale with the global standard deviation, pick your clipping thresholds, collect the list of categories from every row, and only then call train_test_split. Every one of those steps has already seen the test set.
The effect is small on large datasets and brutal on small ones, and it is always in the optimistic direction. The model looks better than it is because the held-out rows quietly shaped the training features.
The fix is mechanical, which is good news:
If you work in Python, put each step in a pipeline and fit the pipeline rather than the individual pieces. It removes the temptation to compute "just the means" on the full dataframe. The same rule applies to target encoding and to any resampling for class imbalance: those happen inside the fold, never before the split.
Two versions of one mistake, and both are easy to miss because the code looks reasonable.
Replacing a high-cardinality column such as city or product code with the average target rate for that category is useful. Computing it once across the entire dataset is not: each row's own label contributes to its own feature, so the model can partly memorise the answer. Compute the encoding inside each training fold, apply it to validation, and smooth it so categories with only a handful of rows do not receive extreme values.
Lifetime spend, total support tickets, number of logins: all useful features, all leaky if the window extends beyond the timestamp of the prediction. If you are predicting whether a customer churned in March, every aggregate must be built from data available at the end of February. Rolling windows and cumulative sums deserve a second look for the same reason.
For every feature, ask one question: would this exact value have been available at the moment the prediction had to be made?
A random row-level split assumes rows are independent. Often they are not. The same customer appears in dozens of rows, the same device generates hundreds of sessions, the same document was uploaded twice with slightly different formatting. A random split scatters near-identical records across training and validation, and the model looks smart because it has effectively seen the answers.
Before you split:
Trimming extremes feels responsible. Sometimes it is: a person aged 412, a transaction of negative ten million. But extremes often carry the signal you care about. Fraud is an outlier. A bulk order is an outlier. A pressure spike in a sensor log is the event you wanted to detect. Clipping those values teaches the model that the most interesting cases look ordinary.
The more damaging cleaning failures are usually not outliers at all. They are inconsistencies that survive into training:
Profile before you fix. Print the minimum, maximum, unique count and most frequent values for every column, then normalise casing and whitespace, parse dates with an explicit format, standardise units, and validate ranges against what you know about the domain. Handle outliers per model: a linear model with a long tail may benefit from clipping or a log transform, while tree-based models usually tolerate raw extremes well enough.
None of this is glamorous, and none of it takes long once it becomes routine. Keep a cleaning log: one line per decision, written as you make it. When validation and production disagree, that log is the first place to look, and it is usually where the answer is hiding.
Photo: freestocks-photos / Pixabay