April 24, 2021

Эффективный рентген таблицы

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

Для демонстрации работы будем использовать игрушечную таблицу (код для генерации представлен в конце статьи), содержащую информацию о поломках машин, а именно - тип машины, флаг наличия дефекта, дату поломки, дату поставки (начала эксплуатации), разницы между этими датами (в месяцах):

Перечислим, какие общие сведения о колонках нас могут интересовать:

  • тип данных;
  • количество уникальны значений;
  • список уникальных значений;
  • максимальное и минимальное значение;
  • максимальная и минимальная длина поля.

Тип данных можно получить с помощью свойства столбца dtype, уникальные значения посредством вызова метода drop_duplicates, их число - вызвав свойство shape. Остальные сведения извлекаются в зависимости от типа. В object хранятся данные смешанного типа и строки, а с остальными грубо будем работать как с числами и датами. В результате, для диагностики столбца получим следующую функцию:

def df_col_info(df_col):
    info={}
    info['type'] = df_col.dtype
    unique = df_col.drop_duplicates()
    info['uniq_num'] = unique.shape[0]
    info['uniq'] = unique

    if df_col.dtype=='object':
        info['len_max'] = df_col.str.len().max()
        info['len_min'] = df_col.str.len().min()
        info['min'] = df_col.astype(str).min()
        info['max'] = df_col.astype(str).max()
    else:
        info['min'] = df_col.min()
        info['max'] = df_col.max()
    return info

Рассмотрим ее работу на примере вывода значения столбца дата_поломки:

Обобщая применение df_col_info для каждой колонки датафрейма создадим еще пару функций:

def df_info(df):
    return get_df_info(df, df_col_info)

def get_df_info(df, df_col_f):
    info_d = {}
    for col in df.columns:
        info_d[col] = df_col_f(df[col])
    return info_d

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

Ниже представляю код для генерации нашего набора данных:

import pandas as pd
import numpy as np
# генерируем список типов машин
np.random.seed(0)
car_types = ['type1', 'type2', 'type3', 'type4']
car_nums = np.random.randint(25, 100, size=len(car_types))
cars_l = [[car_type]*car_num for car_type, car_num \
          in zip(car_types, car_nums)]    
cars_l = [item for sub_l in cars_l for item in sub_l]
# генерируем в случайном порядке флаги наличия поломок для машин разных типов
defects_num = [np.random.randint(car_num+1) for car_num in car_nums]
defects_ar = np.array([])
for i, car_num in enumerate(car_nums):
    defects_car = np.zeros(car_num)
    idx = np.random.choice(np.arange(car_num),size=defects_num[i], 
                           replace=False)
    defects_car[idx]=1 
    defects_ar = np.concatenate((defects_ar,defects_car))
# формируем датафрейм
data = pd.DataFrame({'типы_машин':cars_l, 'наличие_дефекта':defects_ar})
# задаем даты поломок произвольно из заданного периода
defects_dates = pd.date_range(start='2019-01-01',end='2020-12-31',freq='D')   
idx = np.random.choice(np.arange(len(defects_dates)), size=int(sum(defects_ar)),
                       replace=True)
defects_dates_col=pd.Series(data=defects_dates[idx],
                             index=data[data['наличие_дефекта']==1].index)
data['дата_поломки'] = defects_dates_col
# задаем в случайном порядке даты ввода в эксплуатацию машин
start_dates = pd.date_range(start='2018-06-01', end='2019-01-01', freq='D')
idx = np.random.choice(np.arange(len(start_dates)), size=data.shape[0],
                       replace=True)
data['дата_поставки'] = pd.Series(data=start_dates[idx])
# считаем разницу в месяцах между датами поломок и поставок машин
data['разница'] = data['дата_поломки'] - data['дата_поставки'] 
data['разница'] = data['разница'].map(lambda s: np.ceil(s.days/30))
# перемешиваем данные
data = data.sample(frac=1)