Spaces:
Sleeping
Sleeping
| from tqdm import tqdm | |
| from transliterate import translit, detect_language | |
| import pandas as pd | |
| from rapidfuzz import fuzz, process | |
| def normalize_name(name): | |
| """ | |
| Нормализует строку: если обнаруживается русский язык, транслитерирует её в латиницу, | |
| приводит к нижнему регистру. | |
| """ | |
| try: | |
| if detect_language(name) == 'ru': | |
| return translit(name, 'ru', reversed=True).lower() | |
| except Exception: | |
| pass | |
| return name.lower() | |
| def prepare_groups_with_ids(items_df): | |
| """ | |
| Предварительная группировка данных из items по (new_brand, type, volume, new_type_wine, sour) | |
| с учетом нормализованного названия. | |
| Добавляем столбец 'norm_name', чтобы нормализовать значение name один раз заранее. | |
| :param items_df: DataFrame с колонками 'new_brand', 'type', 'name', 'id', 'volume', 'new_type_wine', 'sour'. | |
| :return: Словарь {(new_brand, type, volume, new_type_wine, sour): [(id, name, norm_name, volume, new_type_wine, sour)]}. | |
| """ | |
| items_df = items_df.copy() | |
| items_df['norm_name'] = items_df['name'].apply(normalize_name) | |
| grouped = items_df.groupby(['new_brand', 'type', 'volume', 'new_type_wine', 'sour']).apply( | |
| lambda x: list(zip(x['id'], x['name'], x['norm_name'], x['volume'], x['new_type_wine'], x['sour'], x['year'])) | |
| ).to_dict() | |
| return grouped | |
| def prepare_groups_by_alternative_keys(items_df): | |
| """ | |
| Группировка данных из items по (new_type_wine, new_type, volume, sour) с сохранением id, new_brand, | |
| оригинального и нормализованного имени. | |
| :param items_df: DataFrame с колонками 'new_brand', 'new_type_wine', 'new_type', 'volume', 'name', 'id', 'sour'. | |
| :return: Словарь {(new_type_wine, new_type, volume, sour): [(id, new_brand, name, norm_name, volume, new_type_wine, sour)]}. | |
| """ | |
| items_df = items_df.copy() | |
| items_df['norm_name'] = items_df['name'].apply(normalize_name) | |
| grouped = items_df.groupby(['new_type_wine', 'new_type', 'volume', 'sour']).apply( | |
| lambda x: list(zip(x['id'], x['new_brand'], x['name'], x['norm_name'], x['volume'], x['new_type_wine'], x['sour'], x['year'])) | |
| ).to_dict() | |
| return grouped | |
| def new_find_matches_with_ids(products_df, items_groups, items_df, name_threshold=85): | |
| """ | |
| Поиск совпадений с сохранением id найденных итемов, используя заранее подготовленные | |
| нормализованные группы. | |
| Производится два прохода: | |
| - Первый: поиск по группам (brand, type, volume, new_type_wine, sour); | |
| - Второй: для продуктов без совпадения ищем по альтернативным группам (new_type_wine, new_type, volume, sour), | |
| исключая итемы с исходным брендом. | |
| Сравнение производится по столбцу norm_name, а для вывода используется оригинальное name. | |
| :param products_df: DataFrame с колонками 'id', 'brand', 'type', 'name', 'volume', 'new_type_wine', 'sour', 'new_type'. | |
| :param items_groups: Словарь, сформированный функцией prepare_groups_with_ids. | |
| :param items_df: DataFrame итемов с колонками 'id', 'new_brand', 'new_type_wine', 'new_type', 'volume', 'name', 'sour'. | |
| :param name_threshold: Порог сходства для fuzzy matching. | |
| :return: DataFrame с добавленными столбцами 'matched_items' (список совпадений) и 'alternative' (альтернативные совпадения). | |
| """ | |
| results = [] | |
| no_match_products = [] # Список для хранения продуктов без совпадения в исходной группе | |
| # Первый проход: поиск по группам (brand, type, volume, new_type_wine, sour) | |
| for idx, product in tqdm(products_df.iterrows(), total=len(products_df)): | |
| product_brand = product['brand'] | |
| product_type = product['type'] | |
| product_name = product['name'] | |
| product_volume = product['volume'] | |
| product_type_wine = product['new_type_wine'] | |
| product_sour = product['sour'] | |
| key = (product_brand, product_type, product_volume, product_type_wine, product_sour) | |
| items_data = items_groups.get(key, []) | |
| if items_data: | |
| # Распаковываем: id, оригинальное имя, нормализованное имя, volume, new_type_wine, sour | |
| items_ids, items_names, items_norm_names, items_volumes, item_type_wine, items_sour, items_year = zip(*items_data) | |
| else: | |
| items_ids, items_names, items_norm_names, items_volumes, item_type_wine, items_sour, items_year = ([], [], [], [], [], [],[]) | |
| norm_product_name = normalize_name(product_name) | |
| matches = process.extract( | |
| norm_product_name, list(items_norm_names), scorer=fuzz.ratio, score_cutoff=name_threshold | |
| ) | |
| matched_items = [ | |
| { | |
| 'item_id': items_ids[idx_candidate], | |
| 'item_name': items_names[idx_candidate], | |
| 'score': score, | |
| 'volume': items_volumes[idx_candidate], | |
| 'color': item_type_wine[idx_candidate], | |
| 'sour': items_sour[idx_candidate], | |
| 'year': items_year[idx_candidate], | |
| } | |
| for match, score, idx_candidate in matches | |
| ] | |
| if not matched_items: | |
| no_match_products.append((idx, product)) | |
| results.append({ | |
| 'product_id': product['id'], | |
| 'matched_items': matched_items, | |
| 'alternative': [] # Заполняется во втором проходе | |
| }) | |
| # Подготовка альтернативной группировки по (new_type_wine, new_type, volume, sour) | |
| groups_by_alternative_keys = prepare_groups_by_alternative_keys(items_df) | |
| # Второй проход: для продуктов без совпадений ищем по альтернативным группам | |
| for idx, product in tqdm(no_match_products): | |
| product_brand = product['brand'] | |
| product_type_wine = product['new_type_wine'] | |
| product_type = product['new_type'] | |
| product_volume = product['volume'] | |
| product_name = product['name'] | |
| product_sour = product['sour'] | |
| alt_key = (product_type_wine, product_type, product_volume, product_sour) | |
| type_items = groups_by_alternative_keys.get(alt_key, []) | |
| # Фильтруем, исключая итемы с исходным брендом | |
| filtered_items = [item for item in type_items if item[1] != product_brand] | |
| if filtered_items: | |
| alt_ids, alt_brands, alt_names, alt_norm_names, alt_volumes, alt_type_wine, alt_sour, alt_year = zip(*filtered_items) | |
| else: | |
| alt_ids, alt_brands, alt_names, alt_norm_names, alt_volumes, alt_type_wine, alt_sour, alt_year = ([], [], [], [], [], [], [],[]) | |
| norm_product_name = normalize_name(product_name) | |
| alt_matches = process.extract( | |
| norm_product_name, list(alt_norm_names), scorer=fuzz.ratio, score_cutoff=name_threshold | |
| ) | |
| alt_matched_items = [ | |
| { | |
| 'item_id': alt_ids[idx_candidate], | |
| 'item_name': alt_names[idx_candidate], | |
| 'score': score, | |
| 'volume': alt_volumes[idx_candidate], | |
| 'color': alt_type_wine[idx_candidate], | |
| 'sour': alt_sour[idx_candidate], | |
| 'year': alt_year[idx_candidate], | |
| } | |
| for match, score, idx_candidate in alt_matches | |
| ] | |
| results[idx]['alternative'] = alt_matched_items | |
| results_df = pd.DataFrame(results) | |
| merged_df = products_df.merge(results_df, left_on='id', right_on='product_id').drop(columns=['product_id']) | |
| return merged_df |