Время прочтения: 4 мин.
Наша компания совместно с крупными партнерами запустила внешний облачный сервис, к использованию которого активно призывались и сотрудники. Кампания по привлечению внутренних клиентов длилась месяц, она включала почтовые рассылки по 2 письма в неделю.
После первого цикла кампании проводится аналитика, чтобы понять, нужно ли менять траекторию привлечения. Отдел внутренней рекламы попросил выяснить, какая доля сотрудников зарегистрировалась на сайте сервиса.
Задача: | найти сотрудников компании, зарегистрировавшихся на сайте облачного сервиса; |
Дано: | таблица cloud — данные всех зарегистрированных пользователей нового сервиса, таблица employees_hr — внутренняя информация о сотрудниках; |
Все данные сгенерированы, любые совпадения случайны.
# загружаем данные:
# таблица со списком людей, зарегистрированных в новом сервисе
cloud = pd.read_excel('DA.xlsx')
# данные сотрудников компании
employees_hr = pd.read_excel('HR.xlsx')
Первые строки датасетов:
cloud:
ФИО | телефон | ||
0 | Клон Инга Витальевна | doesntexist_1@mail.ru.ru | 1 001 234-56-78 /xa0 |
1 | Селянина Аглая Марсовна | doesntexist_2@gmail.com /xa0 | 1 012 345-67-89 \.\ |
2 | Сазонова Марина Эрастовна | doesntexist_3@yandex.ru | 1 023 456-78-90 . |
employees_hr:
ФИО | телефон | ID | подразделение | ||
0 | Жуков Клим Львович | doesntexist_01@gmail.comm | 1 099 111-00-11 /xa000 | 5412 | 111222 |
1 | Рыбаков Константин Тарасович | \ doesntexist_02@gmail.com | 1 099 111-99-11 /\. | 5874 | 000222 |
2 | Сазонова Марина Эрастовна | doesntexist_03@yandex.ru./. | \\\1 023 456-78-90 | 5327 | 222000 |
3 | Клон Инга Витальевна | doesntexist_04@mail.ru.ru | &&/1 001 234-56-78 | 5401 | 111000 |
Внимательно глядя на строки, можно заметить, что в полях с почтой и номером телефона есть лишние символы. Разумеется, для работы с данными нужно их очистить. Номер телефона и почта есть не что иное, как именованные сущности, и на данный момент есть инструменты, позволяющие их распознать. Поэтому проще извлечь нужную часть строки вместо того, чтобы удалять знаки в цикле. Такой подход работает, когда данных очень много, и мы не можем предположить, какие «загрязнения» могут встретиться во всем наборе.
Чтобы извлечь почту и телефон, можно использовать парсер библиотеки 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())
Вывод:
ФИО | телефон | ||
0 | Клон Инга Витальевна | doesntexist_1@mail.ru.ru | 1 001 234-56-78 |
1 | Селянина Аглая Марсовна | doesntexist_2@gmail.com | 1 012 345-67-89 |
2 | Сазонова Марина Эрастовна | doesntexist_3@yandex.ru | 1 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.ru | 1 001 234-56-78 | doesntexist_04@mail.ru | 111000 | 5412 | |
1 | Корж Юлия Вячеславовна | doesntexist_512@mail.ru | 1 016 025-36-49 | doesntexist_95@mail.ru | 222111 | 5189 | |
2 | Сазонова Марина Эрастовна | doesntexist_3@yandex.ru | 1 023 456-78-90 | doesntexist_03@yandex.ru | 222000 | 5327 |
Из 29 уникальных записей о сотрудниках в выгрузке HR нашлись 3 (9,6%). Передадим эти данные специалистам по рекламе.
На этом выбор инструментов модуля не заканчивается. Отдельного внимания стоит класс MorphTokenizer(), который подключает pymorphy2 для характеристики токена. Некоторые задачи требуют исследования или извлечения словоформ, и подобные инструменты значительно упростят обработку текста.
Иногда бывает весьма полезно обратить внимание на побочные функции библиотек, чтобы использовать их преимущества для ускорения и повышения точности при обработке данных.