Вводная

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

Посмотрел в сторону библиотеки pandas-profiling.

Мне показалось, что инструмент хорошо подходит для датасета в котором отработаны аномалии, пропуски, выбросы, типы данных. Вызвав df.profile_report() получаешь добротный отчет и остается только рыться во всех вкладках отчета и анализировать интересующие столбцы.

Меня смутил большой объем информации, который выдает profile_report(). Мне не хватило более простой обзорной формы, где легко можно сделать вывод и решить для себя стоит обратить внимание на столбец или нет. Возникла потребность самому настроить выдачу отчета под себя.

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


Требования и план разработки

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

Подумав над решением пришел к выводу, что понадобиться класс, в котором инициализируются переменные, которые будут доступны всем функциям. Графически вижу это так:

Графическое изображение плана разработки класса
Графическое изображение плана разработки класса

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

Функция должна иметь возможность быть вызвана независимо от других, следовательно, чтобы удовлетворить это требование результатом работы (return) должен быть DateFrame

Графическое изображение алгоритма работы функции для анализа
Графическое изображение алгоритма работы функции для анализа

Функция вывода всех результатов анализа, решил построить следующим образом

Графическое изображение алгоритма работы функции для сбора всего анализа
Графическое изображение алгоритма работы функции для сбора всего анализа

Разработка

Инициализируем класс

На вход подаем Dataframe

import pandas as pd

class DataAnalysisColumns():

    def __init__(self, df: pd.DataFrame):
        self.df = df
        self.columns = df.columns
        self.TotalRows = df.shape[0]
        self.BoxResult = []

        self.СolumnsDict = {
                  'AnalysisParams':'',
                  'NameColumns' : '',
                  'Value': ''}

Делаем доступными для функций:

Наименование атрибута

Описание

self.df

Dataframe

self.columns

Список с названием столбцов

self.TotalRows

Общие количество строк. Понадобиться для части функций.

self.BoxResult

Пустой список для сохранения результата анализа столбцов.

self.СolumnsDict

Словарь с названиями столбцов итогового отчета.

Общая функция

CreateDf принимает список с результатом анализа и создает DataFrame. Очищает список, в котором хранились результаты с целью использования в следующих функциях.

  def CreateDf(self,BoxResult:list):
      
      OutputDf = pd.DataFrame(data=BoxResult, columns=list(self.СolumnsDict.keys()))
      self.BoxResult.clear()
          
      return OutputDf

Функции анализа

Напишем несколько простых функций с анализом. Проанализируем имена столбцов на предмет наличия пробелов. Ранее при использовании query в pandas пробелы в названиях столбцов попортили нервы.

Второй анализ будет связан с наличием пустых строк в столбце. Выведем абсолютное и относительное число.

Каждая функция завершается созданием отдельного dataframe

 def AnalysisOfColumnNames(self):
      for NameColumn in self.columns:
          if " " in NameColumn:
              ListAttributes = ['Сolumn name', NameColumn, NameColumn]
              self.BoxResult.append(ListAttributes)
      
      return self.CreateDf(self.BoxResult)

    
  def AnalysisOfNull(self):

      for NameColumn in self.columns:
          CountNull = self.df[NameColumn].isnull().sum()

          if CountNull > 0:
              ListAttributes = ['Null Count', 
                             NameColumn , 
                             f'{CountNull} of {self.TotalRows} or {"{0:.0%}".format(CountNull/self.TotalRows)}']
          
              self.BoxResult.append(ListAttributes)
      
      return self.CreateDf(self.BoxResult)

Третья функция немного сложнее. Анализируем состав типа данных внутри столбца. Результаты выводим так же в абсолютных и относительных значениях.

В строках 18-20 пытаюсь выдернуть из <class 'str'>, тип данных c кавычки 'str'. Переменные FirsttNumOfSymbol, LatsNumOfSymbol. Наверное, его можно заменить регулярным выражением. Пытался сделать не получилось. Был бы рад, если в комментариях подскажите это сократит код.

 def AnalysisOfType(self): 
        # анализируем каждый столбец
        for NameColumn in self.columns:
            TypeColumn = str(self.df[NameColumn].dtype)

            # для типа 'object' пробегаемся по всему столбцу
            # формируем сводную по типу данных и считаем кол-ву строк
            if TypeColumn == 'object':
                self.df['TypeData'] = self.df[NameColumn].apply(lambda x: str(type(x)))

                PivotTable = self.df.groupby('TypeData').agg({'TypeData': ['count']}).reset_index()
                PivotTable.columns = ['TypeData', 'count']
                ColumnType = PivotTable['TypeData']
                BoxResult = []

                # собираем все в один результат(строку)
                for i in range(len(PivotTable)):
                    FirsttNumOfSymbol = ColumnType[i].find("'")
                    LatsNumOfSymbol = ColumnType[i].rfind("'") + 1
                    NameTypeRow = ColumnType[i][FirsttNumOfSymbol:LatsNumOfSymbol]

                    Percent = "{0:.0%}".format(PivotTable['count'][i]/self.TotalRows)

                    StringForAppend = f"{NameTypeRow}:{PivotTable['count'][i]}({Percent})"

                    BoxResult.append(StringForAppend)

                ListAttributes = ['Type', 
                                NameColumn, 
                                ",".join(BoxResult)]
                
                self.BoxResult.append(ListAttributes)

        return self.CreateDf(self.BoxResult)

Функции для анализа выбросов и анализ уникальности я оставлю в коде на GitHub.

Сбор всего анализа

Наши функции выдают отдельные DataFrame, соберем их в отдельный список. При помощи concat объединим в один.

В качестве демонстрации работы с итоговой таблицей, изменил порядок столбцов и добавил сортировку.

def AnalysisOfDf(self):
        ListOfAnalysis = [self.AnalysisOfColumnNames(),
                          self.AnalysisOfNull(),
                          self.AnalysisOfType(),
                          self.AnalysisOfOutlier(),
                          self.AnalysisOfUniqueText(),
                         ]
           
        OutputDf = pd.concat(ListOfAnalysis).reset_index(drop=True)

        OutputDf = OutputDf[['NameColumns', 'AnalysisParams', 'Value']]

        return OutputDf.sort_values(by=['NameColumns']).reset_index(drop=True)

Результат

Тестируем на реальных данных. Вызовем весь анализ. Размер таблицы (7043, 24)

Вывод функции AnalysisOfDf
Вывод функции AnalysisOfDf

Посмотрим, как работает отдельно функция по анализу типов данных

Вывод функции AnalysisOfType
Вывод функции AnalysisOfType

Заключение

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

Повторюсь, если у вас есть предложения, что можно улучшить, добавить, скорректировать прошу в комментарии.

Код и данные на GitHub

Комментарии (5)


  1. igor_suhorukov
    16.04.2023 07:07
    +1

    Было бы интересно узнать почему предпочитаете Pandas а не Polars. Во многих случаях можно было бы и не профилировать


    1. DmitriyB_33 Автор
      16.04.2023 07:07
      +1

      Спасибо за наводку. Ознакомлюсь с Polars.


  1. folal
    16.04.2023 07:07

    Вот у вас фрейм Результат, первая строка вывода, два уникальных значения - yes,no. И что?
    А если категорий пятьдесят, вы их все будете выводить?
    Вторая строка, str 100%, и что?
    Иначе говоря, не видно смысла анализа.
    Намного сильнее, думаю, когда анализ указывает разработчику на варианты дальнейших действий. Если ваш результат анализа отметит некий признак как мусор - вероятно, надо удалять. Если пропущенных значений запредельно много, или признак статический, или сработал increasing/decreasing - мусор, видимо. Если пропусков допустимо - то отметить для замены. Если признак временнОй или строка хитрого формата - отметить для дальнейшего парсинга. Богатый разветвленный анализ, исполненный в таком ключе, я бы с удовольствием присмотрел к своей работе.


    1. DmitriyB_33 Автор
      16.04.2023 07:07
      +1

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

      Вопрос: первая строка вывода, два уникальных значения - yes,no. И что? А если категорий пятьдесят, вы их все будете выводить?

      Ответ: в данном случае это мне говорит, о содержание значений в столбце и не каких дополнительных действий для нормализации данных в общем, то и не требуется. Там могло быть например ['Yes' ,''yes', 'NO', 'Nope'] и тогда возможно я бы причесал значения. В алгоритме больше 5 уникальных выводиться не будет. Например для столбца MothlyCharges 1585 уникальных без их перечисления.

      Вопрос: Вторая строка, str 100%, и что?

      Ответ: это мне говорит, что в столбце нет сборной солянки и отдельно мне проверять не нужно, что не так. А например в столбце TotalChange состав следующий 'float':6708(95%),'int':324(5%),'str':11(0%). В этом столбце нужно разбираться. Добавлю, что алгоритм анализирует только тип object, остальные не смотрит. Так в object именно может быть сборная солянка.

      Комментарий: Намного сильнее, думаю, когда анализ указывает разработчику на варианты дальнейших действий.

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


      1. folal
        16.04.2023 07:07

        Ответ: это мне говорит, что в столбце нет сборной солянки...

        Я вот об этом. - если нет, то и выводить не надо, в ваших результатах 90% вывода не требует вашего внимания.