Время прочтения: 7 мин.

Сегодня мы поговорим об анализе текста. Нередко перед аудитором стоит задача обработать большой массив однотипного текста и выбрать/подсветить определенные фрагменты, которые будут использованы в аудиторском проекте.

Рассмотрим стандартную ситуацию, когда аудитору надо сделать и обработать подборку негатива СМИ по контрагентам. Если эта задача разовая, количество контрагентов не велико и анализируемый период достаточно короткий, то не стоит нагружать себя и готовить автоматизированное решение не имеет смысла. Поэтому данная информация будет интересна тем, кто планирует регулярно обрабатывать большие массивы информации.

Для успешного решения данной задачи необходимо выполнить 3 шага:

1 шаг. Получить информацию.

2 шаг. Убрать лишнее.

3 шаг. Сделать разметку.

Рассмотрим каждый из этих шагов более подробно.

1 шаг. Получить информацию.

Мы не планируем подробно останавливаться на рекомендациях по подбору и поиску информации по контрагенту. Будем считать, что массив данных для разметки у Вас уже сформирован и требуется его обработать. Данное предположение сделано поскольку каждый вправе выбрать наиболее приемлемый для себя источник информации:

— результаты запросов в поисковых системах (практически бесплатный, но наиболее трудозатратный вариант);

— выгрузки данных с помощью новостных агрегаторов (наиболее удобный и простой в применении, но достаточно дорогой способ, при этом для разовых работ можно воспользоваться тестовым доступом);

— внутренние источники в организации.

Для тестирования нашего алгоритма мы оформили тестовый доступ к новостному агрегатору, с помощью которого создали подборку СМИ из 6000 новостей за год по 300 компаниям, находящимся в фокусе внимания федеральных, региональных СМИ.

2 шаг. Убрать лишнее.

На этом шаге нам требуется качественно очистить выгруженную информацию от «мусора» или шумов.

На примере задачи с негативом СМИ по контрагентам можно выделить следующие виды шумов:

— информация, которая не соответствует контрагенту;

— информация дублируется;

— информация не является негативной.

Для решения задачи по очистке шумов мы применяли последовательно 4 алгоритма.

Для оптимизации работы рекомендуем сначала использовать быстрые алгоритмы, а более медленные ставить на последние этапы, когда объем обрабатываемой информации будет минимальным.

Алгоритм 1. Удаление дублей с использованием библиотеки Pandas, метод класса DataFrame – drop_duplicates()

Использование данной библиотеки позволяет удалить полные дубли по большому массиву информации менее чем за секунду, поэтому мы использовали ее в первую очередь.

На нашей тестовой выборке мы сократили объем информации подлежащей обработке почти в 2 раза за 16 мсек.

Пример кода:

data.drop_duplicates(subset='Выдержки из текста', inplace = True)
data.drop_duplicates(subset='Заголовок', inplace = True)

Переменные:

— data – массив новостей;

— subset – колонка DataFrame, по которой метод определяет дубликаты.

Алгоритм 2. Удаление шумов не относящуюся к контрагенту.

В данном случае требуется использовать доступную информацию, полученную в ходе сотрудничества с контрагентом.

Для нашей тестовой выборки мы собрали информацию о территориях присутствия выбранных компаний из открытых источниках (официальные сайты компаний, информационные порталы с адресами и телефонами организаций).

После чего написали процедуру, которая удаляет новости по региональному признаку. На выходе у нас остались новости только всех федеральных СМИ и части региональных СМИ, в которых контрагент ведет деятельность.

Алгоритм работал чуть менее полминуты и сократил нашу подборку на 1000 новостей.

Для удаления шумов по территориальному признаку мы использовали стандартные процедуры циклов и ветвления. В цикле просматриваем регионы новостей и проверяем, ведет ли контрагент деятельность в этом регионе. На выходе получаем массив «флагов», по которому мы в дальнейшем фильтруем данные и избавимся от дубликатов.

Пример кода:

ter_data = pd.read_excel('Данные_о_территориальной_принадлежности_контрагента.xlsx')
#создадим список флагов 
not_in_terr_flag = []

for row in range(data.shape[0]):
    # id контрагента в массиве новостей
    id_ka = data['id'].iloc[row]
    # флаг "соответствия" региона новости региону контрагента
    local_flag = 0
    # далее идет сравнение
    if data['Регион источника'].iloc[row] not in re.split('\\.| |,|\"|\)|\)',str(ter_data[ter_data.id == id_ka][['Регион', 'id']]).groupby('INN').sum().iloc[0,0]):
        not_in_terr_flag.append(1)
        continue
    else:
        not_in_terr_flag.append(0)

Алгоритм 3. Удаление дублей побитовым методом.

Учитывая практику некоторых СМИ перепечатывать другие источники, внося незначительные изменения в текст, мы решили удалить дубли путем побитового сравнения текстов.

Наша тестовая процедура удаляет дублирующие новости, совпадающие побитово на 70 и более процентов при условии, что новости опубликованные в одну дату по одному контрагенту.

На тестовой выборке этот алгоритм работает чуть более 35 сек. и удаляет 174 дубля или 9% от своего входа.

В итоге наша исходная выборка сократилась до 1 139 новостей.

Для побитового метода были разработаны 2 функции:

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

— битовый поиск дубликатов по текстам, которая позволяет сравнивает два текста.

Пример кода:

# битовый поиск дубликатов
def word_duplicate(slovo_a, slovo_b, tolerance = .7):
    local_match_counts = 0
    for x, y in zip(bytearray(slovo_a, encoding='utf-8'), 
                    bytearray(slovo_b, encoding='utf-8')):
          if x == y:
            local_match_counts+=1
    if min(len(slovo_a),len(slovo_b)) > 0 and local_match_counts/min(len(slovo_a),len(slovo_b)) > tolerance:
        return True
    else:
        return False

# битовый поиск дубликатов по текстам
def text_duplicate(text_a, text_b, tolerance = .7, w_tolerance = .7):
    predlojenie_a = re.split('\\.| |,|\"|\)|\)', text_a)
    predlojenie_b = re.split('\\.| |,|\"|\)|\)', text_b)
    if len(predlojenie_a) > len(predlojenie_b):
        predlojenie_a, predlojenie_b = predlojenie_b, predlojenie_a
    counter = 0
    for word_a in predlojenie_a:
        for word_b in predlojenie_b:
            if word_duplicate(word_a, word_b, w_tolerance):
                counter += 1
                break
            else:
                continue
    if counter/len(predlojenie_a) > tolerance:
        return True
    else:
        return False       

Параметры tolerance и w_tolerance позволяют регулировать «толерантность» к неполным дубликатам (в данном случае установлено значение 0.7, т.е. слова и предложения признаются дубликатами в случае совпадение на 70 и более процентов)

Алгоритм 4. Удаление дублей с использованием PyMorphy2.

Морфологический анализатор PyMorphy2 позволяет нормализовать формы слов и провести их последующее сравнение. Это наиболее медленный алгоритм в нашем арсенале, поэтому мы использовали его на последнем этапе. Логика нашей процедуры была такой – удаляем дублирующие новости, которые пословно совпадают на 70 и более процентов при условии, что они опубликованные в одну дату по одному контрагенту.

Работа этого алгоритма заняла более 8 часов и позволила удалить еще 363 дубля.

Основной продукт этого этапа — нормализованные формы слов, которые используются нами на этапе разметки.

Суть алгоритма предельно похожа на предыдущий, за исключением использования класса MorphAnalyzer() библиотеки PyMorpy2

Пример кода:

from pymorphy2 import MorphAnalyzer

mor_an = MorphAnalyzer()

# поиск дубликатов с помощью pymorphy
def morphy_word(first, second):
    p = mor_an.parse(first)[0]
    q = mor_an.parse(second)[0]
# учитываются только потенциально информативные части речи
    if p.tag.POS in ['VERB','NOUN', 'ADJF','ADJS','PRTF','PRTS','INFN']\
    and q.tag.POS in ['VERB','NOUN', 'ADJF','ADJS','PRTF','PRTS','INFN']:
        if p.normal_form == q.normal_form:
            return True
        else:
            return False
    else:
        return False        
       
# поиск дубликатов текстов с помощью pymorphy
def morphy_text(first_sent, second_sent, tolerance = .7):
    predlojenie_a = re.split('\\.| |,|\"|\)|\)', first_sent)
    predlojenie_b = re.split('\\.| |,|\"|\)|\)', second_sent)
    if len(predlojenie_a) > len(predlojenie_b):
        predlojenie_a, predlojenie_b = predlojenie_b, predlojenie_a
    counter = 0
    len_sent = 1
    for word_a in predlojenie_a:
        p_new = mor_an.parse(word_a)[0] 
        if p_new.tag.POS in ['VERB','NOUN','ADJF','ADJS','PRTF','PRTS', 'INFN']:
            len_sent += 1

        for word_b in predlojenie_b:
            dupl_word = morphy_word(word_a, word_b)                     
            if dupl_word:
                counter += 1
                break
            else:
                continue

    if counter/len_sent > tolerance:
        return True
    else:
        return False 

3 шаг. Сделать разметку.

На данном этапе необходимо создать «Мешок слов» и разработать пул правил для разметки текста.

Наполнить «Мешок слов» вы можете на свое усмотрение, в качестве примера рисковых событий могут быть выбраны фразы «Банкротство», «Ликвидация», «Долги», «Иски», «Акционерный конфликт».

Наш тестовый «Мешок слов» состоял из 90 слов, а алгоритм разметки позволял разметить новости по компаниям по 12 типам событий. Как я уже ранее писал, алгоритм разметки использует полученную на предыдущем этапе нормализованные тексты, на выходе мы получаем статьи, в которых упомянуты слова и/или словосочетания из «Мешка слов».

Из 1498 новостей, поступивших на обработку, на выходе осталось 136, соответствующих 12-ти выбранным типам событий.

Для алгоритма разметки мы использовали стандартные процедуры циклов и ветвления. Функция получает на вход предложение, ищет в нем слова из «Мешка» и возвращает их или сообщение об их отсутствии.

Пример кода:

def trouble_criteria(sentence):
    data = re.split('\\.| |,|"', sentence)
    target_words = []
    for word_a in data:
        p_new = mor_an.parse(word_a)[0] 
        if p_new.tag.POS in ['VERB', 'NOUN', 'ADJF', 'ADJS', 'PRTF', 'PRTS', 'INFN']:       
            for word_b in bag_of_words:
                dupl_word = morphy_word(word_a, word_b)                     
                if dupl_word:
                    target_words.append(word_b)
                else:
                    continue
    if target_words:
        return set(target_words)
    else:
        return 'Критериев проблемности не обнаружено'

Правильная последовательность применения алгоритмов для удаления дублей и шумов, грамотно составленный «Мешок слов» – вот залог успеха экспресс-обработки и разметки текста! Это позволит Вам существенно сократить количество часов монотонной работы высокооплачиваемых специалистов. Желаем Вам успехов на практике!