AI/ML Engineer Top 100 Interview Questions
100 AI/ML engineer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers problem framing, classical algorithms, clustering, recommender systems, forecasting, text, evaluation, applied deep learning and MLOps, from fundamentals to system design.
Questions 21-40 of 100
Q21.How do you handle a categorical feature with thousands of distinct values?
One-hot encoding thousands of categories creates huge, sparse inputs and lets rare categories overfit. Use encodings that compress the categories or share information between them.
Group rare categories Frequency encoding Hashing Merges categories below a frequency threshold into "other" Replaces each category with how often it appears Maps categories into a fixed number of buckets with a hash function Simple and removes noisy levels One compact column Fixed size and handles new categories Loses detail on rare values Categories with equal counts look the same Collisions merge unrelated values Target-based encoding Learned embeddings Native categorical handling Replaces each category with a smoothed target average, computed out of fold A neural network learns a dense vector per category Some tree methods split on categories directly Very informative Captures similarity between categories No manual encoding Leaks the target if not done out of fold Needs enough data and adds complexity Can overfit very rare levels Practical points:
- Decide how to treat unseen categories at prediction time, such as a shared "unknown" bucket.
- Check whether the feature is really an identifier, such as a user ID, that will not generalize to new rows.
- Combine approaches: frequency plus embedding is a common pairing.
- Compare encodings by validation score and by how the model behaves on rare and new categories, not only on common ones.
Q22.What is multicollinearity, and how do you detect it?
Multicollinearity means two or more features are strongly linearly related, so the model cannot separate their individual effects. Predictions may stay fine, but coefficients become unstable, can flip sign, and lose their meaning.
VIF_j = 1 / (1 − R²_j)
What each term means:
- R²_j: how well feature
jis predicted by a linear regression on all the other features. - VIF_j, the variance inflation factor: how much the variance of feature
j's coefficient is inflated by its correlation with the others. A value of 1 means no collinearity; above 5 to 10 is usually a concern.
How to detect it:
- A correlation matrix catches pairs, but not one feature explained by several others together.
- Variance inflation factors catch both.
- Unstable coefficients that change a lot between bootstrap samples or after small data changes are a warning sign.
How to handle it: drop or combine redundant features, use regularization that spreads weight across them, or use principal components. When only prediction matters, collinearity may be acceptable.
Example
A house price model with both "floor area" and "number of rooms":
- Regressing rooms on floor area and the other features gives
R² = 0.85. VIF = 1 / (1 − 0.85) ≈ 6.7, so the rooms coefficient's variance is about 6.7 times what it would be without collinearity.- Refitting on different bootstrap samples, the rooms coefficient swings between −4,000 and +9,000 per room.
- Replacing rooms with "area per room" removes most of the shared information and stabilizes both coefficients.
Q23.How would you engineer features from timestamps?
A raw timestamp is rarely useful to a model. Break it into the parts that drive behavior, encode cyclical parts so that midnight sits next to 23:00, and measure time relative to meaningful events.
Calendar parts Cyclical encoding Special days Recency Hour, day of week, month, quarter Sine and cosine of the hour or day of year Public holidays, paydays, promotion days Days since last purchase or since sign-up Captures daily, weekly and seasonal patterns Keeps December next to January Explains spikes that calendars alone miss A strong signal for churn and engagement Durations Rolling aggregates Local time Time between events, session length Purchases in the last 7 and 30 days Hour in the user's time zone Captures pace and behavior Summarizes recent history Behavior follows local, not server, time Avoiding leakage:
- Compute every feature as of the prediction time, using only events before it.
- Rolling windows must end before the prediction moment, not at the end of the day or the dataset.
- Watch for timestamps that are filled in later, such as "closed at", which reveal the outcome.
Q24.Why do tree ensembles often beat neural networks on tabular data?
On typical tabular data, gradient-boosted trees and random forests usually match or beat neural networks with less tuning, because their way of building decisions suits how tabular features behave.
Reasons:
- Mixed feature types: tables combine counts, amounts, flags and categories on very different scales. Trees split on thresholds and do not care about scale.
- Irregular patterns: tabular relationships are often step-like, such as "more than 3 failed payments", which trees capture directly while networks prefer smooth functions.
- Uninformative features: trees ignore features that never give good splits, while networks spread attention across them.
- Dataset size: many tabular datasets have thousands to a few million rows, often too few for a network to beat strong tree models.
- Missing values: many tree methods handle them natively.
- Less tuning: good defaults and early stopping get trees close to their best quickly.
When neural networks win on tables:
- Very large datasets.
- High-cardinality categories that benefit from learned embeddings.
- Tables combined with text, images or sequences in one model.
- When the model must share representations with other tasks.
Interview tip: recommend gradient-boosted trees as the default baseline for tabular problems; it shows practical judgment rather than reaching for the most fashionable model.
Q25.How do you calculate log loss, and why does it punish confident mistakes?
Log loss, or cross-entropy loss, measures how well predicted probabilities match the true labels. It is the average negative log of the probability the model gave to the correct outcome.
log_loss = −(1/N) × Σ [y × log(p) + (1 − y) × log(1 − p)]
What each term means:
- y: the true label, 1 or 0.
- p: the predicted probability that the label is 1.
- N: the number of examples.
Why confident mistakes cost so much: the penalty for the correct class is
−log(p). Atp = 0.9it is about 0.105, at 0.5 about 0.693, at 0.1 about 2.303, and at 0.01 about 4.605. It grows without limit as the probability for the true class approaches zero, so one confidently wrong prediction can outweigh many good ones.When to use it: when the probabilities themselves matter, such as pricing risk or ranking by likelihood, because it rewards calibration as well as correct ranking. Clip probabilities away from exactly 0 and 1 before taking logs, to avoid infinite values.
Example
Computing log loss for three sets of predictions on the same four labels:
import math def log_loss(labels: list[int], probabilities: list[float], eps: float = 1e-15) -> float: total = 0.0 for y, p in zip(labels, probabilities): p = min(max(p, eps), 1 - eps) # avoid log(0) total += -(y * math.log(p) + (1 - y) * math.log(1 - p)) return total / len(labels) labels = [1, 0, 1, 1] print(round(log_loss(labels, [0.9, 0.1, 0.8, 0.7]), 4)) # 0.1976 print(round(log_loss(labels, [0.6, 0.4, 0.6, 0.6]), 4)) # 0.5108 print(round(log_loss(labels, [0.9, 0.1, 0.8, 0.01]), 4)) # 1.2598At a 0.5 threshold the first two sets are both 100% accurate, yet the hesitant second set scores much worse on log loss. In the third set, the single confident mistake on the last example costs about 4.6, far more than the other three predictions together.
Q26.How do you calculate R-squared and adjusted R-squared?
R-squared is the share of the target's variance that a regression model explains. Adjusted R-squared corrects it for the number of features, because plain R-squared never falls when you add a feature, even a useless one.
R² = 1 − SS_res / SS_totadjusted R² = 1 − (1 − R²) × (n − 1) / (n − p − 1)
What each term means:
- SS_res: the sum of squared differences between actual and predicted values.
- SS_tot: the sum of squared differences between actual values and their mean, which is the error of always predicting the mean.
- n: the number of rows; p: the number of features.
How to read them:
- R² of 0 means no better than predicting the mean; 1 means perfect. On test data it can even be negative, when the model is worse than the mean.
- Adjusted R² rises only if a new feature improves the fit more than chance would, so it is fairer for comparing models with different numbers of features.
When to use them: for explaining linear regression fits. For choosing predictive models, prefer cross-validated error measured in the target's own units, which stakeholders understand more easily.
Example
A model fit on 50 rows with 5 features:
- With
SS_res = 200andSS_tot = 1,000,R² = 1 − 200 / 1,000 = 0.80. - Adjusted:
1 − 0.20 × 49 / 44 ≈ 0.777. - Adding 10 random features nudges R² up to 0.81, because extra features always fit a little noise.
- Adjusted R² with 15 features:
1 − 0.19 × 49 / 34 ≈ 0.726, a clear drop, revealing that the new features did not help.
Q27.How do you choose the number of clusters?
There is no single correct number of clusters, so combine a quantitative measure of cluster quality with whether the clusters are useful for the purpose, such as distinct customer segments a team can act on.
Elbow method Silhouette score Gap statistic Plot within-cluster spread against k and pick the bend Compares each point's distance to its own cluster with the nearest other cluster Compares the spread with what random data would give The bend is often unclear Favors round, well-separated clusters Needs more computation Stability Business usefulness Re-cluster bootstrap samples and check that clusters persist Ask whether each cluster can be described and acted on Slower, but reveals unreliable splits Subjective, but often decisive A practical process:
- Scale features first, since clustering depends on distances.
- Compute two or three measures over a range of k, such as 2 to 10.
- Inspect the candidate solutions: cluster sizes, typical members and their defining features.
- Check stability on resamples, and prefer solutions that persist.
- Validate with stakeholders, since four segments people can use beat seven that nobody can tell apart.
If no k looks clearly better, the data may not have distinct clusters, and a continuous score or a density-based method may suit it better.
Q28.How do density-based and hierarchical clustering differ from centroid-based clustering?
Centroid-based clustering assigns each point to the nearest of k centers. Density-based clustering finds dense regions separated by sparse ones and marks isolated points as noise. Hierarchical clustering builds a tree of nested clusters that can be cut at any level.
Centroid-based, such as k-means Density-based Hierarchical Number of clusters chosen in advance Number of clusters found from the data Number of clusters set by where you cut the tree Round clusters of similar size Clusters of any shape Cluster shape depends on the linkage rule Outliers forced into a cluster Outliers labeled as noise Outliers merged late, visible in the tree Main parameter: k Main parameters: neighborhood radius and minimum points Main parameters: distance measure and linkage rule Scales to very large data Scales to medium or large data with indexes Suits small to medium data; cost grows quickly Outputs a flat assignment Outputs a flat assignment plus noise Outputs a full hierarchy When to use which:
- Centroid-based for large datasets with compact, similar-sized groups, such as quick customer segmentation.
- Density-based for irregular shapes and when noise must be separated, such as geographic hotspots or anomaly detection.
- Hierarchical when the nesting itself is useful, such as product taxonomies, or when you want to explore several levels of detail.
Density-based methods struggle when clusters have very different densities, so check results visually or with domain knowledge.
Q29.Implement k-means clustering for one-dimensional data.
Pick starting centers, then alternate two steps until nothing changes: assign each value to its nearest center, then move each center to the mean of its assigned values. Every pass can only lower the total squared distance, so the loop converges.
Key points:
- Initialization matters: k-means finds a local optimum. Spreading starting centers across the data, or running several random starts and keeping the best, avoids poor solutions.
- Stop on convergence: when centers stop moving, or after a maximum number of iterations.
- Handle empty clusters: keep the old center, or reseed it from the farthest point.
- Inertia, the total squared distance from points to their centers, measures fit and drives the elbow method.
In one dimension, sorting the data allows exact methods, but the iterative version shown here is the same algorithm used for any number of features, with absolute distance replaced by straight-line distance.
Example
Clustering order values into three groups, then comparing inertia for different k:
def kmeans_1d(values: list[float], k: int, iterations: int = 100): ordered = sorted(values) centers = [ordered[(2 * i + 1) * len(ordered) // (2 * k)] for i in range(k)] # spread out for _ in range(iterations): clusters = [[] for _ in range(k)] for value in values: distances = [abs(value - center) for center in centers] clusters[distances.index(min(distances))].append(value) new_centers = [sum(c) / len(c) if c else centers[i] for i, c in enumerate(clusters)] if new_centers == centers: break centers = new_centers inertia = sum(min((value - center) ** 2 for center in centers) for value in values) return centers, clusters, inertia values = [12, 15, 14, 13, 45, 50, 48, 52, 47, 90, 95, 88] centers, clusters, inertia = kmeans_1d(values, 3) print([round(c, 2) for c in centers]) # [13.5, 48.4, 91.0] print(clusters) # [[12, 15, 14, 13], [45, 50, 48, 52, 47], [90, 95, 88]] for k in range(1, 5): print(k, round(kmeans_1d(values, k)[2], 1)) # Output: # [13.5, 48.4, 91.0] # [[12, 15, 14, 13], [45, 50, 48, 52, 47], [90, 95, 88]] # 1 10364.9 # 2 2766.9 # 3 60.2 # 4 37.7Inertia falls sharply up to k = 3 and barely improves at k = 4, which is the "elbow" that suggests three clusters.
Q30.How would you detect anomalies when you have no labels?
Without labels, model what normal looks like and flag points that fit it poorly. Choose the method by data type and volume, then review a sample of flagged points with domain experts, because "unusual" is not always "bad".
Statistical thresholds Isolation forest Density-based scoring Flags values far from the mean or outside quartile fences Flags points that random splits isolate in very few steps Flags points in much sparser regions than their neighbors Good for single metrics and quick baselines Good for tabular data with many features Good for clusters of different densities Reconstruction error Forecast residuals Flags inputs that an autoencoder trained on normal data reconstructs badly Flags large gaps between a time series forecast and the actual value Good for images, sensor signals and complex data Good for seasonal metrics such as traffic or sales Practical process:
- Define the purpose: fraud, equipment faults and data errors need different features.
- Clean the training window, since known incidents in the "normal" data teach the model that they are normal.
- Set the alert budget: decide how many alerts a team can review per day, and set the threshold from it.
- Collect feedback: reviewed alerts become labels, which can later train a supervised model that performs much better.
Account for seasonality and context. A spike at 09:00 on a Monday may be normal, while the same value at 03:00 is not.
Q31.Write a function that flags outliers using the interquartile range.
Compute the first and third quartiles, take their difference as the interquartile range, and flag values below
Q1 − 1.5 × IQRor aboveQ3 + 1.5 × IQR. Because quartiles ignore the extremes, the fences are not dragged around by the outliers themselves, unlike a mean and standard deviation rule.Key points:
- Robust: a few extreme values barely move the quartiles, so they cannot hide themselves.
- The 1.5 factor is a convention; 3 marks only extreme outliers. Choose it from how many alerts you can review.
- Quartile methods differ slightly between tools; pick one and use it consistently.
- Skewed data, such as response times or incomes, gets many false high outliers. Apply the rule to the logarithm of the values instead.
- Small samples give unstable quartiles, so be cautious below about 20 values.
Flagged values are candidates, not verdicts. A 980 ms response may be a real slow request worth investigating rather than bad data to delete.
Example
Flagging unusual response times in milliseconds:
import statistics def iqr_outliers(values: list[float], factor: float = 1.5): q1, _, q3 = statistics.quantiles(values, n=4, method="inclusive") iqr = q3 - q1 low, high = q1 - factor * iqr, q3 + factor * iqr return [v for v in values if v < low or v > high], (low, high) response_ms = [120, 135, 128, 142, 131, 125, 138, 980, 129, 133, 140, 15, 127] outliers, (low, high) = iqr_outliers(response_ms) print(outliers) # [980, 15] print(low, high) # 110.5 154.5The quartiles are 127 and 138, so the interquartile range is 11 and the fences are
127 − 16.5and138 + 16.5. The 980 ms request and the suspiciously fast 15 ms one, possibly a cached error page, are both flagged.Q32.When would you use a nonlinear embedding for visualization instead of a linear projection?
Use a linear projection, such as principal components, when you want a faithful global picture: distances and directions mean something, and the axes can be interpreted. Use a nonlinear embedding, such as t-SNE or similar neighbor-preserving methods, when you want to see local cluster structure that a linear view would squash together.
Linear projection Nonlinear embedding Preserves global variance and large distances Preserves local neighborhoods Axes are interpretable feature combinations Axes are meaningless Distances between clusters are meaningful Distances between clusters are often not meaningful Cluster sizes reflect the data Cluster sizes can be distorted Deterministic Varies with the random seed and settings Projects new points directly Usually needs refitting or an approximation for new points When to use which:
- Linear first, for a quick and honest overview and as input compression for models.
- Nonlinear to explore whether embeddings, such as images or text vectors, form distinct groups.
Reading nonlinear plots safely:
- Do not read meaning into gaps between clusters, cluster sizes or global layout.
- Try several settings and seeds; trust only structure that persists.
- Use the plot to form hypotheses, then test them with the original data.
Q33.How do collaborative filtering and content-based recommendation differ?
Collaborative filtering recommends items that similar users liked, using only the pattern of user interactions. Content-based recommendation suggests items whose attributes resemble what the user already liked. Most production systems combine both.
Collaborative filtering Content-based Uses user-item interactions Uses item attributes and the user's own history Cannot recommend new items until they get interactions Recommends new items as soon as attributes exist Needs some interactions from a new user first Needs a few liked items or stated preferences from a new user Finds surprising, cross-category suggestions Tends to suggest more of the same Needs many interactions Needs good item metadata Explains as "People like you also liked" Explains as "Because you liked items like this" When to use which:
- Collaborative when interaction data is rich, such as a large catalog with many active users.
- Content-based for new catalogs, fast-changing items such as news, and niche users.
- Hybrid approaches feed both kinds of signals into one ranking model, which is the usual production design.
Both can create filter bubbles. Add deliberate diversity and exploration so the system keeps learning about new interests.
Q34.How does matrix factorization produce recommendations?
Matrix factorization represents each user and each item as a short vector of learned numbers, called latent factors, chosen so that the dot product of a user's and an item's vectors approximates how much the user likes the item. Missing entries in the user-item matrix are then predicted from those vectors.
- Start with a sparse matrix of known interactions, such as ratings or purchases.
- Learn user and item vectors, typically 20 to 200 numbers each, by minimizing the error on known entries plus a regularization penalty.
- Add biases: a global average, a per-user offset and a per-item offset capture generous users and universally popular items.
- Predict every unseen pair with
global mean + user bias + item bias + user · item. - Recommend the highest-scoring unseen items.
Practical points:
- For clicks and purchases rather than ratings, use implicit-feedback variants that treat unobserved items as weak negatives.
- The learned factors often correspond to recognizable themes, such as genre or price level, though they are not labeled.
- New users and items have no vectors, which is the cold-start problem.
Example
Scoring three films for one user with three latent factors:
- The user's vector is
[0.9, 0.1, 0.4]. - Thriller
[0.8, 0.0, 0.3]:0.9 × 0.8 + 0.1 × 0.0 + 0.4 × 0.3 = 0.84. - Comedy
[0.1, 0.9, 0.2]:0.09 + 0.09 + 0.08 = 0.26. - Documentary
[0.3, 0.1, 0.9]:0.27 + 0.01 + 0.36 = 0.64. - With a global mean of 3.6, a user bias of +0.2 and a thriller bias of +0.1, the predicted rating for the thriller is
3.6 + 0.2 + 0.1 + 0.84 = 4.74.
The thriller is recommended first, then the documentary.
Q35.How do you solve the cold-start problem in a recommender system?
Cold start happens when a new user or a new item has no interaction history, so collaborative methods have nothing to work with. Bridge the gap with information that exists from day one, and learn quickly from the first interactions.
For new users:
- Popular and trending items, ideally by region, device or acquisition channel.
- Onboarding questions: a few preferred topics or example items, chosen to be informative.
- Context: the page they arrived on, their search query or time of day.
- Fast updates: use session behavior within minutes, rather than waiting for a nightly retrain.
For new items:
- Content features: text, category, price and image embeddings map the new item near similar existing ones.
- Controlled exposure: reserve a small share of recommendation slots to show new items and gather feedback.
- Creator or brand history: a new item from a popular seller can borrow some of its predecessors' signal.
Exploration: bandit-style strategies balance showing what is known to work with trying new items and new interests. Track cold-start performance separately, since overall metrics are dominated by established users and items.
Q36.How do you calculate precision at k and recall at k for a recommender?
For a single user, precision at k is the share of the top
krecommendations that are relevant, and recall at k is the share of all the user's relevant items that appear in the topk. Average both across users to evaluate the system.precision@k = relevant items in the top k / krecall@k = relevant items in the top k / total relevant items
Key points:
- Relevance comes from held-out behavior, such as items the user bought or rated highly in a later time period.
- Choose k to match the interface: if the page shows 5 slots, measure at 5.
- Precision and recall trade off: larger k usually raises recall and lowers precision.
- They ignore order within the top k; use ranking metrics that reward placing relevant items higher when position matters.
- Users with no relevant items make recall undefined, so exclude them or report them separately.
Offline metrics only approximate value. Confirm improvements with an online experiment on clicks, purchases or retention.
Example
Precision and recall at three cut-offs for one user:
def precision_recall_at_k(recommended: list[str], relevant: set[str], k: int) -> tuple[float, float]: hits = sum(1 for item in recommended[:k] if item in relevant) return hits / k, hits / len(relevant) recommended = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] relevant = {"B", "E", "H", "Z"} # Z was never recommended for k in (3, 5, 10): precision, recall = precision_recall_at_k(recommended, relevant, k) print(f"k={k}: precision {precision:.2f}, recall {recall:.2f}") # Output: # k=3: precision 0.33, recall 0.25 # k=5: precision 0.40, recall 0.50 # k=10: precision 0.30, recall 0.75Recall can never reach 1 here, because one relevant item, Z, is not in the list at all.
Q37.What makes time series forecasting different from ordinary regression?
In time series, observations are ordered and depend on their own past, so the order itself carries information and the future must never leak into training. Ordinary regression assumes rows are independent and can be shuffled freely.
Key differences:
- Temporal dependence: today's value is strongly related to yesterday's, which is a feature, not a nuisance.
- Trend and seasonality: long-term movement and repeating cycles, such as daily, weekly or yearly patterns, must be modeled explicitly.
- Validation: splits must respect time; random cross-validation overstates accuracy.
- Features must exist at prediction time: lagged values and rolling statistics, not values from the forecast period.
- Multi-step horizons: forecasting 30 days ahead means either predicting each day separately or feeding predictions back in, and errors grow with the horizon.
- Changing patterns: promotions, holidays and structural breaks shift behavior, so models need retraining and event features.
- Uncertainty matters: planning decisions need prediction intervals, not just a single number.
Many forecasting problems are best solved by turning them into supervised learning with carefully built lag features. That works only if the time-based rules above are followed strictly.
Q38.What is stationarity, and how do you make a series stationary?
A series is stationary when its statistical properties, such as mean, variance and the relationship between a value and its recent past, stay the same over time. Many classical forecasting models assume it, because patterns learned from the past then stay valid in the future.
Signs of non-stationarity:
- A trend: the mean rises or falls over time.
- Seasonality: regular cycles in the mean.
- Changing variance: fluctuations grow as the level grows, common with sales and revenue.
How to make a series stationary:
- Difference it: model the change from one period to the next,
y_t − y_(t−1), which removes a trend. - Seasonal differencing:
y_t − y_(t−s), such ass = 7for daily data with a weekly cycle. - Transform it: take logarithms to stabilize variance that grows with the level.
- Remove components: subtract an estimated trend and seasonal pattern.
- Check: plot the result, and use a statistical unit-root test as supporting evidence.
Tree-based and neural forecasting models do not strictly require stationarity. Even so, differencing or relative-change targets usually help them extrapolate a trend they would otherwise flatten.
Example
Removing a trend by differencing:
- Weekly sign-ups: 100, 104, 109, 113, 118 and 122. The mean keeps rising, so the series is not stationary.
- First differences:
104 − 100 = 4, then 5, 4, 5 and 4. - The differenced series hovers around 4.4 with no trend, so a simple model can forecast the next change, about 4 or 5.
- Adding that change back to the last value forecasts about 126 or 127 for next week.
Q39.How do you validate a forecasting model without leaking the future?
Train only on data from before each validation period, and evaluate on the period that follows, repeating the process across several points in time. This is called walk-forward or rolling-origin validation.
flowchart LR A["Train on weeks 1 to 20"] --> B["Test on weeks 21 to 24"] B --> C["Train on weeks 1 to 24"] C --> D["Test on weeks 25 to 28"] D --> E["Train on weeks 1 to 28"] E --> F["Test on weeks 29 to 32"]- Pick several cut-off dates spread over the history.
- Train on data before each cut-off, and forecast the next horizon, matching the real use, such as 4 weeks ahead.
- Average errors across folds, and look at their spread.
- Keep the final period untouched as a test of the chosen model.
Leaks to watch for:
- Features built with future data, such as rolling means centered on the date or normalization fitted on the full series.
- Late-arriving data: if sales are finalized three days late, the model must not use the last three days at prediction time.
- Random shuffling in any step, including hyperparameter search.
- Choosing the model on the test period by trying many variants against it.
Expanding windows use all available history; sliding windows keep a fixed length. Sliding windows suit series where old behavior no longer applies.
Q40.How do you build lag and rolling-window features for forecasting?
For each prediction date, create features from values strictly before that date: lagged values, such as the same day last week, and rolling statistics over recent windows. Each row then becomes an ordinary supervised learning example.
Key points:
- Choose lags from the data's cycles: lag 1 for momentum, lag 7 for weekly patterns, lag 365 for yearly.
- Rolling windows such as means, minimums, maximums and standard deviations over the last 3, 7 or 28 periods summarize recent behavior.
- End every window before the prediction date. A window that includes the target day leaks the answer.
- Match the forecast horizon: if you forecast 7 days ahead, the most recent usable value is 7 days old, so lag 1 is not available.
- Drop or handle early rows that do not have enough history.
- Group by series when you have many, such as one per store, so lags never cross from one store's data into another's.
Add calendar features and known future events, such as holidays and planned promotions, alongside lags. These are known in advance and are safe to use.
Example
Building lag and rolling features from ten days of sales:
def make_features(series: list[float], lags: tuple = (1, 7), window: int = 3) -> list[dict]: rows = [] for t in range(max(max(lags), window), len(series)): row = {f"lag_{lag}": series[t - lag] for lag in lags} history = series[t - window:t] # ends before day t, so no leakage row[f"mean_{window}"] = round(sum(history) / window, 2) row["target"] = series[t] rows.append(row) return rows sales = [20, 22, 21, 25, 30, 28, 27, 35, 33, 31] for row in make_features(sales): print(row) # Output: # {'lag_1': 27, 'lag_7': 20, 'mean_3': 28.33, 'target': 35} # {'lag_1': 35, 'lag_7': 22, 'mean_3': 30.0, 'target': 33} # {'lag_1': 33, 'lag_7': 21, 'mean_3': 31.67, 'target': 31}Only three rows are usable, because the 7-day lag needs a week of history. With real data, that lost warm-up period is a small cost.