Построение надёжного процесса проверки табличной информации
Практические способы контроля качества наборов данных
Перед построением модели данные должны пройти проверку на полноту, уникальность и допустимость значений. Python позволяет делать такую проверку регулярной и понятной. В этой части собраны варианты для разных этапов: первичная сводка, автономный отчёт, схема таблицы и одна запись.
Основной подход строится вокруг переиспользуемой функции, которая вычисляет метрики по каждому столбцу. Такой способ даёт контроль над логикой проверок, хранит логику в одном месте и позволяет быстро оценить новые выгрузки.
Универсальная сводная функция на pandas
Код проходит по всем столбцам и считает количество пропусков, процент заполненных значений, число уникальных значений и наличие нулей либо отрицательных чисел.
import pandas as pd
def summary_quality(df):
rows = []
for column in df.columns:
s = df[column]
total = len(s)
missing = int(s.isna().sum())
unique = int(s.nunique(dropna=False))
if pd.api.types.is_numeric_dtype(s):
zeros = float(s.eq(0).sum())
negative = float(s.lt(0).sum())
else:
zeros = float('nan')
negative = float('nan')
rows.append({
'column': column,
'dtype': str(s.dtype),
'completeness': round((1 - missing / total) * 100, 2),
'missing': missing,
'unique': unique,
'zeros': zeros,
'negative': negative
})
return pd.DataFrame(rows)
sample = pd.DataFrame({
'region': ['Восток', 'Запад', 'Восток', None],
'sales': [120, 150, None, 150],
'year': [2022, 2021, 2022, 2021]
})
print(summary_quality(sample))
качество данных на python (качество данных в python)
column dtype completeness missing unique zeros negative 0 region object 75.0 1 3 NaN NaN 1 sales float64 75.0 1 3 0.0 0.0 2 year int64 100.0 0 2 0.0 0.0
Значение completeness равно доле непустых строк. Пропуск в region и sales даёт 75 процентов, а year заполнен полностью на 100 процентов. Число unique показывает количество разных значений вместе с пропуском, когда он есть.
Метод nunique по умолчанию исключает пропуски, поэтому s.nunique() для region вернул бы 2. Передача dropna=False включается в подсчёт и даёт 3. Такой результат более точно отражает фактическое состояние данных.
Сводную функцию удобно вызывать после загрузки нового файла, перед слиянием таблиц и после изменения процедуры сбора данных.
Как сформировать автономный отчёт о составе данных?
Библиотека ydata-profiling преобразует DataFrame в отдельную страницу с графиками и таблицами. Такой отчёт удобно показывать команде без передачи исходного файла.
pip install ydata-profiling
import pandas as pd
from ydata_profiling import ProfileReport
sample = pd.DataFrame({
'region': ['Восток', 'Запад', 'Восток', None],
'sales': [120, 150, None, 150],
'year': [2022, 2021, 2022, 2021]
})
report = ProfileReport(sample, title='Сводка по данным')
report.to_file('quality_report.html')
Создание файла не выводит значение в консоль, но рядом со скриптом появляется HTML-документ. Его можно открыть в браузере и сохранить как артефакт.
Если среда содержит старый пакет pandas-profiling, импорт из ydata_profiling не выполнится. Для решения перед запуском устанавливают текущую версию: pip install ydata-profiling. При длительном проекте полезно зафиксировать версию в requirements.txt.
Как зафиксировать требования к схеме и границам значений?
Библиотека pandera подходит для формализации правил в виде объекта DataFrameSchema. Описание содержит ожидаемые типы и границы допустимых значений.
import pandas as pd
import pandera as pa
from pandera.errors import SchemaErrors
schema = pa.DataFrameSchema({
'region': pa.Column(str, checks=pa.Check.isin(['Восток', 'Запад', 'Север'])),
'sales': pa.Column(float, checks=[pa.Check.ge(0), pa.Check.le(10000)], nullable=True),
'year': pa.Column(int, checks=pa.Check.in_range(2000, 2030))
})
invalid_frame = pd.DataFrame({
'region': ['Сибирь', 'Запад'],
'sales': [1200.0, -150.0],
'year': [2024, 2024]
})
try:
schema.validate(invalid_frame, lazy=True)
except SchemaErrors as exc:
print(exc.failure_cases)
schema_context column check failure_case index 0 DataFrame region isin Сибирь 0 1 DataFrame sales ge -150 1
Аргумент lazy=True позволяет собрать все ошибки за один запуск. Если его не передать, первое нарушение остановит выполнение.
Схема pandera требует явно указывать тип столбца. Числовая колонка с пропуском должна иметь nullable=True, иначе None будет признан недопустимым значением.
Как отсекать некорректные объекты до попадания в расчёт?
Модель pydantic подходит для случаев, когда каждый ряд приходит как отдельный словарь из внешнего источника. Тип поля, минимальная граница и формат даты проверяются при создании объекта.
from datetime import datetime
from pydantic import BaseModel, Field, ValidationError
class Transaction(BaseModel):
tx_id: str
amount: float = Field(ge=0)
created: datetime
try:
Transaction(tx_id='t1', amount=-10, created='2024-01-05 10:00:00')
except ValidationError as exc:
print(exc.errors())
[{'type': 'greater_than_equal', 'loc': ('amount',), 'msg': 'Input should be greater than or equal to 0', 'input': -10}]
Строка с датой автоматически преобразуется в объект datetime, если она соответствует формату ISO 8601.
Поля без Optional считаются обязательными. Пропуск None для amount приведёт к отдельной ошибке. Следует либо передать значение, либо объявить amount с типом Optional[float] и допустимым пропуском None.
Расширенная диагностика датафрейма с флагами проблем
Удобный приём состоит в добавлении булевых колонок, каждая из которых означает наличие проблемы. Это позволяет быстро строить фильтры и сравнивать качество сегментов.
import pandas as pd
events = pd.DataFrame({
'event_id': ['e1', 'e1', 'e2', 'e3'],
'event_date': ['2024-01-05', '2024/01/05', '2024-02-31', '2024-01-05'],
'price': [100.0, 200.0, 150.0, None],
'category': ['A', 'A', 'B', None]
})
events['event_date_dt'] = pd.to_datetime(events['event_date'], errors='coerce')
events['invalid_date'] = events['event_date_dt'].isna()
events['empty_category'] = events['category'].isna()
events['row_id'] = range(len(events))
events['duplicated_event'] = events['event_id'].duplicated(keep=False)
print(events.loc[:, ['row_id', 'event_id', 'invalid_date', 'empty_category', 'duplicated_event']])
row_id event_id invalid_date empty_category duplicated_event 0 0 e1 False False True 1 1 e1 False False True 2 2 e2 True False False 3 3 e3 False True False
Повторы ключа как отдельный вид дублей
Строки считаются дублями в логике бизнеса, когда повторяется ключ, а не обязательно весь набор полей. Метод duplicated с subset отмечает такие повторы.
duplicated_ids = events[events['event_id'].duplicated(keep=False)]
print(duplicated_ids.loc[:, ['event_id', 'event_date', 'price']])
event_id event_date price 0 e1 2024-01-05 100.0 1 e1 2024/01/05 200.0
Выбросы по межквартильному размаху
Метод межквартильного размаха выделяет значения за пределами полуторного интервала. Он не требует предположения о нормальности и работает на небольших выборках.
scores = pd.DataFrame({
'client_id': [1001, 1002, 1003, 1004, 1005],
'amount': [85.0, 88.0, 90.0, 92.0, 210.0]
})
def find_iqr_outliers(s):
q1 = s.quantile(0.25)
q3 = s.quantile(0.75)
iqr = q3 - q1
left = q1 - 1.5 * iqr
right = q3 + 1.5 * iqr
return s.lt(left) | s.gt(right)
print(scores.loc[find_iqr_outliers(scores['amount'])])
client_id amount 4 1005 210.0
Согласованность дат начала и окончания
Сравнение двух колонок выявляет интервалы с отрицательной длительностью. Такие записи часто появляются из-за ошибок ввода.
intervals = pd.DataFrame({
'id': [1, 2],
'start': ['2024-01-01', '2024-06-01'],
'finish': ['2024-05-01', '2023-12-01']
})
intervals['start'] = pd.to_datetime(intervals['start'])
intervals['finish'] = pd.to_datetime(intervals['finish'])
print(intervals.loc[intervals['start'].gt(intervals['finish']), 'id'])
1 2 Name: id, dtype: int64
Такой же подход подходит для сравнения других признаков: минимальная цена и максимальная скидка, плановая дата и фактическая дата.