Вводная
В рамках расширения своих компетенций периодически провожу анализ данных датасетов. В какой-то момент осознал, что трачу время на столбцы с аналитиками, в которых все в порядке. Данные полные, тип данных единый, интерпретация понятна. Если столбцов несколько десятков, то обзорная проверка атрибутов каждого столбца занимает довольно значительное время.
Посмотрел в сторону библиотеки 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)
Посмотрим, как работает отдельно функция по анализу типов данных
Заключение
Данное решение является для меня самым первым этапом анализа. Потенциальные проблемы или их отсутствие видны в таблицы. После этого можно начинать следующие шаги и сфокусироваться на требующих детальной разборки столбцах.
Повторюсь, если у вас есть предложения, что можно улучшить, добавить, скорректировать прошу в комментарии.
Код и данные на GitHub