Эффективная очистка текста с помощью Pandas

Open in Colab


telegram

Вступление

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

В этой статье будут показаны примеры очистки текстовых полей в большом файле и даны советы по эффективной очистке неструктурированных текстовых полей с помощью Python и pandas.

Оригинал статьи Криса по ссылке

Проблема

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

В восторге от этой возможности, вы загружаете данные и понимаете, что они довольно большие. В этой статье я буду использовать данные, включающие продажи за 2019 год.

Выборочный набор данных представляет собой CSV-файл размером 565 МБ с 24 столбцами и 2,3 млн строк, а весь датасет занимает 5 Гб (25 млн строк). Это ни в коем случае не большие данные, но они достаточно большие для обработки в Excel и некоторых методов pandas.

Давайте начнем с импорта модулей и чтения данных.

Я также воспользуюсь пакетом sidetable для обобщения данных. Он не требуется для очистки, но может быть полезен для подобных сценариев исследования данных.

Данные

Загрузим данные:

Посмотрим на них:

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

Модуль sidetable позволяет обобщать данные в удобочитаемом формате и является альтернативой методу groupby с дополнительными преобразованиями.

Похоже, во всех трех случаях

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

В идеале мы хотели бы сгруппировать вместе все продажи Hy-Vee, Costco и т.д.

Нам нужно очистить данные!

Попытка очистки №1

Первый подход, который мы рассмотрим, - это использование .loc плюс логический фильтр с аксессором str для поиска соответствующей строки в столбце Store Name.

Этот код будет искать строку Hy-Vee без учета регистра и сохранять значение Hy-Vee в новом столбце с именем Store_Group_1. Данный код эффективно преобразует такие названия, как Hy-Vee # 3 / BDI / Des Moines или Hy-Vee Food Store / Urbandale, в обычное Hy-Vee.

Вот, что %timeit говорит об эффективности:

Можем использовать параметр regex=False для ускорения вычислений:

Вот значения в новом столбце:

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

Подход .loc включает много кода и может быть медленным. Поищем альтернативы, которые быстрее выполнять и легче поддерживать.

Попытка очистки №2

Другой очень эффективный и гибкий подход - использовать np.select для запуска нескольких совпадений и применения указанного значения при совпадении.

Есть несколько хороших ресурсов, которые я использовал, чтобы узнать про np.select. Эта статья от Dataquest - хороший обзор, а также презентация Натана Чивера (Nathan Cheever). Рекомендую и то, и другое.

Самое простое объяснение того, что делает np.select, состоит в том, что он оценивает список условий и применяет соответствующий список значений, если условие истинно.

В нашем случае условиями будут разные строки для поиски (string lookups), а в качестве значений нормализованные строки, которые хотим использовать.

После просмотра данных, вот список условий и значений в списке store_patterns. Каждый кортеж в этом списке представляет собой поиск по str.contains() и соответствующее текстовое значение, которое мы хотим использовать для группировки похожих счетов.

Одна из серьезных проблем при работе с np.select заключается в том, что легко получить несоответствие условий и значений. Я решил объединить в кортеж, чтобы упростить отслеживание совпадений данных.

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

Используя zip, можем взять store_patterns и разбить его на store_criteria и store_values:

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

Вот как это выглядит сейчас:

Так лучше, но 32,28% выручки по-прежнему приходится на other счета.

Далее, если есть счет, который не соответствует шаблону, то используем Store Name вместо того, чтобы объединять все в other.

Вот как мы это сделаем:`

Здесь используется функция comb_first, чтобы заполнить все None значения Store Name. Это удобный прием, о котором следует помнить при очистке данных.

Проверим наши данные:

Выглядит лучше, т.к. можем продолжать уточнять группировки по мере необходимости. Например, можно построить поиск по строке для Costco.

Производительность не так уж и плоха для большого набора данных:

13.2 s ± 328 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Гибкость данного подхода в том, что можно использовать np.select для числового анализа и текстовых примеров.

Единственная проблема, связанная с этим подходом, заключается в большом количестве кода.

Есть ли другой подход, который мог бы иметь аналогичную производительность, но был бы немного чище?

Попытка очистки №3

Следующее решение основано на этом примере кода от Мэтта Харрисона (Matt Harrison). Он разработал функцию generalize, которая выполняет сопоставление и очистку за нас!

Я внес некоторые изменения, чтобы привести ее в соответствие с этим примером, но хочу отдать должное Мэтту. Я бы никогда не подумал об этом решении, если бы оно не выполняло 99% всей работы!

Эта функция может быть вызвана для серии pandas и ожидает список кортежей.

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

Вот список эквивалентных шаблонов:

Преимущество этого решения состоит в том, что поддерживать данный список намного проще, чем в предыдущем примере store_patterns.

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

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

Теперь, когда все данные настроены, вызвать их очень просто:

Как насчет производительности?

15.5 s ± 409 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

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

Обратной стороной этого подхода является то, что он предназначен для очистки строк. Решение np.select более полезно, поскольку его можно применять и к числовым значениям.

А как насчет типов данных?

В последних версиях pandas есть специальный тип string. Я попытался преобразовать Store Name в строковый тип pandas, чтобы увидеть, есть ли улучшение производительности. Никаких изменений не заметил. Однако не исключено, что в будущем скорость будет повышена, так что имейте это в виду.

Тип category показал многообещающие результаты.

Обратитесь к моей предыдущей статье за подробностями о типе данных категории.

Можем преобразовать данные в тип category с помощью astype:

Теперь повторно запустите пример np.select точно так же, как мы делали ранее:

786 ms ± 108 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Мы перешли с 13 до менее 1 секунды, сделав одно простое изменение. Удивительно!

Причина, по которой это произошло, довольно проста. Когда pandas преобразует столбец в категориальный тип, функция str.contains() будет вызываться для каждого уникального текстового значения. Поскольку этот набор данных содержит много повторяющихся данных, мы получаем огромный прирост производительности.

Посмотрим, работает ли это для нашей функции generalize:

df['Store_Group_4'] = generalize(df['Store Name'], store_patterns_2)

К сожалению, получаем ошибку:

ValueError: Cannot setitem on a Categorical with a new category, set the categories first

Эта ошибка подчеркивает некоторые проблемы, с которыми я сталкивался в прошлом при работе с категориальными (Categorical) данными. При merging и joining категориальных данных вы можете столкнуться с подобными типами проблем.

Я попытался найти хороший способ изменить работу generalize(), но не смог.

Тем не менее есть способ воспроизвести категориальный подход (Category approach), построив таблицу поиска (lookup table).

Таблица поиска

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

Мы можем построить таблицу поиска и запустить ресурсоемкую функцию только один раз для каждой строки.

Чтобы проиллюстрировать, как это работает со строками, давайте преобразуем значение обратно в строковый тип вместо категории:

Сначала мы создаем DataFrame поиска, который содержит все уникальные значения, и запускаем функцию generalize:

Можем объединить (merge) его обратно в окончательный DataFrame:

1.38 s ± 15.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

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

Кроме того, промежуточный lookup_df может стать отличным выходом для аналитика, который поможет очистить больше данных. Эту экономию можно измерить часами работы!

Резюме

Этот информационный бюллетень Рэнди Ау (Randy Au) - хорошее обсуждение важности очистки данных и отношения любви / ненависти, которое многие специалисты по данным чувствуют при выполнении данной задачи. Я согласен с предположением Рэнди о том, что очистка данных - это анализ.

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

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

Вот краткое изложение рассмотренных решений:

Решение Время исполнения Примечания
np.select 13 с Может работать для нетекстового анализа
generalize 15 с Только текст
Категориальные данные и np.select 786 мс Категориальные данные могут быть сложными при merging и joining
Таблица поиска и generalize 1.3 с Таблица поиска может поддерживаться кем-то другим

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

Однако по мере увеличения размера данных (представьте, что вы проводите анализ для 50 штатов), вам нужно будет понять, как эффективно использовать pandas для очистки текста.