The central thesis of this talk is that a graph neural network can learn things a tabular deep learning model structurally cannot — but that it may never survive your production latency and retraining budget, and that this is fine, because you can extract the value as offline features instead. Mariia Bulycheva, now a Senior Machine Learning Engineer at Intapp, describes work she did at her previous employer Zalando, where the team built and trained a GNN to improve the recommender behind the landing page. The talk is unusually honest about the ending: the end-to-end GNN did not ship, and the shipped system is a hybrid.
Zalando is a European online shopping platform for clothing, shoes, beauty, and lifestyle products. Every user journey starts on the landing page of the site or the opening page of the app, which makes the content shown there the highest leverage surface on the platform: relevant and engaging content keeps the user clicking, irrelevant content makes them drop out. This post reconstructs the approach from the presentation transcript, including the audience Q&A. Paragraphs marked Context are supplementary explanation for readers new to graph machine learning, and paragraphs marked Author's note are my own commentary; neither is a claim made by the speaker.
What You Will Learn
- How to reframe a click-prediction recommender as a link prediction problem on a heterogeneous graph, and what that representation buys you over tabular data.
- How to convert impression logs into training and test graphs without leaking information between them.
- What actually happens inside a GNN layer — neighbour sampling, message passing, and how contextual embeddings differ from content embeddings.
- Why batching on a graph is different, and how PyTorch Geometric's
LinkNeighborLoaderhandles it. - The two forms of leakage specific to graphs, and the
disjoint_train_ratiohyperparameter that mitigates one of them. - Why the team could not run the GNN in the serving path, and the hybrid architecture they shipped instead.
- The caveats raised in Q&A around cold-start content, engagement bias, result diversity, and seasonality.
The Ranking Problem on the Landing Page
Zalando uses the word content broadly. A piece of content can be a single shoppable item such as a pair of shorts, a carousel of similar products such as sneakers or jeans, a video curated by Zalando covering a specific pair of shoes or a beauty product, or a video posted by one of Zalando's creators — people who sign up, get shipped products, record looks, and post them so users can shop the look. Whatever the format, every piece of content is associated with a particular article, which in e-commerce is called an SKU.
The pipeline shape is a familiar two-stage recommender. An upstream algorithm selects roughly 2,000 candidate content pieces. The model Bulycheva's team owned has to score those 2,000 and pick the top 40 to render. The scoring target is the probability of a click, because a click means the user journey continues, and if the content is sponsored — that is, an ad — a click is direct revenue.
The team already had a well-performing system running online. The problem was that they had stopped being able to improve it, and in particular they could not steer it toward longer-term outcomes such as user retention or eventual purchases using classic deep learning models. That plateau, not a greenfield project, is what motivated looking at graphs.
Why a Graph Representation Helps
Bulycheva gives four reasons the graph framing fits platform engagement.
Engagement is natively a graph. Users and content are nodes; interactions are links. On Spotify the nodes would be users and tracks and the links would be plays and skips; at Zalando the nodes are users and shoppable content and the links are clicks, and can be extended to add-to-cart or add-to-wishlist.
Graphs explicitly model higher-order relations. Because you can traverse multiple hops, the model can express things like friends-of-friends on Facebook, or items frequently bought together on an e-commerce platform. A tabular model sees one user-item pair at a time and has no native way to reach two hops out.
Nodes carry features. You can attach user demographics to user nodes, or separately trained image representations to content nodes. Zalando already had a pipeline producing image embeddings — projections of articles into a latent space where visually similar articles sit close together. Bulycheva is precise about the limitation of those embeddings: they capture only the article's intrinsic characteristics. Two red knee-length long-sleeve dresses will be near-identical in that latent space while having completely different prices and completely different purchase histories. Training embeddings on the graph instead yields a contextual representation — how a node is connected to users and to other content.
The graph can be heterogeneous and weighted. Multiple node types and multiple link types are allowed, so the graph can go beyond users and content to include, for example, brand nodes: Zalando has a "follow" action on brands, which is a direct, explicit preference signal that fits naturally as an edge. Edge weights let you encode recency or freshness of a connection, or for video content the watch rate — how much of the video the user actually watched.
With that framing, the task becomes link prediction: given the existing view and click edges from the past, predict future click edges. More precisely, the team predicts click given view — conditional on this content being shown to this user, how likely is a click?
Turning Logs into a Graph
Zalando did not stand up a graph database such as Neo4j. The graph is built by a separate pipeline that runs before training, from the existing tabular user logs.
Every time a user loads the homepage they are shown 40 pieces of content, which
produces 40 rows of (user, content) with two labels. View is 1 if the
content appeared in the viewport for at least three seconds, whether because the
user scrolled to it or because it was at the top of the initial viewport.
Click can only be 1 if there was a view.
The single most important structural decision is that the train and test graphs must be fully disconnected. With tabular data this is trivial — rows are independent, so you split by row. On a graph, if you start from one connected graph, dissecting the training portion out of it is genuinely hard. The team sidestepped the problem by building the two graphs separately from disjoint time windows: seven days of user activity for the train graph, and the one consecutive day that follows for the test graph.
They also simplified the edge schema. The initial graph had two node types (client IDs and entity IDs) and two link types (viewed and clicked). Since the objective is click-given-view, they discarded the clicked links entirely and kept only viewed links, carrying a binary label: 0 for a view with no click, 1 for a view that produced a click. Same target, simpler model.
| Property | Train graph (7 days) | Test graph (1 day) |
|---|---|---|
| Distinct clients | ~5 million | ~1 million |
| Entity nodes | ~12,000 | ~7,000 |
| Viewed edges | ~20 million | ~3 million |
| Clicked edges | ~1.5 million (label = 1) | reported as "around 200" (see note) |
The 12,000 entity nodes are not the whole content library — they are the pieces that were actually shown on the homepage during that window and that people engaged with.
Author's note: the test-graph click figure of "around 200" against 3 million views is stated verbatim in the transcript, but it implies a click rate of about 0.0067%, against 7.5% (1.5M of 20M) in the training graph — a gap of roughly three orders of magnitude. This looks like a spoken or transcription slip — 200,000 would be consistent — but the source does not disambiguate it, so treat the test-set click count as unknown rather than assuming either value.
Node Features
Every node carries a 25 × 128 feature matrix, built from the pre-existing image embeddings.
- For a user node: the image embeddings of their 25 most recently purchased items.
- For a content node: the embeddings of up to 25 articles associated with that piece of content, zero-padded if there are fewer. A single-SKU content piece is therefore one embedding followed by 24 rows of zeros.
The embedding dimension is 128, hence 25 × 128. Note the consequence, which comes up again in the Q&A: a rarely-shopping user is mostly zeros, and a frequent shopper has a dense matrix. The team does not otherwise distinguish user types.
Tooling
The team experimented with two libraries. Deep Graph Library (DGL), in the TensorFlow-based configuration they used, is lower-level; PyTorch Geometric (PyG) implements much more for you. Bulycheva's recommendation for anyone starting out is counterintuitive and worth quoting in substance: start with DGL, precisely because it is lower level and forces you to understand how a GNN actually works. With PyG, so much is implemented that you do not see the pitfalls of a graph neural network. They started on DGL, found it genuinely useful, and switched to PyG once things got too complicated.
The downstream production system runs on TensorFlow while PyG is PyTorch-based,
and she reports this was not a problem in practice — the pieces combined fine.
Data preparation converts the logs into tensors and then into PyG's HeteroData
structure, the library's container for heterogeneous graphs.
Inside the Model
The end-to-end architecture Bulycheva describes has three stages.
- Feature preprocessing. Each node's 25 × 128 matrix must be reduced to a single vector. For user nodes they found an LSTM layer worked well; for content nodes they used a simple mean or max pool. She is explicit that the LSTM is optional and gives no reason for the asymmetry. (Author's note: an LSTM is order-sensitive, which fits a chronologically ordered purchase history in a way that a pool does not — but that rationale is mine, not hers.)
- Three GNN layers. She suggests thinking of these as analogous to convolutional layers in a CNN, and treats the layer count as a hyperparameter. The output is a new numerical representation per node — the user embedding and the content embedding.
- A simple classifier. A dot product of the two embeddings followed by a sigmoid gives the click probability. She notes you can go more complex, but a simple classifier is usually enough here.
What Happens in a GNN Layer
At initialisation, the features are not stored in the graph. The graph stores only indices into the 128-dimensional embedding vectors, keeping it lightweight, and the features are populated on the fly during training.
Then comes neighbour sampling. Conceptually, message passing wants to pull the features of every node connected to your node of interest into that node. Doing that exhaustively explodes: the first hop is manageable, the second hop is the neighbours of all of those, and so on exponentially. So you sample. Zalando used plain random sampling, which Bulycheva flags as the naive choice.
Then aggregation: the sampled neighbours' features are passed through trainable matrices into the centre node and concatenated with that node's own initial features, producing its new embedding.
Her two-layer walkthrough makes the ordering concrete. Given a node of interest and its first and second neighbourhoods, sampling selects a subset from each hop and discards the rest. Message passing then starts from the most distant hop and works inward: the two-hop nodes push their features through trainable matrices into the one-hop nodes, which concatenate; the one-hop nodes push through further trainable matrices into the centre node, which concatenates with its own features; a dense layer produces the final embedding vector. The trainable matrices are what learning adjusts.
This is the payoff she keeps returning to. "Embedding" is an overloaded word: you can train an embedding representing an entity's intrinsic properties, or one representing how it is connected within a population. The GNN produces the second kind — contextual embeddings.
Batching a Graph
Batching tabular data is obvious: take 256 rows, then the next 256. Batching images is obvious. Batching a graph is not, because the units are entangled. Depending on the task you batch either nodes or links; since this is link prediction, the team batches links.
PyG provides LinkNeighborLoader for exactly this. A link in the graph is stored
with an index, its own features, and its adjacent nodes. The loader batches links
together with their adjacent nodes and executes the neighbour sampling strategy
across the neighbourhoods, with a per-hop sample count you set as a
hyperparameter. The typical shape is decreasing fanout — Bulycheva's example is
10 neighbours from the first hop, 5 from the second, 3 from the third.
The result is a batch of, say, 256 links each accompanied by a small subgraph that preserves how that edge sits in the full graph. Training then proceeds per-subgraph: propagate messages through the sampled neighbourhoods, obtain the client and entity embeddings, predict the link, compare to the label, compute loss, backpropagate.
Training Pitfalls
Train/test leakage. The first pitfall she names, and her recommendation is a pipeline one: if you are constructing the graph rather than storing a knowledge graph database, build the train and test graphs separately from the start rather than dissecting one graph afterwards.
Label leakage through message-passing edges. This one is subtle and the team did not anticipate it. You perform message passing along edges, and you are also predicting labels for edges. If the model repeatedly passes information through an edge whose label it must predict, information about that label leaks into the representation. Bulycheva says the effect is hard to reason about abstractly but visible once you experiment.
The mitigation from the research literature is a holdout set of supervision-only
links: edges used purely as training labels and never used for message passing.
In PyG this is the disjoint_train_ratio hyperparameter, and the team confirmed
it improved generalisation. It is a genuine sweet-spot parameter. Set it too low
and the model sees all the links during message passing, restoring the leak. Set
it too high and you have removed too many edges from the message-passing graph,
so the model either fails to train or fails to generalise. Zalando's optimal
value was 0.3 — 30% of links reserved for supervision only.
Cold start needs an explicit fallback. A client who has never visited the platform is simply not connected to the graph, so there is nothing to propagate. The standard answer, which Zalando uses, is to show new clients the most popular content; their engagement then attaches them to the graph.
Naive sampling costs you accuracy. Random sampling can pick a neighbour that is barely relevant — an item the user viewed once — while ignoring items they viewed more often or more recently. Weighting the sampling by link recency or importance is the obvious improvement, and Bulycheva is clear that the team never got to it.
Over-smoothing. If you sample too many neighbours or use neighbourhoods that are too large, adjacent nodes' neighbourhoods overlap so heavily that their resulting embeddings become nearly identical. The embeddings then stop carrying distinguishing contextual information — which defeats the reason you built the GNN.
Offline Evaluation
The headline offline number is a ROC-AUC of 0.7788; the transcript does not say which configuration produced it. Bulycheva explains the metric for the audience: it measures how well the model separates clickable from non-clickable content, which is precisely the job.
The hyperparameters swept were the disjoint train ratio, the number of GNN layers, and the number of nodes sampled per neighbourhood. The comparison presented was a deep-and-cross neural network without GNN features as the baseline, against the same model fed GNN features inferred from graph networks of differing depth and sampling width. Adding the GNN features improved ROC-AUC.
Asked in Q&A how the hyperparameter values were chosen, her answer was unglamorous: experimentation, evaluation, and starting from research papers and what other companies had done. The architecture's backbone is GraphSAGE, which she recalled as developed by computer scientists at Stanford and implemented by Pinterest, and believed to still be running in Pinterest's production — she qualified both points with "I think", so treat the attribution as her recollection. She also notes a constraint on the search itself: industrial training-time limits cap how many hyperparameter configurations you can try.
Context: GraphSAGE is a well-known inductive GNN architecture based on sampling and aggregating a fixed number of neighbours per hop, which is why the per-layer fanout numbers above look the way they do. Its inductive property — generating embeddings for nodes unseen at training time — is what makes it practical for recommender graphs where new users and items arrive constantly.
The Production Wall
Offline results looked good, so the team tried to roll it out, and hit three problems.
Retraining cadence. On a shopping platform, and even more so on Instagram or TikTok, you want to adapt to shifting user preferences and newly appearing content as fast as possible — the hyper-personalised, within-session responsiveness users now expect. Zalando's existing system was retrained every 30 minutes, using incremental transfer learning on the rows of data that arrived during that window. They knew experimentally that skipping even two or three retraining cycles degraded performance noticeably. (The transcript says "every 30 minutes" and then refers to "hourly retrainings"; the cadence figure is the one she states explicitly, so treat the sub-hour cycle as the reliable number.)
That incremental trick does not port to a graph. You cannot take the last 30 minutes of activity as a standalone small graph and train on it, because the new nodes and edges only mean something in relation to the larger graph they attach to — the nodes that would pass features to them are, by construction, not in the increment. Making this work demands rethinking both data preparation and the retraining pipeline, which Bulycheva describes as substantial operational overhead and new infrastructural thinking.
Inference latency. Serving a landing page is latency-critical; extra milliseconds while the page loads are a visibly bad experience. With a classic model you take the user features and the entity features, run them through the deep-and-cross network, and get a click probability. With a GNN you additionally need the neighbouring users and entities, which means aggregating more data and reaching into neighbourhoods at request time. This was not feasible for the team because of the latency increase.
Training time and depth. Making the model better by adding layers increases complexity exponentially, and training time is itself bounded — exceed the limit and the model is stale by the time it ships.
Architecture And Data Flow
The shipped solution is a hybrid. Rather than serving the GNN, the team uses it only to produce embeddings: train the GNN on a daily, sometimes weekly cadence, run inference to generate user and entity contextual embeddings, write them to a feature store, and let the existing deep-and-cross downstream model consume them as input features at request time. That model, not the GNN, produces the click probability online.
The point Bulycheva stresses is that this already delivers value. You do not need the GNN end-to-end: swapping the contextual embeddings in for the image embeddings previously used as content features gives a performance boost on its own.
Author's note: the following diagram is my own rendering of the two architectures described in the talk, drawn to make the offline/online boundary explicit. Every box and edge corresponds to something Bulycheva describes, but the layout and the grouping are mine, not a reproduction of her slides.
flowchart TD
subgraph Offline["Offline - daily or weekly"]
Logs["User impression logs
(view / click, 7-day window)"] --> Graph["Graph build pipeline
PyG HeteroData"]
Graph --> Train["GNN training
LSTM/pool preprocessing
3 GraphSAGE-style layers
LinkNeighborLoader batching"]
Train --> Infer["GNN inference"]
Infer --> Store[("Feature store
user + entity
contextual embeddings")]
end
subgraph Online["Online - request time"]
Req["Landing page request"] --> Cand["Candidate selection
~2,000 content pieces"]
Cand --> DCN["Deep and cross network
retrained every 30 min"]
Store -.->|"embeddings as features"| DCN
DCN --> Top["Top 40 by click probability"]
Top --> Rules["Business rules
sponsored / carousel spacing"]
Rules --> Page["Rendered landing page"]
endThe abandoned alternative — the GNN scoring links directly in the request path — is what the latency and retraining constraints ruled out.
Trade-offs And Limitations
Author's note — evaluation was offline only. The talk reports ROC-AUC improvements from adding GNN features and does not report an online A/B test result, click-through lift, or the retention and long-term engagement outcomes that motivated the project in the first place. The observation that this is a meaningful gap — the stated goal was optimising for longer-term metrics, and no measurement against those metrics is presented — is mine, not a caveat she raises.
Engagement bias, and the choice not to model it away. Asked about overfitting to heavy shoppers who spend all day on the platform, Bulycheva agreed this is a real and much-discussed issue at Zalando. If you train on all the data undifferentiated, you improve the model for the highly engaged users, because they generate the signal. Their policy was to target mid-engagement users, on the reasoning that they are where the needle can actually move: very low engagers often do not care, arriving once a year on Black Friday to buy what they already came for. The mitigation is data filtering on user activity to balance the distribution, plus outlier filtering — but she was candid that there is nothing very specific beyond that, and that they still want heavy users to have a good experience.
Asked whether separate models per engagement tier would help, she said no, for a data-volume reason: strip out the highly engaged users and you are left with very little to train on, since users who visit two or three times contribute almost nothing. She also suggests the reverse of the intuition — highly active users may share structure with less active ones, so the model can transfer information from the dense part of the graph to the sparse part.
Result diversity is not solved by the model. An audience member pointed out that scoring each of the 40 slots independently by click probability could produce 40 variations of the same product. Bulycheva confirmed the fix is business rules applied on top of the model's scores — constraints such as not placing too much sponsored content consecutively, and not stacking multiple carousels in a row. She adds that ML engineers dislike this because it makes clean A/B testing hard, since the rules sit between the model's output and what the user sees.
Cold-start content is handled outside the graph. Asked how genuinely new products get recommended when they have no purchase or interaction history, Bulycheva described a separate pipeline: new content sits in a dedicated pool and is served via Thompson sampling until it accumulates 500 views. At that point it is sufficiently connected to the graph to be handled by the main recommender. (The transcript renders this as "Thomson sampling"; Thompson sampling is the standard multi-armed bandit method and is the unambiguous reading.)
Context: Thompson sampling is a Bayesian bandit strategy that samples from the posterior distribution over each option's reward and picks the sampled maximum, which naturally allocates more exposure to promising options while still exploring uncertain ones. It is a good fit for a warm-up pool precisely because it does not need the graph structure the GNN depends on.
Seasonality is deliberately not modelled. Asked whether seasonal effects such as December shopping peaks are accounted for, Bulycheva said no. The session timestamp is available as a feature, but they do not model month- or season-level seasonality. The cycle that matters for them is the seven-day week, because shopping patterns differ substantially between weekdays and weekends — which is also why the training window is seven days. Her reasoning is that longer-range seasonal patterns go stale fast, and that what happened two months ago says little about engagement now. She contrasts this with pricing, where she previously worked: there, experimental cycles and A/B testing phases were far longer because the team needed to capture cross-season shifts in prices and demand. Landing-page viewing and clicking behaviour, in her experience, does not show that much seasonal dependency.
Author's note: "not much seasonal dependency in landing-page click behaviour" is a claim about Zalando's own data as observed by the team, not a general property of e-commerce. If your catalogue is strongly seasonal, verify it before adopting a seven-day-only training window.
Practical Takeaways
- Build the train and test graphs as separate artefacts from disjoint time windows; never split one graph.
- Sweep
disjoint_train_ratiorather than accepting a default — 0.3 was Zalando's optimum, not a constant. - Store indices in the graph and hydrate features at training time.
- Learn on DGL, move to PyG when convenience outweighs pedagogy; PyTorch training with TensorFlow serving was not an obstacle.
- Use
LinkNeighborLoaderwith decreasing per-hop fanout, e.g. 10/5/3. - Reach for the hybrid pattern — GNN embeddings in a feature store, existing ranker in the request path — before committing to end-to-end GNN serving.
- Plan the non-graph escape hatches up front: a popularity fallback for new users, a bandit warm-up pool for new items.
- Weight neighbour sampling by link recency or importance; random sampling is what shipped and the first thing she would improve.
Future Directions the Speaker Named
Bulycheva closed with the work she would have pursued, noting she has since moved off recommender systems. Beyond smarter sampling, she would enrich node features — the model used only the last purchased items, while Zalando holds far more data that could go into the graph. She also flags that GNNs expose levers for controlling novelty and diversity of recommendations, which matters because Zalando is moving toward positioning itself around entertainment and inspiration: deliberately taking the user out of their usual tunnel and showing them something they have never bought but might like.
Key Terms
- Link prediction — The task of predicting whether an edge exists (or will exist) between two nodes; here, whether a user will click a shown content piece.
- Heterogeneous graph — A graph with more than one node type and/or edge type, such as user, content, and brand nodes connected by view, click, and follow edges.
- Message passing — The GNN operation where a node's representation is updated by aggregating transformed features from its sampled neighbours, applied iteratively from the outermost hop inward.
- Contextual embedding — A node representation learned from graph connectivity, capturing how a node relates to others, as opposed to a content/intrinsic embedding derived from the item itself.
- Fanout — The number of neighbours sampled at each hop during neighbour sampling, typically decreasing with distance from the target node.
disjoint_train_ratio— The PyTorch Geometric setting reserving a fraction of training edges for supervision only, excluding them from message passing to prevent label leakage.- Over-smoothing — The degenerate state where neighbourhood overlap makes node embeddings converge toward one another, destroying their discriminative value.
- GraphSAGE — The sample-and-aggregate GNN architecture used as the backbone here; inductive, so it can embed nodes unseen during training.
HeteroData— PyTorch Geometric's container type for heterogeneous graphs.LinkNeighborLoader— The PyTorch Geometric loader that batches edges along with their adjacent nodes and sampled neighbourhoods into trainable subgraphs.- ROC-AUC — Area under the receiver operating characteristic curve; the probability that a randomly chosen positive is ranked above a randomly chosen negative, used here as the offline ranking-quality metric.
- SKU — Stock keeping unit; the individual article a piece of content points at.
- Thompson sampling — A Bayesian bandit algorithm used to allocate exposure to new content until it has enough interactions to join the graph.
The most transferable lesson here is the shape of the compromise. The graph representation genuinely captured something the tabular model could not — higher-order structure and contextual, connectivity-derived embeddings — but the operational properties that make graphs expressive are the same properties that made them hostile to a 30-minute retraining loop and a millisecond-sensitive serving path. Rather than fighting that, the team demoted the GNN to an offline feature generator and kept the existing ranker in the request path. That is a pattern worth reaching for whenever a promising model class collides with an established serving budget: ask what the model produces that is durable enough to precompute, and ship that instead.
Reference: Mariia Bulycheva, Reimagining Platform Engagement with Graph Neural Networks, recorded at InfoQ Dev Summit Munich; the InfoQ page is dated April 13, 2026.