Есть ли способ изменить этот код, чтобы сократить время выполнения?

поэтому я хочу изменить этот код, чтобы сократить время выполнения библиотеки fuzzywuzzy. В настоящее время для набора данных с 800 строками требуется около часа, а когда я использовал это для набора данных с 4,5 тыс. строк, он продолжал работать почти 6 часов, но все еще безрезультатно. Я должен был остановить ядро.

Мне нужно использовать этот код по крайней мере для данных 20 КБ. Может ли кто-нибудь предложить какие-либо изменения в этом коде, чтобы получить результаты быстрее? Это код -

import pandas as pd
import numpy as np
from fuzzywuzzy import fuzz,process

df = pd.read_csv(r'path')
df.head()

data = df['Body']
print(data)

clean = []
threshold = 80 
for row in data:
  # score each sentence against each other
  # [('string', score),..]
  scores = process.extract(row, data, scorer=fuzz.token_set_ratio)
  # basic idea is if there is a close second match we want to evaluate 
  # and keep the longer of the two
  if scores[1][1] > threshold:
     clean.append(max([x[0] for x in scores[:2]],key=len))
  else:
     clean.append(scores[0][0])

# remove dupes
clean = set(clean)

#converting 'clean' list to dataframe and giving the column name for the cleaned column
clean_data = pd.DataFrame(clean, columns=['Body'])

clean_data.to_csv(r'path') 

Вот как выглядят мои данные -

https://docs.google.com/spreadsheets/d/1p9RC9HznhdJFH4kFYdE_TgnHdoRf8P6gTEAkB3lQWEE/edit?usp=sharing

Поэтому, если вы заметили, что строки 14 и 15, а строки 19 и 20 являются частичными дубликатами, я хочу, чтобы код определял такие предложения и удалял более короткие.

Обновлять -

Я внес небольшое изменение в решение Rapidfuzz, предоставленное @Darryl G, и теперь код выглядит так:

`import pandas as pd
import numpy as np
import openpyxl
from rapidfuzz.fuzz import token_set_ratio as rapid_token_set_ratio
from rapidfuzz import process as process_rapid
from rapidfuzz import utils as rapid_utils
import time

df = pd.read_excel(r'path')

data = df['Body']
print(data)

def excel_sheet_to_dataframe(path):
    '''
        Loads sheet from Excel workbook using openpyxl
    '''
    wb = openpyxl.load_workbook(path)
    ws = wb.active
    data = ws.values
     # Get the first line in file as a header line
    columns = next(data)[0:]
    
    return pd.DataFrame(data, columns=columns)


clean_rapid = []
threshold = 80 

def process_rapid_fuzz(data):
    '''
        Process using rapid fuzz rather than fuzz_wuzzy
    '''
    series = (rapid_utils.default_process(d) for d in data)       # Pre-process to make lower-case and remove non-alphanumeric 
                                                                   # characters (generator)
    processed_data = pd.Series(series)   

    for query in processed_data:
        scores = process_rapid.extract(query, processed_data, scorer=rapid_token_set_ratio, score_cutoff=threshold)
        if len(scores) > 1 and scores[1][1] > threshold:
            m = max(scores[:2], key = lambda k:len(k[0]))                # Of up to two matches above threshold, takes longest
            clean_rapid.append(m[0])                                    # Saving the match index
        else:
            clean_rapid.append(query)

################ Testing
t0 = time.time()
df = excel_sheet_to_dataframe(r'path')   # Using Excel file in working folder

# Desired data in body column
data = df['Body'].dropna()                                           # Dropping None rows (few None rows at end after Excel import)

result_fuzzy_rapid = process_rapid_fuzz(data)
print(f'Elapsed time {time.time() - t0}')

# remove dupes
clean_rapid = set(clean_rapid)

#converting 'clean' list to dataframe and giving the column name for the cleaned column
clean_data = pd.DataFrame(clean_rapid, columns=['Body'])

#exporting the cleaned data
clean_data.to_excel(r'path')`

Теперь проблема в том, что в выходном файле все точки и т. д. удаляются. Как мне их сохранить?


person Shrumo    schedule 22.07.2021    source источник
comment
Можете ли вы предоставить небольшую выдержку из вашего CSV-файла?   -  person Andy Knight    schedule 22.07.2021
comment
Ознакомьтесь с ответом maxbachmann в векторизация или ускорение Fuzzywuzzy Сопоставление строк в столбце PANDAS, что дало 10-кратное улучшение для аналогичной проблемы.   -  person DarrylG    schedule 22.07.2021
comment
@AndyKnight Привет, я добавил фрагмент того, как выглядят мои данные. Надеюсь, поможет   -  person Shrumo    schedule 22.07.2021
comment
@Shrumo - это не ссылка, просто использующая один столбец одного фрейма данных (например, столбец 'org_name'). Разве он не использует fuzzy_wuzzy для поиска ближайших совпадений каждой строки во всем столбце? Это похоже на то, что вы делаете.   -  person DarrylG    schedule 22.07.2021
comment
@DarrylG Привет, я видел это решение, однако моя цель - идентифицировать такие дубликаты и удалить их. код, которым я поделился, выполняет свою работу, просто он очень трудоемкий и непрактичный для набора данных о ставках. Надеюсь на некоторые материалы.   -  person Shrumo    schedule 22.07.2021
comment
Если возможно, вы должны разместить данные в виде текста, а не изображения. Таким образом, другие могут попытаться обработать его. Еще лучше вы могли бы предоставить ссылку на онлайн-файл данных.   -  person DarrylG    schedule 22.07.2021
comment
@DarrylG О, позвольте мне опубликовать ссылку на данные. Спасибо за предложение!   -  person Shrumo    schedule 22.07.2021
comment
@DarrylG Я вставил ссылку на данные для справки.   -  person Shrumo    schedule 22.07.2021


Ответы (2)


Это ответ на вторую часть вашего вопроса. processed_data содержит предварительно обработанные строки, поэтому запрос уже предварительно обработан. Предварительная обработка выполняется process.extract по умолчанию. DarrylG переместил эту предварительную обработку перед циклом, чтобы строки не подвергались предварительной обработке несколько раз. Если вы не хотите, чтобы строки сравнивались без их предварительной обработки, вы можете напрямую перебрать исходные данные: изменить:

series = (rapid_utils.default_process(d) for d in data)
processed_data = pd.Series(series)   

for query in processed_data:

to

for query in data:

Если вам нужно исходное поведение, но вы хотите, чтобы в результатах была необработанная строка, вы можете использовать индекс строки результата для извлечения необработанной строки.

def process_rapid_fuzz(data):
    '''
        Process using rapid fuzz rather than fuzz_wuzzy
    '''
    series = (rapid_utils.default_process(d) for d in data)
    processed_data = pd.Series(series)   

    for query in processed_data:
        scores = process_rapid.extract(query, processed_data,
            scorer=rapid_token_set_ratio,
            score_cutoff=threshold,
            limit=2)
        m = max(scores[:2], key = lambda k:len(k[0]))
        clean_rapid.append(data[m[2]])

Есть несколько дополнительных улучшений, которые возможны в реализации:

  1. Вы можете убедиться, что текущий query не будет совпадать, заменив его на None в processed_data, а затем используя process.extractOne, чтобы найти следующее лучшее совпадение выше порога. Это по крайней мере так же быстро, как process.extract, и может быть значительно быстрее.
  2. Вы сравниваете каждый элемент processed_data с каждым элементом processed_data. Это означает, что вы всегда выполняете сравнение data[n] <-> data[m] и data[m] <-> data[n], даже если они гарантированно дадут одинаковый результат. Только однократное сравнение должно сэкономить около 50% времени выполнения.
def process_rapid_fuzz(data):
    '''
        Process using rapid fuzz rather than fuzz_wuzzy
    '''
    series = (rapid_utils.default_process(d) for d in data)
    processed_data = pd.Series(series)   

    for idx, query in enumerate(processed_data):
        # None is skipped by process.extract/extractOne, so it will never be part of the results
        processed_data[idx] = None
        match = process_rapid.extractOne(query, processed_data,
            scorer=rapid_token_set_ratio,
            score_cutoff=threshold)
        # compare the length using the original strings
        # alternatively len(match[0]) > len(query)
        # if you do want to compare the length of the processed version
        if match and len(data[match[2]]) > len(data[idx]):
            clean_rapid.append(data[match[2]])
        else:
            clean_rapid.append(data[idx])
person maxbachmann    schedule 29.07.2021
comment
Извините за этот вопрос, заранее. В этой предложенной вами строке - 'clean_rapid.append(data[m[2]])'. Это для части кода «если» или «иначе»? - person Shrumo; 29.07.2021
comment
В этом случае вам не нужны if + else, так как запрос является частью результатов из process.extract в этой версии -> у вас есть либо один, либо два результата. В обоих этих случаях макс работает. - person maxbachmann; 29.07.2021
comment
о, ладно... поэтому, когда я запускаю код в одном из своих наборов данных, я получаю одну ошибку, которая говорит: "Ошибка типа: предложение должно быть строкой". Я предполагаю, что может быть какая-то строка, которая не имеет значения в виде строки. Но предположим, я хочу определить, какая строка вызывает эту проблему, как я могу это сделать? Потому что, когда я ищу с помощью фильтра в Excel, я не могу найти значение, которое не является строкой - person Shrumo; 29.07.2021
comment
Вы удалили пустые строки из ввода? - person maxbachmann; 29.07.2021
comment
Да, я сделал.. Я имею в виду, я сначала удалил все пробелы из данных, а затем запустил этот код - person Shrumo; 29.07.2021
comment
странный. Возможно, поймать исключение и распечатать объект + индекс, на котором он не работает. - person maxbachmann; 29.07.2021
comment
Да, вот где я борюсь. Кажется, я не могу найти, какая строка создает эту проблему. - person Shrumo; 29.07.2021
comment
Кроме того, есть ли какие-либо предостережения в отношении Rapidfuzz? Я имею в виду, предположим, что строка начинается с '//' или '[]' или любых подобных символов, не считает ли она ее строкой и, возможно, пропускает ее в выходном файле? - person Shrumo; 29.07.2021
comment
Он игнорирует только элементы, которые равны None. В настоящее время поддерживаются только строки, но в следующем выпуске это изменится: github.com/maxbachmann/RapidFuzz. /проблемы/100 - person maxbachmann; 29.07.2021
comment
Какие None, как в пустых ячейках? - person Shrumo; 29.07.2021
comment
тип None в Python. Например. numpy.nan выдаст ошибку TypeError, так как я не могу обнаружить ее, не имея numpy в качестве зависимости (все равно изменится, когда мне понадобится зависимость Numpy в следующем выпуске) - person maxbachmann; 29.07.2021
comment
О, понял! Спасибо за вашу помощь ! - person Shrumo; 29.07.2021

Этот подход основан на RapidFuzz из ответа в векторизации или Ускорение нечеткого сопоставления строк в столбце PANDAS.

Результат

  • Метод OP Fuzzy Wuzzy): 2565,7 секунды
  • Метод RapidFuzz: 649,5 секунд.

Таким образом: улучшение в 4 раза

  • Примечание. Тестовые данные ~2 000 записей из OP Google Sheet Data загружается в локальную книгу Excel.

Быстрое внедрение фаззинга

import pandas as pd
import numpy as np
import openpyxl
from rapidfuzz.fuzz import token_set_ratio as rapid_token_set_ratio
from rapidfuzz import process as process_rapid
from rapidfuzz import utils as rapid_utils
import time

def excel_sheet_to_dataframe(path):
    '''
        Loads sheet from Excel workbook using openpyxl
    '''
    wb = openpyxl.load_workbook(path)
    ws = wb.active
    data = ws.values
     # Get the first line in file as a header line
    columns = next(data)[0:]
    
    return pd.DataFrame(data, columns=columns)

def process_rapid_fuzz(data):
    '''
        Process using rapid fuzz rather than fuzz_wuzzy
    '''
    series = (rapid_utils.default_process(d) for d in data)       # Pre-process to make lower-case and remove non-alphanumeric 
                                                                   # characters (generator)
    processed_data = pd.Series(series)   

    clean_rapid = []
    threshold = 80 
    n = 0
    for query in processed_data:
        scores = process_rapid.extract(query, processed_data, scorer=rapid_token_set_ratio, score_cutoff=threshold)
        
        m = max(scores[:2], key = lambda k:len(k[0]))                # Of up to two matches above threshold, takes longest
        clean_rapid.append(m[-1])                                    # Saving the match index
        
    clean_rapid = set(clean_rapid)                                   # remove duplicate indexes

    return data[clean_rapid]                                         # Get actual values by indexing to Pandas Series

################ Testing
t0 = time.time()
df = excel_sheet_to_dataframe('Duplicates1.xlsx')   # Using Excel file in working folder

# Desired data in body column
data = df['Body'].dropna()                                           # Dropping None rows (few None rows at end after Excel import)

result_fuzzy_rapid = process_rapid_fuzz(data)
print(f'Elapsed time {time.time() - t0}')

Версия опубликованного кода, используемая для сравнения

import pandas as pd
import numpy as np
from fuzzywuzzy import fuzz, process
import openpyxl
import time

def excel_sheet_to_dataframe(path):
    '''
        Loads sheet from Excel workbook using openpyxl
    '''
    wb = openpyxl.load_workbook(path)
    ws = wb.active
    data = ws.values
     # Get the first line in file as a header line
    columns = next(data)[0:]
    
    return pd.DataFrame(data, columns=columns)

def process_fuzzy_wuzzy(data):
    clean = []
    threshold = 80 
   
    for idx, query in enumerate(data):
        # score each sentence against each other
        # [('string', score),..]
        scores = process.extract(query, data, scorer=fuzz.token_set_ratio)
        # basic idea is if there is a close second match we want to evaluate 
        # and keep the longer of the two
        if len(scores) > 1 and scores[1][1] > threshold:    # If second one is close
            m = max(scores[:2], key=lambda k:len(k[0]))
            clean.append(m[-1])
        else:
            clean.append(idx)

    # remove duplicates
    clean = set(clean)
    return data[clean]                                        # Get actual values by indexing to Pandas Series

################ Testing
t0 = time.time()
# Get DataFrame for sheet from Excel
df = excel_sheet_to_dataframe('Duplicates1.xlsx')  

# Will Process data in 'body' column of DataFrame
data = df['Body'].dropna()                                    # Dropping None rows (few None rows at end after Excel import)

# Process Data (Pandas Series)
result_fuzzy_wuzzy = process_fuzzy_wuzzy(data)
print(f'Elapsed time {time.time() - t0}')
person DarrylG    schedule 23.07.2021
comment
@DarryIG Большое спасибо! я просто попробую этот код. Я предполагаю, что второй код является модификацией моего кода, так что любой из них будет работать? Извините, я все еще новичок в этом, и в этом коде есть несколько новых функций, просто хотел подтвердить - person Shrumo; 23.07.2021
comment
@Shrumo - это в основном ваш код, но немного измененный, главное отличие заключается в использовании файлов Excel. - person DarrylG; 23.07.2021
comment
@Shrumo - использовал Excel, а не версию Google Sheet, поскольку использование API более сложно. - person DarrylG; 23.07.2021
comment
Хорошо, понял ! Я буду держать вас в курсе, как он работает. Очень ценю помощь! - person Shrumo; 23.07.2021
comment
@ Shrumo - только что заметил, что моя среда выполнения намного лучше, чем ваша, с использованием вашего исходного кода. Я использую старый компьютер (~ 7 лет), поэтому такого быть не должно. Возможно, мой мод использования индексов, а не фактических данных, также ускорил ваш исходный код. - person DarrylG; 23.07.2021
comment
@DarryIG о, это удивительно. Я уверен, что дело не в системе, так как мой не такой старый. Можете ли вы уточнить часть индексов? Я пытаюсь понять все аспекты этого. Любая информация будет полезна - person Shrumo; 23.07.2021
comment
@Shrumo - вместо того, чтобы хранить строки в массиве (который довольно велик), я просто сохраняю индекс совпадений. Затем, в конце, я извлекаю исходящие строки, используя индексы для извлечения элементов из серии данных Pandas. Например, с индексом [0, 1, 5] я бы взял строку с индексами 0, 1 и 5 в исходной серии Pandas (т.е. данные — это исходная серия pandas). - person DarrylG; 23.07.2021
comment
@DarryIG О, хорошо ... понял .. Также я просто пробовал второй код (который был модификацией моего исходного кода), и мне просто интересно, есть ли причина добавлять эту строку перед циклом for - def process_fuzzy_wuzzy(data): <br/> clean [] <br/> return data[clean] - person Shrumo; 23.07.2021
comment
Давайте продолжим обсуждение в чате. - person Shrumo; 23.07.2021