Я часто пользуюсь функциями для работы с большими данными. Они позволяют упросить и ускорить работу. Некоторые я нашел на просторах интернета, другие написал сам. Сегодня хочу поделиться четырьмя из них, может кому-то будет полезно.

Быстрое чтение больших файлов

Один из первых инструментов, с которым сталкивается аналитик либо 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 есть у вас, чем приходится часто пользоваться?

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


  1. vectorplus
    18.08.2022 00:06
    +1

    Apache Arrow


    1. VZ1 Автор
      18.08.2022 09:31

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


  1. TrahatelGovna
    18.08.2022 09:10
    +1

    Отличный пост! Спасибо большое!