Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files
preprocess/utils/common/brand_matching.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from tqdm import tqdm
|
| 2 |
+
import re
|
| 3 |
+
from ahocorasick import Automaton
|
| 4 |
+
from rapidfuzz import fuzz, process
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def contains_full_word(word, text, case_sensitive=True):
|
| 8 |
+
"""
|
| 9 |
+
Проверяет, содержится ли слово word в строке text как отдельное слово.
|
| 10 |
+
Параметр case_sensitive задаёт, учитывать ли регистр.
|
| 11 |
+
"""
|
| 12 |
+
flags = 0 if case_sensitive else re.IGNORECASE
|
| 13 |
+
pattern = r'\b' + re.escape(word) + r'\b'
|
| 14 |
+
return re.search(pattern, text, flags) is not None
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def unwrap_brands(products):
|
| 18 |
+
res={}
|
| 19 |
+
#brands=items['brand'].unique()
|
| 20 |
+
new_brands=sorted([x for x in products['brand'].unique() if isinstance(x, str)], key=len)
|
| 21 |
+
#items['new_brand'].unique() if isinstance(x, str)], key=len)
|
| 22 |
+
|
| 23 |
+
for i in tqdm(new_brands):
|
| 24 |
+
for j in new_brands:
|
| 25 |
+
if contains_full_word(i, j, case_sensitive=False):
|
| 26 |
+
if i != j:
|
| 27 |
+
#if len(i)>1:#i != 'А' and i != "Я":
|
| 28 |
+
res[j]=i
|
| 29 |
+
return res
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def split_n_match(products, items, th_len=3):
|
| 34 |
+
result={}
|
| 35 |
+
conditionally_spited=[]
|
| 36 |
+
for i in tqdm(items['brand'].unique()):
|
| 37 |
+
if '/' in i:
|
| 38 |
+
conditionally_spited.append(i)
|
| 39 |
+
for i in tqdm(products['brand'].unique()):
|
| 40 |
+
for j in conditionally_spited:
|
| 41 |
+
if len(i)>th_len and contains_full_word(i,j):
|
| 42 |
+
result[j]=i
|
| 43 |
+
return result
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def fill_brands_in_dataframe(brands, df, col_name='new_brand', is_brand=True):
|
| 48 |
+
"""
|
| 49 |
+
Заполняет колонку 'brand' в DataFrame найденными брендами.
|
| 50 |
+
|
| 51 |
+
:param brands: Список брендов.
|
| 52 |
+
:param df: DataFrame с колонками ['id', 'brand', 'name', ...].
|
| 53 |
+
:return: DataFrame с обновлённой колонкой 'brand'.
|
| 54 |
+
"""
|
| 55 |
+
# Инициализируем автомат для быстрого поиска брендов
|
| 56 |
+
automaton = Automaton()
|
| 57 |
+
|
| 58 |
+
# Добавляем бренды в автомат
|
| 59 |
+
for idx, brand in enumerate(brands):
|
| 60 |
+
if isinstance(brand, str) and brand:
|
| 61 |
+
automaton.add_word(brand.lower(), (idx, brand))
|
| 62 |
+
|
| 63 |
+
automaton.make_automaton()
|
| 64 |
+
|
| 65 |
+
def find_brand(name):
|
| 66 |
+
"""
|
| 67 |
+
Находит лучший бренд для данного имени.
|
| 68 |
+
"""
|
| 69 |
+
matched_brands = set()
|
| 70 |
+
for _, (_, brand) in automaton.iter(name.lower()):
|
| 71 |
+
# Проверяем, что бренд встречается как отдельное слово
|
| 72 |
+
if re.search(rf'\b{re.escape(brand.lower())}\b', name.lower()):
|
| 73 |
+
matched_brands.add(brand)
|
| 74 |
+
|
| 75 |
+
# Возвращаем бренд с максимальной длиной (более точное совпадение)
|
| 76 |
+
return max(matched_brands, key=len) if matched_brands else None
|
| 77 |
+
|
| 78 |
+
# Обновляем колонку brand только для пустых значений
|
| 79 |
+
# df['new_brand'] = df.apply(
|
| 80 |
+
# lambda row: find_brand(row['name']), #if pd.isna(row['brand']) else row['brand'],
|
| 81 |
+
# axis=1
|
| 82 |
+
# )
|
| 83 |
+
if is_brand==True:
|
| 84 |
+
df[col_name] = df.apply(lambda row: find_brand(row['name']) or row['brand'], axis=1)
|
| 85 |
+
else:
|
| 86 |
+
df[col_name] = df.apply(lambda row: find_brand(row['name']) or None, axis=1)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def get_same_brands(products, items):
|
| 91 |
+
comp_list=[]
|
| 92 |
+
#not_comp_prods=[]
|
| 93 |
+
#not_comp_items=[]
|
| 94 |
+
prod_brand_list=list(products['brand'].unique())
|
| 95 |
+
items_brand_list=list(items['new_brand'].unique())
|
| 96 |
+
for i in tqdm(prod_brand_list):
|
| 97 |
+
if i in items_brand_list:
|
| 98 |
+
comp_list.append(i)
|
| 99 |
+
|
| 100 |
+
return comp_list, prod_brand_list, items_brand_list
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def match_brands_improved(items_brands, prods_brands, threshold=85):
|
| 104 |
+
"""
|
| 105 |
+
Улучшенный алгоритм сопоставления брендов с учётом нечёткого поиска и фильтрации ошибок.
|
| 106 |
+
|
| 107 |
+
:param items_brands: Список брендов из датафрейма items.
|
| 108 |
+
:param prods_brands: Список брендов из датафрейма prods.
|
| 109 |
+
:param threshold: Порог сходства для нечёткого поиска.
|
| 110 |
+
:return: Словарь соответствий {бренд из items: ближайший бренд из prods}.
|
| 111 |
+
"""
|
| 112 |
+
brand_mapping = {}
|
| 113 |
+
|
| 114 |
+
for item_brand in tqdm(items_brands):
|
| 115 |
+
if isinstance(item_brand, str):
|
| 116 |
+
# Разделяем бренд на части
|
| 117 |
+
parts = [part.strip() for part in re.split(r"[\/\(\)]", item_brand) if part.strip()]
|
| 118 |
+
best_match = None
|
| 119 |
+
best_score = 0
|
| 120 |
+
|
| 121 |
+
for part in parts:
|
| 122 |
+
match, score, _ = process.extractOne(part, prods_brands, scorer=fuzz.ratio)
|
| 123 |
+
# Фильтрация по длине строк и порогу
|
| 124 |
+
if score >= threshold and abs(len(part) - len(match)) / len(part) <= 0.3:
|
| 125 |
+
if score > best_score:
|
| 126 |
+
best_match = match
|
| 127 |
+
best_score = score
|
| 128 |
+
|
| 129 |
+
# Сохранение результата
|
| 130 |
+
if best_match:
|
| 131 |
+
brand_mapping[item_brand] = best_match#, best_score)
|
| 132 |
+
|
| 133 |
+
return brand_mapping
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
|
preprocess/utils/common/extracters.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def extract_years(text):
|
| 5 |
+
"""
|
| 6 |
+
Извлекает сочетание числа и слова, указывающего возраст (например: '50 лет', '21 years').
|
| 7 |
+
"""
|
| 8 |
+
# Регулярное выражение ищет числа и слова 'лет' или 'years' с учетом регистра
|
| 9 |
+
match = re.search(r'\b(?<!\d)(\d{1,2})\s*(лет|years)\b', text, re.IGNORECASE)
|
| 10 |
+
if match:
|
| 11 |
+
# Приводим слово 'лет' или 'years' к исходному регистру
|
| 12 |
+
return f"{match.group(1)} {match.group(2)}"
|
| 13 |
+
return None
|
| 14 |
+
|
| 15 |
+
def extract_production_year(text):
|
| 16 |
+
"""
|
| 17 |
+
Извлекает год производства (четырехзначное число в диапазоне 1900–2099) из строки.
|
| 18 |
+
Например: '2019'.
|
| 19 |
+
"""
|
| 20 |
+
match = re.search(r'\b(19\d{2}|20\d{2})\b', text)
|
| 21 |
+
if match:
|
| 22 |
+
return match.group(1)
|
| 23 |
+
return None
|
| 24 |
+
|
| 25 |
+
def extract_alcohol_content(text):
|
| 26 |
+
"""
|
| 27 |
+
Извлекает содержание алкоголя из строки.
|
| 28 |
+
Например: '40%'.
|
| 29 |
+
"""
|
| 30 |
+
match = re.search(r'(\d{1,2}(?:[.,]\d+)?\s*%)', text)
|
| 31 |
+
if match:
|
| 32 |
+
# Заменяем запятую на точку для единообразия (если нужно)
|
| 33 |
+
return match.group(1).replace(' ', '').replace(',', '.')
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def is_volume(value):
|
| 38 |
+
"""
|
| 39 |
+
Проверяет, является ли значение валидным объемом (<= 10 литров).
|
| 40 |
+
"""
|
| 41 |
+
try:
|
| 42 |
+
volume = float(value)
|
| 43 |
+
return volume if volume <= 10 else None
|
| 44 |
+
except ValueError:
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
def extract_volume_or_number(text):
|
| 48 |
+
"""
|
| 49 |
+
Извлекает объем в литрах или число с плавающей точкой из строки.
|
| 50 |
+
Например: '0,75л', '0.5', или '1,5 л'.
|
| 51 |
+
"""
|
| 52 |
+
# Попытка найти объем с буквой 'л' или без пробела перед ней
|
| 53 |
+
match_with_l = re.search(r'(\d+(?:[\.,]\d+)?\s*[лЛ]|(?:\d+(?:[\.,]\d+)?[лЛ]))', text)
|
| 54 |
+
if match_with_l:
|
| 55 |
+
return is_volume(match_with_l.group(1).replace(',', '.').replace('л', '').replace('Л', '').strip())
|
| 56 |
+
|
| 57 |
+
# Если не найдено, ищем просто число с плавающей точкой
|
| 58 |
+
match_number = re.search(r'(?<!№)\b(\d{1,2}(?:[\.,]\d+))\b(?!\s*(№|-er|er|\d{3,}))', text)
|
| 59 |
+
if match_number:
|
| 60 |
+
return is_volume(match_number.group(1).replace(',', '.'))
|
| 61 |
+
|
| 62 |
+
return None
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
|
preprocess/utils/common/parallel_brand_mutching.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from ahocorasick import Automaton
|
| 3 |
+
from rapidfuzz import fuzz, process
|
| 4 |
+
from unidecode import unidecode
|
| 5 |
+
from pqdm.threads import pqdm
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def normalize(text):
|
| 9 |
+
"""
|
| 10 |
+
Приводит текст к нижнему регистру и транслитерирует его в латиницу.
|
| 11 |
+
"""
|
| 12 |
+
return unidecode(text.lower())
|
| 13 |
+
|
| 14 |
+
def build_regex_for_brands(brands):
|
| 15 |
+
"""
|
| 16 |
+
Нормализует бренды и создаёт одно регулярное выражение для точного поиска.
|
| 17 |
+
Возвращает скомпилированный паттерн и словарь: нормализованное название -> оригинальное название.
|
| 18 |
+
"""
|
| 19 |
+
norm_to_brand = {}
|
| 20 |
+
for brand in brands:
|
| 21 |
+
norm_brand = normalize(brand)
|
| 22 |
+
if norm_brand not in norm_to_brand:
|
| 23 |
+
norm_to_brand[norm_brand] = brand
|
| 24 |
+
pattern = re.compile(r'\b(?:' + '|'.join(re.escape(nb) for nb in norm_to_brand.keys()) + r')\b')
|
| 25 |
+
return pattern, norm_to_brand
|
| 26 |
+
|
| 27 |
+
def process_string(s, regex_pattern, norm_to_brand, norm_brand_list, index_to_brand, threshold):
|
| 28 |
+
"""
|
| 29 |
+
Обрабатывает одну строку:
|
| 30 |
+
1. Пытается найти бренд через регулярное выражение.
|
| 31 |
+
2. Если точного совпадения нет – разбивает строку и выполняет нечёткий поиск.
|
| 32 |
+
Возвращает кортеж: (исходная строка, найденный бренд или None).
|
| 33 |
+
"""
|
| 34 |
+
norm_s = normalize(s)
|
| 35 |
+
# Пытаемся найти бренд через регулярное выражение
|
| 36 |
+
match = regex_pattern.search(norm_s)
|
| 37 |
+
if match:
|
| 38 |
+
return s, norm_to_brand[match.group(0)]
|
| 39 |
+
|
| 40 |
+
# Если точного совпадения нет, разбиваем строку по разделителям и анализируем части
|
| 41 |
+
parts = [part.strip() for part in re.split(r"[\/\(\)]", s) if part.strip()]
|
| 42 |
+
parts.append(s) # анализ всей строки
|
| 43 |
+
best_match = None
|
| 44 |
+
best_score = 0
|
| 45 |
+
for part in parts:
|
| 46 |
+
norm_part = normalize(part)
|
| 47 |
+
res = process.extractOne(norm_part, norm_brand_list, scorer=fuzz.ratio, score_cutoff=threshold)
|
| 48 |
+
if res is not None:
|
| 49 |
+
match_norm, score, idx = res
|
| 50 |
+
if score > best_score:
|
| 51 |
+
best_match = index_to_brand[idx]
|
| 52 |
+
best_score = score
|
| 53 |
+
if best_score == 100:
|
| 54 |
+
break
|
| 55 |
+
if best_match:
|
| 56 |
+
return s, best_match
|
| 57 |
+
return s, None
|
| 58 |
+
|
| 59 |
+
def check_brands_in_strings_pqdm(strings, brands, threshold=85, n_jobs=8):
|
| 60 |
+
"""
|
| 61 |
+
Поиск брендов в строках с учетом вариантов написания и транслитерации.
|
| 62 |
+
Использует предварительный поиск через регулярное выражение и, при необходимости,
|
| 63 |
+
нечёткий поиск. Обработка выполняется параллельно с отображением прогресса с помощью pqdm.
|
| 64 |
+
|
| 65 |
+
:param strings: Список строк для поиска брендов.
|
| 66 |
+
:param brands: Список брендов для поиска.
|
| 67 |
+
:param threshold: Порог сходства для нечёткого поиска.
|
| 68 |
+
:param n_jobs: Число рабочих потоков (или процессов, если использовать pqdm.processes).
|
| 69 |
+
:return: Словарь вида {строка: найденный бренд}.
|
| 70 |
+
"""
|
| 71 |
+
# Подготавливаем список нормализованных брендов и сопоставление индексов с оригинальными брендами.
|
| 72 |
+
norm_brand_list = []
|
| 73 |
+
index_to_brand = []
|
| 74 |
+
for brand in brands:
|
| 75 |
+
norm_brand = normalize(brand)
|
| 76 |
+
norm_brand_list.append(norm_brand)
|
| 77 |
+
index_to_brand.append(brand)
|
| 78 |
+
|
| 79 |
+
# Создаем комбинированный паттерн для точного поиска.
|
| 80 |
+
regex_pattern, norm_to_brand = build_regex_for_brands(brands)
|
| 81 |
+
|
| 82 |
+
# Определяем вспомогательную функцию, закрывающую необходимые параметры.
|
| 83 |
+
def process_string_wrapper(s):
|
| 84 |
+
return process_string(s, regex_pattern, norm_to_brand, norm_brand_list, index_to_brand, threshold)
|
| 85 |
+
|
| 86 |
+
# Обрабатываем строки параллельно с отображением прогресса.
|
| 87 |
+
results = pqdm(strings, process_string_wrapper, n_jobs=n_jobs)
|
| 88 |
+
|
| 89 |
+
brand_mapping = {}
|
| 90 |
+
for s, matched_brand in results:
|
| 91 |
+
if matched_brand:
|
| 92 |
+
brand_mapping[s] = matched_brand
|
| 93 |
+
return brand_mapping
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
|
preprocess/utils/common/top_inserts.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from preprocess.utils.common.extracters import *
|
| 2 |
+
from preprocess.utils.common.utils import *
|
| 3 |
+
from preprocess.utils.common.parallel_brand_matching import *
|
| 4 |
+
from tqdm import tqdm
|
| 5 |
+
import re
|
| 6 |
+
import math
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def top_inserts_matching(other_brands, p_brands, items, th=65):
|
| 12 |
+
replaced={}
|
| 13 |
+
for i in other_brands:
|
| 14 |
+
l=i.split('/')
|
| 15 |
+
if len(l)>2:
|
| 16 |
+
replaced[l[0].replace('Шато','')]=i
|
| 17 |
+
else:
|
| 18 |
+
if 'Шато' in i:
|
| 19 |
+
replaced[i.replace('Шато','')]=i
|
| 20 |
+
|
| 21 |
+
ob=[i.split('/')[0].replace('Шато','') for i in other_brands]
|
| 22 |
+
ob_in_pb=check_brands_in_strings_pqdm(ob, p_brands, threshold=th)
|
| 23 |
+
|
| 24 |
+
result={}
|
| 25 |
+
for k in ob_in_pb.keys():
|
| 26 |
+
if k in replaced.keys():
|
| 27 |
+
result[replaced[k]]=ob_in_pb[k]
|
| 28 |
+
else:
|
| 29 |
+
result[k]=ob_in_pb[k]
|
| 30 |
+
|
| 31 |
+
items.loc[items['new_name'].isin(result.keys()), 'new_brand'] = items['new_name'].map(result)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def process_unbrended_names(items, p_brands, process_text, types, grape_varieties, onther_words):
|
| 35 |
+
result={}
|
| 36 |
+
for n in tqdm(items[items['new_brand'].isna()]['name'].values):
|
| 37 |
+
|
| 38 |
+
name, alcohol, volume_or_number, years, production_year, gb, color, sour=process_text(n)
|
| 39 |
+
#name, alcohol, volume_or_number, years, production_year, gb, color, sour=prcess_text('Вино Токай Фурминт п/сл. бел.0.75л')
|
| 40 |
+
name=trim_name(name, types)
|
| 41 |
+
name=trim_name(name, grape_varieties)
|
| 42 |
+
name=trim_name(name, onther_words)
|
| 43 |
+
name=name.replace('.','').replace(',','').replace('(','').replace(')','')
|
| 44 |
+
#result.append(clean_wine_name(name).strip())
|
| 45 |
+
result[n]=clean_wine_name(name).strip()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
items['new_name']=None
|
| 50 |
+
items.loc[items['name'].isin(result.keys()), 'new_name'] = items['name'].map(result)
|
| 51 |
+
|
| 52 |
+
u_nn=list(items[~items['new_name'].isna()]['new_name'].unique())
|
| 53 |
+
res={}
|
| 54 |
+
for i in tqdm(u_nn):
|
| 55 |
+
lenta=len(items[items['new_name']==i])
|
| 56 |
+
if lenta>1:
|
| 57 |
+
res[i]=lenta
|
| 58 |
+
|
| 59 |
+
th=math.sqrt(((np.array(list(res.values())).mean()+np.array(list(res.values())).std())**2)//2)
|
| 60 |
+
other_brands=[i for i,j in res.items() if j>th]
|
| 61 |
+
|
| 62 |
+
reess=check_brands_in_strings_pqdm(other_brands, p_brands)
|
| 63 |
+
|
| 64 |
+
items.loc[items['new_name'].isin(reess.keys()), 'new_brand'] = items['new_name'].map(reess)
|
| 65 |
+
|
| 66 |
+
top_inserts_matching(other_brands, p_brands, items)
|
preprocess/utils/common/utils.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from tqdm import tqdm
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def remove_quotes(text):
|
| 6 |
+
return re.sub(r'["\']', '', text)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def remove_l(text):
|
| 10 |
+
result = re.sub(r'\bл\b', '', text, flags=re.IGNORECASE)
|
| 11 |
+
|
| 12 |
+
# Убираем возможные лишние пробелы, возникающие после удаления
|
| 13 |
+
result = re.sub(r'\s{2,}', ' ', result).strip()
|
| 14 |
+
return result
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def clean_wine_name(name):
|
| 18 |
+
"""
|
| 19 |
+
Удаляет в конце строки отдельно стоящие буквы (однобуквенные слова), не входящие в состав других слов.
|
| 20 |
+
Например, "токай л" превратится в "токай".
|
| 21 |
+
"""
|
| 22 |
+
# Регулярное выражение ищет:
|
| 23 |
+
# \s+ – один или несколько пробельных символов;
|
| 24 |
+
# \b – граница слова;
|
| 25 |
+
# [A-Za-zА-ЯЁа-яё] – ровно одна буква (латинская или кириллическая);
|
| 26 |
+
# \b – граница слова;
|
| 27 |
+
# \s*$ – любые пробелы до конца строки.
|
| 28 |
+
return re.sub(r'\s+\b[A-Za-zА-ЯЁа-яё]\b\s*$', '', name)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def find_full_word(text, word_list):
|
| 32 |
+
"""
|
| 33 |
+
Ищет первое полное вхождение слова из word_list в строке text.
|
| 34 |
+
Возвращает найденное слово или None, если совпадение не найдено.
|
| 35 |
+
"""
|
| 36 |
+
for word in word_list:
|
| 37 |
+
pattern = r'\b' + re.escape(word) + r'\b'
|
| 38 |
+
if re.search(pattern, text, re.IGNORECASE):
|
| 39 |
+
return word
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def merge_wine_type(items, colors=None, color_merge_dict=None):
|
| 44 |
+
result=[]
|
| 45 |
+
for row in tqdm(items.iterrows()):
|
| 46 |
+
try:
|
| 47 |
+
if row[1]['type_wine'] is not None:
|
| 48 |
+
color=find_full_word(row[1]['type_wine'], colors)
|
| 49 |
+
if color is not None:
|
| 50 |
+
result.append(color)
|
| 51 |
+
else:
|
| 52 |
+
color=find_full_word(row[1]['name'], colors)
|
| 53 |
+
if color is not None:
|
| 54 |
+
result.append(color)
|
| 55 |
+
else:
|
| 56 |
+
result.append(None)
|
| 57 |
+
else:
|
| 58 |
+
color=find_full_word(row[1]['name'], colors)
|
| 59 |
+
if color is not None:
|
| 60 |
+
result.append(color)
|
| 61 |
+
else:
|
| 62 |
+
result.append(None)
|
| 63 |
+
except Exception as ex:
|
| 64 |
+
print(ex)
|
| 65 |
+
result.append(None)
|
| 66 |
+
|
| 67 |
+
items['new_type_wine']=result
|
| 68 |
+
items['new_type_wine']=items['new_type_wine'].replace(color_merge_dict)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def merge_types(items, products):
|
| 72 |
+
alco_types=[i.strip().lower() for i in products['type'].unique()]
|
| 73 |
+
alco_types.append('ликёр')
|
| 74 |
+
result=[]
|
| 75 |
+
for row in tqdm(items.iterrows()):
|
| 76 |
+
try:
|
| 77 |
+
type_in_name=find_full_word(row[1]['name'], alco_types)
|
| 78 |
+
if type_in_name is not None:
|
| 79 |
+
result.append(type_in_name)
|
| 80 |
+
continue
|
| 81 |
+
if row[1]['type'] is not None:
|
| 82 |
+
type_in_type=find_full_word(row[1]['type'], alco_types)
|
| 83 |
+
if type_in_type is not None:
|
| 84 |
+
result.append(type_in_type)
|
| 85 |
+
else:
|
| 86 |
+
result.append(row[1]['type'])
|
| 87 |
+
else:
|
| 88 |
+
result.append(None)
|
| 89 |
+
except Exception as ex:
|
| 90 |
+
print(ex)
|
| 91 |
+
result.append(None)
|
| 92 |
+
|
| 93 |
+
items['new_type']=result
|
| 94 |
+
items['new_type']=items['new_type'].replace({'ликёр': 'ликер', None: 'unmatched'})
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def trim_name(text, words_to_remove):
|
| 98 |
+
"""
|
| 99 |
+
Удаляет из текста только те слова, которые полностью совпадают с элементами списка words_to_remove.
|
| 100 |
+
|
| 101 |
+
:param text: Исходная строка.
|
| 102 |
+
:param words_to_remove: Список слов, которые необходимо удалить.
|
| 103 |
+
:return: Обновлённая строка с удалёнными словами.
|
| 104 |
+
"""
|
| 105 |
+
# Создаём регулярное выражение, которое ищет любое из указанных слов как отдельное слово.
|
| 106 |
+
# Используем re.escape, чтобы экранировать спецсимволы в словах.
|
| 107 |
+
pattern = r'\b(?:' + '|'.join(re.escape(word) for word in words_to_remove) + r')\b'
|
| 108 |
+
#print(pattern)
|
| 109 |
+
|
| 110 |
+
# Заменяем найденные полные слова на пустую строку.
|
| 111 |
+
new_text = re.sub(pattern, '', text, flags=re.IGNORECASE)
|
| 112 |
+
|
| 113 |
+
# Убираем лишние пробелы, возникающие после удаления слов.
|
| 114 |
+
new_text = re.sub(r'\s+', ' ', new_text).strip()
|
| 115 |
+
|
| 116 |
+
return new_text
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def name_trimmer(df, prcess_text, types_and_others):
|
| 120 |
+
result={}
|
| 121 |
+
gbs=[]
|
| 122 |
+
sours=[]
|
| 123 |
+
for idx, row in tqdm(df.iterrows()):
|
| 124 |
+
text, alcohol, volume_or_number, years, production_year, gb, color, sour=prcess_text(str(row['name']))
|
| 125 |
+
text=trim_name(text, types_and_others).replace(',','').replace('.','')
|
| 126 |
+
result[row['id']]=text.lower().strip() #remove_l(text).lower().strip()
|
| 127 |
+
|
| 128 |
+
gbs.append(gb)
|
| 129 |
+
sours.append(sour)
|
| 130 |
+
return result, gbs, sours
|
preprocess/utils/items/attrs.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def check_spark(row, col_name='name', types=['Игристое', 'игр']):
|
| 2 |
+
if col_name in row.keys():
|
| 3 |
+
for t in types:
|
| 4 |
+
if t.lower() in row[col_name].lower() and 'Пилигрим' not in row[col_name].lower():
|
| 5 |
+
return 'Игристое'
|
| 6 |
+
return None
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def check_color_and_sour(row, col_name='type_wine', types=['Белое', 'Розовое', 'Красное']):
|
| 10 |
+
if col_name in row.keys():
|
| 11 |
+
for t in types:
|
| 12 |
+
if t.lower() in row[col_name].lower():
|
| 13 |
+
return 'Вино'
|
| 14 |
+
return None
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def is_type_exist(row, types):
|
| 18 |
+
for t in types:
|
| 19 |
+
if t.lower() in row['type'].lower(): # Сравнение без учета регистра
|
| 20 |
+
return t
|
| 21 |
+
return None
|
| 22 |
+
|
| 23 |
+
def check_type(row, types):
|
| 24 |
+
#checker=False
|
| 25 |
+
for t in types:
|
| 26 |
+
if t.lower() in row['name'].lower(): # Сравнение без учета регистра
|
| 27 |
+
return t
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
def get_type(row, types):
|
| 31 |
+
if 'type' not in row.keys():
|
| 32 |
+
return check_type(row, types)
|
| 33 |
+
elif 'type' in row.keys():
|
| 34 |
+
semi_res=is_type_exist(row, types)
|
| 35 |
+
if semi_res!=None:
|
| 36 |
+
return semi_res
|
| 37 |
+
else:
|
| 38 |
+
return check_type(row, types)
|
| 39 |
+
return None
|
| 40 |
+
|