Хватит гадать, как очистить данные. Используйте этот повторяемый пятишаговый рабочий процесс Python для диагностики и исправления наиболее распространённых ошибок данных.
Делиться

Данные часто неидеальны. Вы столкнётесь с множеством несоответствий в данных: пустыми значениями, отрицательными значениями, несоответствиями строк и т. д. Если не устранить их на ранних этапах анализа данных, впоследствии выполнение запросов и анализ данных превратится в проблему.
Итак, я уже занимался очисткой данных с помощью SQL и Excel, но не с помощью Python. Поэтому, чтобы изучить Pandas (одну из библиотек анализа данных Python), я займусь очисткой данных.
В этой статье я поделюсь с вами простым и понятным для новичков рабочим процессом очистки данных. К концу статьи вы должны будете уверенно использовать Python для очистки и анализа данных.
Набор данных, с которым мы будем работать
Я буду работать с синтетическим, неструктурированным набором данных HR, содержащим типичные ошибки (несогласованные даты, смешанные типы данных, составные столбцы). Этот набор данных взят с Kaggle и предназначен для отработки навыков очистки, преобразования, разведочного анализа и предварительной обработки данных для визуализации и машинного обучения.
Набор данных содержит более 1000 строк и 13 столбцов, включая информацию о сотрудниках, такую как имена, комбинации отделов и регионов, контактные данные, статусы, зарплаты и оценки эффективности. В него включены примеры:
- Дубликаты
- Отсутствующие значения
- Несогласованные форматы дат
- Ошибочные записи (например, нечисловые значения зарплаты)
- Составные столбцы (например, «Department_Region» типа «Cloud Tech-Texas», которые можно разделить)
Он содержит такие столбцы:
- Employee_ID: уникальный синтетический идентификатор (например, EMP1001)
- Имя, Фамилия: случайно сгенерированные личные имена
- Имя: Полное имя (может быть указано без имени/фамилии)
- Возраст: включает пропущенные значения.
- Department_Region: составной столбец (например, «HR-Florida»)
- Статус: статус сотрудника (активный, неактивный, ожидающий)
- Join_Date: Несоответствующий формат (ГГГГ/ММ/ДД)
- Зарплата: включает недействительные записи (например, «Н/Д»)
- Электронная почта, телефон: синтетическая контактная информация
- Performance_Score: Категориальный рейтинг производительности
- Remote_Work: Булев флаг (True/False)
Вы можете получить доступ к набору данных здесь и поэкспериментировать с ним.
Набор данных полностью синтетический. Он не содержит данных реальных людей и безопасен для использования в публичных, академических или коммерческих проектах.
Этот набор данных находится в открытом доступе по лицензии CC0 1.0 Universal. Вы можете использовать, изменять и распространять его без ограничений.
Обзор рабочего процесса очистки
Рабочий процесс очистки данных, с которым я буду работать, состоит из 5 простых этапов.
- Нагрузка
- Осмотреть
- Чистый
- Обзор
- Экспорт
Давайте подробнее рассмотрим каждый из этих этапов.
Шаг 1 — загрузка CSV-файла (и устранение первых скрытых проблем)
Перед загрузкой набора данных следует учесть несколько моментов. Однако это необязательный шаг, и мы, вероятно, не столкнёмся с большинством этих проблем в нашем наборе данных. Однако знать об этом не помешает. Вот несколько ключевых моментов, которые следует учитывать при загрузке.
Проблемы с кодировкой (utf-8, latin-1)
Кодировка определяет, как символы хранятся в файле в виде байтов. Python и Pandas обычно по умолчанию используют UTF-8, которая обрабатывает большинство современных текстовых и специальных символов глобально. Однако, если файл был создан в старой системе или в среде, отличной от английской, он может использовать другую кодировку, чаще всего Latin-1.
Поэтому при попытке прочитать файл Latin-1 в кодировке UTF-8 Pandas обнаружит байты, которые не распознаются как допустимые последовательности UTF-8. При попытке прочитать CSV-файл с проблемами кодировки обычно возникает ошибка UnicodeDecodeError.
Если загрузка по умолчанию не удалась, можно попробовать указать другую кодировку:
# Первая попытка (по умолчанию) try: df = pd.read_csv('messy_data.csv') except UnicodeDecodeError: # Вторая попытка с общей альтернативой df = pd.read_csv('messy_data.csv', encoding='latin-1')
Неправильные разделители
CSV расшифровывается как «Comma Separated Values» (значения, разделённые запятыми), но на самом деле во многих файлах в качестве разделителей используются другие символы, например, точка с запятой (распространённая в Европе), табуляция или даже вертикальная черта (|). Pandas обычно по умолчанию использует запятую (,).
Таким образом, если в вашем файле используется точка с запятой (;), но вы загружаете его с разделителем по умолчанию — запятой, Pandas будет воспринимать всю строку как один столбец. В результате получится DataFrame с одним столбцом, содержащим целые строки данных, что сделает работу с ним невозможной.
Решение довольно простое. Попробуйте проверить исходный файл (открыв его в текстовом редакторе, например, VS Code или Notepad++), чтобы увидеть, какой символ разделяет значения. Затем передайте этот символ аргументу sep, например:
# Если в файле используются точки с запятой df = pd.read_csv('messy_data.csv', sep=';') # Если в файле используются символы табуляции (TSV) df = pd.read_csv('messy_data.csv', sep='t')
Столбцы, которые импортируются неправильно
Иногда Pandas угадывает тип данных для столбца на основе первых нескольких строк, но последующие строки содержат неожиданные данные (например, текст, смешанный со столбцом, который начинался с цифр).
Например, библиотека Pandas может правильно определить значения 0.1, 0.2 и 0.3 как числа с плавающей точкой, но если строка 100 содержит значение N/A, Pandas может принудительно преобразовать весь столбец в объектный (строковый) тип, чтобы вместить смешанные значения. Это неудобно, поскольку вы теряете возможность выполнять быстрые векторные числовые операции с этим столбцом, пока не удалите неверные значения.
Чтобы исправить это, я использую аргумент dtype, чтобы явно указать Pandas, какой тип данных должен быть у столбца. Это предотвращает скрытое приведение типов.
df = pd.read_csv('messy_data.csv', dtype={'price': float, 'quantity': 'Int64'})
Прочитав первые несколько строк
Вы можете сэкономить время, проверяя первые несколько строк непосредственно во время загрузки с помощью параметра nrows. Это очень удобно, особенно при работе с большими наборами данных, поскольку позволяет проверить кодировку и разделители, не загружая весь файл объёмом 10 ГБ.
# Загрузите только первые 50 строк для подтверждения кодировки и разделителя temp_df = pd.read_csv('large_messy_data.csv', nrows=50) print(temp_df.head())
Убедившись в правильности аргументов, вы можете загрузить полный файл.
Давайте загрузим набор данных Employee. Не думаю, что здесь возникнут какие-либо проблемы.
импортируйте pandas как pd df = pd.read_csv('Messy_Employee_dataset.csv') df
Выход :
1020 строк × 12 столбцов
Теперь мы можем перейти к шагу 2: осмотр.
Шаг 2 — Проверка набора данных
Я отношусь к этому этапу как к судебной экспертизе. Я ищу доказательства скрытого под поверхностью хаоса. Если я потороплюсь с этим этапом, то гарантированно получу море боли и аналитических ошибок в будущем. Я всегда выполняю эти четыре критически важные проверки перед написанием любого кода для преобразований.
Следующие методы позволяют мне получить полный отчет о состоянии здоровья по 1020 записям моих сотрудников:
1. df.head() и df.tail(): понимание границ
Я всегда начинаю с визуальной проверки. Я использую функции df.head() и df.tail(), чтобы увидеть первые и последние пять строк. Это моя быстрая проверка, чтобы убедиться, что все столбцы выровнены и данные визуально понятны.
Мои выводы:
Когда я запустил df.head(), я заметил, что мой идентификатор сотрудника находится в столбце, а DataFrame вместо этого использует числовой индекс по умолчанию (0, 1, 2, …) .
Хотя я знаю, что можно использовать идентификатор сотрудника в качестве индекса, пока оставлю его. Более серьёзный визуальный риск, который я здесь ищу, — это неровное расположение данных в неправильном столбце или очевидные начальные и конечные пробелы в именах, которые могут создать проблемы в дальнейшем.
2. df.info(): Выявление проблем и пропусков типов данных
Это самый важный метод. Он сообщает мне имена столбцов, типы данных (Dtype) и точное количество ненулевых значений.
Мои выводы по 1020 строкам:
- Пропущенный возраст: общее количество записей — 1020 , но в столбце «Возраст» всего 809 ненулевых значений. Это значительный объём пропущенных данных, и мне придётся решить, как с ними справиться позже: вводить их или удалять строки?
- Отсутствует зарплата: в столбце «Зарплата» 996 ненулевых значений, что является лишь небольшим пробелом, но все же проблему мне необходимо решить.
- Проверка типа идентификатора: идентификатор сотрудника корректно указан как объект (строка). Это неверно. Идентификаторы — это идентификаторы, а не числа, подлежащие усреднению, и использование строкового типа предотвращает случайное удаление начальных нулей библиотекой Pandas.
3. Проверка целостности данных: количество дубликатов и уникальных данных
После проверки dtypes мне нужно узнать, есть ли у меня дублирующиеся записи и насколько последовательны мои категориальные данные.
- Проверка на дубликаты: я выполнил df.duplicated().sum() и получил результат 0. Это идеально! Это означает, что у меня нет одинаковых строк, засоряющих мой набор данных.
- Проверка уникальных значений (df.nunique()): я использую этот метод для оценки разнообразия в каждом столбце. Низкие значения в категориальных столбцах — это нормально, но я ищу столбцы, которые должны быть уникальными, но таковыми не являются, или столбцы со слишком большим количеством уникальных значений, что может указывать на опечатки.
- В базе данных Employee_ID 1020 уникальных записей. Это идеально. Это означает, что все записи уникальны.
- Поле First_Name / Last_Name содержит восемь уникальных записей. Это немного странно. Это подтверждает синтетическую природу набора данных. Мой анализ не будет искажен большим разнообразием имён, поскольку я буду рассматривать их как стандартные строки.
- В столбце Department_Region 36 уникальных записей. Существует высокая вероятность опечаток. 36 уникальных значений для региона/отдела — это слишком много. На следующем этапе мне потребуется проверить этот столбец на наличие вариаций написания (например, «HR» вместо «Human Resources»).
- Электронная почта ( 64 уникальные записи). При наличии 1020 сотрудников наличие всего 64 уникальных адресов электронной почты говорит о том, что многие сотрудники используют один и тот же адрес электронной почты-заглушку. Я отмечу этот адрес для исключения из анализа, поскольку он бесполезен для идентификации личности.
- Телефон ( 1020 уникальных записей). Это идеальный вариант, поскольку он подтверждает, что номера телефонов являются уникальными идентификаторами.
- Возраст / Оценка эффективности / Статус / Удалённая работа (2–4 уникальные записи). Такие низкие значения ожидаемы для категориальных или порядковых данных, что означает, что они готовы к кодированию.
4. df.describe(): обнаружение нечетных и невозможных значений
Я использую df.describe() для получения статистической сводки по всем моим числовым столбцам. Именно здесь мгновенно проявляются действительно невозможные значения — «красные флажки». Я в основном обращаю внимание на строки с минимальными и максимальными значениями.
Мои выводы:
Я сразу заметил проблему в столбце, который, как я ожидал, должен был быть столбцом «Номер телефона», поскольку Pandas по ошибке преобразовал его в числовой тип.
Среднее -4,942253 * 10⁹ Мин. -9,994973 * 10⁹ Макс. -3,896086 * 10⁶ 25% -7,341992e * 10⁹ 50% 4,943997 * 10⁹ 75% -2,520391e * 10⁹
Оказывается, все значения телефонных номеров были огромными отрицательными числами! Это подтверждает два факта:
Pandas ошибочно распознал этот столбец как число, хотя телефонные номера являются строками.
В тексте должны быть символы, которые Pandas не может интерпретировать (например, скобки, тире или коды стран). Мне нужно преобразовать их в объектный тип и полностью очистить.
5. df.isnull().sum(): Количественная оценка пропущенных данных
В то время как df.info() возвращает количество ненулевых значений, df.isnull().sum() возвращает общее количество нулевых значений, что является более понятным способом количественной оценки моих следующих шагов.
Мои выводы:
- Возраст имеет 211 нулей (1020 – 809 = 211), и
- В строке «зарплата» 24 нуля (1020 – 996 = 24). Это точное количество подготавливает почву для шага 3.
Этот процесс проверки — моя подстраховка. Если бы я пропустил отрицательные номера телефонов , любой аналитический этап, связанный с числовыми данными, оказался бы неудачным или, что ещё хуже, привёл бы к неожиданным искажениям результатов.
Определив необходимость обработки номера телефона как строки и существенные пропущенные значения в поле «Возраст», я составил конкретный список для очистки. Это предотвращает ошибки во время выполнения и, что крайне важно, гарантирует, что мой окончательный анализ будет основан на достоверных, неповреждённых данных.
Шаг 3 — Стандартизируйте имена столбцов, исправьте типы данных и обработайте пропущенные значения
Имея на руках список недостатков (отсутствующий возраст, отсутствующая зарплата, ужасные отрицательные номера телефонов и запутанные категориальные данные), я перехожу к основной работе. Я разделяю этот этап на три этапа: обеспечение согласованности, устранение ошибок и заполнение пробелов.
1. Стандартизация названий столбцов и настройка индекса (правило согласованности)
Прежде чем приступать к серьёзным манипуляциям с данными, я обеспечиваю строгую согласованность имён столбцов. Почему? Потому что случайное написание df['Employee ID '] вместо df['employee_id'] — это неявная и раздражающая ошибка. После того, как имена будут очищены, я устанавливаю индекс.
Мое золотое правило — snake_case и строчные буквы везде, а столбцы ID должны быть индексом.
Я использую простую команду для удаления пробелов, замены пробелов подчеркиваниями и перевода всех букв в нижний регистр.
# Команда стандартизации df.columns = df.columns.str.lower().str.replace(' ', '_').str.strip() # До: ['Employee_ID', 'First_Name', 'Phone'] # После: ['employee_id', 'first_name', 'phone']
Теперь, когда наши столбцы стандартизированы, я могу перейти к установке employee_id в качестве индекса.
# Установите идентификатор сотрудника как индекс DataFrame # Это крайне важно для эффективного поиска и чистых слияний в дальнейшем. df.set_index('employee_id', inplace=True) # Давайте рассмотрим это очень быстро print(df.index)
Выход:
Индекс(['EMP1000', 'EMP1001', 'EMP1002', 'EMP1003', 'EMP1004', 'EMP1005', 'EMP1006', 'EMP1007', 'EMP1008', 'EMP1009', … 'EMP2010', 'EMP2011', 'EMP2012', 'EMP2013', 'EMP2014', 'EMP2015', 'EMP2016', 'EMP2017', 'EMP2018', 'EMP2019'], dtype='object', name='employee_id', length=1020)
Отлично, все на месте.
2. Исправление типов данных и их повреждение (борьба с отрицательными номерами телефонов)
Проверка df.describe() выявила самый серьёзный структурный недостаток: столбец Phone был импортирован как ненужный числовой тип. Поскольку номера телефонов — это идентификаторы (а не количества), они должны быть строками.
На этом этапе я преобразую весь столбец в строковый тип, что позволит превратить все отрицательные числа в научной нотации в понятный человеку текст (хотя и с большим количеством нецифровых символов). Саму очистку текста (удаление скобок, тире и т. д.) я оставлю для отдельного этапа стандартизации (шаг 4).
# Немедленно исправьте тип данных Phone # Примечание: имя столбца теперь «phone» из-за стандартизации в версии 3.1 df['phone'] = df['phone'].astype(str)
3. Работа с пропущенными значениями (разница в возрасте и зарплате)
Наконец, я устраняю пробелы, выявленные функцией df.info(): 211 пропущенных значений возраста и 24 пропущенных значения зарплаты (из 1020 строк ). Моя стратегия полностью зависит от роли столбца и величины пропущенных данных:
- Зарплата (24 пропущенных значения) : В данном случае удаление или отбрасывание всех пропущенных значений будет наилучшей стратегией. Зарплата — критически важный показатель для финансового анализа. Вменение его данных может привести к искажению выводов. Поскольку пропущена лишь небольшая часть (2,3%), я решил отбросить неполные записи.
- Возраст (211 пропущенных значений) . Лучшая стратегия в данном случае — заполнение пропущенных значений. Возраст часто используется в качестве характеристики для прогнозного моделирования (например, текучести кадров). Исключение 20% данных слишком затратно. Я заполню пропущенные значения, используя медианный возраст, чтобы избежать искажения распределения средним значением.
Я реализую эту стратегию двумя отдельными командами:
# 1. Удаление: удаление строк, в которых отсутствуют критические данные «зарплата» df.dropna(subset=['salary'], inplace=True) # 2. Ввод: заполнение отсутствующих данных «возраст» медианой median_age = df['age'].median() df['age'].fillna(median_age, inplace=True)
После этих команд я бы снова запустил df.info() или isnull().sum(), просто чтобы убедиться, что ненулевые значения для зарплаты и возраста теперь отражают чистый набор данных.
# Повторная проверка количества нулей для зарплаты и возраста df['salary'].isnull().sum()) df['age'].isnull().sum())
Выход:
np.int64(0)
Все идет нормально!
Решив здесь структурные проблемы и проблемы с отсутствующими данными, последующие шаги могут полностью сосредоточиться на стандартизации значений, например, на 36 сложных уникальных значениях в department_region , которыми мы займемся на следующем этапе.
Шаг 4 — Стандартизация ценностей: обеспечение согласованности данных
Мой DataFrame теперь имеет правильную структуру, но значения внутри всё ещё некорректны. Этот шаг важен для обеспечения согласованности. Если «IT», «it» и «Info. Tech» означают один и тот же отдел, мне нужно привести их к единому, понятному значению («IT»). Это предотвратит ошибки при группировке, фильтрации и любом статистическом анализе по категориям.
1. Очистка поврежденных строковых данных (исправление номера телефона)
Помните испорченный столбец с номерами телефонов из шага 2? Сейчас это просто набор отрицательных чисел в научной нотации, которые мы преобразовали в строки на шаге 3. Теперь пора извлечь сами цифры.
Итак, я удалю все символы, не являющиеся цифрами (тире, скобки, точки и т. д.), и преобразую результат в понятный, унифицированный формат. Регулярные выражения (.str.replace()) идеально подходят для этого. Я использую D для сопоставления любого символа, не являющегося цифрой, и заменяю его пустой строкой.
# Текущий столбец телефона представляет собой строку вида '-9.994973e+09' # Мы используем регулярное выражение для удаления всего, что не является цифрой df['phone'] = df['phone'].str.replace(r'D', '', regex=True) # При необходимости мы также можем обрезать или отформатировать полученную строку # Например, оставив только последние 10 цифр: df['phone'] = df['phone'].str.slice(-10) print(df['phone'])
Выход:
employee_id EMP1000 1651623197 EMP1001 1898471390 EMP1002 5596363211 EMP1003 3476490784 EMP1004 1586734256 … EMP2014 2470739200 EMP2016 2508261122 EMP2017 1261632487 EMP2018 8995729892 EMP2019 7629745492 Имя: телефон, Длина: 996, dtype: объект
Теперь выглядит гораздо лучше. Всегда полезно очищать идентификаторы, содержащие ненужные символы (например, идентификаторы с начальными символами или почтовые индексы с расширениями).
2. Разделение и стандартизация категориальных данных (фиксация 36 регионов)
Проверка df.nunique() выявила 36 уникальных значений в столбце department_region. Когда я просмотрел все уникальные значения в этом столбце, оказалось, что все они аккуратно структурированы по типу «отдел-регион» (например, devops-california, finance-texas, cloud tech-new york).
Думаю, один из способов решить эту проблему — разделить этот столбец на два отдельных. Я разделю столбец по дефису (-) и назначу части новым столбцам: «Департамент» и «Регион».
# 1. Разделим объединенный столбец на два новых, чистых столбца df[['department', 'region']] = df['department_region'].str.split('-', expand=True) Далее я удалю столбец department_region, так как он теперь практически бесполезен # 2. Удалим лишний объединенный столбец df.drop('department_region', axis=1, inplace=True) Давайте рассмотрим наши новые столбцы print(df[['department', 'region']])
Выход:
отдел регион employee_id EMP1000 devops калифорния EMP1001 финансы техас EMP1002 администратор невада EMP1003 администратор невада EMP1004 облачные технологии флорида … … … EMP2014 финансы невада EMP2016 облачные технологии техас EMP2017 финансы нью-йорк EMP2018 hr флорида EMP2019 devops иллинойс [996 строк x 2 столбца]
После разделения новый столбец «Отдел» содержит всего 6 уникальных значений (например, «devops», «finance», «admin» и т. д.). Это отличная новость. Значения уже стандартизированы и готовы к анализу! Думаю, мы всегда могли бы сопоставить все похожие отделы с одной категорией. Но я пропущу этот момент. Не хочу слишком усложнять эту статью.
3. Преобразование столбцов дат (исправление Join_Date)
Столбец Join_Date обычно считывается как строковый (объектный) тип, что делает анализ временных рядов невозможным. Это означает, что нам необходимо преобразовать его в полноценный объект datetime библиотеки Pandas.
pd.to_datetime() — это основная функция. Я часто использую error='coerce' в качестве подстраховки: если Pandas не может разобрать дату, она преобразует это значение в NaT (Not a Time), что является чистым значением NULL, предотвращая сбой всей операции.
# Преобразовать столбец join_date в объекты datetime df['join_date'] = pd.to_datetime(df['join_date'], error='coerce')
Преобразование дат позволяет проводить эффективный анализ временных рядов, например, рассчитывать средний срок службы сотрудников или определять показатели текучести кадров по годам.
После этого шага все значения в наборе данных становятся чистыми, однородными и правильно отформатированными. Категориальные столбцы (например, отдел и регион) готовы к группировке и визуализации, а числовые столбцы (например, зарплата и возраст) — к статистическому моделированию. Набор данных официально готов к анализу.
Шаг 5 — Окончательная проверка качества и экспорт
Прежде чем закрыть блокнот, я всегда провожу последнюю проверку, чтобы убедиться, что все идеально, а затем экспортирую данные, чтобы позже провести их анализ.
Окончательная проверка качества данных
Это быстро. Я повторно запускаю два самых важных метода проверки, чтобы убедиться, что все мои команды очистки действительно сработали:
- df.info() : Я подтверждаю, что в критических столбцах (возраст, зарплата) больше нет пропущенных значений и что типы данных верны (телефон — строка, join_date — дата и время).
- df.describe() : Я обеспечиваю, чтобы статистическая сводка отображала правдоподобные цифры. Столбец «Телефон» теперь должен отсутствовать в этом выводе (поскольку это строка), а столбцы «Возраст» и «Зарплата» должны иметь логические минимальное и максимальное значения.
Если эти проверки пройдены, я знаю, что данные достоверны.
Экспорт чистого набора данных
Последний шаг — сохранить очищенную версию данных. Обычно я сохраняю её как новый CSV-файл, чтобы исходный файл не был поврежден и использовался для справки. Здесь я использую index=False, если не хочу, чтобы employee_id (который теперь является индексом) сохранялся как отдельный столбец, или index=True, если хочу сохранить индекс как первый столбец в новом CSV-файле.
# Экспортируем чистый DataFrame в новый CSV-файл # Мы используем index=True для сохранения нашего первичного ключа (employee_id) в экспортированном файле df.to_csv('cleaned_employee_data.csv', index=True)
Экспортируя файл с новым понятным именем (например, _clean.csv), вы официально отмечаете окончание этапа очистки и обеспечиваете чистый лист для следующего этапа проекта.
Заключение
Честно говоря, раньше я чувствовал себя подавленным, когда дело касалось запутанных наборов данных. Отсутствующие значения, странные типы данных, непонятные столбцы — это было похоже на синдром чистого листа.
Но этот структурированный, повторяемый рабочий процесс изменил всё. Сосредоточившись на загрузке, проверке, очистке, просмотре и экспорте , мы мгновенно навели порядок: стандартизировали названия столбцов, сделали employee_id индексом и использовали продуманные стратегии для импутации и разделения неупорядоченных столбцов.
Теперь я могу сразу перейти к самому интересному анализу, не терзаясь постоянными сомнениями в своих результатах. Если у вас возникнут трудности с первоначальным этапом очистки данных, попробуйте этот рабочий процесс. Буду рад узнать, как он работает. Если хотите поэкспериментировать с набором данных, скачать его можно здесь.
Хотите познакомиться? Не стесняйтесь поздороваться на этих платформах.
Твиттер
Ютуб
Середина
Источник: towardsdatascience.com



























