Building an AI Cross-Sell Recommendation System for Sales

Building an AI Cross-Sell Recommendation System for Sales

AI Development Areas

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1284
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1240
  • image_logo-advance_0.webp
    B2B Advance company logo design
    696
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    982
  • image_logo-aider_0.webp
    AIDER company logo development
    917
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1031

Building an AI Cross-Sell Recommendation System for Sales

We were approached by an e-commerce project with 50,000 SKUs: the rule "printer → cartridge" yielded only 8% conversion. They needed a machine learning-based recommendation system that finds non-obvious pairs and adapts to each customer's behavior. We implemented a hybrid system for e-commerce and retail: association rules for basic bundles + gradient boosting for personalization. Result: an 18% increase in average order value in the first month, providing a significant revenue boost. Here’s how it works.

Problems We Solve

Static rules do not capture hidden patterns. For example, customers who bought a child car seat have a 22% probability of buying a trunk organizer—even though the items are from different categories. ML finds such patterns through association rules (Lift > 2.0). Personalization via gradient boosting takes into account customer history: if the user already bought an organizer, the recommendation shifts to another item.

Another common problem is goods with a short consumption cycle (consumables, subscriptions). Here, predicting the next purchase is required. A model based on interval distribution outputs items the customer is about to reorder. This way, we increase repeat sales without promotions.

The third problem is the cold start for new products. If a product has no purchase history, Apriori rules won't work. The solution is to use content-based features (category, price, brand) and collaborative filtering through embeddings. For this, we enrich the gradient boosting model with product features.

How We Build the CrossSellRecommender Model

Market Basket Analysis + Personalization

Hybrid architecture: first Apriori gives raw rules, then gradient boosting ranks candidates for a specific user. This combination provides a conversion lift 3 times higher than a rule-based approach.

import pandas as pd import numpy as np from mlxtend.frequent_patterns import apriori, association_rules from mlxtend.preprocessing import TransactionEncoder from sklearn.ensemble import GradientBoostingClassifier class CrossSellRecommender: def __init__(self, min_support: float = 0.01, min_confidence: float = 0.1): self.min_support = min_support self.min_confidence = min_confidence self.rules = None self.cross_sell_map = {} self.personalization_model = None def fit_association_rules(self, orders_df: pd.DataFrame, order_col: str = "order_id", item_col: str = "item_id"): """Find association rules via Apriori""" # Transaction baskets baskets = orders_df.groupby(order_col)[item_col].apply(list).tolist() te = TransactionEncoder() te_array = te.fit_transform(baskets) df_encoded = pd.DataFrame(te_array, columns=te.columns_) # Frequent itemsets frequent_itemsets = apriori( df_encoded, min_support=self.min_support, use_colnames=True, max_len=3 ) # Association rules self.rules = association_rules( frequent_itemsets, metric="lift", min_threshold=1.2 ) self.rules = self.rules[self.rules['confidence'] >= self.min_confidence] self.rules = self.rules.sort_values('lift', ascending=False) # Mapping: item → list of recommendations with metrics for _, rule in self.rules.iterrows(): for antecedent in rule['antecedents']: if antecedent not in self.cross_sell_map: self.cross_sell_map[antecedent] = [] for consequent in rule['consequents']: if antecedent != consequent: self.cross_sell_map[antecedent].append({ 'item_id': consequent, 'confidence': rule['confidence'], 'lift': rule['lift'], 'support': rule['support'] }) # Sort by lift for item in self.cross_sell_map: self.cross_sell_map[item].sort(key=lambda x: x['lift'], reverse=True) def recommend_cross_sell(self, cart_items: list[str], user_history: list[str] = None, n: int = 5) -> list[dict]: """Cross-sell for current cart""" candidates = {} for item_id in cart_items: related = self.cross_sell_map.get(item_id, []) for rec in related: rec_id = rec['item_id'] # Skip if already in cart or in history if rec_id in cart_items: continue if user_history and rec_id in user_history: continue if rec_id not in candidates: candidates[rec_id] = {'score': 0, 'triggers': []} candidates[rec_id]['score'] += rec['lift'] candidates[rec_id]['triggers'].append(item_id) # Normalization if not candidates: return [] sorted_candidates = sorted( [{'item_id': k, **v} for k, v in candidates.items()], key=lambda x: x['score'], reverse=True ) return sorted_candidates[:n] def get_complementary_categories(self, category: str) -> list[str]: """Complementary categories""" category_rules = self.rules[ self.rules['antecedents'].apply(lambda x: category in str(x)) ]['consequents'].apply(lambda x: list(x)).explode().value_counts() return category_rules.head(5).index.tolist() 

Temporal Patterns: Next Purchase

class NextPurchasePredictor: """Predict next purchase based on history""" def predict_next_items(self, user_id: str, purchase_history: list[dict], catalog_features: pd.DataFrame) -> list[tuple]: """ purchase_history: [{item_id, date, quantity, category}] Returns: [(item_id, probability)] """ if len(purchase_history) < 3: return [] # Repeat purchase patterns item_intervals = {} for i in range(1, len(purchase_history)): item = purchase_history[i]['item_id'] prev_same = [h for h in purchase_history[:i] if h['item_id'] == item] if prev_same: days_between = ( pd.to_datetime(purchase_history[i]['date']) - pd.to_datetime(prev_same[-1]['date']) ).days if item not in item_intervals: item_intervals[item] = [] item_intervals[item].append(days_between) # Predict repeat purchases predictions = [] last_purchase_date = pd.to_datetime(purchase_history[-1]['date']) today = pd.Timestamp.now() days_since_last = (today - last_purchase_date).days for item_id, intervals in item_intervals.items(): avg_interval = np.mean(intervals) std_interval = np.std(intervals) if len(intervals) > 1 else avg_interval * 0.3 # Probability via normal distribution from scipy.stats import norm prob = norm.cdf(days_since_last + 7, avg_interval, std_interval + 1) prob -= norm.cdf(days_since_last - 7, avg_interval, std_interval + 1) prob = min(max(prob, 0), 1) if prob > 0.1: predictions.append((item_id, prob)) return sorted(predictions, key=lambda x: x[1], reverse=True)[:10] 
More on Apriori Tuning

Association rules with min_support=0.01, min_confidence=0.1 typically yield 500-5000 significant rules for 100K orders. Lift > 2.0 indicates a strong association. For hyperparameter tuning, we use cross-validation: split data by time (train/test) and select min_support, min_confidence that maximize lift on the validation set.

How to Train the CrossSellRecommender on Your Data

  1. Data preparation. Collect order history with columns: order_id, item_id, date. Minimum number of transactions — 10,000.
  2. Run Apriori. Use the fit_association_rules method with min_support=0.01 and min_confidence=0.1. This will give about 1000 rules.
  3. Tune parameters. Check the distribution of lift and confidence. If there are too many rules, increase min_support to 0.02 or min_confidence to 0.2.
  4. Evaluate quality. Use precision@k and recall@k metrics on a held-out set. Rule-based cross-sell gives an average basket uplift of 15-25%. Combining with personalization (user history) adds another 5-10% to acceptance rate.

How to Measure Recommendation Quality?

Main offline metrics: precision@k, recall@k, lift@k. We use a truncated basket — hide part of items from each transaction and check if they appear in recommendations. Online metrics: acceptance rate, AOV uplift, share of carts with cross-sell. On an A/B test with 50% audience, typical acceptance rate uplift is 10-25%.

Why is the Hybrid Model More Effective Than Rule-Based?

Rule-based (e.g., "bought A → show B") does not scale to thousands of items: rules become sparse and don't account for context. ML finds high-lift associations even for rare pairs. In addition, the hybrid personalizes ranking: the same item is ranked differently for different users. In our projects, the acceptance rate of the hybrid is 3 times higher than that of rules.

Implementation Stages

Stage What We Do Result Duration
1. Data audit Check completeness and quality: number of SKUs, history length, order frequency Metrics dashboard, list of gaps 1-2 days
2. Pipeline building ETL to collect data from CRM, cleaning, basket aggregation orders_df dataset 3-4 days
3. Rule training Apriori, tuning min_support and min_confidence via cross-validation Rules file (.pkl) 1-2 days
4. Personalization Train gradient boosting on user history Ranking model 3-5 days
5. A/B test 50% audience — old system, 50% — new. Measure conversion and AOV Report with uplift 7-14 days
6. Production deployment Integration via REST API, monitoring p99 latency Documentation, dashboard 2-3 days

What's Included in the Result

  • Documentation: description of rules, metrics, instructions for model updates.
  • Source code: repository with modules CrossSellRecommender, NextPurchasePredictor, configs and tests.
  • API endpoints: two methods — recommend_by_cart (based on current cart) and recommend_by_user (based on history).
  • Team training: we show how to interpret results and retrain the model.
  • Guarantee: we support the A/B test until statistically significant uplift is achieved.

How to Estimate ROI for Your Business

Metric Typical Value How We Calculate
Average order value increase 15-25% (AOV_test - AOV_control) / AOV_control * 100%
Acceptance rate 5-12% Clicks on recommendations / impressions
Share of carts with cross-sell 30-50% Carts with ≥1 recommended item / total carts
Payback time 2-4 months Implementation cost / monthly revenue increase

To assess the cross-sell potential for your assortment, contact us. We will conduct a data audit and provide a prototype in 2 weeks with a confidentiality guarantee. Request a consultation — we'll discuss the details of your project and prepare a tailored proposal.