Я часто пользуюсь функциями для работы с большими данными. Они позволяют упросить и ускорить работу. Некоторые я нашел на просторах интернета, другие написал сам. Сегодня хочу поделиться четырьмя из них, может кому-то будет полезно.
Быстрое чтение больших файлов
Один из первых инструментов, с которым сталкивается аналитик либо Data Scientist — это Pandas, библиотека Python для обработки и анализа данных. С её помощью мы импортируем и сортируем данные, делаем выборки и находим зависимости. Например, чтобы прочитать файл средствами Pandas в Python мы пишем:
import pandas as pd
data = pd.read_csv('data_file.csv')
Такой подход простой и понятный. Каждый Data Scientist или аналитик знает это. Но если данных много? Скажем 100 000 000 строк, они постоянно меняются, сроки горят, и до обеда надо проверить еще 100 гипотез?
Возьмем исследовательский набор данных о диабете с сайта Kaggle и продублируем каждую строку 100 000 для создания нашего тестового набора данных. В результате получается 76 800 000 строк.
Изначальный датасет выглядит так:
Pregnancies | Glucose | BloodPressure | SkinThickness | Insulin | BMI | DiabetesPedigreeFunction | Age | Outcome | |
---|---|---|---|---|---|---|---|---|---|
0 | 6 | 148 | 72 | 35 | 0 | 33.6 | 0.627 | 50 | 1 |
1 | 1 | 85 | 66 | 29 | 0 | 26.6 | 0.351 | 31 | 0 |
2 | 8 | 183 | 64 | 0 | 0 | 23.3 | 0.672 | 32 | 1 |
3 | 1 | 89 | 66 | 23 | 94 | 28.1 | 0.167 | 21 | 0 |
4 | 0 | 137 | 40 | 35 | 168 | 43.1 | 2.288 | 33 | 1 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
763 | 10 | 101 | 76 | 48 | 180 | 32.9 | 0.171 | 63 | 0 |
764 | 2 | 122 | 70 | 27 | 0 | 36.8 | 0.340 | 27 | 0 |
765 | 5 | 121 | 72 | 23 | 112 | 26.2 | 0.245 | 30 | 0 |
766 | 1 | 126 | 60 | 0 | 0 | 30.1 | 0.349 | 47 | 1 |
767 | 1 | 93 | 70 | 31 | 0 | 30.4 | 0.315 | 23 | 0 |
Преобразовываем его для наших экспериментальных задач:
# Чтение датасета из файла
df = pd.read_csv('diabetes.csv')
# Создание тестового файла
df = df.loc[df.index.repeat(100000)]
# Сохранение в файл для экспирементов
df.to_csv('benchmark.csv')
print(len(df), 'строк')
76 800 000 строк
Время выполнения
4 мин 26.6 сек
Преобразование было долгим, но будем надеется, что время потрачено не зря. Сколько же займет чтение такого файла?
df = pd.read_csv('benchmark.csv')
Время выполнения:
7 мин 57.4 сек
Если каждый раз загружать изменившийся датасет заново, на наши 100 гипотез уйдет 13 часов. Сделать до обеда не получится.
Мы хотим загружать данные быстрее, но при этом не терять всех преимуществ, которые даёт Pandas. Простая функция использующая datatable поможет нам сделать это:
import datatable as dt
import pandas as pd
def read_fast_csv(f):
frame = dt.fread(f)
ds = frame.to_pandas()
return ds
Попробуем теперь прочитать наш большой датасет:
ds = read_fast_csv('benchmark.csv')
Время выполнения
1 мин 21.5 сек
Это же в 6 раз быстрее! Значит до обеда успеваем!
Быстрое знакомство с датасетом
Данная функция выводит несколько параметров датасета с целью ознакомления с его содержимым, в частности:
Размер датасета
Информацию о дубликатах
Процент отсутствующих значений
Количество уникальных значений
Первые пять строчек датасета
def brief_df (df):
# Подсчитываем пустые значения и уникальные значение
rows_na =df.isna().sum().reset_index().rename(columns={0: "valuesNa"})
rows_notna = df.notna().sum().reset_index().rename(columns={0: "valuesNotNa"})
rows_analysis = pd.merge(rows_na, rows_notna, on="index", how= "outer")
rows_analysis["completeRatio"] = round((rows_analysis["valuesNotNa"]) / (rows_analysis["valuesNotNa"]+rows_analysis["valuesNa"])*100,2)
cardinality = df.nunique().reset_index().rename(columns={0: "cardinality"})
rows_analysis = pd.merge(rows_analysis, cardinality)
# Размер датасета и кол-во дубликатов
print("Размер:", df.shape)
dup_raw = df.duplicated ().sum()
dup_per = round((dup_raw*100)/df.shape[0],2)
print ("Дубликаты:", dup_raw, "->", dup_per, "%")
# Статистика по пустым значениям
print("Проверка на отсутсвующие значениия")
display(rows_analysis)
# Первые пять строк
print("Первые пять строк")
display(df.head())
Попробуем проанализировать наш большой датасет, который мы создали в первом разделе:
brief_df(df)
Размер: (76800000, 10)
Дубликаты: 76799232 -> 100.0 %
Обзор пустых значений:
index | valuesNa | valuesNotNa | completeRatio | cardinality | |
---|---|---|---|---|---|
0 | C0 | 0 | 76800000 | 100.0 | 768 |
1 | Pregnancies | 0 | 76800000 | 100.0 | 17 |
2 | Glucose | 0 | 76800000 | 100.0 | 136 |
3 | BloodPressure | 0 | 76800000 | 100.0 | 47 |
4 | SkinThickness | 0 | 76800000 | 100.0 | 51 |
5 | Insulin | 0 | 76800000 | 100.0 | 186 |
6 | BMI | 0 | 76800000 | 100.0 | 248 |
7 | DiabetesPedigreeFunction | 0 | 76800000 | 100.0 | 517 |
8 | Age | 0 | 76800000 | 100.0 | 52 |
9 | Outcome | 0 | 76800000 | 100.0 | 2 |
Первые пять строк
C0 | Pregnancies | Glucose | BloodPressure | SkinThickness | Insulin | BMI | DiabetesPedigreeFunction | Age | Outcome | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 6 | 148 | 72 | 35 | 0 | 33.6 | 0.627 | 50 | True |
1 | 0 | 6 | 148 | 72 | 35 | 0 | 33.6 | 0.627 | 50 | True |
2 | 0 | 6 | 148 | 72 | 35 | 0 | 33.6 | 0.627 | 50 | True |
3 | 0 | 6 | 148 | 72 | 35 | 0 | 33.6 | 0.627 | 50 | True |
4 | 0 | 6 | 148 | 72 | 35 | 0 | 33.6 | 0.627 | 50 | True |
Да, дубликатов у нас и правда 100%, ведь мы продублировали каждую строку 100 000 раз. Так что все сходится.
Столбцы с датами в разных форматах
Для анализа и визуализации данных часто требуется работать с датами в разных форматах, когда дата дана только одна. А еще хуже, если все даты прописаны как попало. Приходится приводить все даты к стандартному виду, а потом программным путем добывать из каждой даты разную информацию. Я написал функцию, которая сильно упрощает этот процесс.
Сначала смоделируем нашу ситуацию, и к нашему исходному датасету добавим колонку со случайными датами в диапазоне двух последних лет:
import numpy as np
# Чтение датасета из файла
df = pd.read_csv('diabetes.csv')
df['date'] = np.random.choice(pd.date_range('2020-01-01', '2022-12-31'), 768)
df.to_csv('diabetes_dates.csv')
Ниже код функции, которая к существующему датасету добавляет колонки с наиболее частыми требуемыми форматами, чтобы сделать выборки по годам, по кварталам, месяцам или неделям, либо взять полную дату для целей визуализации.
import datetime
from datetime import timedelta
def granular_dates(df, col):
df['ts_date'] = pd.to_datetime(df[col]).dt.normalize()
# Полная дата с названием месяца
df['ts_date_str'] = df["ts_date"].dt.strftime("%d %B %Y")
# Краткая дата с сокращением месяца
df['ts_date_str_short'] = df["ts_date"].dt.strftime("%d %b %Y")
# Только год
df['ts_year'] = df.ts_date.dt.year
# Только номер месяца
df['ts_month'] = df.ts_date.dt.month
# Только число
df['ts_day'] = df.ts_date.dt.day
# Год и квартал
df['ts_quarter'] = pd.PeriodIndex(df.ts_date, freq="Q").astype(str)
# Номер недели
df['ts_dayweek'] = df.ts_date.dt.dayofweek
# День недели
df['ts_dayweektext'] = df.ts_date.dt.strftime("%a")
# Дата конца недели (воскресенья)
df['ts_week_start'] = df.ts_date.apply(lambda x: x - timedelta(days=x.weekday())).dt.strftime("%b-%d")
# Дата конца недели (воскресенья)
df['ts_week_end'] = df.ts_date.apply(lambda x: x - timedelta(days=x.weekday()) + timedelta(days=6)).dt.strftime("%b-%d")
Теперь всего одна строчка кода (не считая чтение файла):
# Чтение датасета
df = pd.read_csv('diabetes_dates.csv')
# Добавление колонок с датами разного формата
granular_dates(df, 'date')
В результате к датасету добавляются такие колонки:
ts_date | ts_date_str | ts_date_str_short | ts_year | ts_month | ts_day | ts_quarter | ts_dayweek | ts_dayweektext | ts_week_start | ts_week_end |
2022-06-13 | 13 June 2022 | 13 Jun 2022 | 2022 | 6 | 13 | 2022Q2 | 0 | Mon | Jun-13 | Jun-19 |
2022-03-10 | 10 March 2022 | 10 Mar 2022 | 2022 | 3 | 10 | 2022Q1 | 3 | Thu | Mar-07 | Mar-13 |
2022-05-23 | 23 May 2022 | 23 May 2022 | 2022 | 5 | 23 | 2022Q2 | 0 | Mon | May-23 | May-29 |
2020-11-22 | 22 November 2020 | 22 Nov 2020 | 2020 | 11 | 22 | 2020Q4 | 6 | Sun | Nov-16 | Nov-22 |
2021-10-27 | 27 October 2021 | 27 Oct 2021 | 2021 | 10 | 27 | 2021Q4 | 2 | Wed | Oct-25 | Oct-31 |
Экспорт в Google таблицы
Итак, мы теперь можем быстро прочитать огромные датасеты, ознакомиться с их ключевыми параметрами и сделать выборки по всевозможным комбинациям дат. Теперь пора предоставить результаты анализа другим членам команды. Иногда это удобнее сделать через облачный сервис. Часто приходится использовать таблицы Google.
В Python есть замечательный пакет Gspread, который поможет вам экспортировать ваши выборки прямо в таблицу Google, откуда ей можно будет поделиться с другими для совместной работы из любой точки мира.
После того, как вы установили пакет и получили от Google ключ API для своего аккаунта, используйте функцию ниже, чтобы создать таблицу из ваших данных:
def writedf (df, gc, wsheetname, colstart, rowstart, colend, boolheader):
wsheet=gc.worksheet(wsheetname)
en_update = str(df.shape[0]+1)
range_update = colstart + rowstart + ':' + colend
cols_list = df.columns.to_list()
header = pd.DataFrame(cols_list).T
header.columns = cols_list
if boolheader == True:
df = pd.concat([header, df])
data = df.values.tolist()
wsheet.update(range_update, data)
Одно из преимуществ использования этой функции заключается в том, что данные записываются одним запросом, вместо записи каждой ячейки отдельно, что сэкономит количество обращений к API электронных таблиц Google, а значит меньше шанс нарваться на ограничения.
import gspread
# Утсанавливаем соединение
gc = gspread.service_account()
# Заполняем таблицу в Google
writedf (df, gc, 'mySheet', 0, 0, 100, True)
Подобным образом можно так же подружить наш датасет с Notion — современной замене Evernote, но это был бы материал для целой отдельной статьи.
Было бы интересно узнать, какие наработки функций Python есть у вас, чем приходится часто пользоваться?