Spaces:
Sleeping
Sleeping
Upload Funcs.py
Browse files
Funcs.py
CHANGED
|
@@ -27,31 +27,31 @@ from collections import Counter
|
|
| 27 |
|
| 28 |
|
| 29 |
|
| 30 |
-
def check_spark(row, col_name='name', types=['
|
| 31 |
if col_name in row.keys():
|
| 32 |
for t in types:
|
| 33 |
-
if t.lower() in row[col_name].lower() and '
|
| 34 |
-
return '
|
| 35 |
return None
|
| 36 |
|
| 37 |
-
def check_color_and_sour(row, col_name='type_wine', types=['
|
| 38 |
if col_name in row.keys():
|
| 39 |
for t in types:
|
| 40 |
if t.lower() in row[col_name].lower():
|
| 41 |
-
return '
|
| 42 |
return None
|
| 43 |
|
| 44 |
|
| 45 |
def is_type_exist(row, types):
|
| 46 |
for t in types:
|
| 47 |
-
if t.lower() in row['type'].lower(): #
|
| 48 |
return t
|
| 49 |
return None
|
| 50 |
|
| 51 |
def check_type(row, types):
|
| 52 |
#checker=False
|
| 53 |
for t in types:
|
| 54 |
-
if t.lower() in row['name'].lower(): #
|
| 55 |
return t
|
| 56 |
return None
|
| 57 |
|
|
@@ -69,19 +69,19 @@ def get_type(row, types):
|
|
| 69 |
|
| 70 |
def extract_years(text):
|
| 71 |
"""
|
| 72 |
-
|
| 73 |
"""
|
| 74 |
-
#
|
| 75 |
-
match = re.search(r'\b(?<!\d)(\d{1,2})\s*(
|
| 76 |
if match:
|
| 77 |
-
#
|
| 78 |
return f"{match.group(1)} {match.group(2)}"
|
| 79 |
return None
|
| 80 |
|
| 81 |
def extract_production_year(text):
|
| 82 |
"""
|
| 83 |
-
|
| 84 |
-
|
| 85 |
"""
|
| 86 |
match = re.search(r'\b(19\d{2}|20\d{2})\b', text)
|
| 87 |
if match:
|
|
@@ -90,19 +90,19 @@ def extract_production_year(text):
|
|
| 90 |
|
| 91 |
def extract_alcohol_content(text):
|
| 92 |
"""
|
| 93 |
-
|
| 94 |
-
|
| 95 |
"""
|
| 96 |
match = re.search(r'(\d{1,2}(?:[.,]\d+)?\s*%)', text)
|
| 97 |
if match:
|
| 98 |
-
#
|
| 99 |
return match.group(1).replace(' ', '').replace(',', '.')
|
| 100 |
return None
|
| 101 |
|
| 102 |
|
| 103 |
def is_volume(value):
|
| 104 |
"""
|
| 105 |
-
|
| 106 |
"""
|
| 107 |
try:
|
| 108 |
volume = float(value)
|
|
@@ -112,16 +112,16 @@ def is_volume(value):
|
|
| 112 |
|
| 113 |
def extract_volume_or_number(text):
|
| 114 |
"""
|
| 115 |
-
|
| 116 |
-
|
| 117 |
"""
|
| 118 |
-
#
|
| 119 |
-
match_with_l = re.search(r'(\d+(?:[\.,]\d+)?\s*[
|
| 120 |
if match_with_l:
|
| 121 |
-
return is_volume(match_with_l.group(1).replace(',', '.').replace('
|
| 122 |
|
| 123 |
-
# ��
|
| 124 |
-
match_number = re.search(r'(?<!
|
| 125 |
if match_number:
|
| 126 |
return is_volume(match_number.group(1).replace(',', '.'))
|
| 127 |
|
|
@@ -130,37 +130,37 @@ def extract_volume_or_number(text):
|
|
| 130 |
|
| 131 |
def get_sour(s):
|
| 132 |
"""
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
|
| 137 |
Args:
|
| 138 |
-
s (str):
|
| 139 |
|
| 140 |
Returns:
|
| 141 |
-
str or None:
|
| 142 |
"""
|
| 143 |
-
#
|
| 144 |
keywords = [
|
| 145 |
r'brut',
|
| 146 |
r'semi-sweet',
|
| 147 |
r'sweet',
|
| 148 |
-
r'
|
| 149 |
-
r'
|
| 150 |
-
r'
|
| 151 |
-
r'
|
| 152 |
-
r'
|
| 153 |
-
r'
|
| 154 |
-
r'
|
| 155 |
-
r'
|
| 156 |
-
r'
|
| 157 |
-
r'
|
| 158 |
]
|
| 159 |
|
| 160 |
-
#
|
| 161 |
-
#
|
| 162 |
-
# (?<!\w) -
|
| 163 |
-
# (?!\w) -
|
| 164 |
pattern = re.compile(r'(?<!\w)(?:' + '|'.join(keywords) + r')(?!\w)', re.IGNORECASE)
|
| 165 |
|
| 166 |
match = pattern.search(s)
|
|
@@ -172,29 +172,29 @@ def get_sour(s):
|
|
| 172 |
|
| 173 |
def get_color(s):
|
| 174 |
"""
|
| 175 |
-
|
| 176 |
-
|
| 177 |
|
| 178 |
Args:
|
| 179 |
-
strings (list):
|
| 180 |
|
| 181 |
Returns:
|
| 182 |
-
dict:
|
| 183 |
"""
|
| 184 |
-
#
|
| 185 |
-
keywords = [r'
|
| 186 |
-
r'
|
| 187 |
-
r'
|
| 188 |
-
r'
|
| 189 |
-
r'
|
| 190 |
-
r'
|
| 191 |
r'rosso',
|
| 192 |
r'roso',
|
| 193 |
r'roseto',
|
| 194 |
r'rosetto',
|
| 195 |
r'red',
|
| 196 |
r'white']
|
| 197 |
-
#
|
| 198 |
pattern = re.compile('|'.join(keywords), re.IGNORECASE)
|
| 199 |
#gift_box_phrases={}
|
| 200 |
#for idx, s in enumerate(strings):
|
|
@@ -207,16 +207,16 @@ def get_color(s):
|
|
| 207 |
|
| 208 |
def get_GB(s):
|
| 209 |
"""
|
| 210 |
-
|
| 211 |
-
|
| 212 |
|
| 213 |
Args:
|
| 214 |
-
strings (list):
|
| 215 |
|
| 216 |
Returns:
|
| 217 |
-
dict:
|
| 218 |
"""
|
| 219 |
-
#
|
| 220 |
keywords = [r'cristal decanter in oak gift box',
|
| 221 |
r'in the carton gift box with 2 glasses',
|
| 222 |
r'decanter in the carton gift box',
|
|
@@ -239,30 +239,30 @@ def get_GB(s):
|
|
| 239 |
r'in wood case'
|
| 240 |
r'in wood box',
|
| 241 |
r'in wood',
|
| 242 |
-
r'
|
| 243 |
-
r'
|
| 244 |
-
r'
|
| 245 |
-
r'
|
| 246 |
-
r'
|
| 247 |
-
r'
|
| 248 |
-
r'
|
| 249 |
-
r'
|
| 250 |
-
r'
|
| 251 |
-
r'
|
| 252 |
-
r'
|
| 253 |
-
r'
|
| 254 |
-
r'
|
| 255 |
-
r'
|
| 256 |
-
r'
|
| 257 |
-
r'
|
| 258 |
-
r'
|
| 259 |
-
r'
|
| 260 |
-
r'
|
| 261 |
-
r'
|
| 262 |
-
r'
|
| 263 |
-
r'
|
| 264 |
-
r'
|
| 265 |
-
#
|
| 266 |
pattern = re.compile('|'.join(keywords), re.IGNORECASE)
|
| 267 |
#gift_box_phrases={}
|
| 268 |
#for idx, s in enumerate(strings):
|
|
@@ -291,7 +291,7 @@ def prcess_text(origin):
|
|
| 291 |
if volume_or_number is not None:
|
| 292 |
volume_with_comma=str(volume_or_number).replace('.', ',')
|
| 293 |
text=text.replace(str(volume_or_number), '').replace(str(volume_with_comma), '')
|
| 294 |
-
text=text.replace(str(volume_or_number)+'
|
| 295 |
# else:
|
| 296 |
# volume_or_number=re_extract_volume(text)
|
| 297 |
# if volume_or_number is not None:
|
|
@@ -299,7 +299,7 @@ def prcess_text(origin):
|
|
| 299 |
# text=text.replace(str(volume_or_number), '').replace(str(volume_with_comma), '')
|
| 300 |
years = extract_years(text)
|
| 301 |
if years is not None:
|
| 302 |
-
text=text.replace(str(years), '').replace(str('
|
| 303 |
production_year = extract_production_year(text)
|
| 304 |
if production_year is not None:
|
| 305 |
text=text.replace(str(production_year), '')
|
|
@@ -322,30 +322,30 @@ def prcess_text(origin):
|
|
| 322 |
|
| 323 |
|
| 324 |
def remove_l(text):
|
| 325 |
-
result = re.sub(r'\b
|
| 326 |
|
| 327 |
-
#
|
| 328 |
result = re.sub(r'\s{2,}', ' ', result).strip()
|
| 329 |
return result
|
| 330 |
|
| 331 |
|
| 332 |
def trim_name(text, words_to_remove):
|
| 333 |
"""
|
| 334 |
-
|
| 335 |
|
| 336 |
-
:param text:
|
| 337 |
-
:param words_to_remove:
|
| 338 |
-
:return:
|
| 339 |
"""
|
| 340 |
-
#
|
| 341 |
-
#
|
| 342 |
pattern = r'\b(?:' + '|'.join(re.escape(word) for word in words_to_remove) + r')\b'
|
| 343 |
#print(pattern)
|
| 344 |
|
| 345 |
-
#
|
| 346 |
new_text = re.sub(pattern, '', text, flags=re.IGNORECASE)
|
| 347 |
|
| 348 |
-
#
|
| 349 |
new_text = re.sub(r'\s+', ' ', new_text).strip()
|
| 350 |
|
| 351 |
return new_text
|
|
@@ -448,16 +448,16 @@ def process_products(products):
|
|
| 448 |
|
| 449 |
def fill_brands_in_dataframe(brands, df, col_name='new_brand', is_brand=True):
|
| 450 |
"""
|
| 451 |
-
|
| 452 |
|
| 453 |
-
:param brands:
|
| 454 |
-
:param df: DataFrame
|
| 455 |
-
:return: DataFrame
|
| 456 |
"""
|
| 457 |
-
#
|
| 458 |
automaton = Automaton()
|
| 459 |
|
| 460 |
-
#
|
| 461 |
for idx, brand in enumerate(brands):
|
| 462 |
if isinstance(brand, str) and brand:
|
| 463 |
automaton.add_word(brand.lower(), (idx, brand))
|
|
@@ -466,18 +466,18 @@ def fill_brands_in_dataframe(brands, df, col_name='new_brand', is_brand=True):
|
|
| 466 |
|
| 467 |
def find_brand(name):
|
| 468 |
"""
|
| 469 |
-
|
| 470 |
"""
|
| 471 |
matched_brands = set()
|
| 472 |
for _, (_, brand) in automaton.iter(name.lower()):
|
| 473 |
-
#
|
| 474 |
if re.search(rf'\b{re.escape(brand.lower())}\b', name.lower()):
|
| 475 |
matched_brands.add(brand)
|
| 476 |
|
| 477 |
-
#
|
| 478 |
return max(matched_brands, key=len) if matched_brands else None
|
| 479 |
|
| 480 |
-
#
|
| 481 |
# df['new_brand'] = df.apply(
|
| 482 |
# lambda row: find_brand(row['name']), #if pd.isna(row['brand']) else row['brand'],
|
| 483 |
# axis=1
|
|
@@ -519,31 +519,31 @@ def get_same_brands(products, items):
|
|
| 519 |
|
| 520 |
def match_brands_improved(items_brands, prods_brands, threshold=85):
|
| 521 |
"""
|
| 522 |
-
|
| 523 |
|
| 524 |
-
:param items_brands:
|
| 525 |
-
:param prods_brands:
|
| 526 |
-
:param threshold:
|
| 527 |
-
:return:
|
| 528 |
"""
|
| 529 |
brand_mapping = {}
|
| 530 |
|
| 531 |
for item_brand in tqdm(items_brands):
|
| 532 |
if isinstance(item_brand, str):
|
| 533 |
-
#
|
| 534 |
parts = [part.strip() for part in re.split(r"[\/\(\)]", item_brand) if part.strip()]
|
| 535 |
best_match = None
|
| 536 |
best_score = 0
|
| 537 |
|
| 538 |
for part in parts:
|
| 539 |
match, score, _ = process.extractOne(part, prods_brands, scorer=fuzz.ratio)
|
| 540 |
-
#
|
| 541 |
if score >= threshold and abs(len(part) - len(match)) / len(part) <= 0.3:
|
| 542 |
if score > best_score:
|
| 543 |
best_match = match
|
| 544 |
best_score = score
|
| 545 |
|
| 546 |
-
#
|
| 547 |
if best_match:
|
| 548 |
brand_mapping[item_brand] = best_match#, best_score)
|
| 549 |
|
|
@@ -552,14 +552,14 @@ def match_brands_improved(items_brands, prods_brands, threshold=85):
|
|
| 552 |
|
| 553 |
def normalize(text):
|
| 554 |
"""
|
| 555 |
-
|
| 556 |
"""
|
| 557 |
return unidecode(text.lower())
|
| 558 |
|
| 559 |
def build_regex_for_brands(brands):
|
| 560 |
"""
|
| 561 |
-
|
| 562 |
-
|
| 563 |
"""
|
| 564 |
norm_to_brand = {}
|
| 565 |
for brand in brands:
|
|
@@ -571,20 +571,20 @@ def build_regex_for_brands(brands):
|
|
| 571 |
|
| 572 |
def process_string(s, regex_pattern, norm_to_brand, norm_brand_list, index_to_brand, threshold):
|
| 573 |
"""
|
| 574 |
-
|
| 575 |
-
1.
|
| 576 |
-
2.
|
| 577 |
-
|
| 578 |
"""
|
| 579 |
norm_s = normalize(s)
|
| 580 |
-
#
|
| 581 |
match = regex_pattern.search(norm_s)
|
| 582 |
if match:
|
| 583 |
return s, norm_to_brand[match.group(0)]
|
| 584 |
|
| 585 |
-
#
|
| 586 |
parts = [part.strip() for part in re.split(r"[\/\(\)]", s) if part.strip()]
|
| 587 |
-
parts.append(s) #
|
| 588 |
best_match = None
|
| 589 |
best_score = 0
|
| 590 |
for part in parts:
|
|
@@ -603,17 +603,17 @@ def process_string(s, regex_pattern, norm_to_brand, norm_brand_list, index_to_br
|
|
| 603 |
|
| 604 |
def check_brands_in_strings_pqdm(strings, brands, threshold=85, n_jobs=8):
|
| 605 |
"""
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
:param strings:
|
| 611 |
-
:param brands:
|
| 612 |
-
:param threshold:
|
| 613 |
-
:param n_jobs:
|
| 614 |
-
:return:
|
| 615 |
"""
|
| 616 |
-
#
|
| 617 |
norm_brand_list = []
|
| 618 |
index_to_brand = []
|
| 619 |
for brand in brands:
|
|
@@ -621,14 +621,14 @@ def check_brands_in_strings_pqdm(strings, brands, threshold=85, n_jobs=8):
|
|
| 621 |
norm_brand_list.append(norm_brand)
|
| 622 |
index_to_brand.append(brand)
|
| 623 |
|
| 624 |
-
#
|
| 625 |
regex_pattern, norm_to_brand = build_regex_for_brands(brands)
|
| 626 |
|
| 627 |
-
#
|
| 628 |
def process_string_wrapper(s):
|
| 629 |
return process_string(s, regex_pattern, norm_to_brand, norm_brand_list, index_to_brand, threshold)
|
| 630 |
|
| 631 |
-
#
|
| 632 |
results = pqdm(strings, process_string_wrapper, n_jobs=n_jobs)
|
| 633 |
|
| 634 |
brand_mapping = {}
|
|
@@ -640,34 +640,34 @@ def check_brands_in_strings_pqdm(strings, brands, threshold=85, n_jobs=8):
|
|
| 640 |
|
| 641 |
def clean_wine_name(name):
|
| 642 |
"""
|
| 643 |
-
|
| 644 |
-
|
| 645 |
"""
|
| 646 |
-
#
|
| 647 |
-
# \s+
|
| 648 |
-
# \b
|
| 649 |
-
# [A-Za-z
|
| 650 |
-
# \b
|
| 651 |
-
# \s*$
|
| 652 |
-
return re.sub(r'\s+\b[A-Za-z
|
| 653 |
|
| 654 |
|
| 655 |
def most_common_words(strings, top_n=None):
|
| 656 |
"""
|
| 657 |
-
|
| 658 |
|
| 659 |
-
|
| 660 |
-
- strings:
|
| 661 |
-
- top_n:
|
| 662 |
-
|
| 663 |
|
| 664 |
-
|
| 665 |
-
-
|
| 666 |
"""
|
| 667 |
all_words = []
|
| 668 |
for s in tqdm(strings):
|
| 669 |
s=str(s)
|
| 670 |
-
#
|
| 671 |
words = re.findall(r'\w+', s.lower())
|
| 672 |
all_words.extend(words)
|
| 673 |
|
|
@@ -681,12 +681,12 @@ def top_inserts_matching(other_brands, p_brands, items, th=65):
|
|
| 681 |
for i in other_brands:
|
| 682 |
l=i.split('/')
|
| 683 |
if len(l)>2:
|
| 684 |
-
replaced[l[0].replace('
|
| 685 |
else:
|
| 686 |
-
if '
|
| 687 |
-
replaced[i.replace('
|
| 688 |
|
| 689 |
-
ob=[i.split('/')[0].replace('
|
| 690 |
rr60_ob=check_brands_in_strings_pqdm(ob, p_brands, threshold=th)
|
| 691 |
|
| 692 |
result={}
|
|
@@ -704,7 +704,7 @@ def process_unbrended_names(items, p_brands, types, grape_varieties, onther_word
|
|
| 704 |
for n in tqdm(items[items['new_brand'].isna()]['name'].values):
|
| 705 |
|
| 706 |
name, alcohol, volume_or_number, years, production_year, gb, color, sour=prcess_text(n)
|
| 707 |
-
#name, alcohol, volume_or_number, years, production_year, gb, color, sour=prcess_text('
|
| 708 |
name=trim_name(name, types)
|
| 709 |
name=trim_name(name, grape_varieties)
|
| 710 |
name=trim_name(name, onther_words)
|
|
@@ -735,8 +735,8 @@ def process_unbrended_names(items, p_brands, types, grape_varieties, onther_word
|
|
| 735 |
|
| 736 |
def find_full_word(text, word_list):
|
| 737 |
"""
|
| 738 |
-
|
| 739 |
-
|
| 740 |
"""
|
| 741 |
for word in word_list:
|
| 742 |
pattern = r'\b' + re.escape(word) + r'\b'
|
|
@@ -780,7 +780,7 @@ def merge_wine_type(items, colors=None, color_merge_dict=None):
|
|
| 780 |
|
| 781 |
def merge_types(items, products):
|
| 782 |
alco_types=[i.strip().lower() for i in products['type'].unique()]
|
| 783 |
-
alco_types.append('
|
| 784 |
result=[]
|
| 785 |
for row in tqdm(items.iterrows()):
|
| 786 |
try:
|
|
@@ -801,13 +801,13 @@ def merge_types(items, products):
|
|
| 801 |
result.append(None)
|
| 802 |
|
| 803 |
items['new_type']=result
|
| 804 |
-
items['new_type']=items['new_type'].replace({'
|
| 805 |
|
| 806 |
|
| 807 |
def normalize_name(name):
|
| 808 |
"""
|
| 809 |
-
|
| 810 |
-
|
| 811 |
"""
|
| 812 |
try:
|
| 813 |
if detect_language(name) == 'ru':
|
|
@@ -818,13 +818,13 @@ def normalize_name(name):
|
|
| 818 |
|
| 819 |
def prepare_groups_with_ids(items_df):
|
| 820 |
"""
|
| 821 |
-
|
| 822 |
-
|
| 823 |
|
| 824 |
-
|
| 825 |
|
| 826 |
-
:param items_df: DataFrame
|
| 827 |
-
:return:
|
| 828 |
"""
|
| 829 |
items_df = items_df.copy()
|
| 830 |
items_df['norm_name'] = items_df['name'].apply(normalize_name)
|
|
@@ -836,11 +836,11 @@ def prepare_groups_with_ids(items_df):
|
|
| 836 |
|
| 837 |
def prepare_groups_by_alternative_keys(items_df):
|
| 838 |
"""
|
| 839 |
-
|
| 840 |
-
|
| 841 |
|
| 842 |
-
:param items_df: DataFrame
|
| 843 |
-
:return:
|
| 844 |
"""
|
| 845 |
items_df = items_df.copy()
|
| 846 |
items_df['norm_name'] = items_df['name'].apply(normalize_name)
|
|
@@ -853,26 +853,26 @@ def prepare_groups_by_alternative_keys(items_df):
|
|
| 853 |
|
| 854 |
def new_find_matches_with_ids(products_df, items_groups, items_df, name_threshold=85):
|
| 855 |
"""
|
| 856 |
-
|
| 857 |
-
|
| 858 |
|
| 859 |
-
|
| 860 |
-
-
|
| 861 |
-
-
|
| 862 |
-
|
| 863 |
|
| 864 |
-
|
| 865 |
|
| 866 |
-
:param products_df: DataFrame
|
| 867 |
-
:param items_groups:
|
| 868 |
-
:param items_df: DataFrame
|
| 869 |
-
:param name_threshold:
|
| 870 |
-
:return: DataFrame
|
| 871 |
"""
|
| 872 |
results = []
|
| 873 |
-
no_match_products = [] #
|
| 874 |
|
| 875 |
-
#
|
| 876 |
for idx, product in tqdm(products_df.iterrows(), total=len(products_df)):
|
| 877 |
product_brand = product['brand']
|
| 878 |
product_type = product['type']
|
|
@@ -884,7 +884,7 @@ def new_find_matches_with_ids(products_df, items_groups, items_df, name_threshol
|
|
| 884 |
key = (product_brand, product_type, product_volume, product_type_wine, product_sour)
|
| 885 |
items_data = items_groups.get(key, [])
|
| 886 |
if items_data:
|
| 887 |
-
#
|
| 888 |
items_ids, items_names, items_norm_names, items_volumes, item_type_wine, items_sour = zip(*items_data)
|
| 889 |
else:
|
| 890 |
items_ids, items_names, items_norm_names, items_volumes, item_type_wine, items_sour = ([], [], [], [], [], [])
|
|
@@ -911,13 +911,13 @@ def new_find_matches_with_ids(products_df, items_groups, items_df, name_threshol
|
|
| 911 |
results.append({
|
| 912 |
'product_id': product['id'],
|
| 913 |
'matched_items': matched_items,
|
| 914 |
-
'alternative': [] #
|
| 915 |
})
|
| 916 |
|
| 917 |
-
#
|
| 918 |
groups_by_alternative_keys = prepare_groups_by_alternative_keys(items_df)
|
| 919 |
|
| 920 |
-
#
|
| 921 |
for idx, product in tqdm(no_match_products):
|
| 922 |
product_brand = product['brand']
|
| 923 |
product_type_wine = product['new_type_wine']
|
|
@@ -928,7 +928,7 @@ def new_find_matches_with_ids(products_df, items_groups, items_df, name_threshol
|
|
| 928 |
|
| 929 |
alt_key = (product_type_wine, product_type, product_volume, product_sour)
|
| 930 |
type_items = groups_by_alternative_keys.get(alt_key, [])
|
| 931 |
-
#
|
| 932 |
filtered_items = [item for item in type_items if item[1] != product_brand]
|
| 933 |
if filtered_items:
|
| 934 |
alt_ids, alt_brands, alt_names, alt_norm_names, alt_volumes, alt_type_wine, alt_sour = zip(*filtered_items)
|
|
@@ -960,8 +960,8 @@ def new_find_matches_with_ids(products_df, items_groups, items_df, name_threshol
|
|
| 960 |
|
| 961 |
def contains_full_word(word, text, case_sensitive=True):
|
| 962 |
"""
|
| 963 |
-
|
| 964 |
-
|
| 965 |
"""
|
| 966 |
flags = 0 if case_sensitive else re.IGNORECASE
|
| 967 |
pattern = r'\b' + re.escape(word) + r'\b'
|
|
@@ -978,7 +978,7 @@ def unwrap_brands(products):
|
|
| 978 |
for j in new_brands:
|
| 979 |
if contains_full_word(i, j, case_sensitive=False):
|
| 980 |
if i != j:
|
| 981 |
-
#if len(i)>1:#i != '
|
| 982 |
res[j]=i
|
| 983 |
return res
|
| 984 |
|
|
|
|
| 27 |
|
| 28 |
|
| 29 |
|
| 30 |
+
def check_spark(row, col_name='name', types=['Игристое', 'игр']):
|
| 31 |
if col_name in row.keys():
|
| 32 |
for t in types:
|
| 33 |
+
if t.lower() in row[col_name].lower() and 'Пилигрим' not in row[col_name].lower():
|
| 34 |
+
return 'Игристое'
|
| 35 |
return None
|
| 36 |
|
| 37 |
+
def check_color_and_sour(row, col_name='type_wine', types=['Белое', 'Розовое', 'Красное']):
|
| 38 |
if col_name in row.keys():
|
| 39 |
for t in types:
|
| 40 |
if t.lower() in row[col_name].lower():
|
| 41 |
+
return 'Вино'
|
| 42 |
return None
|
| 43 |
|
| 44 |
|
| 45 |
def is_type_exist(row, types):
|
| 46 |
for t in types:
|
| 47 |
+
if t.lower() in row['type'].lower(): # Сравнение без учета регистра
|
| 48 |
return t
|
| 49 |
return None
|
| 50 |
|
| 51 |
def check_type(row, types):
|
| 52 |
#checker=False
|
| 53 |
for t in types:
|
| 54 |
+
if t.lower() in row['name'].lower(): # Сравнение без учета регистра
|
| 55 |
return t
|
| 56 |
return None
|
| 57 |
|
|
|
|
| 69 |
|
| 70 |
def extract_years(text):
|
| 71 |
"""
|
| 72 |
+
Извлекает сочетание числа и слова, указывающего возраст (например: '50 лет', '21 years').
|
| 73 |
"""
|
| 74 |
+
# Регулярное выражение ищет числа и слова 'лет' или 'years' с учетом регистра
|
| 75 |
+
match = re.search(r'\b(?<!\d)(\d{1,2})\s*(лет|years)\b', text, re.IGNORECASE)
|
| 76 |
if match:
|
| 77 |
+
# Приводим слово 'лет' или 'years' к исходному регистру
|
| 78 |
return f"{match.group(1)} {match.group(2)}"
|
| 79 |
return None
|
| 80 |
|
| 81 |
def extract_production_year(text):
|
| 82 |
"""
|
| 83 |
+
Извлекает год производства (четырехзначное число в диапазоне 1900–2099) из строки.
|
| 84 |
+
Например: '2019'.
|
| 85 |
"""
|
| 86 |
match = re.search(r'\b(19\d{2}|20\d{2})\b', text)
|
| 87 |
if match:
|
|
|
|
| 90 |
|
| 91 |
def extract_alcohol_content(text):
|
| 92 |
"""
|
| 93 |
+
Извлекает содержание алкоголя из строки.
|
| 94 |
+
Например: '40%'.
|
| 95 |
"""
|
| 96 |
match = re.search(r'(\d{1,2}(?:[.,]\d+)?\s*%)', text)
|
| 97 |
if match:
|
| 98 |
+
# Заменяем запятую на точку для единообразия (если нужно)
|
| 99 |
return match.group(1).replace(' ', '').replace(',', '.')
|
| 100 |
return None
|
| 101 |
|
| 102 |
|
| 103 |
def is_volume(value):
|
| 104 |
"""
|
| 105 |
+
Проверяет, является ли значение валидным объемом (<= 10 литров).
|
| 106 |
"""
|
| 107 |
try:
|
| 108 |
volume = float(value)
|
|
|
|
| 112 |
|
| 113 |
def extract_volume_or_number(text):
|
| 114 |
"""
|
| 115 |
+
Извлекает объем в литрах или число с плавающей точкой из строки.
|
| 116 |
+
Например: '0,75л', '0.5', или '1,5 л'.
|
| 117 |
"""
|
| 118 |
+
# Попытка найти объем с буквой 'л' или без пробела перед ней
|
| 119 |
+
match_with_l = re.search(r'(\d+(?:[\.,]\d+)?\s*[лЛ]|(?:\d+(?:[\.,]\d+)?[лЛ]))', text)
|
| 120 |
if match_with_l:
|
| 121 |
+
return is_volume(match_with_l.group(1).replace(',', '.').replace('л', '').replace('Л', '').strip())
|
| 122 |
|
| 123 |
+
# ��сли не найдено, ищем просто число с плавающей точкой
|
| 124 |
+
match_number = re.search(r'(?<!№)\b(\d{1,2}(?:[\.,]\d+))\b(?!\s*(№|-er|er|\d{3,}))', text)
|
| 125 |
if match_number:
|
| 126 |
return is_volume(match_number.group(1).replace(',', '.'))
|
| 127 |
|
|
|
|
| 130 |
|
| 131 |
def get_sour(s):
|
| 132 |
"""
|
| 133 |
+
Извлекает из строки ключевое слово, если оно присутствует как отдельное слово.
|
| 134 |
+
Использует отрицательные просмотр назад/вперёд для проверки, что перед и после найденного
|
| 135 |
+
ключевого слова нет буквенно-цифровых символов.
|
| 136 |
|
| 137 |
Args:
|
| 138 |
+
s (str): Исходная строка.
|
| 139 |
|
| 140 |
Returns:
|
| 141 |
+
str or None: Найденное ключевое слово, если оно присутствует как отдельное слово, иначе None.
|
| 142 |
"""
|
| 143 |
+
# Список ключевых слов
|
| 144 |
keywords = [
|
| 145 |
r'brut',
|
| 146 |
r'semi-sweet',
|
| 147 |
r'sweet',
|
| 148 |
+
r'брют',
|
| 149 |
+
r'сухое',
|
| 150 |
+
r'полусухое',
|
| 151 |
+
r'полусладкое',
|
| 152 |
+
r'сладкое',
|
| 153 |
+
r'п/сух',
|
| 154 |
+
r'п/сл',
|
| 155 |
+
r'п/с',
|
| 156 |
+
r'сл',
|
| 157 |
+
r'сух'
|
| 158 |
]
|
| 159 |
|
| 160 |
+
# Собираем шаблон с использованием негативных просмотр назад и вперёд,
|
| 161 |
+
# чтобы убедиться, что совпадение не является частью более длинного слова.
|
| 162 |
+
# (?<!\w) - перед совпадением не должно быть символа [a-zA-Z0-9_]
|
| 163 |
+
# (?!\w) - после совпадения не должно быть символа [a-zA-Z0-9_]
|
| 164 |
pattern = re.compile(r'(?<!\w)(?:' + '|'.join(keywords) + r')(?!\w)', re.IGNORECASE)
|
| 165 |
|
| 166 |
match = pattern.search(s)
|
|
|
|
| 172 |
|
| 173 |
def get_color(s):
|
| 174 |
"""
|
| 175 |
+
Извлекает строки, содержащие упоминания о подарочной упаковке,
|
| 176 |
+
и возвращает их в виде словаря с индексами.
|
| 177 |
|
| 178 |
Args:
|
| 179 |
+
strings (list): Список строк.
|
| 180 |
|
| 181 |
Returns:
|
| 182 |
+
dict: Словарь, где ключи — индексы строк, а значения — строки с упоминаниями о подарочной упаковке.
|
| 183 |
"""
|
| 184 |
+
# Список ключевых слов и фраз для поиска
|
| 185 |
+
keywords = [r'красное',
|
| 186 |
+
r'белое',
|
| 187 |
+
r'розовое'
|
| 188 |
+
r'кр',
|
| 189 |
+
r'бел',
|
| 190 |
+
r'розе',
|
| 191 |
r'rosso',
|
| 192 |
r'roso',
|
| 193 |
r'roseto',
|
| 194 |
r'rosetto',
|
| 195 |
r'red',
|
| 196 |
r'white']
|
| 197 |
+
# Создаем шаблон регулярного выражения
|
| 198 |
pattern = re.compile('|'.join(keywords), re.IGNORECASE)
|
| 199 |
#gift_box_phrases={}
|
| 200 |
#for idx, s in enumerate(strings):
|
|
|
|
| 207 |
|
| 208 |
def get_GB(s):
|
| 209 |
"""
|
| 210 |
+
Извлекает строки, содержащие упоминания о подарочной упаковке,
|
| 211 |
+
и возвращает их в виде словаря с индексами.
|
| 212 |
|
| 213 |
Args:
|
| 214 |
+
strings (list): Список строк.
|
| 215 |
|
| 216 |
Returns:
|
| 217 |
+
dict: Словарь, где ключи — индексы строк, а значения — строки с упоминаниями о подарочной упаковке.
|
| 218 |
"""
|
| 219 |
+
# Список ключевых слов и фраз для поиска
|
| 220 |
keywords = [r'cristal decanter in oak gift box',
|
| 221 |
r'in the carton gift box with 2 glasses',
|
| 222 |
r'decanter in the carton gift box',
|
|
|
|
| 239 |
r'in wood case'
|
| 240 |
r'in wood box',
|
| 241 |
r'in wood',
|
| 242 |
+
r'хрустальный декантер в подарочной упаковке из дуба',
|
| 243 |
+
r'декантер в подарочной упаковке из картона',
|
| 244 |
+
r'в подарочной упаковке из картона с 2 бокалами'
|
| 245 |
+
r'в подарочной упаковке из картона',
|
| 246 |
+
r'в подарочной упаковке из Дуба',
|
| 247 |
+
r'в П У графин и деревянная коробка',
|
| 248 |
+
r'в подарочной упаковке',
|
| 249 |
+
r'подарочная упаковка',
|
| 250 |
+
r'подарочный набор',
|
| 251 |
+
r'в деревянной коробке',
|
| 252 |
+
r'деревянная коробка',
|
| 253 |
+
r'в п/у+2 бокаланов',
|
| 254 |
+
r'в п/у из картона',
|
| 255 |
+
r'в п/у+бокал',
|
| 256 |
+
r'в п/у (дер.коробке)',
|
| 257 |
+
r'в п/у солома',
|
| 258 |
+
r'в п/у',
|
| 259 |
+
r'в п у',
|
| 260 |
+
r'п/уп',
|
| 261 |
+
r'п/у',
|
| 262 |
+
r'в тубе',
|
| 263 |
+
r'туба',
|
| 264 |
+
r'ПУ']
|
| 265 |
+
# Создаем шаблон регулярного выражения
|
| 266 |
pattern = re.compile('|'.join(keywords), re.IGNORECASE)
|
| 267 |
#gift_box_phrases={}
|
| 268 |
#for idx, s in enumerate(strings):
|
|
|
|
| 291 |
if volume_or_number is not None:
|
| 292 |
volume_with_comma=str(volume_or_number).replace('.', ',')
|
| 293 |
text=text.replace(str(volume_or_number), '').replace(str(volume_with_comma), '')
|
| 294 |
+
text=text.replace(str(volume_or_number)+' л', '').replace(str(volume_with_comma)+' л', '')
|
| 295 |
# else:
|
| 296 |
# volume_or_number=re_extract_volume(text)
|
| 297 |
# if volume_or_number is not None:
|
|
|
|
| 299 |
# text=text.replace(str(volume_or_number), '').replace(str(volume_with_comma), '')
|
| 300 |
years = extract_years(text)
|
| 301 |
if years is not None:
|
| 302 |
+
text=text.replace(str(years), '').replace(str('выдержка'), '').replace(str('Выдержка'), '').replace(str('aging'), '')
|
| 303 |
production_year = extract_production_year(text)
|
| 304 |
if production_year is not None:
|
| 305 |
text=text.replace(str(production_year), '')
|
|
|
|
| 322 |
|
| 323 |
|
| 324 |
def remove_l(text):
|
| 325 |
+
result = re.sub(r'\bл\b', '', text, flags=re.IGNORECASE)
|
| 326 |
|
| 327 |
+
# Убираем возможные лишние пробелы, возникающие после удаления
|
| 328 |
result = re.sub(r'\s{2,}', ' ', result).strip()
|
| 329 |
return result
|
| 330 |
|
| 331 |
|
| 332 |
def trim_name(text, words_to_remove):
|
| 333 |
"""
|
| 334 |
+
Удаляет из текста только те слова, которые полностью совпадают с элементами списка words_to_remove.
|
| 335 |
|
| 336 |
+
:param text: Исходная строка.
|
| 337 |
+
:param words_to_remove: Список слов, которые необходимо удалить.
|
| 338 |
+
:return: Обновлённая строка с удалёнными словами.
|
| 339 |
"""
|
| 340 |
+
# Создаём регулярное выражение, которое ищет любое из указанных слов как отдельное слово.
|
| 341 |
+
# Используем re.escape, чтобы экранировать спецсимволы в словах.
|
| 342 |
pattern = r'\b(?:' + '|'.join(re.escape(word) for word in words_to_remove) + r')\b'
|
| 343 |
#print(pattern)
|
| 344 |
|
| 345 |
+
# Заменяем найденные полные слова на пустую строку.
|
| 346 |
new_text = re.sub(pattern, '', text, flags=re.IGNORECASE)
|
| 347 |
|
| 348 |
+
# Убираем лишние пробелы, возникающие после удаления слов.
|
| 349 |
new_text = re.sub(r'\s+', ' ', new_text).strip()
|
| 350 |
|
| 351 |
return new_text
|
|
|
|
| 448 |
|
| 449 |
def fill_brands_in_dataframe(brands, df, col_name='new_brand', is_brand=True):
|
| 450 |
"""
|
| 451 |
+
Заполняет колонку 'brand' в DataFrame найденными брендами.
|
| 452 |
|
| 453 |
+
:param brands: Список брендов.
|
| 454 |
+
:param df: DataFrame с колонками ['id', 'brand', 'name', ...].
|
| 455 |
+
:return: DataFrame с обновлённой колонкой 'brand'.
|
| 456 |
"""
|
| 457 |
+
# Инициализируем автомат для быстрого поиска брендов
|
| 458 |
automaton = Automaton()
|
| 459 |
|
| 460 |
+
# Добавляем бренды в автомат
|
| 461 |
for idx, brand in enumerate(brands):
|
| 462 |
if isinstance(brand, str) and brand:
|
| 463 |
automaton.add_word(brand.lower(), (idx, brand))
|
|
|
|
| 466 |
|
| 467 |
def find_brand(name):
|
| 468 |
"""
|
| 469 |
+
Находит лучший бренд для данного имени.
|
| 470 |
"""
|
| 471 |
matched_brands = set()
|
| 472 |
for _, (_, brand) in automaton.iter(name.lower()):
|
| 473 |
+
# Проверяем, что бренд встречается как отдельное слово
|
| 474 |
if re.search(rf'\b{re.escape(brand.lower())}\b', name.lower()):
|
| 475 |
matched_brands.add(brand)
|
| 476 |
|
| 477 |
+
# Возвращаем бренд с максимальной длиной (более точное совпадение)
|
| 478 |
return max(matched_brands, key=len) if matched_brands else None
|
| 479 |
|
| 480 |
+
# Обновляем колонку brand только для пустых значений
|
| 481 |
# df['new_brand'] = df.apply(
|
| 482 |
# lambda row: find_brand(row['name']), #if pd.isna(row['brand']) else row['brand'],
|
| 483 |
# axis=1
|
|
|
|
| 519 |
|
| 520 |
def match_brands_improved(items_brands, prods_brands, threshold=85):
|
| 521 |
"""
|
| 522 |
+
Улучшенный алгоритм сопоставления брендов с учётом нечёткого поиска и фильтрации ошибок.
|
| 523 |
|
| 524 |
+
:param items_brands: Список брендов из датафрейма items.
|
| 525 |
+
:param prods_brands: Список брендов из датафрейма prods.
|
| 526 |
+
:param threshold: Порог сходства для нечёткого поиска.
|
| 527 |
+
:return: Словарь соответствий {бренд из items: ближайший бренд из prods}.
|
| 528 |
"""
|
| 529 |
brand_mapping = {}
|
| 530 |
|
| 531 |
for item_brand in tqdm(items_brands):
|
| 532 |
if isinstance(item_brand, str):
|
| 533 |
+
# Разделяем бренд на части
|
| 534 |
parts = [part.strip() for part in re.split(r"[\/\(\)]", item_brand) if part.strip()]
|
| 535 |
best_match = None
|
| 536 |
best_score = 0
|
| 537 |
|
| 538 |
for part in parts:
|
| 539 |
match, score, _ = process.extractOne(part, prods_brands, scorer=fuzz.ratio)
|
| 540 |
+
# Фильтрация по длине строк и порогу
|
| 541 |
if score >= threshold and abs(len(part) - len(match)) / len(part) <= 0.3:
|
| 542 |
if score > best_score:
|
| 543 |
best_match = match
|
| 544 |
best_score = score
|
| 545 |
|
| 546 |
+
# Сохранение результата
|
| 547 |
if best_match:
|
| 548 |
brand_mapping[item_brand] = best_match#, best_score)
|
| 549 |
|
|
|
|
| 552 |
|
| 553 |
def normalize(text):
|
| 554 |
"""
|
| 555 |
+
Приводит текст к нижнему регистру и транслитерирует его в латиницу.
|
| 556 |
"""
|
| 557 |
return unidecode(text.lower())
|
| 558 |
|
| 559 |
def build_regex_for_brands(brands):
|
| 560 |
"""
|
| 561 |
+
Нормализует бренды и создаёт одно регулярное выражение для точного поиска.
|
| 562 |
+
Возвращает скомпилированный паттерн и словарь: нормализованное название -> оригинальное название.
|
| 563 |
"""
|
| 564 |
norm_to_brand = {}
|
| 565 |
for brand in brands:
|
|
|
|
| 571 |
|
| 572 |
def process_string(s, regex_pattern, norm_to_brand, norm_brand_list, index_to_brand, threshold):
|
| 573 |
"""
|
| 574 |
+
Обрабатывает одну строку:
|
| 575 |
+
1. Пытается найти бренд через регулярное выражение.
|
| 576 |
+
2. Если точного совпадения нет – разбивает строку и выполняет нечёткий поиск.
|
| 577 |
+
Возвращает кортеж: (исходная строка, найденный бренд или None).
|
| 578 |
"""
|
| 579 |
norm_s = normalize(s)
|
| 580 |
+
# Пытаемся найти бренд через регулярное выражение
|
| 581 |
match = regex_pattern.search(norm_s)
|
| 582 |
if match:
|
| 583 |
return s, norm_to_brand[match.group(0)]
|
| 584 |
|
| 585 |
+
# Если точного совпадения нет, разбиваем строку по разделителям и анализируем части
|
| 586 |
parts = [part.strip() for part in re.split(r"[\/\(\)]", s) if part.strip()]
|
| 587 |
+
parts.append(s) # анализ всей строки
|
| 588 |
best_match = None
|
| 589 |
best_score = 0
|
| 590 |
for part in parts:
|
|
|
|
| 603 |
|
| 604 |
def check_brands_in_strings_pqdm(strings, brands, threshold=85, n_jobs=8):
|
| 605 |
"""
|
| 606 |
+
Поиск брендов в строках с учетом вариантов написания и транслитерации.
|
| 607 |
+
Использует предварительный поиск через регулярное выражение и, при необходимости,
|
| 608 |
+
нечёткий поиск. Обработка выполняется параллельно с отображением прогресса с помощью pqdm.
|
| 609 |
+
|
| 610 |
+
:param strings: Список строк для поиска брендов.
|
| 611 |
+
:param brands: Список брендов для поиска.
|
| 612 |
+
:param threshold: Порог сходства для нечёткого поиска.
|
| 613 |
+
:param n_jobs: Число рабочих потоков (или процессов, если использовать pqdm.processes).
|
| 614 |
+
:return: Словарь вида {строка: найденный бренд}.
|
| 615 |
"""
|
| 616 |
+
# Подготавливаем список нормализованных брендов и сопоставление индексов с оригинальными брендами.
|
| 617 |
norm_brand_list = []
|
| 618 |
index_to_brand = []
|
| 619 |
for brand in brands:
|
|
|
|
| 621 |
norm_brand_list.append(norm_brand)
|
| 622 |
index_to_brand.append(brand)
|
| 623 |
|
| 624 |
+
# Создаем комбинированный паттерн для точного поиска.
|
| 625 |
regex_pattern, norm_to_brand = build_regex_for_brands(brands)
|
| 626 |
|
| 627 |
+
# Определяем вспомогательную функцию, закрывающую необходимые параметры.
|
| 628 |
def process_string_wrapper(s):
|
| 629 |
return process_string(s, regex_pattern, norm_to_brand, norm_brand_list, index_to_brand, threshold)
|
| 630 |
|
| 631 |
+
# Обрабатываем строки параллельно с отображением прогресса.
|
| 632 |
results = pqdm(strings, process_string_wrapper, n_jobs=n_jobs)
|
| 633 |
|
| 634 |
brand_mapping = {}
|
|
|
|
| 640 |
|
| 641 |
def clean_wine_name(name):
|
| 642 |
"""
|
| 643 |
+
Удаляет в конце строки отдельно стоящие буквы (однобуквенные слова), не входящие в состав других слов.
|
| 644 |
+
Например, "токай л" превратится в "токай".
|
| 645 |
"""
|
| 646 |
+
# Регулярное выражение ищет:
|
| 647 |
+
# \s+ – один или несколько пробельных символов;
|
| 648 |
+
# \b – граница слова;
|
| 649 |
+
# [A-Za-zА-ЯЁа-яё] – ровно одна буква (латинская или кириллическая);
|
| 650 |
+
# \b – граница слова;
|
| 651 |
+
# \s*$ – любые пробелы до конца строки.
|
| 652 |
+
return re.sub(r'\s+\b[A-Za-zА-ЯЁа-яё]\b\s*$', '', name)
|
| 653 |
|
| 654 |
|
| 655 |
def most_common_words(strings, top_n=None):
|
| 656 |
"""
|
| 657 |
+
Возвращает список наиболее часто повторяющихся слов из списка строк.
|
| 658 |
|
| 659 |
+
Параметры:
|
| 660 |
+
- strings: список строк
|
| 661 |
+
- top_n: количество наиболее часто встречающихся слов, которые необходимо вернуть.
|
| 662 |
+
Если None, возвращаются все слова, отсортированные по частоте.
|
| 663 |
|
| 664 |
+
Возвращает:
|
| 665 |
+
- Список кортежей (слово, частота)
|
| 666 |
"""
|
| 667 |
all_words = []
|
| 668 |
for s in tqdm(strings):
|
| 669 |
s=str(s)
|
| 670 |
+
# Извлекаем слова, приводим их к нижнему регистру и удаляем пунктуацию
|
| 671 |
words = re.findall(r'\w+', s.lower())
|
| 672 |
all_words.extend(words)
|
| 673 |
|
|
|
|
| 681 |
for i in other_brands:
|
| 682 |
l=i.split('/')
|
| 683 |
if len(l)>2:
|
| 684 |
+
replaced[l[0].replace('Шато','')]=i
|
| 685 |
else:
|
| 686 |
+
if 'Шато' in i:
|
| 687 |
+
replaced[i.replace('Шато','')]=i
|
| 688 |
|
| 689 |
+
ob=[i.split('/')[0].replace('Шато','') for i in other_brands]
|
| 690 |
rr60_ob=check_brands_in_strings_pqdm(ob, p_brands, threshold=th)
|
| 691 |
|
| 692 |
result={}
|
|
|
|
| 704 |
for n in tqdm(items[items['new_brand'].isna()]['name'].values):
|
| 705 |
|
| 706 |
name, alcohol, volume_or_number, years, production_year, gb, color, sour=prcess_text(n)
|
| 707 |
+
#name, alcohol, volume_or_number, years, production_year, gb, color, sour=prcess_text('Вино Токай Фурминт п/сл. бел.0.75л')
|
| 708 |
name=trim_name(name, types)
|
| 709 |
name=trim_name(name, grape_varieties)
|
| 710 |
name=trim_name(name, onther_words)
|
|
|
|
| 735 |
|
| 736 |
def find_full_word(text, word_list):
|
| 737 |
"""
|
| 738 |
+
Ищет первое полное вхождение слова из word_list в строке text.
|
| 739 |
+
Возвращает найденное слово или None, если совпадение не найдено.
|
| 740 |
"""
|
| 741 |
for word in word_list:
|
| 742 |
pattern = r'\b' + re.escape(word) + r'\b'
|
|
|
|
| 780 |
|
| 781 |
def merge_types(items, products):
|
| 782 |
alco_types=[i.strip().lower() for i in products['type'].unique()]
|
| 783 |
+
alco_types.append('ликёр')
|
| 784 |
result=[]
|
| 785 |
for row in tqdm(items.iterrows()):
|
| 786 |
try:
|
|
|
|
| 801 |
result.append(None)
|
| 802 |
|
| 803 |
items['new_type']=result
|
| 804 |
+
items['new_type']=items['new_type'].replace({'ликёр': 'ликер', None: 'unmatched'})
|
| 805 |
|
| 806 |
|
| 807 |
def normalize_name(name):
|
| 808 |
"""
|
| 809 |
+
Нормализует строку: если обнаруживается русский язык, транслитерирует её в латиницу,
|
| 810 |
+
приводит к нижнему регистру.
|
| 811 |
"""
|
| 812 |
try:
|
| 813 |
if detect_language(name) == 'ru':
|
|
|
|
| 818 |
|
| 819 |
def prepare_groups_with_ids(items_df):
|
| 820 |
"""
|
| 821 |
+
Предварительная группировка данных из items по (new_brand, type, volume, new_type_wine, sour)
|
| 822 |
+
с учетом нормализованного названия.
|
| 823 |
|
| 824 |
+
Добавляем столбец 'norm_name', чтобы нормализовать значение name один раз заранее.
|
| 825 |
|
| 826 |
+
:param items_df: DataFrame с колонками 'new_brand', 'type', 'name', 'id', 'volume', 'new_type_wine', 'sour'.
|
| 827 |
+
:return: Словарь {(new_brand, type, volume, new_type_wine, sour): [(id, name, norm_name, volume, new_type_wine, sour)]}.
|
| 828 |
"""
|
| 829 |
items_df = items_df.copy()
|
| 830 |
items_df['norm_name'] = items_df['name'].apply(normalize_name)
|
|
|
|
| 836 |
|
| 837 |
def prepare_groups_by_alternative_keys(items_df):
|
| 838 |
"""
|
| 839 |
+
Группировка данных из items по (new_type_wine, new_type, volume, sour) с сохранением id, new_brand,
|
| 840 |
+
оригинального и нормализованного имени.
|
| 841 |
|
| 842 |
+
:param items_df: DataFrame с колонками 'new_brand', 'new_type_wine', 'new_type', 'volume', 'name', 'id', 'sour'.
|
| 843 |
+
:return: Словарь {(new_type_wine, new_type, volume, sour): [(id, new_brand, name, norm_name, volume, new_type_wine, sour)]}.
|
| 844 |
"""
|
| 845 |
items_df = items_df.copy()
|
| 846 |
items_df['norm_name'] = items_df['name'].apply(normalize_name)
|
|
|
|
| 853 |
|
| 854 |
def new_find_matches_with_ids(products_df, items_groups, items_df, name_threshold=85):
|
| 855 |
"""
|
| 856 |
+
Поиск совпадений с сохранением id найденных итемов, используя заранее подготовленные
|
| 857 |
+
нормализованные группы.
|
| 858 |
|
| 859 |
+
Производится два прохода:
|
| 860 |
+
- Первый: поиск по группам (brand, type, volume, new_type_wine, sour);
|
| 861 |
+
- Второй: для продуктов без совпадения ищем по альтернативным группам (new_type_wine, new_type, volume, sour),
|
| 862 |
+
исключая итемы с исходным брендом.
|
| 863 |
|
| 864 |
+
Сравнение производится по столбцу norm_name, а для вывода используется оригинальное name.
|
| 865 |
|
| 866 |
+
:param products_df: DataFrame с колонками 'id', 'brand', 'type', 'name', 'volume', 'new_type_wine', 'sour', 'new_type'.
|
| 867 |
+
:param items_groups: Словарь, сформированный функцией prepare_groups_with_ids.
|
| 868 |
+
:param items_df: DataFrame итемов с колонками 'id', 'new_brand', 'new_type_wine', 'new_type', 'volume', 'name', 'sour'.
|
| 869 |
+
:param name_threshold: Порог сходства для fuzzy matching.
|
| 870 |
+
:return: DataFrame с добавленными столбцами 'matched_items' (список совпадений) и 'alternative' (альтернативные совпадения).
|
| 871 |
"""
|
| 872 |
results = []
|
| 873 |
+
no_match_products = [] # Список для хранения продуктов без совпадения в исходной группе
|
| 874 |
|
| 875 |
+
# Первый проход: поиск по группам (brand, type, volume, new_type_wine, sour)
|
| 876 |
for idx, product in tqdm(products_df.iterrows(), total=len(products_df)):
|
| 877 |
product_brand = product['brand']
|
| 878 |
product_type = product['type']
|
|
|
|
| 884 |
key = (product_brand, product_type, product_volume, product_type_wine, product_sour)
|
| 885 |
items_data = items_groups.get(key, [])
|
| 886 |
if items_data:
|
| 887 |
+
# Распаковываем: id, оригинальное имя, нормализованное имя, volume, new_type_wine, sour
|
| 888 |
items_ids, items_names, items_norm_names, items_volumes, item_type_wine, items_sour = zip(*items_data)
|
| 889 |
else:
|
| 890 |
items_ids, items_names, items_norm_names, items_volumes, item_type_wine, items_sour = ([], [], [], [], [], [])
|
|
|
|
| 911 |
results.append({
|
| 912 |
'product_id': product['id'],
|
| 913 |
'matched_items': matched_items,
|
| 914 |
+
'alternative': [] # Заполняется во втором проходе
|
| 915 |
})
|
| 916 |
|
| 917 |
+
# Подготовка альтернативной группировки по (new_type_wine, new_type, volume, sour)
|
| 918 |
groups_by_alternative_keys = prepare_groups_by_alternative_keys(items_df)
|
| 919 |
|
| 920 |
+
# Второй проход: для продуктов без совпадений ищем по альтернативным группам
|
| 921 |
for idx, product in tqdm(no_match_products):
|
| 922 |
product_brand = product['brand']
|
| 923 |
product_type_wine = product['new_type_wine']
|
|
|
|
| 928 |
|
| 929 |
alt_key = (product_type_wine, product_type, product_volume, product_sour)
|
| 930 |
type_items = groups_by_alternative_keys.get(alt_key, [])
|
| 931 |
+
# Фильтруем, исключая итемы с исходным брендом
|
| 932 |
filtered_items = [item for item in type_items if item[1] != product_brand]
|
| 933 |
if filtered_items:
|
| 934 |
alt_ids, alt_brands, alt_names, alt_norm_names, alt_volumes, alt_type_wine, alt_sour = zip(*filtered_items)
|
|
|
|
| 960 |
|
| 961 |
def contains_full_word(word, text, case_sensitive=True):
|
| 962 |
"""
|
| 963 |
+
Проверяет, содержится ли слово word в строке text как отдельное слово.
|
| 964 |
+
Параметр case_sensitive задаёт, учитывать ли регистр.
|
| 965 |
"""
|
| 966 |
flags = 0 if case_sensitive else re.IGNORECASE
|
| 967 |
pattern = r'\b' + re.escape(word) + r'\b'
|
|
|
|
| 978 |
for j in new_brands:
|
| 979 |
if contains_full_word(i, j, case_sensitive=False):
|
| 980 |
if i != j:
|
| 981 |
+
#if len(i)>1:#i != 'А' and i != "Я":
|
| 982 |
res[j]=i
|
| 983 |
return res
|
| 984 |
|