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

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

После первого цикла кампании проводится аналитика, чтобы понять, нужно ли менять траекторию привлечения. Отдел внутренней рекламы попросил выяснить, какая доля сотрудников зарегистрировалась на сайте сервиса.

Задача:найти сотрудников компании, зарегистрировавшихся на сайте облачного сервиса;
Дано:таблица cloud — данные всех зарегистрированных пользователей нового сервиса,
таблица employees_hr — внутренняя информация о сотрудниках;

Все данные сгенерированы, любые совпадения случайны.

# загружаем данные: 

# таблица со списком людей, зарегистрированных в новом сервисе
cloud = pd.read_excel('DA.xlsx')

# данные сотрудников компании
employees_hr = pd.read_excel('HR.xlsx')

Первые строки датасетов:

cloud:

 ФИОemailтелефон
0Клон Инга Витальевнаdoesntexist_1@mail.ru.ru1 001 234-56-78 /xa0
1Селянина Аглая Марсовнаdoesntexist_2@gmail.com /xa01 012 345-67-89 \.\
2Сазонова Марина Эрастовнаdoesntexist_3@yandex.ru1 023 456-78-90 .

employees_hr:

 ФИОemailтелефонIDподразделение
0Жуков Клим Львовичdoesntexist_01@gmail.comm1 099 111-00-11 /xa0005412111222
1Рыбаков Константин Тарасович\ doesntexist_02@gmail.com1 099 111-99-11 /\.        5874000222
2Сазонова Марина Эрастовнаdoesntexist_03@yandex.ru./.\\\1 023 456-78-905327222000
3Клон Инга Витальевнаdoesntexist_04@mail.ru.ru&&/1 001 234-56-785401111000
      

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

Чтобы извлечь почту и телефон, можно использовать парсер библиотеки Natasha. Но есть способ намного проще и быстрее. Используем токенайзер, который входит в этот модуль. Он не просто разбивает текст на единицы, а формирует линейную последовательную структуру с информацией по каждому токену:

some_text = '''Как и два года назад, ключевыми факторами, которые влияют на деятельность подразделения, респонденты называют нехватку кадровых ресурсов (78%) и большие затраты времени на получение необходимой информации (88%).'''
# инициализируем токенайзер:
tokenizer = Tokenizer()
# токенайзер возвращает генератор, поэтому понадобится упаковать все значения в список
tokens = list(tokenizer(some_text))  

Вывод:

[…  Token(
     value='два',
     span=[6, 9),
     type='RU'
 ), Token(
     value='года',
     span=[10, 14),
     type='RU'
 ), Token(
     value='назад',
     span=[15, 20),
     type='RU'
 ), Token(
     value=',',
     span=[20, 21),
     type='PUNCT'
 )…]

Такая структура содержит извлекаемые атрибуты: из нее можно вытащить и значение, и координаты, и тип токена. Она полезна не только для простого извлечения нужных объектов, но и может пригодиться, например, для анализа координат разных типов токенов.

И парсер, и токенайзер работают на правилах, однако парсер – высокоуровневый модуль, позволяющий извлечь намного более сложные структуры и получить разветвленную характеристику токена (в основном благодаря классу MorphTokenizer(), но сейчас не о нём). Стандартный токенайзер (Tokenizer()) – гибкий и простой в использовании инструмент, он работает быстрее парсера (особенно на больших данных).

Разметка токенов (‘type’ в структуре) производится регулярными выражениями. В токенайзере есть очень удобный метод ‘add_rules’, позволяющий встраивать правила для разметки токенов определенного типа. Если нужного правила нет, можно создать собственное:

# создаем новое правило, выделяющее число процентов из текста
PERCENT = TokenRule('PERCENT', '\\d+[%]')
# добавляем готовое правило 
tokenizer = Tokenizer().add_rules(PERCENT)
# разбиваем текст на токены
list(tokenizer(some_text))

Вывод:

[…  Token(
     value='кадровых',
     span=[119, 127),
     type='RU'
 ), Token(
     value='ресурсов',
     span=[128, 136),
     type='RU'
 ), Token(
     value='(',
     span=[137, 138),
     type='PUNCT'
 ), Token(
     value='78%',
     span=[138, 141),
     type='PERCENT'
 ), Token(
     value=')',
     span=[141, 142),
     type='PUNCT'
 )…]

Правила для нахождения почты и номера телефона уже существуют, поэтому остается только добавить их в функцию и обработать данные:

def email_extract(text): 
    #добавляем правило нахождения почты
    tokenizer = Tokenizer().add_rules(EMAIL_RULE)
    # разбиваем на токены 
    tokens = list(tokenizer(text))
    # берем из результата только те значения, тип которых равен ‘email’
    emails = [token.value for token in tokens if token.type=='EMAIL']
    return emails

#аналогично для номера телефона
def phone_number_extract(text):
    tokenizer = Tokenizer().add_rules(PHONE_RULE)
    tokens = list(tokenizer(text))   
    phone_numbers = [token.value for token in tokens if token.type=='PHONE']
    return phone_numbers

# перезаписываем столбцы, заполняя их извлеченными строками (если нужен анализ результата, или
# есть риск потерять исходные данные, лучше записать в новые столбцы):
cloud['email'] = cloud['email'].apply(email_extract())
cloud['телефон'] = cloud['телефон'].apply(phone_number_extract())

employees_hr['email'] = employees_hr['email'].apply(email_extract())
employees_hr['телефон'] = employees_hr['телефон'].apply(phone_number_extract())

Вывод:

 ФИОemailтелефон
0Клон Инга Витальевнаdoesntexist_1@mail.ru.ru1 001 234-56-78
1Селянина Аглая Марсовнаdoesntexist_2@gmail.com1 012 345-67-89
2Сазонова Марина Эрастовнаdoesntexist_3@yandex.ru1 023 456-78-90

Как можно заметить, от двойного постфикса в адресе почты это не спасло. Однако теперь мы знаем ограниченный набор искажений в данных. Уберем их привычным способом:

cloud.replace({'email' : {'.ru.ru'   : '.ru',
                          '.com.com' : '.com'}}, inplace=True)

Готово! Данные очищены. Осталось самое простое — сгруппировать таблицы по имени и номеру телефона и посмотреть совпадения. Объединяем по указанным столбцам:

registered = pd.merge(cloud, employees_hr, on=[‘ФИО’, ‘телефон’])

Получаем вывод:

ФИОcloud_emailтелефонemployees_hr_emailподразделениеID
0Клон Инга Витальевнаdoesntexist_1@mail.ru1 001 234-56-78doesntexist_04@mail.ru1110005412 
1Корж Юлия Вячеславовнаdoesntexist_512@mail.ru1 016 025-36-49doesntexist_95@mail.ru2221115189 
2Сазонова Марина Эрастовнаdoesntexist_3@yandex.ru1 023 456-78-90doesntexist_03@yandex.ru2220005327 

Из 29 уникальных записей о сотрудниках в выгрузке HR нашлись 3 (9,6%). Передадим эти данные специалистам по рекламе.

На этом выбор инструментов модуля не заканчивается. Отдельного внимания стоит класс MorphTokenizer(), который подключает pymorphy2 для характеристики токена. Некоторые задачи требуют исследования или извлечения словоформ, и подобные инструменты значительно упростят обработку текста.

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