AI-система рекомендации размеров одежды и обуви
Возврат из-за несоответствия размера — крупнейшая статья потерь в fashion e-commerce: 30-40% возвратов связаны именно с fit-проблемами. AI-система рекомендации размеров снижает этот показатель на 20-35%, повышая конверсию за счёт уверенности покупателя в выборе.
Архитектура size recommendation
Задача состоит из двух компонентов: нормализация размерных сеток брендов и персонализация на основе истории покупок. Разные бренды используют разные стандарты (EU, UK, US, IT), плюс внутри одного бренда размеры варьируются по категориям.
import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.preprocessing import LabelEncoder
class SizeNormalizer:
"""Нормализация размерных сеток к единому стандарту"""
SIZE_CHARTS = {
'EU': {'36': 'XS', '38': 'S', '40': 'M', '42': 'L', '44': 'XL', '46': 'XXL'},
'UK': {'8': 'XS', '10': 'S', '12': 'M', '14': 'L', '16': 'XL', '18': 'XXL'},
'US': {'0': 'XS', '2': 'S', '4': 'M', '6': 'L', '8': 'XL', '10': 'XXL'},
}
def normalize_to_standard(self, size: str, brand: str,
category: str, system: str = 'EU') -> dict:
"""Конвертация к стандартному размеру с диапазоном измерений (см)"""
# Стандартные измерения для женских топов
measurements = {
'XS': {'chest': (80, 84), 'waist': (60, 64), 'hips': (86, 90)},
'S': {'chest': (84, 88), 'waist': (64, 68), 'hips': (90, 94)},
'M': {'chest': (88, 92), 'waist': (68, 72), 'hips': (94, 98)},
'L': {'chest': (92, 96), 'waist': (72, 76), 'hips': (98, 102)},
'XL': {'chest': (96, 100), 'waist': (76, 80), 'hips': (102, 106)},
}
chart = self.SIZE_CHARTS.get(system, {})
standard = chart.get(str(size), size)
# Бренд-специфичная поправка из исторических данных возвратов
brand_offset = self._get_brand_offset(brand, category)
return {
'original_size': size,
'standard_label': standard,
'measurements_cm': measurements.get(standard, {}),
'brand_offset': brand_offset,
'adjusted_label': self._apply_offset(standard, brand_offset)
}
def _get_brand_offset(self, brand: str, category: str) -> int:
"""
Поправка из анализа возвратов: +1 = бренд маломерит (рекомендовать на размер больше),
-1 = бренд большемерит
"""
# Загружается из таблицы, обученной на возвратах
brand_offsets = {
'zara': {'tops': 1, 'pants': 0, 'dresses': 1},
'h&m': {'tops': 0, 'pants': 1, 'dresses': 0},
'mango': {'tops': 0, 'pants': 0, 'dresses': -1},
}
return brand_offsets.get(brand, {}).get(category, 0)
def _apply_offset(self, size: str, offset: int) -> str:
order = ['XS', 'S', 'M', 'L', 'XL', 'XXL']
if size not in order:
return size
idx = max(0, min(len(order) - 1, order.index(size) + offset))
return order[idx]
class PersonalizedSizeRecommender:
"""Персонализация на основе истории покупок и возвратов"""
def __init__(self):
self.model = GradientBoostingClassifier(
n_estimators=150, learning_rate=0.05, max_depth=4, random_state=42
)
self.label_encoder = LabelEncoder()
def build_user_profile(self, purchase_history: pd.DataFrame,
user_id: str) -> dict:
"""Профиль пользователя из истории покупок"""
user_purchases = purchase_history[
(purchase_history['user_id'] == user_id) &
(purchase_history['returned'] == False)
]
if user_purchases.empty:
return {}
# Какие размеры оставил (не вернул) по категориям
kept_sizes = user_purchases.groupby(['category', 'brand'])['size_eu'].agg(
lambda x: x.mode().iloc[0] if len(x) > 0 else None
).to_dict()
# Количество возвратов по размерным причинам
all_purchases = purchase_history[purchase_history['user_id'] == user_id]
size_returns = all_purchases[
all_purchases['return_reason'].isin(['too_small', 'too_large'])
]
return_pattern = 'neutral'
if len(size_returns) > 0:
too_small = (size_returns['return_reason'] == 'too_small').sum()
too_large = (size_returns['return_reason'] == 'too_large').sum()
if too_small > too_large * 1.5:
return_pattern = 'tends_small' # Обычно берёт маленький размер
elif too_large > too_small * 1.5:
return_pattern = 'tends_large'
return {
'user_id': user_id,
'kept_sizes': kept_sizes,
'return_pattern': return_pattern,
'total_purchases': len(user_purchases),
'return_rate': len(size_returns) / max(len(all_purchases), 1)
}
def recommend_size(self, user_profile: dict, product: dict,
normalizer: SizeNormalizer) -> dict:
"""Рекомендация размера с объяснением"""
category = product.get('category', 'tops')
brand = product.get('brand', '')
# Базовый размер из профиля
kept_sizes = user_profile.get('kept_sizes', {})
# Ищем: точное совпадение бренд+категория → только категория → любой
base_size = (
kept_sizes.get((category, brand)) or
next((v for (cat, _), v in kept_sizes.items() if cat == category), None) or
next(iter(kept_sizes.values()), None)
)
if not base_size:
return {'recommended_size': None, 'confidence': 0.0,
'reason': 'Недостаточно данных о покупателе'}
# Нормализация + бренд-поправка
normalized = normalizer.normalize_to_standard(base_size, brand, category)
recommended = normalized['adjusted_label']
# Поправка на паттерн возвратов
return_pattern = user_profile.get('return_pattern', 'neutral')
if return_pattern == 'tends_small':
recommended = normalizer._apply_offset(recommended, 1)
elif return_pattern == 'tends_large':
recommended = normalizer._apply_offset(recommended, -1)
# Уверенность: больше покупок → выше уверенность
purchases_count = user_profile.get('total_purchases', 0)
confidence = min(0.95, 0.5 + purchases_count * 0.05)
# Причина для UI
reasons = []
if normalized['brand_offset'] != 0:
direction = 'маломерит' if normalized['brand_offset'] > 0 else 'большемерит'
reasons.append(f'{brand} {direction} в категории {category}')
if return_pattern != 'neutral':
reasons.append(f'На основе ваших предыдущих возвратов')
return {
'recommended_size': recommended,
'size_range': normalized.get('measurements_cm', {}),
'confidence': round(confidence, 2),
'brand_adjusted': normalized['brand_offset'] != 0,
'reason': '; '.join(reasons) if reasons else 'На основе вашей истории покупок',
'also_consider': normalizer._apply_offset(recommended, 1) # Соседний размер
}
Интеграция с продуктовой карточкой
На карточке товара система отображает персонализированную рекомендацию с уровнем уверенности. При отсутствии истории покупок — fallback на общую статистику по размерам для данного бренда.
class SizeRecommendationService:
def get_recommendation(self, user_id: str, product_id: str,
db) -> dict:
product = db.get_product(product_id)
purchase_history = db.get_user_purchases(user_id, limit=50)
normalizer = SizeNormalizer()
recommender = PersonalizedSizeRecommender()
user_profile = recommender.build_user_profile(purchase_history, user_id)
if not user_profile:
# Cold-start: статистика по бренду
popular_sizes = db.get_brand_size_distribution(
product['brand'], product['category']
)
return {
'source': 'brand_statistics',
'popular_size': popular_sizes.get('mode'),
'distribution': popular_sizes.get('distribution'),
'confidence': 0.4
}
recommendation = recommender.recommend_size(user_profile, product, normalizer)
recommendation['source'] = 'personalized'
return recommendation
Метрики системы
| Метрика | До системы | После системы |
|---|---|---|
| Возвраты по размеру | 28% | 18% |
| Конверсия на карточке | 3.2% | 4.1% |
| Confidence > 0.7 у % пользователей | — | 65% |
| Coverage (есть история) | — | 72% пользователей |
Система обучается постоянно: каждый возврат с причиной «не подошёл размер» уточняет бренд-поправку. Минимальная история для персонализации: 3 завершённые покупки без возврата. При горизонте 6 месяцев эксплуатации coverage достигает 80%+ активной базы.







