Построение надёжного процесса проверки табличной информации

Раздел: Data Science -> Data Science и визуализация

Практические способы контроля качества наборов данных

Перед построением модели данные должны пройти проверку на полноту, уникальность и допустимость значений. 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

Такой же подход подходит для сравнения других признаков: минимальная цена и максимальная скидка, плановая дата и фактическая дата.

Качество данных в Python - comments

En
качество данных на python (python)